⬢ DragonFlyBSD Kernel Audit
DF-2233 / run.log
← back to finding ↓ download raw
DF-2233 PoC evidence -- runtime log (assembled from verbatim session captures;
per-boot guest files are wiped by vm.sh snapshot reverts, raw outputs preserved)

SETUP (every boot): findings/poc/pushrig.sh  -> /root/rig2233 build
Guest: DragonFly dfbsd 6.5-DEVELOPMENT #0 X86_64_GENERIC, 6 vCPU, INVARIANTS,
       cc 8.3, /usr/src pristine (iconv.c md5 1331efafd4d722e4eb0dbc2276cbc0dc)

=============================================================================
RUN 1 (rig1) -- STOCK /boot/kernel/libiconv.ko, no custom module involved.
Walk side : 4 x  churnadd walk 200000000   (kern.iconv.add EINVAL probes;
            every probe runs the LOCKLESS iconv_lookupconv() walk,
            sys/libiconv/iconv.c:418 -> :145-160)
Free side : loop { kldunload libiconv; kldload libiconv }
Result   : GUEST PANICKED in under 45 seconds.

serial console (findings/poc/DF-2233/panic.txt):
    Fatal user address access from kernel mode from churnadd at ffffffff82600350
    Fatal trap 12: page fault while in kernel mode
    cpuid = 0; lapic id = 0
    fault virtual address  = 0x800
    fault code              = supervisor read data, page not present
    instruction pointer     = 0x8:0xffffffff82600350
    current process         = 1244 (churnadd)
    Stopped at      iconv_lookupconv+0x30:  movq    0x800(%rsi),%rdi

Reading: the walker dispatched ICONV_CONVERTER_NAME(dcp) (kobj method) on a
converter class whose ops table had been freed/NULLed by the concurrent
kldunload (kobj_class_uninstantiate, sys/kern/subr_kobj.c:225-243 ->
kfree(cls->ops); cls->ops = NULL), i.e. load at NULL+0x800.

=============================================================================
RUN 2 (rig23, second boot) -- bundled race2233.ko (pristine sources + harness)
loaded, free side = churnadd bad + kldunload race2233 loop.
Result   : GUEST PANICKED again, IDENTICAL signature (panic.2.txt):

    Fatal user address access from kernel mode from churnadd at ffffffff82600270
    Fatal trap 12: page fault while in kernel mode
    fault virtual address  = 0x800
    instruction pointer     = 0x8:0xffffffff82600270
    Stopped at      iconv_lookupconv+0x30:  movq    0x800(%rsi),%rdi

Second independent reproduction on a fresh boot.

=============================================================================
RUN 3 (rig2) -- cslist walk vs register/free churn WITHOUT any unload.
race2233.ko loaded; 3 x sysctl -w debug.race2233_run=100000000
(in-kernel iconv_open hammer: full cslist + converters walk per call);
2 x churnadd bad 20000000 (register + copyin(EFAULT) + unregister churn,
exercising iconv_unregister_cspair under LK_EXCLUSIVE).
Result after 60 s, guest UP:
    debug.race2233_walks: 300000000
    debug.race2233_opens: 9375000
    debug.race2233_iters: 300000000
No panic: the freed 128-byte M_ICONV chunks stay mapped in the slab and are
immediately reused by the next registration (LIFO), so the lockless walk
performs stale-but-mapped reads.  This is still a use-after-free READ
(walker iterates entries freed under the exclusive lock); it simply does not
fault in this allocator configuration.

Side observation (worth upstream attention): after heavy successful
open/close churn the UTF-16BE<->UTF-8 pair refcount LEAKS and never drains:

    cslist: sysctl=0 errno=0 bytes=160
      id=1 base=0 ref=12685 to=[UTF-16BE] from=[UTF-8]
      id=2 base=0 ref=0    to=[UTF-8] from=[UTF-16BE]

Afterwards kldunload of the module fails with "Device busy" forever.

=============================================================================
RUN 4 (rig3c) -- in-kernel walkers (failing opens only, zero refs held) +
tight kldunload/kldload loop of race2233.ko.
Result   : GUEST DIED HARD within ~50 s -- no trap text reached the serial
console (silent death consistent with the module being unmapped while
walker threads execute inside its text; triple-fault class).

=============================================================================
FIX VALIDATION (see fixvalidation.log) -- sys/libiconv/iconv.c patched with
fix.diff (lock across traversal + in-flight refcount drain guard), modules
rebuilt from patched tree:

 SOAK A  fixed libiconvfix.ko + 4 x churnadd walk hammer, NO unload, 90 s :
         quiet, guest healthy.
 SOAK B2 fixed race2233.ko + 3 in-kernel walkers + 2 churnadd bad, NO unload,
         120 s : quiet; debug.race2233_walks reached 316,798,072. Guest up.
 SOAK C3 fixed module, walkers + churn hammering + 240 s kldunload storm :
         NO PANIC; the module either refuses unload (EBUSY) or unloads only
         in a drain gap; after unload all callers fail cleanly (unknown oid).
         Guest survived the whole storm.
 SOAK C4 fixed module, supervisor restarting walkers after every reload,
         180 s : script completed alive ("1 unload+reload cycles completed
         alive"); ~4 s AFTER the storm finished the guest rebooted silently
         once -- the documented RESIDUAL micro-race (a caller whose
         iconv_refs increment lands after the unloader's final check is not
         drained; module text can be unmapped under it).  This residual is
         the generic DragonFly kldunload has-no-drain-API limitation, not the
         reported traversal UAF: across all fixed-build runs the baseline
         signature (iconv_lookupconv / iconv_lookupcs faulting on freed
         entries) never reappeared.

BASELINE vs FIXED summary:
 baseline : panic in iconv_lookupconv+0x30 (freed-converter deref) < 45 s,
            reproduced twice on independent boots.
 fixed    : walk fully serialized; 240 s unload storms and 316M-walk soaks
            clean; failure mode under maximum aggression changed to the
            unload-drain assert / one silent reboot (separate defect class,
            needs linker-level drain support in DragonFly).