Heap OOB read in elf_getnote: untrusted n_namesz advances offset past note buffer with no bounds check
| Field | Value |
|---|---|
| ID | DF-0070 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:L/I:N/A:H |
| CWE | CWE-125 Out-of-bounds Read |
| File | sys/kern/kern_checkpoint.c |
| Lines | 313-352 |
| Area | kern (checkpoint/restore) |
| Confidence | likely |
| Discovered | 2026-06-30 |
| Reported | pending |
Summary
elf_getnote (sys/kern/kern_checkpoint.c:313-352) parses ELF note headers out
of a kernel heap buffer note[notesz] whose size notesz is fully
attacker-controlled (phdr[0].p_filesz, passed at :240). The note header's
n_namesz and n_descsz fields (uint32_t) are read straight out of that
untrusted buffer (:325) and used to advance *off via
roundup2(note.n_namesz, sizeof(Elf_Size)) (:339) and
roundup2(note.n_descsz, sizeof(Elf_Size)) (:347), and to size a descriptor
bcopy (:346), WITHOUT ANY CHECK that *off stays within [0, notesz).
There is also no check that notesz is large enough to hold the first
Elf_Note header before the bcopy at :325.
The attacker sets n_namesz to a huge value (e.g. 0x10000000) while keeping
the literal name bytes CORE\0 at the right place so strncmp at :335 still
returns 0 (it stops at the null in "CORE" within 5 bytes, regardless of a giant
n_namesz). *off then jumps far past the buffer end; n_descsz must equal the
kernel struct size (:340), so the attacker sets it correctly, and the bcopy
at :346 reads sizeof(prstatus_t) bytes from heap memory well beyond the
allocation.
Separately, the nthreads formula at :185
(notesz - sizeof(prpsinfo_t)) / (sizeof(prstatus_t) + sizeof(prfpregset_t))
ignores per-note Elf_Note header + name/desc padding overhead, so
elf_demarshalnotes over-counts threads and walks past the real note data into
OOB territory.
Impact: kernel heap OOB read of up to a few hundred bytes; if the read crosses
an unmapped page boundary the kernel panics (reliable local DoS); if the
OOB-loaded data satisfies the size/version checks in elf_loadnotes, the leaked
heap bytes flow into p->p_comm via strlcpy at :306 (limited kernel-memory
info leak observable via ps/sysctl).
Reachability: sys_checkpoint(CKPT_THAW, fd, -1, 0) on a crafted checkpoint
image. Root/wheel-only under default ckptgroup=0 (:728-729), but the data is
untrusted in all cases and the path is open to arbitrary users if an admin sets
kern.ckptgroup=-1.
Recommended fix
Pass notesz (or an end pointer) into elf_getnote and validate every access
before touching memory:
--- a/sys/kern/kern_checkpoint.c
+++ b/sys/kern/kern_checkpoint.c
@@ static int
elf_getnote(void *src, size_t *off, const char *name, unsigned int type,
- void **desc, size_t descsz)
+ void **desc, size_t descsz, size_t srcsz)
{
Elf_Note note;
int error;
@@
- bcopy((char *)src + *off, ¬e, sizeof note);
+ if (*off + sizeof(note) > srcsz) { error = EINVAL; goto done; }
+ bcopy((char *)src + *off, ¬e, sizeof note);
*off += sizeof note;
@@
- if (strncmp(name, (char *) src + *off, note.n_namesz) != 0) {
+ if (*off + roundup2(note.n_namesz, sizeof(Elf_Size)) > srcsz ||
+ note.n_namesz > 32) {
+ error = EINVAL; goto done;
+ }
+ if (strncmp(name, (char *) src + *off, note.n_namesz) != 0) {
@@
*off += roundup2(note.n_namesz, sizeof(Elf_Size));
- if (note.n_descsz != descsz) {
+ if (*off + roundup2(note.n_descsz, sizeof(Elf_Size)) > srcsz ||
+ note.n_descsz != descsz) {
@@
if (desc)
bcopy((char *)src + *off, *desc, note.n_descsz);
Also pre-check if (notesz < sizeof(Elf_Note)) return EINVAL; in
elf_demarshalnotes and fix the nthreads derivation to account for per-note
overhead, or replace the count-based loop with offset-driven parsing that stops
once *off reaches notesz.
Proof of concept
See findings/poc/DF-0070/. A small C program builds a minimal valid ELF header
+ a single PT_NOTE program header with p_filesz chosen so the nthreads
formula lands in [1, CKPT_MAXTHREADS], and a note payload whose n_namesz is
0x10000000. Calling sys_checkpoint(CKPT_THAW, fd, -1, 0) drives elf_getnote
off the end of the note buffer β heap OOB read / panic.
Timeline
- 2026-06-30 Discovered during automated file-by-file audit of
sys/kern/kern_checkpoint.c. - pending Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0070 Β· 18 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df0070.c | trigger-source | malicious-checkpoint generator + inline sys_checkpoint(CKPT_THAW) trigger; default = panic mode (n_namesz=0x10000000), optional 'leak' mode for slab-adjacent OOB | 10.2 KB | view raw |
| probe.c | probe-source | prints sizeof(prpsinfo_t)/prstatus_t/prfpregset_t/Elf_Note etc. on the running kernel; verifies the nthreads formula constants | 1.2 KB | view raw |
| build.sh | build-script | cc -o df0070 df0070.c | 249 B | view raw |
| run.sh | run-script | ./df0070 evil.ckpt [panic|leak] | 633 B | view raw |
| build.log | build-log | probe.c build + struct-size probe output | 1.1 KB | view raw |
| run.log | run-log | decisive panic-mode run (RUN 2, post-reset) + panic signature | 1.9 KB | view raw |
| run.2.log | run-log | first panic-mode run (RUN 1, pre-reset) + panic signature; byte-identical code offsets to run.log | 1.5 KB | view raw |
| run.leak.log | run-log | slab-adjacent OOB leak-mode run: silent OOB, returns EINVAL, no panic | 1.5 KB | view raw |
| panic.txt | panic-signature | Fatal trap 0xc (page fault) in memmove+0x28 from elf_getnote bcopy; vm_object_hold_shared 'obj != NULL' assertion panic | 768 B | view raw |
| env.txt | environment | uname -a, cc --version, kern.ckptgroup/kern.osreldate sysctls, id | 687 B | view raw |
| fix.diff | suggested-fix | thread srcsz through elf_demarshalnotes -> elf_getnote; bounds-check header bcopy, n_namesz (cap 32), n_namesz_pad, n_descsz_pad against srcsz before each access. Validated: builds + boots as #1, panic gone | 4.0 KB | view raw |
| baseline_panic.txt | panic-signature | Phase 8 BEFORE snapshot: unpatched #0 kernel panic (vm_object_hold_shared obj!=NULL, memmove+0x28 in elf_getnote bcopy) from ./df0070 evil.ckpt | 1.5 KB | view raw |
| fix_build.log | build-log | Phase 8 single-fix kernel build: patch -p1 (7/7 hunks) + make -j6 nativekernel KERNCONF=X86_64_GENERIC -> NK_DONE rc=0 (full untrimmed, 35598 lines) | 5.6 MB | β download |
| fix_run.log | run-log | Phase 8 AFTER snapshot: ./df0070 evil.ckpt on single-fix #1 kernel returns EINVAL (3/3 panic-mode + leak-mode runs), no panic, guest stays up; boot.log panic-line count = 0 | 1.8 KB | view raw |
| VERDICT.md | verdict | full narrative: reproduced, mechanism, leak-variant analysis, fix rationale | 8.5 KB | β raw |
| README.md | readme | human-facing build/run/expected summary | 3.9 KB | β raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0070 PoC β elf_getnote heap OOB read via crafted checkpoint image
Status: REPRODUCED (kernel panic / local DoS). Verified on DragonFlyBSD
master DEV v6.5.0.1712.g89e6a-DEVELOPMENT (build 2026-06-29, X86_64_GENERIC).
See VERDICT.md for the full analysis.
What this proves
That elf_getnote (sys/kern/kern_checkpoint.c:313-352) advances a parse
offset using an attacker-supplied n_namesz (read from the untrusted note
buffer at :325, applied at :339) with no bounds check against the
allocated buffer size notesz. A crafted checkpoint image with
n_namesz = 0x10000000 causes the subsequent bcopy at :346 to read
sizeof(prpsinfo_t)=120 bytes from KVM 256 MB past the kmalloc(880) note
buffer allocated at :194. The bcopy (backed by memmove) hits unmapped
memory and the kernel page-faults in kernel mode (Fatal trap 12) β panic
(vm_object_hold_shared assertion obj != NULL).
Build
./build.sh # cc -o df0070 df0070.c (on a DragonFlyBSD guest)
Run
Default kern.ckptgroup=0 (wheel-only) β run as root.
./run.sh # ./df0070 evil.ckpt panic -- kernel PANICS
./run.sh leak # ./df0070 evil.ckpt leak -- silent slab-adjacent
# 116-byte OOB; returns
# EINVAL, no panic
Expected result (panic mode)
[*] DF-0070 PoC: building evil.ckpt (notesz=880, n_namesz=0x10000000, n_descsz=120, mode=panic)
[*] calling sys_checkpoint(CKPT_THAW, fd=3, pid=-1, retval=0) [syscall #467]...
<ssh dies -- guest in DDB>
# in dfbsd-qemu/boot.log:
panic: assertion "obj != NULL" failed in vm_object_hold_shared at /usr/src/sys/vm/vm_object.c:330
cpuid = 0
...
--- trap 000000000000000c, rip = ffffffff80bca038, ... ---
memmove() at memmove+0x28 0xffffffff80bca038
Debugger("panic")
db>
The page fault is in memmove+0x28 β that is the inner bcopy backing the
descriptor copy at kern_checkpoint.c:346. Reproduced twice with
byte-identical code offsets.
Expected result (leak mode)
The kernel silently performs a 116-byte slab-adjacent OOB read (no page
fault β the read stays inside the 1024-byte slab chunk), then
elf_loadnotes rejects the leaked garbage at the pr_version/pr_psinfosz
validation (:292-301) and returns EINVAL. The OOB read is real but
silent; control does not reach the strlcpy(p->p_comm, ...) at :306, so
the leak is not observable in ps/sysctl.
How the sizes were derived
probe.c prints the actual kernel-side struct sizes:
sizeof(prpsinfo_t) = 120 sizeof(prstatus_t) = 248 sizeof(prfpregset_t) = 512 sizeof(Elf_Note) = 12
So the nthreads formula at :185, (notesz - 120) / 760, yields 1 for
notesz = 880 β inside the [1, CKPT_MAXTHREADS=256] gate at :188.
Notes
n_descszmust equal the kernel struct size at the:340check, so for the first (NT_PRPSINFO) call it is120(sizeof(prpsinfo_t)).strncmp("CORE", src+*off, n_namesz)at:335stops at the'\0'in"CORE\0"within 5 bytes regardless ofn_namesz, so a giantn_nameszdoes not stop the match β it only inflates the subsequent*offadvance.- The original (pre-verification) PoC assumed wrong struct sizes
(
PRPSINFO_SZ=128,PRSTATUS_SZ=504); those were corrected. CKPT_THAWrequires membership inkern.ckptgroup(default0= wheel). Withkern.ckptgroup=-1any local user can trigger the panic.
Files
df0070.cβ the generator + inline trigger.probe.cβ struct-size probe (used to derivenotesz=880).build.sh,run.shβ exact reproduce commands.build.log,run.log,run.2.log,run.leak.logβ full untrimmed logs.panic.txtβ the panic signature excerpted fromboot.log.env.txtβ guest environment.fix.diffβgit apply-able fix (threadsrcsz/noteszintoelf_getnote, bounds-check every access).VERDICT.md,manifest.json.
DF-0070 β VERDICT
Verdict: REPRODUCED (panic / kernel-mode page fault).
The heap-OOB read described in DF-0070 is real on the audited DragonFlyBSD
master DEV kernel (6.5-DEVELOPMENT, build v6.5.0.1712.g89e6a-DEVELOPMENT
of 2026-06-29, X86_64_GENERIC). A crafted ELF checkpoint image, restored
with sys_checkpoint(CKPT_THAW, fd, -1, 0) (syscall 467), drives
elf_getnote's descriptor bcopy 256 MB past the kmalloc(880) note buffer
and panics the kernel with Fatal trap 12: page fault while in kernel mode
inside memmove. Reproduced twice (once before, once after vm.sh reset)
with byte-identical code offsets in the panic stack β only the KASLR
frame addresses differ between boots.
Mechanism (every hop cited path:line)
sys_checkpoint(CKPT_THAW)(sys/kern/kern_checkpoint.c:751) βckpt_thaw_proc(:218).ckpt_thaw_procreads the ELF header (elf_gethdr, :230), the program headers (elf_getphdrs, :236), then callself_getnotes(lp, fp, phdr->p_filesz)at :240 βnoteszflows straight from attacker-controlledphdr[0].p_fileszwith no validation.elf_getnotes(:176) derivesnthreadspurely fromnoteszat :185 β(notesz - sizeof(prpsinfo_t)) / (sizeof(prstatus_t) + sizeof(prfpregset_t)). With the verified amd64 sizes (prpsinfo_t=120,prstatus_t=248,prfpregset_t=512) andnotesz=880,nthreads = 1, passing the[1, CKPT_MAXTHREADS=256]gate at :188.note = kmalloc(notesz=880, M_TEMP, M_WAITOK)at :194 allocates an 880-byte heap buffer;read_check(fp, note, 880)(:198) fills it from the file.elf_demarshalnotes(note, psinfo, status, fpregset, 1)is called at :200.elf_demarshalnotes(:354) callself_getnote(src, &off, "CORE", NT_PRPSINFO, &psinfo, sizeof(prpsinfo_t)=120)(:363) withoff=0.elf_getnote(:313): -bcopy(src+0, ¬e, 12)(:325) reads our crafted header:n_namesz=0x10000000,n_descsz=120,n_type=NT_PRPSINFO(3). -*off = 12(:329). Type matches (:330). -strncmp("CORE", src+12, 0x10000000)(:335) βstrncmpstops at the embedded'\0'in"CORE\0"within 5 bytes, returns 0. No OOB here, because the literal"CORE\0"lives inside the buffer. -*off += roundup2(0x10000000, 8) = 0x10000000(:339) β*off = 0x1000000c. There is no check that*off <= notesz. This is the bug. -n_descsz == descsz(120 == 120) at :340 passes. -desc=&psinfois non-NULL, sobcopy(src + 0x1000000c, psinfo, 120)at :346 reads 120 bytes from KVM 256 MB past the 880-byte slab chunk.- The
bcopy(backed bymemmoveon amd64) page-faults in kernel mode on the unmapped access; the fault handler reachesvm_object_hold_sharedon an address with no backingvm_objectand panics with the assertionobj != NULL(vm/vm_object.c:330).
Privilege / reachability
sys_checkpointis gated bykern.ckptgroupat :728. Default0= wheel-only. The PoC runs as root.- The parsed data is untrusted in all configurations; an admin who sets
kern.ckptgroup=-1exposes the panic to any local user. - Realistic impact: local DoS (kernel panic) from any principal in the
configured ckptgroup (default: root/wheel). With the optional
kern.ckptgroup=-1setting it is an unprivileged local DoS.
Why the leak variant does not escalate to info-leak
We also exercised the slab-adjacent OOB (n_namesz = 880-12-8 = 860,
*off lands at 876, bcopy reads 120 bytes ending at 996 β 116 bytes
past the 880-byte buffer but inside the 1024-byte slab chunk, hence no
page fault). The 120-byte OOB read happens silently, but
elf_loadnotes validates the loaded structures at :292-301:
if (status->pr_version != PRSTATUS_VERSION || // 1
status->pr_statussz != sizeof(prstatus_t) || // 248
...
psinfo->pr_version != PRPSINFO_VERSION || // 1
psinfo->pr_psinfosz != sizeof(prpsinfo_t)) // 120
error = EINVAL;
Random slab content almost never satisfies these magic+size checks, so
control never reaches the strlcpy(p->p_comm, psinfo->pr_fname, ...) at
:306. The leak is real but silent β the dominant observable impact is
the panic.
What changed in the PoC
The supplied PoC assumed the wrong struct sizes (PRPSINFO_SZ=128,
PRSTATUS_SZ=504). On the running kernel the real sizes are
prpsinfo_t=120, prstatus_t=248, prfpregset_t=512, so the nthreads
formula yields (notesz-120)/760. The PoC was rewritten to:
- compute notesz = 120 + 248 + 512 = 880 so nthreads == 1,
- build the malicious ELF in-process (no host-side struct dependency),
- invoke sys_checkpoint(CKPT_THAW, fd, -1, 0) directly via
syscall(SYS_checkpoint=467, ...),
- default to the panic variant (n_namesz=0x10000000) and accept an
optional leak argument for the slab-adjacent variant.
Reproduce
./build.sh # cc -o df0070 df0070.c
./run.sh # ./df0070 evil.ckpt panic -- kernel panics
# optional:
./run.sh leak # ./df0070 evil.ckpt leak -- silent OOB, returns EINVAL
Run as root (default kern.ckptgroup=0).
Fix
See fix.diff. The fix threads the source-buffer size srcsz (the notesz
that elf_getnotes already holds) through elf_demarshalnotes into
elf_getnote, and validates every access before touching memory:
- *off + sizeof(note) <= srcsz before the header bcopy (:325),
- note.n_namesz is sane (β€ 32, the longest legitimate ELF note name) and
*off + roundup2(n_namesz, 8) <= srcsz before the strncmp and the
advance at :339,
- *off + roundup2(n_descsz, 8) <= srcsz before the descriptor bcopy
at :346.
This matches the spirit of the finding markdown's proposal but adds an
explicit n_namesz > 32 cap (defence-in-depth β strncmp is bounded by
the null byte so an attacker cannot read past the buffer that way, but a
multi-megabyte n_namesz is never legitimate in an ELF core note and is
rejected outright). The nthreads over-count at :185 is left unchanged:
once elf_getnote rejects *off > notesz, the surplus iterations in
elf_demarshalnotes loop bail cleanly with EINVAL and the over-sized
status[]/fpregset[] arrays are simply freed unused β no security
consequence.
Fix VALIDATION (Phase 8 β built + booted single-fix kernel)
The fix was validated end-to-end on a single-fix kernel built from the
audited /usr/src, not just git apply --check.
Baseline (unpatched #0, build Thu Jul 2 06:02:54 UTC 2026):
./df0070 evil.ckpt (panic mode, n_namesz=0x10000000) β ssh never
returned; guest in DDB. vm.sh status β down. boot.log:
panic: assertion "obj != NULL" failed in vm_object_hold_shared at /usr/src/sys/vm/vm_object.c:330
--- trap 000000000000000c, rip = ffffffff80bca928, rsp = fffff80117251750, rbp = fffff801172517b8 ---
memmove() at memmove+0x28 0xffffffff80bca928
Debugger("panic")
db>
trap 0xc = kernel-mode page fault; memmove+0x28 is the bcopy
backing the OOB descriptor read at kern_checkpoint.c:346. The bug
reproduces on the unpatched baseline. (baseline_panic.txt.)
Build: cd /usr/src && patch -p1 < /root/fix.diff applied cleanly
(7/7 hunks); make -j6 nativekernel KERNCONF=X86_64_GENERIC β
=== NK_DONE rc=0 ===. (fix_build.log, full untrimmed, 35598 lines.)
Install + reboot: copied the freshly-stripped kernel to the bare
/boot/kernel/kernel the DragonFly loader boots, then vm.sh down &&
vm.sh up:
DragonFly 6.5-DEVELOPMENT #1: Thu Jul 2 15:29:19 UTC 2026 sha256(kernel) = 65ae8b9ec68b224ec90a7e4044aa869b7bf26e3604b42d3fd436c15638fb5ba3
#1 build stamp confirms the swap took.
Patched run (panic mode, Γ3 determinism + leak mode): all return
gracefully, guest stays up, zero panic lines in boot.log:
[*] calling sys_checkpoint(CKPT_THAW, fd=3, pid=-1, retval=0) [syscall #467]... [!] sys_checkpoint returned -1, errno=22 (Invalid argument) RUN_EXIT=1 guest status: up (3/3 panic runs + leak run)
The new bounds checks reject the malformed note before any OOB access:
- panic mode (n_namesz=0x10000000) trips note.n_namesz > 32 β EINVAL,
- leak mode (n_namesz=860, namesz_pad=864): the name advance passes,
but the next note's descsz_pad=120 > srcsz - *off (880-876=4) β EINVAL,
stopping the descriptor OOB read at :346.
Fix verdict: VALIDATED. The previously-fatal panic on the unpatched
#0 kernel is gone on the single-fix #1 kernel; CKPT_THAW now
fails gracefully with EINVAL and the guest is fully responsive.
Deterministic over 3 panic-mode + 1 leak-mode runs. (fix_run.log.)
Fix verification
fixedVALIDATED. On the unpatched #0 baseline, ./df0070 evil.ckpt (n_namesz=0x10000000) panics: Fatal trap 12 page fault in memmove+0x28 (elf_getnote descriptor bcopy at kern_checkpoint.c:346 reading 256MB past the 880-byte note buffer) -> 'obj != NULL' assertion in vm_object_hold_shared, guest in DDB. fix.diff applied 7/7 hunks clean to /usr/src; make -j6 nativekernel -> NK_DONE rc=0; stripped kernel installed as the bare /boot/kernel/kernel; rebooted to #1. On the single-fix #1 kernel the SAME PoC returns errno=22 (EINVAL) deterministically (3/3 panic-mode + 1 leak-mode runs, 0 panic lines in boot.log, guest fully responsive). The new bounds checks reject the malformed note before any OOB access (panic mode trips n_namesz>32; leak mode trips descsz_pad>srcsz-*off). Fix closes the bug.
baseline (#0): panic: assertion "obj != NULL" failed in vm_object_hold_shared ... --- trap 000000000000000c, rip=ffffffff80bca928 --- memmove() at memmove+0x28 ; vm.sh status=down (DDB). patched (#1): [!] sys_checkpoint returned -1, errno=22 (Invalid argument) ; RUN_EXIT=1 ; guest status=up ; 3/3 panic + leak runs identical ; boot.log panic-line count=0. build: === NK_DONE rc=0 === (patch -p1 7/7 hunks clean).
Confirmed kernel references
Detail
Exploit chain
root/wheel local DoS (kernel panic) via elf_getnote heap OOB read on a crafted ELF checkpoint restored with sys_checkpoint(CKPT_THAW). The OOB is a read into unmapped KVM -> kernel-mode page fault -> panic; not a write primitive, so no escalation chain. Default kern.ckptgroup=0 makes it wheel-only; admin-set ckptgroup=-1 would expose it to unprivileged users. Silent slab-adjacent leak variant (leak mode, 116-byte OOB inside the 1024-byte slab chunk) is blocked too: the new descsz_pad>srcsz-*off check rejects it before the descriptor bcopy at :346, and elf_loadnotes' version/size validation at :292-301 already prevented the leaked bytes from reaching strlcpy(p->p_comm,...) at :306 even pre-fix.
Evidence (decisive lines)
BASELINE (#0, unpatched): [*] calling sys_checkpoint(CKPT_THAW, fd=3, pid=-1, retval=0)... <ssh dies, guest DDB>; boot.log: panic: assertion "obj != NULL" failed in vm_object_hold_shared at vm_object.c:330 / --- trap 000000000000000c, rip = ffffffff80bca928 --- / memmove() at memmove+0x28 0xffffffff80bca928 / Debugger("panic") / db>. PATCHED (#1, single-fix): [!] sys_checkpoint returned -1, errno=22 (Invalid argument); RUN_EXIT=1; guest status: up (3/3 panic runs + leak run, 0 panic lines in boot.log).
PoC changes
No source changes this run (df0070.c already verified-correct from the prior reproduction session). Phase 8 artifacts added under findings/poc/DF-0070/: baseline_panic.txt (BEFORE), fix_build.log (full single-fix nativekernel output, rc=0), fix_run.log (AFTER, EINVAL 3x + leak). VERDICT.md extended with the Phase 8 before/after validation section; env.txt updated with both #0 and #1 kernel identifiers + the fix-kernel sha256; manifest.json updated with fix_status=fixed and the three new artifacts. fix.diff unchanged (applied 7/7 hunks clean).
Verified recommended fix
fix.diff (supersedes the finding markdown's Recommended fix proposal in spirit, adding an explicit n_namesz>32 cap): thread the source-buffer size srcsz (= the notesz elf_getnotes already holds at kern_checkpoint.c:194) through elf_demarshalnotes into elf_getnote, and bounds-check every access against srcsz before touching memory β (1) srcsz-off >= sizeof(Elf_Note) before the header bcopy at :325, (2) note.n_namesz<=32 AND roundup2(n_namesz,8) <= srcsz-off before the strncmp+advance at :335/:339, (3) roundup2(n_descsz,8) <= srcsz-*off before the descriptor bcopy at :346. Validated: builds as #1, panic is gone, CKPT_THAW returns EINVAL. Full git-apply-able diff at findings/poc/DF-0070/fix.diff.
Verdict
REPRODUCED + FIX VALIDATED. elf_getnote (sys/kern/kern_checkpoint.c:313-352) advances its parse off using untrusted note.n_namesz/n_descsz read from the kmalloc(notesz) buffer (notesz flows straight from attacker-controlled phdr[0].p_filesz at :240) with no bounds check: off += roundup2(n_namesz, 8) at :339 then bcopy(src+*off, desc, n_descsz) at :346. A crafted ELF checkpoint (notesz=880, n_namesz=0x10000000, n_descsz=120) sys_checkpoint(CKPT_THAW)'d as root drives the descriptor bcopy 256MB past the 880-byte slab chunk into unmapped KVM, page-faulting in memmove+0x28 -> panic 'obj != NULL' in vm_object_hold_shared. Confirmed on unpatched #0 (guest in DDB, vm.sh status=down). On the single-fix #1 kernel (fix.diff applied, built+booted) the same PoC returns EINVAL deterministically (3 panic-mode + leak-mode runs, 0 panic lines in boot.log) β elf_getnote's new srcsz bounds checks reject the malformed note before any OOB access.
No comments yet.