DragonFlyBSD Kernel Audit
← triage · dashboard
DF-2476

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2476 · 9 files
FileTypeDescriptionSize
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
VERDICT.md verdict full analysis: bug confirmed in source; latent because path needs loader-preloaded md + reboot
↓ download 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 into sc->bio_queue and trip the UAF.
  • build.sh / run.sh, build.log, run.log, env.txt, fix.diff, fix_build.log, manifest.json.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

not_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).
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0 (base kernel unchanged; only md.ko rebuilt with fix)

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.