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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5756 Β· 11 files
FileTypeDescriptionSize
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 makes ucs4_to_utf8 fail the dstlen < w check (iconv_ucs.c:505-506), so msdosfs silently substitutes ? instead of rejecting invalid Unicode (userland iconv(3) returns EILSEQ β€” see logs/difftest.log).
  • The raw CESU-8 emission (out buffer β‰₯3) is proven directly by the cesu_witness kernel 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), while highsur-lone "\xD8\x3D" β†’ ret=-1 β€” the asymmetry in one output.
  • run.log (crafted image): LFN unit patched to U+DC00 β†’ dirent abcd?fghij.k (3f substitution); unit patched to U+D83D β†’ ?xyzabcd.k.
  • logs/difftest.log: userland iconv(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.k and ?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 ? in win2unixchr); the converter itself now refuses invalid Unicode instead of laundering it.
VERDICT.md
↓ download raw

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.c initially 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 dirmask and DF dirent (_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

fixed
baseline reproduced→ patch + rebuild →patched clean

libiconv.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)
↓ fix.diffper-fix-DF-5756

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 win2unixchr substitutes '?'; with a >=3-byte consumer buffer the CESU-8 bytes ED B0 80 are emitted (proven by cesu_witness.ko through the exported iconv_convchr API)

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.