DF-2622 / verdict.json
{ "finding_id": "DF-2622", "status": "reproduced", "reproduced": 1, "impact": "none", "confidence": "certain", "verdict": "CONFIRMED live on the stock kernel: both mount(2) inputs that make label==devstr (volume string exactly \"@\" and volume string \"\") reach vfsops.c:1020 and execute slice = label[-1] = devstr[-1], reading one byte BELOW the 80-byte devstr[MNAMELEN] stack array. The byte's class leaks to the console through the label choice printed at :1037 ('a'->BOOT, 'd'->ROOT, else->DATA, ~1.6 bits) before the mount fails ENOENT in hammer2_init_devvp(\"\"). The instrumented kernel printed the actual byte: 0x00 on one call and 0xff on the immediately following call - live stack garbage below the array varying with stack history, proving the read is out of bounds of any valid object. Honest ceiling: no memory content reaches userland, the console is the same privileged context that issued the mount, and no corruption occurs - Low severity as filed is correct; the defect itself (unconditional label[-1]) is real and trivially fixed. fix.diff guards with (label > devstr) and defaults slice=0; on kernel #2 the '@'/'' triggers show identical user-visible behavior (label=DATA, ENOENT) with devstr[-1] never dereferenced, and in-bounds cases (root mount 'vbd0s1d'->ROOT, '/dev/vn0@'->DATA) are unchanged.", "exploit_chain": "", "evidence": [ "run_stock.log - './mnt2622 @ /mnt/h2x' and './mnt2622 \"\" /mnt/h2x' both FAILED errno=2 while the console printed hammer2_mount: device=\"\" label=\"DATA\" (the OOB-read-dependent label decision)", "console_excerpts.txt [B] - instrumented kernel: DF2622: label==devstr=1 devstr[-1]=00 on one call, devstr[-1]=ff on the next (varying stale stack byte, both labeled OUT-OF-BOUNDS by the instrumentation)", "console_excerpts.txt [B] - in-bounds references: boot root mount devstr=\"vbd0s1d\" label[-1]='d' -> ROOT; \"/dev/vn0@\" -> label[-1]='0' -> DATA", "console_excerpts.txt [C] - fix kernel #2: same triggers, label=DATA, no dereference below devstr", "mnt2622.c - direct mount(2) with attacker-controlled hammer2_mount_info.volume" ], "kernel_refs": [ "sys/vfs/hammer2/hammer2_vfsops.c:1007-1031", "sys/vfs/hammer2/hammer2_vfsops.c:989", "sys/vfs/hammer2/hammer2_vfsops.c:1037" ], "poc_changes": "No seed code. Wrote a direct mount(2) trigger (struct hammer2_mount_info per hammer2_mount.h) because the userland mount(8) path was unnecessary; confirmed copyinstr at :989 accepts both \"@\" and \"\" and the :1008 guard does not reject them. Captured console via the QEMU serial log.", "attempts": 1, "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": 300, "guest_dirty": 0, "build_cmd": "cc -O -o mnt2622 mnt2622.c (guest)", "run_cmd": "./mnt2622 '@' /mnt/h2x ; ./mnt2622 '' /mnt/h2x ; dmesg | tail (console oracle)", "code_hash": "mnt2622.c sha256 74bfb8e08f00bd8ed418439da7697c19579303768dcb77e73fc53a188a482851; run_2622.sh sha256 23d26d0a12623991639da3c92a895ceede18d0a855bb419a3dc041bb6701b6d6", "notes": "Reachable only with mount privilege (root, or a user with the fs mount cap when vfs.usermount=1); the oracle output (console kprintf) is normally visible only in that same privileged context, hence impact=none rather than leak. The byte at devstr[-1] on this build sits below the array in unused/red-zone stack; it changed between two back-to-back mount(2) calls (0x00 -> 0xff).", "recommended_fix": "In hammer2_vfs_mount only read label[-1] when label > devstr; otherwise default the slice to 0 (empty device string has no partition letter).", "fix_status": "fixed", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #2: Sat Aug 29 00:17:00 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 (label > devstr guard, slice=0 default) included in the combined kernel #2 build (fix_build.log). Re-running the exact triggers: '@' and '' mounts fail ENOENT exactly as before and the console still shows device=\"\" label=\"DATA\" (the sane default), with devstr[-1] now never dereferenced; the in-bounds slice-letter feature is preserved (boot root mount still selects ROOT via 'd'). No OOB read is possible on the patched path by construction.", "fix_evidence": [ "fix.diff", "console_excerpts.txt [C]", "fix_build.log / fix_install.log" ] } |