# DF-2236 — iconv_xlat16_open trusts attacker-controlled cp_data (NULL deref + OOB read)

## Verdict
**REPRODUCED** — kernel panic (fatal trap 12, NULL pointer dereference) in `iconv_xlat16_open`
confirmed on the unpatched GENERIC kernel. Fix validated: the patched libiconv.ko rejects
the malicious table with EINVAL instead of panicking.

## Bug analysis

`iconv_xlat16_open()` (`sys/libiconv/iconv_xlat16.c:59-93`) interprets `csp->cp_data`
as a `[0x200-entry index of uint32_t* pointers][variable count of 0x80-uint32_t data blocks]`
layout, but **never validates** that `cp_data` is non-NULL or large enough:

```c
headp = (uint32_t *)((caddr_t)csp->cp_data + sizeof(dp->d_table));  // cp_data + 0x1000
idxp = (uint32_t **)csp->cp_data;                                   // cp_data (may be NULL!)
for (i = 0; i < 0x200; i++) {
    if (*idxp) {                  // dereferences cp_data — NULL => panic
        dp->d_table[i] = headp;
        headp += 0x80;            // advances 0x200 bytes per non-NULL entry, unchecked
    }
    idxp++;
}
```

- **CWE-476 NULL deref**: if `ia_datalen=0`, `cp_data` stays NULL → `*idxp` dereferences
  NULL → **fatal trap 12 page fault**. This is the demonstrated crash.
- **CWE-125 OOB read**: if `ia_datalen` is non-zero but undersized, `idxp` reads past
  `cp_data` (OOB index entries) and `headp` advances past `cp_data` (OOB data pointers).
  Later, `iconv_xlat16_conv()` dereferences `dp->d_table[c1][c2]` → OOB heap read.

## Reproduction

1. Register a malicious xlat16 charset pair `"CP437"→"ISO8859-1"` with `ia_datalen=0`
   (→ `cp_data=NULL`) via `kern.iconv.add` sysctl.
2. Create a 32 MB FAT16 image (`newfs_msdos -F 16`).
3. `mount_msdos -D CP437 /dev/vn0 /mnt` → kernel `msdosfs_mount` calls
   `iconv_open("CP437","ISO8859-1")` → finds the malicious pair → `iconv_xlat16_open`
   → `*NULL` → **panic**.

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

The `mount_msdos` userland registration is not a barrier: `kiconv_add_xlat16_cspair()`
returns 0 (success) when the pair already exists (`kiconv_lookupcs()` hit), so the
pre-registered malicious pair is the one `iconv_open` finds.

## Privilege correction (finding claim error)

The finding claims "`kern.iconv.add` sysctl has NO privilege check" and rates this
**PR:L (unprivileged)**. This is **incorrect**: on DragonFly 6.5-DEVELOPMENT, writing
to `kern.iconv.add` requires root — an unprivileged user gets `EPERM`:

```
$ ./df2236_register CP437 ISO8859-1   # as maxx (uid 1001)
sysctl kern.iconv.add failed: Operation not permitted
```

The SYSCTL_PROC node uses `CTLFLAG_RW` without `CTLFLAG_ANYBODY`, so the kernel's
sysctl write path enforces `suser_cred`. The real privilege is **PR:H (root-only)**,
making this a **root→kernel DoS**, not an unprivileged→kernel exploit. The code bug
(NULL deref / OOB) is real, but the severity is lower than claimed.

## Fix

Three-part fix (in `fix.diff`):

1. **`sys/sys/iconv.h`**: add `int cp_datalen` to `struct iconv_cspair` to track the
   actual allocation size.
2. **`sys/libiconv/iconv.c`**: set `cp_datalen` in `iconv_register_cspair` (default 0)
   and in `iconv_sysctl_add` (store `ia_datalen`).
3. **`sys/libiconv/iconv_xlat16.c`**: validate `cp_data != NULL` and
   `cp_datalen >= sizeof(dp->d_table)` before use; check that each implied 0x200-byte
   data block stays within `cp_data + cp_datalen` before assigning `dp->d_table[i]`.

## Fix validation

| Kernel | PoC result |
|--------|-----------|
| Unpatched (#0 baseline) | **fatal trap 12** panic in `iconv_xlat16_open+0x34` (NULL deref) |
| Patched libiconv.ko | mount returns **EINVAL** gracefully, **no panic**, guest alive |
| Patched + normal mount (no charset) | mount **succeeds** (exit 0) — fix doesn't break normal operation |

Rebuilt `libiconv.ko` with `make KERNCONF=X86_64_GENERIC` in `sys/libiconv/` (rc=0),
installed to `/boot/kernel/libiconv.ko`, reloaded.
