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

DIOCGETSRCNODES discloses raw kernel heap pointers (RB-tree links) through read-only /dev/pf

Summary

DIOCGETSRCNODES copies each struct pf_src_node to userland via bcopy+copyout (:2884,:2902) sanitizing only the 4-byte rule.nr inside the 8-byte pf_rule_ptr union. The RB_ENTRY linkage rbe_left/rbe_right/rbe_parent leaks verbatim whenever a per-cpu source tree holds >=2 nodes; kif and the pointer's upper half also copied raw. Distinct ioctl from the DF-0277/0278/0279 leak family (all three remain UNFIXED, as does DF-0276's wrong kfree at :2169); unlike them this one is in the FREAD allow-list so an O_RDONLY fd suffices - demonstrated with open(O_RDONLY): leaked 0xfffff8008bb64460 etc. at the RB_ENTRY offsets, stable across dumps. Kernel heap-address disclosure (KASLR-defeat / heap-layout oracle) to anyone with /dev/pf read access: root on stock, non-root wherever devfs rules grant read for monitoring. Fix: sanitize entry/kif/rule.ptr before copyout.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3297 Β· 14 files
FileTypeDescriptionSize
leak_srcnodes.c β€” 2.0 KB view raw
conn2.c β€” 405 B view raw
srcnodes.conf β€” 101 B ↓ download
build.sh β€” 121 B view raw
run.sh β€” 210 B view raw
build.log β€” 133 B view raw
run.log β€” 1.2 KB view raw
leak_sample.txt β€” 1.6 KB view raw
env.txt β€” 416 B view raw
fix.diff β€” 440 B view raw
README.md β€” 1.9 KB ↓ raw
VERDICT.md β€” 2.6 KB ↓ raw
manifest.json β€” 887 B view raw
verdict.json β€” 2.8 KB view raw

DF-3297 β€” pf DIOCGETSRCNODES leaks kernel heap pointers (RB-tree links)

What

pfioctl() DIOCGETSRCNODES copies each struct pf_src_node to userland with a raw bcopy(n, pstore, sizeof(*pstore)) (pf_ioctl.c:2884) followed by copyout (pf_ioctl.c:2902). The only sanitization is a 4-byte store pstore->rule.nr = n->rule.ptr->nr (pf_ioctl.c:2886) into the 8-byte union pf_rule_ptr. Leaked verbatim:

  • entry.rbe_left / entry.rbe_right / entry.rbe_parent β€” raw RB-tree kernel heap pointers (non-NULL whenever a per-cpu src tree holds >=2 nodes)
  • the upper 4 bytes of rule.ptr (the union's high half survives the u32 nr store)
  • kif (always NULL today β€” pf_insert_src_node, pf.c:582-599, never sets it β€” but is copied raw)

Sibling of the DF-0277 (DIOCGETRULE) / DF-0278 (DIOCGETADDR) / DF-0279 (DIOCGETALTQ) leak family β€” different ioctl, not covered by those findings. Unlike those, DIOCGETSRCNODES is in the FREAD allow list (pf_ioctl.c:1069), so a read-only /dev/pf fd suffices wherever one is granted.

Reachability

/dev/pf is 0600 root:wheel on a stock system (pf_ioctl.c:3360), so on stock this is a root-visible pointer disclosure (KASLR-defeat class). Setups that relax devfs permissions for monitoring (pfctl -s Sources uses this ioctl) expose it to non-root readers.

Reproduce

kldload pf.ko
pfctl -f srcnodes.conf && pfctl -e
for i in 2 3 4 5 6 7 8; do ifconfig lo0 127.0.0.$i netmask 255.0.0.0 alias; done
for ip in 127.0.0.2 ... 127.0.0.8; do ./conn2 $ip & done; wait   # >=2 nodes/cpu
cc -O2 -o leak_srcnodes leak_srcnodes.c
./leak_srcnodes        # opened O_RDONLY

Expected

node 1:
  +0000: 0xfffff8008bb64460   <-- kernel pointer   (rbe_left)
  +0008: 0xfffff8008bb645a0   <-- kernel pointer   (rbe_right)
node 0:
  +0010: 0xfffff8008bb64500   <-- kernel pointer   (rbe_parent)

(genuine DFly kernel-heap addresses 0xfffff8xx_xxxxxxxx; stable across consecutive dumps β€” see leak_sample.txt)

VERDICT.md
↓ download raw

DF-3297 VERDICT β€” reproduced (kernel heap-pointer disclosure, read-only fd)

Result

status: reproduced, impact: leak (raw kernel heap pointers disclosed to userland through a read-only /dev/pf descriptor).

How it was run

Guest (same boot as DF-3296 baseline work): pf.ko loaded, one keep state (max-src-states 100) rule on lo0:65001, lo0 aliases 127.0.0.2-8 added, one TCP connect per alias. leak_srcnodes opens /dev/pf with O_RDONLY (proving the FREAD-allow-list path) and dumps DIOCGETSRCNODES output qword-by-qword.

Observed (see leak_sample.txt / run.log): - rbe_left / rbe_right / rbe_parent at +0x00/+0x08/+0x10 hold genuine kernel-heap pointers (0xfffff8008bb64460, 0xfffff8008bb645a0, 0xfffff8008bb64500, 0xfffff8011757d6e0, ...). - Pointers are stable across consecutive dumps (same addresses minutes apart) β€” deterministic disclosure, not noise. - +0x40 (union pf_rule_ptr): only the low 4 bytes are overwritten with nr (pf_ioctl.c:2886); the pointer's upper half would leak verbatim for rules with source-tracking (non-NULL rule.ptr). In these runs the nodes had rule.ptr == NULL (observed zero at +0x40; the kernel's own if (n->rule.ptr != NULL) guard at pf_ioctl.c:2885 corroborates), so this half-leak is code-certain but not directly demonstrated β€” the RB links are the unambiguous, reproduced disclosure.

Root cause

pf_ioctl.c:2884 raw bcopy of struct pf_src_node (which embeds RB_ENTRY(pf_src_node) entry, three pointers) with no zeroization; only rule.nr is sanitized (pf_ioctl.c:2886). Same defect family as DF-0277/0278/0279 β€” this ioctl was missing from that set.

Impact ceiling

Kernel heap-address disclosure (KASLR-defeat / heap-layout oracle) for anyone with read access to /dev/pf (root on stock; non-root where devfs rules grant it, e.g. monitoring accounts). No direct memory-safety impact; severity Low.

Fix

fix.diff: zero the linkage/pointer fields of the copy before copyout (pstore->entry links, pstore->kif, full pstore->rule union) β€” mirrors what pfctl actually consumes (addr/raddr/counters/rule.nr). Not build-validated (Low, non-corruption class; validation reserved for memory-corruption findings per contract) β€” the patch is two bzero/field stores, compile-checked by inspection.

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

not_testable
↓ fix.diffper-fix-DF-3297

Confirmed kernel references

Detail

Exploit chain

reader with /dev/pf O_RDONLY -> DIOCGETSRCNODES (psn_len two-step) -> userland buffer contains pf_src_node structs with verbatim rbe_* kernel heap pointers -> heap-layout oracle aiding any separate memory-corruption bug; no direct memory-safety impact.

Evidence (decisive lines)

['leak_sample.txt: node dumps with 0xfffff8008bb64500 / 0xfffff8008bb64460 / 0xfffff8011757d6e0 at RB_ENTRY offsets, stable across two runs', 'run.log: full reproduction sequence (O_RDONLY open annotated)', 'VERDICT.md: offset map + the code-certain (not demonstrated) upper-half rule.ptr leak']

PoC changes

Authored from scratch; needed include for IFNAMSIZ and lo0 aliases (127.0.0.2-8) to force >=2 nodes per cpu tree - singleton trees have NULL links.

Verified recommended fix

Zero pstore->entry, pstore->kif and pstore->rule.ptr (after re-assigning rule.nr) in DIOCGETSRCNODES before copyout, pf_ioctl.c:2884-2902.

Verdict

DIOCGETSRCNODES (pf_ioctl.c:2884 bcopy of struct pf_src_node + 2902 copyout) discloses raw kernel heap pointers to userland: the RB_ENTRY links rbe_left/rbe_right/rbe_parent are copied verbatim; only the 4-byte rule.nr is sanitized (2886). Reproduced on the guest through an O_RDONLY /dev/pf fd (the ioctl is in the FREAD allow-list, pf_ioctl.c:1069): with >=2 source-tracking nodes per cpu tree, qwords such as 0xfffff8008bb64460 / 0xfffff8008bb645a0 / 0xfffff8011757d6e0 print at +0x00/+0x08/+0x10, stable across consecutive dumps. Same family as DF-0277/0278/0279 but a distinct, previously unreported ioctl. Stock exposure is root-only (/dev/pf 0600 root:wheel, pf_ioctl.c:3360); wherever devfs grants read access (monitoring) this is a kernel heap-address oracle / KASLR defeat for unprivileged readers. Low severity, information disclosure only.