β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5755

No overlong (non-shortest) UTF-8 form validation in utf8_to_ucs4 β†’ arbitrary character synthesis into converted filenames (U+002F '/' into FAT LFN and 8.3 metadata via msdosfs KICONV) and encode_surrogate uint32 wrap for 4-byte overlongs

Summary

utf8_to_ucs4 derives the width from the leading byte and concatenates payloads without checking the value requires that width: '\xC0\xAF' decodes to U+002F ('/'), and 4-byte encodings of BMP values make encode_surrogate's (code-0x10000) uint32-wrap into a phantom surrogate pair ('\xF0\x8F\xBF\xBF' -> DB FF DF FF). Live-verified as uid 65534 on a KICONV msdosfs mount: 'OV\xC0\xAFSLASH' creates successfully and readdir returns 'OV/SLASH' (a literal path separator written into BOTH the LFN and the raw 8.3 alias - invalid on every other FAT consumer), and overlongs alias distinct names (spoofing/hiding). Base-system userland iconv(3) returns EILSEQ for the same inputs. shortest-form rejection fix validated (witness ret=-1, creates EINVAL, controls unchanged). Upstream-shared with FreeBSD.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5755 Β· 26 files
FileTypeDescriptionSize
README.md β€” 4.7 KB ↓ raw
VERDICT.md β€” 4.2 KB ↓ raw
verdict.json β€” 4.6 KB view raw
fix.diff β€” 610 B view raw
kiconv5755.c β€” 4.2 KB view raw
dump5755.c β€” 2.8 KB view raw
difftest.c β€” 1.8 KB view raw
cesu_witness.c β€” 2.5 KB view raw
mountdbg.c β€” 901 B view raw
dbg.sh β€” 638 B view raw
build.sh β€” 66 B view raw
run.sh β€” 205 B view raw
run_patched.sh β€” 1010 B view raw
capture_baseline.sh β€” 865 B view raw
run.log β€” 2.7 KB view raw
witness.log β€” 431 B view raw
hashes.txt β€” 188 B view raw
logs/env.txt β€” 528 B view raw
logs/difftest.log β€” 534 B view raw
logs/difftest_patched.log β€” 534 B view raw
logs/witness_patched.log β€” 434 B view raw
logs/run5755_patched.log β€” 1.3 KB view raw
logs/run5756_baseline.log β€” 852 B view raw
logs/run5756_patched.log β€” 852 B view raw
logs/build_patched.log β€” 6.6 KB view raw
logs/module_build_patched.log β€” 9.0 KB view raw

DF-5755 β€” kernel libiconv UCS converter accepts overlong (non-shortest) UTF-8 β†’ arbitrary character synthesis into converted filenames

File audited (pass 2): sys/libiconv/iconv_ucs.c Reserved ID: DF-5755 Guest: DragonFly 6.5-DEVELOPMENT x86_64 (see logs/env.txt)

Root cause (one line)

utf8_to_ucs4() (sys/libiconv/iconv_ucs.c:411-478) decodes the leading byte into a width w (2/3/4) and concatenates the continuation payloads without ever checking that the decoded value is not representable in fewer bytes β€” so "\xC0\xAF" (2-byte overlong) decodes to U+002F /, and 4-byte encodings of BMP values reach encode_surrogate() (iconv_ucs.c:524-528, called from :218) with code < 0x10000, whose code - 0x10000 uint32-wraps into a phantom surrogate pair ("\xF0\x8F\xBF\xBF" β†’ DB FF DF FF).

Threat / reachability

Any kernel consumer that feeds attacker bytes through a UTF-8β†’X kiconv converter gets characters the input bytes never spelled. The in-tree end-to-end path is msdosfs mounted with MSDOSFSMNT_KICONV and cs_local=UTF-8 (the UTF-16BE↔UTF-8 pairs are auto-registered by iconv_ucs_init(), iconv_ucs.c:377-383):

  • create: unix2winchr() β†’ convchr(pm_u2w) (msdosfs_conv.c:990) β†’ iconv_ucs_conv() FROM_UTF8 branch (iconv_ucs.c:193-230) β†’ the decoded character is written into the on-disk LFN by unix2winfn() (msdosfs_conv.c:559-579, no validation) and into the 8.3 alias by unix2dosfn() (msdosfs_conv.c:431-458, only magic 1/2 are special-cased).
  • read back: the only / filter is win2unixfn()'s literal-unit check (msdosfs_conv.c:669-713) β€” it rejects an LFN containing /, and msdosfs then falls back to the 8.3 alias, which also carries the synthesized / and is returned verbatim by readdir.

Proven on the guest with uid 65534 (nobody) doing the creates on a root-mounted volume (root mounts; unprivileged users with write access corrupt the metadata β€” the same threat model as DF-5746).

What the evidence shows (baseline kernel)

  1. witness.log (kernel module, cesu_witness.c): * "\xC0\xAF" β†’ ret=0 out=00 2f (synthesized /, byte-identical to the valid "\x2F" case) * "\xF0\x8F\xBF\xBF" β†’ ret=0 out=db ff df ff (surrogate wraparound)
  2. run.log (msdosfs e2e): nobody creates "OV\xC0\xAFSLASH", "OV\xC0\xAEDOT", "OV\xE0\x80\xAFS3" β€” readdir returns "OV/SLASH" (hex 4f 56 2f 53 4c 41 53 48) and "OV/S3" β€” filenames containing a literal path separator, plus the alias "OV.DOT" (a name that never existed as a component).
  3. Raw image dump (run.log tail): synthesized U+002F present in the on-disk LFN units (4f 00 56 00 2f 00 …, seq=0x41, attr=0x0f) and in the raw 8.3 alias (4f 56 2f 53 4c 41 53 48 20 20 20).
  4. logs/difftest.log: base-system userland iconv(3) returns EILSEQ for all three overlong inputs β€” the kernel is the outlier.

Impact: FAT metadata corruption (/ in LFN and 8.3 names is invalid on every FAT consumer), filename spoofing/hiding across mounts and OSes (chkdsk/Windows interop, backup tools consuming readdir paths that contain /), and name aliasing ("OV\xC0\xAEDOT" becomes the distinct name "OV.DOT"). No memory corruption; severity Medium (CWE-176).

Files

file what
kiconv5755.c mount(2) KICONV UTF-8/UTF-8 + setuid(nobody) creates + readdir hex dump
dump5755.c raw-image scanner for the UTF-16LE LFN / 8.3 patterns
cesu_witness.c kernel module calling iconv_open/iconv_convchr directly (also serves DF-5756)
difftest.c userland iconv(3) differential
mountdbg.c, dbg.sh, capture_baseline.sh, run5755.sh (run.sh), run_patched.sh orchestration / debugging
fix.diff shortest-form validation in utf8_to_ucs4()
logs/ full untrimmed build/run/witness/difftest logs, baseline + patched

Build

sh build.sh          # userland tools + cesu_witness.ko (on the guest)

Run

kldload /boot/kernel/msdos_iconv.ko           # pulls libiconv.ko
sh run5755.sh                                  # e2e baseline (also kldloads witness)
./difftest                                     # userland contrast

Expected output (baseline)

  • witness: overlong-slash … out=00 2f, overlong-4b … out=db ff df ff
  • dirent name="OV/SLASH" (hex 4f 56 2f …), name="OV.DOT"
  • dump: all /-bearing LFN/8.3 patterns [FOUND]

On the patched kernel (fix.diff)

  • witness: overlong-slash ret=-1, overlong-4b ret=-1 (rejected; output untouched)
  • creates of the three overlong names β†’ EINVAL; control name plainok still creates
  • image dump: all seven //. patterns absent
  • difftest output unchanged (userland never accepted them anyway)
VERDICT.md
↓ download raw

DF-5755 β€” VERDICT

Status: reproduced (certain). Fix authored and validated on the guest (fix_status: fixed).

What was run

Guest DragonFly dfbsd 6.5-DEVELOPMENT #0 … x86_64 (logs/env.txt), stock kernel + msdos_iconv.ko + libiconv.ko. All triggers run as root for the mount, then drop to uid/gid 65534 (nobody) before creating files β€” kiconv5755.c (setgroups/setgid/setuid before the open(O_CREAT) loop). The kernel-side primitive was additionally proven in-module by cesu_witness.ko, which iconv_open()s the exact pair msdosfs opens (UTF-16BE←UTF-8, auto-registered by iconv_ucs_init, sys/libiconv/iconv_ucs.c:377-383) and iconv_convchr()s the adversarial sequences with an 8-byte out buffer.

Reproduced manifestations (baseline)

evidence file:line observation
kernel converter sys/libiconv/iconv_ucs.c:419-477 (utf8_to_ucs4, no shortest-form check) "\xC0\xAF" β†’ ret=0, out=00 2f; "\xF0\x8F\xBF\xBF" β†’ ret=0, out=db ff df ff (witness.log)
surrogate wrap iconv_ucs.c:215-218 + :524-528 (encode_surrogate with code<0x10000) the db ff df ff above β€” code-0x10000 wraps to 0xFFFFFFFF
msdosfs create msdosfs_conv.c:990 (unix2winchr→convchr(pm_u2w)) → :559-579 (unix2winfn, unvalidated write) nobody's open("OV\xC0\xAFSLASH") succeeds (run.log)
readdir spoof msdosfs_conv.c:669-713 (LFN / rejected) + 8.3 fallback :243-294 (dos2unixfn, no filter) dirent name="OV/SLASH" hex 4f 56 2f 53 4c 41 53 48; also "OV.DOT" alias
on-disk metadata raw image dump LFN units 4f 00 56 00 2f 00 53 00 4c 00 (seq=0x41, attr=0x0f) and 8.3 alias 4f 56 2f 53 4c 41 53 48 20 20 20
userland contrast logs/difftest.log base-system iconv(3): EILSEQ for all overlong inputs

Why it matters (impact ceiling)

Not memory corruption. It is filesystem metadata corruption + filename spoofing reachable by an unprivileged local user with write access to any KICONV msdosfs mount (root mounts β€” identical setup bar to DF-5746):

  • / inside on-disk names is invalid on every other FAT consumer (Windows/chkdsk, non-KICONV mounts fall back to the 8.3 alias β€” also corrupted here);
  • readdir hands userland a filename containing / β€” tar/backup/script consumers interpret it as a path;
  • overlong encodings alias with canonical ones ("OV\xC0\xAEDOT" reads back as "OV.DOT"), enabling name-hiding/spoofing games.

Fix validation (fix.diff: shortest-form rejection in utf8_to_ucs4)

Applied to the guest's /usr/src/sys/libiconv/iconv_ucs.c (stacked with DF-5756's fix), libiconv.ko rebuilt (-Werror clean, logs/module_build_patched.log), loaded before msdos_iconv.ko. Rerun of the exact baseline sequence:

trigger baseline patched
witness overlong-slash ret=0 out=00 2f ret=-1, out untouched
witness overlong-4b ret=0 out=db ff df ff ret=-1
create OV\xC0\xAFSLASH etc. (nobody) OK, / on disk EINVAL
control create plainok OK OK (no regression)
image dump patterns (7) all FOUND all absent
witness valid-slash / valid-pair 00 2f / f0 9f 99 88 identical

fix_status: fixed, fix_baseline_reproduced: 1, fix_patched_reproduced: 0 (bad behavior gone), with no regression on valid conversions.

Notes

  • Upstream FreeBSD sys/libkern/iconv_ucs.c (compared 2026-10-11) is identical in the affected function β€” the defect is upstream-shared.
  • DF-2235 (read-before-bounds-check at iconv_ucs.c:247-254) is a different defect and is not re-reported here.
  • PoC fixes vs. the seed (there was no seed; authored fresh): msdosfs args.mask/args.dirmask are literal mode bits, msdosfs chmod cannot add bits, dirent has no d_reclen on DF (use _DIRENT_DIRSIZ).

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

libiconv.ko rebuilt with fix.diff (stacked with DF-5756's) and reloaded before msdos_iconv.ko; witness ret=-1 for both overlong cases with output untouched; nobody's overlong creates fail EINVAL while the plainok control still creates; all 7 '/'/'.' patterns absent from the image dump; valid-slash/valid-pair outputs byte-identical (no regression).

logs/module_build_patched.log (clean -Werror module build); logs/witness_patched.log (ret=-1 x4, valid cases unchanged); logs/run5755_patched.log (EINVAL creates + '[absent]' dump patterns + plainok OK)
↓ fix.diffper-fix-DF-5755

Confirmed kernel references

Detail

Exploit chain

root mounts msdosfs with MSDOSFSMNT_KICONV + cs_local=cs_dos=UTF-8 -> unprivileged user (uid 65534, write access) open(2)s a component containing 0xC0 0xAF -> VFS passes raw bytes -> msdosfs unix2winchr -> iconv_convchr(pm_u2w) -> iconv_ucs_conv FROM_UTF8 branch -> utf8_to_ucs4 decodes overlong to U+002F without validation (iconv_ucs.c:419-477) -> unix2winfn writes '/' into LFN units and unix2dosfn into the 8.3 alias (msdosfs_conv.c:559-579/431-458, unvalidated) -> readdir: LFN rejected by win2unixfn's literal-'/' check, 8.3 fallback carries the '/' to userland -> dirent 'OV/SLASH' with embedded separator; overlong '.' variant aliases to the distinct name 'OV.DOT'.

Evidence (decisive lines)

witness.log (kernel module: overlong-slash out=00 2f; overlong-4b out=db ff df ff); run.log (nobody creates OK; dirents 'OV/SLASH' hex 4f 56 2f 53 4c 41 53 48, 'OV.DOT'; raw-image [FOUND] LFN 4f 00 56 00 2f 00 ... seq=0x41 attr=0x0f and 8.3 alias 'OV/SLASH   '); logs/difftest.log (userland EILSEQ); logs/witness_patched.log + logs/run5755_patched.log (ret=-1, EINVAL, patterns absent); logs/module_build_patched.log (-Werror clean)

PoC changes

authored fresh (no seed): msdosfs mount args.mask/args.dirmask are literal mode bits (not umask); msdosfs chmod cannot add bits (use mount-time mask); DF dirent has no d_reclen (use _DIRENT_DIRSIZ); LFN dump patterns must be per-wePart (entries split names with 3 metadata bytes); vn0 must be detached between runs or vnconfig goes stale

Verified recommended fix

reject non-shortest UTF-8 in utf8_to_ucs4(): if ((w==2 && ucs4<0x80) || (w==3 && ucs4<0x800) || (w==4 && ucs4<0x10000)) return 0;

Verdict

utf8_to_ucs4() (sys/libiconv/iconv_ucs.c:411-478) performs no shortest-form validation, so overlong UTF-8 sequences decode to arbitrary characters: on a stock DragonFly 6.5-DEVELOPMENT guest, the kernel converter turned "\xC0\xAF" into U+002F and "\xF0\x8F\xBF\xBF" into a wrapped surrogate pair (witness: 00 2f / db ff df ff), and an unprivileged uid-65534 user on a root-mounted msdosfs KICONV volume (cs_local=UTF-8, pairs auto-registered by iconv_ucs_init) created files whose on-disk LFN and 8.3 alias contain the synthesized '/' (proven in the raw image dump), with readdir returning 'OV/SLASH' β€” a filename containing a path separator β€” while base-system userland iconv(3) returns EILSEQ for the same inputs. Not memory corruption; filesystem-metadata corruption and filename spoofing. Fix (shortest-form rejection) rebuilt and validated: overlong creates fail EINVAL, all '/' patterns absent from the image, valid conversions byte-identical.