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)
PoC verification
Evidence pack
findings/poc/DF-3306 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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)
- File:
sys/net/ip_mroute/ip_mroute.c:1423-1428 - Class: latent use-after-free (unreachable today β see below)
- Bucket: memcorrupt (hardening)
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).
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
fixedOne-line unschedule added; kernel with the fix builds and boots (no runtime test possible - code path unreachable by construction).
fix_build.log
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.
No comments yet.