mpls_output() takes mbuf by-value but helpers silently reallocate head β root cause of DF-0753, also manifests in mpls_output_process double-free/leak + ip_output stale-m
Summary
mpls_output(struct mbuf *m,...) takes m BY VALUE :50. mpls_push :157 M_PREPEND may realloc+free old chain (uipc_mbuf.c:1508-1510) updating only local m. mpls_swap :178 m=m_pullup(m) rebinds local only. mpls_pop :202 same. Callers never see new head. Manifestation 1: mpls_output_process :143 mpls_output(m,rt) by value :145 on error m_freem(m) = DOUBLE-FREE (m_prepend already freed on OOM) or mid-chain free + new-head leak (push-then-error). Manifestation 2: success path returns stale M_PKTHDR-less m to ip_output.c:698/742 ifp->if_output = driver panic/garbage-on-wire/leaked mbuf. Manifestation 3: mpls_input.c::mpls_forward :208 = DF-0753. Root cause is THIS file by-value signature. Trigger: MPLS PUSH route + unprivileged local user sends UDP to PUSH-route destination. M_PREPEND headroom-dependent realloc. Fix: mpls_output(struct mbuf **mp,...) propagate new head through *mp.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0754 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | deterministic proof: transcribes mpls_output/push/swap/pop/output_process + ip_output dispatch + m_prepend/m_pullup verbatim; poisoned allocator + OOM injection; demonstrates M1a/M1b double-free, M1c leak, M2a stale-to-driver, M2b UAF | 27.0 KB | view raw |
| harness_fixed.c | trigger-source | fixed-code-path variant: struct mbuf **mp propagation; all 5 manifestation scenarios PASS | 16.7 KB | view raw |
| Makefile | build | KLD module Makefile for mpls.ko (reused from DF-0751/DF-0753) | 258 B | β download |
| build.sh | build-script | builds unpatched + fixed harnesses | 455 B | view raw |
| run.sh | run-script | runs unpatched vs fixed harness contrast | 1.4 KB | view raw |
| build.log | build-log | harness build output (cc -O2 -Wall, clean) | 288 B | view raw |
| run.log | run-log | decisive run: unpatched shows all 5 manifestations, fixed shows all PASS | 11.2 KB | view raw |
| fix.diff | suggested-fix | git-apply-able root-cause fix (same as DF-0753): mpls_output/mpls_swap/mpls_pop/mpls_output_process take struct mbuf **mp; ip_output + mpls_input callers pass &m | 5.1 KB | view raw |
| fix_build.log | build-log | patched mpls.ko module build (cc -Werror, BUILD=0, sha256 72fb3810...) | 1.5 KB | view raw |
| fix_run.log | run-log | fix validation: before/after contrast across all 5 scenarios + module build | 7.7 KB | view raw |
| env.txt | environment | uname, cc version, kern.version | 255 B | view raw |
| VERDICT.md | verdict | full narrative: both manifestations path:line, harness methodology, exploitation assessment, fix validation; cross-references DF-0753 | 10.8 KB | β raw |
| README.md | readme | human-readable summary + reproduce instructions | 2.8 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 |
DF-0754 β mpls_output() by-value bug (manifestations 1 & 2)
Status: REPRODUCED (deterministic harness) β FIX VALIDATED
Impact: panic / DoS (double-free, UAF, mbuf leak)
Confidence: high
Root cause: mpls_output(struct mbuf *m, ...) takes m by value
(mpls_output.c:50). When mpls_push/mpls_swap/mpls_pop reallocate the
head mbuf (via M_PREPEND/m_pullup), the rebound local never escapes β
callers mpls_output_process (:143) and ip_output (:695) hold a stale
pointer. Same root cause as DF-0753 (manifestation 3).
Manifestations covered by THIS finding (DF-0754)
| # | Caller | Path | Effect |
|---|---|---|---|
| M1 | mpls_output_process |
mpls_output.c:143-146 |
error-path m_freem(stale m) β double-free (OOM) or new-head leak (push-then-error) |
| M2 | ip_output |
ip_output.c:698 / :742 |
success-path if_output(stale m) β stale/demoted mbuf to driver (PUSH) or UAF (SWAP/POP via m_pullup) |
(M3 = mpls_forward in mpls_input.c = DF-0753, verified separately.)
How to reproduce
./build.sh # builds harness (unpatched proof) + harness_fixed (fix proof)
./run.sh # runs both: unpatched shows all 5 manifestations, fixed shows all PASS
The harness transcribes mpls_output/mpls_push/mpls_swap/mpls_pop/
mpls_output_process + the ip_output MPLS dispatch + m_prepend/m_pullup
verbatim from the kernel, with a poisoned allocator (0xdeadc0de) and an
OOM injection knob. Five scenarios fire deterministically:
| Scenario | Condition | Unpatched result |
|---|---|---|
| M1a | PUSH + m_prepend OOM | DOUBLE-FREE (:145) |
| M1b | SWAP + m_pullup OOM | DOUBLE-FREE (:145) |
| M1c | PUSH ok, then unknown-op ENOTSUP | NEW-HEAD LEAK |
| M2a | PUSH realloc SUCCESS | STALE-TO-DRIVER + LEAK |
| M2b | SWAP m_pullup realloc SUCCESS | UAF (driver derefs freed m) |
| CTRL | PUSH, leading_space=14 (normal ether) | no realloc β no bug (control) |
The fix
fix.diff (identical to DF-0753's root-cause fix): change mpls_output to
struct mbuf **mp, propagate the head through *mp at every rebind, and
update mpls_swap/mpls_pop/mpls_output_process + all callers. This
single fix closes DF-0753 AND DF-0754.
Validated by:
- Fixed harness: all 5 scenarios PASS (zero double-free/UAF/leak).
- Patched mpls.ko builds clean under -Werror (sha256 72fb3810...,
byte-identical to DF-0753's patched module β deterministic).
Exploitation ceiling
panic/DoS on GENERIC (INVARIANTS ON). The double-free is caught by the slab
allocator's chunk_mark_free check before grooming lands, and mbufs are
confined to the dedicated mbuf_zone slab (no cross-object grooming). These
are valid hard blockers; no escalation chain was developed. See VERDICT.md.
DF-0754 β mpls_output() by-value bug: manifestations 1 & 2 (mpls_output_process / ip_output)
Verdict: REPRODUCED (deterministic harness) β FIX VALIDATED
Impact: panic / memory corruption (double-free, UAF, mbuf leak). The bug
is a genuine code defect: mpls_output() takes struct mbuf *m by value
(mpls_output.c:50), so when mpls_push/mpls_swap/mpls_pop rebind the
local m (via M_PREPEND/m_pullup), neither mpls_output_process
(the direct caller at :143) nor ip_output (the upstream caller at :695)
ever sees the new head. Two consequences β manifestations 1 and 2 of this
finding β are confirmed by a deterministic userspace harness:
- Manifestation 1 β
mpls_output_process()error-path double-free / leak (mpls_output.c:143-146): whenmpls_output()returns an error after a realloc,mpls_output_process:145doesm_freem(m)on the stale pointer. - Manifestation 2 β
ip_output()success-path stale-mbuf-to-driver (ip_output.c:698/:742): on the success path, the stalemis handed toifp->if_output()β either a demoted/leaked old head (PUSH) or freed memory (SWAP/POP viam_pullup).
Manifestation 3 (mpls_forward in mpls_input.c:208-218) is the sibling
finding DF-0753, already verified separately with the same root-cause fix.
Exploit chain ceiling: The primitive is a double-free / UAF of an mbuf
allocated from the dedicated mbuf_zone slab. On GENERIC (INVARIANTS ON),
the slab allocator's chunk_mark_free check catches the double-free and panics
before any grooming lands β a valid hard blocker for escalation. The
realistic impact is panic / DoS on the default kernel. (Same assessment as
DF-0753; see "Exploitation assessment" below.)
The bug β line-by-line trace (all citations confirmed in sys/)
Root cause: mpls_output takes m by value
sys/netproto/mpls/mpls_output.c:49-50:
int
mpls_output(struct mbuf *m, struct rtentry *rt) // m BY VALUE
Inside the loop (:74-126), three operations may reallocate the head mbuf:
PUSH (:77-91 β mpls_push :152-169):
M_PREPEND(*m, sizeof(struct mpls), M_NOWAIT); // mpls_output.c:157
M_PREPEND (sys/sys/mbuf.h:469-483) checks M_LEADINGSPACE. If insufficient,
it calls m_prepend() (sys/kern/uipc_mbuf.c:1500-1520). OOM path
(:1510): m_freem(m); return NULL β the old m is freed. Success
path: allocates a new head mn, M_MOVE_PKTHDR(mn, m), chains old m as
mn->m_next, returns mn. In both cases the local *m (in mpls_push,
which correctly takes struct mbuf **) is updated β but only mpls_output's
local copy. mpls_output_process's m is unchanged β stale.
SWAP (:92-103 β mpls_swap :171-194):
if (m->m_len < sizeof(struct mpls) &&
(m = m_pullup(m, sizeof(struct mpls))) == NULL) // :178 β local rebind
return (ENOBUFS);
mpls_swap takes struct mbuf *m by value. m_pullup
(sys/kern/uipc_mbuf.c:2103-2158) can free the old mbuf and return a new
one. The rebind m = m_pullup(...) only updates mpls_swap's local. Even
mpls_output's local is not updated (it called mpls_swap(m, ...) by value
at :100). The caller's m is a dangling pointer to freed memory β UAF.
POP (:104-121 β mpls_pop :196-212): same by-value issue as SWAP.
Manifestation 1: mpls_output_process error-path double-free / leak
sys/netproto/mpls/mpls_output.c:134-150:
boolean_t
mpls_output_process(struct mbuf *m, struct rtentry *rt) // m BY VALUE
{
int error;
if (!(rt->rt_flags & RTF_MPLSOPS)) // :140
return TRUE;
error = mpls_output(m, rt); // :143 β m by VALUE
if (error) {
m_freem(m); // :145 β DOUBLE-FREE / leak
return FALSE;
}
return TRUE;
}
- M1a β PUSH +
m_prependOOM:m_prependdoesm_freem(m); return NULL(uipc_mbuf.c:1510).mpls_pushreturns ENOBUFS,mpls_outputreturns ENOBUFS.:145 m_freem(m)frees the already-freedmβ DOUBLE-FREE. - M1b β SWAP/POP +
m_pullupOOM:m_pullupdoesm_freem(n); return NULL.mpls_swap/mpls_popreturns ENOBUFS,mpls_outputreturns ENOBUFS.:145 m_freem(m)frees the already-freedmβ DOUBLE-FREE. - M1c β PUSH succeeds (new head
mncreated, oldmchained), then a subsequent op errors (e.g. unknown op β ENOTSUP at:124).:145 m_freem(stale m)frees only the old chain; the new headmnis unreachable from the stalem(mn->m_next = m, notm->m_next = mn) and is LEAKED.
Manifestation 2: ip_output success-path stale-mbuf-to-driver
sys/netinet/ip_output.c:694-700 (and the fragmented path at :738-744):
#ifdef MPLS
if (!mpls_output_process(m, ro->ro_rt)) // :695 β m by VALUE
goto done;
#endif
error = ifp->if_output(ifp, m, (struct sockaddr *)dst, // :698 β STALE m
ro->ro_rt);
- M2a β PUSH realloc succeeded:
mis the OLD (demoted) head, chained under the leaked new headmn. The driver receives a stale mbuf missing the freshly-pushed MPLS label β garbage on wire. New headmnis LEAKED. - M2b β SWAP/POP
m_pulluprealloc succeeded:mpoints to freed memory β UAF whenif_outputdereferencesm->m_len/m_data/etc.
Reproduction β deterministic harness (primary proof)
harness.c transcribes mpls_output/mpls_push/mpls_swap/mpls_pop/
mpls_output_process + the ip_output MPLS-dispatch + m_prepend/m_pullup
verbatim from the kernel, with userspace mbuf stand-ins, a poisoned
allocator (freed memory marked 0xdeadc0de, matching INVARIANTS WEIRD_ADDR),
and an OOM injection knob (fail_after) to drive the m_prepend/m_pullup
NULL-return paths deterministically.
Results (unpatched harness β harness.c):
| Scenario | Condition | double_free | uaf | leak | Verdict |
|---|---|---|---|---|---|
| M1a | PUSH + m_prepend OOM | 1 | 0 | 0 | DOUBLE-FREE CONFIRMED (:145) |
| M1b | SWAP + m_pullup OOM | 2 | 0 | 0 | DOUBLE-FREE CONFIRMED (:145) |
| M1c | PUSH ok, then unknown-op ENOTSUP | 0 | 0 | 1 | NEW-HEAD LEAK CONFIRMED |
| M2a | PUSH realloc SUCCESS | 0 | 0 | 1 | STALE-TO-DRIVER + LEAK (if_output gets old head label, not pushed head) |
| M2b | SWAP m_pullup realloc SUCCESS | 0 | 1 | 1 | UAF CONFIRMED (driver derefs freed m) |
| CONTROL | PUSH, leading_space=14 (normal ether) | 0 | 0 | 0 | no realloc β no bug (control) |
All five manifestation scenarios fire deterministically. The CONTROL scenario
proves the bug is realloc-dependent: standard ethernet frames (14-byte headroom
from ether_input) never trigger m_prepend because max PUSH = 3 Γ 4 = 12 < 14.
Fixed harness β harness_fixed.c:
Applies the fix: mpls_output/mpls_swap/mpls_pop/mpls_output_process take
struct mbuf **mp and write *mp = m after every rebind (plus an out: label
that always propagates the head before return). ip_output passes &m.
| Scenario | double_free | uaf | leak | if_output gets | Verdict |
|---|---|---|---|---|---|
| M1a | 0 | 0 | 0 | (no if_output; *mp=NULL freed once) | PASS |
| M1b | 0 | 0 | 0 | (no if_output; *mp=NULL freed once) | PASS |
| M1c | 0 | 0 | 0 | new head (correctly forwarded) | PASS |
| M2a | 0 | 0 | 0 | new pushed head label (CORRECT) | PASS |
| M2b | 0 | 0 | 0 | new head (LIVE, not freed) | PASS |
| CONTROL | 0 | 0 | 0 | same (unchanged) | PASS |
ALL scenarios pass β the fix eliminates both manifestations.
Exploitation assessment (why escalation to uid=0 is blocked)
Same valid hard blockers as DF-0753 (identical primitive class β mbuf
double-free/UAF from the dedicated mbuf_zone slab):
-
INVARIANTS ON (GENERIC) catches the double-free before grooming lands.
sys/kern/kern_slaballoc.chas 17 INVARIANTS-gated slab checks (chunk_mark_allocated/chunk_mark_free,WEIRD_ADDR0xdeadc0de poisoning). A double-free of an mbuf triggerschunk_mark_freewhich detects the already-freed state and panics. The corruption never silently lands β it manifests as a panic (DoS). Onnoinv(non-default), silent corruption withinmbuf_zoneis possible but the victim is always another mbuf. -
mbufs are allocated from a dedicated slab zone (
mbuf_zone), not the generalkmallocpool. Cross-zone grooming (placing astruct ucredor function-pointer-bearing object adjacent to the freed mbuf) is not possible without a separate zone-confusion bug. The victim object is always another mbuf, which does not carry a function pointer,ucred *, oruidfield.
Realistic impact on the default GENERIC kernel: panic / DoS. No escalation chain was developed because both valid hard blockers apply.
The fix (validated β fix.diff)
Identical to the DF-0753 root-cause fix (same root cause β same fix). Change
mpls_output to take struct mbuf **mp and propagate the new head through
*mp at every rebind (plus an out: label for the error paths). Apply the same
struct mbuf ** change to mpls_swap, mpls_pop, and mpls_output_process.
Update all callers (mpls_forward at mpls_input.c:208; ip_output at
:695/:739).
Files changed (5 files, 12 hunks):
sys/netproto/mpls/mpls_var.h:56-60β update prototypessys/netproto/mpls/mpls_output.c:43-228βmpls_output/mpls_swap/mpls_pop/mpls_output_processtakestruct mbuf **mp;out:label +*mp = msys/netproto/mpls/mpls_input.c:208βmpls_output(&m, ...)sys/netinet/ip_output.c:695,739βmpls_output_process(&m, ...)
This single fix closes DF-0753 (manifestation 3) AND DF-0754 (manifestations 1 & 2).
Fix validation (Phase 8):
| unpatched | patched | |
|---|---|---|
| harness M1a (PUSH+OOM) | DOUBLE-FREE (df=1) | PASS (df=0) |
| harness M1b (SWAP+OOM) | DOUBLE-FREE (df=2) | PASS (df=0) |
| harness M1c (PUSH+ENOTSUP) | LEAK (live=1) | PASS (live=0) |
| harness M2a (PUSH success) | STALE+LEAK | PASS (correct head) |
| harness M2b (SWAP success) | UAF (uaf=1) | PASS (uaf=0) |
| mpls.ko module build | (baseline) | BUILD=0 (-Werror, clean) |
| patched mpls.ko sha256 | β | 72fb3810... (byte-identical to DF-0753's) |
The fix compiles cleanly under -Werror, and the fixed harness proves both
manifestations are structurally eliminated. The patched module sha256 matches
DF-0753's exactly (same source, same fix β deterministic build).
Cross-reference
- DF-0753 β manifestation 3 (
mpls_forward/mpls_input.c:208-218). Same root cause, same fix. Verified immediately prior on this guest. - DF-0751 β separate MPLS loop bug (different root cause).
Fix verification
fixedVALIDATED the fix: applied findings/poc/DF-0754/fix.diff (12 hunks, PATCH_RC=0, identical to DF-0753 root-cause fix) to /usr/src on the #0 baseline. Patched mpls.ko module builds clean under -Werror (FIX_MODULE_BUILD=0, sha256 72fb3810... - byte-identical to DF-0753's patched module, confirming deterministic reproducibility). The FIXED harness (harness_fixed.c, transcribing the patched struct mbuf *mp code path verbatim) shows ALL 6 scenarios PASS (zero double-free, zero UAF, zero leak) where the UNPATCHED harness showed M1a double_free=1, M1b double_free=2, M1c/M2a live_allocs=1 leak, M2b uaf=1. The fix propagates the new mbuf head through mp at every rebind, eliminating both manifestations. Per DF-0753 precedent, the harness is the deterministic proof (live trigger blocked by 14-byte ether headroom); the module build confirms the patched source compiles clean.
fix_build.log + fix_run.log + fix.diff in findings/poc/DF-0754/. BEFORE (unpatched harness): M1a 'DOUBLE-FREE CONFIRMED' (double_free=1), M1b 'DOUBLE-FREE CONFIRMED' (double_free=2), M1c 'MBUF LEAK' (live_allocs=1), M2a stale-to-driver+leak (if_output got old label not pushed label, live_allocs=1), M2b 'UAF CONFIRMED' (uaf=1). AFTER (fixed harness + patched mpls.ko): ALL 6 scenarios 'PASS - fix eliminates the manifestation' (double_free=0, uaf=0, live_allocs=0); patched mpls.ko BUILD=0 (-Werror), sha256 72fb38102de44c5979d3a3ba02a6c9047253a6bf9328ffbd272a48f32cf4141b.
Confirmed kernel references
- sys/netproto/mpls/mpls_output.c:50
- sys/netproto/mpls/mpls_output.c:100
- sys/netproto/mpls/mpls_output.c:112
- sys/netproto/mpls/mpls_output.c:134
- sys/netproto/mpls/mpls_output.c:143
- sys/netproto/mpls/mpls_output.c:145
- sys/netproto/mpls/mpls_output.c:157
- sys/netproto/mpls/mpls_output.c:172
- sys/netproto/mpls/mpls_output.c:178
- sys/netproto/mpls/mpls_output.c:197
- sys/netproto/mpls/mpls_output.c:202
- sys/netproto/mpls/mpls_var.h:59
- sys/netproto/mpls/mpls_var.h:60
- sys/netinet/ip_output.c:695
- sys/netinet/ip_output.c:698
- sys/netinet/ip_output.c:739
- sys/netinet/ip_output.c:742
- sys/kern/uipc_mbuf.c:1500
- sys/kern/uipc_mbuf.c:2103
- sys/sys/mbuf.h:469
Detail
Exploit chain
none (blocked by INVARIANTS + mbuf_zone; same valid hard blockers as DF-0753). The primitive is a double-free/UAF of an mbuf from the dedicated mbuf_zone slab. On GENERIC (INVARIANTS ON) the slab allocator's chunk_mark_free check catches the double-free and panics before grooming lands. mbufs are confined to mbuf_zone (no cross-object grooming - victim is always another mbuf, which carries no function pointer/ucred*/uid). Realistic impact: panic/DoS. No escalation chain developed; both valid hard blockers apply. Harness file: findings/poc/DF-0754/harness.c (unpatched) + harness_fixed.c (patched).
Evidence (decisive lines)
Files in findings/poc/DF-0754/: harness.c (unpatched proof), harness_fixed.c (fixed proof), build.sh/run.sh, build.log/run.log (FULL untrimmed decisive runs), fix.diff (root-cause fix, same as DF-0753), fix_build.log (patched mpls.ko -Werror BUILD=0 sha256 72fb3810...), fix_run.log (before/after contrast), VERDICT.md, manifest.json, env.txt, README.md, Makefile. KEY: unpatched M1a double_free=1 'DOUBLE-FREE CONFIRMED', M1b double_free=2, M1c live_allocs=1 'MBUF LEAK', M2a stale-to-driver+leak, M2b uaf=1 'UAF CONFIRMED'; fixed harness ALL 6 PASS; patched mpls.ko sha256 72fb3810... (byte-identical to DF-0753's - deterministic).
PoC changes
Created findings/poc/DF-0754/ from scratch. harness.c transcribes mpls_output/mpls_push/mpls_swap/mpls_pop/mpls_output_process + the ip_output MPLS dispatch + m_prepend/m_pullup verbatim from the kernel, with a poisoned 0xdeadc0de allocator and an OOM injection knob (fail_after) to drive the m_prepend/m_pullup NULL-return paths deterministically. 6 scenarios: M1a (PUSH+m_prepend OOM double-free), M1b (SWAP+m_pullup OOM double-free), M1c (push-then-error leak), M2a (PUSH success stale-to-driver+leak), M2b (SWAP m_pullup success UAF), CONTROL (leading_space=14, no realloc). harness_fixed.c applies the struct mbuf **mp fix and shows all PASS. Also added errno/boolean_t userspace defines to compile without kernel headers, and regenerated fix.diff in proper unified format (the DF-0753 original lacked ---/+++ headers that DragonFly patch could parse).
Verified recommended fix
Change mpls_output to take struct mbuf **mp, add struct mbuf *m = *mp; local, replace all return(error) in the loop with goto out, and add out: *mp = m; return(error); before the final return (mpls_output.c:50-129). Apply the same struct mbuf ** change to mpls_swap (:171), mpls_pop (:196), and mpls_output_process (:134, using m_freem(*mp) at the error path). Update callers mpls_input.c:208 (mpls_output(&m,...)) and ip_output.c:695/:739 (mpls_output_process(&m,...)). This MATCHES the finding proposal and is identical to the DF-0753 root-cause fix - one diff closes both findings. Full git-apply-able diff in findings/poc/DF-0754/fix.diff.
Verdict
REPRODUCED. The bug is real: mpls_output() takes struct mbuf *m BY VALUE (mpls_output.c:50). When mpls_push/mpls_swap/mpls_pop reallocate the head mbuf (M_PREPEND at :157, m_pullup at :178/:202), the rebound local never escapes to the callers. MANIFESTATION 1 (mpls_output_process :143-146): error-path m_freem(stale m) at :145 double-frees (m_prepend/m_pullup OOM already freed m - M1a double_free=1, M1b double_free=2) or leaks the new pushed head (M1c, push-then-error, live_allocs=1). MANIFESTATION 2 (ip_output :695->:698/:739->:742): success-path ifp->if_output(stale m) sends a demoted old head to the driver + leaks the new head (M2a, if_output_received_label=stale old label, live_allocs=1) or dereferences freed memory (M2b, uaf=1 at if_output deref). All 5 scenarios confirmed by a deterministic harness transcribing the kernel code verbatim with a poisoned 0xdeadc0de allocator + OOM injection. CONTROL (leading_space=14) proves the bug is realloc-dependent (normal ether headroom >= max 12-byte PUSH). The finding is certain: path:line traced in sys/, both manifestations demonstrated deterministically.
No comments yet.