* [PATCH v4] pci_crash: capture PCI config space at panic time
@ 2026-07-23 18:49 hangej
2026-07-23 19:10 ` sashiko-bot
0 siblings, 1 reply; 2+ messages in thread
From: hangej @ 2026-07-23 18:49 UTC (permalink / raw)
To: bhelgaas; +Cc: linux-pci, linux-kernel, dwmw2, kexec, nh-open-source, hangej
From: Johannes Hange <hangej@amazon.com>
Add CONFIG_PCI_CRASH, a crash-time PCI config-space capture facility. A
pre-allocated, RCU-published snapshot of all PCI devices is maintained via
a bus notifier; at panic time pci_crash_save() reads config space into the
buffer using a trylock-based accessor that skips devices on lock
contention. The buffer and a physical-page directory (pagemap) are
exported through VMCOREINFO so crash analysis tools can extract PCI
register state without relying on /proc or sysfs in the crashed kernel.
Key design points:
- Snapshot rebuild is debounced (200 ms) to coalesce VF enumeration
storms; rebuild runs in process context on system_wq.
- Retired snapshots are freed via queue_rcu_work() (process context)
because pci_dev_put() may trigger device_release() ->
devres_release_all() which can sleep -- incompatible with the softirq
context of plain call_rcu() callbacks.
- pci_crash_endpoint_reachable() gates every config read with
software-state checks (pci_dev_is_disconnected, pci_channel_offline,
pci_dev_is_removed, D3cold) plus a live upstream-bridge LNKSTA read
to confirm link presence. The pci_dev_is_removed() check catches
cleanly-removed devices whose host bridge module may have been
unloaded (bus->ops freed) but whose pci_dev struct persists due to
snapshot references.
- capture= module parameter selects 'always' (every panic) or 'aer'
(only when an uncorrectable AER error is detected on a root port).
- devices= parameter filters the capture set by class code or the
keyword 'bridges'.
- Buffer is kvmalloc'd with __GFP_ZERO; capped at 24 MiB, 4096 bytes
per device. A pagemap records physical addresses of each buffer page
for the crash parser.
Introduce pci_dev_is_removed() as a public read-only helper in
include/linux/pci.h, mirroring the existing pci_dev_is_disconnected()
pattern. The PCI_DEV_REMOVED bit (set by pci_destroy_dev during clean
removal) was previously only accessible via drivers/pci/pci.h.
Signed-off-by: Johannes Hange <hangej@amazon.com>
---
Documentation/PCI/index.rst | 1 +
Documentation/PCI/pci-crash-capture.rst | 236 ++++
.../admin-guide/kernel-parameters.txt | 16 +
MAINTAINERS | 8 +
drivers/pci/access.c | 60 +
include/linux/pci.h | 8 +
include/linux/pci_crash.h | 122 ++
kernel/Kconfig.kexec | 16 +
kernel/Makefile | 1 +
kernel/pci_crash.c | 1013 +++++++++++++++++
kernel/vmcore_info.c | 13 +
11 files changed, 1494 insertions(+)
create mode 100644 Documentation/PCI/pci-crash-capture.rst
create mode 100644 include/linux/pci_crash.h
create mode 100644 kernel/pci_crash.c
diff --git a/Documentation/PCI/index.rst b/Documentation/PCI/index.rst
index 5d720d2a415e..7f499a43ddb4 100644
--- a/Documentation/PCI/index.rst
+++ b/Documentation/PCI/index.rst
@@ -19,4 +19,5 @@ PCI Bus Subsystem
endpoint/index
controller/index
boot-interrupts
+ pci-crash-capture
tph
diff --git a/Documentation/PCI/pci-crash-capture.rst b/Documentation/PCI/pci-crash-capture.rst
new file mode 100644
index 000000000000..3a57696afa6f
--- /dev/null
+++ b/Documentation/PCI/pci-crash-capture.rst
@@ -0,0 +1,236 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+========================
+PCI Crash Capture Buffer
+========================
+
+Overview
+========
+
+The PCI crash capture module (``CONFIG_PCI_CRASH``) saves PCI configuration
+space for all (or selected) devices at panic time. The data is written into
+a pre-allocated buffer whose physical pages are exported via VMCOREINFO,
+allowing crash analysis tools to extract device state from the vmcore.
+
+This is useful because AER (Advanced Error Reporting) registers are volatile
+and cleared by device reset during kexec into the crash kernel. Capturing
+them before kexec preserves the error state that caused or contributed to the
+crash.
+
+Boot Parameters
+===============
+
+``pci_crash.capture=`` (default: ``always``)
+ When to capture PCI config space. Comma-separated tokens:
+
+ ``aer``
+ Capture only if a root port reports an uncorrectable error in its
+ AER ROOT_STATUS register. Non-PCI panics skip capture entirely
+ (a handful of MMIO reads to root ports, sub-microsecond).
+
+ ``always``
+ Capture on every panic regardless of AER state. Useful for
+ cascading failures where a PCI link-down causes an MCE or NMI
+ watchdog timeout before DPC/AER fires, so the crash reason is
+ unrelated but the AER registers still hold the originating error.
+
+``pci_crash.devices=`` (default: ``all``)
+ Which devices to include in the capture buffer. Comma-separated tokens:
+
+ ``all``
+ Every PCI device in the system.
+
+ ``bridges``
+ PCI-to-PCI bridges (class 0604) and CardBus bridges (class 0607).
+
+ ``root_ports``
+ PCIe root ports only.
+
+ ``XXYY``
+ Hex PCI class code (class byte XX, subclass byte YY).
+ Up to 8 class codes may be specified.
+
+ Bridges are always implicitly included regardless of the filter value
+ because they hold AER registers needed for root cause analysis. The
+ filter is applied at device enumeration and hotplug rebuild time, not at
+ crash time (zero overhead on the panic path).
+
+Both parameters are writable at runtime via sysfs
+(``/sys/module/pci_crash/parameters/``).
+
+Architecture
+============
+
+::
+
+ late_initcall
+ │
+ ├── register PCI bus notifier (before the first rebuild)
+ ├── enumerate PCI devices (filtered by devices= param)
+ ├── allocate buffer via kvmalloc (may be vmalloc for >4 MiB)
+ ├── build pagemap: kmalloc'd array of per-page physical addresses
+ └── publish snapshot via rcu_assign_pointer()
+
+ hotplug (BUS_NOTIFY_ADD_DEVICE / BUS_NOTIFY_DEL_DEVICE)
+ │
+ └── schedule delayed rebuild (200 ms debounce)
+ └── re-enumerate, re-allocate buffer + pagemap,
+ publish new snapshot, retire old via queue_rcu_work()
+
+ panic (__crash_kexec → crash_save_vmcoreinfo → pci_crash_save)
+ │
+ ├── rcu_read_lock(); sample the published snapshot
+ ├── quick-scan root port AER ROOT_STATUS (capture=aer)
+ │ └── bail if no uncorrectable errors
+ ├── for each device: skip if unreachable, else read config space
+ │ via pci_bus_read_config_dword_trylock()
+ ├── flush dcache (buffer + pagemap) to RAM
+ └── VMCOREINFO exports: PCI_CRASH_PAGEMAP, PCI_CRASH_BUF_SZ,
+ PCI_CRASH_VERSION
+
+Buffer Format
+=============
+
+The buffer consists of a 32-byte header followed by variable-length
+device records:
+
+.. code-block:: c
+
+ struct pci_crash_buffer_header { /* 32 bytes */
+ __le32 magic; /* 0x50434943 "PCIC" */
+ __le32 version; /* 1 */
+ __le32 device_count;
+ __le32 config_size; /* 0 = variable-length records */
+ __le64 timestamp; /* ktime_get_real_fast_ns() */
+ __le32 flags; /* reserved */
+ __le32 reserved;
+ };
+
+ struct pci_crash_device_record { /* 8 + cfg_size bytes */
+ __le16 domain;
+ __u8 bus;
+ __u8 devfn;
+ __le32 config_size; /* 256 or 4096 */
+ __u8 config_data[]; /* 0xffffffff for unreachable dwords */
+ };
+
+The pagemap (exported via ``PCI_CRASH_PAGEMAP``) allows the parser to
+locate buffer pages without walking page tables:
+
+.. code-block:: c
+
+ struct pci_crash_pagemap {
+ __le32 magic; /* 0x5043504d "PCPM" */
+ __le32 num_pages;
+ __le64 buf_size;
+ __le32 buf_offset; /* offset of buffer start within first page */
+ __le64 addrs[]; /* physical address per page */
+ };
+
+All multi-byte fields are little-endian. The struct sizes and the
+``addrs[]`` offset are asserted with ``BUILD_BUG_ON()`` so the on-wire
+layout cannot drift away from the userspace parser silently.
+
+VMCOREINFO keys
+===============
+
+``pci_crash_save()`` exports the following keys into VMCOREINFO (consumed by
+makedumpfile / the crash-utility and any bespoke vmcore parser). They are
+emitted whenever a valid snapshot exists at panic time; the buffer may be
+unfilled when the AER quick-scan found no errors and skipped the capture
+(``capture=aer``). Parsers must check the buffer header magic (``PCIC``)
+to confirm config space was actually captured:
+
+``PCI_CRASH_PAGEMAP=<hex>``
+ Physical address of the ``struct pci_crash_pagemap``. The pagemap is
+ always kmalloc'd (direct-mapped), so this physical address is stable and
+ the parser can read it directly from the vmcore. From the pagemap the
+ parser reconstructs the (possibly vmalloc'd, physically discontiguous)
+ buffer page by page.
+
+``PCI_CRASH_VERSION=<dec>``
+ On-wire format version (``PCI_CRASH_VERSION``). Parsers must reject a
+ version they do not understand rather than misinterpret the layout.
+
+``PCI_CRASH_BUF_SZ=<dec>``
+ Total buffer size in bytes, matching ``pci_crash_pagemap::buf_size``.
+
+Safety Considerations
+=====================
+
+``pci_crash_save()`` runs from ``crash_save_vmcoreinfo()`` inside
+``__crash_kexec()``, before ``machine_kexec()``. It executes in crash
+context, so every access on that path is constrained accordingly:
+
+- **Config reads use** ``pci_bus_read_config_dword_trylock()``, which takes
+ ``pci_lock`` with a *trylock* and skips the device on contention. Depending
+ on ``crash_kexec_post_notifiers`` the other CPUs may still be running or may
+ already be halted (possibly while holding ``pci_lock``), and the panicking
+ CPU may itself have been interrupted mid config access while holding it.
+ ``pci_lock`` is a raw, non-reentrant spinlock, so a blocking acquire could
+ deadlock the dump in either case; the trylock skips the device instead. This
+ avoids the lock deadlock only; it does not make the read itself fault-safe,
+ which is why unreachable devices are skipped first (below).
+
+- **Unreachable devices are skipped before any access to them.** A config
+ read to a device whose PCIe link is physically down can, on some
+ architectures (notably arm64), raise a synchronous external abort. Before
+ reading an endpoint, the module establishes reachability *without touching
+ the endpoint*:
+
+ - software state -- ``pci_dev_is_disconnected()``, ``pci_channel_offline()``,
+ ``pci_dev_is_removed()`` and ``PCI_D3cold`` are flag reads (no MMIO); they
+ catch devices a subsystem has already marked gone or powered off.
+ ``pci_dev_is_removed()`` additionally covers a cleanly-removed device whose
+ ``pci_dev`` is kept alive by a snapshot reference while its host bridge
+ module (and thus ``bus->ops``) may already be freed; and
+
+ - the immediate upstream PCIe port's Link Status (Data Link Layer Link
+ Active). The upstream port is on-die and always responds, so reading its
+ Link Status cannot fault on the endpoint's dead link; if the link is down,
+ the endpoint is skipped and its record is filled with ``0xffffffff``.
+
+- ``ktime_get_real_fast_ns()`` is NMI-safe (lockless timekeeper snapshot).
+
+- **Live capture state is a single RCU-published snapshot.** The rebuild
+ worker (process context) swaps it via ``rcu_assign_pointer()`` and frees the
+ old snapshot via ``queue_rcu_work()`` (the free runs in process context after
+ a grace period, because dropping device references via ``pci_dev_put()`` can
+ sleep); ``pci_crash_save()`` reads it under ``rcu_read_lock()``. RCU keeps the snapshot alive for the *fill*, but the
+ exported buffer/pagemap addresses are consumed after ``pci_crash_save()``
+ returns (the VMCOREINFO export, and ``machine_kexec()`` snapshotting RAM),
+ i.e. after ``rcu_read_unlock()`` -- and on the default panic path peer CPUs
+ are still live and may retire snapshots. ``pci_crash_save()`` therefore
+ *pins* the snapshot it captured (``pci_crash_captured_snap``); the RCU free
+ callback leaks a pinned snapshot instead of freeing it, which is harmless
+ because the system is rebooting into the crash kernel.
+
+- Buffer capped at 24 MiB to bound allocation on systems with thousands of
+ VFs; per-device reads are clamped to 4096 bytes and the fill loop
+ bounds-checks every record against the buffer end.
+
+- ``pci_crash_ready`` defers param parsing and rebuild until ``late_initcall``
+ completes; kernel command-line values are stored and take effect once the
+ PCI subsystem is up.
+
+Architecture support and residual risk
+---------------------------------------
+
+The upstream-port Link-Status pre-check eliminates the common and
+deterministic hang: an endpoint whose link is already down at panic is never
+read. Two narrow residual cases remain on architectures where a config read
+to a dead device raises a fatal abort (e.g. arm64 ``do_sea()``, which has no
+kernel-mode recovery for an external abort):
+
+- a link that drops in the small window *between* the upstream-port check and
+ the endpoint read (a true hardware race); and
+
+- a multi-level fabric collapse in which an upstream port is itself behind a
+ dead link (only the immediate parent is checked).
+
+On x86 a read to an absent device returns all-ones harmlessly, so these cases
+are arm64-specific. Capturing such a device may therefore, in those narrow
+races, abort the dump on arm64. Making the read itself recoverable would
+require new architecture support in the abort handler and is intentionally not
+part of this module; it can be added later as a separate, properly-typed
+arch facility without changing the on-wire format.
diff --git a/Documentation/admin-guide/kernel-parameters.txt b/Documentation/admin-guide/kernel-parameters.txt
index b5493a7f8f22..7ef515c8b849 100644
--- a/Documentation/admin-guide/kernel-parameters.txt
+++ b/Documentation/admin-guide/kernel-parameters.txt
@@ -5298,6 +5298,22 @@ Kernel parameters
nomsi Do not use MSI for native PCIe PME signaling (this makes
all PCIe root ports use INTx for all services).
+ pci_crash.capture=
+ [PCI] When to capture PCI config space at panic time.
+ always (default): capture on every panic.
+ aer: capture only if root port AER reports
+ uncorrectable errors.
+ Requires CONFIG_PCI_CRASH=y.
+
+ pci_crash.devices=
+ [PCI] Which devices to include in crash capture.
+ all (default): every PCI device.
+ bridges: PCI bridges only.
+ root_ports: PCIe root ports only.
+ XXYY: hex class code (up to 8).
+ Bridges always implicitly included.
+ Requires CONFIG_PCI_CRASH=y.
+
pcmv= [HW,PCMCIA] BadgePAD 4
pd_ignore_unused
diff --git a/MAINTAINERS b/MAINTAINERS
index f37a81950e25..47a562820582 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -20560,6 +20560,14 @@ S: Maintained
F: drivers/leds/leds-pca9532.c
F: include/linux/leds-pca9532.h
+PCI CRASH BUFFER
+M: Johannes Hange <hangej@amazon.com>
+L: linux-pci@vger.kernel.org
+S: Maintained
+F: Documentation/PCI/pci-crash-capture.rst
+F: include/linux/pci_crash.h
+F: kernel/pci_crash.c
+
PCI DRIVER FOR AARDVARK (Marvell Armada 3700)
M: Thomas Petazzoni <thomas.petazzoni@bootlin.com>
M: Pali Rohár <pali@kernel.org>
diff --git a/drivers/pci/access.c b/drivers/pci/access.c
index b123da16b63b..c05d02efd602 100644
--- a/drivers/pci/access.c
+++ b/drivers/pci/access.c
@@ -1,5 +1,6 @@
// SPDX-License-Identifier: GPL-2.0
#include <linux/pci.h>
+#include <linux/pci_crash.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/ioport.h>
@@ -27,9 +28,11 @@ DEFINE_RAW_SPINLOCK(pci_lock);
#ifdef CONFIG_PCI_LOCKLESS_CONFIG
# define pci_lock_config(f) do { (void)(f); } while (0)
# define pci_unlock_config(f) do { (void)(f); } while (0)
+# define pci_trylock_config(f) ({ (void)(f); true; })
#else
# define pci_lock_config(f) raw_spin_lock_irqsave(&pci_lock, f)
# define pci_unlock_config(f) raw_spin_unlock_irqrestore(&pci_lock, f)
+# define pci_trylock_config(f) raw_spin_trylock_irqsave(&pci_lock, f)
#endif
#define PCI_OP_READ(size, type, len) \
@@ -85,6 +88,63 @@ EXPORT_SYMBOL(pci_bus_write_config_byte);
EXPORT_SYMBOL(pci_bus_write_config_word);
EXPORT_SYMBOL(pci_bus_write_config_dword);
+#ifdef CONFIG_PCI_CRASH
+/**
+ * pci_bus_read_config_dword_trylock - non-blocking config read for the crash path
+ * @bus: target PCI bus
+ * @devfn: target device/function
+ * @pos: dword-aligned config space offset
+ * @value: result; set to ~0 (PCI "no response") if the read is skipped
+ *
+ * Like pci_bus_read_config_dword() but acquires pci_lock with a trylock instead
+ * of blocking. The PCI crash capture (CONFIG_PCI_CRASH) reads config space from
+ * crash_save_vmcoreinfo() inside __crash_kexec(), which can run while a halted
+ * peer CPU still holds pci_lock, or after this CPU was interrupted mid config
+ * access while holding it. pci_lock is a raw (non-reentrant) spinlock, so a
+ * blocking acquire in either case would spin forever and hang the dump. On
+ * contention this skips the device (value ~0, PCIBIOS_SET_FAILED) instead.
+ *
+ * This only avoids the pci_lock deadlock. On x86 with legacy conf1 or
+ * mmconfig_32 port-I/O, a second blocking lock (pci_config_lock in
+ * arch/x86/pci/common.c) is taken below bus->ops->read() and is NOT covered
+ * by this trylock -- those paths can still hang the dump. arm64 ECAM and
+ * x86-64 MMCONFIG are fully covered (no second lock). The trylock also does
+ * not make the underlying MMIO access fault-tolerant: a read to a device whose
+ * link is down can still raise an external abort, so callers must confirm the
+ * device is reachable first.
+ *
+ * Context: crash/panic path only. Returns 0 on success or a PCIBIOS_* error
+ * (PCIBIOS_SET_FAILED on lock contention).
+ */
+int pci_bus_read_config_dword_trylock(struct pci_bus *bus, unsigned int devfn,
+ int pos, u32 *value)
+{
+ unsigned long flags;
+ u32 data = 0;
+ int res;
+
+ if (pos & 3) {
+ PCI_SET_ERROR_RESPONSE(value);
+ return PCIBIOS_BAD_REGISTER_NUMBER;
+ }
+ if (!bus || !bus->ops || !bus->ops->read) {
+ PCI_SET_ERROR_RESPONSE(value);
+ return PCIBIOS_DEVICE_NOT_FOUND;
+ }
+ if (!pci_trylock_config(flags)) {
+ PCI_SET_ERROR_RESPONSE(value);
+ return PCIBIOS_SET_FAILED;
+ }
+ res = bus->ops->read(bus, devfn, pos, 4, &data);
+ if (res)
+ PCI_SET_ERROR_RESPONSE(value);
+ else
+ *value = data;
+ pci_unlock_config(flags);
+ return res;
+}
+#endif /* CONFIG_PCI_CRASH */
+
int pci_generic_config_read(struct pci_bus *bus, unsigned int devfn,
int where, int size, u32 *val)
{
diff --git a/include/linux/pci.h b/include/linux/pci.h
index 64b308b6e61c..1f6e1cebaecd 100644
--- a/include/linux/pci.h
+++ b/include/linux/pci.h
@@ -2709,6 +2709,14 @@ static inline bool pci_dev_is_disconnected(const struct pci_dev *dev)
return READ_ONCE(dev->error_state) == pci_channel_io_perm_failure;
}
+/* Bit index in pci_dev->priv_flags for device removal state. */
+#define PCI_DEV_REMOVED 3
+
+static inline bool pci_dev_is_removed(const struct pci_dev *dev)
+{
+ return test_bit(PCI_DEV_REMOVED, &dev->priv_flags);
+}
+
void pci_request_acs(void);
bool pci_acs_enabled(struct pci_dev *pdev, u16 acs_flags);
bool pci_acs_path_enabled(struct pci_dev *start,
diff --git a/include/linux/pci_crash.h b/include/linux/pci_crash.h
new file mode 100644
index 000000000000..dc29fe120ba9
--- /dev/null
+++ b/include/linux/pci_crash.h
@@ -0,0 +1,122 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * PCI Crash Buffer - Capture PCI config space at panic time
+ *
+ * This module captures PCI configuration space data (including AER
+ * extended capability registers) for all PCI devices at panic time.
+ * The data is stored in a buffer whose pages are captured in the
+ * vmcore for off-site analysis.
+ *
+ * Copyright (c) 2026 Amazon.com, Inc. or its affiliates.
+ */
+#ifndef _LINUX_PCI_CRASH_H
+#define _LINUX_PCI_CRASH_H
+
+#include <linux/types.h>
+
+#define PCI_CRASH_MAGIC 0x50434943 /* "PCIC" in ASCII */
+#define PCI_CRASH_VERSION 1
+
+/**
+ * struct pci_crash_buffer_header - Header for PCI crash buffer
+ * @magic: Magic number (PCI_CRASH_MAGIC)
+ * @version: Format version (PCI_CRASH_VERSION)
+ * @device_count: Number of device records following this header
+ * @config_size: 0 -- indicates variable-length records. Each device
+ * record stores its own config_size (pdev->cfg_size:
+ * 256 for legacy PCI, 4096 for PCIe). Parsers walk
+ * records sequentially using per-record config_size.
+ * @timestamp: Capture timestamp from ktime_get_real_fast_ns()
+ * @flags: Reserved for future use (0 for now)
+ * @reserved: Padding to align to 32 bytes
+ *
+ * Total size: 32 bytes
+ */
+struct pci_crash_buffer_header {
+ __le32 magic;
+ __le32 version;
+ __le32 device_count;
+ __le32 config_size;
+ __le64 timestamp;
+ __le32 flags;
+ __le32 reserved;
+} __packed;
+
+/**
+ * struct pci_crash_device_record - Per-device record in crash buffer
+ * @domain: PCI domain number
+ * @bus: PCI bus number
+ * @devfn: Device and function number (PCI_DEVFN format)
+ * @config_size: Config space size for this device (pdev->cfg_size:
+ * 256 for legacy PCI, 4096 for PCIe)
+ * @config_data: Raw PCI config space (config_size bytes)
+ *
+ * Records are variable-length: total size per record is
+ * PCI_CRASH_RECORD_META + config_size bytes.
+ */
+struct pci_crash_device_record {
+ __le16 domain;
+ __u8 bus;
+ __u8 devfn;
+ __le32 config_size;
+ __u8 config_data[];
+} __packed;
+
+#define PCI_CRASH_HEADER_SIZE sizeof(struct pci_crash_buffer_header)
+#define PCI_CRASH_RECORD_META sizeof(struct pci_crash_device_record)
+
+/**
+ * struct pci_crash_pagemap - Physical page directory for crash buffer
+ *
+ * The PCI crash buffer may be allocated via vmalloc (for buffers
+ * exceeding ~4 MB where the buddy allocator cannot provide contiguous
+ * pages). virt_to_phys() returns garbage for vmalloc addresses, so
+ * we maintain this small kmalloc'd directory that maps the buffer's
+ * virtual pages to their actual physical addresses.
+ *
+ * At panic time, crash_core.c exports the pagemap's physical address
+ * via VMCOREINFO. The parser reads the pagemap, then reads each
+ * physical page from the vmcore to reconstruct the full buffer.
+ *
+ * The pagemap itself is always kmalloc'd (direct-mapped), so
+ * virt_to_phys() works correctly on it.
+ *
+ * @magic: 0x5043504d ("PCPM") -- validates this is a pagemap
+ * @num_pages: Number of entries in the addrs[] array
+ * @buf_size: Exact buffer size in bytes (last page may be partial)
+ * @buf_offset: Offset of buffer start within the first page
+ * @addrs: Physical address of each PAGE_SIZE page backing the buffer
+ */
+struct pci_crash_pagemap {
+ __le32 magic;
+ __le32 num_pages;
+ __le64 buf_size;
+ __le32 buf_offset;
+ __le64 addrs[];
+} __packed;
+
+#define PCI_CRASH_PAGEMAP_MAGIC 0x5043504d /* "PCPM" in ASCII */
+
+struct pci_bus;
+
+#ifdef CONFIG_PCI_CRASH
+void pci_crash_save(void);
+extern void *pci_crash_buffer;
+extern size_t pci_crash_buffer_size;
+extern phys_addr_t pci_crash_pagemap_phys;
+
+/*
+ * Non-blocking config read used only by the crash capture path; defined in
+ * drivers/pci/access.c (where pci_lock lives). Not exported and not part of
+ * the public PCI API -- it is specific to this feature.
+ */
+int pci_bus_read_config_dword_trylock(struct pci_bus *bus, unsigned int devfn,
+ int pos, u32 *value);
+#else
+static inline void pci_crash_save(void) {}
+#define pci_crash_buffer ((void *)NULL)
+#define pci_crash_buffer_size ((size_t)0)
+#define pci_crash_pagemap_phys ((phys_addr_t)0)
+#endif
+
+#endif /* _LINUX_PCI_CRASH_H */
diff --git a/kernel/Kconfig.kexec b/kernel/Kconfig.kexec
index 15632358bcf7..056767c8ea8d 100644
--- a/kernel/Kconfig.kexec
+++ b/kernel/Kconfig.kexec
@@ -179,4 +179,20 @@ config CRASH_MAX_MEMORY_RANGES
the computation behind the value provided through the
/sys/kernel/crash_elfcorehdr_size attribute.
+
+config PCI_CRASH
+ bool "Capture PCI config space at panic time"
+ depends on VMCORE_INFO && PCI && (ARM64 || X86)
+ help
+ Capture PCI configuration space (including AER extended capability
+ registers) for all PCI devices at panic time. The data is stored
+ in a buffer whose pages are recorded in VMCOREINFO for off-site
+ crash analysis.
+
+ This is useful for diagnosing PCI errors that caused or contributed
+ to the crash, especially when AER registers are volatile and cleared
+ by device reset during kexec.
+
+ If unsure, say Y.
+
endmenu
diff --git a/kernel/Makefile b/kernel/Makefile
index 1e1a31673577..584ab235496e 100644
--- a/kernel/Makefile
+++ b/kernel/Makefile
@@ -82,6 +82,7 @@ obj-$(CONFIG_KEXEC_CORE) += kexec_core.o
obj-$(CONFIG_CRASH_DUMP) += crash_core.o
obj-$(CONFIG_CRASH_DM_CRYPT) += crash_dump_dm_crypt.o
obj-$(CONFIG_CRASH_DUMP_KUNIT_TEST) += crash_core_test.o
+obj-$(CONFIG_PCI_CRASH) += pci_crash.o
obj-$(CONFIG_KEXEC) += kexec.o
obj-$(CONFIG_KEXEC_FILE) += kexec_file.o
obj-$(CONFIG_KEXEC_ELF) += kexec_elf.o
diff --git a/kernel/pci_crash.c b/kernel/pci_crash.c
new file mode 100644
index 000000000000..15d7fa0fc74f
--- /dev/null
+++ b/kernel/pci_crash.c
@@ -0,0 +1,1013 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * PCI Crash Buffer - Capture PCI config space for crash analysis
+ *
+ * Copyright (c) 2026 Amazon.com, Inc. or its affiliates.
+ *
+ * Captures PCI configuration space at crash time so AER error
+ * registers reflect the crash-time state for off-site analysis.
+ *
+ * Design:
+ * - Init (late_initcall): enumerate devices, allocate buffer.
+ * - Hotplug: bus notifier queues deferred rebuild of device list
+ * and buffer via workqueue -- no PCI reads.
+ * - Crash: crash_save_vmcoreinfo() calls pci_crash_save() which
+ * reads config space into buffer, flushes dcache to RAM so
+ * data survives kexec into crash kernel.
+ *
+ * Records are variable-length: each device's record is exactly
+ * 8 + pdev->cfg_size bytes (264 for legacy PCI, 4104 for PCIe).
+ * The parser walks records sequentially using per-record config_size.
+ *
+ * Buffer pages may be physically scattered (kvmalloc falls back to
+ * vmalloc for buffers exceeding ~4 MB). A small kmalloc'd pagemap
+ * records each page's physical address so the crash parser can
+ * reconstruct the buffer without page-table walking.
+ *
+ * Config reads at crash time use pci_bus_read_config_dword_trylock(), which
+ * trylocks pci_lock and skips the device on contention. pci_crash_save() runs
+ * from crash_save_vmcoreinfo() inside __crash_kexec(); depending on
+ * crash_kexec_post_notifiers, peer CPUs may already be halted (possibly while
+ * holding pci_lock) and this CPU may itself hold pci_lock (a panic inside a
+ * config access). pci_lock is a raw, non-reentrant spinlock, so a blocking
+ * acquire would deadlock the dump in either case; the trylock skips instead.
+ * This guards against the lock deadlock only -- a read to an unreachable device
+ * is handled separately by pci_crash_endpoint_reachable().
+ * for_each_pci_dev() needs pci_bus_sem -- only used at init/hotplug.
+ */
+
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/crash_dump.h>
+#include <linux/init.h>
+#include <linux/kernel.h>
+#include <linux/mm.h>
+#include <linux/moduleparam.h>
+#include <linux/mutex.h>
+#include <linux/pci.h>
+#include <linux/pci_crash.h>
+#include <linux/slab.h>
+#include <linux/timekeeping.h>
+#include <linux/workqueue.h>
+#include <linux/rcupdate.h>
+
+#include <linux/cacheflush.h>
+#include <linux/unaligned.h>
+
+/**
+ * pci_crash_flush_dcache() - Flush a memory region from CPU cache to RAM
+ * @addr: virtual address of region to flush
+ * @size: size in bytes
+ *
+ * Used at crash time to ensure the crash kernel sees our buffer/pagemap
+ * writes after kexec.
+ */
+static inline void pci_crash_flush_dcache(void *addr, size_t size)
+{
+ /* Only ARM64 and x86 implemented; Kconfig enforces depends on (ARM64 || X86). */
+#ifdef CONFIG_ARM64
+ unsigned long start = (unsigned long)addr;
+ unsigned long end = start + size;
+
+ dcache_clean_inval_poc(start, end);
+#elif defined(CONFIG_X86)
+ clflush_cache_range(addr, size);
+#else
+#error "CONFIG_PCI_CRASH requires ARM64 or X86 dcache flush support (Kconfig depends)"
+#endif
+}
+
+/*
+ * Live capture state is published as a single RCU-managed snapshot so the
+ * lockless crash-time reader (pci_crash_save) always observes a consistent
+ * {devs, num_devs, buffer, pagemap} set and can never race the rebuild
+ * worker freeing the old arrays. The retired snapshot is reclaimed via
+ * queue_rcu_work() once no reader can hold it -- see pci_crash_rebuild_snapshot().
+ */
+struct pci_crash_snapshot {
+ struct pci_dev **devs;
+ unsigned int num_devs;
+ void *buffer;
+ size_t buffer_size;
+ struct pci_crash_pagemap *pagemap;
+ size_t pagemap_size;
+ phys_addr_t pagemap_phys;
+ struct rcu_work rcu_work;
+};
+
+static struct pci_crash_snapshot __rcu *pci_crash_snap;
+
+/*
+ * Scalars consumed by crash_core.c's crash_save_vmcoreinfo() right AFTER it
+ * calls pci_crash_save(). pci_crash_save() publishes them from the snapshot
+ * it captured; on a skipped or failed capture they are set to 0 so no stale
+ * pagemap is exported into the vmcore.
+ */
+void *pci_crash_buffer;
+EXPORT_SYMBOL_GPL(pci_crash_buffer);
+
+size_t pci_crash_buffer_size;
+EXPORT_SYMBOL_GPL(pci_crash_buffer_size);
+
+phys_addr_t pci_crash_pagemap_phys;
+EXPORT_SYMBOL_GPL(pci_crash_pagemap_phys);
+
+/*
+ * Set by pci_crash_save() to the snapshot it captured, so its buffer/pagemap
+ * (whose addresses are exported into the vmcore via vmcore_info.c) cannot be
+ * reclaimed out from under the crash kernel. pci_crash_save() publishes the
+ * buffer scalars and returns; vmcore_info.c then reads them and machine_kexec()
+ * snapshots RAM -- all AFTER rcu_read_unlock(), so RCU read-side protection has
+ * already ended by the time the buffer matters. A rebuild racing on a live
+ * peer CPU (the default panic path runs __crash_kexec() before halting peers)
+ * could otherwise queue_rcu_work()-free this snapshot before kexec. The free
+ * callback below honours this pin and leaks the snapshot instead -- harmless,
+ * the system is going down. Written under rcu_read_lock() before the scalars
+ * are published, so the grace period ordering guarantees the callback sees it.
+ */
+static struct pci_crash_snapshot *pci_crash_captured_snap;
+
+/*
+ * Reclaim a retired snapshot after a grace period: drop dev refs + free.
+ * Runs via queue_rcu_work() in process context -- pci_dev_put() may trigger
+ * device_release() -> devres_release_all() which can sleep.
+ */
+static void pci_crash_snapshot_free_work(struct work_struct *work)
+{
+ struct pci_crash_snapshot *s =
+ container_of(to_rcu_work(work), struct pci_crash_snapshot, rcu_work);
+ unsigned int i;
+
+ /*
+ * Pinned by an in-progress crash capture: its buffer address is live in
+ * the vmcore export. Leak it rather than free memory kexec will read.
+ */
+ if (READ_ONCE(pci_crash_captured_snap) == s)
+ return;
+
+ for (i = 0; i < s->num_devs; i++)
+ if (s->devs && s->devs[i])
+ pci_dev_put(s->devs[i]);
+ kvfree(s->devs);
+ kvfree(s->buffer);
+ kfree(s->pagemap);
+ kfree(s);
+}
+
+static DEFINE_MUTEX(pci_crash_lock);
+
+/*
+ * Set in pci_crash_init() after delayed_work, PCI bus and notifier are
+ * ready. Guards parse + rebuild in param setters: at boot (level -1)
+ * the setter just stores the string; pci_crash_init() parses and does
+ * the initial rebuild once PCI is up.
+ */
+static bool pci_crash_ready;
+
+/*
+ * capture -- when to capture PCI config space.
+ * Comma-separated tokens:
+ * aer -- root port ROOT_STATUS has uncorrectable errors
+ * always -- every panic regardless of PCI error state (default)
+ *
+ * Writable at runtime (0644) so operators and tests can toggle without
+ * reboot. Writes re-parse capture_flags immediately.
+ */
+#define PCI_CRASH_PARAM_CAPTURE_LEN 32
+static char capture[PCI_CRASH_PARAM_CAPTURE_LEN] = "always";
+
+#define PCI_CRASH_CAPTURE_AER BIT(0)
+#define PCI_CRASH_CAPTURE_ALWAYS BIT(1)
+static unsigned long capture_flags = PCI_CRASH_CAPTURE_ALWAYS;
+
+static void pci_crash_parse_capture(void);
+
+static int capture_param_set(const char *val, const struct kernel_param *kp)
+{
+ char *trimmed;
+
+ if (strlen(val) >= sizeof(capture))
+ return -EINVAL;
+
+ /* Serialize against concurrent sysfs writers mutating the string. */
+ mutex_lock(&pci_crash_lock);
+ strscpy(capture, val, sizeof(capture));
+ trimmed = strim(capture);
+ if (trimmed != capture)
+ memmove(capture, trimmed, strlen(trimmed) + 1);
+ if (READ_ONCE(pci_crash_ready))
+ pci_crash_parse_capture();
+ mutex_unlock(&pci_crash_lock);
+ return 0;
+}
+
+static int capture_param_get(char *buf, const struct kernel_param *kp)
+{
+ return scnprintf(buf, PAGE_SIZE, "%s\n", capture);
+}
+
+static const struct kernel_param_ops capture_param_ops = {
+ .set = capture_param_set,
+ .get = capture_param_get,
+};
+module_param_cb(capture, &capture_param_ops, NULL, 0644);
+MODULE_PARM_DESC(capture, "When to capture: aer, always (default: always)");
+
+/*
+ * devices -- which devices to capture.
+ * Comma-separated tokens:
+ * all -- every PCI device (default)
+ * bridges -- PCI bridges (class 0604, 0607)
+ * root_ports -- PCIe root ports only
+ * XXYY -- hex PCI class code (class + subclass)
+ *
+ * Bridges are always implicitly included regardless of filter value
+ * because they hold the AER registers needed for root cause analysis.
+ * Applies at rebuild time only -- zero cost at crash time. Writable
+ * at runtime (0644); writes re-parse and trigger async rebuild.
+ */
+#define PCI_CRASH_PARAM_DEVICES_LEN 256
+static char devices[PCI_CRASH_PARAM_DEVICES_LEN] = "all";
+
+static void pci_crash_parse_devices(void);
+static struct delayed_work pci_crash_rebuild_dwork;
+
+/* Debounce period for bus notifications (ms).
+ * SR-IOV liveupdate can enumerate ~3000 VFs in ~1.5s -- this coalesces
+ * the storm into a single rebuild after the last event.
+ */
+#define PCI_CRASH_REBUILD_DELAY_MS 200
+
+static int devices_param_set(const char *val, const struct kernel_param *kp)
+{
+ if (strlen(val) >= sizeof(devices))
+ return -EINVAL;
+
+ mutex_lock(&pci_crash_lock);
+ strscpy(devices, val, sizeof(devices));
+ {
+ char *trimmed = strim(devices);
+
+ if (trimmed != devices)
+ memmove(devices, trimmed, strlen(trimmed) + 1);
+ }
+ if (READ_ONCE(pci_crash_ready)) {
+ pci_crash_parse_devices();
+ mod_delayed_work(system_wq, &pci_crash_rebuild_dwork,
+ msecs_to_jiffies(PCI_CRASH_REBUILD_DELAY_MS));
+ }
+ mutex_unlock(&pci_crash_lock);
+ return 0;
+}
+
+static int devices_param_get(char *buf, const struct kernel_param *kp)
+{
+ return scnprintf(buf, PAGE_SIZE, "%s\n", devices);
+}
+
+static const struct kernel_param_ops devices_param_ops = {
+ .set = devices_param_set,
+ .get = devices_param_get,
+};
+module_param_cb(devices, &devices_param_ops, NULL, 0644);
+MODULE_PARM_DESC(devices,
+ "Which devices: all, bridges, root_ports, XXYY hex class (default: all)");
+
+#define PCI_CRASH_DEVICES_ALL BIT(0)
+#define PCI_CRASH_DEVICES_BRIDGES BIT(1)
+#define PCI_CRASH_DEVICES_ROOT_PORTS BIT(2)
+/* Max distinct class-code filters in a devices= list; 8 covers realistic use. */
+#define PCI_CRASH_MAX_DEVICE_CLASSES 8
+static unsigned long devices_flags = PCI_CRASH_DEVICES_ALL;
+static u16 device_classes[PCI_CRASH_MAX_DEVICE_CLASSES];
+static unsigned int device_class_count;
+
+static void pci_crash_parse_capture(void)
+{
+ char *buf, *token, *rest;
+ unsigned long flags = 0;
+
+ if (!*capture) {
+ WRITE_ONCE(capture_flags, PCI_CRASH_CAPTURE_ALWAYS);
+ return;
+ }
+
+ buf = kstrdup(capture, GFP_KERNEL);
+ if (!buf) {
+ WRITE_ONCE(capture_flags, PCI_CRASH_CAPTURE_ALWAYS);
+ return;
+ }
+
+ rest = buf;
+ while ((token = strsep(&rest, ",")) != NULL) {
+ if (strcmp(token, "aer") == 0)
+ flags |= PCI_CRASH_CAPTURE_AER;
+ else if (strcmp(token, "always") == 0)
+ flags |= PCI_CRASH_CAPTURE_ALWAYS;
+ else
+ pr_warn("unknown capture token: %s\n",
+ token);
+ }
+ kfree(buf);
+
+ if (!flags) {
+ pr_warn("no valid capture tokens, defaulting to always\n");
+ flags = PCI_CRASH_CAPTURE_ALWAYS;
+ }
+ WRITE_ONCE(capture_flags, flags);
+}
+
+static void pci_crash_parse_devices(void)
+{
+ char *buf, *token, *rest;
+ unsigned long val;
+
+ devices_flags = 0;
+ device_class_count = 0;
+
+ if (!*devices) {
+ devices_flags = PCI_CRASH_DEVICES_ALL;
+ return;
+ }
+
+ buf = kstrdup(devices, GFP_KERNEL);
+ if (!buf) {
+ devices_flags = PCI_CRASH_DEVICES_ALL;
+ return;
+ }
+
+ rest = buf;
+ while ((token = strsep(&rest, ",")) != NULL) {
+ if (strcmp(token, "all") == 0) {
+ devices_flags |= PCI_CRASH_DEVICES_ALL;
+ } else if (strcmp(token, "bridges") == 0) {
+ devices_flags |= PCI_CRASH_DEVICES_BRIDGES;
+ } else if (strcmp(token, "root_ports") == 0) {
+ devices_flags |= PCI_CRASH_DEVICES_ROOT_PORTS;
+ } else if (kstrtoul(token, 16, &val) == 0 && val <= 0xFFFF) {
+ if (device_class_count < PCI_CRASH_MAX_DEVICE_CLASSES)
+ device_classes[device_class_count++] = (u16)val;
+ else
+ pr_warn("too many device classes (max %d)\n",
+ PCI_CRASH_MAX_DEVICE_CLASSES);
+ } else {
+ pr_warn("unknown devices token: %s\n",
+ token);
+ }
+ }
+ kfree(buf);
+
+ if (!devices_flags && device_class_count == 0) {
+ pr_warn("no valid devices tokens, defaulting to all\n");
+ devices_flags = PCI_CRASH_DEVICES_ALL;
+ }
+}
+
+static bool pci_crash_device_matches(struct pci_dev *pdev)
+{
+ unsigned int i;
+ u16 dev_class = pdev->class >> 8;
+
+ if (devices_flags & PCI_CRASH_DEVICES_ALL)
+ return true;
+
+ /* Bridges always included -- they hold AER registers */
+ if (dev_class == PCI_CLASS_BRIDGE_PCI ||
+ dev_class == PCI_CLASS_BRIDGE_CARDBUS)
+ return true;
+
+ if ((devices_flags & PCI_CRASH_DEVICES_ROOT_PORTS) &&
+ pci_pcie_type(pdev) == PCI_EXP_TYPE_ROOT_PORT)
+ return true;
+
+ for (i = 0; i < device_class_count; i++) {
+ if (dev_class == device_classes[i])
+ return true;
+ }
+
+ return false;
+}
+
+/* Sanity limit -- prevents multi-GB allocations on systems with many VFs */
+#define PCI_CRASH_MAX_BUFFER_SIZE (24 * 1024 * 1024)
+
+/*
+ * PCIe extended config space size. Per-device reads are clamped to this in
+ * case a device's cfg_size is corrupt at crash time.
+ */
+#define PCI_CRASH_MAX_CFG_SIZE 4096
+
+/**
+ * pci_crash_build_pagemap() - Build physical page directory for buffer
+ * @buf: buffer allocated via kvmalloc (may be vmalloc'd)
+ * @buf_size: buffer size in bytes
+ *
+ * Allocates a kmalloc'd directory containing the physical address of
+ * each page backing @buf. The pagemap is always direct-mapped, so
+ * virt_to_phys() works on it at crash time.
+ *
+ * Return: the new pagemap, or NULL on allocation failure.
+ */
+static struct pci_crash_pagemap *pci_crash_build_pagemap(void *buf,
+ size_t buf_size)
+{
+ unsigned int num_pages = DIV_ROUND_UP(offset_in_page(buf) + buf_size, PAGE_SIZE);
+ struct pci_crash_pagemap *pm;
+ unsigned int i;
+
+ pm = kmalloc(struct_size(pm, addrs, num_pages), GFP_KERNEL);
+ if (!pm)
+ return NULL;
+
+ pm->magic = cpu_to_le32(PCI_CRASH_PAGEMAP_MAGIC);
+ pm->num_pages = cpu_to_le32(num_pages);
+ pm->buf_size = cpu_to_le64(buf_size);
+ pm->buf_offset = cpu_to_le32(offset_in_page(buf));
+
+ for (i = 0; i < num_pages; i++) {
+ struct page *page;
+ phys_addr_t pa;
+
+ if (is_vmalloc_addr(buf + i * PAGE_SIZE))
+ page = vmalloc_to_page(buf + i * PAGE_SIZE);
+ else
+ page = virt_to_page(buf + i * PAGE_SIZE);
+
+ if (!page) {
+ kfree(pm);
+ return NULL;
+ }
+ pa = page_to_phys(page);
+ pm->addrs[i] = cpu_to_le64(pa);
+ }
+
+ return pm;
+}
+
+/**
+ * pci_crash_endpoint_reachable() - Decide if @pdev is safe to read at crash time
+ * @pdev: PCI device about to be read
+ *
+ * A config read to a device whose PCIe link is physically down can, on some
+ * architectures (notably arm64), raise a synchronous external abort. In the
+ * crash path that abort is unrecoverable -- the arm64 SEA handler (do_sea())
+ * has no kernel-mode fixup and calls arm64_notify_die(), double-faulting the
+ * panic and hanging the very dump we are trying to produce. On x86 such a
+ * read returns all-ones harmlessly. So before touching an endpoint we must
+ * establish reachability WITHOUT reading the endpoint itself.
+ *
+ * Two cheap, panic-safe signals are used, in order:
+ *
+ * 1. Software state -- pci_dev_is_disconnected(), pci_channel_offline(),
+ * pci_dev_is_removed() and PCI_D3cold are pure flag reads (no MMIO). They
+ * catch devices a subsystem has already marked gone (hotplug remove, failed
+ * AER/DPC recovery, powered-off). pci_dev_is_removed() also covers a
+ * cleanly-removed device whose pci_dev is pinned by a snapshot reference
+ * while its host bridge module (hence bus->ops) may already be freed.
+ * They are necessary but not sufficient: in the
+ * cascading-failure window this feature targets, a link can be down before
+ * any subsystem has updated error_state, so these flags can still read
+ * "live".
+ *
+ * 2. Parent-bridge link state -- read PCI_EXP_LNKSTA on the immediate upstream
+ * PCIe port and test Data Link Layer Link Active (DLLLA). The upstream
+ * port is on-die and always responds, so reading *its* config space cannot
+ * raise an abort caused by the endpoint's dead link. If DLLLA is clear,
+ * the endpoint is unreachable and is skipped without ever being touched.
+ * The bridge read uses the crash-safe trylock accessor so it cannot hang
+ * on pci_lock either.
+ *
+ * Return: false if @pdev should be skipped (record filled with 0xFFFFFFFF).
+ *
+ * Residual: a single immediate-parent check; a link that drops between this
+ * check and the read (TOCTOU), or a multi-level fabric collapse where the
+ * upstream port itself sits behind a dead link, is not covered here. See
+ * Documentation/PCI/pci-crash-capture.rst for the documented arm64 caveat.
+ */
+static bool pci_crash_endpoint_reachable(struct pci_dev *pdev)
+{
+ struct pci_dev *bridge;
+ u32 lnksta = 0;
+ u16 sta;
+ int pos;
+
+ /* Software-state flags: pure reads, no MMIO -- always panic-safe. */
+ if (pci_dev_is_disconnected(pdev) || pci_channel_offline(pdev) ||
+ pci_dev_is_removed(pdev) ||
+ pdev->current_state == PCI_D3cold)
+ return false;
+
+ /*
+ * Live link check via the immediate upstream PCIe port. Only meaningful
+ * for a device sitting below a PCIe downstream/root port; for non-PCIe
+ * or root-complex-integrated devices there is no such link to test, so
+ * treat them as reachable and let the crash-safe accessor handle the read.
+ */
+ bridge = pci_upstream_bridge(pdev);
+ if (bridge && pci_is_pcie(bridge) &&
+ (pci_pcie_type(bridge) == PCI_EXP_TYPE_ROOT_PORT ||
+ pci_pcie_type(bridge) == PCI_EXP_TYPE_DOWNSTREAM) &&
+ bridge->pcie_cap && bridge->bus) {
+ pos = bridge->pcie_cap + PCI_EXP_LNKSTA;
+ /*
+ * Read the aligned dword containing LNKSTA from the on-die
+ * upstream port (always present), then extract the 16-bit field.
+ *
+ * Fail closed: if the bridge read cannot complete -- lock
+ * contention (PCIBIOS_SET_FAILED) or any PCIBIOS error -- we
+ * cannot prove the link is up, so treat the endpoint as
+ * unreachable rather than issuing an MMIO read that may raise a
+ * fatal external abort. Contention is exactly a crash-path
+ * condition we assume is likely, so "unknown" must mean "skip".
+ */
+ if (pci_bus_read_config_dword_trylock(bridge->bus, bridge->devfn,
+ pos & ~0x3, &lnksta) != 0)
+ return false;
+
+ sta = (pos & 0x2) ? (lnksta >> 16) : (lnksta & 0xffff);
+ if (!(sta & PCI_EXP_LNKSTA_DLLLA))
+ return false; /* link down -> endpoint gone */
+ }
+
+ return true;
+}
+
+/**
+ * pci_crash_read_config_space() - Read config space for one device
+ * @pdev: PCI device to read
+ * @ptr: destination pointer within the crash buffer
+ * @cfg_size: number of config bytes to read (already clamped by the caller)
+ *
+ * Reads @cfg_size bytes one dword at a time. Devices deemed unreachable by
+ * pci_crash_endpoint_reachable() (disconnected, in error recovery, powered
+ * off, or behind a down PCIe link) are skipped without being touched: on x86
+ * such a read returns all-ones harmlessly, but on other architectures (e.g.
+ * arm64) it can raise a fatal external abort that double-faults the panic
+ * path. Skipped or failed reads store 0xFFFFFFFF -- the standard PCI
+ * convention for absent/unreachable registers.
+ */
+static void pci_crash_read_config_space(struct pci_dev *pdev, u8 *ptr,
+ unsigned int cfg_size)
+{
+ struct pci_crash_device_record *record =
+ (struct pci_crash_device_record *)ptr;
+ u8 *cfg_data = ptr + PCI_CRASH_RECORD_META;
+ unsigned int offset;
+ u32 val;
+
+ /* Defensive: never trust cfg_size, never deref a torn-down bus. */
+ if (cfg_size > PCI_CRASH_MAX_CFG_SIZE)
+ cfg_size = PCI_CRASH_MAX_CFG_SIZE;
+
+ if (!pdev->bus) {
+ record->domain = 0;
+ record->bus = 0;
+ record->devfn = pdev->devfn;
+ record->config_size = cpu_to_le32(cfg_size);
+ memset(cfg_data, 0xff, cfg_size);
+ return;
+ }
+
+ record->domain = cpu_to_le16(pci_domain_nr(pdev->bus));
+ record->bus = pdev->bus->number;
+ record->devfn = pdev->devfn;
+ record->config_size = cpu_to_le32(cfg_size);
+
+ if (!pci_crash_endpoint_reachable(pdev)) {
+ memset(cfg_data, 0xff, cfg_size);
+ return;
+ }
+
+ for (offset = 0; offset < cfg_size; offset += 4) {
+ if (pci_bus_read_config_dword_trylock(pdev->bus, pdev->devfn,
+ offset, &val)) {
+ put_unaligned_le32(0xFFFFFFFF, &cfg_data[offset]);
+ continue;
+ }
+ put_unaligned_le32(val, &cfg_data[offset]);
+ }
+}
+
+/**
+ * pci_crash_fill_buffer() - Populate buffer with config space
+ * @s: snapshot whose buffer is filled from its captured device list
+ *
+ * Records are variable-length: each is PCI_CRASH_RECORD_META +
+ * pdev->cfg_size bytes. The header's config_size is 0 to indicate
+ * variable-length; parsers walk records using per-record config_size.
+ *
+ * Uses ktime_get_real_fast_ns() for the timestamp -- safe in NMI/panic
+ * context (lockless, reads the NMI-safe timekeeper snapshot).
+ *
+ * Caller holds rcu_read_lock() so @s stays valid for the whole fill.
+ */
+static void pci_crash_fill_buffer(struct pci_crash_snapshot *s)
+{
+ struct pci_crash_buffer_header *header = s->buffer;
+ struct pci_dev **devs = s->devs;
+ u8 *ptr, *end;
+ unsigned int i;
+
+ header->magic = cpu_to_le32(PCI_CRASH_MAGIC);
+ header->version = cpu_to_le32(PCI_CRASH_VERSION);
+ header->device_count = cpu_to_le32(s->num_devs);
+ header->config_size = 0;
+ header->timestamp = cpu_to_le64(ktime_get_real_fast_ns());
+ header->flags = 0;
+ header->reserved = 0;
+
+ ptr = (u8 *)s->buffer + PCI_CRASH_HEADER_SIZE;
+ end = (u8 *)s->buffer + s->buffer_size;
+ for (i = 0; i < s->num_devs; i++) {
+ struct pci_dev *pdev = devs[i];
+ unsigned int cfg_size;
+ size_t rec_size;
+
+ if (unlikely(!pdev))
+ break;
+
+ cfg_size = pdev->cfg_size;
+ if (cfg_size > PCI_CRASH_MAX_CFG_SIZE)
+ cfg_size = PCI_CRASH_MAX_CFG_SIZE;
+ rec_size = PCI_CRASH_RECORD_META + cfg_size;
+
+ /*
+ * Never write past the buffer if the device set or a device's
+ * cfg_size grew since the buffer was sized at rebuild time.
+ */
+ if (unlikely(ptr + rec_size > end))
+ break;
+
+ pci_crash_read_config_space(pdev, ptr, cfg_size);
+ ptr += rec_size;
+ }
+
+ header->device_count = cpu_to_le32(i);
+}
+
+/**
+ * pci_crash_rebuild_snapshot() - Rebuild device list and allocate buffer
+ *
+ * Two-pass approach:
+ * Pass 1: count PCI devices
+ * Pass 2: populate device array (filtered) and compute exact buffer
+ * size from actual pdev->cfg_size per device (no padding)
+ *
+ * The devices param controls which devices are included. Bridges are
+ * always included regardless of devices setting (they hold AER registers).
+ * devices=all (default) includes everything.
+ *
+ * Does NOT read PCI config space -- reads happen only at crash time.
+ * This keeps rebuild fast during VF enumeration storms (~6000 ADD
+ * events on large accelerator hosts during liveupdate).
+ *
+ * After allocation, builds the pagemap so the crash parser can
+ * locate the buffer's physical pages in the vmcore.
+ *
+ * Publishes the new snapshot via rcu_assign_pointer() and retires the
+ * previous one via queue_rcu_work(), so the lockless crash-time reader never
+ * sees a half-updated state or a freed array.
+ *
+ * Caller must hold pci_crash_lock.
+ */
+static void pci_crash_rebuild_snapshot(void)
+{
+ struct pci_crash_snapshot *old, *new;
+ struct pci_dev *pdev = NULL;
+ unsigned int count = 0, i;
+ size_t total_size;
+
+ old = rcu_dereference_protected(pci_crash_snap,
+ lockdep_is_held(&pci_crash_lock));
+
+ /* Pass 1: count devices (upper bound). */
+ for_each_pci_dev(pdev)
+ count++;
+
+ new = kzalloc(sizeof(*new), GFP_KERNEL);
+ if (!new) {
+ pr_warn_ratelimited("snapshot alloc failed; keeping previous (capture may be stale)\n");
+ return;
+ }
+ INIT_RCU_WORK(&new->rcu_work, pci_crash_snapshot_free_work);
+
+ if (count == 0) {
+ pr_info("no PCI devices found\n");
+ goto publish; /* publish an empty snapshot */
+ }
+
+ new->devs = kvmalloc_array(count, sizeof(*new->devs),
+ GFP_KERNEL | __GFP_ZERO);
+ if (!new->devs) {
+ kfree(new);
+ pr_warn_ratelimited("devs alloc failed; keeping previous (capture may be stale)\n");
+ return;
+ }
+
+ /*
+ * Pass 2: populate filtered device array and compute the exact
+ * buffer size. count (pass 1) is an upper bound; actual may be less.
+ */
+ total_size = PCI_CRASH_HEADER_SIZE;
+ pdev = NULL;
+ i = 0;
+ for_each_pci_dev(pdev) {
+ if (i >= count) {
+ pci_dev_put(pdev);
+ break;
+ }
+ if (!pci_crash_device_matches(pdev))
+ continue;
+ new->devs[i] = pci_dev_get(pdev);
+ total_size += PCI_CRASH_RECORD_META + pdev->cfg_size;
+ i++;
+ }
+ new->num_devs = i;
+
+ if (new->num_devs == 0) {
+ /* Publish empty: releases the previous (now stale) device set. */
+ kvfree(new->devs);
+ new->devs = NULL;
+ pr_info("no devices match devices=%s\n", devices);
+ goto publish;
+ }
+
+ if (total_size > PCI_CRASH_MAX_BUFFER_SIZE) {
+ pr_warn_ratelimited("buffer too large (%zu > %d bytes); keeping previous snapshot (capture may be stale)\n",
+ total_size, PCI_CRASH_MAX_BUFFER_SIZE);
+ goto err_free_devs;
+ }
+
+ new->buffer = kvmalloc(total_size, GFP_KERNEL | __GFP_ZERO);
+ if (!new->buffer)
+ goto err_free_devs;
+ new->buffer_size = total_size;
+
+ new->pagemap = pci_crash_build_pagemap(new->buffer, total_size);
+ if (!new->pagemap)
+ goto err_free_buf;
+ new->pagemap_size = struct_size(new->pagemap, addrs,
+ le32_to_cpu(new->pagemap->num_pages));
+ new->pagemap_phys = virt_to_phys(new->pagemap);
+
+ pr_info("rebuild: %u devices (%zu bytes, %u pages)\n",
+ new->num_devs, total_size,
+ le32_to_cpu(new->pagemap->num_pages));
+
+publish:
+ /*
+ * Publish the new snapshot and retire the old one. Readers in
+ * pci_crash_save() hold rcu_read_lock(), so queue_rcu_work() defers
+ * the old snapshot's frees until a grace period elapses, then runs
+ * the free in process context (pci_dev_put may sleep via
+ * device_release -> devres_release_all).
+ */
+ rcu_assign_pointer(pci_crash_snap, new);
+ if (old)
+ WARN_ON(!queue_rcu_work(system_wq, &old->rcu_work));
+ return;
+
+err_free_buf:
+ kvfree(new->buffer);
+err_free_devs:
+ for (i = 0; i < new->num_devs; i++)
+ pci_dev_put(new->devs[i]);
+ kvfree(new->devs);
+ kfree(new);
+ /*
+ * Allocation failed building the new snapshot. Keep the existing
+ * snapshot live (do not publish) so capture still works with the
+ * prior device set; warn (ratelimited) so persistent failures show.
+ */
+ pr_warn_ratelimited("rebuild failed; keeping previous snapshot (capture may be stale)\n");
+}
+
+#ifdef CONFIG_PCIEAER
+/*
+ * Quick-scan root ports for a received uncorrectable AER error -- the signal
+ * that this panic is PCI-related and worth capturing.
+ *
+ * Return: true on the first root port whose ROOT_STATUS reports an
+ * uncorrectable error.
+ */
+static bool pci_crash_aer_error_present(struct pci_crash_snapshot *s)
+{
+ unsigned int i;
+
+ for (i = 0; i < s->num_devs; i++) {
+ struct pci_dev *pdev = s->devs[i];
+ u32 status = 0;
+
+ if (!pdev || !pdev->aer_cap)
+ continue;
+ if (pci_pcie_type(pdev) != PCI_EXP_TYPE_ROOT_PORT)
+ continue;
+ /*
+ * Same reachability gate as the capture path. A root port is
+ * on-die (no upstream bridge), so this reduces to the software-
+ * state flags -- we read the port's own AER registers, never an
+ * endpoint behind a potentially-dead link.
+ */
+ if (!pci_crash_endpoint_reachable(pdev))
+ continue;
+
+ /*
+ * Fail closed, like the reachability check: a failed read
+ * (lock contention or PCIBIOS error) sets status to ~0, which
+ * would falsely test as "uncorrectable error present" and force
+ * a pointless full capture. Unknown means "no error seen".
+ */
+ if (pci_bus_read_config_dword_trylock(pdev->bus, pdev->devfn,
+ pdev->aer_cap + PCI_ERR_ROOT_STATUS,
+ &status) != 0)
+ continue;
+ if (status & PCI_ERR_ROOT_UNCOR_RCV)
+ return true;
+ }
+ return false;
+}
+#else
+static inline bool pci_crash_aer_error_present(struct pci_crash_snapshot *s)
+{
+ return false;
+}
+#endif
+
+/**
+ * pci_crash_save() - Capture PCI config space at crash time
+ *
+ * Called from crash_save_vmcoreinfo() inside __crash_kexec(), which
+ * runs before machine_kexec() boots the crash kernel. This is the
+ * only reliable capture point -- panic notifiers run AFTER kexec by
+ * default (crash_kexec_post_notifiers=0).
+ *
+ * Capture check (capture param):
+ * always -- capture unconditionally
+ * aer -- quick-scan root port AER ROOT_STATUS for uncorrectable
+ * errors; skip if none found
+ *
+ * When capture=always, captures on every panic.
+ * This is useful for cascading failures: a PCI link-down can cause
+ * an MCE or NMI watchdog timeout before DPC/AER fires, so the crash
+ * reason is UNKNOWN but AER registers may still hold error state.
+ *
+ * Reads config space fresh -- successful reads get current register
+ * state, failed reads (offline devices) write 0xFFFFFFFF.
+ *
+ * Flushes both buffer and pagemap from CPU cache to RAM so data
+ * survives kexec into crash kernel.
+ *
+ * Runs under rcu_read_lock(): a rebuild worker may still be mid-flight on a
+ * peer CPU, so RCU keeps the sampled snapshot alive for the whole capture.
+ */
+void pci_crash_save(void)
+{
+ struct pci_crash_snapshot *s;
+ unsigned long cflags;
+
+ /* Cleared first; set only on a successful capture below. */
+ pci_crash_buffer = NULL;
+ pci_crash_buffer_size = 0;
+ pci_crash_pagemap_phys = 0;
+
+ rcu_read_lock();
+ s = rcu_dereference(pci_crash_snap);
+ if (!s || s->num_devs == 0)
+ goto out;
+ if (!s->buffer || s->buffer_size == 0)
+ goto out;
+
+ /*
+ * Pin this snapshot so a rebuild racing on a live peer CPU cannot
+ * queue_rcu_work()-free its buffer/pagemap before machine_kexec() snapshots
+ * RAM. The scalars below are read by vmcore_info.c AFTER we return and
+ * rcu_read_unlock() -- i.e. after RCU read-side protection has ended --
+ * so RCU alone does not keep the buffer alive that long. Written here,
+ * under rcu_read_lock() and before the scalars are published; the free
+ * callback honours it (see pci_crash_snapshot_free_work).
+ */
+ WRITE_ONCE(pci_crash_captured_snap, s);
+
+ /*
+ * Publish the buffer location now -- before the AER quick-scan that may
+ * skip the capture -- so vmcore_info.c always exports a valid (possibly
+ * empty) buffer. vmcore_info.c reads only these scalars, immediately
+ * after we return and still inside __crash_kexec() before
+ * machine_kexec().
+ */
+ pci_crash_buffer = s->buffer;
+ pci_crash_buffer_size = s->buffer_size;
+ pci_crash_pagemap_phys = s->pagemap_phys;
+
+ cflags = READ_ONCE(capture_flags);
+ if (!(cflags & PCI_CRASH_CAPTURE_ALWAYS)) {
+ if (!(cflags & PCI_CRASH_CAPTURE_AER)) {
+ /* Neither 'always' nor a usable 'aer' mode -- skip. */
+ goto out;
+ }
+ if (!pci_crash_aer_error_present(s)) {
+ pr_info("no PCI errors detected, skipping capture\n");
+ goto out;
+ }
+ }
+
+ pci_crash_fill_buffer(s);
+
+ /*
+ * Flush buffer and pagemap from CPU cache to RAM so the
+ * crash kernel sees our writes after kexec.
+ */
+ pci_crash_flush_dcache(s->buffer, s->buffer_size);
+ if (s->pagemap && s->pagemap_size > 0)
+ pci_crash_flush_dcache(s->pagemap, s->pagemap_size);
+
+ pr_info("CAPTURE: %u devices, %zu bytes\n",
+ s->num_devs, s->buffer_size);
+out:
+ rcu_read_unlock();
+}
+EXPORT_SYMBOL_GPL(pci_crash_save);
+
+static void pci_crash_rebuild_worker(struct work_struct *work)
+{
+ mutex_lock(&pci_crash_lock);
+ pci_crash_rebuild_snapshot();
+ mutex_unlock(&pci_crash_lock);
+}
+
+static int pci_crash_bus_notifier(struct notifier_block *nb,
+ unsigned long action, void *data)
+{
+ if (action == BUS_NOTIFY_ADD_DEVICE ||
+ action == BUS_NOTIFY_DEL_DEVICE)
+ mod_delayed_work(system_wq, &pci_crash_rebuild_dwork,
+ msecs_to_jiffies(PCI_CRASH_REBUILD_DELAY_MS));
+
+ return NOTIFY_OK;
+}
+
+static struct notifier_block pci_crash_bus_nb = {
+ .notifier_call = pci_crash_bus_notifier,
+};
+
+static int __init pci_crash_init(void)
+{
+ /*
+ * The on-wire buffer/pagemap layout is shared with userspace vmcore
+ * parsers, which hardcode these sizes. Catch any struct drift at
+ * build time.
+ */
+ BUILD_BUG_ON(sizeof(struct pci_crash_buffer_header) != 32);
+ BUILD_BUG_ON(sizeof(struct pci_crash_device_record) != 8);
+ BUILD_BUG_ON(offsetof(struct pci_crash_pagemap, addrs) != 20);
+
+ /* Nothing to do in crash kernel -- the buffer from the first kernel
+ * is already in RAM (flushed before kexec) and the parser finds it
+ * via the pagemap in VMCOREINFO.
+ */
+ if (is_kdump_kernel())
+ return 0;
+
+ INIT_DELAYED_WORK(&pci_crash_rebuild_dwork, pci_crash_rebuild_worker);
+
+ pci_crash_parse_capture();
+ pci_crash_parse_devices();
+
+ /*
+ * Register the hotplug notifier BEFORE the initial snapshot so no
+ * ADD/DEL event in the startup window is missed. The notifier only
+ * schedules the debounced rebuild worker, which serializes on
+ * pci_crash_lock behind this initial rebuild.
+ */
+ bus_register_notifier(&pci_bus_type, &pci_crash_bus_nb);
+
+ mutex_lock(&pci_crash_lock);
+ pci_crash_rebuild_snapshot();
+ mutex_unlock(&pci_crash_lock);
+
+ WRITE_ONCE(pci_crash_ready, true);
+
+#ifndef CONFIG_PCIEAER
+ if ((capture_flags & PCI_CRASH_CAPTURE_AER) &&
+ !(capture_flags & PCI_CRASH_CAPTURE_ALWAYS))
+ pr_warn("capture=aer but CONFIG_PCIEAER=n; capture will not trigger unless set to 'always'\n");
+#endif
+
+ rcu_read_lock();
+ {
+ struct pci_crash_snapshot *s = rcu_dereference(pci_crash_snap);
+
+ pr_info("ready: %u devices (%zu bytes), capture=%s devices=%s\n",
+ s ? s->num_devs : 0, s ? s->buffer_size : 0,
+ capture, devices);
+ }
+ rcu_read_unlock();
+
+ return 0;
+}
+late_initcall(pci_crash_init);
+
+/* Built-in only: crash infrastructure must outlive all drivers. */
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("Capture PCI config space at panic time for crash analysis");
+MODULE_AUTHOR("Amazon.com, Inc.");
diff --git a/kernel/vmcore_info.c b/kernel/vmcore_info.c
index 8614430ca212..8813f7e2e516 100644
--- a/kernel/vmcore_info.c
+++ b/kernel/vmcore_info.c
@@ -14,6 +14,7 @@
#include <linux/cpuhotplug.h>
#include <linux/memblock.h>
#include <linux/kmemleak.h>
+#include <linux/pci_crash.h>
#include <asm/page.h>
#include <asm/sections.h>
@@ -91,6 +92,18 @@ void crash_save_vmcoreinfo(void)
vmcoreinfo_data = vmcoreinfo_data_safecopy;
vmcoreinfo_append_str("CRASHTIME=%lld\n", ktime_get_real_seconds());
+
+ /* Capture PCI config space before kexec into crash kernel */
+ pci_crash_save();
+ if (pci_crash_pagemap_phys && pci_crash_buffer_size > 0) {
+ vmcoreinfo_append_str("PCI_CRASH_PAGEMAP=0x%llx\n",
+ (unsigned long long)pci_crash_pagemap_phys);
+ vmcoreinfo_append_str("PCI_CRASH_VERSION=%d\n",
+ PCI_CRASH_VERSION);
+ vmcoreinfo_append_str("PCI_CRASH_BUF_SZ=%zu\n",
+ pci_crash_buffer_size);
+ }
+
update_vmcoreinfo_note();
}
--
2.47.3
^ permalink raw reply related [flat|nested] 2+ messages in thread
* Re: [PATCH v4] pci_crash: capture PCI config space at panic time
2026-07-23 18:49 [PATCH v4] pci_crash: capture PCI config space at panic time hangej
@ 2026-07-23 19:10 ` sashiko-bot
0 siblings, 0 replies; 2+ messages in thread
From: sashiko-bot @ 2026-07-23 19:10 UTC (permalink / raw)
To: hangej; +Cc: linux-pci
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] An out-of-bounds write corrupts the crash capture buffer if a device's cfg_size is not a 4-byte multiple.
--
--- Patch [1]: [PATCH v4] pci_crash: capture PCI config space at panic time ---
commit 453754eacce8780b38337ecf239a5ffdf63c4b25
Author: Johannes Hange <hangej@amazon.com>
pci_crash: capture PCI config space at panic time
This commit introduces a facility to capture PCI configuration space during
a kernel panic. It allocates a snapshot buffer updated via bus notifiers and
fills it using non-blocking config reads when a panic occurs. The resulting
buffer and pagemap are exported through VMCOREINFO for crash analysis.
> diff --git a/kernel/pci_crash.c b/kernel/pci_crash.c
> new file mode 100644
> --- /dev/null
> +++ b/kernel/pci_crash.c
[ ... ]
> +static void pci_crash_read_config_space(struct pci_dev *pdev, u8 *ptr,
> + unsigned int cfg_size)
> +{
[ ... ]
> + if (!pci_crash_endpoint_reachable(pdev)) {
> + memset(cfg_data, 0xff, cfg_size);
> + return;
> + }
> +
> + for (offset = 0; offset < cfg_size; offset += 4) {
[Severity: Medium]
Could this loop write out of bounds if cfg_size is unaligned?
Since this code executes during a panic where memory corruption is common,
pdev->cfg_size could potentially be corrupted to a value that is not a
multiple of 4. If that happens, the loop will unconditionally write 4 bytes
on its final iteration, which could overflow the allocated payload slice
for this device.
This might either corrupt the adjacent record's header or cause a double
fault if the final write crosses a vmalloc page boundary.
Would it make sense to align cfg_size down to a 4-byte boundary before
iterating, or clamp the final write to prevent it from going past the end
of the buffer?
> + if (pci_bus_read_config_dword_trylock(pdev->bus, pdev->devfn,
> + offset, &val)) {
> + put_unaligned_le32(0xFFFFFFFF, &cfg_data[offset]);
> + continue;
> + }
> + put_unaligned_le32(val, &cfg_data[offset]);
> + }
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260723185216.1089927-1-hangej@amazon.com?part=1
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-07-23 19:10 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-23 18:49 [PATCH v4] pci_crash: capture PCI config space at panic time hangej
2026-07-23 19:10 ` sashiko-bot
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.