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)
PoC verification
Evidence pack
findings/poc/DF-2951 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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)
- Protocol:
td_ucredis the syscall-context credential, synchronized fromp_ucredat 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 isp_spin-interlocked β raw unlockedp->p_ucredloads outside that protocol are the violation. The sysctl core itself readstd->td_ucred(kern_sysctl.c:1447);sysctl_hostnamedeviates. - kern_mib.c:222 caches
prfrom onep->p_ucredload, then :228-229 re-loadp->p_ucred(three more loads: the handler argument and the twosizeof-expression operands, evaluation order unspecified). Between any load and its deref,cratom_procon another thread can drop the last non-cached reference andcrfreethe cred (crfree β refcount 0 β ucred freed; itscr_prisonfield then holds whatever the objcache recycler wrote). - Consequence scale:
pr_hostischar[256](sys/jail.h:118) β the subsequentSYSCTL_OUTcopiesstrlen(pr_host)+1bytes 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. - 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.
Recommended fix (one-liner in spirit)
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_testableNo 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.
[]
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.
No comments yet.