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_RESTRICTEDROOTprocess that holds/dev/pciopen while another privileged action triggers apci_devqmutation:devctl detachof a PCI driver,kldunload/kldloadof a PCI driver, ACPI / PCIe hotplug event (Thunderbolt / ExpressCard / CardBus), or suspend / resume cycling throughpci_cfg_save/restorepaths 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
dinfomemory can be reclaimed with attacker-controlled data beforeSTAILQ_NEXTdereferences it, potentially a controlled kernel read/write primitive. The race is narrow (must hit the window betweenSTAILQ_REMOVEand 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/kldunloadof 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.
Recommended fix
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
sys/bus/pci/pci_user.c:59βpci_tokendeclaration (static, only referenced inpci_user.c)sys/bus/pci/pci_user.c:332β sole acquire sitesys/bus/pci/pci_user.c:463-467βPCIOCGETCONFSTAILQ_NEXTwalksys/bus/pci/pci.c:353-368βpci_find_dbsfnakedSTAILQ_FOREACHsys/bus/pci/pci.c:576, 594-595βpci_read_deviceinsert + counter bump (no token)sys/bus/pci/pci.c:2155-2162βpci_freecfgremove + free + counter decrement (no token)- CWE-416 Use After Free
Timeline
- 2026-07-14 Discovered during automated audit.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1068 Β· 3 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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/.
Recommended fix
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
fixedVALIDATED 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
Confirmed kernel references
- s
- y
- s
- /
- b
- u
- s
- /
- p
- c
- i
- /
- p
- c
- i
- .
- c
- :
- 2
- 1
- 5
- 5
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.
No comments yet.