# 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)
