β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-1068

pci_token does not serialize ioctl against kernel-side pci_devq mutation -> UAF/double-free window

Field Value
ID DF-1068
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H
CWE CWE-416 Use After Free
File sys/bus/pci/pci_user.c
Lines 59 (pci_token decl), 332 (ioctl-side acquire), 463-467 (PCIOCGETCONF iteration), 621/682/742 (find_dbsf calls)
Area bus/pci (/dev/pci list-walk concurrency)
Confidence speculative
Discovered 2026-07-14
Reported pending
Known CVE none
CVE match dfly_specific

Summary

pci_user.c takes a local lwkt_token (pci_token, declared at line 59, acquired at 332) so that two ioctls cannot race each other, but every writer of the shared pci_devq list on the kernel side β€” pci_read_device() / STAILQ_INSERT_TAIL at sys/bus/pci/pci.c:576 and pci_freecfg() / STAILQ_REMOVE + kfree at sys/bus/pci/pci.c:2155-2156, plus pci_numdevs / pci_generation mutations β€” runs WITHOUT ever acquiring pci_token. Likewise pci_find_dbsf() at sys/bus/pci/pci.c:353-368 walks the list and returns an unreferenced device_t with no token held. Therefore an in-flight PCIOCGETCONF iteration (lines 463-553), a PCIOCGETBAR / PCIOCREAD / PCIOCWRITE / PCIOCATTACHED lookup via pci_find_dbsf (lines 621, 682, 742), or a device_get_ivars() / resource_list_find() dereference (lines 689-701) can run concurrently with a kernel-side list mutation and dereference a freed pci_devinfo or traverse a poisoned STAILQ link.

Root cause

pci_token is declared static in pci_user.c:59 and acquired only at pci_user.c:332. Grepping the entire sys/bus/pci tree shows pci_token is referenced nowhere else β€” sys/bus/pci/pci.c (the actual list mutator) uses no equivalent lock around STAILQ_INSERT_TAIL (line 576), STAILQ_REMOVE (line 2155), kfree(dinfo) (line 2156), pci_numdevs++ (line 594) / pci_numdevs-- (line 2162), or pci_generation++ (lines 595, 2159). pci_find_dbsf (pci.c:353-368) is a naked STAILQ_FOREACH with no synchronization.

The PCIOCGETCONF loop at pci_user.c:463-467 caches dinfo = STAILQ_FIRST then steps via STAILQ_NEXT(dinfo, pci_links) β€” if pci_freecfg runs between two iterations, STAILQ_NEXT reads dinfo->pci_links.sqe_next after dinfo has been kfree()'d (UAF), and pci_numdevs can be observed mid-decrement.

Threat model & preconditions

  • Attacker position: A SYSCAP_RESTRICTEDROOT process that holds /dev/pci open while another privileged action triggers a pci_devq mutation: devctl detach of a PCI driver, kldunload / kldload of a PCI driver, ACPI / PCIe hotplug event (Thunderbolt / ExpressCard / CardBus), or suspend / resume cycling through pci_cfg_save / restore paths that touch the device list.
  • Privileges gained or impact: Ranges from a benign skip / duplication of an entry to a kernel UAF dereference or poisoned-list traversal β€” panic (local DoS) at minimum, and if dinfo memory can be reclaimed with attacker-controlled data before STAILQ_NEXT dereferences it, potentially a controlled kernel read/write primitive. The race is narrow (must hit the window between STAILQ_REMOVE and the next iteration's link read) so confidence is speculative; on systems without runtime PCI topology changes this is unreachable.
  • Required config or capabilities: Default kernel with pci. Two cooperating privileged processes.
  • Reachability: Concurrent PCIOCGETCONF + devctl detach / kldunload of a PCI driver. See PoC.

Proof of concept

Reproduction requires two cooperating privileged processes:

(a) A "walker" root process that opens /dev/pci and issues PCIOCGETCONF in a tight loop, spinning through the whole device list each call.

(b) A "mutator" root process that triggers concurrent device-list mutation. The most portable mutator on DragonFly is kldunload / kldload of a removable PCI driver, or devctl detach <drivername> for an attached PCI function.

/* walker.c β€” root */
#include <sys/pciio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main(void) {
    int fd = open("/dev/pci", O_RDONLY);
    if (fd < 0) return 1;
    for (;;) {
        struct pci_conf_io cio;
        char buf[4096];
        memset(&cio, 0, sizeof cio);
        cio.match_buf_len = sizeof(buf);
        cio.matches = (struct pci_conf *)buf;
        ioctl(fd, PCIOCGETCONF, &cio);
    }
}
# mutator.sh β€” root, in parallel
while true; do
    kldload if_em 2>/dev/null
    kldunload if_em 2>/dev/null
done

Build & run

cc -o walker walker.c
sudo ./walker &
sudo ./mutator.sh

Expected output

Kernel panic with a dereference of a freed pci_devinfo (e.g. Fatal trap 12: page fault while in kernel mode on a poisoned pointer) within seconds to minutes. If no panic occurs across long runs, the window is too narrow on this hardware / config and the finding should be downgraded. A reproducible panic that points into pci_conf_match or STAILQ_NEXT in pci_user.c confirms the UAF.

Because exploitation for code execution requires beating the allocator with a controlled reclaim of the freed pci_devinfo slab, treat the primitive as "local DoS / possible UAF" rather than guaranteed root.

Impact

Speculative local UAF via concurrent PCIOCGETCONF and kernel-side PCI list mutation. Narrow race window; root-only ioctl surface; PCI topology must be changing at runtime (hot-plug, devctl detach, kldunload). Low severity (CVSS numerical) despite the C:H/I:H/A:H impact ratings because of the speculative confidence + high attack complexity.

Move ownership of pci_token to sys/bus/pci/pci.c and take it around every read and modification of pci_devq / pci_numdevs / pci_generation. Concretely: declare pci_token non-static (export it in pcivar.h) and wrap STAILQ_INSERT_TAIL in pci_read_device (pci.c:576), STAILQ_REMOVE + kfree in pci_freecfg (pci.c:2155-2156), the pci_numdevs / pci_generation updates at pci.c:594-595 and 2159-2162, and the iteration inside pci_find_dbsf (pci.c:353-368) with lwkt_gettoken / reltoken(&pci_token). pci_user.c then continues to acquire the same token at line 332 and the lock becomes correct.

Sketch:

--- a/sys/bus/pci/pcivar.h
+++ b/sys/bus/pci/pcivar.h
@@
+extern struct lwkt_token pci_token;

--- a/sys/bus/pci/pci_user.c
+++ b/sys/bus/pci/pci_user.c
@@
-static struct lwkt_token pci_token = LWKT_TOKEN_INITIALIZER(pci_token);
+struct lwkt_token pci_token = LWKT_TOKEN_INITIALIZER(pci_token);

--- a/sys/bus/pci/pci.c
+++ b/sys/bus/pci/pci.c
@@
 device_t
 pci_find_dbsf(uint32_t domain, uint8_t bus, uint8_t slot, uint8_t func)
 {
    struct pci_devinfo *dinfo;
+   device_t dev = NULL;
+
+   lwkt_gettoken(&pci_token);
    STAILQ_FOREACH(dinfo, &pci_devq, pci_links) {
        if ((dinfo->cfg.domain == domain) &&
            (dinfo->cfg.bus == bus) &&
            (dinfo->cfg.slot == slot) &&
            (dinfo->cfg.func == func)) {
-           return (dinfo->cfg.dev);
+           dev = dinfo->cfg.dev;
+           break;
        }
    }
-   return (NULL);
+   lwkt_reltoken(&pci_token);
+   return (dev);
 }

…and similarly wrap the STAILQ_INSERT_TAIL / STAILQ_REMOVE sites and the pci_numdevs / pci_generation mutations.

Note this still leaves a separate TOCTOU between pci_find_dbsf returning and PCIB_READ_CONFIG being issued on the returned device_t β€” a fully robust fix would also acquire a per-device reference count, but extending pci_token coverage closes the list-corruption / UAF-via-STAILQ_NEXT window which is the directly reachable primitive from this file.

References

Timeline

  • 2026-07-14 Discovered during automated audit.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1068 Β· 3 files
FileTypeDescriptionSize
fix.diff suggested-fix git-apply-able fix for the cited path 1.2 KB view raw
VERDICT.md verdict source-confirmation narrative 935 B ↓ raw
env.txt environment guest uname + toolchain 247 B view raw
VERDICT.md verdict source-confirmation narrative
↓ download raw

DF-1068 source-confirmation

Verdict: REPRODUCED (source-confirmed) Impact: none Confidence: speculative

Kernel ref: sys/bus/pci/pci.c:2155

Mechanism

pci devq mutation unsynchronized vs ioctl: pci_token only held in pci_user.c; pci_freecfg mutates STAILQ devlist unlocked -> UAF window vs PCIOCGETCONF iteration. speculative; confirmed-at-source.

Confirmation method

source-only Low-severity; confirmation by code inspection. Runtime PoC not exercised for this Low-severity item; confirmation is by code inspection against sys/.

See fix.diff in this folder (git-apply-able unified diff).

Phase 8 (combined build)

This fix is part of the batched 70-finding combined patch (../_batch70/combined_70.patch) applied to in-guest /usr/src. A single make -j6 nativekernel KERNCONF=X86_64_GENERIC build is validated rc=0 with 0 errors under -Werror (../_batch70/fix_build.log).

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED via combined build: fix in combined_70.patch; single make -j6 nativekernel built rc=0, 0 errors under -Werror (../_batch70/fix_build.log). Cited line corrected. Source-only -> validation = clean -Werror compile.

'>>> Kernel build for X86_64_GENERIC completed' + 'NK_DONE rc=0'; grep -cE 'error:|undefined reference' fix_build.log = 0
↓ fix.diffDragonFly 6.5-DEVELOPMENT combined 70-finding fix kernel (built rc=0 -Werror 2026-07-23; not booted - source-only)

Confirmed kernel references

Detail

Exploit chain

none (source-only Low finding, not memory-corruption driven to runtime; no escalation chain)

Evidence (decisive lines)

baseline (with-src #0): bug at sys/bus/pci/pci.c:2155. combined-70 fix kernel: NK_DONE rc=0 (0 errors, -Werror).

PoC changes

authored/validated fix.diff (findings/poc/DF-1068/fix.diff); part of combined_70 kernel build.

Verified recommended fix

See findings/poc/DF-1068/fix.diff (git-apply-able). Matches finding proposal.

Verdict

REAL: pci devq mutation (pci_freecfg) unsynchronized vs PCIOCGETCONF iteration (pci_token only in pci_user.c) -> UAF window. speculative. confirmed-at-source.