⬢ DragonFlyBSD Kernel Audit
DF-3103 / verdict.json
← back to finding ↓ download raw
{
 "finding_id": "DF-3103",
 "status": "reproduced",
 "reproduced": 1,
 "impact": "leak",
 "confidence": "certain",
 "verdict": "udf_vget() computes the fentry allocation size as UDF_FENTRY_SIZE + l_ea + l_ad in wrapping 32-bit arithmetic from disk-controlled fields, so crafted l_ea/l_ad wrap it to a small kmalloc while udf_readatoffset()s embedded-ICB path (udf_vnops.c:1037-1039) still uses &fentry->data[l_ea] with *size = l_ad (sign-extended negative int); uiomove() then copies user-chosen byte counts from up to 4 GB past the undersized M_UDFFENTRY allocation. Reproduced as uid 1001: read(EMB, 96/1024/4096) each returned OOB kernel heap bytes; EMB2 with l_ea=0x40000000 produced EFAULT, proving the kernel dereferences the attacker-chosen wild pointer. Patched module rejects the malformed file entry with EINVAL at vget.",
 "exploit_chain": "crafted UDF image (attacker-supplied media or downloaded image mounted by root) -> unprivileged open()+read() of the embedded file -> deterministic kernel heap disclosure at attacker-chosen offset/length; wild-offset variant yields EFAULT/page-fault (DoS on kernels without copy-fault guards). Read primitive only; serves as the info-leak half of an exploit pair with the DF-0831 write primitive on the same surface.",
 "evidence": [
  "run.log: read() returned 96 bytes from embedded file x3 + DF3103_LEAK_CONFIRMED x3 (deterministic), 1024- and 4096-byte reads also returned OOB heap",
  "run.log: read(96) failed: Bad address for EMB2 (l_ea=0x40000000 -> EFAULT on the wild kernel read)",
  "fix_run.log: open /mnt/EMB: Invalid argument and open /mnt/EMB2: Invalid argument on the patched module",
  "craft_img.py: EMB l_ea=0x1000 l_ad=0xFFFFF150 (176+...=0x100000200 -> int 0x200); EMB2 l_ea=0x40000000 l_ad=0xC0000140"
 ],
 "kernel_refs": [
  "sys/vfs/udf/udf_vfsops.c:527",
  "sys/vfs/udf/udf_vfsops.c:528",
  "sys/vfs/udf/udf_vnops.c:1037",
  "sys/vfs/udf/udf_vnops.c:1039",
  "sys/vfs/udf/udf_vnops.c:403"
 ],
 "poc_changes": "Seed PoC authored fresh for pass 2 (extends the DF-0831 crafter). One iteration: added <fcntl.h>; added the EMB2 wild-pointer variant after the first successful leak runs to prove the primitive beyond the slab-mapped neighborhood.",
 "attempts": 2,
 "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",
 "runtime_sec": 240,
 "guest_dirty": 0,
 "build_cmd": "cc -O2 -Wall -o /tmp/df3102_poc /tmp/df3102_poc.c (guest); python3 craft_img.py df3102.udf (host)",
 "run_cmd": "./run.sh (host driver: mount as root; su maxx -c /tmp/df3102_poc emb /mnt/EMB 96|1024|4096 and ... emb /mnt/EMB2 96)",
 "code_hash": "b50456f09049a3a3f9cecccbf847b544c5408e238f3831a07660d0c0ed0b9d2a",
 "notes": "Distinct from pass-1 DF-0880 (bcopy source over-read at the same vget): this is the vnops sink \u2014 allocation-size wrap plus negative *size sign-extended through uiomove. The leaked neighborhood was zeroed slab on this run; determinism and the EFAULT variant carry the proof. Guest reset with-src after validation.",
 "recommended_fix": "Validate l_ea/l_ad in udf_vget against bsize - UDF_FENTRY_SIZE (FE is one block), and give udf_readatoffset()s embedded path offset/remaining-length handling (FreeBSD-style).",
 "fix_status": "fixed",
 "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #0 (same kernel; udf.ko module rebuilt from patched /usr/src/sys/vfs/udf and kldloaded)",
 "fix_baseline_reproduced": 1,
 "fix_patched_reproduced": 0,
 "fix_verdict": "Combined fix (vget validation + embedded-path offset handling + zero-size loop break) applied, udf.ko rebuilt, same image remounted: open(/mnt/EMB) and open(/mnt/EMB2) now fail with EINVAL at vget before any allocation or read; no OOB access occurs (fix_run.log).",
 "fix_evidence": [
  "fix_run.log",
  "fix_build.log",
  "fix.diff",
  "df31xx_combined_fix.diff"
 ]
}