Unsynchronized cspair lifetime handoff: iconv_xlat_open/iconv_xlat_close manipulate cp_refcount lockless against under-lock cspair teardown, and iconv_sysctl_add's error path frees referenced pairs unconditionally β UAF writes into freed cspairs, dangling d_table, kernel heap corruption (reproduced panic:BADFREE2)
Summary
The refcount that protects cp_data/csp is claimed and dropped OUTSIDE iconv_lock while the frees it must exclude run UNDER it, and the sysctl-add error path frees pairs unconditionally (never checking the refcount it exists to protect): a colliding open claims a reference on freed memory and keeps a dangling d_table that every later conv reads and close decrements. Rig on verbatim production sources: 3/3 panics ('BADFREE2' in iconv_sysctl_add's kfree) plus live dangling-handle probes (refcount=-2046); two-part fix (serialize the handoff + copyin-before-publish + refcount-checked reap) validated over 21.3M opens, DANGLING=0, positive control passes. Both race sides privilege-gated (sysctl write caps + mount-only open path) - Low, root-triggered DoS/heap-corruption class, no unpriv->root chain.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5765 Β· 20 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.5 KB | β raw | |
| VERDICT.md | β | 8.1 KB | β raw | |
| harness.c | β | 7.4 KB | view raw | |
| hammer.c | β | 4.1 KB | view raw | |
| Makefile | β | 571 B | β download | |
| build.sh | β | 87 B | view raw | |
| run.sh | β | 123 B | view raw | |
| xfer.sh | β | 801 B | view raw | |
| setup.log | β | 396 B | view raw | |
| build.log | β | 9.2 KB | view raw | |
| run.log | β | 2.4 KB | view raw | |
| run.2.log | β | 2.0 KB | view raw | |
| panic.txt | β | 1.7 KB | view raw | |
| panic.2.txt | β | 1.2 KB | view raw | |
| panic.3-stale-build.txt | β | 2.1 KB | view raw | |
| run_fixed.log | β | 1.9 KB | view raw | |
| fix_build.log | β | 8.8 KB | view raw | |
| env.txt | β | 393 B | view raw | |
| fix.diff | β | 5.1 KB | view raw | |
| verdict.json | β | 6.2 KB | view raw |
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: BADFREE2at_kfree+0x593called fromiconv_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.cslistshows no refcount anomalies, no panic (run_fixed.log). Positive control: a validaddstill 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 (-ldumps cslist/drvlist)Makefile,build.sh,run.sh,xfer.shrun.log,run.2.logβ decisive unpatched runs (console captures)panic.txt,panic.2.txt,panic.3-stale-build.txtβ panic capturesrun_fixed.log,fix_build.logβ fix-validation runsetup.logβ verbatim-source md5s;env.txtβ guest environmentVERDICT.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).
DF-5765 β VERDICT
Status: reproduced (panic). Fix validated (fixed).
The finding (pass 2 on sys/libiconv/iconv_xlat.c, beyond DF-2247/2248/2249/5746)
The known trio covers sizing bugs of the flat converter (table
length unvalidated at open; min() truncation; convchar==1
unchecked). What remained in the 125 lines was lifetime:
iconv_xlat_open/iconv_xlat_close are the only code that claims and
releases a cspair's existence guarantee, and they do it unsynchronized:
iconv_xlat.c:60-64βkobj_create(M_WAITOK)(sleeps), thendp->d_table = csp->cp_data(:61), thencsp->cp_refcount++(:63).iconv_xlat.c:73βdp->d_csp->cp_refcount--.- Callers never hold
iconv_lock:iconv_openwalksiconv_cslistunlocked (iconv.c:243viaiconv_lookupcs:163-176) and dispatches the open method aticonv.c:245/:264;iconv_closeaticonv.c:269-273. - The freeing paths hold
iconv_lock:iconv_sysctl_adderror pathiconv.c:439-442βiconv_unregister_cspair(:218-225: TAILQ_REMOVE +kfree(cp_data)+kfree(csp)), andiconv_mod_unload(:85-95, same free via refcount==0 check that the unlocked++can hide from). - Aggravator: the add error path frees unconditionally β it never
checks
cp_refcountβ so even a correctly counted reference does not protect the pair (this is why the fix needed two parts; see below).
Verification (guest-live, production-code KLD rig)
Rig: xlatrace_harness.ko compiles the verbatim production
iconv.c + iconv_xlat.c (md5s in setup.log) and drives them exactly
as production does:
- racer kthreads = the VFS-mount role: production
iconv_open()βiconv_convstr()βiconv_close()on pairzz9to:zz9from; - userland
hammer(root) = the sysctl role: productionkern.iconv.addwithia_datalen=256,ia_data=0x1000(unmapped) β productioncopyinEFAULT β productionbad:path registers, allocates a full-size table, then unconditionally unregisters/frees, in an 8M-iteration loop per round.
Observed (unpatched) β 3/3 race sessions:
- Dangling handoff probe (harness-side read-only check right
after a successful production open: the handle's cspair refcount
must be β₯1): fired with
refcount=-2046/-2047/-204(cleanest capture in panic.3-stale-build.txt):[xlatrace] PROOF: live handle d_csp=0xfffff80116c2bae0 refcount=-2046 -- pair freed/reused under the open (d_table=0xfffff8008da91600)β i.e. the pair was freed under the live handle and ~2Γ10Β³ subsequent decrements landed on the reused chunk. panic: BADFREE2at_kfree+0x593βiconv_sysctl_add+0x172(production kfree of allocator-state-corrupted memory), ssh dies,vm.sh statusβ down. Identical signature across runs (cpuid 2, 5, 2, 3).
The unpatched runs never completed run.sh (panic mid-round), so the in-guest run.log was lost each time; the serial console captures (run.log, run.2.log, panic*.txt) are the runtime evidence.
Primitive characterization / uid=0?
The corruption primitive is a kernel-heap freelist corruption (small, partially controlled Β±1 writes at a predictable offset in a freed 64-112B chunk, repeatable millions of times, leading to BADFREE2). But the escalation chain is closed by construction: both sides of the race are root-gated on a stock system β
kern.iconv.addwrites requirecaps_priv_check(SYSCAP_NOSYSCTL_WR)(kern_sysctl.c:1446-1447);iconv_open()is only reachable from mount(2)-time charset setup (cd9660/msdosfs/smbfs), root-only withvfs.usermount=0(default).
An unprivileged user can drive neither side; a hard-blocked prerequisite (privilege on both fronts) β so impact ceiling = privileged-user-triggered kernel memory corruption / DoS. Severity Low per rubric ("already-privileged user"), impact=panic.
Fix validation (mandatory for memcorrupt) β fixed
fix.diff (git-apply-able vs sys/libiconv/iconv.c, authored after
verification, never applied to the read-only tree) has two parts:
- serialize the handoff:
iconv_open()/iconv_close()holdiconv_lockLK_EXCLUSIVE across lookup+converter-open / converter-close (both direct and composite open paths); - stop freeing referenced pairs:
iconv_sysctl_addcopies the table in before publishing the cspair, and itsbad:path only reaps the pair whencp_refcount == 0.
Honest detour: fix iteration 1 contained only part 1; the first
validation attempt still panicked. Investigation showed that session
(and one other) had loaded a stale build due to a MAKEOBJDIRPREFIX
objdir mixup between build.sh and the rebuild (proven behaviorally:
under part 2's copyin-before-publish, successful opens during
pure-EFAULT churn are impossible β and a verified-clean rebuild shows
exactly opens=0 β yet those runs showed successful opens). With the
objdir fixed and the source patch verified (grep ndata iconv.c,
fresh single-objdir build), the complete fix was validated:
- short race: 4M hammer iterations + racers β
opens=0 DANGLING=0, no panic,cslist: 0 bytes(nothing published, nothing orphaned); - positive control: a valid
kern.iconv.add(real 256-byte table) still succeeds (csid=1); racers performed 774k/257k balanced open/conv/close cycles on it,DANGLING=0, final refcount 0; - full
run.sh(baseline + 2Γ8M race rounds):FIX_RUN_RC=0, cumulativeopens=21335040 convs=21335040 closes=21335040 DANGLING=0, final cslist1 pairs, all refcounts 0, no panic (run_fixed.log).
Baseline-vs-patched table:
| build | race runs | PROOF/dangling | panic |
|---|---|---|---|
| stock | 3 sessions | refcount β204β¦β2047 probes | BADFREE2 Γ3 (identical trace) |
| fixed | 2Γ8M iters + short + positive | 0 | none |
Negative results (what I checked and killed in this file)
iconv_xlat_convbyte-index and length math beyond the known trio: with a full-size table the loop is exactly 1:1,*inbytesleft -= ncannot underflow for themin()path (n β€ both),casetype != 0returns β1 cleanly (xlat has no case support; callers use xlat16 for that), NULLinbuf/outbufhandled (:88-89).kobj_create(M_WAITOK)cannot fail-with-NULL here (M_WAITOK).d_tablealiasingcsp->cp_datais sound if the lifetime claim were synchronized β which is exactly the finding.- No unprivileged entry: sysctl write privilege gate + mount-only open path (see above), so no uid=0 chain exists for this bug.
bt_input.c / hda_acer_c720_patch.c (same batch)
sys/netbt/bt_input.c: current tree is still the pre-DF-5752-fix state β producersnetisr_queue(NETISR_BLUETOOTH, m)at hci_unit.c:437/459/481/543,btintr(bt_input.c:32-40) ignores the netmsg and drains all units viahci_intr. DF-5752 owns the queue contract, DF-5754 owns the walk-vs-detach UAF (hci_unit.c:126-129). Beyond those two:netisr_register(NULL hashfn)is safe (netisr.c:543-544 defaults it), netbt's MOD_UNLOAD returns EBUSY (bt_proto.c:48-49) so no dangling netisr handler, and the mplock wrap matches netbt's lock discipline β nothing new in 41 lines.sys/gnu/dev/sound/pci/hda/hda_acer_c720_patch.c: clean. The patch is gated on codec id ALC283 and Acer C720 subvendor ids (hdaa_patches.c:786-790, values from hardware config space), applied only at codec configure/reinit (hdaa.c:6226, :6563) β the attacker is malicious/buggy physical hardware.hdaa_widget_getis bounds-checked (hdaa.c:1673-1678) and NULL-checked (:195); thewclass.pin.configread (:196) is a union of scalars only (hdaa.h:122-134 β no pointer disclosure); all coefficient writes are fixed constants throughhda_commandwhose return codes the codec can NAK without kernel-state consequence; thetsleepblocks are#if 0. No software-reachable input, no memory-safety defect.
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
fixedFixed: with fix.diff applied to the rig's verbatim production iconv.c and rebuilt clean (single objdir, source patch verified), the identical race rig that panicked 3/3 times stock survives baseline + 2x8M churn rounds with zero dangling handoffs (opens=21335040, DANGLING=0), kern.iconv.cslist shows no refcount anomalies, and a valid (positive-control) kern.iconv.add still registers a working pair that racers open/convert/close 257k+ times balanced. The previously-observed panic:BADFREE2 and PROOF lines are gone.
run_fixed.log (full fixed run: FIX_RUN_RC=0, DANGLING=0, cslist '1 pairs, all refcounts 0'); fix_build.log; panic.txt/panic.2.txt/panic.3-stale-build.txt (baseline BADFREE2 x3); fix.diff
Confirmed kernel references
- sys/libiconv/iconv_xlat.c:54-66
- sys/libiconv/iconv_xlat.c:68-76
- sys/libiconv/iconv_xlat.c:60
- sys/libiconv/iconv_xlat.c:61
- sys/libiconv/iconv_xlat.c:63
- sys/libiconv/iconv_xlat.c:73
- sys/libiconv/iconv.c:233-273
- sys/libiconv/iconv.c:243
- sys/libiconv/iconv.c:245
- sys/libiconv/iconv.c:264
- sys/libiconv/iconv.c:397-443
- sys/libiconv/iconv.c:426-431
- sys/libiconv/iconv.c:439-442
- sys/libiconv/iconv.c:218-225
- sys/libiconv/iconv.c:85-98
- sys/kern/kern_sysctl.c:1446-1447
Detail
Exploit chain
root: loop sysctl kern.iconv.add (ia_datalen=256, ia_data=unmapped -> EFAULT error path) to publish+unconditionally-free the zz9to:zz9from cspair and table while root-driven mount-role threads (production iconv_open->iconv_convstr->iconv_close) collide -> open lands on the pair, sleeps in kobj_create(M_WAITOK), pair is freed under it -> cp_refcount++ writes into freed chunk, d_table dangles -> conv/close write/read freed memory repeatedly (observed refcount=-2046 on a reused chunk) -> allocator freelist corruption -> panic:BADFREE2 in iconv_sysctl_add's kfree. Unprivileged escalation: not possible (both entry paths privilege-gated).
Evidence (decisive lines)
run.log + panic.txt (run1 console: BADFREE2, cpuid=2); run.2.log + panic.2.txt (run2: interleaved PROOF refcount=-204 + BADFREE2 cpuid=5); panic.3-stale-build.txt (cleanest PROOF line: d_csp=0xfffff80116c2bae0 refcount=-2046 d_table=0xfffff8008da91600); run_fixed.log + fix_build.log (validated fix: FIX_RUN_RC=0, opens=21335040 DANGLING=0, cslist all-zero); setup.log (verbatim production-source md5s)
PoC changes
Self-authored pass-2 rig (no prior seed): KLD compiles verbatim production iconv.c+iconv_xlat.c (Makefile mirrors production module build); harness adds per-CPU racer kthreads (CPU0 left free; 1-tick sleep every 1024 attempts after the first attempt livelocked userland -- pure-spin kthreads starve user LWPs), a read-only post-open refcount probe via a mirror struct, and sysctls hw.xlatrace.{ctl,stats}; userland hammer needed errno.h added; run.sh uses dotted sysctl names.
Verified recommended fix
Hold iconv_lock across iconv_open/iconv_close converter dispatch, copy the table in before publishing the cspair in kern.iconv.add, and only reap the pair on the error path when cp_refcount==0 (fix.diff, sys/libiconv/iconv.c).
Verdict
REPRODUCED as panic:BADFREE2 (3/3 unpatched race sessions, identical trace _kfree+0x593 <- iconv_sysctl_add+0x172) plus an on-console dangling-handoff probe (live handle refcount=-2046). iconv_xlat_open/iconv_xlat_close claim and drop the cspair lifetime reference (iconv_xlat.c:63/:73) with no lock, while iconv_sysctl_add's error path frees the cspair and its table under iconv_lock (iconv.c:439-442 -> 218-225) unconditionally (no cp_refcount check) and iconv_open/iconv_close never take that lock (iconv.c:233-273); a racing open sleeps in kobj_create (M_WAITOK) between the unlocked list-walk hit and the refcount claim, then writes into freed memory and caches a dangling d_table, corrupting the kernel heap until the production kfree catches it. Impact ceiling is a root-triggered race on a stock system: kern.iconv.add writes need SYSCAP_NOSYSCTL_WR (kern_sysctl.c:1446-1447) and iconv_open is mount(2)-time-only (vfs.usermount=0 default), so no unprivileged->uid0 chain exists; classified Low severity / DoS-class panic. fix.diff (serialize open/close under iconv_lock + copyin-before-publish + refcount-checked reap) validated on the identical rig: 2x8M churn rounds + 21.3M balanced opens with DANGLING=0, positive-control add still works, no panic.
No comments yet.