# DF-3124 — VERDICT

**Status: reproduced** (impact: panic — NULL-deref local DoS; confidence: certain)
Reproduced twice, each time from a fresh `vm.sh reset with-src` boot:
deterministic.

## Root cause (line-accurate)

- `sys/vfs/isofs/cd9660/cd9660_vfsops.c:494` —
  `cd9660_iconv->open(cs_local, cs_disk, &isomp->im_d2l);` — return value
  ignored.
- `sys/vfs/isofs/cd9660/cd9660_vfsops.c:495` — same for `im_l2d`.
- On failure both handles stay NULL (isomp is `M_ZERO`-allocated, line 412)
  while `im_flags` keeps `ISOFSMNT_KICONV` (masked in at line 486-489).
- Consumers pass the raw handle to the KOBJ dispatcher:
  - `sys/vfs/isofs/cd9660/cd9660_util.c` `sgetrune()` —
    `cd9660_iconv->convchr(handle, ...)` guarded only by
    `flags & ISOFSMNT_KICONV && cd9660_iconv` (module pointer), NOT by
    handle != NULL. Reached from every non-'.'/non-'..' name comparison:
    `cd9660_lookup.c:237-238` (`isofncmp(..., imp->im_d2l, imp->im_l2d)`).
  - `cd9660_util.c` `isochar()` — same pattern, reached on Joliet mounts
    (readdir via `isofntrans`).
- `iconv_convchr()` (sys/libiconv/iconv.c:290-294) dispatches through the
  handle as a kobj → `movq (%rdi),%rsi` at address 0 → page fault → panic.
- `cd9660_unmount()` (cd9660_vfsops.c:584-589) NULL-checks both handles
  before `close()` — proving NULL handles are an anticipated state that the
  consume paths don't defend against.

## Why iconv_open fails in practice

`mount_cd9660 -o kiconv` (sbin/mount_cd9660) sets the flag without ever
registering a charset pair (set_charset() is only called for `-C`), leaving
`cs_local`/`cs_disk` as empty strings; `iconv_open("","")` finds no
registered cspair (and no converter modules) → ENOENT, silently ignored.
Any direct mount(2) caller (this PoC) hits the same path. A successful
`-C` registration that later becomes unavailable (module unload — see
DF-2233) reaches the same NULL-handle state at lookup time.

## Reproduction narrative

1. Host: crafted a 40-sector minimal ISO9660 image (Python; PVD at sector
   16 with root directory record → extent 18, terminator at 17, "."/".."
   records at 18). No Joliet SVD needed — the panic goes through sgetrune().
2. Guest: `vnconfig -c -S 2048 /dev/vn0`, `kldload libiconv`,
   `kldload cd9660_iconv`, compile `trigger.c` (`cc -O -I/usr/src/sys`).
3. **Baseline**: `mount_cd9660 /dev/vn0 /mnt/poc` (no kiconv) — mounts,
   `stat /mnt/poc/a` → ENOENT, clean unmount. Image is valid.
4. **Trigger**: `/root/trigger` — mount(2) with `ISOFSMNT_KICONV` and
   zeroed `cs_local`/`cs_disk`; mount succeeds (open failures ignored);
   `stat("/mnt/poc/a")` → guest panics instantly:
   ```
   Fatal user address access from kernel mode from trigger at ffffffff82600bb7
   Fatal trap 12: page fault while in kernel mode
   fault virtual address    = 0x0
   instruction pointer      = 0x8:0xffffffff82600bb7     (inside libiconv.ko)
   current process          = 914 (trigger)
   Stopped at      iconv_convchr+0x27:     movq    (%rdi),%rsi
   ```
5. Re-ran from a fresh reset: identical panic (pid 869). Deterministic.

## Exploit chain

None. The faulting address is a fixed NULL (the KOBJ ops load); there is no
attacker influence over the dereferenced pointer, no write primitive. This
is a denial-of-service bug, not an escalation. (Checked: handle is used
purely as `*(kobj_t)` for method dispatch — no partial control path.)

## Reachability / gating

- Mounting cd9660 requires root-equivalent privilege
  (`get_fscap("cd9660") == SYSCAP_RESTRICTEDROOT`,
  sys/kern/vfs_syscalls.c:5383-5398 — even with `vfs.usermount=1`).
- After such a mount exists, ANY local user triggers the panic with a
  plain `stat()`/`ls` under the mountpoint.
- Severity: Low (privileged mount-time opt-in with a broken charset;
  unprivileged post-mount DoS).

## Fix

`fix.diff` — check both open() returns; on any failure close the
half-opened handle, NULL both, and clear `ISOFSMNT_KICONV` so
isochar()/sgetrune() take their non-iconv fallback (identical discipline
to the existing `cd9660_iconv == NULL` path). Also forces NUL-termination
of the copied charset names (adjacent hardening, cf. DF-0854 in msdosfs).

Fix validation: **not_testable by choice** — this is a Low-severity
NULL-deref DoS; per the audit's conditional-verification policy the
kernel-rebuild fix-validation cycle is reserved for Critical/High and
memcorrupt/privesc findings. The fix is a guarded fallback that mirrors
logic already present in cd9660_unmount().

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