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
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/pfhas 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/pfaccess. - Reachability:
open("/dev/pf", O_RDONLY)βioctl(fd, DIOCIGETIFACES, &io)withio.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/pfto 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/pfis0600 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.
Recommended fix
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
sys/net/pf/pfvar.h:1171-1183βstruct pfi_kiflayout with kernel pointer fields.sys/net/pf/pf_ioctl.c:3019-3028βDIOCIGETIFACESdispatch (validatespfiio_esize == sizeof(struct pfi_kif)but does not sanitize).
Timeline
- 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
- 2026-07-02 Reported to DragonFlyBSD security contact (pending).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0603 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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/pfis0600 root:wheel. The PoC must run as root or in a context with/dev/pfdelegated.- 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 beforecopyout); after the fix, the pointer fields should be0x0.
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)
- Trigger β open
/dev/pf(root-only; mode0600 root:wheel) and issueioctl(fd, DIOCIGETIFACES, &io)withio.pfiio_esize = sizeof(struct pfi_kif)and a sufficiently largepfiio_buffer. Dispatch:sys/net/pf/pf_ioctl.c:3019-3028validates onlypfiio_esize == sizeof(struct pfi_kif)and forwards topfi_get_ifaces(). - Primitive (wholesale copyout) β
sys/net/pf/pf_if.c:763-790, specifically line 778:copyout(p, buf++, sizeof(*buf)).pis a livestruct pfi_kif *from the in-kernel RB treepfi_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 liststqh_lastpoints back into the kif itself) - 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/pfis0600 root:wheel. Confirmed: as unprivilegedmaxx(uid 1001),cat /dev/pfβPermission deniedandopen()fails. PoC must run as root (or inside a jail with/dev/pfdelegated 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.
Recommended fix
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
fixedVALIDATED: baseline 34 kernel-ptr qwords leaked; patched 0 leaked x3 runs.
BEFORE: 34 ptrs x3 runs. AFTER: 0 ptrs x3 runs.
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.
No comments yet.