{
  "finding_id": "DF-5746",
  "status": "reproduced",
  "reproduced": true,
  "impact": "leak",
  "confidence": "certain",
  "verdict": "iconv_sysctl_add (sys/libiconv/iconv.c:410) validates only the upper bound of ia_datalen, so kern.iconv.add accepts a table that is empty (datalen=0 -> cp_data==NULL -> deterministic NULL-deref panic Fatal trap 12, fault VA 0x0, iconv_xlat16_open+0x34 on the first iconv_open during msdosfs KICONV mount) or index-only (datalen=4096 -> iconv_xlat16_open builds d_table pointers up to ~256KB past the attacker-sized M_ICONVDATA allocation; every readdir then reads uint32s OOB and returns them as the converted filename). The leak was reproduced 4/4 runs on the baseline guest with heap-marker proof: filenames came back as ???????KMKMKMK... where 4b4d is the little-endian decode of marker strings stored in neighbouring kernel allocations; run R4 hit markers on all 36/36 leaked bytes. Read primitive only (all converter writes stay bounded by outbytesleft) and both triggers are root-gated (sysctl write needs SYSCAP_NOSYSCTL_WR, mount(2) needs root with vfs.usermount=0), so no unpriv->uid0 chain exists for this bug; the ceiling is kernel heap disclosure plus a root-triggerable instant panic. fix.diff (store cp_datalen in the cspair; reject short/empty tables in iconv_xlat16_open/iconv_xlat_open) was rebuilt as libiconv.ko on the guest and validated: panic gone, leak gone (both now fail the mount with EINVAL), while a full well-formed 13312-byte table still mounts and converts byte-identically to baseline.",
  "exploit_chain": "root: sysctl(kern.iconv.add) registers pair to=PWNLxx from=UTF-16BE converter=xlat16 with ia_datalen=4096 (index-only) and 16 marker pairs for heap grooming -> mount(2)(msdos, MSDOSFSMNT_KICONV|LONGNAME, cs_local=PWNLxx) -> update_mp opens the victim pair for pm_w2u -> iconv_xlat16_open maps d_table[X]=cp_data+4096+512*rank(X) past the 4096-byte allocation -> readdir of LFN ABCDEFGHIJKLMNOP.k -> win2unixchr -> iconv_xlat16_conv reads code=d_table[c1][c2] OOB -> (code>>8,code&0xff) written into the dirent name -> marker/neighbour heap bytes disclosed to any user able to list the directory. datalen=0 variant panics in the same open path (VA 0 read).",
  "evidence": "panic.txt (Fatal trap 12, VA 0x0, Stopped at iconv_xlat16_open+0x34); run.log + run.1.log (4 leak runs, marker bytes in filenames); leak_sample.txt (raw bytes per run); run.patched.log (both malicious triggers -> EINVAL, benign table -> identical output as baseline); module_build.log (patched libiconv.ko build, clean -Werror); VERDICT.md for the full narrative incl. killed facets.",
  "kernel_refs": [
    "sys/libiconv/iconv.c:410",
    "sys/libiconv/iconv.c:421",
    "sys/libiconv/iconv.c:426",
    "sys/libiconv/iconv_xlat16.c:67",
    "sys/libiconv/iconv_xlat16.c:71",
    "sys/libiconv/iconv_xlat16.c:192",
    "sys/libiconv/iconv_xlat16.c:243",
    "sys/libiconv/iconv_xlat.c:61",
    "sys/libiconv/iconv_xlat.c:99",
    "sys/vfs/msdosfs/msdosfs_vfsops.c:115",
    "sys/vfs/msdosfs/msdosfs_conv.c:946",
    "sys/kern/kern_sysctl.c:1447"
  ],
  "poc_changes": "rewrote the seed sketch into standalone triggers: struct msdosfs_args pulled via -I/usr/src/sys like mount_msdos; DragonFly specifics fixed (VFS name is msdos not msdosfs, unmount(2) not umount, struct dirent via <dirent.h> with _DIRENT_DIRSIZ); 64MB FAT16 (8MB too small for FAT16); heap-marker grooming pairs added to make the OOB read rigorous; added trigger_benign (full 13312-byte table) as the no-regression control and trigger_neg (negative datalen, killed: rejected by the unsigned compare at iconv.c:410).",
  "attempts": 4,
  "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": 2580,
  "guest_dirty": 0,
  "build_cmd": "cd /root && sh build.sh   # cc -O0 -g -I/usr/src/sys -o trigger_{null,leak,benign} ...",
  "run_cmd": "kldload /boot/kernel/msdos_iconv.ko; sh prep_img.sh; vnconfig -c vn0 /root/fat.img; ./trigger_leak R1 /dev/vn0 /mnt; ./trigger_null /dev/vn0 /mnt",
  "code_hash": "f7b0097aec48cf22ec569d69510fbfeb7af099249bd5f95eecdf298574f4d024",
  "notes": "Root cause and fix both live in the libiconv core: the kernel must not trust the serialized table layout because mount(2) callers pass arbitrary charset names/pairs directly (no userland helper involved). Severity High under a usermount threat model, Medium on default config (root-gated; vfs.usermount=0 on this guest). Known findings DF-2233/DF-2234 not re-reported; cslist SYSCTL_OUT sizing, add-path strlens, negative datalen, xlat16 index math, convstr/convmem bounds all checked and killed (see VERDICT.md).",
  "recommended_fix": "Record ia_datalen in struct iconv_cspair (cp_datalen) at iconv_sysctl_add and have iconv_xlat16_open/iconv_xlat_open reject pairs whose data is NULL or smaller than the layout they parse (index array + one 512-byte block per nonzero index entry; 256 bytes for xlat).",
  "fix_status": "fixed",
  "fix_baseline_reproduced": true,
  "fix_patched_reproduced": false,
  "fix_verdict": "fix.diff applied to the guest /usr/src, libiconv.ko rebuilt (module-only; the cspair struct is internal to libiconv.ko) and installed; rebooted. Baseline: trigger_null panicked the guest (Fatal trap 12 VA 0x0 in iconv_xlat16_open) and trigger_leak leaked marker heap bytes in readdir filenames (4 runs). Patched: trigger_null -> mount fails EINVAL, guest stays up; trigger_leak -> mount fails EINVAL, no directory output, no leak; trigger_benign (full well-formed 13312-byte table) mounts and converts byte-identically to baseline (B\\x01B\\x02...B\\x10B). No regression; both bad behaviors gone.",
  "fix_evidence": "run.patched.log (all three triggers on the patched module, guest uptime shown); module_build.log (clean -Werror build); VERDICT.md fix-validation table."
}