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

Premature kfree of persistent per-slot swdesc in txp_rxbuf_reclaim error path causes UAF and double-free

Summary

txp_rxbuf_reclaim at if_txp.c:793-796: MGETHDR/MCLGET failure -> err_sd: kfree(sd). sd is persistent per-slot alloc (txp_alloc_rings:952), only freed in txp_detach:350. rb_sd never NULLed, sc_rxbufprod not advanced. Next reclaim reads dangling rb_sd (UAF read sd_mbuf). txp_rxring_empty (ifconfig down/up) wild-frees sd_mbuf + NULLs. txp_detach double-frees rb_sd. Remote flood to exhaust mbufs or local memory pressure. Sibling of DF-1337 (channel.c UAF pattern). Fix: do NOT kfree sd in error path, NULL sd_mbuf, return.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1461 Β· 12 files
FileTypeDescriptionSize
harness.c trigger-source pattern-faithful userspace harness demonstrating the UAF/double-free from txp_rxbuf_reclaim's error path 6.1 KB view raw
build.sh build-script cc -O2 -Wall -o harness harness.c 144 B view raw
run.sh run-script ./harness 115 B view raw
run.log run-log harness output on #0 baseline kernel β€” BUG CONFIRMED + FIX CONFIRMED 901 B view raw
fix_run.log run-log harness output on #1 patched kernel β€” identical (harness tests pattern, not kernel code) 890 B view raw
fix.diff suggested-fix git-apply-able fix: remove kfree(sd) from err_sd, NULL sd_mbuf in err_mbuf, keep err_sd label with return 265 B view raw
fix_build.log build-log full nativekernel build output for the single-fix kernel (rc=0, 0 errors) 5.6 MB ↓ download
env.txt environment uname, cc version, network interfaces (no 3Com HW) 409 B view raw
VERDICT.md verdict full narrative: mechanism, reachability, harness evidence, fix, impact 6.2 KB ↓ raw
README.md readme build/run/expected + rationale for harness approach 1.2 KB ↓ raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme build/run/expected + rationale for harness approach
↓ download raw

DF-1461: txp_rxbuf_reclaim UAF / double-free

Finding

DF-1461 (High): txp_rxbuf_reclaim in sys/dev/netif/txp/if_txp.c frees a persistent per-slot allocation (struct txp_swdesc *sd) in its error path (lines 793–796), creating a use-after-free and double-free.

Build

cc -O2 -Wall -o harness harness.c

Run

./harness

Expected output

The harness replicates the exact memory-management pattern of txp_rxbuf_reclaim in userspace (malloc/free) to demonstrate the bug without requiring the 3Com Typhoon NIC hardware:

--- Testing BUGGY txp_rxbuf_reclaim (if_txp.c:793-796) ---
...
  => BUG CONFIRMED: dangling pointer + UAF read + double-free

--- Testing FIXED txp_rxbuf_reclaim ---
...
  => FIX CONFIRMED: no double-free, no UAF

Why a harness instead of a live trigger?

The txp driver (3Com 3cR990 "Typhoon", PCI vendor 0x10b7) is compiled into the X86_64_GENERIC kernel (device txp in the config), but there is no 3Com hardware on the QEMU/KVM guest. The driver never probes or attaches, so txp_rxbuf_reclaim is unreachable at runtime. The bug is confirmed by source-level tracing and demonstrated by a pattern-faithful harness.

Reproduction

./build.sh && ./run.sh
VERDICT.md verdict full narrative: mechanism, reachability, harness evidence, fix, impact
↓ download raw

DF-1461 β€” VERDICT

Verdict: REPRODUCED (source-level + harness; runtime-unreachable without 3Com HW)

Bug mechanism

txp_rxbuf_reclaim() in sys/dev/netif/txp/if_txp.c:747-797 refills the receive buffer ring by allocating an mbuf for each empty slot. On MGETHDR or MCLGET failure, the error path frees the persistent per-slot struct txp_swdesc *sd β€” an allocation that lives for the entire lifetime of the interface and is only meant to be freed in txp_detach.

The vulnerable code (if_txp.c:761-797)

while (1) {
    sd = rbd->rb_sd;                    // line 762: read persistent ptr
    if (sd->sd_mbuf != NULL)
        break;                          // slot already set up

    MGETHDR(sd->sd_mbuf, M_NOWAIT, MT_DATA);
    if (sd->sd_mbuf == NULL)
        goto err_sd;                    // line 768: MGETHDR failed

    MCLGET(sd->sd_mbuf, M_NOWAIT);
    if ((sd->sd_mbuf->m_flags & M_EXT) == 0)
        goto err_mbuf;                  // line 772: MCLGET failed
    // ... success path ...
}
sc->sc_rxbufprod = i;                   // line 789: ONLY reached on success
return;

err_mbuf:
    m_freem(sd->sd_mbuf);
err_sd:
    kfree(sd, M_DEVBUF);               // line 796: BUG β€” frees persistent alloc
}

Three consequences

  1. Dangling pointer + UAF read. After the error path, rbd->rb_sd still points to the freed sd (never NULLed), and sc->sc_rxbufprod is never advanced (line 789 only runs on the success path). The next call to txp_rxbuf_reclaim β€” from txp_intr (line 630) or txp_tick (line 1155) β€” reads sd = rbd->rb_sd (line 762), a dangling pointer, then dereferences sd->sd_mbuf (line 763). Under INVARIANTS, the freed slab chunk is poisoned with 0xdeadc0de, so this UAF read panics immediately. Without INVARIANTS, it silently reads whatever now occupies that slab.

  2. Double-free in txp_detach. At detach time (line 349-350): c for (i = 0; i < RXBUF_ENTRIES; i++) kfree(sc->sc_rxbufs[i].rb_sd, M_DEVBUF); The slot whose rb_sd was already freed in the error path is freed a second time β†’ double-free. Under INVARIANTS this panics with a slab assertion (chunk_mark_free on an already-free chunk).

  3. Wild free in txp_rxring_empty. The ifconfig down path calls txp_rxring_empty (if_txp.c:1076-1090) which frees sd->sd_mbuf for each slot. On the UAF slot, sd is a dangling pointer β†’ wild free.

Trigger conditions

The error path fires when MGETHDR or MCLGET fails under memory pressure (mbuf exhaustion). On a system with 3Com Typhoon hardware, an attacker can trigger this by flooding the interface to exhaust the mbuf pool, or under natural memory pressure. The bug is in the GENERIC kernel (device txp), but the code path is only reachable when a 3cR990 NIC is present.

Reachability on this guest

  • txp driver: compiled into X86_64_GENERIC kernel (device txp).
  • 3Com hardware: absent. pciconf -l shows only virtio and Intel 440FX devices. No PCI vendor 0x10b7 device exists.
  • Result: the driver never probes or attaches; no txp_softc exists; txp_rxbuf_reclaim is unreachable at runtime.

This is case (d) from the procedure: "genuinely not reachable on this kernel" β€” the vulnerable code path is dead code at runtime because no matching hardware is present. The bug is a real latent defect confirmed by source tracing and demonstrated by a pattern-faithful harness.

Harness evidence

The harness (harness.c) replicates the exact memory-management pattern of txp_rxbuf_reclaim using userspace malloc/free:

  • Buggy version: kfree(sd) on MGETHDR failure β†’ rb_sd dangling, UAF read on next call, double-free in detach.
  • Fixed version: no free of sd; NULL sd_mbuf; next call succeeds normally; detach is clean.

Output (from run.log):

--- Testing BUGGY txp_rxbuf_reclaim (if_txp.c:793-796) ---
  rb_sd[0] = 0x... (NOT NULLed β€” dangling pointer)
  sd = rb_sd[0] = 0x... => UAF READ: sd->sd_mbuf deref = 0x0
  detach: freeing rb_sd[0] β€” already freed in err_sd => DOUBLE-FREE
  => BUG CONFIRMED: dangling pointer + UAF read + double-free

--- Testing FIXED txp_rxbuf_reclaim ---
  rb_sd[0] = 0x... (still valid β€” NOT freed)
  rb_sd[0]->sd_mbuf = 0x0 (NULLed β€” safe)
  rc=0, rxbufprod now =1 (advanced)
  => FIX CONFIRMED: no double-free, no UAF

Fix

fix.diff β€” two changes to the error path of txp_rxbuf_reclaim:

  1. Remove kfree(sd, M_DEVBUF) at line 796. sd is a persistent per-slot allocation (allocated at txp_alloc_rings:952 with M_WAITOK, only freed in txp_detach:350). Freeing it in the error path is the root cause of the UAF/double-free.

  2. Add sd->sd_mbuf = NULL after m_freem(sd->sd_mbuf) in the err_mbuf path. When MCLGET fails, the mbuf header was allocated but the cluster attach failed; m_freem releases it, and NULLing sd_mbuf leaves the slot in a clean state so the next reclaim attempt can retry the allocation.

The err_sd label is kept (the goto err_sd at line 768 still references it) but its body is now just return;.

Fix validation

  • Diff applies: git apply --check β†’ clean (rc=0).
  • Compiles: make -j6 nativekernel KERNCONF=X86_64_GENERIC β†’ rc=0, 0 errors. Full build log in fix_build.log.
  • Boots: patched kernel (#1, Fri Jul 17 18:09:16 UTC 2026) boots and is stable. kern.version confirms the rebuild.
  • Runtime test: not_testable β€” the vulnerable code path requires 3Com Typhoon hardware not present on this guest. The fix is verified at the source level (no kfree(sd) in txp_rxbuf_reclaim, confirmed via grep) and structurally (the error path now NULLs sd_mbuf and returns without freeing the persistent allocation).

Impact

On a system with 3Com 3cR990 hardware, an attacker who can cause mbuf exhaustion (via network flood or local memory pressure) triggers a kernel UAF read β†’ double-free. Under INVARIANTS (default GENERIC): panic (DoS). Without INVARIANTS: potential heap corruption exploitable for privilege escalation, as the double-free gives a write primitive into the M_DEVBUF slab.

CWE-416 (Use After Free), CWE-415 (Double Free). CVSS 3.1: AV:A/AC:M/PR:N/UI:N/S:U/C:H/I:H/A:H (7.1 High with hardware).

Fix verification

not_testable

compile+boot validated. Harness before/after.

BEFORE: kfree(sd) in err_sd. AFTER: removed + sd_mbuf=NULL.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Fri Jul 17 18:09:16 UTC 2026

Confirmed kernel references

β€”

Detail

Exploit chain

none -- HW-gated (3Com Typhoon NIC absent). INVARIANTS catches as panic.

Evidence (decisive lines)

β€”

Verdict

REPRODUCED (harness). txp_rxbuf_reclaim err_sd kfree(sd) on persistent per-slot alloc -> dangling ptr + UAF read + double-free in detach. txp in GENERIC, no 3Com HW.