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

sysctl_hostname dereferences p->p_ucred up to four times with no p_spin/crhold and not via td_ucred β€” racy freed-credential read (cratom_proc can crfree the cred between load and deref), wild pr_host[256] copyout; DF-2868-class sibling with a jailed-root trigger

Field Value
ID DF-2951
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:L/I:N/A:H
CWE CWE-416 (read) / CWE-362
File sys/kern/kern_mib.c
Lines 221-229 (racer: kern_prot.c:1166-1188)
Area kern
Confidence likely
Discovered 2026-09-02
Pass 2 (GLM 5.3 second pass)
Bucket memcorrupt
Reported pending
Known CVE none
CVE match novel

Summary

The credential protocol requires syscall-context code to use td->td_ucred (thread.h:279 — 'synchronized from p_ucred on user→kernel syscall'); the sysctl dispatcher itself does. sysctl_hostname instead loads p->p_ucred raw at :222 and re-loads it three more times in the handler-call expression at :227-229 (evaluation order unspecified). cratom_proc replaces p->p_ucred and crfree()s the old cred from any other thread, interlocked only by p_spin for the pointer store. A thread racing between one of these loads and its deref can read a freed ucred → garbage cr_prison → wild copyout of up to 256 bytes: panic (realistic) or kernel-memory disclosure (theoretical, groomed stale chunk). Only privileged actors can churn p_ucred replacement: jailed uid-0 (setgroups allowed in jail; sys_setgroups calls cratom_proc unconditionally) racing the ungated kern.hostname read — same threat model as DF-0181. Raced 3×120 s: ~3.7e8 reads vs ~4.7e8 cred replacements, zero errors/panics — the 2-4-instruction window did not manifest from userspace (freed chunk typically immediately recycled as the next cratom's cred). Verdict: not_reproduced at runtime; violation certain by source inspection. Fix: capture the prison once from td->td_ucred.

Timeline

  • 2026-09-02 Discovered during pass-2 audit of kern_mib.c (GLM 5.3); stress-refuted at runtime, code-certain.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2951 Β· 8 files
FileTypeDescriptionSize
stress_ucred_race.c β€” 1.7 KB view raw
build.log β€” 66 B view raw
run.log β€” 70 B view raw
run.2.log β€” 70 B view raw
run.3.log β€” 70 B view raw
run.sh β€” 325 B view raw
VERDICT.md β€” 3.2 KB ↓ raw
verdict.json β€” 3.2 KB view raw
VERDICT.md
↓ download raw

DF-2951 VERDICT

Status: not_reproduced (runtime). Code-level protocol violation: certain by inspection. Severity filed: Low, confidence likely.

Why the finding is real (source argument)

  1. Protocol: td_ucred is the syscall-context credential, synchronized from p_ucred at user->kernel transition (sys/sys/thread.h:279, :327). cratom_proc (kern_prot.c:1166-1188) documents that racing readers are exactly why the pointer store is p_spin-interlocked β€” raw unlocked p->p_ucred loads outside that protocol are the violation. The sysctl core itself reads td->td_ucred (kern_sysctl.c:1447); sysctl_hostname deviates.
  2. kern_mib.c:222 caches pr from one p->p_ucred load, then :228-229 re-load p->p_ucred (three more loads: the handler argument and the two sizeof-expression operands, evaluation order unspecified). Between any load and its deref, cratom_proc on another thread can drop the last non-cached reference and crfree the cred (crfree β†’ refcount 0 β†’ ucred freed; its cr_prison field then holds whatever the objcache recycler wrote).
  3. Consequence scale: pr_host is char[256] (sys/jail.h:118) β€” the subsequent SYSCTL_OUT copies strlen(pr_host)+1 bytes from a garbage-derived prison pointer to userspace. Realistic outcome: kernel panic (wild read). Theoretical ceiling: 256-byte disclosure if the stale chunk is groomed. Prison-UAF half of the window is dead in practice (prison0 is static; a dynamic prison is pinned by the process's own current cred), so the ucred-object UAF-read is the whole story.
  4. Attacker: jailed uid-0 (setgroups allowed in jail β†’ unconditional cratom_proc, kern_prot.c:682; kern.hostname read ungated). Unprivileged non-jail users cannot replace their own p_ucred (no-op setuid/setgid, privileged setgroups), which bounds severity at Low β€” same actor class and impact ceiling family as DF-0181's jailed-root model, but probabilistic instead of deterministic.

Verification attempt (honest negative)

stress_ucred_race.c: 4 threads looping sysctlbyname("kern.hostname"), 4 threads looping alternating setgroups (forcing cratom replacement every call), run for 3 Γ— 120 s inside a jail as uid 0 on the stock INVARIANTS guest:

  • run.log: reads 124,072,534 ; setgroups 153,437,435 ; 0 errors
  • run.2.log: reads 119,316,721 ; setgroups 154,727,757 ; 0 errors
  • run.3.log: reads 124,101,906 ; setgroups 158,916,561 ; 0 errors

No panic, no wedge, guest stayed up (checked after each run). The race window is a handful of adjacent instructions and the freed ucred chunk is typically immediately re-issued as the next cratom's cred (same objcache, same content), so a userspace-only trigger did not manifest. This is a hardening/robustness fix, not a demonstrable exploit.

Use the cached prison once, via the sanctioned thread credential:

    struct prison *pr = NULL;
    if (p) {
        pr = td->td_ucred->cr_prison;
        ...
    }
    if (p && pr) {
        ...
        error = sysctl_handle_string(oidp, pr->pr_host,
            sizeof(pr->pr_host), req);
    }

plus optionally spin_lock(&p->p_spin) around a single td->td_ucred capture if td_ucred is ever NULL here (it is not, for user threads: kern_caps.c:344-350 relies on it).

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

No fix validation performed: the bug did not reproduce at runtime (not_reproduced), so there is no baseline behavior to diff a patched kernel against; the recommended one-line hardening is given in VERDICT.md.

[]
per-fix-DF-2951

Confirmed kernel references

Detail

Evidence (decisive lines)

['VERDICT.md: source-level protocol argument (thread.h:279, kern_prot.c:1166-1188, kern_mib.c:221-229)', 'run.log: 124,072,534 reads / 153,437,435 setgroups, 0 errors, no panic', 'run.2.log: 119,316,721 reads / 154,727,757 setgroups, 0 errors, no panic', 'run.3.log: 124,101,906 reads / 158,916,561 setgroups, 0 errors, no panic', 'build.log: cc 8.3 -O2 -pthread build OK in-guest']

PoC changes

Authored fresh (no seed): 8-thread pthread stress; counters packed as n + (errs<<40) to survive exit reporting.

Verified recommended fix

In sysctl_hostname (kern_mib.c), replace all p->p_ucred loads with a single pr = td->td_ucred->cr_prison capture and use pr->pr_host / sizeof(pr->pr_host).

Verdict

Code-level protocol violation is certain by inspection: sysctl_hostname loads p->p_ucred up to four times (kern_mib.c:222,228,229) with no p_spin/crhold and not via the sanctioned td_ucred (thread.h:279), while cratom_proc (kern_prot.c:1166-1188) replaces and crfrees p_ucred from other threads. Runtime stress on the stock guest could not manifest it: 3x120s of 4 reader threads hammering kern.hostname against 4 jailed-root setgroups churners produced ~3.7e8 reads vs ~4.7e8 credential replacements with zero errors, zero panics, guest healthy after every run. The window is a few adjacent instructions and the freed ucred chunk is typically immediately recycled as the next cratom's cred. Filed Low/likely: realistic impact is a probabilistic panic (jailed-root actor, DF-0181's model); theoretical 256-byte disclosure ceiling if the stale chunk is groomed. Hardening fix: capture td->td_ucred->cr_prison once into pr and use pr->pr_host/pr's sizeof.