# VERDICT — DF-5746

**Finding:** `kern.iconv.add` accepts converter tables of unvalidated size;
table-driven converters trust the layout → NULL-deref panic (datalen=0) and
attacker-directed kernel heap OOB read disclosed to userspace (datalen < layout).

**Status: reproduced** (impact: **leak**; deterministic **panic** variant also
reproduced) — confidence: **certain**. Fix authored and **validated** on the
guest (`fix_status: fixed`).

## What was run (guest: DragonFly 6.5-DEVELOPMENT x86_64, env.txt)

All triggers talk to the kernel exactly the way the documented surface does:
`sysctl(kern.iconv.add)` to register charset pairs (write requires
`SYSCAP_NOSYSCTL_WR` — root, `sys/kern/kern_sysctl.c:1447`) and `mount(2)` of
msdosfs with `MSDOSFSMNT_KICONV|MSDOSMNT_LONGNAME` + `cs_local`/`cs_dos`
(`sys/vfs/msdosfs/msdosfs_vfsops.c:115-131` `update_mp()` opens 4 pairs by
name). The LFN file `ABCDEFGHIJKLMNOP.k` on a 64 MB FAT16 image drives the
per-character lookups (`win2unixchr()` → `convchr(pm_w2u,…)` at
`sys/vfs/msdosfs/msdosfs_conv.c:946`).

## Manifestation 1 — deterministic kernel panic (datalen = 0)

`trigger_null.c`: pair `xlat16:PWNX2:UTF-16BE` with `ia_datalen=0`
(`iconv.c:410` accepts it — only `>` CSMAXDATALEN is checked; `iconv.c:426`
`if (din.ia_datalen)` skips the kmalloc so `cp_data == NULL`). Mounting
dereferences `*idxp` with `idxp == NULL` (`iconv_xlat16.c:71`). Serial console
(`panic.txt`):

```
local: PWNX2 dos: PWND2
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x0
supervisor read data, page not present
Stopped at iconv_xlat16_open+0x34: movq (%rsi,%rax,1),%rdx
```

Guest went down (vm status: down). Reproduced 1/1.

## Manifestation 2 — kernel heap OOB read disclosed via readdir (datalen=4096, index-only)

`trigger_leak.c`: victim pair `PWNLxx:UTF-16BE` with `ia_datalen = 4096`
(the index array only — **no data blocks**), 18 index slots marked (for the
filename's characters). `iconv_xlat16_open()` computes
`d_table[X] = cp_data + 4096 + 512*rank(X)` for each marked slot — all of them
past the end of the 4096-byte `M_ICONVDATA` allocation. 16 "marker" pairs are
registered immediately afterwards so the neighbouring heap objects contain a
known pattern (`"MKxx!NN" + 0xCA 0xFE`, 4096 bytes each).

Every `readdir` then makes the kernel read `code = d_table[c1][c2]`
(`iconv_xlat16.c:192`) out of bounds and copy `(code>>8, code&0xff)` into the
output filename (`iconv_xlat16.c:243-244`).

Observed across 4 runs (baseline kernel; `run.log`, `run.1.log`):

| run | returned filename (hex) |
|---|---|
| R1 | `3f×7 4b4d ×13 0201 4b4d` |
| R2 | `3f×7 4b4d ×13 0201 4b4d` |
| R3 | `4b4d×7 0201 3f×7 0201 4b4d 3f` |
| R4 | `4b4d ×18` (every slot hit markers — 36/36 bytes) |

`4b 4d` = `'K','M'` — the little-endian decode of the marker strings stored
in *neighbouring kernel allocations*, i.e. bytes the victim allocation does
not contain. `3f` (`'?'`) marks slots whose OOB read hit zero (error path).
The layout-dependence across runs is itself evidence of live heap contents.

Primitive characterisation: attacker chooses the allocation size (any
`ia_datalen` ≤ 266240), the number of "blocks" (0..512, stride 512 B), and
the input characters (per-char slot selection) — a mouldable ~256 KB read
window past a groomable heap object, with contents returned byte-for-byte to
unprivileged *readers* of the directory. Read primitive only: no OOB write
exists (all converter writes are bounded by `outbytesleft`), so no uid0 chain;
both triggers are root-gated at the sysctl/mount boundary.

## Killed facets (checked, not bugs)

* **negative `ia_datalen`** → `kmalloc((size_t)-N)` panic hypothesis: dead —
  `iconv.c:410` compares `int` against the `size_t` expression
  `ICONV_CSMAXDATALEN`, so negatives promote to huge unsigned and are rejected
  (empirically `EINVAL`, `trigger_neg.c`).
* **cslist sysctl beyond DF-2234's strcpy**: fixed-size `SYSCTL_OUT` records;
  `csi` fully `bzero()`ed incl. padding (`iconv.c:360`); counts/id reads are
  under `LK_SHARED`. `cp_base` (`iconv.c:367`) is never assigned anywhere in
  the tree — dead read, always 0.
* **`iconv_sysctl_add` field strlens**: `SYSCTL_IN` copies the whole struct;
  non-terminated 31-byte names terminate inside the struct (`ia_datalen` /
  pointer bytes guarantee a NUL) — no stack overread.
* **`iconv_xlat16_conv` index math**: `C2I1/C2I2` bound `c1 ≤ 0x1ff`,
  `c2 ≤ 0x7f` on every path (checked the towlower/towupper remaps too) —
  in-bounds *iff* the table is well-formed (hence this finding).
* **`iconv_convstr`/`iconv_convmem`**: all converter writes bounded by
  `outbytesleft`; the `*p=0` at `iconv.c:521` lands within `dst+strlen(src)`.
* (out-of-file observation for a future audit: `iconv_ucs_conv`
  `sys/libiconv/iconv_ucs.c:247-254` reads a 2nd byte before the
  `ir < inlen` check at `:267` — 1-byte overread on odd-length UTF-16 input;
  value only steers an error branch; no in-tree caller found passing odd
  lengths. Not filed against iconv.c.)
* DF-2233 (lockless list/unload race) and DF-2234 (cslist strcpy) — known,
  not re-reported.

## Fix validation (fix.diff: `cp_datalen` + converter-side validation)

Applied to the guest's `/usr/src`, rebuilt `libiconv.ko` only (the struct is
module-internal — no other kernel/module consumer), installed, rebooted.
`fix_build.log`/`module_build.log` (gcc 8.3, `-Werror`, clean).

| trigger | baseline kernel | patched kernel |
|---|---|---|
| `trigger_null` (datalen=0) | **Fatal trap 12, VA 0x0, guest down** | `mount: Invalid argument`, guest up |
| `trigger_leak` (index-only table) | **marker bytes leaked in filenames ×4 runs** | `mount: Invalid argument`, no listing, no leak |
| `trigger_benign` (full 13312 B table) | mounts; names `B\x01B\x02…B\x10B` | mounts; **identical** `B\x01B\x02…B\x10B` |

Baseline bad behaviors are gone; legitimate well-formed tables keep working.
`fix_baseline_reproduced=1`, `fix_patched_reproduced=0`, `fix_status=fixed`.

## Bottom line

One missing kernel-side size check (`iconv.c:410`) turns a root-reachable
sysctl+mount pair into (a) a one-syscall-adjacent deterministic kernel panic
and (b) a mouldable kernel-heap info leak returned through ordinary
`readdir()` — because the xlat16/xlat converters trust a serialized layout
the kernel never validates. Severity: **High** for a `vfs.usermount=1` /
unprivileged-mount threat model, **Medium** on default config (root-gated);
the guest's `vfs.usermount=0`. The 3-line-per-file fix eliminates both
manifestations with no behavioural change for well-formed tables.

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