# DF-2237 — iconv_xlat16_conv NULL-deref after case-conversion index mutation

`sys/libiconv/iconv_xlat16.c` lines 155-193 — verified 2026-10-17 on the
QEMU/KVM DragonFly 6.5-DEVELOPMENT guest (X86_64_GENERIC, stock kernel).

## What this pack proves

1. **Both cited mutation sites reach the unguarded line-192 load:**
   - single-byte path (lines 186-189, `ctmp = code >> 16` via
     `XLAT16_HAS_FROM_LOWER_CASE`): fault VA **0x108** = `NULL + 0x42*4`
     (`panic.txt`);
   - double-byte path (lines 155-158, `towlower()` via an armed `_wctype`
     pair): fault VA **0xc** = `NULL + 0x03*4` (`panic2.txt`).
   Both are `supervisor read data, page not present` — a READ, not a write.
2. The trigger tables are registered through the **real `kern.iconv.add`
   sysctl byte format** (identical wire format to `lib/libkiconv`
   `kiconv_add_xlat16_table`), i.e. the exact path a `mount_msdosfs -C`
   style helper uses.
3. The conv call is driven through the **real exported KPI**
   (`iconv_open` + `iconv_conv_case`) — the same dispatch msdosfs uses at
   `sys/vfs/msdosfs/msdosfs_conv.c:903,988`.
4. `fix.diff` (guard on `dp->d_table[c1]` after mutation) — controls stay
   bit-identical, both triggers degrade to `ret=-1` (normal miss), no panic.

## Reproduce

Guest with `/usr/src` present (see `dfbsd-qemu/vm.sh reset with-src`), then
from the host, in order:

```
sh p0_controls.sh        # fresh boot, build, controls only (no panic)
# create snapshot df2237-built after p0 (see VERDICT.md "repro trail")
sh p1_trigger_single.sh  # single-byte trigger -> Fatal trap 12, VA=0x108
sh p2_trigger_double.sh  # double-byte trigger -> Fatal trap 12, VA=0xc
sh pf_fix.sh             # apply fix.diff in guest /usr/src, rebuild
                         # libiconv.ko, re-run controls + triggers: no panic
```

Guest-side build only: `sh /root/df2237/build.sh` (userland `armtbl` /
`mmap0`, KLD harness modes 0/1/2). Note `KCFLAGS+=` is required —
`sys/conf/kmod.mk:98` resets `CFLAGS` with a plain assignment.

## Key artifacts

- `armtbl.c` — crafts the sparse xlat16 tables (serialized 0x200-entry
  caddr_t index + 0x80-entry uint32 blocks) and registers 3 charset pairs.
- `df2237_mod.c` — KLD harness; mode 0 controls, mode 1/2 triggers.
- `panic.txt` / `panic2.txt` — decisive Fatal trap 12 dumps (serial log).
- `t1_boot.log` / `t2_boot.log` — full serial logs of the trigger boots.
- `fix.diff`, `fix_build.log`, `fix_run.log` — validated fix.
- `mmap0_user.log` — unprivileged user CAN map page 0 on this guest
  (relevant to the impact ceiling; see VERDICT.md).

## Expected results

| run                 | stock kernel                       | patched module            |
|---------------------|------------------------------------|---------------------------|
| controls (mode 0)   | `41 42`, `63`, `12 34`, ret=0      | identical                  |
| trigger T1 (mode 1) | Fatal trap 12, fault VA 0x108      | `SURVIVED ret=-1`, no panic |
| trigger T2 (mode 2) | Fatal trap 12, fault VA 0xc        | `SURVIVED ret=-1`, no panic |
