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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5803 Β· 18 files
FileTypeDescriptionSize
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.corefile must expand to more than MAXPATHLEN (1024) characters. The default %N.core cannot (p_comm ≀ 19). kern.corefile is a root-only sysctl β€” the configuration is privileged; the trigger (any core-dumping crash, e.g. abort()) is unprivileged and trivial.
  • RLIMIT_CORE != 0 for 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 triggers
  • poc.sh β€” single-shot harness (build/baseline/setup/trigger/restore)
  • groom_rig.sh β€” deterministic groom+trigger loop (run N), restore
  • build.log, setup.log, run.log, run.2.log β€” untrimmed guest output
  • leak_sample.txt, leak_full_line.txt β€” raw dmesg lines
  • env.txt β€” guest identity
  • fix.diff β€” one-line-class fix (NUL-terminate before logging)
VERDICT.md
↓ download raw

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, then kfree.

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.msgbuf RC=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_testable
baseline reproduced→ patch + rebuild →patched clean

Fix 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
↓ fix.diffper-fix-DF-5803

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().