β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2234

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2234 Β· 15 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download raw

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) β€” 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 strcpys 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.

Fix verification

fixed
↓ fix.diffper-fix-DF-2234

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