hw/block/fdc: READ ID succeeds on an empty drive, so guests cannot detect diskette removal
hw/block/fdc: READ ID succeeds on an empty drive, so guests cannot detect diskette removal
Description of problem
On a floppy drive with no medium, READ ID terminates normally (ST0 = 0x00) and
returns a fabricated sector ID. A guest therefore cannot distinguish an empty
drive from one holding a diskette, and never notices that the medium has been
ejected.
Two independent defects are involved:
-
fdctrl_result_timer()(READ ID) never checks whether a medium is present. The only error path is a data rate mismatch:/* READ_ID can't automatically succeed! */ if ((fdctrl->dsr & FD_DSR_DRATEMASK) != cur_drv->media_rate) { fdctrl_stop_transfer(fdctrl, FD_SR0_ABNTERM, FD_SR1_MA, 0x00); } else { fdctrl_stop_transfer(fdctrl, 0x00, 0x00, 0x00); /* success */ }and
media_rateis only ever assigned inpick_geometry(); it is not reset when the medium is removed. So after an eject the rates still match and READ ID reports success. The sector ID it returns comes from the "Pretend we are spinning" hack right above, i.e. it is made up. -
fdctrl_start_transfer()reports a missing medium without ST1.MA.fd_seek()returns 2 both for "track/head out of range" and for "no medium", and case 2 answers withST0 = ABNTERM, ST1 = 0x00. A guest cannot tell the two apart, and in particular gets no missing address mark indication.(While reading that switch: the comments are swapped.
fd_seek()returns 2 for a bad track or head and 3 for a sector pastlast_sect, but case 2 is labelled "sect too big" and case 3 "track too big". Worth a separate trivial patch; not touched here.)
Steps to reproduce
Start a machine with an empty floppy drive and issue READ ID:
qemu-system-x86_64 -machine pc -device floppy,id=floppy0The qtest /x86_64/fdc/read_id_no_media added by the patch does exactly this.
Against current master it fails:
ERROR:tests/qtest/fdc-test.c: test_read_id_no_media:
assertion failed (st0 & ST0_IC_MASK == ST0_IC_ABNTERM): (0 == 64)Additional information
Impact on guests. OS/2 2.x asks the user to remove the diskette and press
ENTER; it then probes the drive and never accepts the removal, so the
installation cannot continue. Under SeaBIOS, DOS gets INT 13h AH = 0x20
(controller failure) for a read from an empty A:, where SeaBIOS's own
DISK_RET_EMEDIA (0xC0) is the accurate answer. DOS maps both to the same
"General failure reading drive A", so the corrected code changes nothing a DOS
user sees; PC-DOS 7 and IBM DOS 5.02 were checked, reading and writing, with and
without a medium.
The test suite currently pins the buggy behaviour. main() starts QEMU with
-device floppy,id=floppy0 and no medium, so the drive is empty for the whole
run, yet test_read_id asserts a normal termination and reads a fabricated
cylinder 8 / head 1:
g_assert_cmpint(cyl, ==, 8);
g_assert_cmpint(head, ==, 1);
g_assert_cmpint(st0, ==, head << 2);This contradicts its immediate neighbours test_no_media_on_start ("Media
changed bit must be set all time after start if there is no media in drive") and
test_media_change, which correctly describe DSKCHG as the signal for an absent
medium. The patch inserts a medium before READ ID and ejects it afterwards; that
change is behaviour-neutral (the rewritten test_read_id passes both before and
after the fix).
Measurements (host Linux, -machine pc, empty 1.44M drive):
| before | after | |
|---|---|---|
qtest /fdc/read_id_no_media |
fails, ST0 = 0x00 | passes, ST0 = 0x40, ST1 = 0x01 |
qtest fdc suite (16 tests) |
— | all pass |
DOS, INT 13h AH=02h on empty A: |
0.11 s, AH = 0x20 ECONTROLLER | 0.21 s, AH = 0xC0 EMEDIA |
PC-DOS 7, DIR A: on empty drive |
"General failure reading drive A" | identical, pixel for pixel |
IBM DOS 5.02, DIR A: on empty drive |
"General failure reading drive A" | identical, pixel for pixel |
PC-DOS 7, DIR A: with a FAT12 diskette |
lists files | identical |
PC-DOS 7, COPY COMMAND.COM A: (write path) |
55386 bytes copied | identical |
| WfW 3.11, File Manager on empty A: | "disk not formatted" dialog | identical, pixel for pixel |
| WfW 3.11, File Manager with a FAT12 diskette | lists files | identical |
| OS/2 2.11 installer, eject + ENTER | re-prompts | identical screen |
| OS/2 2.11 installer, correct diskette inserted | proceeds | identical |
Linux 7.0, open("/dev/fd0"), empty |
0.064 s, ENXIO | 0.064 s, ENXIO |
| Linux 7.0, medium inserted | reads 512 bytes | reads 512 bytes |
Linux 2.0.34 (Slackware 3.5 bare.i), boot from floppy |
OK | OK, identical screen |
| Linux 2.0.34, medium ejected, root-floppy prompt | re-prompts | re-prompts, identical screen |
boot from a real diskette + INT 13h read |
0.21 s, OK | 0.21 s, OK |
No Linux version issues READ ID: FD_READID (0xEA) is defined in
include/uapi/linux/fdreg.h for FDRAWCMD users, but drivers/block/floppy.c
never sends it, from 1.2.13 to current master. interpret_errors() gives
ST1_MAM no branch of its own for an abnormal termination, so a READ on an
empty drive is retried exactly as before; only the message changes from "unknown
error. ST[0..2] are: 0x40 0x0 0x0" to "probe failed...". Only 2.0.34 and 7.0
were booted, but interpret_errors(), setup_rw_floppy() and rw_interrupt()
are byte-for-byte identical between v6.1 and master, so the intermediate
releases behave the same.
The guests that do use READ ID misread the current answer rather than the new one. OS/2 2.11's own floppy driver -- distinguishable in a trace from SeaBIOS's INT 13h path by its READ opcode 0x46 against SeaBIOS's 0xE6, and by its varying SPECIFY parameters (0xA1/0xDF/0xEF 0x02) against SeaBIOS's constant 0xAF 0x02 -- recalibrates and issues READ ID across the 500/300/250 kbps data rates. Today it is told that READ ID succeeded, so it follows up with READ commands that then fail; with this change it stops at READ ID and issues none (10 READ commands before, 0 after, at the same step of the installation). The screen it shows is unchanged either way: it still cannot tell an absent diskette from an unreadable one. Windows for Workgroups 3.11 displays "the disk in drive A is not formatted" for an empty drive both before and after the fix. Neither guest regresses, and neither starts detecting the removal -- that needs the missing completion described below.
Tested against a real 2.0.34 kernel because commit fd9bdbd3 ("fdc: fix
detection under Linux") warns that FDC changes tend to expose kernel bugs. On an
empty drive that kernel issues no data transfer at all; its whole command stream
after an eject is PERPENDICULAR MODE, CONFIGURE, SPECIFY, RECALIBRATE, SENSE
INTERRUPT, SEEK to cylinder 1, SENSE INTERRUPT — and then it reads DIR, finds
DSKCHG still set, and concludes that no medium is present. That commit's concern
was fd_seek() bounds on empty drives, which this patch does not touch.
What this does not fix. Real hardware produces no index pulses on an empty
drive, so READ ID never completes at all; guests detect the absent medium by
their own timeout. OS/2 relies on exactly that: it accepts the removal only when
the command does not complete. Its driver waits 2.5 to 5.3 s per attempt
(measured across four attempts in one trace), and it rejects every immediate
answer tried (ABNTERM|MA, ABNTERM|NR|MA, RDYCHG with and without MA).
Emulating the missing completion does fix OS/2, but SeaBIOS then waits out its
FLOPPY_IRQ_TIMEOUT (5 s) twice during floppy_media_sense(), because it never
reads DIR: PORT_FD_DIR (0x3f7) is only ever written, as CCR. Measured 10.1 s
for a single INT 13h read of an empty A:.
QEMU's DSKCHG emulation is already correct here — fd_init() sets
media_changed = 1 and fd_seek() clears it only for an inserted medium, so a
SEEK to another cylinder leaves DSKCHG set exactly when the drive is empty
(verified from a guest: 0x80 empty vs 0x00 with a diskette). A BIOS-side
DSKCHG check would therefore avoid the timeout entirely. That is a SeaBIOS
change and deliberately out of scope for this report.
Linux has done exactly that since at least 2.0: recalibrate, seek to cylinder 1 to produce a step pulse, then read DIR. It never needs READ ID to find out that a drive is empty.
fdctrl_format_sector() conflates the same two fd_seek() error cases; it is
left untouched, since ST1.MA is questionable for FORMAT (it writes the address
marks rather than searching for them).
Patch
Sent to qemu-devel, CC'ing the addresses reported by scripts/get_maintainer.pl:
Message-Id: <20260710113134.43012-1-christian@quante.one>
The message is still awaiting moderation (I am not subscribed to the list); it will appear at https://lore.kernel.org/qemu-devel/20260710113134.43012-1-christian@quante.one/
Single commit (code and test together, so bisect stays green), applies cleanly
to master (4ee536fac7), checkpatch.pl clean.