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

Kernel pointer leak via DIOCIGETIFACES wholesale copyout of struct pfi_kif

Field Value
ID DF-0603
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/net/pf/pf_if.c
Lines 763-790
Area net/pf (firewall interface tracking)
Confidence certain
Discovered 2026-07-02
Reported pending

Summary

pfi_get_ifaces() copies the entire in-kernel struct pfi_kif to userspace via copyout(p, buf++, sizeof(*buf)) at line 778. The struct (sys/net/pf/pfvar.h:1171-1183) contains raw kernel pointers β€” pfik_tree.rbe_left/rbe_right/rbe_parent, pfik_ifp, pfik_group, and pfik_dynaddrs.tqh_first/tqh_last β€” all of which are emitted verbatim. DIOCIGETIFACES only requires the caller to have opened /dev/pf (which is checked at pf_ioctl.c:3022 to take exactly sizeof(struct pfi_kif)), so the kernel ABI guarantees the leak. 6–7 live kernel heap/text pointers per kif are returned; iterating over all kifs yields a comprehensive KASLR / heap-layout oracle.

Root cause

sys/net/pf/pf_if.c:763-790:

763: int
764: pfi_get_ifaces(const char *name, struct pfi_kif *buf, int *size)
765: {
...
778:        if (copyout(p, buf++, sizeof(*buf))) {

p is a struct pfi_kif * taken directly from the in-kernel RB tree pfi_ifs. Unlike newer OpenBSD which uses a separate userspace-visible struct, DragonFly exports the kernel-internal layout wholesale.

Per sys/net/pf/pfvar.h:1171-1183 and sys/sys/tree.h:333-339, each kif contains: - pfik_tree (RB_ENTRY = 3 kernel pointers + color β‰ˆ 32 bytes) - pfik_ifp (kernel address of struct ifnet) - pfik_group (kernel address of struct ifg_group) - pfik_dynaddrs (TAILQ_HEAD = 2 kernel pointers)

The dispatch in pf_ioctl.c:3019-3028 only validates pfiio_esize == sizeof(struct pfi_kif) and never sanitizes the struct before copyout.

Threat model & preconditions

  • Attacker position: anyone who can open /dev/pf (root on the host, or root inside a jail to which /dev/pf has been delegated via devfs rules β€” a standard jail-with-pf setup).
  • Privileges gained or impact: leaked host-kernel pointers defeat KASLR and reveal heap layout, substantially lowering the bar for a subsequent kernel exploit (e.g. a separate UAF or heap-overflow primitive becomes reliably exploitable). For jails specifically, this leaks host-kernel addresses to a context that is supposed to be sandboxed. No crash, no code execution by itself.
  • Required config or capabilities: /dev/pf access.
  • Reachability: open("/dev/pf", O_RDONLY) β†’ ioctl(fd, DIOCIGETIFACES, &io) with io.pfiio_esize = sizeof(struct pfi_kif).

Proof of concept

PoC source: findings/poc/DF-0603/pfleak.c

Build & run

cc -o pfleak pfleak.c
sudo ./pfleak          # or inside a jail with /dev/pf delegated

Expected output

Printed pointer fields are non-NULL kernel addresses (e.g. 0xffff... values), confirming the leak. Compare against the boot-loader KASLR base to demonstrate offset recovery.

Impact

  • Blast radius: any DragonFly system exposing /dev/pf to a privileged process. Realistic in VPN concentrators, routers, and jails with delegated /dev/pf.
  • Severity rationale: Low. Deterministic and repeatable leak, but the attacker is already privileged (/dev/pf is 0600 root:wheel). Primary impact is KASLR bypass and heap-layout disclosure for an already- privileged process, or host-kernel-address leak across a jail boundary.
  • Reliability: 100% β€” straight-line copyout, no race.

Define a separate userspace-visible struct containing only the intended fields (name, counters, tzero, flags, states, rules β€” no pointers, no RB_ENTRY, no TAILQ_HEAD) and copyout that. Minimal interim fix: zero the pointer fields before copyout.

--- a/sys/net/pf/pf_if.c
+++ b/sys/net/pf/pf_if.c
@@ -773,8 +773,21 @@ pfi_get_ifaces(const char *name, struct pfi_kif *buf, int *size)
        if (*size > n++) {
            struct pfi_kif  ucopy;
+           struct pfi_kif  *src = p;
            if (!p->pfik_tzero)
                p->pfik_tzero = time_second;
-           pfi_kif_ref(p, PFI_KIF_REF_RULE);
-           if (copyout(p, buf++, sizeof(*buf))) {
+           ucopy = *src;
+           /* NEVER leak kernel pointers to userspace */
+           bzero(&ucopy.pfik_tree,    sizeof(ucopy.pfik_tree));
+           ucopy.pfik_ifp    = NULL;
+           ucopy.pfik_group  = NULL;
+           bzero(&ucopy.pfik_dynaddrs, sizeof(ucopy.pfik_dynaddrs));
+           pfi_kif_ref(src, PFI_KIF_REF_RULE);
+           if (copyout(&ucopy, buf++, sizeof(ucopy))) {
                pfi_kif_unref(p, PFI_KIF_REF_RULE);
                crit_exit();
                return (EFAULT);

Long term: introduce struct pfi_kif_uv (mirroring OpenBSD's approach) and use it for the DIOCIGETIFACES ABI.

References

Timeline

  • 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
  • 2026-07-02 Reported to DragonFlyBSD security contact (pending).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0603 Β· 14 files
FileTypeDescriptionSize
pfleak.c trigger-source DIOCIGETIFACES reproducer + kernel-pointer scanner 3.7 KB view raw
build.sh build-script cc -o pfleak pfleak.c 221 B view raw
run.sh run-script kldload pf.ko (if needed) + ./pfleak 64 353 B view raw
run.log run-log baseline unpatched run: 34 ptrs leaked 2.8 KB view raw
run.2.log run-log baseline reproducibility run 2: 34 ptrs 2.8 KB view raw
run.3.log run-log baseline reproducibility run 3: 34 ptrs 2.8 KB view raw
fix.diff suggested-fix zero pointer fields of stack-local ucopy before copyout (sys/net/pf/pf_if.c) 906 B view raw
fix_build.log build-log rebuilt pf.ko module, MOD_DONE rc=0 8.4 KB view raw
fix_run.log run-log patched-module run: 0 ptrs leaked 789 B view raw
env.txt environment uname, cc, kldstat, vm.randomize_mmap 550 B view raw
VERDICT.md verdict full narrative: mechanism, impact, fix validation 5.3 KB ↓ raw
README.md readme human-facing PoC readme 1.2 KB ↓ raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme human-facing PoC readme
↓ download raw

DF-0603 β€” PoC: DIOCIGETIFACES pfi_kif kernel pointer leak

Privileged local KASLR-bypass PoC. pfi_get_ifaces() copies the entire in-kernel struct pfi_kif to userspace, including raw kernel pointers (pfik_tree RB_ENTRY, pfik_ifp, pfik_group, pfik_dynaddr TAILQ_HEAD).

Files

  • pfleak.c β€” minimal reproducer (DIOCIGETIFACES + hexdump of pointer fields).
  • (added by per-PoC verifier) build.sh, run.sh, build.log, run.log, VERDICT.md, manifest.json, fix.diff.

Build & run

cc -o pfleak pfleak.c
sudo ./pfleak          # root, or inside a jail with /dev/pf delegated

Expected first outcome

Printed pointer fields are non-NULL kernel addresses (e.g. 0xffff...), confirming the leak. Compare against sysctl vm.phystext / boot-loader KASLR base to demonstrate offset recovery.

Notes for the per-PoC verifier

  • /dev/pf is 0600 root:wheel. The PoC must run as root or in a context with /dev/pf delegated.
  • Iterate over all kifs (one call with a large buffer) for a comprehensive heap-layout oracle β€” 6–7 live kernel pointers per kif.
  • Verify the fix with git apply findings/poc/DF-0603/fix.diff (zeroing pointer fields before copyout); after the fix, the pointer fields should be 0x0.
VERDICT.md verdict full narrative: mechanism, impact, fix validation
↓ download raw

DF-0603 β€” Verdict

Verdict

REPRODUCED. Info-leak confirmed: DIOCIGETIFACES returns the entire in-kernel struct pfi_kif to userspace verbatim, including raw kernel pointers (RB tree linkage, pfik_ifp, pfik_group, pfik_dynaddrs TAILQ_HEAD). On the unpatched 6.5-DEVELOPMENT #0 kernel the PoC observes 34 kernel-pointer-shaped qwords across 9 kifs in a single call. Three independent runs return the same count (deterministic). The single-fix module brings this to 0 with no functional regression (kif names, counters, flags, states, rules all still returned).

Mechanism (trigger β†’ primitive β†’ effect)

  1. Trigger β€” open /dev/pf (root-only; mode 0600 root:wheel) and issue ioctl(fd, DIOCIGETIFACES, &io) with io.pfiio_esize = sizeof(struct pfi_kif) and a sufficiently large pfiio_buffer. Dispatch: sys/net/pf/pf_ioctl.c:3019-3028 validates only pfiio_esize == sizeof(struct pfi_kif) and forwards to pfi_get_ifaces().
  2. Primitive (wholesale copyout) β€” sys/net/pf/pf_if.c:763-790, specifically line 778: copyout(p, buf++, sizeof(*buf)). p is a live struct pfi_kif * from the in-kernel RB tree pfi_ifs. The struct layout (sys/net/pf/pfvar.h:1171-1183) carries these kernel-pointer fields: - pfik_tree.rbe_left / rbe_right / rbe_parent (3 ptrs, RB_ENTRY at offset 16/24/32 β€” sys/sys/tree.h:333-339) - pfik_ifp (struct ifnet *, offset 184) - pfik_group (struct ifg_group *, offset 192) - pfik_dynaddrs.tqh_first / tqh_last (TAILQ_HEAD, offset 208/216; for empty lists tqh_last points back into the kif itself)
  3. Effect β€” a comprehensive host-kernel heap/text pointer oracle: 3–5 live kernel pointers per kif Γ— N kifs. On this guest (KASLR off) the leak is redundant for symbol resolution but is still a real pointer disclosure; on a KASLR-hardened kernel it would defeat KASLR and reveal heap layout (the classic precondition for turning a separate UAF/overflow primitive into a reliable exploit). Across a jail boundary with delegated /dev/pf, this leaks host-kernel addresses to a sandboxed context.

Privilege / threat model

  • /dev/pf is 0600 root:wheel. Confirmed: as unprivileged maxx (uid 1001), cat /dev/pf β‡’ Permission denied and open() fails. PoC must run as root (or inside a jail with /dev/pf delegated via devfs rules β€” a standard jail-with-pf setup, per the finding's threat model).
  • This is a pure info-leak (CWE-200) β€” no write primitive, no escalation chain exists. Per Phase 6 hard-blocker rule, read-only primitives have no escalation chain; impact ceiling is KASLR-defeat / heap-layout disclosure.

PoC changes

The original PoC folder shipped only a README.md (no source). I wrote pfleak.c from scratch: - Uses <net/pf/pfvar.h> for struct pfi_kif / struct pfioc_iface / DIOCIGETIFACES. - Allocates a 64-entry buffer and issues one DIOCIGETIFACES; reports the returned count. - For each kif, scans the 8-byte qwords of the struct and prints any word matching the canonical x86_64 kernel-address mask (v & 0xffff000000000000) == 0xffff000000000000. - Prints name/flags/tzero/states/rules so functional correctness is visible pre/post fix. - Exits non-zero if no pointer was leaked (used by the fix-validation step as the "fixed" success marker).

Exploit chain

N/A β€” pure read-only info leak. No escalation chain is derivable; the impact ceiling is host-kernel pointer disclosure (KASLR-defeat + heap-layout oracle for a privileged /dev/pf opener, or a cross-jail host-address leak).

Fix validation (Phase 8)

Important architectural note: PF is not compiled into the GENERIC kernel (sys/conf/files:1576 marks net/pf/pf_if.c as optional pf, and sys/config/X86_64_GENERIC does not include device pf). PF ships as a loadable module at /boot/kernel/pf.ko. The bug therefore lives in the module, and the correct validation vehicle is a rebuilt pf.ko, not a rebuilt kernel (make nativekernel was started, discovered to be a no-op for this code path, and abandoned β€” see fix_build.log).

Single-fix build: applied fix.diff to /usr/src/sys/net/pf/pf_if.c on the with-src snapshot, then cd /usr/src/sys/net/pf && make KERNBUILDDIR=/usr/obj/usr/src/sys/X86_64_GENERIC β‡’ pf.ko rebuilt, rc=0 (fix_build.log).

Before/after (same guest, same kernel, swapped module + reloaded):

kernel/module PoC output
unpatched /boot/kernel/pf.ko (3.6 MB, buildID d3c1d9…) SUMMARY: 34 kernel-pointer-shaped qwords leaked across 9 kif(s)
fixed /boot/kernel/pf.ko (377 KB, buildID a6d0a4…) SUMMARY: 0 kernel-pointer-shaped qwords leaked across 9 kif(s)

3 consecutive runs on the patched module all return 0 leaks. The kif names, counters, flags, states and rules are still returned correctly (no functional regression). Fix closes the leak.

fix.diff zeroes the pointer fields of a stack-local copy of the kif before copyout() β€” same shape as the finding markdown's ## Recommended fix proposal. matches finding proposal. A longer-term fix (separate userspace-visible struct mirroring OpenBSD's pfi_kif_uv) is out of scope for this verification.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: baseline 34 kernel-ptr qwords leaked; patched 0 leaked x3 runs.

BEFORE: 34 ptrs x3 runs. AFTER: 0 ptrs x3 runs.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0 (kernel unchanged; fix in rebuilt pf.ko, buildID a6d0a46d...)

Confirmed kernel references

Detail

Exploit chain

none (pure info leak, read-only). Impact ceiling: KASLR-defeat / heap-layout oracle.

Evidence (decisive lines)

baseline #0: 34 kernel-pointer-shaped qwords leaked across 9 kifs (x3 runs). patched pf.ko: 0 qwords leaked (x3 runs).

PoC changes

Wrote pfleak.c from scratch (folder had README.md only). Added build.sh, run.sh, VERDICT.md, manifest.json, fix.diff, full logs.

Verified recommended fix

In pfi_get_ifaces(), copy live kif into stack-local struct pfi_kif ucopy, bzero pointer fields (pfik_tree, pfik_ifp=NULL, pfik_group=NULL, pfik_dynaddrs), copyout(&ucopy,...). Matches finding proposal. Full git-apply-able diff in findings/poc/DF-0603/fix.diff.

Verdict

REPRODUCED. pfi_get_ifaces() copyout()s entire struct pfi_kif verbatim including raw kernel pointers (pfik_tree RB_ENTRY, pfik_ifp, pfik_group, pfik_dynaddrs). 34 kernel-pointer-shaped qwords leak across 9 kifs per call. Root-only (/dev/pf 0600 root:wheel). KASLR-defeat / cross-jail host-address leak.