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)
PoC verification
Evidence pack
findings/poc/DF-5755 Β· 26 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 byunix2winfn()(msdosfs_conv.c:559-579, no validation) and into the 8.3 alias byunix2dosfn()(msdosfs_conv.c:431-458, only magic 1/2 are special-cased). - read back: the only
/filter iswin2unixfn()'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)
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)run.log(msdosfs e2e): nobody creates"OV\xC0\xAFSLASH","OV\xC0\xAEDOT","OV\xE0\x80\xAFS3"β readdir returns"OV/SLASH"(hex4f 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).- Raw image dump (
run.logtail): 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). logs/difftest.log: base-system userlandiconv(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"(hex4f 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 nameplainokstill creates - image dump: all seven
//.patterns absent difftestoutput unchanged (userland never accepted them anyway)
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.dirmaskare literal mode bits, msdosfschmodcannot add bits,direnthas nod_reclenon 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
fixedlibiconv.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)
Confirmed kernel references
- sys/libiconv/iconv_ucs.c:411
- sys/libiconv/iconv_ucs.c:419
- sys/libiconv/iconv_ucs.c:460
- sys/libiconv/iconv_ucs.c:215
- sys/libiconv/iconv_ucs.c:218
- sys/libiconv/iconv_ucs.c:524
- sys/libiconv/iconv_ucs.c:377
- sys/vfs/msdosfs/msdosfs_vfsops.c:119
- sys/vfs/msdosfs/msdosfs_conv.c:990
- sys/vfs/msdosfs/msdosfs_conv.c:559
- sys/vfs/msdosfs/msdosfs_conv.c:431
- sys/vfs/msdosfs/msdosfs_conv.c:669
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.
No comments yet.