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)
PoC verification
Evidence pack
findings/poc/DF-3297 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 u32nrstore) 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)
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_testableConfirmed 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
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.
No comments yet.