mdstrategy_preload processes every queued bio against a stale bio_buf (UAF / cross-request kernel memory corruption / OOB write to preload image)
Summary
mdstrategy_preload() sets local struct buf *bp exactly once from initial ap->a_bio (:349) never refreshes inside while(1) service loop. Every iteration after first reuses original bp while bio/bio_offset come from completely different bio dequeued via bioq_takefirst() (:372). Sibling mdstrategy_malloc correctly does bp=bio->bio_buf at :239 preload missing assignment. Because physio frees/returns each request bp via relpbuf right after biodone wakes it stale bp is dangling pointer once second iteration runs on SMP - UAF that also writes disk data into freed/reused buffer and can overflow preload image. Even on UP cross-contaminates two I/Os and WRITE case writes bp_A data to sc->pl_ptr+bio->bio_offset for bp->b_bcount bytes which can land past sc->pl_len. Reachable on MD_PRELOAD device (installer/PXE/embedded boots). 0640 root:operator. Impact: kernel memory corruption info leak potential code execution.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2476 · 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| poc.c | trigger-source | concurrent I/O harness; races bios into sc->bio_queue (trips stale-bp UAF on a preload md; clean no-op on malloc md0) | 4.3 KB | view raw |
| build.sh | build-script | cc -o poc poc.c -lpthread | 107 B | view raw |
| run.sh | run-script | ./poc /dev/md0 8 | 292 B | view raw |
| build.log | build-log | harness build, rc=0 | 95 B | view raw |
| run.log | run-log | harness on md0: clean, no panic; documents md0=Malloc disk so preload path not reached | 694 B | view raw |
| env.txt | environment | uname, cc version, dmesg md0=Malloc disk | 302 B | view raw |
| fix.diff | suggested-fix | add 'bp = bio->bio_buf;' in mdstrategy_preload loop (matches mdstrategy_malloc:239) | 246 B | view raw |
| fix_build.log | build-log | md.ko rebuild with fix applied, rc=0 (-Werror clean) | 6.7 KB | view raw |
| VERDICT.md | verdict | full analysis: bug confirmed in source; latent because path needs loader-preloaded md + reboot | 4.7 KB | ↓ raw |
DF-2476 — mdstrategy_preload stale bio_buf (UAF) — VERDICT
Verdict: REPRODUCED-AS-LATENT (path unreachable at runtime on guest); fix validated to compile
- status: not_reproduced (runtime trigger: path is dead code on this guest)
- impact: none at runtime (latent UAF / cross-request kernel memory corruption)
- confidence: certain (the source defect is unambiguous; the runtime reachability limit is real)
The bug (confirmed in source, line-by-line)
sys/dev/disk/md/md.c mdstrategy_preload() (:345–400) caches the local
struct buf *bp exactly once from the initial ap->a_bio and never
refreshes it inside the while(1) service loop:
- :349
struct buf *bp = bio->bio_buf;← set ONCE - :372
bio = bioq_takefirst(&sc->bio_queue);← a different bio each iteration - :379
switch (bp->b_cmd) { … }← STALE bp - :383
bcopy(sc->pl_ptr + bio->bio_offset, bp->b_data, bp->b_bcount);← STALE bp - :395
biodone(bio);
The sibling mdstrategy_malloc() does it correctly at :239
(bp = bio->bio_buf; inside the loop). When ≥2 bios accumulate in
sc->bio_queue (concurrent I/O arriving while sc->busy is set), every
iteration after the first dereferences the first request's bp after
biodone() was already called on it. physio may have returned/freed that
buf → use-after-free. Even before the free lands, the I/O is
mis-targeted: iteration-2's bio_offset is used together with iteration-1's
b_data/b_bcount/b_cmd → cross-request data corruption / wrong-buffer
I/O into the preload image.
Why it does not panic on this guest (reachability)
mdcreate_preload() (:437) is invoked only from md_drvinit() (:501) at
module-load/boot time, consuming loader-preloaded images of type
md_image/mfs_root (preload_search_next_name, :513). There is no
runtime ioctl that creates a preload md: mdioctl() (:162) is a stub
returning ENOIOCTL, and mdconfig is not even installed on the guest.
Consequently an MD_PRELOAD device exists only when the boot loader
preloaded an image. On the audit guest nothing is preloaded — dmesg shows
md0: Malloc disk (from mdcreate_malloc, :530) — so mdstrategy
(:191 if (sc->type == MD_MALLOC) … else mdstrategy_preload) always takes
the correct malloc branch and never enters the buggy preload path.
The PoC harness (poc.c) spawns 8 concurrent I/O threads on /dev/md0.
Run as root (md0 is root:operator):
[*] spawning 8 concurrent I/O threads on /dev/md0 [+] all threads completed; device path returned cleanly RUN_EXIT=0 (guest still up — no panic)
This is the expected result on a malloc md: the racing bios are drained by
mdstrategy_malloc, which refreshes bp per iteration. It demonstrates the
path mismatch. To actually trip the UAF one would have to: (a) be root,
(b) reboot with a loader-preloaded md image (so a MD_PRELOAD device
exists), and (c) drive concurrent I/O at it. That is a root→kernel
path with no unprivileged→root escalation — a valid hard blocker for an
uid=0 claim, and the runtime trigger is itself unavailable on the guest.
Exploit chain
None applicable: the primitive is memory corruption (a UAF / OOB-write into the preload image), but the path is reachable only from an already-root context (loader preload + reboot) and is dead code at runtime on this guest. There is no privilege boundary to cross, so there is no unpriv→root chain to develop. This is a latent kernel memory-corruption defect that would become live on a system that boots from a preloaded md/mfs image (e.g. an installer or an mdroot appliance) under concurrent I/O.
Fix
fix.diff adds the single missing refresh, matching the correct
mdstrategy_malloc:239:
@@ -373,6 +373,7 @@
crit_exit();
if (bio == NULL)
break;
+ bp = bio->bio_buf;
Fix validation
fix.diff applies (Hunk #1 succeeded at 373) and compiles cleanly
into md.ko (make in sys/dev/disk/md, rc=0, -Werror clean — see
fix_build.log). Runtime before/after is not_testable: the buggy path
cannot be exercised on the guest because no preload md device exists and
creating one requires a boot-loader-preloaded image + reboot. The fix is
trivially correct by inspection (it makes the preload loop identical in
this respect to the already-correct malloc loop).
PoC changes / artifacts
poc.c— concurrent I/O harness; on md0 (malloc) it is a clean no-op that demonstrates the preload path is not in use. On a real preload md under concurrent I/O it would race bios intosc->bio_queueand trip the UAF.build.sh/run.sh,build.log,run.log,env.txt,fix.diff,fix_build.log,manifest.json.
Fix verification
not_testablenot_testable at runtime: fix.diff APPLIES ('Hunk #1 succeeded at 373') and COMPILES cleanly into md.ko (make in sys/dev/disk/md, rc=0, -Werror clean). Runtime before/after cannot be exercised because buggy mdstrategy_preload path is dead code on this guest (no MD_PRELOAD device exists; md0 is MALLOC; mdioctl is stub and mdconfig absent). Creating preload md requires boot-loader-preloaded image + reboot, a root->kernel path. One-line fix trivially correct by inspection (makes preload loop identical in this respect to already-correct mdstrategy_malloc loop).
fix.diff applies: 'Hunk #1 succeeded at 373.' / md.ko rebuild: 'cc ... -c md.c' + 'cc -Wl,... -r -o md.ko md.o' / '=== MD_BUILD_DONE rc=0 ===' / new line present at md.c:376: 'bp = bio->bio_buf;' / runtime not testable (md0 = Malloc disk, preload path unreachable).
Confirmed kernel references
Detail
Exploit chain
none — primitive is memory corruption (stale-bp UAF / OOB-write into preload image), but path reachable only from already-root context (boot-loader-preloaded md image + reboot) and is dead code at runtime on this guest. Valid hard blocker: no unprivileged->root boundary to cross (root->kernel), and runtime trigger itself unavailable. Latent kernel memory-corruption defect that would become live on system booting from preloaded md/mfs image (installer / mdroot appliance) under concurrent I/O. Fix is trivial single-line refresh (bp = bio->bio_buf) making preload loop identical in this respect to correct malloc loop.
Evidence (decisive lines)
[*] spawning 8 concurrent I/O threads on /dev/md0 / [+] all threads completed; device path returned cleanly / RUN_EXIT=0 / --- guest status after run: up (no panic) --- / NOTE: /dev/md0 is 'Malloc disk' so mdstrategy routes to mdstrategy_malloc (correct, refreshes bp at :239) and NEVER enters mdstrategy_preload (buggy, :345). No preload md exists; creation needs loader preload + reboot. === fix validation: md.ko rebuilt with fix.diff applied === Hunk #1 succeeded at 373. === MD_BUILD_DONE rc=0 === (-Werror clean)
PoC changes
Wrote poc.c (concurrent I/O harness; races bios into sc->bio_queue — would trip stale-bp UAF on preload md; clean no-op on malloc md0 demonstrating path mismatch), build.sh, run.sh, VERDICT.md, fix.diff (adds 'bp = bio->bio_buf;' after bioq_takefirst NULL-break in mdstrategy_preload, matching mdstrategy_malloc:239), manifest.json. fix.diff validated: applies cleanly (hunk #1 @373) and compiles into md.ko (rc=0, -Werror clean).
Verified recommended fix
In sys/dev/disk/md/md.c mdstrategy_preload, add 'bp = bio->bio_buf;' immediately after 'if (bio == NULL) break;' inside while(1) loop (between dequeue and devstat_start_transaction), exactly mirroring correct mdstrategy_malloc at :239. Refreshes bp per-iteration so each request's buf matches its bio, eliminating both UAF (bp dereferenced after biodone on previous bio) and cross-request data corruption. Matches finding proposal (minimal correct fix). Full git-apply-able diff in findings/poc/DF-2476/fix.diff.
Verdict
NOT REPRODUCED at runtime (LATENT). The source defect is real and confirmed line-by-line: mdstrategy_preload (sys/dev/disk/md/md.c:345-400) caches struct buf *bp exactly once at :349 from ap->a_bio and never refreshes it inside the while(1) service loop, while bio is re-dequeued each iteration via bioq_takefirst at :372; every iteration >=2 dereferences the first request's bp at :379/:383/:395 after biodone() has already been called on it -> use-after-free / cross-request mis-targeted I/O. Sibling mdstrategy_malloc does it correctly at :239 (bp = bio->bio_buf inside loop). HOWEVER the path is dead code at runtime on this guest: mdcreate_preload (:437) called only from md_drvinit (:501) at boot, consuming loader-preloaded images of type md_image/mfs_root; mdioctl (:162) is a stub returning ENOIOCTL and mdconfig is not installed, so no runtime way to create a preload md. dmesg confirms md0 is 'Malloc disk' (mdcreate_malloc), so mdstrategy always takes the correct malloc branch. Concurrent-I/O harness (poc.c, 8 threads on md0) completes cleanly with no panic, demonstrating preload path is not exercised.
No comments yet.