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

Kernel heap info leak to unprivileged user's tty via unterminated PT_INTERP path (uprintf %s)

Summary

extract_interpreter kmallocs exactly p_filesz, bcopy's the payload, never NUL-terminates nor verifies a NUL exists; the failed-interpreter uprintf('ELF interpreter %s not found') walks past the chunk printing adjacent kernel heap to the executing user's terminal. Reproduced deterministically: 23 extra bytes/run 3/3 at p_filesz=1000 (8/8 matrix), leaked stale M_TEMP kernel path strings + raw heap bytes; groomable (kmalloc+copyin groomer in pack). Any unprivileged user, self-owned crafted ELF, no special mounts. Reject-non-terminated fix validated on a rebuilt kernel (ENOEXEC, legitimate binaries unregressed). Root cause in imgact_elf.c; filed here as the exec-gate family entry driven by this file's brand/interp wiring.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5629 Β· 17 files
FileTypeDescriptionSize
make_interp.c β€” 3.0 KB view raw
make_interp2.c β€” 1.6 KB view raw
run_leak.c β€” 681 B view raw
groom.c β€” 1.1 KB view raw
run_guest.sh β€” 1.1 KB view raw
matrix.sh β€” 889 B view raw
analyze.sh β€” 792 B view raw
run.log β€” 1.8 KB view raw
leak_sample.txt β€” 588 B view raw
fline1.txt β€” 1.0 KB view raw
fline2.txt β€” 1.0 KB view raw
fline3.txt β€” 1.0 KB view raw
tty1.txt β€” 1.0 KB view raw
verdict.json β€” 3.8 KB view raw
VERDICT.md β€” 3.2 KB ↓ raw
fix.diff β€” 1.6 KB view raw
env.txt β€” 287 B view raw
VERDICT.md
↓ download raw

DF-5629 VERDICT

Status: reproduced (impact: leak) β€” fix validated

Guest: DragonFly 6.5-DEVELOPMENT x86_64 (X86_64_GENERIC), cc 8.3, kern.elf64.fallback_brand = -1. Executed as unprivileged user maxx (uid 1001); capture via script(1) pty (uprintf goes to the controlling terminal, subr_prf.c:146/tprintf:186).

How it was proven

  1. Crafted ELF64, EI_OSABI = 0 (brands DragonFly per imgact_elf.c:569-573, brand wired at sys/cpu/x86_64/misc/elf_machdep.c:58-68), PT_INTERP payload = 1000 non-NUL bytes (p_filesz=1000 <= MAXPATHLEN passes imgact_elf.c:663-666).
  2. extract_interpreter (imgact_elf.c:1817-1863) kmalloc(1000) β€” lands in the kmalloc-1024 zone; bytes [1000..1023] of the chunk are stale kernel heap; uprintf("ELF interpreter %s not found\n", interp) (imgact_elf.c:844) prints payload + stale bytes + whatever follows the chunk until a NUL.
  3. Baseline (unpatched kernel #0, Thu Jul 2 06:02:54 UTC 2026): - 3/3 deterministic samples, 23 extra bytes each. - Leaked content is decodable kernel heap residue: run1: tmp/df5629/make_interp2, run2: /tmp/df5629, run3: tmp/df5629/m + /tmp/df5629 (see leak_sample.txt) - An ungroomed run additionally leaked ff ff (2 bytes of adjacent kernel heap) β€” real heap bytes, not our own payload. - Size matrix (8 runs/size): size=1000 β†’ 8/8 runs leaked (184 bytes total); size=48 β†’ 7/8; size=32 β†’ 6/8; size=1024 β†’ 0/8 (neighbor's first byte tends to be NUL); confirming the read walks past the requested allocation into allocator/neighbor memory.
  4. The value leaks to the tty of the unprivileged user: local kernel heap info disclosure (stale M_TEMP contents: kernel paths and pointers of other subsystems; length is heap-layout dependent and can be extended by grooming β€” groom.c).

Fix validation (kernel #1, patched with fix.diff, built 23:30 UTC)

  • make nativekernel + installkernel + reboot; uname -v confirms #1.
  • Same PoC binary: direct exec β†’ Exec format error (ENOEXEC); extract_interpreter now rejects payloads with no NUL inside p_filesz (imgact_elf.c:done label). tty transcript contains 0 "ELF interpreter" lines (was 1 + 23 heap bytes).
  • No regression: normal dynamic binaries (ld-elf.so.2, NUL-terminated PT_INTERP) still exec (the whole system boots and runs).

Why this matters

Any unprivileged user can exec a file they own; no mount options, sysctls, or privileges involved. The leak length is bounded only by the distance to the next NUL byte in kernel heap after the kmalloc chunk β€” with grooming, multi-chunk disclosure is plausible (kernel pointers, file paths, other processes' data). Class: CWE-170/CWE-125-adjacent (termination + OOB read), kernleak bucket.

Root cause lines: sys/kern/imgact_elf.c:1817-1863 (no NUL-termination), consumer sys/kern/imgact_elf.c:844 (unbounded %s). Executed as: maxx (uid 1001), unprivileged.

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

Patched kernel (nativekernel rebuild with fix.diff, kernel #1): same binary -> ENOEXEC, 0 'ELF interpreter' lines on the pty (baseline: 1 line + 23 leaked heap bytes); normal NUL-terminated interpreters unaffected (system boots/runs).

run.log 'DF-5629 fix validation' section; VERDICT.md; fix.diff in pack.
↓ fix.diffper-fix-DF-5629

Confirmed kernel references

Detail

Exploit chain

execve(crafted ELF with 1000-byte non-NUL PT_INTERP, uid 1001) -> brand match via EI_OSABI==ELFOSABI_NONE (imgact_elf.c:569-573) -> kmalloc(1000) unterminated (imgact_elf.c:668, 1817-1863) -> load_file(interp) fails -> uprintf %s walks past chunk (imgact_elf.c:844) -> stale heap bytes of the recycled kmalloc-1024 chunk (bytes 1000..1023 + neighbor run until NUL) printed to the user's controlling terminal; leak length heap-layout dependent and groomable (setsockopt kmalloc+copyin groomer in pack).

Evidence (decisive lines)

run.log (size matrix 8 execs/size: size=1000 8/8 runs leaked, 184 bytes total; final 3 samples: extra=23 bytes each); leak_sample.txt + fline1-3.txt (hex of leaked bytes: kernel path strings); tty1.txt (raw transcript); VERDICT.md fix-validation section (patched kernel: 0 leak lines, ENOEXEC).

PoC changes

Seed concept only; authored full builder+trigger from scratch: make_interp2.c parameterizes PT_INTERP payload size (empirical matrix found size=1000 gives deterministic leak: kmalloc(1000) leaves 24 stale in-chunk bytes); run_leak.c execve trigger run under script(1) so uprintf lands on a capturable pty; groom.c (setsockopt kmalloc+copyin at uipc_syscalls.c:1257) for heap seeding.

Verified recommended fix

extract_interpreter(): reject PT_INTERP payloads that are not NUL-terminated within p_filesz before returning success (see fix.diff).

Verdict

Unprivileged local kernel heap info leak: extract_interpreter() (sys/kern/imgact_elf.c:1817-1863) never NUL-terminates the kmalloc'd PT_INTERP payload, so uprintf("ELF interpreter %s not found", interp) at imgact_elf.c:844 walks past the chunk and prints adjacent kernel heap bytes to the executing user's tty. Reproduced 3/3 runs (deterministic 23 bytes/run: stale kernel M_TEMP strings such as 'tmp/df5629/make_interp2' and '/tmp/df5629'), plus an ungroomed 2-byte sample (ff ff). Reachable by any user via execve of a self-owned crafted ELF (OSABI=0 brands DragonFly via the interp/brand config wired in sys/cpu/x86_64/misc/elf_machdep.c:58-68). Fix validated on a rebuilt kernel: binary now ENOEXEC, 0 leak lines, no regression.