load_section truncated-file check bypassed by 64-bit wrap of (off_t)filsz + offset
| Field | Value |
|---|---|
| ID | DF-2720 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:N |
| CWE | CWE-190 Integer Overflow or Wraparound |
| File | sys/kern/imgact_elf.c |
| Lines | 274 |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-08-30 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | base:kern |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
The guard (off_t)filsz + offset > vp->v_filesize is evaluated in
unsigned 64-bit arithmetic regardless of the cast:
p_offset=0xFFFFFFFFFFFFF000 + p_filesz=0x2000 wraps to 0x1000 and
passes, so a segment is mapped file-backed at object pindex
0xFFFFFFFFFFFFF (~2^52 pages past EOF). Reproduced: exec succeeds with
no "truncated ELF file", the wrapped range appears in the process
core, touching it SIGSEGVs cleanly, the anon tail zero-fills. No
panic/leak/corruption on the INVARIANTS guest β hardening-grade
bypass of the loader's file-bounds invariant.
Recommended fix
- if ((off_t)filsz + offset > vp->v_filesize || filsz > memsz) {
+ if (offset + filsz < offset || offset + filsz > vp->v_filesize ||
+ filsz > memsz) {
Timeline
- 2026-08-30 Discovered during pass-2 audit of imgact_elf.c (GLM 5.3); bypass reproduced same run.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2720 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| make_wrapseg.py | β | 1.9 KB | view raw | |
| make_wrapseg2.py | β | 1.4 KB | view raw | |
| wrapseg | β | 8.0 KB | β download | |
| wrapseg2 | β | 8.0 KB | β download | |
| wrapseg3 | β | 8.0 KB | β download | |
| wrapseg4 | β | 8.0 KB | β download | |
| ctl_norm | β | 8.0 KB | β download | |
| wrap_tail | β | 8.0 KB | β download | |
| wrap_mid | β | 8.0 KB | β download | |
| run.log | β | 248 B | view raw | |
| core_phdrs.txt | β | 402 B | view raw | |
| fix.diff | β | 458 B | view raw | |
| verdict.json | β | 2.3 KB | view raw | |
| VERDICT.md | β | 1.3 KB | β raw | |
| README.md | β | 1.9 KB | β raw | |
| env.txt | β | 268 B | view raw |
DF-2720 β load_section file-size check bypassed by 64-bit wrap ((off_t)filsz + offset)
- File: sys/kern/imgact_elf.c:274
- Severity: Low (check bypass; no memory-unsafety demonstrated)
- Confidence: certain (bypass reproduced; impact assessed none)
- Class: CWE-190 integer overflow / hardening
Root cause
if ((off_t)filsz + offset > vp->v_filesize || filsz > memsz) /* imgact_elf.c:274 */
offset is vm_offset_t (unsigned 64-bit), so the addition is computed
modulo 2^64 regardless of the (off_t) cast. With
p_offset = 0xFFFFFFFFFFFFF000 and p_filesz = 0x2000 the sum wraps to
0x1000, which is β€ file size β the "truncated ELF file" check passes and
the segment is mapped file-backed with object pindex
0xFFFFFFFFFFFFF000 >> 12 (~2^52 pages past EOF).
Reproduce
python3 make_wrapseg2.py # t2_wrapfile / t2_wrapanon + controls # guest: /tmp/t2_wrapfile ; echo $? -> 139 (SIGSEGV) # /tmp/t2_wrapanon ; echo $? -> 65 (anon tail reads zero) # controls: t2_readfile -> 66, t2_readanon -> 65, t2_readtext -> 66
Exec succeeds (no "elf_load_section: truncated ELF file" uprintf β
contrast with a genuinely-too-large p_filesz, which prints it), the wrapped
file-backed range is present in the map (visible in the process core:
vaddr=0x600000 filesz=0x2000), and touching it SIGSEGVs (vm_fault on a
vnode page far beyond EOF fails instead of zero-filling). The anon tail maps
and reads as zero.
Impact
None demonstrated beyond a self-inflicted SIGSEGV: the pager handles the beyond-EOF pindex gracefully on this guest (no INVARIATS panic, no leak, no corruption). The check exists precisely to prevent walking "off the end of the file object" β the wrap defeats it, so this is defense-in-depth.
Fix
Detect the wrap:
- if ((off_t)filsz + offset > vp->v_filesize || filsz > memsz) {
+ if (offset + filsz < offset || offset + filsz > vp->v_filesize ||
+ filsz > memsz) {
DF-2720 VERDICT
Status: reproduced (check bypass confirmed) β impact: none (no memory unsafety) β confidence: certain.
What was run
t2_* family of crafted ELF64 binaries on the INVARIANTS guest, uid 1001:
- t2_exit77 β rc=77 (harness sanity: entry/exit path correct, DF SYS_exit=1)
- t2_readtext β rc=66 (read own text, nonzero)
- t2_readfile β rc=66 (valid file-backed PT_LOAD part reads real bytes)
- t2_readanon β rc=65 (valid anon bss tail reads zero)
- t2_wrapfile β rc=139 β PT_LOAD with p_offset=0xFFFFFFFFFFFFF000,
p_filesz=0x2000: exec succeeded (check bypassed β no "truncated ELF file"),
the range [0x600000,0x602000) IS mapped file-backed (seen in the core:
type=1 flags=6 vaddr=0x600000 filesz=0x2000), and reading it SIGSEGVs. - t2_wrapanon β rc=65 β the wrapped segment's anon tail maps and reads zero, proving load_section processed the segment fully.
Impact assessment
No kernel panic (checked serial log), no leak, no corruption. The beyond-EOF vnode pindex faults cleanly through vm_fault β SIGSEGV. The finding stands as a hardening/robustness issue: the truncated-file check is bypassable and the documented invariant ("fail so vm_map doesn't walk off the end of the file object", imgact_elf.c:265-270) is violated.
No exploit chain; no fix-validation build performed (impact none).
Fix verification
not_testableImpact none; no fix-validation build performed (hardening only).
[]
Confirmed kernel references
Detail
Evidence (decisive lines)
['findings/poc/DF-2720/run.log β t2_* matrix: wrapfile rc=139, wrapanon rc=65, all controls correct (exit77=77, readtext=66, readfile=66, readanon=65)', 'findings/poc/DF-2720/core_phdrs.txt β wrapped segment present in core at vaddr=0x600000 filesz=0x2000', 'VERDICT.md β narrative']
PoC changes
First asm harness used Linux syscall numbers (SYS_exit=60) producing SIGSYS noise; rebuilt with DF numbers (SYS_exit=1) and exit-code protocol (65=zero read, 66=nonzero, 139=read fault)
Verified recommended fix
Check for wrap explicitly: if (offset + filsz < offset || offset + filsz > vp->v_filesize || filsz > memsz) -> ENOEXEC
Verdict
The truncated-file check '(off_t)filsz + offset > vp->v_filesize' (imgact_elf.c:274) is computed in unsigned 64-bit arithmetic: p_offset=0xFFFFFFFFFFFFF000 + p_filesz=0x2000 wraps to 0x1000 and passes. Exec succeeds (no 'truncated ELF file'), the segment is mapped file-backed at object pindex 0xFFFFFFFFFFFFF (~2^52 pages past EOF, confirmed present in the process core), the anon tail maps and reads zero, and touching the wrapped file-backed range SIGSEGVs cleanly. No panic, leak, or corruption on the INVARIANTS guest - hardening-grade check bypass only.
No comments yet.