{
  "finding_id": "DF-3015",
  "status": "reproduced",
  "reproduced": 1,
  "impact": "panic",
  "confidence": "certain",
  "verdict": "Controlled kernel heap overflow reproduced from an unprivileged symlink(2) on a crafted UFS1 image: fs_maxsymlinklen is copied unvalidated from the superblock into mnt_maxsymlinklen (ffs_vfsops.c:504,724), so ufs_symlink (ufs_vnops.c:1562-1565) bcopy's up to 1023 attacker-controlled bytes into the 60-byte i_shortlink, spilling ~919 bytes past the 320-byte slab chunk of struct inode. Proven by: symlink uid/gid reported as 0x41414141 by stat (attacker bytes inside a live inode), INVARIANTS freelist-poisoning panic (0x5353... in chunk_mark_allocated via ffs_vget), fatal trap 9 GPF in ffs_update from a smashed live neighbour, and a calibration leak verifying the exact write offset into a chosen neighbouring inode (i_number match at leak[144..152)). Two distinct panics, deterministic placement, controlled content and length.",
  "exploit_chain": "crafted image (fs_maxsymlinklen=0x7fff) mounted by root -> unprivileged symlink(2) with 61..1023-byte target -> bcopy overflow past struct inode into the 320-byte slab class -> spray/groom to steady state (150 filler inodes) -> leak neighbouring inode header via DF-0778 readlink primitive to calibrate offsets and confirm adjacency -> 282-byte weapon write sets neighbouring inode di_mode=IFREG|ISUID|0755 while leaving root di_uid untouched -> exec would give euid=0; final step blocked because the same linear write destroys i_fs/i_dev/i_devvp and those kernel pointers contain NUL bytes (size-class-aligned heap addresses) so the string-based bcopy cannot rewrite them; repairing them needs an allocator-forged overlapping inode whose field layouts collide (documented in VERDICT.md). Deliverable demonstrated: controlled memcorrupt + reliable panic + disclosed attacker bytes in a live inode.",
  "evidence": [
    "findings/poc/DF-3015/run.log \u2014 baseline log incl. ls showing symlink uid/gid = 1094795585 (0x41414141) and calibration leak with i_number match at leak[144..152)",
    "findings/poc/DF-3015/panic.txt \u2014 INVARIANTS panic: chunk_mark_allocated zone assertion after freelist c_Next poisoned with 0x53 bytes (ffs_vget path)",
    "findings/poc/DF-3015/panic2.txt \u2014 Fatal trap 9 GPF at ffs_update+0x3e after 1023-byte trigger smashed a live inode",
    "findings/poc/DF-3015/trigger3015.c + setup3015.sh \u2014 minimal unprivileged trigger + crafted-image builder",
    "findings/poc/DF-3015/exp3015.c \u2014 grooming/calibration harness (train, adjacency verification, pointer leak)",
    "findings/poc/DF-3015/fix.diff \u2014 clamp at mount + guard in ufs_symlink; validated by in-guest kernel rebuild (run.patched.log)"
  ],
  "kernel_refs": [
    "sys/vfs/ufs/ufs_vnops.c:1561",
    "sys/vfs/ufs/ufs_vnops.c:1565",
    "sys/vfs/ufs/ffs_vfsops.c:504",
    "sys/vfs/ufs/ffs_vfsops.c:724",
    "sys/vfs/ufs/dinode.h:112",
    "sys/vfs/ufs/inode.h:105",
    "sys/kern/kern_slaballoc.c:1659"
  ],
  "poc_changes": "Seed was authored fresh in this run (no prior PoC). syscall-free trigger: plain symlink(2). Superblock patch offset computed from guest headers (fs_maxsymlinklen @ 8192+1320=9512). Had to learn in-guest struct geometry (sizeof(inode)=304, i_din@176, 320-byte chunk class) and slab LIFO/round-robin behaviour to build the grooming harness; the naive adjacency assumption was corrected after the leak returned a self-echo.",
  "attempts": 6,
  "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": 5400,
  "guest_dirty": 0,
  "build_cmd": "sh setup3015.sh; cc -O1 -o trigger3015 trigger3015.c && cc -O1 -o exp3015 exp3015.c",
  "run_cmd": "./trigger3015 /mnt/p/l1   (as uid 1001)",
  "code_hash": "trigger3015.c 417182a98ce72df32c751cc2dc7e28707e8b6bac81e09dfdd5a5d82f8a88308c; exp3015.c 38334992ae82c45babc1b8feea90c16bd833b878cad7b220105815677be8ca30",
  "notes": "Threat model = crafted FFS media (same as DF-0778/0820/0887): root mounts image, unprivileged user triggers. Two independent panics captured from one trigger class. Freelist poisoning gives arbitrary in-zone allocation on non-INVARIANTS builds. DF-3016 (ufs_readdir eoffset int truncation) was investigated in the same run and REFUTED: the (int) cast at ufs_vnops.c:1652 is only reachable when b_loffset+b_bcount > i_size, which bounds i_size-b_loffset to (0, b_bcount) \u2014 no truncation possible; runtime getdents on a di_size=0x80000000 directory returns cleanly (files in findings/poc/DF-3016/).",
  "recommended_fix": "Clamp mnt_maxsymlinklen to UFS1_MAXSYMLINKLEN at both mount assignment sites and independently bound ufs_symlink's fast path by UFS1_MAXSYMLINKLEN (fix.diff).",
  "fix_status": "fixed",
  "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #1: Sat Sep  5 09:25:16 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64",
  "fix_baseline_reproduced": 1,
  "fix_patched_reproduced": 0,
  "fix_verdict": "fix.diff applied to guest /usr/src, nativekernel rebuilt (#1 Sep 5 09:25:16), rebooted; identical crafted image + identical unprivileged trigger now stores the 1023-byte symlink via the long path with uid/gid intact (baseline showed 0x41414141 and two kernel panics); no panic after trigger; legitimate <=59-byte fast symlinks still work. Baseline bad behaviour GONE.",
  "fix_evidence": "findings/poc/DF-3015/run.patched.log"
}