DF-5765 / verdict.json
{ "finding_id": "DF-5765", "status": "reproduced", "reproduced": true, "impact": "panic", "confidence": "certain", "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_adds 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.", "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_adds kfree. Unprivileged escalation: not possible (both entry paths privilege-gated).", "evidence": "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)", "kernel_refs": [ "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" ], "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.", "attempts": 7, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "runtime_sec": 4800, "guest_dirty": 0, "build_cmd": "cd /root/xlatrace && sh build.sh # verbatim-copy production iconv.c/iconv_xlat.c/iconv_converter_if.m (md5 recorded), make -m /usr/src/share/mk SYSDIR=/usr/src/sys, cc -O2 -g -o hammer hammer.c", "run_cmd": "cd /root/xlatrace && kldload ./xlatrace_harness.ko && sh run.sh # racers (production iconv_open/convstr/close) + ./hammer 8000000 x2 (production kern.iconv.add EFAULT churn); unpatched: panic BADFREE2 + PROOF lines; fixed: clean RC=0", "code_hash": "9776e802f6fd303c343eb4833a5c1cdddacf6568cb74d5cca08349699cd91155", "notes": "Attempt timeline: A,B unpatched full races -> BADFREE2 both (panic.txt, panic.2.txt); C,D fix-build attempts that STILL panicked -> proven stale-objdir builds (MAKEOBJDIRPREFIX mixup between build.sh and rebuild; behaviorally impossible under the v2 patch, which yields opens=0 during EFAULT churn) -> counted as additional unpatched-behavior repros (C transcript lost to console rotation, D = panic.3-stale-build.txt); E short fixed race (4M iters, opens=0, DANGLING=0); F positive control (valid add csid=1, 257k balanced opens); G full fixed run.sh (2x8M, RC=0, 21.3M opens, DANGLING=0). Fix required BOTH parts: serializing the handoff alone leaves the unconditional free-while-referenced in iconv_sysctl_adds error path. Guest reset with-src performed after the run. bt_input.c and hda_acer_c720_patch.c audited clean in the same batch (see VERDICT.md negative notes).", "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).", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "Fixed: with fix.diff applied to the rigs 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.", "fix_evidence": "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" } |