Commit 0b7cc42531 for qemu.org
commit 0b7cc42531eafa02742c783bd895035f3c867400
Author: Denis V. Lunev <den@openvz.org>
Date: Tue Aug 18 09:55:48 2026 +0200
tests/qtest/ahci: regression test for a request outliving an unplug
Add /ahci/io/{ncq,dma,pio}/unplug: arm a read against a null-co backend
whose latency keeps it in flight, then eject the controller through the
ACPI ejection register. Each of the three reaches the freed AHCIDevice
array by a different route, so covering one command class would leave
the other two untested.
That register is what a guest writes to finish a PCI unplug, and unlike
the pciehp attention button it reaches ahci_uninit() with no secondary
bus reset, so nothing cancels the request on the way. It also dictates
the machine: q35 has no ACPI hotplug on pcie.0, so the eject has no
effect there.
The latency is what holds the request; a blkdebug breakpoint cannot
stand in for it, because cancelling a suspended request waits for it and
the unplug would never return.
Unfixed, all three fail reliably under AddressSanitizer. On a plain build
the use-after-free only faults when the freed page has been returned, so
expect the odd pass there.
Cc: John Snow <jsnow@redhat.com>
Cc: Philippe Mathieu-Daudé <philmd@oss.qualcomm.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
diff --git a/tests/qtest/ahci-test.c b/tests/qtest/ahci-test.c
index 84d4e6b0a5..b143862ce7 100644
--- a/tests/qtest/ahci-test.c
+++ b/tests/qtest/ahci-test.c
@@ -1793,6 +1793,78 @@ static void test_atapi_engine_restart_dma(void)
test_atapi_engine_restart_in_flight(true);
}
+/*
+ * Regression test: an unplug runs no device reset, so it is the last chance to
+ * detach an outstanding request. Its completion would otherwise walk the
+ * AHCIDevice array that ahci_uninit() has freed, which each of the NCQ, DMA
+ * and PIO completions reaches by a different route.
+ *
+ * The ACPI ejection register is what a guest writes to finish a PCI unplug.
+ * -M pc is what puts it in reach: q35 has no ACPI hotplug on pcie.0, so the
+ * unplug never happens there. Unlike the pciehp attention button this reaches
+ * the unplug with no secondary bus reset, which is the ordering that leaves a
+ * request outstanding.
+ */
+static void test_unplug_in_flight(uint8_t ide_cmd)
+{
+ AHCIQState *ahci;
+ AHCICommand *cmd;
+ uint64_t ptr;
+ uint8_t port;
+ QTestState *qts;
+
+ /*
+ * The latency keeps the backend read in flight across the unplug. A
+ * blkdebug breakpoint cannot stand in for it: cancelling a suspended
+ * request waits for it, so the unplug would never return.
+ */
+ ahci = ahci_boot_and_enable(
+ "-M pc "
+ "-blockdev driver=null-co,node-name=drive0,read-zeroes=on,"
+ "latency-ns=100000000 "
+ "-device ich9-ahci,addr=1f.2,id=ahci0 "
+ "-device ide-hd,drive=drive0,bus=ahci0.0 ");
+ qts = ahci->parent->qts;
+ port = ahci_port_select(ahci);
+ ahci_port_clear(ahci, port);
+
+ ptr = ahci_alloc(ahci, AHCI_SECTOR_SIZE);
+ g_assert(ptr);
+
+ cmd = ahci_command_create(ide_cmd);
+ ahci_command_adjust(cmd, 0, ptr, AHCI_SECTOR_SIZE, 0);
+ ahci_command_commit(ahci, cmd, port);
+ ahci_command_issue_async(ahci, cmd);
+
+ /* Eject slot 0x1f of the root bus, which frees the AHCIDevice array. */
+ qtest_outl(qts, 0xae10, 0);
+ qtest_outl(qts, 0xae08, 1u << 0x1f);
+ qtest_qmp_eventwait(qts, "DEVICE_DELETED");
+
+ /* Four times the backend latency, so the completion has surely run. */
+ g_usleep(400 * 1000);
+ qtest_qmp_assert_success(qts, "{ 'execute': 'query-status' }");
+
+ ahci_command_free(cmd);
+ ahci_free(ahci, ptr);
+ ahci_shutdown(ahci);
+}
+
+static void test_unplug_ncq(void)
+{
+ test_unplug_in_flight(READ_FPDMA_QUEUED);
+}
+
+static void test_unplug_dma(void)
+{
+ test_unplug_in_flight(CMD_READ_DMA);
+}
+
+static void test_unplug_pio(void)
+{
+ test_unplug_in_flight(CMD_READ_PIO);
+}
+
/*
* Regression test: a PIO write outlives the command list it was issued from.
* ide_cancel_dma_sync() does not reach s->pio_aiocb, so the second DRQ phase
@@ -2351,6 +2423,9 @@ int main(int argc, char **argv)
test_atapi_engine_restart_pio);
qtest_add_func("/ahci/cdrom/engine_restart/dma",
test_atapi_engine_restart_dma);
+ qtest_add_func("/ahci/io/ncq/unplug", test_unplug_ncq);
+ qtest_add_func("/ahci/io/dma/unplug", test_unplug_dma);
+ qtest_add_func("/ahci/io/pio/unplug", test_unplug_pio);
qtest_add_func("/ahci/io/pio/engine_stop",
test_write_engine_stop_in_flight);
qtest_add_func("/ahci/cdrom/drain/pio", test_atapi_drain_pio);