sysctl_sysctl_next_ls(): unbounded recursion level writes past newoid[CTL_MAXNAME] β value-controlled kernel stack smash from an unprivileged MIB walk
Summary
sysctl_sysctl_next_ls writes *next = oidp->oid_number at next[level-1] where next is the caller's fixed int newoid[CTL_MAXNAME] (12). The !namelen and oid_number > *name descents recurse into child lists with level+1 WITHOUT consuming name components, so recursion depth tracks MIB tree depth and level is never bounds-checked; sys___sysctl caps the user's namelen at 12 but a bare {CTL_SYSCTL,CTL_SYSCTL_NEXT} query reaches the walker with namelen=0 and descends to full tree depth. Any tree nested deeper than 12 levels turns every unprivileged sysctl -aN next-walk into an ascending one-int-per-level overwrite of the kernel stack above newoid[] (saved registers, RBP, return addresses) with values = oid numbers chosen by whoever created the nodes. Demonstrated on the guest: 24-deep KLD-planted oid chain (0x41414100+i), then nobody-triggered Fatal trap 9 with frame pointer = 0x4141410d4141410c (the planted numbers, exactly newoid[12..13]) and crash at sysctl_sysctl_next+0x63 ret β twice. Honest blocker: no in-tree subsystem nests that deep (natural depth 5; all 1256 dynamic-registration callers kernel-driven), so arming needs kldload/root β a memory-corruption landmine, not a stock-config escalation. level > CTL_MAXNAME guard fix validated by kernel rebuild: identical trigger walks 1834 oids cleanly, guest up.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5844 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| deepoid.c | β | 1.6 KB | view raw | |
| Makefile | β | 54 B | β download | |
| trigger.c | β | 1.0 KB | view raw | |
| build.sh | β | 158 B | view raw | |
| run.sh | β | 141 B | view raw | |
| build.log | β | 5.4 KB | view raw | |
| panic.txt | β | 2.0 KB | view raw | |
| panic.root-run.txt | β | 2.0 KB | view raw | |
| fix_validation.log | β | 165 B | view raw | |
| env.txt | β | 484 B | view raw | |
| fix.diff | β | 276 B | view raw | |
| VERDICT.md | β | 5.1 KB | β raw | |
| manifest.json | β | 967 B | view raw | |
| verdict.json | β | 3.8 KB | view raw |
DF-5844 β VERDICT
Status: reproduced (panic / kernel stack memory corruption from an
unprivileged sysctl next-walk; impact capped at panic for this rig because
planting the >12-deep tree requires a root-loaded KLD β see Honest scoping).
Root cause (path:line)
sys/kern/kern_sysctl.c:786βint newoid[CTL_MAXNAME](12 ints;CTL_MAXNAME = 12, sys/sys/sysctl.h:55) insysctl_sysctl_next().sys/kern/kern_sysctl.c:719-727βsysctl_sysctl_next_ls()writes*next = oidp->oid_number(line 727) atnext[level-1];levelstarts at 1.- The
!namelenbranch (kern_sysctl.c:733-744) and theoidp->oid_number > *namebranch (kern_sysctl.c:749-758) recurse intoSYSCTL_CHILDREN(oidp)withnext+1, level+1without consuming a name component in the former case, so recursion depth tracks the MIB tree depth. No check oflevelagainstCTL_MAXNAMEexists anywhere in the function. sys___sysctlcaps the user'snamelenat 12 (kern_sysctl.c:1494), but that does not bound this walk:sysctl_sysctl_nextis reached withnamelen = 0for a bare{CTL_SYSCTL, CTL_SYSCTL_NEXT}query, and the!namelendescent recurses to full tree depth.
Tree depth β₯ 13 β newoid[12], newoid[13], β¦ are written past the array,
one 32-bit oid_number per level, marching up sysctl_sysctl_next's stack
frame (saved registers, saved RBP, return addresses of its callers), then
SYSCTL_OUT(req, newoid, j * sizeof(int)) (kern_sysctl.c:791) would
additionally copy out j > 12 ints from beyond the array (stack info leak)
if the smash itself didn't fault first.
Reproduction (baseline, unpatched kernel #0, Jul 2 2026)
- Guest measured natural MIB depth = 5 (
sysctl -aN | awk -F.max components) β no in-tree subsystem nests >12, so the rig plants one. deepoid.ko(root, rig only): 24 nested dynamic nodes, explicit oid numbers0x41414100+i(chain[12] = 0x4141410c, chain[13] = 0x4141410d, β¦).- Trigger as
nobody:/tmp/triggerβ a plainsysctl({1,3,...})next-walk loop; no privileges needed (read-only walker, no caps check on this path: sysctl_root write-gates at kern_sysctl.c:1439-1450 don't apply to reads).
Result (serial console, panic.txt):
deepoid: loaded 24-deep chain (oid 0x41414100+) Fatal trap 9: general protection fault while in kernel mode frame pointer = 0x10:0x4141410d4141410c Stopped at sysctl_sysctl_next+0x63: ret
RBP = 0x4141410d4141410c β literally the planted oid numbers of chain
levels 13 and 12: proof the OOB writes are fully value-controlled by the
oid creator and land exactly at newoid[12..13], hitting the saved frame
pointer; the fault is the ret in sysctl_sysctl_next popping attacker
data. Same panic reproduced twice (root-run and nobody-run) β see
panic.root-run.txt, panic.txt.
Primitive characterization (why this matters beyond the rig)
- What: ascending, one-int-per-level overwrite of the kernel stack above a 48-byte local array, values = oid numbers (fully chosen by whoever can create the deep chain), count = depthβ12 ints.
- Where: fixed offset in
sysctl_sysctl_next's frame β saved registers, frame pointer, return address ofsysctl_sysctl_nextitself, then callers (sysctl_root,userland_sysctl, syscall trap frame). With depth ~60+ the writes reach the trap frame's return-to-user path. - Who triggers: any user β
sysctl -aN, libkvm-based tools, anything that walks the MIB. No privilege gate exists on the walker. - Honest scoping (hard blocker for uid=0): to turn this into
unprivβroot, the victim system must first contain a >12-deep sysctl
tree, and creating one requires
kldload/root viasysctl_add_oid(1,256 kernel callers, none unpriv-triggered; natural depth measured 5). So the escalation chain ends at "root must plant the weapon" β on a stock system no unprivileged user can build the deep tree. It is a landmine for any future subsystem that auto-generates nested oid trees (Γ la FreeBSD'sdev.foo.0.barauto-trees), and an immediate kernel-smash for third-party modules that nest >12. Severity filed Medium accordingly.
Fix validation
fix.diff: reject level > CTL_MAXNAME at sysctl_sysctl_next_ls entry
(the too-deep subtree is then reported empty β consistent with the
12-component naming limit). Applied to guest /usr/src, kernel rebuilt
(make nativekernel, BUILD_RC=0), installed, rebooted (#1, Oct 14 01:16).
Identical rig + identical su -m nobody -c /tmp/trigger:
- baseline: Fatal trap 9, RBP = 0x4141410d4141410c, DDB, guest down.
- patched:
walk finished after 1834 oids: No such file or directory, TRIG_RC=0,sysctl -aN | wc -l= 1829, guest up. (fix_validation.log)
Bad behavior gone; fix confirmed effective.
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
fixedfix.diff applied to guest /usr/src/sys/kern/kern_sysctl.c, kernel rebuilt (nativekernel, BUILD_RC=0), installed, rebooted; identical rig + identical su -m nobody trigger now completes cleanly (walk finished after 1834 oids: ENOENT, TRIG_RC=0, sysctl -aN succeeds, guest stays up). Bad behavior gone.
fix_validation.log; fix.diff; env.txt (patched kernel #1 uname)
Confirmed kernel references
Detail
Exploit chain
root(KLD): sysctl_add_oid chain depth 24 with explicit oid numbers -> unprivileged user: sysctl({CTL_SYSCTL,CTL_SYSCTL_NEXT,...}) walk (trigger.c / sysctl -aN) -> next_ls !namelen descent recurses to tree depth -> *next = oid_number writes newoid[12..23] = 0x4141410c.. smashes saved RBP/RIP of sysctl_sysctl_next -> Fatal trap 9 at ret. Escalation to uid=0 stops at tree-planting: requires kldload (root); documented as hard blocker, not pursued further.
Evidence (decisive lines)
panic.txt (nobody-run: 'frame pointer = 0x10:0x4141410d4141410c', 'Stopped at sysctl_sysctl_next+0x63: ret'); panic.root-run.txt (same smash, root run); run.log; fix_validation.log (patched kernel: walk finished after 1834 oids, TRIG_RC=0, guest up); VERDICT.md full narrative
PoC changes
Original sketch concept only; wrote rig from scratch: deepoid.c KLD plants 24-deep chain of CTLTYPE_NODE oids via SYSCTL_ADD_NODE with explicit recognizable oid numbers (0x41414100+i); trigger.c iterates __sysctl({CTL_SYSCTL,CTL_SYSCTL_NEXT,
Verified recommended fix
Bound the walk: reject level > CTL_MAXNAME at sysctl_sysctl_next_ls entry (fix.diff validated on guest)
Verdict
sysctl_sysctl_next_ls() recurses to MIB tree depth with no check of level against CTL_MAXNAME (12), writing attacker(KLD)-chosen 32-bit oid numbers one-per-level past int newoid[CTL_MAXNAME] on sysctl_sysctl_next()'s stack. Reproduced twice from an unprivileged nobody next-walk after planting a 24-deep dynamic chain: Fatal trap 9, frame pointer = 0x4141410d4141410c (planted oids), crash at sysctl_sysctl_next+0x63 ret. Value-controlled stack smash; uid=0 chain blocked only by the fact that planting a >12-deep tree requires root (kldload / sysctl_add_oid) - no in-tree subsystem nests deeper than 5, so on a stock system no unprivileged user can arm it. Landmine for any future auto-nested oid tree; immediate kernel smash for modules that nest >12.
No comments yet.