⬢ DragonFlyBSD Kernel Audit
DF-5344 / verdict.json
← back to finding ↓ download raw
{
  "finding_id": "DF-5344",
  "status": "reproduced",
  "reproduced": true,
  "impact": "leak",
  "confidence": "certain",
  "verdict": "Confirmed out-of-bounds read past the 48-byte static cpu_brand[] in printcpuinfo(): the CPUID brand leaves (0x80000002..4, hypervisor/firmware controlled) are memcpyd into cpu_brand with no NUL termination (identcpu.c:118-125), and the space-skip loop (:161) plus strcpy(cpu_model, brand) (:164) then walk past the array hunting for a terminator. Live proof on the QEMU guest: booting the stock kernel with -cpu qemu64,model-id=<48 chars> (accepted by QEMU 7.2, unterminated) yields hw.model of exactly 48 bytes read by an unprivileged uid=1001 user - only possible if strcpy read its terminator past cpu_brand[47]. A guard-page replica of the exact algorithm faults precisely at brand[48] for both consumption paths; the 47-char control survives. On the shipped kernel build the adjacent .bss object (hw_clockrate, zero at that instant) terminates the walk after 1 byte, so no bytes are disclosed and the write stays inside cpu_model[128]; any non-zero boot-time neighbor flows into world-readable sysctl hw.model, and >=80 contiguous non-NUL bytes would overflow cpu_model[128]. Fix validated on a rebuilt kernel: forced cpu_brand[47]=0 truncates the brand in-bounds (hw.model 47 bytes vs baseline 48) with no OOB.",
  "exploit_chain": "hypervisor/firmware supplies 48 non-NUL CPUID brand bytes -> printcpuinfo() strcpy reads past cpu_brand[47] into adjacent kernel .bss -> copied bytes exposed world-readable via sysctl hw.model; layout-dependent write overflow of cpu_model[128] if >=80 contiguous non-NUL bytes follow. No uid0 path: victim object is a fixed static string.",
  "evidence": "boot-brand48.log:32 (CPU: 48 As on stock kernel); run.2.log (unprivileged sysctl hw.model = 48 bytes); run.log (guard-page harness: modes 0,1 EXIT=139 at brand[48], control mode 2 EXIT=0); env.txt (nm layout cpu_brand -> hw_clockrate -> cpu_model); fix_run.log (patched kernel: 47 As); fix_build.log (full -Werror-clean rebuild); VERDICT.md",
  "kernel_refs": [
    "sys/platform/pc64/x86_64/identcpu.c:84",
    "sys/platform/pc64/x86_64/identcpu.c:118",
    "sys/platform/pc64/x86_64/identcpu.c:160",
    "sys/platform/pc64/x86_64/identcpu.c:164"
  ],
  "poc_changes": "No seed existed; authored from scratch: (1) live trigger via QEMU -cpu model-id of exactly 48 chars (QEMU 7.2 accepts it and copies it into the brand leaves without termination), (2) guard-page harness brand_guard.c replicating identcpu.c:118-164 verbatim since no ASAN runtime exists on guest or host.",
  "attempts": 3,
  "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul  2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (baseline) / #1: Sat Oct 3 05:06:43 UTC 2026 (fix-validation rebuild)",
  "runtime_sec": 1500,
  "guest_dirty": 0,
  "build_cmd": "cc -O0 -g -o brand_guard brand_guard.c   (in-guest, base gcc 8.3; kernel fix build: make -j6 in /usr/obj/usr/src/sys/X86_64_GENERIC)",
  "run_cmd": "qemu-system-x86_64 -enable-kvm -m 2048 -smp 2 -drive file=ovl-brand.qcow2,if=virtio,format=qcow2 -cpu qemu64,model-id=<48 x A> ... ; then sysctl -n hw.model | wc -c  (expect 49=bug, 48=fixed)",
  "code_hash": "6f3b24c9d56efeba44d1d67b23bdfd7c058f740112b74fd69efe6f4560e96957",
  "notes": "Baseline: hw.model 49 bytes incl. newline (48 brand bytes, terminator read OOB). Fixed: 48 bytes incl. newline. QEMU 7.2 accepts model-id >= 48 chars without error and does not zero-terminate the 48-byte copy - the hostile input is constructible by any VM operator. Same pattern exists in FreeBSDs x86 identcpu.c (upstream divergence note).",
  "recommended_fix": "Force NUL termination after filling cpu_brand from CPUID 0x80000002..4: cpu_brand[NELEM(cpu_brand) - 1] = \\0; (see fix.diff, validated)",
  "fix_status": "fixed",
  "fix_kernel_uname": "DragonFly 6.5-DEVELOPMENT #1: Sat Oct 3 05:06:43 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC (only change: findings/poc/DF-5344/fix.diff)",
  "fix_baseline_reproduced": 1,
  "fix_patched_reproduced": 0,
  "fix_verdict": "Single-fix kernel (only cpu_brand[NELEM(cpu_brand)-1]=\\0 added at identcpu.c:131, -Werror-clean, MD5 differs from stock) re-tested with the identical 48-char CPUID brand: hw.model now 47 chars + NUL read in-bounds (dmesg CPU: line shows 47 As vs baseline 48), guest boots and runs normally. The out-of-bounds read is gone.",
  "fix_evidence": "fix_build.log (full untrimmed rebuild: identcpu.c compiled -Werror, linking kernel.debug, RC=0); fix_run.log (patched-kernel sysctl/dmesg output); guest MD5s: stock 4625046e8061b34e1758d291bc6cad18 vs patched 41f844478778b38436f4370db9859299"
}