* [PATCH RFC v4 0/5] PCI: pciehp: Report surprise removal during safe removal
@ 2026-09-27 18:20 Abhin Parekadan Jose
2026-09-27 18:20 ` [PATCH RFC v4 1/5] PCI: Report surprise removal event Abhin Parekadan Jose
` (4 more replies)
0 siblings, 5 replies; 7+ messages in thread
From: Abhin Parekadan Jose @ 2026-09-27 18:20 UTC (permalink / raw)
To: Bjorn Helgaas, Lukas Wunner, Michael S. Tsirkin
Cc: Ilpo Järvinen, Shuai Xue, Kees Cook, Mahesh J Salgaonkar,
Oliver O'Halloran, linux-pci, linuxppc-dev, linux-kernel,
Abhin Parekadan Jose
If a safe removal is already in progress when the device is surprise
removed, pciehp cannot report the disconnect [1]. The removal blocks
waiting on a device interrupt or status read, and pciehp's IRQ thread is
single-threaded and is itself executing that removal, so it never runs
again to report the device gone. The removal hangs indefinitely.
pciehp_isr() does run while the IRQ thread is blocked, but pciehp_ist()
must ignore link and presence changes caused by Secondary Bus Reset or
DPC, and telling those apart takes seconds which cannot be spent in
hardirq.
Patch 4 does not do that work in hardirq. pciehp_isr() only checks
whether the card is present or the link is active, and defers the rest
to a work item in process context. The work item does not need to know
why the link changed, only whether the card is gone: it waits for DPC
recovery or a Secondary Bus Reset in progress to complete, then checks
again under reset_lock, and if the card is absent and the link is down
marks the devices below disconnected, which schedules the driver's
disconnect work from patch 1. pciehp_ist() is unchanged and
remains the only consumer of the one-shot flags PCI_DPC_RECOVERED and
PCI_LINK_CHANGED, which tell it whether a link change can be ignored.
Patches:
1/5 Michael's "PCI: Report surprise removal event" from his RFC v5,
which adds disconnect_work_enable and pdev->disconnect_work.
Changed to serialize disconnect_work_enable with a per-device
spinlock.
2/5 Add pci_hp_wait_link_change(), which awaits a Secondary Bus Reset
in progress without consuming PCI_LINK_CHANGED.
3/5 Add pci_dpc_wait_recovery(), which awaits DPC recovery without
consuming PCI_DPC_RECOVERED.
4/5 The pciehp change. Adds disconnect_work to struct controller,
scheduled from pciehp_isr() on PDC or DLLSC when neither Presence
Detect State nor Data Link Layer Link Active indicates a card.
5/5 A POC driver for the QEMU edu device that blocks in remove()
waiting for an interrupt, standing in for del_gendisk() stuck in
blk_mq_freeze_queue_wait(). Not for merge -- included so the
hang can be reproduced.
Changes since RFC v3 [5]:
- 5/5: Reinitialize the completion in remove() before requesting the
delayed interrupt, so that an earlier completion cannot let remove()
return while the interrupt is still pending. (Sashiko)
- 1/5 to 4/5: No changes.
Changes since RFC v2 [4]:
- 1/5: Protect disconnect_work_enable with a per-device spinlock, held
while testing it and scheduling the work, instead of lockless
accesses and barriers. pci_dev_set_disconnected() could otherwise
test the flag, get preempted, and queue the work while a newly bound
driver re-initializes it. With the lock, cancel_work_sync() is
sufficient again, so go back to it from disable_work_sync().
(Sashiko)
- 2/5: Test PCI_LINK_CHANGING with test_bit_acquire(), so that the
caller's subsequent accesses are ordered after the end of the code
section even if wait_event() returns without sleeping. (Sashiko)
- 3/5, 4/5, 5/5: No changes.
Changes since RFC v1 [2]:
- 1/5: Use disable_work_sync() instead of cancel_work_sync() in
pci_clear_disconnect_work(), so that a racing schedule_work() cannot
queue the work after remove() has returned. (Sashiko)
- 2/5, 3/5: New.
- 4/5: Drop schedule_notification_work(); pci_dev_set_disconnected()
schedules the driver's disconnect work for all callers again.
(Michael)
- 4/5: Don't consume PCI_DPC_RECOVERED and PCI_LINK_CHANGED in the
work item. In v1 it ran the same spurious link change test as
pciehp_ist(), and whichever ran first took the flags, so pciehp_ist()
could tear down a device that was only reset. (Sashiko)
- 4/5: Check presence with pciehp_card_present_or_link_active() in
both pciehp_isr() and the work item, as pciehp_ist() does, so that a
port with Presence Detect State hardwired to zero is not mistaken
for an empty slot.
- 4/5: Return early from the work item if pciehp_ist() has already
taken the pending events, and treat a read error of the presence
check as "not present". (Sashiko)
- 4/5: In pciehp_isr(), check presence before dropping the runtime PM
reference on the port's parent. In the work item, take a runtime PM
reference and check presence under reset_lock, since a slot reset
may make Presence Detect State and Link Active flap.
- 5/5: Build only with CONFIG_EDU_SRPOC, which depends on PCI; don't
claim the interrupt when the device reads all ones; clear bus
mastering on teardown. (Sashiko)
Testing
Reproducing this needs QEMU changes, since neither device_del nor the
attention button produces a true surprise removal. A branch with both
is here [3]:
- a delayed-IRQ register on the edu device (BAR0 0x30, write N ms)
- a pcie_surprise_del monitor command that drops the device and
generates PDC=1, DLLSC=1, PDS=0
Both tests use:
./qemu-system-aarch64 -machine virt,gic-version=3 -cpu cortex-a57 \
-m 512 -smp 2 -kernel Image -initrd initramfs.cpio.gz \
-device pcie-root-port,id=rp1,chassis=1,slot=1 \
-device edu,bus=rp1,id=edu0 -append "console=ttyAMA0 rdinit=/init" \
-nographic -monitor unix:/tmp/qemu-mon.sock,server,nowait
and need a guest kernel with CONFIG_EDU_SRPOC=y.
Test 1 (Hang in remove() on a user thread, then surprise removal):
guest# echo 1 > /sys/bus/pci/devices/0000:01:00.0/remove &
host$ echo "pcie_surprise_del edu0" | socat - unix-connect:/tmp/qemu-mon.sock
This is the case patch 1 solves on its own: pciehp's IRQ thread is
free and handles the removal.
Test 2 (Hang in remove() on pciehp's IRQ thread, then surprise removal):
guest# echo 0 > /sys/bus/pci/slots/1/power &
host$ echo "pcie_surprise_del edu0" | socat - unix-connect:/tmp/qemu-mon.sock
Without patch 4 the safe removal does not return until the delayed
interrupt fires 600 s later. With it,
pciehp_isr() schedules ctrl->disconnect_work, which marks the device
disconnected; the POC driver's disconnect work completes the wait and
remove() proceeds.
Open questions
- Is this a viable approach?
[1] https://lore.kernel.org/all/aHlZE18kPuHuDtTT@wunner.de/
[2] https://lore.kernel.org/all/20260905183905.997833-1-abhinjoses@gmail.com/
[3] https://gitlab.com/abhinkop/qemu/-/commits/suprise-removal
[4] https://lore.kernel.org/all/20260927165459.829900-1-abhinjoses@gmail.com/
[5] https://lore.kernel.org/all/20260927175203.928270-1-abhinjoses@gmail.com/
Assisted-by: LLM
Abhin Parekadan Jose (4):
PCI: pciehp: Add pci_hp_wait_link_change()
PCI/DPC: Add pci_dpc_wait_recovery()
PCI: pciehp: Report surprise removal from pciehp_isr()
misc: Add edu_srpoc surprise removal POC driver
Michael S. Tsirkin (1):
PCI: Report surprise removal event
drivers/misc/Kconfig | 11 ++
drivers/misc/Makefile | 1 +
drivers/misc/edu_srpoc.c | 172 +++++++++++++++++++++++++
drivers/pci/hotplug/pci_hotplug_core.c | 21 ++-
drivers/pci/hotplug/pciehp.h | 1 +
drivers/pci/hotplug/pciehp_hpc.c | 67 ++++++++++
drivers/pci/pci.h | 10 ++
drivers/pci/pcie/dpc.c | 54 ++++++--
drivers/pci/probe.c | 1 +
include/linux/pci.h | 57 ++++++++
10 files changed, 381 insertions(+), 14 deletions(-)
create mode 100644 drivers/misc/edu_srpoc.c
base-commit: fd179f8a05be3ccae366b9b96e176b51fbe54aab
--
2.51.1
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH RFC v4 1/5] PCI: Report surprise removal event
2026-09-27 18:20 [PATCH RFC v4 0/5] PCI: pciehp: Report surprise removal during safe removal Abhin Parekadan Jose
@ 2026-09-27 18:20 ` Abhin Parekadan Jose
2026-09-27 18:20 ` [PATCH RFC v4 2/5] PCI: pciehp: Add pci_hp_wait_link_change() Abhin Parekadan Jose
` (3 subsequent siblings)
4 siblings, 0 replies; 7+ messages in thread
From: Abhin Parekadan Jose @ 2026-09-27 18:20 UTC (permalink / raw)
To: Bjorn Helgaas, Lukas Wunner, Michael S. Tsirkin
Cc: Ilpo Järvinen, Shuai Xue, Kees Cook, Mahesh J Salgaonkar,
Oliver O'Halloran, linux-pci, linuxppc-dev, linux-kernel,
Abhin Parekadan Jose
From: "Michael S. Tsirkin" <mst@redhat.com>
At the moment, in case of a surprise removal, the regular remove
callback is invoked, exclusively. This works well, because mostly,
the cleanup would be the same.
However, there's a race: imagine device removal was initiated by a user
action, such as driver unbind, and it in turn initiated some cleanup
and is now waiting for an interrupt from the device. If the device is
now surprise-removed, that never arrives and the remove callback hangs
forever.
For example, this was reported for virtio-blk:
1. the graceful removal is ongoing in the remove() callback,
where disk deletion del_gendisk() is ongoing, which waits
for the requests to complete,
2. Now few requests are yet to complete, and surprise removal
started.
At this point, virtio block driver will not get notified by
the driver core layer, because it is likely serializing
remove() happening by +user/driver unload and PCI hotplug
driver-initiated device removal. So vblk driver doesn't
know that device is removed, block layer is waiting for
requests completions to arrive which it never gets.
So del_gendisk() gets stuck.
Drivers can artificially add timeouts to handle that, but it can be
flaky.
Instead, let's add a way for the driver to be notified about the
disconnect. It can then do any necessary cleanup, knowing that
the device is inactive.
Since cleanups can take a long time, this takes an approach of a work
struct that the driver initiates and enables on probe, and tears down on
remove.
Signed-off-by: Michael S. Tsirkin <mst@redhat.com>
Link: https://lore.kernel.org/all/fba3d235e38c1c6fcef2a30ed083ad9e25b20fa3.1752094439.git.mst@redhat.com/
[Abhin: adapted subject, serialize with a per-device spinlock]
Signed-off-by: Abhin Parekadan Jose <abhinjoses@gmail.com>
---
Changes since RFC v2:
- Protect disconnect_work_enable with a per-device spinlock, held while
testing it and scheduling the work, instead of lockless accesses and
barriers. pci_dev_set_disconnected() could otherwise test the flag,
get preempted, and queue the work while a newly bound driver
re-initializes it. With the lock, cancel_work_sync() is sufficient
again, so go back to it from disable_work_sync(). (Sashiko)
Changes since RFC v1:
- Use disable_work_sync() instead of cancel_work_sync() in
pci_clear_disconnect_work() as schedule_work() on a
disabled work item is a no-op. (Sashiko)
RFC v2: https://lore.kernel.org/all/20260927165459.829900-2-abhinjoses@gmail.com/
Sashiko review of v2: https://lore.kernel.org/all/20260927170707.6F9241F000FF@smtp.kernel.org/
RFC v1: https://lore.kernel.org/all/20260905183905.997833-2-abhinjoses@gmail.com/
Sashiko review of v1: https://lore.kernel.org/all/20260905184649.E8F621F00A3A@smtp.kernel.org/
---
drivers/pci/pci.h | 7 ++++++
drivers/pci/probe.c | 1 +
include/linux/pci.h | 57 +++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 65 insertions(+)
diff --git a/drivers/pci/pci.h b/drivers/pci/pci.h
index ba3c3fddddc23..175273756c183 100644
--- a/drivers/pci/pci.h
+++ b/drivers/pci/pci.h
@@ -802,9 +802,16 @@ static inline bool pci_dev_set_io_state(struct pci_dev *dev,
static inline int pci_dev_set_disconnected(struct pci_dev *dev, void *unused)
{
+ unsigned long flags;
+
pci_dev_set_io_state(dev, pci_channel_io_perm_failure);
pci_doe_disconnected(dev);
+ spin_lock_irqsave(&dev->disconnect_lock, flags);
+ if (dev->disconnect_work_enable)
+ schedule_work(&dev->disconnect_work);
+ spin_unlock_irqrestore(&dev->disconnect_lock, flags);
+
return 0;
}
diff --git a/drivers/pci/probe.c b/drivers/pci/probe.c
index 27008e2ea5afc..3a05ad32bfd54 100644
--- a/drivers/pci/probe.c
+++ b/drivers/pci/probe.c
@@ -2515,6 +2515,7 @@ struct pci_dev *pci_alloc_dev(struct pci_bus *bus)
};
spin_lock_init(&dev->pcie_cap_lock);
+ spin_lock_init(&dev->disconnect_lock);
#ifdef CONFIG_PCI_MSI
raw_spin_lock_init(&dev->msi_lock);
#endif
diff --git a/include/linux/pci.h b/include/linux/pci.h
index d31a8d107b1ef..aed62ae709f59 100644
--- a/include/linux/pci.h
+++ b/include/linux/pci.h
@@ -592,6 +592,10 @@ struct pci_dev {
u8 reset_methods[PCI_NUM_RESET_METHODS]; /* In priority order */
struct gpio_desc *wake; /* WAKE# GPIO */
+ /* Report disconnect events. 0x0 - disable, 0x1 - enable */
+ u8 disconnect_work_enable;
+ spinlock_t disconnect_lock; /* Protects disconnect_work_enable */
+ struct work_struct disconnect_work;
#ifdef CONFIG_PCIE_TPH
u16 tph_cap; /* TPH capability offset */
@@ -2123,6 +2127,59 @@ pci_release_mem_regions(struct pci_dev *pdev)
pci_select_bars(pdev, IORESOURCE_MEM));
}
+/*
+ * Run this first thing after getting a disconnect work, to prevent it from
+ * running multiple times.
+ * Returns: true if disconnect was enabled, proceed. false if disabled, abort.
+ */
+static inline bool pci_test_and_clear_disconnect_enable(struct pci_dev *pdev)
+{
+ unsigned long flags;
+ bool enabled;
+
+ spin_lock_irqsave(&pdev->disconnect_lock, flags);
+ enabled = pdev->disconnect_work_enable;
+ pdev->disconnect_work_enable = 0x0;
+ spin_unlock_irqrestore(&pdev->disconnect_lock, flags);
+
+ return enabled;
+}
+
+/*
+ * Caller must initialize @pdev->disconnect_work before invoking this.
+ * The work function must run and check pci_test_and_clear_disconnect_enable.
+ * Note that device can go away right after this call.
+ */
+static inline void pci_set_disconnect_work(struct pci_dev *pdev)
+{
+ unsigned long flags;
+
+ spin_lock_irqsave(&pdev->disconnect_lock, flags);
+ pdev->disconnect_work_enable = 0x1;
+ spin_unlock_irqrestore(&pdev->disconnect_lock, flags);
+
+ /* check the device did not go away meanwhile. */
+ if (pci_device_is_present(pdev))
+ return;
+
+ spin_lock_irqsave(&pdev->disconnect_lock, flags);
+ if (pdev->disconnect_work_enable)
+ schedule_work(&pdev->disconnect_work);
+ spin_unlock_irqrestore(&pdev->disconnect_lock, flags);
+}
+
+static inline void pci_clear_disconnect_work(struct pci_dev *pdev)
+{
+ unsigned long flags;
+
+ spin_lock_irqsave(&pdev->disconnect_lock, flags);
+ pdev->disconnect_work_enable = 0x0;
+ spin_unlock_irqrestore(&pdev->disconnect_lock, flags);
+
+ /* No one can queue the work any more; wait for a queued or running one */
+ cancel_work_sync(&pdev->disconnect_work);
+}
+
bool pci_suspend_retains_context(struct pci_dev *pdev);
#else /* CONFIG_PCI is not enabled */
--
2.51.1
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH RFC v4 2/5] PCI: pciehp: Add pci_hp_wait_link_change()
2026-09-27 18:20 [PATCH RFC v4 0/5] PCI: pciehp: Report surprise removal during safe removal Abhin Parekadan Jose
2026-09-27 18:20 ` [PATCH RFC v4 1/5] PCI: Report surprise removal event Abhin Parekadan Jose
@ 2026-09-27 18:20 ` Abhin Parekadan Jose
2026-09-27 18:20 ` [PATCH RFC v4 3/5] PCI/DPC: Add pci_dpc_wait_recovery() Abhin Parekadan Jose
` (2 subsequent siblings)
4 siblings, 0 replies; 7+ messages in thread
From: Abhin Parekadan Jose @ 2026-09-27 18:20 UTC (permalink / raw)
To: Bjorn Helgaas, Lukas Wunner, Michael S. Tsirkin
Cc: Ilpo Järvinen, Shuai Xue, Kees Cook, Mahesh J Salgaonkar,
Oliver O'Halloran, linux-pci, linuxppc-dev, linux-kernel,
Abhin Parekadan Jose
pci_hp_spurious_link_change() awaits the end of a code section causing
spurious link changes, such as a Secondary Bus Reset, and then tells
whether such a section has executed, using test_and_clear_bit() on
PCI_LINK_CHANGED. The answer is one-shot: only the first caller sees
true.
Split the wait out into pci_hp_wait_link_change(), which leaves
PCI_LINK_CHANGED alone, and implement pci_hp_spurious_link_change() on
top of it. Test PCI_LINK_CHANGING with test_bit_acquire(), so that the
caller's subsequent accesses are ordered after the end of the code
section even if wait_event() returns without sleeping. No functional
change intended.
Signed-off-by: Abhin Parekadan Jose <abhinjoses@gmail.com>
Assisted-by: LLM
---
Changes since RFC v2:
- Test PCI_LINK_CHANGING with test_bit_acquire(). (Sashiko)
RFC v2: https://lore.kernel.org/all/20260927165459.829900-3-abhinjoses@gmail.com/
Sashiko review of v2: https://lore.kernel.org/all/20260927170355.759301F000FF@smtp.kernel.org/
---
drivers/pci/hotplug/pci_hotplug_core.c | 21 +++++++++++++++++++--
drivers/pci/pci.h | 1 +
2 files changed, 20 insertions(+), 2 deletions(-)
diff --git a/drivers/pci/hotplug/pci_hotplug_core.c b/drivers/pci/hotplug/pci_hotplug_core.c
index fadcf98a8a660..75524fe2bec6e 100644
--- a/drivers/pci/hotplug/pci_hotplug_core.c
+++ b/drivers/pci/hotplug/pci_hotplug_core.c
@@ -528,6 +528,24 @@ void pci_hp_unignore_link_change(struct pci_dev *pdev)
wake_up_all(&pci_hp_link_change_wq);
}
+/**
+ * pci_hp_wait_link_change - await end of code section causing spurious link changes
+ * @pdev: PCI hotplug bridge
+ *
+ * Await the end of a concurrently executing code section which is causing
+ * spurious link changes on the Secondary Bus of @pdev, if there is one.
+ *
+ * Unlike pci_hp_spurious_link_change(), leave the record that such a code
+ * section has executed in place. May be called by hotplug drivers which need
+ * the link to have settled, but not the cause of a link change, so that they
+ * don't take the answer away from the caller of pci_hp_spurious_link_change().
+ */
+void pci_hp_wait_link_change(struct pci_dev *pdev)
+{
+ wait_event(pci_hp_link_change_wq,
+ !test_bit_acquire(PCI_LINK_CHANGING, &pdev->priv_flags));
+}
+
/**
* pci_hp_spurious_link_change - check for spurious link changes
* @pdev: PCI hotplug bridge
@@ -551,8 +569,7 @@ void pci_hp_unignore_link_change(struct pci_dev *pdev)
*/
bool pci_hp_spurious_link_change(struct pci_dev *pdev)
{
- wait_event(pci_hp_link_change_wq,
- !test_bit(PCI_LINK_CHANGING, &pdev->priv_flags));
+ pci_hp_wait_link_change(pdev);
return test_and_clear_bit(PCI_LINK_CHANGED, &pdev->priv_flags);
}
diff --git a/drivers/pci/pci.h b/drivers/pci/pci.h
index 175273756c183..cfa0202bf610b 100644
--- a/drivers/pci/pci.h
+++ b/drivers/pci/pci.h
@@ -360,6 +360,7 @@ static inline int pci_proc_detach_bus(struct pci_bus *bus) { return 0; }
/* Functions for PCI Hotplug drivers to use */
int pci_hp_add_bridge(struct pci_dev *dev);
+void pci_hp_wait_link_change(struct pci_dev *pdev);
bool pci_hp_spurious_link_change(struct pci_dev *pdev);
/* Lock for read/write access to pci device and bus lists */
--
2.51.1
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH RFC v4 3/5] PCI/DPC: Add pci_dpc_wait_recovery()
2026-09-27 18:20 [PATCH RFC v4 0/5] PCI: pciehp: Report surprise removal during safe removal Abhin Parekadan Jose
2026-09-27 18:20 ` [PATCH RFC v4 1/5] PCI: Report surprise removal event Abhin Parekadan Jose
2026-09-27 18:20 ` [PATCH RFC v4 2/5] PCI: pciehp: Add pci_hp_wait_link_change() Abhin Parekadan Jose
@ 2026-09-27 18:20 ` Abhin Parekadan Jose
2026-09-27 18:20 ` [PATCH RFC v4 4/5] PCI: pciehp: Report surprise removal from pciehp_isr() Abhin Parekadan Jose
2026-09-27 18:20 ` [PATCH RFC v4 5/5] misc: Add edu_srpoc surprise removal POC driver Abhin Parekadan Jose
4 siblings, 0 replies; 7+ messages in thread
From: Abhin Parekadan Jose @ 2026-09-27 18:20 UTC (permalink / raw)
To: Bjorn Helgaas, Lukas Wunner, Michael S. Tsirkin
Cc: Ilpo Järvinen, Shuai Xue, Kees Cook, Mahesh J Salgaonkar,
Oliver O'Halloran, linux-pci, linuxppc-dev, linux-kernel,
Abhin Parekadan Jose
pci_dpc_recovered() awaits completion of DPC recovery and then tells
whether DPC recovered successfully, using test_and_clear_bit() on
PCI_DPC_RECOVERED. The answer is one-shot: only the first caller sees
true.
Split pci_dpc_recovered() into:
- pci_dpc_hp_sync_supported(), the check whether hotplug can
synchronize with DPC at all.
- dpc_wait_completed(), the wait for recovery with its 4 second
timeout;
- the final test_and_clear_bit().
Add pci_dpc_wait_recovery(), which does the check and the wait but
leaves PCI_DPC_RECOVERED alone. No functional change intended.
Signed-off-by: Abhin Parekadan Jose <abhinjoses@gmail.com>
Assisted-by: LLM
---
drivers/pci/pci.h | 2 ++
drivers/pci/pcie/dpc.c | 54 ++++++++++++++++++++++++++++++++----------
2 files changed, 44 insertions(+), 12 deletions(-)
diff --git a/drivers/pci/pci.h b/drivers/pci/pci.h
index cfa0202bf610b..b1d9df904c625 100644
--- a/drivers/pci/pci.h
+++ b/drivers/pci/pci.h
@@ -941,12 +941,14 @@ void pci_restore_dpc_state(struct pci_dev *dev);
void pci_dpc_init(struct pci_dev *pdev);
void dpc_process_error(struct pci_dev *pdev);
pci_ers_result_t dpc_reset_link(struct pci_dev *pdev);
+void pci_dpc_wait_recovery(struct pci_dev *pdev);
bool pci_dpc_recovered(struct pci_dev *pdev);
unsigned int dpc_tlp_log_len(struct pci_dev *dev);
#else
static inline void pci_save_dpc_state(struct pci_dev *dev) { }
static inline void pci_restore_dpc_state(struct pci_dev *dev) { }
static inline void pci_dpc_init(struct pci_dev *pdev) { }
+static inline void pci_dpc_wait_recovery(struct pci_dev *pdev) { }
static inline bool pci_dpc_recovered(struct pci_dev *pdev) { return false; }
#endif
diff --git a/drivers/pci/pcie/dpc.c b/drivers/pci/pcie/dpc.c
index 2b779bd1d861b..a6438a7e3d9c7 100644
--- a/drivers/pci/pcie/dpc.c
+++ b/drivers/pci/pcie/dpc.c
@@ -92,15 +92,7 @@ static bool dpc_completed(struct pci_dev *pdev)
return true;
}
-/**
- * pci_dpc_recovered - whether DPC triggered and has recovered successfully
- * @pdev: PCI device
- *
- * Return true if DPC was triggered for @pdev and has recovered successfully.
- * Wait for recovery if it hasn't completed yet. Called from the PCIe hotplug
- * driver to recognize and ignore Link Down/Up events caused by DPC.
- */
-bool pci_dpc_recovered(struct pci_dev *pdev)
+static bool pci_dpc_hp_sync_supported(struct pci_dev *pdev)
{
struct pci_host_bridge *host;
@@ -108,13 +100,18 @@ bool pci_dpc_recovered(struct pci_dev *pdev)
return false;
/*
- * Synchronization between hotplug and DPC is not supported
- * if DPC is owned by firmware and EDR is not enabled.
- */
+ * Synchronization between hotplug and DPC is only supported if DPC is owned
+ * by the OS, or by firmware with EDR enabled.
+ */
host = pci_find_host_bridge(pdev->bus);
if (!host->native_dpc && !IS_ENABLED(CONFIG_PCIE_EDR))
return false;
+ return true;
+}
+
+static void dpc_wait_completed(struct pci_dev *pdev)
+{
/*
* Need a timeout in case DPC never completes due to failure of
* dpc_wait_rp_inactive(). The spec doesn't mandate a time limit,
@@ -122,6 +119,39 @@ bool pci_dpc_recovered(struct pci_dev *pdev)
*/
wait_event_timeout(dpc_completed_waitqueue, dpc_completed(pdev),
msecs_to_jiffies(4000));
+}
+
+/**
+ * pci_dpc_wait_recovery - await completion of DPC recovery
+ * @pdev: PCI device
+ *
+ * Wait for recovery if DPC was triggered for @pdev and recovery hasn't
+ * completed yet. Unlike pci_dpc_recovered(), leave the record of a
+ * successful recovery in place. Called from the PCIe hotplug driver where
+ * it needs the link to have settled, but not the cause of a link change.
+ * Nothing is awaited if synchronization between hotplug and DPC is not
+ * supported for @pdev.
+ */
+void pci_dpc_wait_recovery(struct pci_dev *pdev)
+{
+ if (pci_dpc_hp_sync_supported(pdev))
+ dpc_wait_completed(pdev);
+}
+
+/**
+ * pci_dpc_recovered - whether DPC triggered and has recovered successfully
+ * @pdev: PCI device
+ *
+ * Return true if DPC was triggered for @pdev and has recovered successfully.
+ * Wait for recovery if it hasn't completed yet. Called from the PCIe hotplug
+ * driver to recognize and ignore Link Down/Up events caused by DPC.
+ */
+bool pci_dpc_recovered(struct pci_dev *pdev)
+{
+ if (!pci_dpc_hp_sync_supported(pdev))
+ return false;
+
+ dpc_wait_completed(pdev);
return test_and_clear_bit(PCI_DPC_RECOVERED, &pdev->priv_flags);
}
--
2.51.1
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH RFC v4 4/5] PCI: pciehp: Report surprise removal from pciehp_isr()
2026-09-27 18:20 [PATCH RFC v4 0/5] PCI: pciehp: Report surprise removal during safe removal Abhin Parekadan Jose
` (2 preceding siblings ...)
2026-09-27 18:20 ` [PATCH RFC v4 3/5] PCI/DPC: Add pci_dpc_wait_recovery() Abhin Parekadan Jose
@ 2026-09-27 18:20 ` Abhin Parekadan Jose
2026-09-27 18:20 ` [PATCH RFC v4 5/5] misc: Add edu_srpoc surprise removal POC driver Abhin Parekadan Jose
4 siblings, 0 replies; 7+ messages in thread
From: Abhin Parekadan Jose @ 2026-09-27 18:20 UTC (permalink / raw)
To: Bjorn Helgaas, Lukas Wunner, Michael S. Tsirkin
Cc: Ilpo Järvinen, Shuai Xue, Kees Cook, Mahesh J Salgaonkar,
Oliver O'Halloran, linux-pci, linuxppc-dev, linux-kernel,
Abhin Parekadan Jose
A surprise removal during a safe removal cannot be reported: the
removal blocks waiting on a device interrupt or status read, and the
single-threaded IRQ thread is itself executing that removal, so it
cannot report that the device is gone. The removal hangs.
The hardirq handler pciehp_isr() still runs while the IRQ thread is
blocked, so it can report the disconnect. However, pciehp_ist()
deliberately ignores link and presence changes caused by a Secondary
Bus Reset or Downstream Port Containment, where the device is only
temporarily inaccessible. Distinguishing those normally requires
waiting for the SBR or DPC to conclude, which takes seconds and is not
possible in hardirq context.
Schedule a work item from pciehp_isr() when a PDC or DLLSC event
arrives and neither Presence Detect State nor Data Link Layer Link
Active indicates that a card is present.
This provides us a pathway to wait/block/sleep as we will not be
in pciehp_isr().
In pciehp_disconnect_work(), wait for the DPC recovery or SBR to complete
before checking presence and not consume the link change flags, so that
pciehp_ist() can still see them and ignore the link change if it was
caused by a DPC or SBR. Even after the DPC or SBR has completed, if the
device is really gone then pciehp_disconnect_work() will see that and
report the surprise removal. Like pciehp_ist(), it holds a runtime PM
reference on the port and checks presence under reset_lock, since a
slot reset may make Presence Detect State and Link Active flap.
Link: https://lore.kernel.org/all/aHlZE18kPuHuDtTT@wunner.de/
Signed-off-by: Abhin Parekadan Jose <abhinjoses@gmail.com>
Assisted-by: LLM
---
Changes since RFC v1:
- Drop schedule_notification_work(). pci_dev_set_disconnected() schedules
the driver's disconnect_work again, as in patch 1, for all callers, and
pciehp_disconnect_work() calls it through pci_walk_bus(). (Michael)
- Return early from pciehp_disconnect_work() if pending_events is zero.
pciehp_ist() has then already taken the events and handles them itself.
(Sashiko)
- Treat a read error of the presence check as "not present", both when
scheduling the work in pciehp_isr() and when checking presence in
pciehp_disconnect_work(). (Sashiko)
- Check presence with pciehp_card_present_or_link_active(), as
pciehp_ist() does, so that a port with Presence Detect State hardwired
to zero is not mistaken for an empty slot.
- In pciehp_isr(), check presence before dropping the runtime PM
reference on the port's parent. In pciehp_disconnect_work(), take a
runtime PM reference and check presence under reset_lock.
- Don't call the spurious link change test from pciehp_disconnect_work().
It consumes the one-shot flags PCI_DPC_RECOVERED and PCI_LINK_CHANGED,
so pciehp_ist() could miss them and tear down a device that was only
reset. Await DPC recovery and Secondary Bus Reset with the new
pci_dpc_wait_recovery() and pci_hp_wait_link_change() (patches 2 and 3)
instead, without consuming the flags. pciehp_is_spurious_link_change()
is dropped and pciehp_ist() is unchanged. (Sashiko)
RFC v1: https://lore.kernel.org/all/20260905183905.997833-3-abhinjoses@gmail.com/
Michael's review: https://lore.kernel.org/all/20260912115420-mutt-send-email-mst@kernel.org/
Sashiko review: https://lore.kernel.org/all/20260905185217.E9BC21F00A3A@smtp.kernel.org/
---
drivers/pci/hotplug/pciehp.h | 1 +
drivers/pci/hotplug/pciehp_hpc.c | 67 ++++++++++++++++++++++++++++++++
2 files changed, 68 insertions(+)
diff --git a/drivers/pci/hotplug/pciehp.h b/drivers/pci/hotplug/pciehp.h
index debc79b0adfb2..c8ceb9320e2e9 100644
--- a/drivers/pci/hotplug/pciehp.h
+++ b/drivers/pci/hotplug/pciehp.h
@@ -116,6 +116,7 @@ struct controller {
unsigned int ist_running;
int request_result;
wait_queue_head_t requester;
+ struct work_struct disconnect_work;
};
/**
diff --git a/drivers/pci/hotplug/pciehp_hpc.c b/drivers/pci/hotplug/pciehp_hpc.c
index 4c62140a3cb44..470cc16d4a828 100644
--- a/drivers/pci/hotplug/pciehp_hpc.c
+++ b/drivers/pci/hotplug/pciehp_hpc.c
@@ -620,12 +620,64 @@ static void pciehp_ignore_link_change(struct controller *ctrl,
up_read(&ctrl->reset_lock);
}
+/*
+ * Workaround to not wait in the isr.
+ */
+static void pciehp_disconnect_work(struct work_struct *work)
+{
+ struct pci_bus *bus;
+ struct controller *ctrl = container_of(work, struct controller,
+ disconnect_work);
+ struct pci_dev *pdev = ctrl_dev(ctrl);
+ u32 events;
+ int present;
+
+ events = atomic_read(&ctrl->pending_events);
+
+ /*
+ * Zero means pciehp_ist() has already consumed the events with
+ * atomic_xchg() and is handling them itself, with the full event
+ * mask available for the spurious link change test. Leave it to
+ * the IRQ thread: acting here would override its decision, and a
+ * zero carries no information about the device.
+ *
+ * The events only stay pending for us when the IRQ thread is
+ * blocked and cannot drain them, e.g. in pciehp_unconfigure_device()
+ * during a safe removal. That is the case this work item exists
+ * for.
+ */
+ if (!events)
+ return;
+
+ pci_config_pm_runtime_get(pdev);
+
+ /* Wait for DPC recovery or a SBR to complete */
+ pci_dpc_wait_recovery(pdev);
+ pci_hp_wait_link_change(pdev);
+
+ /* Presence Detect State and Link Active may flap during a slot reset */
+ down_read_nested(&ctrl->reset_lock, ctrl->depth);
+ present = pciehp_card_present_or_link_active(ctrl);
+ up_read(&ctrl->reset_lock);
+
+ pci_config_pm_runtime_put(pdev);
+
+ bus = ctrl->pcie->port->subordinate;
+
+ /* The card may have returned */
+ if (!bus || present > 0)
+ return;
+
+ pci_walk_bus(bus, pci_dev_set_disconnected, NULL);
+}
+
static irqreturn_t pciehp_isr(int irq, void *dev_id)
{
struct controller *ctrl = (struct controller *)dev_id;
struct pci_dev *pdev = ctrl_dev(ctrl);
struct device *parent = pdev->dev.parent;
u16 status, events = 0;
+ int present = 1;
/*
* Interrupts only occur in D3hot or shallower and only if enabled
@@ -697,6 +749,14 @@ static irqreturn_t pciehp_isr(int irq, void *dev_id)
}
ctrl_dbg(ctrl, "pending interrupts %#06x from Slot Status\n", events);
+
+ /*
+ * Check presence while the port is kept accessible. This may race
+ * with a slot reset, so pciehp_disconnect_work() checks again.
+ */
+ if (events & (PCI_EXP_SLTSTA_PDC | PCI_EXP_SLTSTA_DLLSC))
+ present = pciehp_card_present_or_link_active(ctrl);
+
if (parent)
pm_runtime_put(parent);
@@ -722,6 +782,11 @@ static irqreturn_t pciehp_isr(int irq, void *dev_id)
/* Save pending events for consumption by IRQ thread. */
atomic_or(events, &ctrl->pending_events);
+
+ /* The card may be gone, let process context decide */
+ if (present <= 0)
+ schedule_work(&ctrl->disconnect_work);
+
return IRQ_WAKE_THREAD;
}
@@ -1036,6 +1101,7 @@ struct controller *pcie_init(struct pcie_device *dev)
init_waitqueue_head(&ctrl->requester);
init_waitqueue_head(&ctrl->queue);
INIT_DELAYED_WORK(&ctrl->button_work, pciehp_queue_pushbutton_work);
+ INIT_WORK(&ctrl->disconnect_work, pciehp_disconnect_work);
dbg_ctrl(ctrl);
down_read(&pci_bus_sem);
@@ -1096,6 +1162,7 @@ struct controller *pcie_init(struct pcie_device *dev)
void pciehp_release_ctrl(struct controller *ctrl)
{
cancel_delayed_work_sync(&ctrl->button_work);
+ cancel_work_sync(&ctrl->disconnect_work);
kfree(ctrl);
}
--
2.51.1
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH RFC v4 5/5] misc: Add edu_srpoc surprise removal POC driver
2026-09-27 18:20 [PATCH RFC v4 0/5] PCI: pciehp: Report surprise removal during safe removal Abhin Parekadan Jose
` (3 preceding siblings ...)
2026-09-27 18:20 ` [PATCH RFC v4 4/5] PCI: pciehp: Report surprise removal from pciehp_isr() Abhin Parekadan Jose
@ 2026-09-27 18:20 ` Abhin Parekadan Jose
4 siblings, 0 replies; 7+ messages in thread
From: Abhin Parekadan Jose @ 2026-09-27 18:20 UTC (permalink / raw)
To: Bjorn Helgaas, Lukas Wunner, Michael S. Tsirkin
Cc: Ilpo Järvinen, Shuai Xue, Kees Cook, Mahesh J Salgaonkar,
Oliver O'Halloran, linux-pci, linuxppc-dev, linux-kernel,
Abhin Parekadan Jose
A test driver for the QEMU edu device that reproduces the surprise
removal hang described in MST's RFC v5 thread.
- hacked in a reg to the edu device on qemu to raise a delayed irq
- This driver writes to that reg in remove and waits for the irq to be
handled. This simulates del_gendisk() blocked in
blk_mq_freeze_queue_wait()
remove() blocks until the delayed interrupt arrives, 600 seconds after
it is requested, or until the device is surprise removed and its
disconnect work runs. That is the purpose of the driver, so a normal
unbind takes 600 seconds, and on a QEMU without the delayed interrupt
register it never returns. It is only built with CONFIG_EDU_SRPOC.
Assisted-by: LLM
Signed-off-by: Abhin Parekadan Jose <abhinjoses@gmail.com>
---
Changes since RFC v3:
- Reinitialize the completion in remove() before requesting the delayed
interrupt, so that an earlier completion cannot let remove() return
while the interrupt is still pending. (Sashiko)
Changes since RFC v1:
- Build only with CONFIG_EDU_SRPOC, which depends on PCI, instead of
unconditionally. (Sashiko)
- Don't claim the interrupt if the status register reads all ones,
i.e. the device is gone. (Sashiko)
- Clear bus mastering in the probe error path and in remove(). (Sashiko)
- Document that remove() blocks by design. (Sashiko)
- Fixed a checkpatch warning about braced if() blocks.
RFC v1: https://lore.kernel.org/all/20260905183905.997833-4-abhinjoses@gmail.com/
Sashiko review: https://lore.kernel.org/all/20260905185027.291191F00A3A@smtp.kernel.org/
RFC v3: https://lore.kernel.org/all/20260927175203.928270-6-abhinjoses@gmail.com/
Sashiko review of v3: https://lore.kernel.org/all/20260927175937.E006F1F00893@smtp.kernel.org/
---
drivers/misc/Kconfig | 11 +++
drivers/misc/Makefile | 1 +
drivers/misc/edu_srpoc.c | 172 +++++++++++++++++++++++++++++++++++++++
3 files changed, 184 insertions(+)
create mode 100644 drivers/misc/edu_srpoc.c
diff --git a/drivers/misc/Kconfig b/drivers/misc/Kconfig
index 7364931dad3a1..99457e53d21cf 100644
--- a/drivers/misc/Kconfig
+++ b/drivers/misc/Kconfig
@@ -57,6 +57,17 @@ config DUMMY_IRQ
The sole purpose of this module is to help with debugging of systems on
which spurious IRQs would happen on disabled IRQ vector.
+config EDU_SRPOC
+ tristate "QEMU edu surprise removal POC driver"
+ depends on PCI
+ help
+ Test driver for the QEMU edu device. Its remove() callback blocks
+ until the device raises a delayed interrupt or is surprise removed,
+ to reproduce a hang in remove() during surprise removal. Needs an
+ edu device with the delayed interrupt register at BAR0 0x30.
+
+ If unsure, say N.
+
config IBMVMC
tristate "IBM Virtual Management Channel support"
depends on PPC_PSERIES
diff --git a/drivers/misc/Makefile b/drivers/misc/Makefile
index e8d8d5d88c0df..9ebcc6ce60f34 100644
--- a/drivers/misc/Makefile
+++ b/drivers/misc/Makefile
@@ -9,6 +9,7 @@ obj-$(CONFIG_AD525X_DPOT_I2C) += ad525x_dpot-i2c.o
obj-$(CONFIG_AD525X_DPOT_SPI) += ad525x_dpot-spi.o
obj-$(CONFIG_ATMEL_SSC) += atmel-ssc.o
obj-$(CONFIG_DUMMY_IRQ) += dummy-irq.o
+obj-$(CONFIG_EDU_SRPOC) += edu_srpoc.o
obj-$(CONFIG_ICS932S401) += ics932s401.o
obj-$(CONFIG_LKDTM) += lkdtm/
obj-$(CONFIG_TI_FPC202) += ti_fpc202.o
diff --git a/drivers/misc/edu_srpoc.c b/drivers/misc/edu_srpoc.c
new file mode 100644
index 0000000000000..1f395b2c31ee3
--- /dev/null
+++ b/drivers/misc/edu_srpoc.c
@@ -0,0 +1,172 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * edu_srpoc.c Surprise Removal POC driver for the QEMU edu device
+ *
+ * In remove(), schedules a delayed interrupt on the edu device and
+ * blocks waiting for it to complete. This simulates del_gendisk()
+ * blocked in blk_mq_freeze_queue_wait() on slow in-flight I/O.
+ *
+ * Surprise-remove the device during this window to reproduce the hang.
+ *
+ * edu BAR 0 registers used:
+ * 0x08 Factorial: write N to compute N! asynchronously
+ * 0x20 Status: write EDU_STATUS_IRQFACT to enable IRQ on completion
+ * 0x24 IRQ status: bit 0 = FACT_IRQ, bit 9 = DELAY_IRQ
+ * 0x30 Delayed IRQ: write N (ms). Hacked in this functionality(not upstream).
+ * 0x64 IRQ lower: write bitmask to ack
+ */
+
+#include <linux/module.h>
+#include <linux/pci.h>
+#include <linux/interrupt.h>
+#include <linux/completion.h>
+#include <linux/delay.h>
+
+#define PCI_VENDOR_ID_EDU 0x1234
+#define PCI_DEVICE_ID_EDU 0x11e8
+
+#define EDU_REG_FACT 0x08
+#define EDU_REG_STATUS 0x20
+#define EDU_REG_DELAYED_IRQ 0x30
+#define EDU_REG_IRQ_STATUS 0x24
+#define EDU_REG_IRQ_LOWER 0x64
+
+#define EDU_STATUS_IRQFACT 0x80
+#define EDU_FACT_IRQ BIT(0)
+#define EDU_DELAY_IRQ BIT(9)
+
+struct edu_dev {
+ struct pci_dev *pdev;
+ void __iomem *regs;
+ struct completion irq_done;
+};
+
+static irqreturn_t edu_irq_handler(int irq, void *data)
+{
+ struct edu_dev *edu = data;
+ u32 status;
+
+ status = ioread32(edu->regs + EDU_REG_IRQ_STATUS);
+ /* All ones means the device is gone; the interrupt is not ours */
+ if (!status || PCI_POSSIBLE_ERROR(status))
+ return IRQ_NONE;
+
+ iowrite32(status, edu->regs + EDU_REG_IRQ_LOWER);
+
+ if (status & (EDU_FACT_IRQ | EDU_DELAY_IRQ))
+ complete(&edu->irq_done);
+
+ return IRQ_HANDLED;
+}
+
+static void edu_disconnect(struct work_struct *work)
+{
+ struct pci_dev *pdev = container_of(work, struct pci_dev,
+ disconnect_work);
+ struct edu_dev *edu = pci_get_drvdata(pdev);
+
+ if (!pci_test_and_clear_disconnect_enable(pdev))
+ return;
+
+ if (!edu)
+ return;
+
+ dev_info(&pdev->dev, "disconnect_work fired — unblocking remove()\n");
+ complete(&edu->irq_done);
+}
+
+static int edu_probe(struct pci_dev *pdev, const struct pci_device_id *id)
+{
+ struct edu_dev *edu;
+ int err;
+
+ edu = devm_kzalloc(&pdev->dev, sizeof(*edu), GFP_KERNEL);
+ if (!edu)
+ return -ENOMEM;
+
+ edu->pdev = pdev;
+ init_completion(&edu->irq_done);
+
+ err = pci_enable_device(pdev);
+ if (err)
+ return err;
+
+ err = pci_request_regions(pdev, "edu_srpoc");
+ if (err)
+ goto err_disable;
+
+ edu->regs = pci_iomap(pdev, 0, 0);
+ if (!edu->regs) {
+ err = -ENOMEM;
+ goto err_release;
+ }
+
+ pci_set_master(pdev);
+
+ err = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI | PCI_IRQ_INTX);
+ if (err < 0)
+ goto err_iounmap;
+
+ err = request_irq(pci_irq_vector(pdev, 0), edu_irq_handler,
+ IRQF_SHARED, "edu_srpoc", edu);
+ if (err)
+ goto err_free_vectors;
+
+ pci_set_drvdata(pdev, edu);
+
+ INIT_WORK(&pdev->disconnect_work, edu_disconnect);
+ pci_set_disconnect_work(pdev);
+
+ dev_info(&pdev->dev, "edu_srpoc probed\n");
+ return 0;
+
+err_free_vectors:
+ pci_free_irq_vectors(pdev);
+err_iounmap:
+ pci_clear_master(pdev);
+ pci_iounmap(pdev, edu->regs);
+err_release:
+ pci_release_regions(pdev);
+err_disable:
+ pci_disable_device(pdev);
+ return err;
+}
+
+static void edu_remove(struct pci_dev *pdev)
+{
+ struct edu_dev *edu = pci_get_drvdata(pdev);
+
+ reinit_completion(&edu->irq_done);
+ iowrite32(EDU_STATUS_IRQFACT, edu->regs + EDU_REG_STATUS);
+ iowrite32(600000, edu->regs + EDU_REG_DELAYED_IRQ);
+
+ dev_info(&pdev->dev, "Waiting for IRQ in remove()\n");
+ wait_for_completion(&edu->irq_done);
+ dev_info(&pdev->dev, "Unblocked, cleaning up\n");
+
+ pci_clear_disconnect_work(pdev);
+ free_irq(pci_irq_vector(pdev, 0), edu);
+ pci_free_irq_vectors(pdev);
+ pci_clear_master(pdev);
+ pci_iounmap(pdev, edu->regs);
+ pci_release_regions(pdev);
+ pci_disable_device(pdev);
+}
+
+static const struct pci_device_id edu_ids[] = {
+ { PCI_DEVICE(PCI_VENDOR_ID_EDU, PCI_DEVICE_ID_EDU) },
+ { 0 }
+};
+MODULE_DEVICE_TABLE(pci, edu_ids);
+
+static struct pci_driver edu_driver = {
+ .name = "edu_srpoc",
+ .id_table = edu_ids,
+ .probe = edu_probe,
+ .remove = edu_remove,
+};
+
+module_pci_driver(edu_driver);
+MODULE_AUTHOR("Abhin Parekadan Jose");
+MODULE_DESCRIPTION("edu surprise removal POC driver");
+MODULE_LICENSE("GPL");
--
2.51.1
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH RFC v4 1/5] pci: report surprise removal event
2025-07-03 9:26 [PATCH RFC v4 0/5] pci,virtio: report surprise removal event Michael S. Tsirkin
@ 2025-07-03 9:26 ` Michael S. Tsirkin
0 siblings, 0 replies; 7+ messages in thread
From: Michael S. Tsirkin @ 2025-07-03 9:26 UTC (permalink / raw)
To: linux-kernel
Cc: Lukas Wunner, Keith Busch, Bjorn Helgaas, Parav Pandit,
virtualization, stefanha, alok.a.tiwari, linux-pci
At the moment, in case of a surprise removal, the regular remove
callback is invoked, exclusively. This works well, because mostly, the
cleanup would be the same.
However, there's a race: imagine device removal was initiated by a user
action, such as driver unbind, and it in turn initiated some cleanup and
is now waiting for an interrupt from the device. If the device is now
surprise-removed, that never arrives and the remove callback hangs
forever.
For example, this was reported for virtio-blk:
1. the graceful removal is ongoing in the remove() callback, where disk
deletion del_gendisk() is ongoing, which waits for the requests +to
complete,
2. Now few requests are yet to complete, and surprise removal started.
At this point, virtio block driver will not get notified by the driver
core layer, because it is likely serializing remove() happening by
+user/driver unload and PCI hotplug driver-initiated device removal. So
vblk driver doesn't know that device is removed, block layer is waiting
for requests completions to arrive which it never gets. So
del_gendisk() gets stuck.
Drivers can artificially add timeouts to handle that, but it can be
flaky.
Instead, let's add a way for the driver to be notified about the
disconnect. It can then do any necessary cleanup, knowing that the
device is inactive.
Since cleanups can take a long time, this takes an approach
of a work struct that the driver initiates and enables
on probe, and tears down on remove.
Signed-off-by: Michael S. Tsirkin <mst@redhat.com>
---
drivers/pci/pci.h | 6 ++++++
include/linux/pci.h | 45 +++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 51 insertions(+)
diff --git a/drivers/pci/pci.h b/drivers/pci/pci.h
index b81e99cd4b62..208b4cab534b 100644
--- a/drivers/pci/pci.h
+++ b/drivers/pci/pci.h
@@ -549,6 +549,12 @@ static inline int pci_dev_set_disconnected(struct pci_dev *dev, void *unused)
pci_dev_set_io_state(dev, pci_channel_io_perm_failure);
pci_doe_disconnected(dev);
+ if (READ_ONCE(dev->disconnect_work_enable)) {
+ /* Make sure work is up to date. */
+ smp_rmb();
+ schedule_work(&dev->disconnect_work);
+ }
+
return 0;
}
diff --git a/include/linux/pci.h b/include/linux/pci.h
index 51e2bd6405cd..7fbc377de08a 100644
--- a/include/linux/pci.h
+++ b/include/linux/pci.h
@@ -550,6 +550,10 @@ struct pci_dev {
/* These methods index pci_reset_fn_methods[] */
u8 reset_methods[PCI_NUM_RESET_METHODS]; /* In priority order */
+ /* Report disconnect events. 0x0 - disable, 0x1 - enable */
+ u8 disconnect_work_enable;
+ struct work_struct disconnect_work;
+
#ifdef CONFIG_PCIE_TPH
u16 tph_cap; /* TPH capability offset */
u8 tph_mode; /* TPH mode */
@@ -2657,6 +2661,47 @@ static inline bool pci_is_dev_assigned(struct pci_dev *pdev)
return (pdev->dev_flags & PCI_DEV_FLAGS_ASSIGNED) == PCI_DEV_FLAGS_ASSIGNED;
}
+/*
+ * Run this first thing after getting a disconnect work, to prevent it from
+ * running multiple times.
+ * Returns: true if disconnect was enabled, proceed. false if disabled, abort.
+ */
+static inline bool pci_test_and_clear_disconnect_enable(struct pci_dev *pdev)
+{
+ u8 enable = 0x1;
+ u8 disable = 0x0;
+ return try_cmpxchg(&pdev->disconnect_work_enable, &enable, disable);
+}
+
+/*
+ * Caller must initialize @pdev->disconnect_work before invoking this.
+ * The work function must run and check pci_test_and_clear_disconnect_enable.
+ * Note that device can go away right after this call.
+ */
+static inline void pci_set_disconnect_work(struct pci_dev *pdev)
+{
+ /* Make sure WQ has been initialized already */
+ smp_wmb();
+
+ WRITE_ONCE(pdev->disconnect_work_enable, 0x1);
+
+ /* check the device did not go away meanwhile. */
+ mb();
+
+ if (!pci_device_is_present(pdev))
+ schedule_work(&pdev->disconnect_work);
+}
+
+static inline void pci_clear_disconnect_work(struct pci_dev *pdev)
+{
+ WRITE_ONCE(pdev->disconnect_work_enable, 0x0);
+
+ /* Make sure to stop using work from now on. */
+ smp_wmb();
+
+ cancel_work_sync(&pdev->disconnect_work);
+}
+
/**
* pci_ari_enabled - query ARI forwarding status
* @bus: the PCI bus
--
MST
^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2026-09-27 18:20 UTC | newest]
Thread overview: 7+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-27 18:20 [PATCH RFC v4 0/5] PCI: pciehp: Report surprise removal during safe removal Abhin Parekadan Jose
2026-09-27 18:20 ` [PATCH RFC v4 1/5] PCI: Report surprise removal event Abhin Parekadan Jose
2026-09-27 18:20 ` [PATCH RFC v4 2/5] PCI: pciehp: Add pci_hp_wait_link_change() Abhin Parekadan Jose
2026-09-27 18:20 ` [PATCH RFC v4 3/5] PCI/DPC: Add pci_dpc_wait_recovery() Abhin Parekadan Jose
2026-09-27 18:20 ` [PATCH RFC v4 4/5] PCI: pciehp: Report surprise removal from pciehp_isr() Abhin Parekadan Jose
2026-09-27 18:20 ` [PATCH RFC v4 5/5] misc: Add edu_srpoc surprise removal POC driver Abhin Parekadan Jose
-- strict thread matches above, loose matches on Subject: below --
2025-07-03 9:26 [PATCH RFC v4 0/5] pci,virtio: report surprise removal event Michael S. Tsirkin
2025-07-03 9:26 ` [PATCH RFC v4 1/5] pci: " Michael S. Tsirkin
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®