Asymmetric unpaired-surrogate validation: lone low surrogates (U+DC00-DFFF) accepted as ordinary characters and re-encoded as CESU-8 (invalid UTF-8) when the output buffer permits; silent '?' substitution instead of EILSEQ-equivalent via msdosfs
Summary
Only the high-surrogate prefix is recognized: a lone low surrogate passes as a plain 2-byte character and the output stage has no surrogate guard before ucs4_to_utf8 - in-kernel witness converted '\xDC\x00' to ED B0 80 (CESU-8) ret=0 while a lone high surrogate is rejected; via a crafted KICONV image readdir silently substitutes '?'. Userland iconv(3) returns EILSEQ for the same bytes; the UTF-8 input direction rejects surrogates (:208) - the asymmetry. Invalid-Unicode laundering + parser confusion, no memory corruption. reject-leading-low + guard-output-stage fix validated (lowsur ret=-1, valid pairs unregressed). Upstream-shared.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5756 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.2 KB | β raw | |
| VERDICT.md | β | 3.1 KB | β raw | |
| verdict.json | β | 4.3 KB | view raw | |
| fix.diff | β | 1.4 KB | view raw | |
| craft5756.c | β | 2.5 KB | view raw | |
| kread5756.c | β | 2.1 KB | view raw | |
| run.sh | β | 171 B | view raw | |
| run.patched.log | β | 852 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 |
DF-5756 β asymmetric unpaired-surrogate validation in iconv_ucs: lone U+DC00βDFFF accepted and re-encoded as CESU-8 (invalid UTF-8)
File audited (pass 2): sys/libiconv/iconv_ucs.c
Reserved ID: DF-5756
Guest: DragonFly 6.5-DEVELOPMENT x86_64 (see logs/env.txt)
Root cause (one line)
In the UTF-16βUTF-8 direction iconv_ucs_conv() only recognizes the
high-surrogate prefix on the leading unit
(sys/libiconv/iconv_ucs.c:255, (*q & 0xfc) == 0xd8); a lone low
surrogate (U+DC00βU+DFFF) in leading position falls to the plain
2-byte branch (:264-266) and the second half (:293-312) has no
surrogate check at all before ucs4_to_utf8() (:481-522), which
happily emits a 3-byte sequence (w=3 for anything < 0x10000,
:495-497) β i.e. CESU-8: "\xDC\x00" β ED B0 80. The UTF-8 input
direction, by contrast, rejects surrogates at :208.
Threat / reachability
- Any kernel consumer converting attacker-controlled UTF-16 with a β₯3-byte
output buffer receives invalid UTF-8 that many downstream parsers reject
(or worse, pair up into WTF-8/CESU-8 confusion). In-tree e2e: a crafted
FAT image whose LFN contains a lone DC00 unit reaches
win2unixchr()(msdosfs_conv.c:946) β its 2-byte staging buffer makesucs4_to_utf8fail thedstlen < wcheck (iconv_ucs.c:505-506), so msdosfs silently substitutes?instead of rejecting invalid Unicode (userlandiconv(3)returns EILSEQ β seelogs/difftest.log). - The raw CESU-8 emission (out buffer β₯3) is proven directly by the
cesu_witnesskernel module (also used by DF-5755) through the exact converter pair a KICONV mount opens.
Evidence (baseline)
witness.log(kernel):lowsur "\xDC\x00"βret=0 out=ed b0 80(CESU-8 emitted as a successful conversion), whilehighsur-lone "\xD8\x3D"βret=-1β the asymmetry in one output.run.log(crafted image): LFN unit patched to U+DC00 β direntabcd?fghij.k(3fsubstitution); unit patched to U+D83D β?xyzabcd.k.logs/difftest.log: userlandiconv(3)UTF-16BEβUTF-8 on"\xDC\x00"β EILSEQ.
Files
| file | what |
|---|---|
craft5756.c |
patches LFN units in the raw FAT image (low/high surrogate) |
kread5756.c |
mount(2) KICONV UTF-8/UTF-8 + readdir hex dump |
run.sh |
full craftβmountβreaddir sequence |
fix.diff |
reject leading low surrogates + surrogate check before UTF-8 emission |
logs/ |
difftest (baseline+patched), env |
| (shared with DF-5755) | cesu_witness.c, witness.log, witness_patched.log, difftest.c |
Build / Run
cc -O0 -g -Wall -I/usr/src/sys -o kread5756 kread5756.c cc -O0 -g -Wall -o craft5756 craft5756.c sh run.sh
Expected output (baseline)
- witness:
witness[lowsur]: convchr ret=0 β¦ out=ed b0 80 - dirents:
abcd?fghij.kand?xyzabcd.k
On the patched kernel (fix.diff)
- witness:
witness[lowsur]: convchr ret=-1 β¦ out=aa aa aa aa(rejected, nothing emitted) - valid surrogate pair (
D8 3D DE 48) still converts:f0 9f 99 88 - msdosfs-visible behavior unchanged by design (both accept-then-fail and
reject-at-input surface as
?inwin2unixchr); the converter itself now refuses invalid Unicode instead of laundering it.
DF-5756 β VERDICT
Status: reproduced (certain). Fix authored and validated at the
converter level (fix_status: fixed).
Reproduced manifestations (baseline)
| evidence | file:line | observation |
|---|---|---|
| CESU-8 emission | sys/libiconv/iconv_ucs.c:255 (only 0xd8 prefix checked) β :264-266 (lone low surrogate as plain char) β :293-312 (no surrogate check) β ucs4_to_utf8 :495-497 (w=3) |
witness: "\xDC\x00" β ret=0 out=ed b0 80 |
| asymmetry | same | witness: "\xD8\x3D" β ret=-1 (high surrogate alone correctly rejected) |
| crafted-image e2e | msdosfs_conv.c:946 (win2unixchr β convchr(pm_w2u), 2-byte out) |
dirent abcd?fghij.k β silent ? for invalid Unicode; ?xyzabcd.k for lone high |
| userland contrast | logs/difftest.log |
iconv(3) UTF-16BEβUTF-8 "\xDC\x00" β EILSEQ |
Honest scope
The 3-byte CESU-8 bytes themselves do not escape through msdosfs (the
per-character staging buffer is 2 bytes, so the emission fails the
dstlen < w check at iconv_ucs.c:505-506 and msdosfs substitutes ?).
They do escape through any consumer with a β₯3-byte output buffer
calling the same kernel converter β proven in-kernel by cesu_witness.ko
via the exported iconv_open/iconv_convchr API. The msdosfs-visible
defect is the silent acceptance (no EILSEQ-equivalent) of unpaired
surrogates. Severity Low: invalid-Unicode laundering / parser-confusion
input, no memory corruption, no info leak.
Fix validation (fix.diff: leading-DC rejection at the input stage +
surrogate check before UTF-8 emission)
Applied stacked with DF-5755's fix; libiconv.ko rebuilt (-Werror
clean), reloaded before msdos_iconv.ko:
| trigger | baseline | patched |
|---|---|---|
witness lowsur "\xDC\x00" |
ret=0 out=ed b0 80 |
ret=-1, out untouched |
witness highsur-lone |
ret=-1 |
ret=-1 (unchanged) |
witness valid-pair D8 3D DE 48 |
f0 9f 99 88 |
identical (no regression) |
| msdosfs readdir (crafted image) | abcd?fghij.k |
abcd?fghij.k β unchanged by design: win2unixchr renders both accept-then-fail and reject-at-input as ?; the delta is the converter now rejects invalid Unicode (ret=-1) instead of laundering it into CESU-8 when given room |
fix_status: fixed (converter-level behavior delta decisive via witness;
no regression on valid pairs).
PoC notes
craft5756.cinitially searched for the full UTF-16LE name across the LFN entry β LFN entries split names at wePart1/wePart2 with 3 metadata bytes between, so the search was fixed to per-part patterns with an attr==0x0f cross-check.- PoC fixes vs seed: none applicable (authored fresh); msdosfs mount
dirmaskand DFdirent(_DIRENT_DIRSIZ) quirks as in DF-5755.
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-5755's) and reloaded; witness lowsur ret=-1 with output untouched (baseline: ret=0, ED B0 80); highsur-lone unchanged ret=-1; valid surrogate pair D83D DE48 still converts to f0 9f 99 88 (no regression); msdosfs readdir '?'-substitution unchanged as designed (both accept-then-fail and reject-at-input surface as '?' in win2unixchr β the converter now refuses invalid Unicode instead of laundering it).
logs/witness_patched.log (shared DF-5755 pack); run.patched.log; logs/difftest_patched.log (userland unchanged)
Confirmed kernel references
Detail
Exploit chain
crafted FAT image carries an LFN unit 0xDC00 -> msdosfs win2unixfn (msdosfs_conv.c:663-678, unit not 0 nor '/') -> win2unixchr -> convchr(pm_w2u) -> iconv_ucs_conv: (*q&0xfc)==0xdc misses the 0xd8-only check (iconv_ucs.c:255) -> ucslen=2 (:264-266) -> UTF-8 second half has no surrogate check (:293-312) -> ucs4_to_utf8(0xDC00) wants w=3; with msdosfs's 2-byte staging it fails dstlen
Evidence (decisive lines)
witness.log (shared with DF-5755: lowsur ret=0 out=ed b0 80; highsur-lone ret=-1); run.log (dirents 'abcd?fghij.k' hex 61 62 63 64 3f ... and '?xyzabcd.k'); logs/difftest.log (userland EILSEQ for \xDC\x00); logs/witness_patched.log (lowsur ret=-1 out untouched; valid-pair f0 9f 99 88 unchanged); run.patched.log
PoC changes
authored fresh; craft5756.c initially matched full-name UTF-16LE across the LFN entry β LFN entries split names between wePart1/wePart2 with 3 metadata bytes between, so patterns were fixed to per-part with an attr==0x0f cross-check; msdosfs dirmask and DF dirent (_DIRENT_DIRSIZ) quirks as in DF-5755
Verified recommended fix
reject leading low surrogates at the input stage (extend the (*q & 0xfc) == 0xd8 check with a == 0xdc rejection) and reject code in [0xd800,0xe000) before ucs4_to_utf8 emission.
Verdict
iconv_ucs_conv() checks only the high-surrogate prefix (0xd8) on the leading UTF-16 unit (sys/libiconv/iconv_ucs.c:255) and has no surrogate check in the UTF-8 output stage (:293-312), so a lone low surrogate (U+DC00-DFFF) is accepted as an ordinary character and, given >=3 output bytes, re-encoded by ucs4_to_utf8 (:495-497, w=3) as CESU-8: the in-kernel witness converted "\xDC\x00" to ED B0 80 with ret=0 while rejecting a lone high surrogate (ret=-1) β the asymmetry. Via a crafted msdosfs KICONV image the unpaired surrogate surfaces as a silent '?' substitution (dirent 'abcd?fghij.k') instead of an invalid-input rejection; base-system userland iconv(3) returns EILSEQ for the same bytes. No memory corruption, no leak: invalid-Unicode laundering/ parser-confusion input; severity Low. Fix (leading-DC rejection + output-stage surrogate check) rebuilt and validated: witness ret=-1 with nothing emitted, valid surrogate pairs byte-identical.
No comments yet.