@samitouri / QOSamiQemu / commits / 431f59a34d

hw/block/fdc: report a missing address mark on an empty drive

READ ID on a drive with no medium terminates normally and returns the made-up sector ID left over from the "Pretend we are spinning" emulation. The only error path is a data rate mismatch, and media_rate is assigned solely by pick_geometry(); it is never reset when the medium is removed. A guest that has just ejected a diskette is therefore told that one is still present. READ, WRITE and FORMAT have a related problem: fd_seek() answers 2 both for "track/head out of range" and for "no medium", so the callers report ST0 = ABNTERM with ST1 = 0x00 either way. Without ST1.MA the guest cannot tell an absent diskette from a transient error. Give fd_seek() a return code of its own for an absent medium, and let both switch statements report the missing address mark for it. The comments on the two switches were swapped: fd_seek() answers 2 for a bad track or head and 3 for a sector past last_sect, but case 2 read "sect too big" and case 3 "track too big". Both now say what they mean. This is a behaviour change for FORMAT TRACK on an empty drive as well, which now answers ST1.MA rather than ST1 = 0x00. None of the guests tested reaches that path -- DOS gives up during media sensing and never issues the command -- but it seemed wrong to leave fdctrl_format_sector() falling through to "default" for a case fd_seek() now reports explicitly. Failing READ ID does not make guests detect the removal: real hardware never completes the command on an empty drive, because there are no index pulses, and OS/2 for one relies on that timeout. It does stop the controller from claiming a diskette that is not there. tests/qtest/fdc-test.c starts QEMU with "-device floppy,id=floppy0" and no medium, and test_read_id asserts a normal termination with a made-up cylinder 8 / head 1. That contradicts its neighbours test_no_media_on_start and test_media_change, which state that DSKCHG signals an absent medium. Insert a medium before READ ID and eject it afterwards -- the rewritten test passes before and after this change -- and add test_read_id_no_media for the empty drive. Guests checked, reading and writing, with and without a medium: Linux 2.0.34 and 7.0, PC-DOS 7, IBM DOS 5.02, Windows for Workgroups 3.11 and OS/2 2.11. None changes behaviour. No version of the Linux floppy driver from 1.2.13 to master issues READ ID at all -- FD_READID is defined in the uapi header for FDRAWCMD users and the driver never sends it -- so Linux detects an empty drive by stepping the head and reading DSKCHG instead. Buglink: https://gitlab.com/qemu-project/qemu/-/issues/3971 Signed-off-by: Christian Quante <christian@quante.one> Message-ID: <20260714164031.60551-3-christian@quante.one> [kwolf: Added fd_seek() comment for new return value 5] Reviewed-by: Kevin Wolf <kwolf@redhat.com> Signed-off-by: Kevin Wolf <kwolf@redhat.com>

Christian Quante committed Jul 14, 2026 at 18:40 UTC 431f59a34db5234919262b558880a2d57b64a738
2 files changed +106 -14
hw/block/fdc.c
+43 -7
@@ -196,6 +196,12 @@ static void fd_init(FDrive *drv)
196
197 #define NUM_SIDES(drv) ((drv)->flags & FDISK_DBL_SIDES ? 2 : 1)
198
199 +/* Is a diskette present in the drive? */
200 +static bool fd_media_present(FDrive *drv)
201 +{
202 + return drv->blk != NULL && blk_is_inserted(drv->blk);
203 +}
204 +
205 static int fd_sector_calc(uint8_t head, uint8_t track, uint8_t sect,
206 uint8_t last_sect, uint8_t num_sides)
207 {
@@ -222,6 +228,7 @@ static int fd_offset(FDrive *drv)
228 * returns 2 if track is invalid
229 * returns 3 if sector is invalid
230 * returns 4 if seek is disabled
231 + * returns 5 if no floppy is inserted
232 */
233 static int fd_seek(FDrive *drv, uint8_t head, uint8_t track, uint8_t sect,
234 int enable_seek)
@@ -258,7 +265,7 @@ static int fd_seek(FDrive *drv, uint8_t head, uint8_t track, uint8_t sect,
265 #endif
266 drv->head = head;
267 if (drv->track != track) {
261 - if (drv->blk != NULL && blk_is_inserted(drv->blk)) {
268 + if (fd_media_present(drv)) {
269 drv->media_changed = 0;
270 }
271 ret = 1;
@@ -267,8 +274,8 @@ static int fd_seek(FDrive *drv, uint8_t head, uint8_t track, uint8_t sect,
274 drv->sect = sect;
275 }
276
270 - if (drv->blk == NULL || !blk_is_inserted(drv->blk)) {
271 - ret = 2;
277 + if (!fd_media_present(drv)) {
278 + ret = 5;
279 }
280
281 return ret;
@@ -1476,14 +1483,24 @@ static void fdctrl_start_transfer(FDCtrl *fdctrl, int direction)
1483 NUM_SIDES(cur_drv)));
1484 switch (fd_seek(cur_drv, kh, kt, ks, fdctrl->config & FD_CONFIG_EIS)) {
1485 case 2:
1479 - /* sect too big */
1486 + /* track/head out of range */
1487 fdctrl_stop_transfer(fdctrl, FD_SR0_ABNTERM, 0x00, 0x00);
1488 fdctrl->fifo[3] = kt;
1489 fdctrl->fifo[4] = kh;
1490 fdctrl->fifo[5] = ks;
1491 return;
1492 + case 5:
1493 + /*
1494 + * No medium: there is no address mark to be found. Guests that tell
1495 + * an absent diskette from an unreadable one rely on ST1.MA.
1496 + */
1497 + fdctrl_stop_transfer(fdctrl, FD_SR0_ABNTERM, FD_SR1_MA, 0x00);
1498 + fdctrl->fifo[3] = kt;
1499 + fdctrl->fifo[4] = kh;
1500 + fdctrl->fifo[5] = ks;
1501 + return;
1502 case 3:
1486 - /* track too big */
1503 + /* sector too big */
1504 fdctrl_stop_transfer(fdctrl, FD_SR0_ABNTERM, FD_SR1_EC, 0x00);
1505 fdctrl->fifo[3] = kt;
1506 fdctrl->fifo[4] = kh;
@@ -1791,14 +1808,21 @@ static void fdctrl_format_sector(FDCtrl *fdctrl)
1808 NUM_SIDES(cur_drv)));
1809 switch (fd_seek(cur_drv, kh, kt, ks, fdctrl->config & FD_CONFIG_EIS)) {
1810 case 2:
1794 - /* sect too big */
1811 + /* track/head out of range */
1812 fdctrl_stop_transfer(fdctrl, FD_SR0_ABNTERM, 0x00, 0x00);
1813 fdctrl->fifo[3] = kt;
1814 fdctrl->fifo[4] = kh;
1815 fdctrl->fifo[5] = ks;
1816 return;
1817 + case 5:
1818 + /* no medium */
1819 + fdctrl_stop_transfer(fdctrl, FD_SR0_ABNTERM, FD_SR1_MA, 0x00);
1820 + fdctrl->fifo[3] = kt;
1821 + fdctrl->fifo[4] = kh;
1822 + fdctrl->fifo[5] = ks;
1823 + return;
1824 case 3:
1801 - /* track too big */
1825 + /* sector too big */
1826 fdctrl_stop_transfer(fdctrl, FD_SR0_ABNTERM, FD_SR1_EC, 0x00);
1827 fdctrl->fifo[3] = kt;
1828 fdctrl->fifo[4] = kh;
@@ -2306,6 +2330,18 @@ static void fdctrl_result_timer(void *opaque)
2330 FDCtrl *fdctrl = opaque;
2331 FDrive *cur_drv = get_cur_drv(fdctrl);
2332
2333 + /*
2334 + * An empty drive has no address marks to read. Completing READ ID
2335 + * successfully, with the made-up sector ID left over from the "spinning"
2336 + * emulation below, tells the guest that a diskette is still present after
2337 + * it has been ejected. The only error path left was a data rate mismatch,
2338 + * and media_rate is never reset when the medium is removed.
2339 + */
2340 + if (!fd_media_present(cur_drv)) {
2341 + FLOPPY_DPRINTF("read id on empty drive\n");
2342 + fdctrl_stop_transfer(fdctrl, FD_SR0_ABNTERM, FD_SR1_MA, 0x00);
2343 + return;
2344 + }
2345 /* Pretend we are spinning.
2346 * This is needed for Coherent, which uses READ ID to check for
2347 * sector interleaving.
tests/qtest/fdc-test.c
+63 -7
@@ -64,6 +64,12 @@ enum {
64
65 DSKCHG = 0x80,
66 };
67 +enum {
68 + ST0_IC_MASK = 0xc0, /* interrupt code */
69 + ST0_IC_ABNTERM = 0x40, /* abnormal termination */
70 +
71 + ST1_MA = 0x01, /* missing address mark */
72 +};
73
74 static char *test_image;
75
@@ -270,6 +276,21 @@ static void test_cmos(void)
276 g_assert(cmos == 0x40 || cmos == 0x50);
277 }
278
279 +static void media_insert(void)
280 +{
281 + qtest_qmp_assert_success(global_qtest,
282 + "{'execute':'blockdev-change-medium', 'arguments':{"
283 + " 'id':'floppy0', 'filename': %s, 'format': 'raw' }}",
284 + test_image);
285 +}
286 +
287 +static void media_eject(void)
288 +{
289 + qtest_qmp_assert_success(global_qtest,
290 + "{'execute':'eject', 'arguments':{"
291 + " 'id':'floppy0' }}");
292 +}
293 +
294 static void test_no_media_on_start(void)
295 {
296 uint8_t dir;
@@ -301,10 +322,7 @@ static void test_media_insert(void)
322
323 /* Insert media in drive. DSKCHK should not be reset until a step pulse
324 * is sent. */
304 - qtest_qmp_assert_success(global_qtest,
305 - "{'execute':'blockdev-change-medium', 'arguments':{"
306 - " 'id':'floppy0', 'filename': %s, 'format': 'raw' }}",
307 - test_image);
325 + media_insert();
326
327 dir = inb(FLOPPY_BASE + reg_dir);
328 assert_bit_set(dir, DSKCHG);
@@ -333,9 +351,7 @@ static void test_media_change(void)
351
352 /* Eject the floppy and check that DSKCHG is set. Reading it out doesn't
353 * reset the bit. */
336 - qtest_qmp_assert_success(global_qtest,
337 - "{'execute':'eject', 'arguments':{"
338 - " 'id':'floppy0' }}");
354 + media_eject();
355
356 dir = inb(FLOPPY_BASE + reg_dir);
357 assert_bit_set(dir, DSKCHG);
@@ -414,6 +430,9 @@ static void test_read_id(void)
430 uint8_t st0;
431 uint8_t msr;
432
433 + /* READ ID reads an address mark, so it needs a medium in the drive. */
434 + media_insert();
435 +
436 /* Seek to track 0 and check with READ ID */
437 send_seek(0);
438
@@ -491,6 +510,42 @@ static void test_read_id(void)
510 g_assert_cmpint(cyl, ==, 8);
511 g_assert_cmpint(head, ==, 1);
512 g_assert_cmpint(st0, ==, head << 2);
513 +
514 + /* Leave the drive empty, the way the machine starts up. */
515 + media_eject();
516 +}
517 +
518 +/*
519 + * An empty drive spins no diskette, so READ ID finds no address mark and must
520 + * terminate abnormally. Reporting success (with a made-up sector ID) would
521 + * tell the guest that a medium is still present after it has been ejected.
522 + */
523 +static void test_read_id_no_media(void)
524 +{
525 + uint8_t drive = 0;
526 + uint8_t head = 0;
527 + uint8_t st0, st1;
528 +
529 + floppy_send(CMD_READ_ID);
530 + g_assert(!get_irq(FLOPPY_IRQ));
531 + floppy_send(head << 2 | drive);
532 +
533 + while (!get_irq(FLOPPY_IRQ)) {
534 + clock_step(1000000000LL / 50);
535 + }
536 +
537 + st0 = floppy_recv();
538 + st1 = floppy_recv();
539 + floppy_recv(); /* ST2 */
540 + floppy_recv(); /* cylinder */
541 + floppy_recv(); /* head */
542 + floppy_recv(); /* sector */
543 + g_assert(get_irq(FLOPPY_IRQ));
544 + floppy_recv(); /* sector size */
545 + g_assert(!get_irq(FLOPPY_IRQ));
546 +
547 + g_assert_cmpint(st0 & ST0_IC_MASK, ==, ST0_IC_ABNTERM);
548 + g_assert_cmpint(st1 & ST1_MA, ==, ST1_MA);
549 }
550
551 static void test_read_no_dma_1(void)
@@ -625,6 +680,7 @@ int main(int argc, char **argv)
680 qtest_add_func("/fdc/sense_interrupt", test_sense_interrupt);
681 qtest_add_func("/fdc/relative_seek", test_relative_seek);
682 qtest_add_func("/fdc/read_id", test_read_id);
683 + qtest_add_func("/fdc/read_id_no_media", test_read_id_no_media);
684 qtest_add_func("/fdc/verify", test_verify);
685 qtest_add_func("/fdc/media_insert", test_media_insert);
686 qtest_add_func("/fdc/read_no_dma_1", test_read_no_dma_1);