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.
Recommended fix
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)
PoC verification
Evidence pack
findings/poc/DF-2903 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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).
DF-2903 VERDICT
Status: reproduced β impact: panic (guaranteed kernel panic from one crafted MBR sector; DoS class).
How it reproduced
- 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.
vnconfig -c -v -S 512 vn0 /root/ontrack.imgon the stock INVARIANTS guest β immediate panic, serial console (panic.txt):panic: brelse: inappropriate B_PAGING or B_CLUSTERwith the exact stackbrelse β 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) callsbrelse()on thegetpbuf_mem()pbuf from :127.- 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
fixedPatched 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']
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.
No comments yet.