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

Ontrack (DOSPTYP_ONTRACK 0x54) re-read path releases a getpbuf_mem() pbuf via brelse() β€” guaranteed kernel panic from one crafted MBR sector (INVARIANTS) / cross-subsystem pbuf corruption + leak and latent infinite re-read wedge holding ds_token (production)

Field Value
ID DF-2903
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:P/AC:L/PR:N/UI:N/S:C/C:N/I:N/A:H
CWE CWE-667 / CWE-755 / CWE-672
File sys/kern/subr_diskmbr.c
Lines 174-182 (pbuf from :127; done path relpbuf at :316)
Area kern/disk
Confidence certain
Discovered 2026-09-02
Pass 2 (GLM 5.3 second pass)
Bucket base:kern
Reported pending
Known CVE none
CVE match novel

Summary

mbrinit()'s Ontrack branch does brelse(bp) on a buffer obtained from getpbuf_mem() β€” a B_PAGING pbuf that must be released with relpbuf(). On INVARIANTS kernels brelse()'s KASSERT (vfs_bio.c:1275) panics immediately: a single MBR entry of type 0x54 plus a valid 0x55AA magic panics the kernel at disk-probe time (same probe runs unattended as root at USB plug-in and at boot). On production kernels the KASSERT is gone and the pbuf is pushed into the buffer-cache free machinery while the pbuf pool still owns it β€” cross-subsystem buffer lifecycle corruption plus one pbuf leaked out of bswlist_mem per Ontrack attach (replug exhaustion wedges getpbuf_mem = all paging and probe I/O). The branch also contains a latent unbounded loop: with the release corrected, a second Ontrack entry at LBA 63 makes mbrinit re-read forever inside disk_msg_core, which holds ds_token β€” every subsequent open of any disk device system-wide then blocks forever. Lineage: the FreeBSD original used geteblk() where brelse is correct; DragonFly's getpbuf_mem conversion fixed the done: path but missed this branch.

Proof of contest

VERIFIED (findings/poc/DF-2903/): 32 KB image with MBR magic + entry[0].type=0x54, vnconfig -c vn0 ontrack.img β†’ instant panic: brelse: inappropriate B_PAGING or B_CLUSTER bp, stack brelse ← mbrinit ← disk_probe ← disk_msg_core, guest catatonic. Fix (relpbuf + one-shot re-read) validated on in-guest rebuilt kernel: identical image attaches cleanly, /dev/vn0s1 created, guest survives.

relpbuf() instead of brelse() + make the Ontrack re-read one-shot β€” validated fix.diff in findings/poc/DF-2903/.

Timeline

  • 2026-09-02 Discovered during pass-2 audit of subr_diskmbr.c (GLM 5.3); reproduced + fix validated same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2903 Β· 10 files
FileTypeDescriptionSize
README.md β€” 2.6 KB ↓ raw
VERDICT.md β€” 1.9 KB ↓ raw
mkontrack.py β€” 613 B view raw
ontrack.img β€” 32.0 KB ↓ download
run.sh β€” 447 B view raw
run.log β€” 885 B view raw
run_fix.log β€” 507 B view raw
panic.txt β€” 517 B view raw
env.txt β€” 640 B view raw
fix.diff β€” 1.2 KB view raw

DF-2903 β€” Ontrack branch releases a pbuf via brelse() β†’ panic from crafted MBR

What

mbrinit()'s "Ontrack Disk Manager" path (sys/kern/subr_diskmbr.c:174-182) does:

bp->b_flags |= B_INVAL | B_AGE;
brelse(bp);              /* bp is a getpbuf_mem() PBUF β€” must be relpbuf()! */
mbr_offset = 63;
goto reread_mbr;

bp comes from getpbuf_mem() (subr_diskmbr.c:127) β€” a paging buffer with B_PAGING set (vm/vm_pager.c:387). Releasing it through brelse() is a buffer-subsystem violation: on INVARIANTS kernels brelse()'s KASSERT(!(bp->b_flags & (B_CLUSTER|B_PAGING))) (vfs_bio.c:1275) panics immediately; on production (non-INVARIANTS) kernels the pbuf is handed to the buffer-cache free machinery while the pbuf subsystem still owns it β€” cross-subsystem buffer lifecycle corruption plus one pbuf leaked out of bswlist_mem per attach (eventual getpbuf_mem() exhaustion wedging all paging/probe I/O).

History: the FreeBSD original used geteblk() (a normal buffer, where brelse is correct). DragonFly converted the reads to getpbuf_mem() and fixed the done: path to relpbuf() (subr_diskmbr.c:316) but missed the Ontrack branch.

Observed (stock INVARIANTS guest)

A single MBR partition-table entry of type 0x54 (DOSPTYP_ONTRACK) plus a valid 0x55AA magic panics the kernel at disk-probe time:

panic: brelse: inappropriate B_PAGING or B_CLUSTER bp 0xfffff8004f3df690
brelse() at brelse+0x7f2
mbrinit() at mbrinit+0x1ae
disk_probe() at disk_probe+0x85
disk_msg_core() at disk_msg_core+0x252

(panic.txt β€” reproduced 2026-09-03, trigger: vnconfig -c vn0 ontrack.img.)

Secondary latent bug (same branch): once the release is fixed to relpbuf(), the goto reread_mbr loop is unbounded β€” an Ontrack entry also present at LBA 63 (offset 63*512) makes mbrinit re-read forever, wedging disk_msg_core which holds ds_token (subr_disk.c:512) β†’ every subsequent open of any disk device in the system blocks forever. The fix therefore also makes the re-read one-shot (mbr_offset == DOSBBSECTOR).

Threat

Runs with kernel privilege at disk attach: USB plug-in auto-probe (devd/CAM), boot-time probe of an Ontrack-formatted disk, or root vnconfig of a crafted image. One crafted sector = guaranteed panic (INVARIANTS) / buffer-subsystem corruption (production).

Reproduce (guest, root)

python3 mkontrack.py                    # or use the shipped ontrack.img
vnconfig -c -v -S 512 vn0 ontrack.img   # stock kernel: instant panic (panic.txt)

Success criterion (stock): panic above; after fix.diff: attach completes cleanly, /dev/vn0s1 appears, no panic (run_fix.log).

VERDICT.md
↓ download raw

DF-2903 VERDICT

Status: reproduced β€” impact: panic (guaranteed kernel panic from one crafted MBR sector; DoS class).

How it reproduced

  1. 32 KB image: MBR magic 0x55AA at offset 510, partition entry[0] type 0x54 (DOSPTYP_ONTRACK) at offset 450 (mkontrack.py). Same table also placed at LBA 63 for the re-read leg.
  2. vnconfig -c -v -S 512 vn0 /root/ontrack.img on the stock INVARIANTS guest β†’ immediate panic, serial console (panic.txt): panic: brelse: inappropriate B_PAGING or B_CLUSTER with the exact stack brelse ← mbrinit ← disk_probe ← disk_msg_core. The synchronous probe (disk_setdiskinfo_sync, vn.c:584/869) runs mbrinit in the disk core thread; the Ontrack branch (subr_diskmbr.c:178-179) calls brelse() on the getpbuf_mem() pbuf from :127.
  3. Post-panic the guest was catatonic (vm.sh status β†’ down; new ssh connections timed out during banner exchange).

Reachability

  • USB mass-storage plug-in: CAM auto-probes as root β€” no user action needed beyond physical insertion of crafted media.
  • Boot with an Ontrack-managed disk (0x54 was a real-world Disk Manager convention, still honored here).
  • Root vnconfig (used for the PoC).

Non-INVARIANTS behavior (reasoned, not run)

KASSERT compiles out; brelse() then inserts the B_PAGING pbuf into buffer-cache queues (vfs_bio.c:1268+): one pbuf leaked from bswlist_mem per Ontrack attach (cross-subsystem lifecycle corruption; replug loop β†’ getpbuf_mem() exhaustion β†’ permanent paging/probe wedge). The primary, demonstrated impact is the INVARIANTS panic.

Fix validation (this run)

fix.diff hunk 2: relpbuf(bp, NULL) + one-shot re-read guard (mbr_offset == DOSBBSECTOR). Patched kernel (built in-guest with make nativekernel): vnconfig of ontrack.img completes cleanly, slices are created, guest stays up (run_fix.log). Baseline panic vs patched clean-attach recorded side by side in this pack.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Patched kernel (same fix.diff build as DF-2902) attaches the identical ontrack.img cleanly; baseline panic gone (run_fix.log).

['run_fix.log', 'env.txt']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Thu Sep 3 05:01:17 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

crafted MBR (one 0x54 entry + valid magic) on any attached media -> kernel probe at attach (USB plug-in, boot, or root vnconfig) -> mbrinit Ontrack branch -> brelse(pbuf) -> panic (INVARIANTS) or pbuf/buf-cache cross-corruption + leak (production); with the release fixed, unbounded re-read loop wedges disk_msg_core + ds_token system-wide

Evidence (decisive lines)

['panic.txt: exact panic + stack from the stock kernel', 'run_fix.log: same-md5 image on patched kernel -> clean attach, /dev/vn0s1, no panic', 'VERDICT.md: lineage (FreeBSD geteblk -> DFly getpbuf_mem conversion missed this branch) and ds_token wedge analysis']

PoC changes

No seed; authored mkontrack.py (1 sector MBR, entry0 type 0x54, magic 0x55AA; duplicated at LBA63 to also cover the re-read leg of the latent loop).

Verified recommended fix

Release the Ontrack-branch pbuf with relpbuf() and only take the mbr_offset=63 re-read once (mbr_offset == DOSBBSECTOR guard)

Verdict

mbrinit()'s Ontrack (DOSPTYP_ONTRACK 0x54) re-read path releases the getpbuf_mem() pbuf via brelse() (subr_diskmbr.c:178-179) instead of relpbuf(). On INVARIANTS kernels brelse()'s KASSERT panics instantly (reproduced: single 0x54 entry + 0x55AA magic in the MBR -> panic 'brelse: inappropriate B_PAGING or B_CLUSTER', stack brelse <- mbrinit <- disk_probe <- disk_msg_core, from a root vnconfig attach; the same probe runs unattended as root at USB plug-in / boot). On production kernels the KASSERT is compiled out and the B_PAGING pbuf is pushed into the buffer-cache free machinery β€” cross-subsystem buffer lifecycle corruption plus a pbuf leaked out of bswlist_mem per Ontrack attach. Latent second bug in the same branch: once the release is corrected, the goto reread_mbr loop is unbounded β€” an Ontrack entry also at LBA 63 makes mbrinit re-read forever inside disk_msg_core, which holds ds_token, permanently blocking every future open of any disk device. Fix (fix.diff): relpbuf + one-shot re-read guard; validated on an in-guest rebuilt kernel β€” same image attaches cleanly, /dev/vn0s1 created, no panic.