expand_name() logs an unterminated, uninitialized kmalloc buffer via %s β recycled kernel-heap bytes disclosed into the user-readable kernel log
Summary
expand_name kmallocs MAXPATHLEN+1 with no M_ZERO and only NUL-terminates on the success path (kern_sig.c:2514); all three expansion-overflow branches log temp with %s before any terminator exists (2468/2481/2493), so kprintf walks uninitialized recycled heap past the initialized prefix into the kernel message buffer, which any local user can read (sysctl kern.msgbuf unprivileged, verified on guest). Deterministic same-bucket groom verified 3/3 on the guest: a groomer crash leaves a freed 2048-byte chunk; the next unprivileged crashy under a 1020xA+%N kern.corefile overflows and dmesg shows 'Path AAAA...AAA069crashy is too long' β '069' is the leaked tail of groomer pid 1069 from the previous kernel allocation. Trigger needs an admin-set kern.corefile longer than the default (root-only sysctl, capping severity). NUL-terminate-before-log fix git-apply-clean.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5803 Β· 18 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.6 KB | β raw | |
| VERDICT.md | β | 4.1 KB | β raw | |
| crashy.c | β | 1.1 KB | view raw | |
| groomer.c | β | 311 B | view raw | |
| poc.sh | β | 1.0 KB | view raw | |
| groom_rig.sh | β | 2.6 KB | view raw | |
| build.sh | β | 132 B | view raw | |
| run.sh | β | 182 B | view raw | |
| build.log | β | 9 B | view raw | |
| setup.log | β | 1.0 KB | view raw | |
| run.log | β | 89 B | view raw | |
| run.2.log | β | 47 B | view raw | |
| leak_sample.txt | β | 1.1 KB | view raw | |
| leak_full_line.txt | β | 2.1 KB | view raw | |
| env.txt | β | 154 B | view raw | |
| fix.diff | β | 803 B | view raw | |
| verdict.json | β | 3.5 KB | view raw | |
| manifest.json | β | 1.2 KB | view raw |
DF-5803 β expand_name() logs unterminated, uninitialized heap buffer (kernel heap disclosure to log)
What
sys/kern/kern_sig.c:expand_name() (called from coredump(), kern_sig.c:2561)
allocates temp = kmalloc(MAXPATHLEN + 1, M_TEMP, M_NOWAIT) β no M_ZERO β
and on every expansion-overflow error branch logs it with %s:
log(LOG_ERR, "pid %d (%s), uid (%u): Path `%s%s' is too long\n",
pid, name, uid, temp, name); /* kern_sig.c:2468-2471 (%N case) */
/* kern_sig.c:2481-2484 (%P case) */
/* kern_sig.c:2493-2496 (%U case) */
kfree(temp, M_TEMP);
At that point only temp[0 .. n-1] has been initialized (the expansion prefix)
and no NUL terminator exists anywhere in the buffer β temp[n] = '\0' is
only executed on the success path (kern_sig.c:2514). kprintf("%s") walks the
heap past the initialized prefix until it stumbles on a zero byte, copying
uninitialized kernel heap bytes into the kernel message buffer.
The message buffer is readable by any local unprivileged user
(sysctl kern.msgbuf, verified on the guest), so this is a kernel-memory
info leak (CWE-908 / CWE-467-style unbounded %s).
Preconditions (why severity is LowβMedium, not High)
kern.corefilemust expand to more than MAXPATHLEN (1024) characters. The default%N.corecannot (p_comm β€ 19).kern.corefileis a root-only sysctl β the configuration is privileged; the trigger (any core-dumping crash, e.g.abort()) is unprivileged and trivial.RLIMIT_CORE != 0for the crashing process (checked before expand_name).
Repro (deterministic groom + trigger)
build: cc -O -o /tmp/crashy /tmp/crashy.c # abort() (comm "crashy")
cc -O -o /tmp/groomer /tmp/groomer.c # abort() (comm "groomer")
setup (root):
# groom: make the PREVIOUS successful expansion exactly 1023 chars
# + NUL at temp[1023] (filler sized from the groomer pid)
sysctl kern.corefile='<filler S>x%P' # filler = 1023 - len(pid)
/tmp/groomer # crashes; expand_name writes 1023 chars + NUL, kfrees
trigger (root-set corefile, unprivileged crash):
sysctl kern.corefile='<1020 x A>%N'
ulimit -c unlimited; /tmp/crashy # 1020 + 6 > 1024 -> log()
expected:
dmesg: pid NNNN (crashy), uid (0): Path `AAAAβ¦AAA069crashy' is too long
^ 1020 'A' prefix ^^^ leaked heap tail (pid digits)
The groomer's 1023-char expansion is left in the (per-CPU, LIFO) kmalloc
bucket that the trigger's expand_name() immediately reuses; the trigger
overwrites offsets [0..1019] with 'A' and the %s then prints offsets
[1020..] β the previous allocation's tail β until the groom's NUL at 1023.
In practice 1β4 bytes per event with this groom; the class is unbounded
(whatever last occupied the 2048-byte bucket, up to the first zero byte).
Observed (guest, 3/3 batches)
pid 1072 (crashy), uid (0): Path `[1020xA]069crashy' is too long attempt 1: 09crashy' is too long -> LEAK_DETECTED (stress run) attempt 1: 69crashy' is too long -> LEAK_DETECTED (first run)
069 = tail of groomer pid 1069; 09/69 likewise.
Files
crashy.c/groomer.cβ minimal triggerspoc.shβ single-shot harness (build/baseline/setup/trigger/restore)groom_rig.shβ deterministic groom+trigger loop (run N),restorebuild.log,setup.log,run.log,run.2.logβ untrimmed guest outputleak_sample.txt,leak_full_line.txtβ raw dmesg linesenv.txtβ guest identityfix.diffβ one-line-class fix (NUL-terminate before logging)
DF-5803 VERDICT β reproduced (kernel-heap tail disclosure into world-readable log)
Bottom line
Reproduced. expand_name() (sys/kern/kern_sig.c:2444-2516) logs its
kmalloc(M_NOWAIT)-allocated, prefix-only-initialized, never-terminated
temp buffer via %s on all three overflow branches (kern_sig.c:2468, 2481,
2493). kprintf reads heap bytes past the initialized region until the first
NUL and writes them to the kernel log; sysctl kern.msgbuf is readable by
unprivileged users (verified: run_user RC=0). With a deterministic groom
(previous successful expansion of exactly 1023 chars + NUL, same kmalloc
bucket) the trigger's log line provably contains 3 bytes of the previous
allocation β pid 1072 (crashy): Path '[1020xA]069crashy' is too long, where
069 is the tail of groomer pid 1069. 3/3 batches reproduced
(run.log, run.2.log, leak_full_line.txt).
Root cause (path:line)
- sys/kern/kern_sig.c:2453 β
kmalloc(MAXPATHLEN + 1, M_TEMP, M_NOWAIT)β no M_ZERO. - sys/kern/kern_sig.c:2514 β
temp[n] = '\0'executed only on success; all three error branches (2468-2473, 2481-2486, 2493-2499)log()the buffer before any terminator exists, thenkfree.
Threat model
Unprivileged trigger (any core-dumping crash) + admin-set long
kern.corefile (root-only sysctl β the precondition that caps severity at
Low/Medium rather than High). Each crash event discloses up to
(first-NUL β 1020) bytes of recycled kernel heap into the log; typical
volume small (here 1β4 bytes/event), but the class is unbounded if a
previous 2048-bucket user left no zero bytes. Not a corruption primitive:
read-only, content not attacker-chosen (except via grooming), destination is
the log.
Why not higher / not lower
- Not High: the length precondition is unreachable without a root sysctl
write (default
%N.core+ β€19-char comm cannot exceed 1024). - Not Info: it is a genuine uninit-kernel-memory disclosure with an
unprivileged trigger and an unprivileged log reader (
kern.msgbufRC=0), and the volume is only empirically small β nothing bounds it structurally.
Exploit chain
None (info leak, low volume). Chain would be: admin sets a pathological corefile β repeated unprivileged crashes skim recycled heap bytes from the 2048 M_TEMP bucket β occasional pointers/slab remnants land in a user-readable log. Demonstrated volume: previous-expansion tails.
Fix validation
Not rebuilt (Low/Medium finding; kernel rebuild not warranted for the
evidence pack β fix is the textbook NUL-before-log). fix.diff terminates
temp[n] in each error branch before log() (and the same applies
trivially if maintainers prefer a single temp[n]='\0' after the loop).
Negative results recorded for this audit (pass 3)
- lwpsignal token/refcount balance hand-verified on every goto path (entry/SSTOP/SA_CONT/SIGKILL/find_lwp-swap/out) β balanced.
- proc_stopwait hang candidate KILLED: tsleep() SCORE interlock (kern_synch.c:596-610) counts sleeping threads as stopped, and LWKT magnet tokens (lwkt_thread.c:636-707) release on deschedule β no circular wait exists.
- coredump vsize integer-overflow candidate KILLED: mappings are disjoint within β€2^47 user VA, sum cannot wrap size_t (x86_64); same bound <2^32 holds on i386.
- elf coredump writer: phdr/vnh bounds guarded (phdr_max/vnh_max), dry-run/write consistent (map+threads frozen by SCORE), rlimit enforced all-or-nothing (imgact_elf.c:1014).
- kill(-1,β¦)/zombie: killpg_all_callback lacks the SZOMB filter dokillpg has, but exit1 sets P_WEXIT (kern_exit.c:201) before SZOMB so lwpsignal early-outs (kern_sig.c:1156) β harmless.
- pg_lock/token ordering: pgsignal holds lockmgr pg_lock across ksignal (p_token) β inverse orders release tokens at deschedule; deadlock-free.
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_testableFix not rebuilt: leak-class finding (not memory corruption, so rebuild not mandatory); the one-line NUL-termination fix is context-exact and git-apply --check clean against sys/kern/kern_sig.c; baseline bad behavior characterized in run.log/run.2.log/leak_full_line.txt.
fix.diff; baseline evidence run.log, run.2.log, leak_full_line.txt
Confirmed kernel references
Detail
Evidence (decisive lines)
run.log / run.2.log (LEAK_DETECTED lines with leaked bytes between prefix and comm), leak_full_line.txt (raw dmesg: '[1020xA]069crashy'), VERDICT.md mechanism + groom design, fix.diff (NUL-terminate temp[n] before each log(); git apply --check clean against sys/)
PoC changes
Seed sketch had no groom; the single-shot trigger usually hit a NUL at the prefix boundary, so a deterministic same-bucket groom was added (groomer.c + groom_rig.sh): size the previous successful expansion to exactly 1023 chars + NUL so the overflow branch provably prints offsets >=1020.
Verified recommended fix
NUL-terminate temp at temp[n] in each overflow branch before log() (see fix.diff); alternatively log only the configured format, not the partial expansion.
Verdict
expand_name() (sys/kern/kern_sig.c:2444-2516) kmallocs its output buffer without M_ZERO and NUL-terminates it only on the success path (2514); all three expansion-overflow branches log the buffer with %s before any terminator exists (2468/2481/2493), so kprintf walks uninitialized recycled heap past the initialized prefix and copies it into the kernel message buffer, which unprivileged users can read (sysctl kern.msgbuf, RC=0 verified). Reproduced deterministically on the guest with a same-bucket groom: a prior successful 1023-char expansion leaves its tail + NUL in the kmalloc bucket the overflowing trigger reuses, and the log line then contains bytes 1020.. of the previous allocation (e.g. '069' = tail of groomer pid 1069): 'pid 1072 (crashy), uid (0): Path `[1020xA]069crashy' is too long'. 3/3 batches. Precondition capping severity: reaching >MAXPATHLEN expansion requires a root-set kern.corefile (default %N.core cannot); trigger itself is an unprivileged abort().
No comments yet.