@samitouri / QOSamiQemu / commits / 0b7cc42531

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>

Denis V. Lunev committed Aug 18, 2026 at 09:55 UTC 0b7cc42531eafa02742c783bd895035f3c867400
1 file changed +75
tests/qtest/ahci-test.c
+75
@@ -1793,6 +1793,78 @@ static void test_atapi_engine_restart_dma(void)
1793 test_atapi_engine_restart_in_flight(true);
1794 }
1795
1796 +/*
1797 + * Regression test: an unplug runs no device reset, so it is the last chance to
1798 + * detach an outstanding request. Its completion would otherwise walk the
1799 + * AHCIDevice array that ahci_uninit() has freed, which each of the NCQ, DMA
1800 + * and PIO completions reaches by a different route.
1801 + *
1802 + * The ACPI ejection register is what a guest writes to finish a PCI unplug.
1803 + * -M pc is what puts it in reach: q35 has no ACPI hotplug on pcie.0, so the
1804 + * unplug never happens there. Unlike the pciehp attention button this reaches
1805 + * the unplug with no secondary bus reset, which is the ordering that leaves a
1806 + * request outstanding.
1807 + */
1808 +static void test_unplug_in_flight(uint8_t ide_cmd)
1809 +{
1810 + AHCIQState *ahci;
1811 + AHCICommand *cmd;
1812 + uint64_t ptr;
1813 + uint8_t port;
1814 + QTestState *qts;
1815 +
1816 + /*
1817 + * The latency keeps the backend read in flight across the unplug. A
1818 + * blkdebug breakpoint cannot stand in for it: cancelling a suspended
1819 + * request waits for it, so the unplug would never return.
1820 + */
1821 + ahci = ahci_boot_and_enable(
1822 + "-M pc "
1823 + "-blockdev driver=null-co,node-name=drive0,read-zeroes=on,"
1824 + "latency-ns=100000000 "
1825 + "-device ich9-ahci,addr=1f.2,id=ahci0 "
1826 + "-device ide-hd,drive=drive0,bus=ahci0.0 ");
1827 + qts = ahci->parent->qts;
1828 + port = ahci_port_select(ahci);
1829 + ahci_port_clear(ahci, port);
1830 +
1831 + ptr = ahci_alloc(ahci, AHCI_SECTOR_SIZE);
1832 + g_assert(ptr);
1833 +
1834 + cmd = ahci_command_create(ide_cmd);
1835 + ahci_command_adjust(cmd, 0, ptr, AHCI_SECTOR_SIZE, 0);
1836 + ahci_command_commit(ahci, cmd, port);
1837 + ahci_command_issue_async(ahci, cmd);
1838 +
1839 + /* Eject slot 0x1f of the root bus, which frees the AHCIDevice array. */
1840 + qtest_outl(qts, 0xae10, 0);
1841 + qtest_outl(qts, 0xae08, 1u << 0x1f);
1842 + qtest_qmp_eventwait(qts, "DEVICE_DELETED");
1843 +
1844 + /* Four times the backend latency, so the completion has surely run. */
1845 + g_usleep(400 * 1000);
1846 + qtest_qmp_assert_success(qts, "{ 'execute': 'query-status' }");
1847 +
1848 + ahci_command_free(cmd);
1849 + ahci_free(ahci, ptr);
1850 + ahci_shutdown(ahci);
1851 +}
1852 +
1853 +static void test_unplug_ncq(void)
1854 +{
1855 + test_unplug_in_flight(READ_FPDMA_QUEUED);
1856 +}
1857 +
1858 +static void test_unplug_dma(void)
1859 +{
1860 + test_unplug_in_flight(CMD_READ_DMA);
1861 +}
1862 +
1863 +static void test_unplug_pio(void)
1864 +{
1865 + test_unplug_in_flight(CMD_READ_PIO);
1866 +}
1867 +
1868 /*
1869 * Regression test: a PIO write outlives the command list it was issued from.
1870 * 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)
2423 test_atapi_engine_restart_pio);
2424 qtest_add_func("/ahci/cdrom/engine_restart/dma",
2425 test_atapi_engine_restart_dma);
2426 + qtest_add_func("/ahci/io/ncq/unplug", test_unplug_ncq);
2427 + qtest_add_func("/ahci/io/dma/unplug", test_unplug_dma);
2428 + qtest_add_func("/ahci/io/pio/unplug", test_unplug_pio);
2429 qtest_add_func("/ahci/io/pio/engine_stop",
2430 test_write_engine_stop_in_flight);
2431 qtest_add_func("/ahci/cdrom/drain/pio", test_atapi_drain_pio);