# DF-5765 — iconv_xlat cspair lifetime handoff race → kernel heap corruption (panic: BADFREE2)

## What

`sys/libiconv/iconv_xlat.c` claims and drops its cspair lifetime
reference (`csp->cp_refcount++` at iconv_xlat.c:63 inside
`iconv_xlat_open`, `csp->cp_refcount--` at iconv_xlat.c:73 inside
`iconv_xlat_close`) with **no synchronization**, while the cspair
teardown paths (`iconv_sysctl_add` error path, iconv.c:439-442;
`iconv_mod_unload`, iconv.c:85-95) free cspairs *and their tables*
under `iconv_lock` — a lock that `iconv_open()`/`iconv_close()`
(iconv.c:233-273) never take.  A racing open can find a pair, sleep in
`kobj_create(M_WAITOK)` (iconv_xlat.c:60), and then write
`cp_refcount++` into freed memory and cache a dangling `d_table`; the
add error path frees the pair **unconditionally**, even when a
counted reference is held.

## Reproduce (inside the DragonFly guest, as root)

```
cd /root/xlatrace   # == findings/poc/DF-5765/ transferred into the guest
sh build.sh         # copies VERBATIM production iconv.c/iconv_xlat.c
                    # (md5s recorded in setup.log), builds
                    # xlatrace_harness.ko + userland hammer
kldload ./xlatrace_harness.ko
sh run.sh           # baseline, then 2 x 8,000,000-iteration races
```

`run.sh` starts kernel racer threads (one per CPU except CPU0) that
call the **production** `iconv_open`/`iconv_convstr`/`iconv_close` on
the pair `zz9to:zz9from` in a loop, while the userland `hammer`
drives the **production** `kern.iconv.add` sysctl with
`ia_datalen=256, ia_data=0x1000` (unmapped ⇒ `copyin` EFAULT ⇒
production error path registers the pair, allocates the 256-byte
table, then unregisters and kfrees both, in a tight loop).

## Expected / observed

* **Unpatched**: within seconds-to-minutes of churn:
  - console: `[xlatrace] PROOF: live handle d_csp=... refcount=-2046
    -- pair freed/reused under the open (d_table=...)`
  - then `panic: BADFREE2` at `_kfree+0x593` called from
    `iconv_sysctl_add+0x172` (the production error path freeing a
    chunk whose freelist the racers' writes corrupted); ssh dies,
    `vm.sh status` ⇒ down.
  - 3/3 race sessions panicked with the identical signature
    (run.log, run.2.log, panic.3-stale-build.txt).
* **With fix.diff applied** to the rig's verbatim production iconv.c:
  full run.sh completes (`FIX_RUN_RC=0`), 21.3M cumulative opens,
  `DANGLING=0`, `kern.iconv.cslist` shows no refcount anomalies, no
  panic (run_fixed.log).  Positive control: a valid `add` still works
  (csid=1) and racers perform balanced open/conv/close cycles on it.

## Files

* `harness.c` — KLD rig (production sources compiled in verbatim)
* `hammer.c` — userland sysctl-churn driver (`-l` dumps cslist/drvlist)
* `Makefile`, `build.sh`, `run.sh`, `xfer.sh`
* `run.log`, `run.2.log` — decisive unpatched runs (console captures)
* `panic.txt`, `panic.2.txt`, `panic.3-stale-build.txt` — panic captures
* `run_fixed.log`, `fix_build.log` — fix-validation run
* `setup.log` — verbatim-source md5s; `env.txt` — guest environment
* `VERDICT.md`, `manifest.json`, `verdict.json`, `fix.diff`

## Impact

Privilege analysis: BOTH sides of the race are root-gated on a stock
system — writing `kern.iconv.add` requires `SYSCAP_NOSYSCTL_WR`
(kern_sysctl.c:1446-1447) and the `iconv_open` side is only reachable
from mount(2)-time VFS charset setup (root; `vfs.usermount=0`
default).  So: privileged-user-triggerable kernel heap corruption /
DoS — severity Low per the audit rubric ("already-privileged user"),
CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:H/A:H (3.3).
