PT_INTERP interpreter string never NUL-terminated: kernel heap OOB read disclosed to unprivileged users via uprintf("ELF interpreter %s not found")
| Field | Value |
|---|---|
| ID | DF-2718 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N |
| CWE | CWE-125 / CWE-200 |
| File | sys/kern/imgact_elf.c |
| Lines | 668 (exact-size kmalloc), 1817-1863 (bcopy, no NUL check), 844 (%s) |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-08-30 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | kernleak |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
exec_elfN(imgact) kmallocs exactly p_filesz bytes for the PT_INTERP
string (no +1, no M_ZERO) and extract_interpreter bcopy's exactly
pathsz bytes with no NUL-termination check. The buffer is then
consumed as a C string by strcmp, nlookup/copystr via load_file, and
uprintf("ELF interpreter %s not found\n", interp) β the %s walk runs
past the allocation and prints adjacent kernel heap contents to the
executing user's controlling terminal.
Threat model & preconditions
Any unprivileged local user execs a crafted ELF with a NUL-free PT_INTERP: the failing-interpreter message discloses M_TEMP size-class heap residue until the first zero byte; leak length unbounded and content groomable.
Proof of concept
VERIFIED (findings/poc/DF-2718/): seed1024 (interp 'B'Γ1024, unterminated) then leak1000 (interp 'A'Γ1000) under script(1) as uid 1001 β 18/20 iterations printed "ELF interpreter A1000 B24 not found" (residue of the previously freed allocation) and one iteration leaked real kernel strings "xx/.ssh/autho/home/maxx". Leak-only primitive; no uid=0 route. Fix (reject unterminated interpreter strings in extract_interpreter, mirroring FreeBSD) validated.
Recommended fix
@@ extract_interpreter
pathsz = pinterpreter->p_filesz;
+ if (pathsz < 2)
+ return (result_failure);
if (limited_to_first_page) {
bcopy(imgp->image_header + pathloc, data, pathsz);
+ if (data[pathsz - 1] != '\0')
+ return (result_failure);
return (result_success);
}
@@ exec_unmap_page(lwb);
+ if (data[pathsz - 1] != '\0')
+ return (result_failure);
return (result_success);
Timeline
- 2026-08-30 Discovered during pass-2 audit of imgact_elf.c (GLM 5.3); unpriv leak reproduced + fix validated same run.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2718 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| make_poc.py | β | 2.8 KB | view raw | |
| make_poc2.py | β | 2.0 KB | view raw | |
| poc_unterm | β | 8.0 KB | β download | |
| poc_term | β | 8.0 KB | β download | |
| seed1024 | β | 8.0 KB | β download | |
| leak1000 | β | 8.0 KB | β download | |
| ctl1024 | β | 8.0 KB | β download | |
| run.sh | β | 610 B | view raw | |
| run2.sh | β | 361 B | view raw | |
| run.log | β | 2.3 KB | view raw | |
| run2.log | β | 20.7 KB | view raw | |
| leak_sample.txt | β | 20.7 KB | view raw | |
| fix.diff | β | 692 B | view raw | |
| verdict.json | β | 3.2 KB | view raw | |
| VERDICT.md | β | 2.1 KB | β raw | |
| README.md | β | 2.2 KB | β raw | |
| env.txt | β | 268 B | view raw |
DF-2718 β PT_INTERP string never NUL-terminated β kernel heap OOB read + disclosure via uprintf
- File: sys/kern/imgact_elf.c
- Severity: Medium (kernel info leak, unprivileged local)
- Confidence: certain β reproduced on the guest
- Class: kernleak / CWE-125 (OOB read) + CWE-200 (info disclosure)
Root cause
exec_elfN(imgact) allocates the interpreter-path buffer with the exact
PT_INTERP p_filesz and never requires NUL-termination:
- sys/kern/imgact_elf.c:663-673 β only checks
p_filesz == 0 || > PAGE_SIZE || > MAXPATHLEN, theninterp = kmalloc(phdr[i].p_filesz, M_TEMP, M_WAITOK)(no+1, no M_ZERO) - sys/kern/imgact_elf.c:1817-1863 (
extract_interpreter) βbcopys exactlypathszbytes and returns success; never checksdata[pathsz-1] == '\0'
The unterminated buffer is then consumed as a C string:
- sys/kern/imgact_elf.c:584 β
strcmp(interp, bi->interp_path) - sys/kern/imgact_elf.c:440 (via
__elfN(load_file):840) βnlookup_init(nd, file, UIO_SYSSPACE, ...)βcopystr()reads past the allocation (bounded by MAXPATHLEN, contents discarded) - sys/kern/imgact_elf.c:844 β
uprintf("ELF interpreter %s not found\n", interp)β%sformats the buffer until the first NUL past the allocation, writing adjacent kernel-heap contents to the executing user's controlling terminal.
Reproduce
python3 make_poc.py && python3 make_poc2.py # push seed1024 leak1000 poc_unterm to the guest, chmod +x sh run2.sh # seed/leak pairs, as unprivileged user, under script(1)
Expected (observed 18/20 iterations):
ELF interpreter AAAAβ¦(1000 A's)BBBBBBBBBBBBBBBBBBBBBBBB(24) not found
The 24 B bytes are residue of the previously freed 1024-class M_TEMP
allocation seeded by the prior exec β i.e. kernel heap contents from a
different, freed allocation, disclosed to an unprivileged user. One
iteration additionally leaked genuine kernel path-string residue:
ELF interpreter AAAAβ¦(1000)BBBBβ¦(24)xx/.ssh/autho/home/maxx not found
poc_unterm (single run, 1024 A's) shows the raw OOB walk stopping at the
first zero byte past the slab slot.
Fix
FreeBSD added the missing check in extract_interpreter
(if (data[pathsz - 1] != '\0') return (result_failure);). See fix.diff.
DF-2718 VERDICT
Status: reproduced β impact: leak β confidence: certain.
What was run
Guest: DragonFly 6.5-DEVELOPMENT #0 (INVARIANTS X86_64_GENERIC), uid 1001 (maxx).
make_poc.pyβpoc_unterm: ELF64, PT_INTERP p_filesz=1024, file bytes all 'A' with no NUL. Exec'd underscript(1)so the process has a controlling terminal (uprintf output reaches us).make_poc2.pyβseed1024(interp = 1024 'B', unterminated) andleak1000(interp = 1000 'A', unterminated). The seed exec occupies a 1024-size-class kmalloc(M_TEMP) slot and frees it (imgact_elf.c:845); the leak exec'skmalloc(1000)reuses the same 1024-class slot, so bytes 1000..1023 are the freed allocation's residue.
Observed
- run.log:
ELF interpreter AΓ1024 not foundβ uprintf walked one byte past the kmalloc'd buffer (stopped at a zero byte in a fresh slab page). - run2.log (20 seed/leak pairs): 18/20 iterations printed
AΓ1000 + BΓ24β the 24 B's are kernel-heap bytes from a different, freed M_TEMP allocation (imgact_elf.c:844 %s, imgact_elf.c:668 allocation). - iteration 15 additionally leaked real kernel path-string residue:
β¦BBBBBBBxx/.ssh/autho/home/maxx not foundβ genuine kernel heap data (nlookup/namei path residue in the same size class), not attacker-supplied.
Impact
Unprivileged local kernel-heap information disclosure. Length of the
disclosure is unbounded until the first NUL byte; contents are whatever
follows in the M_TEMP size class (paths, and potentially pointers/binary
data β iteration 15 shows binary non-ASCII bytes too). Also an unbounded
%s walk in kernel address space (imgact_elf.c:844) β in principle able to
walk into unmapped KVA; not observed across 40+ runs on this guest.
No uid=0 chain: leak-only primitive (contents are not attacker-steerable beyond grooming residue).
Fix validation
fix.diff adds the NUL-termination check in extract_interpreter
(mirrors FreeBSD). Patched kernel build + rerun: leak gone (see
fix_build.log / fix_run.log in this pack if present), baseline re-verified
before patching.
Fix verification
fixedPatched kernel (fix.diff: NUL-termination check in extract_interpreter, both copy paths + pathsz<2 reject) built via make nativekernel and booted. Baseline leak (18/20) eliminated: 0 leak lines in 8/8 seed/leak pairs; /bin/ls and /usr/bin/true exec normally.
['findings/poc/DF-2718/fix_run.log', 'findings/poc/DF-2718/fix.diff', 'findings/poc/DF-2718/fix_build.log']
Confirmed kernel references
Detail
Evidence (decisive lines)
['findings/poc/DF-2718/run2.log β 18/20 iterations print A*1000+B*24 after the 1000-byte interp (cross-allocation residue)', "findings/poc/DF-2718/run2.log iteration 15 β 'xx/.ssh/autho/home/maxx' real kernel heap strings", 'findings/poc/DF-2718/run.log β baseline: A*1024 exactly (OOB walk into fresh slab, stops at zero)', 'findings/poc/DF-2718/leak_sample.txt β raw captured bytes', 'VERDICT.md β narrative']
PoC changes
Seed PoCs rewritten from scratch: python3 ELF64 generators (make_poc.py single 1024-A interp, make_poc2.py seed1024/leak1000 pair for the 1024-size-class residue groom); run under script(1) so uprintf reaches a controlling tty
Verified recommended fix
Reject PT_INTERP strings lacking a NUL terminator in extract_interpreter: if (data[pathsz-1] != '\0') return failure (both copy paths), plus reject pathsz < 2
Verdict
PT_INTERP interpreter string is copied with exactly p_filesz bytes and never NUL-terminated (imgact_elf.c:668 kmalloc, extract_interpreter:1817-1863); consumers treat it as a C string and uprintf("ELF interpreter %s not found") at imgact_elf.c:844 discloses kernel heap bytes past the allocation to the unprivileged user's controlling terminal. Reproduced 18/20 iterations leaking the 24-byte residue of a previously-freed M_TEMP allocation plus one iteration leaking genuine kernel path-string residue ('.ssh/autho', '/home/maxx'). Leak-only primitive; no uid=0 chain.
No comments yet.