# DF-2234 — iconv_sysctl_cslist stack overflow via over-long charset names

**File:** `sys/libiconv/iconv.c`
**Claim under verification:** `iconv_add()` / `iconv_register_cspair()` accept
unbounded charset names; `iconv_sysctl_cslist()` `strcpy`s them into the
31-byte fields of the on-stack `struct iconv_cspair_info csi` → kernel stack
buffer overflow.

## TL;DR — VERDICT: reproduced (impact: panic — kernel stack smash with
attacker-chosen bytes, triggered by an unprivileged sysctl read; the
*registration* of an over-long name requires root on this tree, so this is
NOT an unprivileged privesc — privesc_candidate: false)

## Root cause (line-accurate)

* `iconv_register_cspair()` (`sys/libiconv/iconv.c:179-216`) stores names of
  **unbounded** length (`csize += strlen(to)+1`; `strcpy(cp, to)` at :201,
  `strcpy(cp, from)` at :207) — no `ICONV_CSNMAXLEN` check anywhere in the
  kernel registration path.
* `iconv_add()` (:382-391) forwards names unchecked; it is an exported KPI
  (any kernel caller / KLD can register arbitrary-length names).
* `iconv_sysctl_cslist()` (:357-377) then does, on the **stack**:
  `strcpy(csi.cs_to, csp->cp_to)` at **:368** and
  `strcpy(csi.cs_from, csp->cp_from)` at **:369**, where
  `csi.cs_to`/`csi.cs_from` are `char[ICONV_CSNMAXLEN]` = `char[31]`
  (`sys/sys/iconv.h:64-70`). Any registered name ≥ 31 chars overflows the
  80-byte stack struct into the caller frame.
* Trigger is any read of `kern.iconv.cslist` (CTLFLAG_RD — unprivileged),
  including the `oldptr == NULL` sizing pass: `sysctl_old_user()`
  (`sys/kern/kern_sysctl.c:1321-1345`) returns 0 and only bumps `oldidx`
  when no buffer is supplied, so the walk + strcpy execute during probing.

## What was demonstrated on the guest

1. **Boundary**: via the only unprivileged-reachable write path
   (`kern.iconv.add`) names are bounded: 30 chars OK, 31 chars → EINVAL
   (checks at `iconv.c:410-416`), and writes are root-only anyway
   (`caps_priv_check(SYSCAP_NOSYSCTL_WR)`, `sys/kern/kern_sysctl.c:1447`).
   → no in-tree path registers > 30 chars today; the overflow is a *latent*
   KPI-contract bug (any future kernel-side composer of names, or any KLD
   calling the exported `iconv_add`, arms it).
2. **Arming the primitive** (as any kernel caller could): `longcs.ko` calls
   the exported `iconv_add("UTF-16BE", 200×'A', 200×'B')` → registers fine
   (no validation).
3. **Unprivileged trigger**: user `maxx` (uid 1001) runs `csread` →
   ```
   Fatal trap 9: general protection fault while in kernel mode
   frame pointer = 0x10:0x4242424242424242      <- 'B' bytes of the name
   Stopped at iconv_sysctl_cslist+0xf7: ret
   ```
   The 200-byte `cs_from` strcpy smashed saved RBP and the return path.
   Kernel modules are built `-fno-stack-protector` (no canary), so the smash
   runs straight into the frame; full length/content control = classic
   kernel stack-overflow primitive **if** an attacker can also control
   registration. On this tree they cannot (root-gated), hence Low severity
   (as filed) is correct: privileged-arm / unprivileged-trigger DoS, latent
   memory-corruption hazard.

## Fix (fix.diff) — validated A/B

* Reject over-long names at the single choke point
  `iconv_register_cspair()` (`strlen(to) >= ICONV_CSNMAXLEN || … → EINVAL`).
* Replace both `strcpy`s in `iconv_sysctl_cslist()` with bounded `strlcpy`
  (defense in depth: the sysctl can never overflow even if a pair slips in).

Validation (patched module, same guest):
`kldload longcs.ko` → `iconv_add failed: 22` (pair never registered);
`addtest 30` still OK / `addtest 31` still EINVAL; unprivileged `csread`
returns the bit-identical 240-byte listing of the control run — no panic.

## Reproduce

```
./pushrig.sh                                   # push + build rigs
vm.sh run_root 'kldload libiconv; /root/rig2234/addtest 30; /root/rig2234/addtest 31'
vm.sh run_user '/home/maxx/addtest 30'         # EPERM (priv gate)
vm.sh run_user '/home/maxx/csread'             # control: clean listing
vm.sh run_root 'kldload /root/rig2234/longcs.ko'
vm.sh run_user '/home/maxx/csread'             # -> Fatal trap 9 (panic.txt)
vm.sh log
```

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