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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5844 Β· 14 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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) in sysctl_sysctl_next().
  • sys/kern/kern_sysctl.c:719-727 β€” sysctl_sysctl_next_ls() writes *next = oidp->oid_number (line 727) at next[level-1]; level starts at 1.
  • The !namelen branch (kern_sysctl.c:733-744) and the oidp->oid_number > *name branch (kern_sysctl.c:749-758) recurse into SYSCTL_CHILDREN(oidp) with next+1, level+1 without consuming a name component in the former case, so recursion depth tracks the MIB tree depth. No check of level against CTL_MAXNAME exists anywhere in the function.
  • sys___sysctl caps the user's namelen at 12 (kern_sysctl.c:1494), but that does not bound this walk: sysctl_sysctl_next is reached with namelen = 0 for a bare {CTL_SYSCTL, CTL_SYSCTL_NEXT} query, and the !namelen descent 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)

  1. Guest measured natural MIB depth = 5 (sysctl -aN | awk -F. max components) β€” no in-tree subsystem nests >12, so the rig plants one.
  2. deepoid.ko (root, rig only): 24 nested dynamic nodes, explicit oid numbers 0x41414100+i (chain[12] = 0x4141410c, chain[13] = 0x4141410d, …).
  3. Trigger as nobody: /tmp/trigger β€” a plain sysctl({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 of sysctl_sysctl_next itself, 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 via sysctl_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's dev.foo.0.bar auto-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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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)
↓ fix.diffper-fix-DF-5844

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,}) like sysctl(8) - a single bare {1,3} query only returns the first oid and does not reach the deep subtree, the walk must iterate.

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.