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

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.

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, &note, sizeof note);
+   if (*off + sizeof(note) > srcsz) { error = EINVAL; goto done; }
+   bcopy((char *)src + *off, &note, 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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0070 Β· 18 files
FileTypeDescriptionSize
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
README.md readme human-facing build/run/expected summary
↓ download 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_descsz must equal the kernel struct size at the :340 check, so for the first (NT_PRPSINFO) call it is 120 (sizeof(prpsinfo_t)).
  • strncmp("CORE", src+*off, n_namesz) at :335 stops at the '\0' in "CORE\0" within 5 bytes regardless of n_namesz, so a giant n_namesz does not stop the match β€” it only inflates the subsequent *off advance.
  • The original (pre-verification) PoC assumed wrong struct sizes (PRPSINFO_SZ=128, PRSTATUS_SZ=504); those were corrected.
  • CKPT_THAW requires membership in kern.ckptgroup (default 0 = wheel). With kern.ckptgroup=-1 any local user can trigger the panic.

Files

  • df0070.c β€” the generator + inline trigger.
  • probe.c β€” struct-size probe (used to derive notesz=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 from boot.log.
  • env.txt β€” guest environment.
  • fix.diff β€” git apply-able fix (thread srcsz/notesz into elf_getnote, bounds-check every access).
  • VERDICT.md, manifest.json.
VERDICT.md verdict full narrative: reproduced, mechanism, leak-variant analysis, fix rationale
↓ download raw

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)

  1. sys_checkpoint(CKPT_THAW) (sys/kern/kern_checkpoint.c:751) β†’ ckpt_thaw_proc (:218).
  2. ckpt_thaw_proc reads the ELF header (elf_gethdr, :230), the program headers (elf_getphdrs, :236), then calls elf_getnotes(lp, fp, phdr->p_filesz) at :240 β€” notesz flows straight from attacker-controlled phdr[0].p_filesz with no validation.
  3. elf_getnotes (:176) derives nthreads purely from notesz at :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) and notesz=880, nthreads = 1, passing the [1, CKPT_MAXTHREADS=256] gate at :188.
  4. 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.
  5. elf_demarshalnotes (:354) calls elf_getnote(src, &off, "CORE", NT_PRPSINFO, &psinfo, sizeof(prpsinfo_t)=120) (:363) with off=0.
  6. elf_getnote (:313): - bcopy(src+0, &note, 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) β€” strncmp stops 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=&psinfo is non-NULL, so bcopy(src + 0x1000000c, psinfo, 120) at :346 reads 120 bytes from KVM 256 MB past the 880-byte slab chunk.
  7. The bcopy (backed by memmove on amd64) page-faults in kernel mode on the unmapped access; the fault handler reaches vm_object_hold_shared on an address with no backing vm_object and panics with the assertion obj != NULL (vm/vm_object.c:330).

Privilege / reachability

  • sys_checkpoint is gated by kern.ckptgroup at :728. Default 0 = wheel-only. The PoC runs as root.
  • The parsed data is untrusted in all configurations; an admin who sets kern.ckptgroup=-1 exposes 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=-1 setting 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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED. 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).
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Thu Jul 2 15:29:19 UTC 2026 (X86_64_GENERIC); /boot/kernel/kernel sha256 65ae8b9ec68b224ec90a7e4044aa869b7bf26e3604b42d3fd436c15638fb5ba3

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.