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:

  1. 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_rate is only ever assigned in pick_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.

  2. 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 with ST0 = 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 past last_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=floppy0

The 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.

Edited by cquante1