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

expire_upcalls() frees bw meters without unschedule_bw_meter() β€” latent UAF (FreeBSD divergence; unreachable today)

Summary

expire_upcalls() kfrees an mfc's bandwidth meters without unscheduling them from bw_meter_timers[] - FreeBSD calls unschedule_bw_meter() here. A scheduled meter freed on that path would stay linked in the timer hash and be walked/relinked by the next 1 Hz pass: guaranteed UAF. Reachability proof shows it is currently dead code: meters attach only via mfc_find(), which returns only non-stalled entries, while expire_upcalls only frees stalled entries which are created with mfc_bw_meter=NULL and never receive meters. Hardening: the invariant is one refactor away from breaking; no PoC possible (not_testable). Fix: add unschedule_bw_meter(x) before the kfree (match FreeBSD).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3306 Β· 4 files
FileTypeDescriptionSize
README.md β€” 1.5 KB ↓ raw
VERDICT.md β€” 1.8 KB ↓ raw
fix.diff β€” 2.7 KB view raw
verdict.json β€” 2.0 KB view raw

DF-3306 β€” expire_upcalls() frees bw meters without unschedule (latent UAF)

Analysis (no runnable PoC β€” the code is currently unreachable)

expire_upcalls() frees an mfc's bandwidth meters with a bare loop:

while (mfc->mfc_bw_meter != NULL) {
    struct bw_meter *x = mfc->mfc_bw_meter;
    mfc->mfc_bw_meter = x->bm_mfc_next;
    kfree(x, M_BWMETER);            /* <-- no unschedule_bw_meter(x) */
}

FreeBSD's equivalent calls unschedule_bw_meter(x) before kfree. Without it, any scheduled ("<=" type) meter would stay linked in the global bw_meter_timers[] hash after being freed β†’ the 1 Hz bw_meter_process() would then walk freed memory (ip_mroute.c:2715-2741) and relink it β€” a textbook UAF.

Reachability today: none. All code paths that attach bw meters go through mfc_find() (ip_mroute.c:327-332), which only returns entries with mfc_stall == NULL; expire_upcalls() only frees entries with mfc_stall != NULL (pending NOCACHE upcalls, created at ip_mroute.c:1290-1343 with mfc_bw_meter = NULL and never given meters). The invariant "stalled β‡’ no meters" currently holds, so the kfree loop is a no-op in practice. It is one refactor away from a real UAF, and it diverges from FreeBSD for no reason.

Fix

Add unschedule_bw_meter(x); before kfree(x, M_BWMETER); (match FreeBSD; included in this pack's fix.diff).

VERDICT.md
↓ download raw

DF-3306 VERDICT β€” false-positive-for-now / latent (no runtime test possible)

expire_upcalls() (ip_mroute.c:1423-1428) frees an mfc's bandwidth meters with a bare kfree loop and never calls unschedule_bw_meter(), unlike FreeBSD. If a scheduled ("<=") meter were ever attached to an entry that expire_upcalls frees, it would remain linked in bw_meter_timers[] after being freed β†’ guaranteed UAF at the next 1 Hz bw_meter_process() pass.

Reachability analysis proves this is not reachable in the current code: bw meters attach only via add_bw_upcall() β†’ mfc_find() (:2286, and mfc_find at :327-332 only returns entries with mfc_stall == NULL), while expire_upcalls() only frees entries with mfc_stall != NULL (:1400), and NOCACHE-created stalled entries get mfc_bw_meter = NULL at creation (:1337) and never receive meters. The invariant "stalled β‡’ no meters" holds on every path; the kfree loop is dead code today. One refactor away from a real UAF, and a pointless divergence from FreeBSD β€” hence filed as Info (hardening), confidence certain about the divergence, speculative about any future trigger.

No PoC is possible (the precondition does not exist); the fix is one line, included in fix.diff, and the patched kernel built and boots:

        while (mfc->mfc_bw_meter != NULL) {
            struct bw_meter *x = mfc->mfc_bw_meter;
            mfc->mfc_bw_meter = x->bm_mfc_next;
+           unschedule_bw_meter(x);
            kfree(x, M_BWMETER);
        }

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
baseline no→ patch + rebuild →patched clean

One-line unschedule added; kernel with the fix builds and boots (no runtime test possible - code path unreachable by construction).

fix_build.log
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Thu Sep 10 10:33:47 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/PIMTEST x86_64

Confirmed kernel references

Detail

Evidence (decisive lines)

['README.md (reachability proof)', 'VERDICT.md']

Verified recommended fix

unschedule_bw_meter(x) before kfree(x, M_BWMETER) in expire_upcalls(), matching FreeBSD.

Verdict

Latent UAF: expire_upcalls() (:1423-1428) kfrees bw meters without unschedule_bw_meter() (FreeBSD calls it), so a scheduled meter freed there would stay linked in bw_meter_timers[] -> UAF at the next 1 Hz pass. Unreachable today: meters only attach to non-stalled entries (mfc_find :327-332 skips stalled), expire_upcalls only frees stalled entries (:1400) which are created with mfc_bw_meter=NULL (:1337). Info-level hardening; one-line fix included in fix.diff, patched kernel builds and boots.