Skip to content

Bound what a G64 tells the drive about its tracks (#823) - #840

Merged
chrisgleissner merged 7 commits into
GideonZ:masterfrom
enver-haase:fix/g64-track-bounds
Oct 5, 2026
Merged

chrisgleissner merged 7 commits into
GideonZ:masterfrom
enver-haase:fix/g64-track-bounds

Conversation

@enver-haase

@enver-haase enver-haase commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Status: ready for review. Both review findings are addressed (b1e60409, ad56e8e8), the host tests run red/green, and the device run on an Ultimate II+L is below.

Fixes #823. A G64 or G71 tells the drive engine where each track is and how long it is, and the firmware used to believe it. insert_disk() programs the drive engine's parameter RAM from the track table, and floppy_mem.vhd reads and writes memory bounded by what it finds there, so every unchecked value in that table reached the engine.

What was wrong

  • The length was never validated. w & 0x3FFF admitted 16383 while GCRIMAGE_MAXTRACKLEN is 7928, and nothing compared the length with what the file delivered. offset > GCRIMAGE_MAXSIZE admitted offset == GCRIMAGE_MAXSIZE, so the length word was read past the buffer; and a track declared zero bytes long reached rotation_speed / tr->track_length.
  • A track only had to fit the allocation, not the file (review finding 1). A 14-byte image whose track 0 declares 0x1E0C sent gcr_data+14 through gcr_data+7705 to the drive: bytes no read wrote, holding whatever the previous mount left, as the buffer is zeroed at allocation and not between images.
  • An MFM-marked track shorter than its own metadata was mapped anyway (review finding 2). 0x8001, one byte marked MFM, made map_gcr_image_to_mfm() read the 162-byte metadata area out of one byte, past the allocation at the end of the buffer, and 1 - 162 went into the uint32_t reservedSpace that gates every track write. MFM_TRACK_HEADER_SIZE was unparenthesised, so x - MFM_TRACK_HEADER_SIZE expanded to x - 2 + 160: every mapped track reserved 320 bytes more than it had, which is why the underflow never showed.
  • The length was read from fourteen of its fifteen bits. Bit 15 is the MFM marker, the length is the other fifteen; save() and write_track() write it that way, and Denise reads it that way. Masked with 0x3FFF, a declared 0x4123 (16675, above the maximum) became 291 and was programmed as a plausible short track.
  • The parameter pair was built from locals nobody wrote. bit_time and track_len were assigned only for a present track and read for an absent one, so half-tracks were handed dummy_track with the neighbour's length, and mount_g64() ignored what load() returned.

What this does

software/drive/gcr_track_bounds.h holds the checks as free functions over plain integers, so they run on a build host:

  • gcr_track_header_is_readable(): the two-byte length word lies inside what the file delivered.
  • gcr_validated_track_length(): the length from fifteen bits, or 0 when the track cannot be used, for zero, above the format maximum, or offset + 2 + length > bytes_read, written as subtractions so nothing wraps.
  • gcr_mfm_reserved_space() and the MFM header check: a track shorter than MFM_TRACK_HEADER_SIZE gets no sectors and no reserved space, so UpdateTrack() admits no write for it. A static_assert fails if the macro's brackets go missing again.
  • gcr_track_parameters(): an absent track gets the dummy address with the short, safe length init() and remove_disk() already use.

A refused track stays as invalidate() left it, add_blank_tracks() gives it a blank one, and the mount goes on. A file load() refuses as a whole leaves the drive empty.

Tested

Host tests, red/green. make -C software/drive/tests, 31 cases, in CI: 31 passed on de633b34. Each guard reverted on its own turns cases red (measured on 4c79cd1a; gcr_track_bounds.h and its tests have not changed since): the truncated-file bound 5 (4 of them fail, the fifth is the boundary that must pass either way), the MFM header check 3, the bare subtraction into reservedSpace 3, the fourteen-bit mask 2, the header-readable check 2. The zero-length check has no case of its own that can fail: a zero length is refused by the other bounds too.

On a device, an Ultimate II+L in a C64, master 7117d885 against a local build of this branch on the same master (e2b9f49f, 2 October; the branch's last firmware change, be8452c5, is from 25 September):

master this PR
E2E suite g71-mfm-bounds: a mixed GCR/MFM G71, one with a track declaring 0x8001, one declaring 0x4123; track 18 sector 1 reads back, the device answers 9/9 OK 9/9 OK
control G64: directory loads, U1 reads every sector tried OK OK
wrong signature GCR-XXXX drive empty, 21, READ ERROR the same
track 1 declaring 0x3FFF with all 16383 bytes in the file only track 1 reads 21, the rest of the disk reads the same
track 35 declared in full, file cut 1000 bytes into it track 35 reads 21, the rest reads the same
menu and REST API answer afterwards yes yes

The device shows no difference: the over-read these bounds stop goes past the buffer, but the drive gets no usable GCR out of it on either firmware, so the drive answers the same. What the device run shows is that green stays green: the malformed tracks are refused, the rest of each disk reads, and nothing hangs. The red/green proof is the host tests.

The suite failed on both firmwares with dos=$31 until de633b34: its block reader sent the DOS commands in lower case, which 64tass passes through as ASCII, and initialised the drive after opening the buffer channel, which frees it.

Not run on a U64 or a C64 Ultimate. The changed code is the drive emulation every device shares; a run there would show the same, and I can add one if wanted.

Built

make u64ii in ghcr.io/gideonz/riscv on de633b34, every output/ and result/ under target/ removed first: exit 0, no warning from disk_image.cc, c1541.cc or gcr_track_bounds.h, app_space PASS (38.6 % free on U64E2_50T, 34.2 % on U64E2_100T), update.ue2 and update.cfw produced.

@chrisgleissner chrisgleissner added 3.16 Targeted for release 3.16. bug Existing behavior is incorrect, broken, or regressed; includes supported documentation defects. labels Sep 9, 2026
@chrisgleissner
chrisgleissner changed the base branch from test-merge to master September 13, 2026 12:42

@chrisgleissner chrisgleissner left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: the PR confirms only that each two-byte track header was read, not that the declared track data was read. A 14-byte GCR-1541 file with track 0 offset 12 and declared length 0x1E0C passes the new header check and capacity check, then sends gcr_data+14 through gcr_data+7705 to the drive even though those bytes were never read and can be stale after a prior mount. Please require offset + 2 + length <= bytes_read without overflow, and add a red/green truncated-file regression. I am resolving the workflow-only merge conflict separately, retaining both the current HTTP integer test and this G64 test.

GcrImage::load() took each track's length straight from the image, masked it
to 14 bits and stored it. That mask admits 16383, while GCRIMAGE_MAXTRACKLEN
is 7928, and nothing compared the length against the space left in the buffer
behind the track's own offset. insert_disk() then programs that length into the
drive engine's parameter RAM, and floppy_mem.vhd both reads and writes memory
bounded by it, so a crafted image could point the hardware about 8 KB past the
end of the allocation.

Three smaller holes on the same path: the offset test admitted offset ==
GCRIMAGE_MAXSIZE, so tr[0] and tr[1] were read one and two bytes past the
buffer; the offset was never compared against how much the file actually
delivered, so a length could be taken from a part of the buffer no read had
filled; and a track declared zero bytes long reached insert_disk()'s division
by track_length.

A track whose metadata does not survive these checks is now left as
invalidate() left it -- absent -- so add_blank_tracks() gives it a blank track
and the mount goes on. add_blank_tracks() in the same file already bound-checks
this way, so the missing check in load() was an omission rather than a choice.

The checks live in a new header of free functions over plain integers rather
than in load() itself, because none of the drive headers can be compiled on a
build host and this is the part worth testing there.

Reported in GideonZ#823, problems 2 and 4.
insert_disk() declared bit_time and track_len without initialisers, assigned
them only inside the branch taken for a track that is present, and read them in
the branch taken for one that is not. Two consequences.

The first is reachable with nothing but a malformed file. GcrImage::load()
returns false on a bad signature after invalidate() has zeroed the whole track
table, but mount_g64() ignored what it returned and inserted the image anyway.
Mounting any .g64 whose first eight bytes are not GCR-1541 or GCR-1571
therefore programmed all 84 parameter slots with dummy_track -- 7692 bytes, no
slack -- paired with whatever those two stack slots happened to hold. A stale
length above that points the drive engine, which reads and writes there, past
the end of the heap block.

The second is on every image. add_blank_tracks() fills only even indices, so
every odd one takes the absent branch and was handed dummy_track together with
the preceding track's length. That is safe today by exactly zero margin: the
largest entry in track_lengths[] equals GCRIMAGE_DUMMYTRACKLEN.

Both loops now build their parameter pair through one function, which gives an
absent track the dummy address with the same short, safe length init() and
remove_disk() already program, and which cannot see a neighbour at all. A zero
or negative length is treated as absent there too, so the division has no way
to reach a zero divisor even if a length slips past the checks at load time.

mount_g64() now acts on load()'s answer: the file is closed and the drive is
left empty, which is the state remove_disk() has already put it in.

Reported in GideonZ#823, problems 1 and 3.
enver-haase added a commit to enver-haase/1541ultimate that referenced this pull request Sep 16, 2026
The first version of this fix compared a declared track length against
GCRIMAGE_MAXSIZE, so a track only had to fit the allocation. It did not have to
be in the file. A 14 byte GCR-1541 image whose track 0 points at offset 12 and
declares 0x1E0C passed both the header check and the length check, and then
gcr_data+14 through gcr_data+7705 were programmed into the drive engine --
bytes no read ever wrote, holding whatever the previous mount left behind,
since the buffer is zeroed at allocation and not between images.

gcr_validated_track_length() now takes bytes_read instead of the capacity and
requires offset + 2 + length <= bytes_read, written as subtractions so nothing
can wrap, and load() passes what f->read() reported. Since bytes_read never
exceeds GCRIMAGE_MAXSIZE, this is strictly the stronger bound and the buffer
cases the earlier tests pin down still hold.

Five cases added under TruncatedImage, including the reported 14 byte file.
Against this commit the suite is 23 green; against a header restored to the
capacity bound, 4 of the 5 fail -- the fifth is the positive boundary, a track
ending exactly where the file does, which must stay accepted either way.

Reported by @chrisgleissner in review of GideonZ#840.
The first version of this fix compared a declared track length against
GCRIMAGE_MAXSIZE, so a track only had to fit the allocation. It did not have to
be in the file. A 14 byte GCR-1541 image whose track 0 points at offset 12 and
declares 0x1E0C passed both the header check and the length check, and then
gcr_data+14 through gcr_data+7705 were programmed into the drive engine --
bytes no read ever wrote, holding whatever the previous mount left behind,
since the buffer is zeroed at allocation and not between images.

gcr_validated_track_length() now takes bytes_read instead of the capacity and
requires offset + 2 + length <= bytes_read, written as subtractions so nothing
can wrap, and load() passes what f->read() reported. Since bytes_read never
exceeds GCRIMAGE_MAXSIZE, this is strictly the stronger bound and the buffer
cases the earlier tests pin down still hold.

Five cases added under TruncatedImage, including the reported 14 byte file.
Against this commit the suite is 23 green; against a header restored to the
capacity bound, 4 of the 5 fail -- the fifth is the positive boundary, a track
ending exactly where the file does, which must stay accepted either way.

Reported by @chrisgleissner in review of GideonZ#840.
@enver-haase

Copy link
Copy Markdown
Contributor Author

You are right, and the hole is exactly where you put it: the check was against the
size of the buffer, so a track had to fit the allocation but not the file. Fixed in
b1e60409.

gcr_validated_track_length() now takes bytes_read in place of the capacity and
requires offset + 2 + length <= bytes_read, written as subtractions so no sum can
wrap, and load() passes what f->read() reported. Since bytes_read never exceeds
GCRIMAGE_MAXSIZE, this is strictly the stronger bound — the buffer cases the earlier
tests pin down still hold, unchanged.

Red and green, both measured:

against this commit                        23 passed
against the header restored to the old bound  4 failed

The four are the truncated cases, your 14-byte file among them, each returning 7692
where it has to return 0. The fifth new case is the positive boundary — a track ending
exactly where the file does — which must stay accepted either way, and does.

CI is green on the same commit, and it runs make -C software/drive/tests in the
container, so those cases ran there too and not only here.

Leaving the workflow-only conflict to you, as you proposed. Ready for another look.

@chrisgleissner
chrisgleissner self-requested a review September 16, 2026 22:59

@chrisgleissner chrisgleissner left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new range check is necessary, but one downstream size assumption remains unchecked. I left one blocking inline finding: a short accepted track can still make the MFM mapping path read beyond gcr_data or underflow its writable-space bound.

Comment thread software/drive/disk_image.cc
Length and MFM marker share one word in a G64, so 0x8001 -- one byte of track,
marked MFM -- is a legal thing to put in an image, and the bound added in the
previous commit passes it: the one declared byte really was delivered.
map_gcr_image_to_mfm() then believed the marker and read the whole 162 byte
metadata area out of that one byte. Placed at the end of the buffer, as in the
reported case -- a file of exactly GCRIMAGE_MAXSIZE with track 0 pointing at
GCRIMAGE_MAXSIZE - 3 and a sector count of 32 in the final byte -- the sector
loop runs past the allocation. The space behind the area was taken as a
subtraction into reservedSpace, a uint32_t that gates every track write, where
1 - 162 cannot come out negative.

MFM_TRACK_HEADER_SIZE was also unparenthesised, so x - MFM_TRACK_HEADER_SIZE
expanded to x - 2 + 160. Every mapped track reserved 320 bytes more than it
had, and the underflow above never happened because the subtraction was not
one. The macro's three other uses -- an addition, a comparison, and - 2 --
come out right either way, which is why this stayed hidden. The static_assert
beside it fails if the brackets are lost again; comparing the macro against
162 would not, since 2 + 160 equals 162. disk_image.h now includes mfmdisk.h,
which defines the WD_MAX_SECTORS_PER_TRACK the macro has always used.

A track shorter than the metadata area is left as init() zeroed it: no
sectors, no reserved space, so UpdateTrack() admits no write for it.

Six cases added under MfmHeader, including the reported end of buffer file.
Against this commit the suite is 29 green; with gcr_mfm_reserved_space()
restored to a bare subtraction the short track reserves 4294967135 bytes, and
with the header check restored to believing the marker the reported case is
mapped.

Reported by @chrisgleissner in review of GideonZ#840.
@enver-haase

Copy link
Copy Markdown
Contributor Author

Confirmed, and closed in ad56e8e8. The finding was right; the mechanism sits one layer lower than either of us thought.

The out-of-bounds read. map_gcr_image_to_mfm() now refuses any track shorter than the metadata area, whatever its marker claims. Such a track is left exactly as init() zeroed it — no sectors, no reserved space — so nothing downstream reads a header that was never delivered. Your end-of-buffer file is pinned down directly in the tests.

The underflow could not have happened, which is the more interesting half. MFM_TRACK_HEADER_SIZE was defined without outer parentheses:

#define MFM_TRACK_HEADER_SIZE   2 + (WD_MAX_SECTORS_PER_TRACK * 5)

so the one place that subtracts it, gtr->track_length - MFM_TRACK_HEADER_SIZE, expanded to gtr->track_length - 2 + 160. That is track_length + 158, not track_length - 162. 1 - 162 never wrapped because the subtraction was not one — instead every mapped track reserved 320 bytes more than it had, and UpdateTrack() admitted track writes that much too large. The macro's three other uses — an addition, a comparison, and a - 2 — come out right either way, which is why this sat unnoticed.

Parenthesising alone would have created the underflow you predicted; the guard alone would have left the 320 bytes. Both are in.

The static_assert next to the macro is spelled 0 - MFM_TRACK_HEADER_SIZE == -162 deliberately: comparing the macro against 162 passes in either spelling, since 2 + 160 is 162, so it would not catch the brackets being lost again. disk_image.h now also includes mfmdisk.h, which defines the WD_MAX_SECTORS_PER_TRACK the macro had always used without including its source.

All 29 tests pass on this commit. To show that is not vacuous, I put each guard back the way it was, one at a time, and reran:

ad56e8e8, unmodified                        29 test(s) passed
reserved space back to a bare subtraction    3 test(s) failed
header check back to believing the marker    4 test(s) failed

In the first revert the one-byte track reserves 4294967135 bytes instead of 0; in the second, the reported file is mapped again. Both reverts were thrown away afterwards; they exist only to prove the new cases bite.

CI is green on this commit and ran make -C software/drive/tests in the container, so the six new cases ran there as well. The firmware builds too: make -C target/u64ii/riscv/ultimate in ghcr.io/gideonz/riscv on a native x86_64 host, exit 0, no errors and no warnings.

Adjacent finding, reported separately as #921. While measuring this I noticed that the WRITE TRACK handler discards UpdateTrack()'s rejection and then writes at a stale offset. Nothing in this PR touches wd177x.cc, so it is not a regression from it — but correcting the reservation here makes that rejection path genuinely reachable, which is why it seemed worth having on record now rather than later.

Scope, so this is not mistaken for an endorsement. Whether a G64 should be carrying MFM tracks at all is a separate question, now raised as #920: bit 15 of the length word is not part of the G64 specification, and VICE rejects such a track outright rather than loading it. This PR takes the mechanism exactly as it stands and only makes it safe to walk over.

For the record, my own view is that it should absolutely not stay. A file whose signature reads GCR-1541 has no business carrying MFM content, and the gap it fills deserves a container of its own rather than a bit taken out of someone else's mandatory field. That argument belongs in #920 though, not in this review — nothing here depends on how it is settled.

Ready for another look.

The length word carries the MFM marker in bit 15, so the length is what the
other fifteen bits say. load() masked with 0x3FFF and took fourteen, and
nothing anywhere gives bit 14 a meaning: it is not a flag in this firmware,
not in Denise, which reads the same marker and masks with 0x7fff, and not in
the published G64 format, where the field is a plain size.

The writers never masked at all. save() at disk_image.cc:847 and write_track()
at :924 both build the word from reported_length >> 8 and then set bit 15 on
top, so an image could not be read back as it was written. A declared 0x4123
came out as 0x0123: 291 bytes, small enough to pass every check in
gcr_validated_track_length() and be programmed as a track.

That is the part worth stating plainly, because the mask defeats the function
it sits in. Read as a length, 0x4123 is 16675, above GCRIMAGE_MAXTRACKLEN, and
the track is refused with the diagnostic this file already prints. Masked, it
became a plausible short track and was programmed instead -- "rejected here
rather than programmed" is what the comment above the function promises.

Where 0x3FFF came from is not a mystery: the drive engine's parameter field
is exactly fourteen bits wide, so masking the declared length to the same
width reads like the obvious thing to do. It holds everywhere except here.

Decoding the file and asking what the drive can do are separate steps. The
drive engine's ceiling is 16383: max_offset in floppy_param_mem.vhd is
fourteen bits wide (line 36), and floppy_mem.vhd wraps offset_count at it for
reads and writes alike. That ceiling never comes into play here, because the
bound this function already applies -- GCRIMAGE_MAXTRACKLEN, 7928 -- is less
than half of it. So the parse can stay faithful to the file and let the bound
do the rejecting, which is what it is for.

Both facts are now written into the comment above the function, so the next
reader does not have to rediscover where 0x3FFF came from.

    against this commit                          31 passed
    against the same header masking with 0x3FFF   2 failed

The two are the new cases, 0x4123 with and without the MFM marker, each
returning 291 where it has to return 0. No hardware is involved: these are
free functions over plain integers, and make -C software/drive/tests runs
them in about a second.
The checks in gcr_track_bounds.h are host tests over plain integers, which is
what makes them worth having, but nothing so far mounts an image that actually
trips them on a device. This suite does, and it is the first thing anywhere to
drive the firmware's mixed GCR/MFM path on real hardware.

Three G71 images, built in the test rather than committed, because what is
interesting about them is the two-byte length word in front of each track and
a binary fixture would hide it:

  mixed      35 GCR tracks and one track carrying the MFM payload this
             firmware writes -- sector count, version byte of zero, five bytes
             per sector, sector data from offset 162 on, exactly as
             mfm_update_callback() lays it out.
  shortmfm   the same disk with the MFM-marked track declaring 0x8001. The
             metadata area alone is 162 bytes, so there is nothing to read.
  overlong   the same disk with a track declaring 0x4123: 16675 read as a
             length, 291 read as fourteen bits.

Each case asserts the same two things. The device is still reachable, and
track 18 sector 1 still comes back. A malformed track on track 39 or 40 may
not cost the rest of the disk, and that is the property these bounds are for.

The block comes back through the drive's own U1 command rather than a job
queue, so the reader needs no ROM entry point and does not change when the
drive model does; an I before it makes the DOS re-read the BAM instead of
refusing a block whose header carries an ID it does not expect yet.

The GCR encoder is 30 lines and is checked by its own output: the sentinel
written into track 18 sector 1 decodes back byte for byte through the inverse
table, and the longest run of zero bits in the encoded stream is two, which is
the defining property of the code. Track lengths land inside each speed zone's
nominal size with room to spare -- 6897 of 7142 on track 18.

    lint_test        OK (ruff 0.16.5 over tests, run-tests)
    registry_test    OK (71 suites)
    runner_policy    OK (99 checks)
    64tass           g71_block_reader.asm assembles, 259 bytes at $0801

Not run on a device yet: the suite is registered and its fixtures are
verified, but the device half needs hardware on the network. Whoever runs it
first should expect the two malformed cases to be the interesting ones.
@enver-haase

Copy link
Copy Markdown
Contributor Author

Two commits on top, fast-forward, no force-push — your anchors survive.

Bit 14 was a lost length bit, not a flag: 0x3FFF turned a declared 0x4123 into a 291-byte track that passed every check, where fifteen bits make it 16675 and GCRIMAGE_MAXTRACKLEN refuses it. The mask matched the engine's fourteen-bit max_offset, but the file and the engine are two different things. 31 passed against the fix, 2 failed against the old mask.

New e2e suite g71-mfm-bounds mounts three G71s — a well-formed mixed GCR/MFM disk, one whose MFM track declares 0x8001, one declaring 0x4123 — and checks that the device stays reachable and track 18 sector 1 still reads back through U1.

It has not run on a device yet and the commit says so outright; my C64U is working again, so that plus the two manual checks #823 asks for are next, and I will report them here.

Your second review is answered by ad56e8e8. The code side is complete and CI is green on 4c79cd1a, but I would not call this finished until the device run is in — look now or wait for that, whichever suits you.

@chrisgleissner

chrisgleissner commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

@chrisgleissner Please comment below each of my findings how they were addressed. This will simplify review. Is this merge ready from your perspective and have you performed exhaustive red green tests of it against a U64 and c64? Could you please provide test results?

I appreciate your PR description shows these, but I am not sure if they were repeated for the most recent code version, and what their results were. Also, your original PR description still mentions your device is bricked.

It would be great if you could update and rewrite your original PR description to reflect the very latest state and latest tests you have performed.

Thank you.

@chrisgleissner chrisgleissner added the storage Disk/tape emulation, images, mounting, copying, filenames, filesystems, and storage media. label Oct 3, 2026
The block reader sent "i0" and "u1 2 0 <track> <sector>" in lower case.
64tass passes .text through as ASCII, $69 and $75, and the DOS knows its
commands only as the PETSCII $49 "I" and $55 "U": both came back as
31, SYNTAX ERROR, and the suite failed on master and on this branch alike
with dos=$31. In upper case the initialise then freed the buffer channel
opened just before it, and U1 answered 70, NO CHANNEL. The initialise now
comes before "#" is opened.

On an Ultimate II+L in a C64, master 7117d88: 9/9, the three G71s read
track 18 sector 1 back.
@enver-haase

Copy link
Copy Markdown
Contributor Author

Please require offset + 2 + length <= bytes_read without overflow, and add a red/green truncated-file regression.

Addressed in b1e60409. gcr_validated_track_length() now takes bytes_read instead of the capacity and requires offset + 2 + length <= bytes_read, written as subtractions so nothing can wrap; load() passes what f->read() reported. Since bytes_read never exceeds GCRIMAGE_MAXSIZE, it is strictly the stronger bound.

Five cases under TruncatedImage, including your 14-byte file (track 0 at offset 12, declaring 0x1E0C). With the header restored to the capacity bound, 4 of the 5 fail; the fifth is a track ending exactly where the file does, which has to pass either way.

@enver-haase

Copy link
Copy Markdown
Contributor Author

@chrisgleissner Both findings are answered above, and the description is rewritten for the current state, including the old line about a bricked device.

Merge-ready from my side, yes. Tested on the current head:

  • Host tests: 31/31 on de633b34, red/green per guard as listed in the description.
  • Device: an Ultimate II+L in a C64, master 7117d885 against this branch on the same master. The E2E suite g71-mfm-bounds passes 9/9 on both, and four more G64s (control, wrong signature, 0x3FFF, truncated) behave the same on both.
    • The device can't show red here: the over-read these bounds stop gives the drive no usable GCR on either firmware.
    • It shows that the malformed tracks are refused, the rest of each disk reads, and nothing hangs.
  • Not run on a U64 or C64 Ultimate. The code is the drive emulation they all share. I can add a C64 Ultimate run if you want one.

One thing the device run caught: the suite itself failed on both firmwares with dos=$31 until de633b34. Its block reader sent the DOS commands in lower case and initialised after opening the buffer channel.

@chrisgleissner
chrisgleissner merged commit 0f4084a into GideonZ:master Oct 5, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

3.16 Targeted for release 3.16. bug Existing behavior is incorrect, broken, or regressed; includes supported documentation defects. storage Disk/tape emulation, images, mounting, copying, filenames, filesystems, and storage media.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

G64 track metadata programs the drive's memory bounds unchecked

2 participants