iconv_add() and iconv_register_cspair() accept unbounded charset names; iconv_sysctl_cslist() strcpy overflows 31-byte stack buffer
Summary
Exported kernel API iconv_add() (line 382) performs no length validation on charset-name strings. iconv_register_cspair() stores them verbatim arbitrary length in heap via strcpy lines 201/207. sysctl_add path DOES validate strlen>=ICONV_CSNMAXLEN(31) and rejects lines 412-415 but iconv_add has no equivalent check. iconv_sysctl_cslist (lines 368-369) does strcpy(csi.cs_to csp->cp_to) and strcpy(csi.cs_from csp->cp_from) where csi.cs_to/cs_from are char[ICONV_CSNMAXLEN]=char[31]. If csp->cp_to >= 31 bytes strcpy writes past csi into adjacent stack locals (cs_base cs_refcount cs_id cs_version saved frame pointer return address). iconv_add is EXPORTED symbol callable by any loaded KLD module. KLD module (loaded by root) with 40-char charset name creates cspair. Any subsequent read of kern.iconv.cslist (world-readable CTLFLAG_RD line 378) by ANY user copies 40 bytes into 31-byte stack buffer kernel stack overflow controlled by cspair name content. Currently latent all in-tree callers pass short compile-time constants (iconv_ucs.c ENCODING_UNICODE UTF-16BE ENCODING_UTF8 UTF-8). API contract unsafe any future/out-of-tree caller passing mount-option-derived string without own length check activates overflow. [VERIFIED pass2: reproduced - KLD registers 200-char names via exported iconv_add then UNPRIVILEGED kern.iconv.cslist read panics at iconv_sysctl_cslist+0xf7 ret with frame pointer 0x4242424242424242, no SSP in kernel modules; arming is root-gated on this tree so privesc candidate false, Low as filed; fix-validated - strlen rejection at register choke point plus strlcpy at both emit sites, unpriv control listing bit-identical]
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2234 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.2 KB | β raw | |
| VERDICT.md | β | 4.4 KB | β raw | |
| Makefile | β | 77 B | β download | |
| longcs.c | β | 2.2 KB | view raw | |
| csread.c | β | 1.2 KB | view raw | |
| addtest.c | β | 1.5 KB | view raw | |
| build.sh | β | 148 B | view raw | |
| run.sh | β | 194 B | view raw | |
| build.log | β | 5.6 KB | view raw | |
| run.log | β | 4.2 KB | view raw | |
| panic.txt | β | 1.1 KB | view raw | |
| env.txt | β | 481 B | view raw | |
| fix.diff | β | 693 B | view raw | |
| manifest.json | β | 1.1 KB | view raw | |
| verdict.json | β | 4.2 KB | view raw |
DF-2234 β PoC evidence pack
Finding: iconv_add()/iconv_register_cspair() accept unbounded charset
names; iconv_sysctl_cslist() strcpy overflows the 31-byte on-stack fields
(sys/libiconv/iconv.c:368-369; no bound at :179-216).
Verdict: REPRODUCED β kernel stack buffer overflow: unprivileged read of
kern.iconv.cslist panics with RBP = 0x4242424242424242 after a KLD
registered 200-char names via the exported iconv_add(). Registration of
over-long names is root-gated on this tree (kern_sysctl.c:1447), so impact =
privileged-arm / unprivileged-trigger DoS (latent memory-corruption hazard),
not unpriv privesc. See VERDICT.md, panic.txt, run.log.
Build
Guest (root): sh build.sh β /root/rig2234/longcs.ko, csread, addtest
(copies of the userland tools are placed in /home/maxx for the unpriv runs).
Run (baseline β expect Fatal trap 9)
sh run.sh # on the guest, as root # host: dfbsd-qemu/vm.sh log
Expected decisive output (see panic.txt):
Fatal trap 9: general protection fault while in kernel mode frame pointer = 0x10:0x4242424242424242 Stopped at iconv_sysctl_cslist+0xf7: ret
Fix validation
cd /usr/src && patch -p1 < fix.diff (rebuild a libiconv module from the patched tree; kldload it instead of stock) kldload /root/rig2234/longcs.ko -> "iconv_add failed: 22" (rejected) addtest 30 / addtest 31 -> ok / EINVAL (unchanged) su maxx -c /home/maxx/csread -> clean, bit-identical control listing
Files
Makefile kmod makefile for longcs.ko longcs.c trigger KLD: iconv_add() with 200-char to/from names csread.c unprivileged kern.iconv.cslist reader (+ dump) addtest.c kern.iconv.add boundary prober (30 vs 31 chars) build.sh, run.sh exact commands build.log full module build output run.log full run narrative + outputs (baseline + fix A/B) panic.txt serial console panic capture env.txt guest environment fix.diff validated fix (registration bound + strlcpy) manifest.json machine catalog verdict.json machine verdict
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() strcpys 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) β noICONV_CSNMAXLENcheck 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 andstrcpy(csi.cs_from, csp->cp_from)at :369, wherecsi.cs_to/csi.cs_fromarechar[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 theoldptr == NULLsizing pass:sysctl_old_user()(sys/kern/kern_sysctl.c:1321-1345) returns 0 and only bumpsoldidxwhen no buffer is supplied, so the walk + strcpy execute during probing.
What was demonstrated on the guest
- Boundary: via the only unprivileged-reachable write path
(
kern.iconv.add) names are bounded: 30 chars OK, 31 chars β EINVAL (checks aticonv.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 exportediconv_add, arms it). - Arming the primitive (as any kernel caller could):
longcs.kocalls the exportediconv_add("UTF-16BE", 200Γ'A', 200Γ'B')β registers fine (no validation). - Unprivileged trigger: user
maxx(uid 1001) runscsreadβ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: retThe 200-bytecs_fromstrcpy 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
strcpys iniconv_sysctl_cslist()with boundedstrlcpy(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.
Fix verification
fixedConfirmed kernel references
Detail
Evidence (decisive lines)
panic.txt: serial capture showing Fatal trap 9, frame pointer 0x10:0x4242424242424242, Stopped at iconv_sysctl_cslist+0xf7 ret, immediately after user maxx ran csread. run.log: boundary proofs (30 chars register as csid 3, 31 chars EINVAL, unprivileged write EPERM), the clean 240-byte control listing, the longcs load message (registered 200-byte charset names) and the full fix A/B outputs.
PoC changes
No seed pack existed; built from scratch. longcs.ko registers the long pair via the exported iconv_add (converter UTF-16BE so stock libiconv.ko suffices); csread.c performs the unprivileged read (works even as a sizing probe since sysctl_old_user returns 0 with oldptr NULL); addtest.c proves the 30/31 boundary and the privilege gate.
Verified recommended fix
Bound charset names at registration (reject >= ICONV_CSNMAXLEN in iconv_register_cspair) and copy them out with strlcpy in iconv_sysctl_cslist.
Verdict
The stack overflow is real: iconv_register_cspair stores names of unbounded length (no ICONV_CSNMAXLEN check anywhere in the kernel registration path) and iconv_sysctl_cslist strcpy-copies them into the char[31] cs_to/cs_from fields of an on-stack struct iconv_cspair_info at iconv.c:368-369. After a KLD registered 200-byte names through the exported iconv_add(), an unprivileged user (uid 1001) reading kern.iconv.cslist panicked the kernel: Fatal trap 9 with frame pointer 0x4242424242424242 stopped at iconv_sysctl_cslist+0xf7 ret, i.e. the name bytes smashed saved RBP and the return path (kernel modules are built -fno-stack-protector). The trigger is fully unprivileged and even fires during the oldptr==NULL sizing pass of any sysctl read. However the ARMING side is gated: kern.iconv.add writes require root (kern_sysctl.c:1447) and its strlen checks reject names >= 31, and every in-tree composer (iconv_ucs_init constants, mount helpers, 31-byte mount-arg fields) is bounded. So on this tree no unprivileged path registers an over-long name; the bug is a latent kernel stack-overflow primitive that any kernel-side caller of iconv_add arms and any user then triggers. privesc_candidate false: impact is privileged-arm / unprivileged-trigger DoS, severity Low as filed. Fix validated A/B on a rebuilt patched module: longcs registration now fails with EINVAL, the 30/31 boundary is unchanged, and the unprivileged read returns the bit-identical control listing with no panic.
No comments yet.