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

lockrange() commits pos one past plex->lock[PLEX_LOCKS] when the table fills while a conflicted requester sleeps (16-byte heap OOB write + usedlocks overrun defeating the == fullness checks)

Summary

The conflicted requester tsleeps (dropping the MP token), other CPUs consume all 256 slots, the sleeper rescans with no fullness recheck and the unconditional pos=lock commits one-past-the-end - a 16-byte OOB write (attacker-influenced stripe + kernel bp) past the 4096-byte table, repeated forever after first hit by the usedlocks overrun. Equality-only fullness + interrupt-context usedlocks RMW are the enabling defects. Unprivileged via any fs on a vinum RAID-5 volume; race-gated (needs >=256 in-flight groups at a wake boundary - not reached in ~11M guest ops on fast vn backing, but routine on real-latency arrays). == to >= + restart recheck + end-of-table guard + crit fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5108 Β· 15 files
FileTypeDescriptionSize
lockrange_race.c β€” 2.6 KB view raw
vcreate.c β€” 2.3 KB view raw
setup.sh β€” 847 B view raw
ulmon.c β€” 1.6 KB view raw
cfg_raid5.bin β€” 185 B ↓ download
cfg_raid5_pad.bin β€” 512 B ↓ download
lbl_x0.bin β€” 512 B ↓ download
lbl_x1.bin β€” 512 B ↓ download
lbl_x2.bin β€” 512 B ↓ download
lbl_d0.bin β€” 512 B ↓ download
run.log β€” 1.7 KB view raw
fix.diff β€” 3.8 KB view raw
VERDICT.md β€” 4.3 KB ↓ raw
manifest.json β€” 1.6 KB view raw
verdict.json β€” 2.8 KB view raw
VERDICT.md
↓ download raw

DF-5108 β€” VERDICT

Status: not_reproduced (race-gated heap OOB write; code path certain, race window not reached within guest budget). Impact class: memcorrupt.

Defect (static, vinumlock.c)

The rangelock allocator can commit pos = one-past-the-end of the 256-entry lock table:

  1. :139 while (plex->usedlocks == PLEX_LOCKS) β€” equality only. Any usedlocks > PLEX_LOCKS (produced by the bug itself, or by the unprotected usedlocks++/-- RMW pair across CPUs β€” :190 vs :219 in interrupt-context unlockrange) permanently defeats the wait AND the full-table wakeup at :220 (waiters then sleep in tsleep("vlock") forever, no PCATCH).
  2. The conflict path :155-171: a requester that finds its stripe held by another request tsleeps on that lock entry. tsleep drops the MP token (vinum is non-MPSAFE β€” kern_device.c:121 wraps every entry point with get_mplock), so while it sleeps, other CPUs' requests consume free slots. unlockrange frees the conflicting entry and wakes it, but a third request can retake that entry before the sleeper is scheduled.
  3. The sleeper then rescans (:168-170, no fullness recheck) with all 256 slots in use: the scan runs to lock == &plex->lock[PLEX_LOCKS], pos stays NULL, and :181-182 sets pos = lock β€” one past the end.
  4. :188-190 writes pos->stripe (attacker-influenced 64-bit disk offset) and pos->bp (kernel pointer) 16 bytes past the kmalloc(PLEX_LOCKS * sizeof(struct rangelock)) = 4096-byte object (vinumconfig.c:1467), and increments usedlocks past 256 β€” feeding (1): every subsequent lockrange on this plex repeats the OOB write.

Reachability: rangelocks are acquired only for RAID-5 plex request groups (vinumraid5.c:592 rqg->lockbase = m.stripebase β†’ vinumrequest.c:396-397 inside launch_requests' crit section β€” whose own comment admits the bottom half completes concurrently). Any local user generating concurrent I/O on a vinum-backed filesystem reaches lockrange; the race needs the table exactly full at a wake boundary.

What was attempted (guest, 6 vCPU)

  • Built a live RAID-5 volume (3 Γ— 100 MB vn-backed drives, 32 KB stripes, forced up) and hammered /dev/vinum/vol/rv with the included lockrange_race (pwrite from N threads at random offsets from a P-stripe pool to force same-stripe conflicts + near-full table).
  • Three completed runs (see run.log): 400Γ—32KΓ—300 stripes Γ— 300 s; 800Γ—32KΓ—270 Γ— 300 s; 300Γ—128KΓ—258 Γ— 300 s β€” β‰ˆ11 M write ops, 0 errors, no panic, no "vlock" wedge, dmesg clean.
  • Why it did not fire: aggregate throughput Γ— service time implies ~80-160 concurrently-held slots; the table never reached 255+1 at a wake boundary. The vn-on-virtio backing completes too quickly to accumulate β‰₯256 in-flight request groups; the MP token serializes submissions further. A production array with real disk latency and deep queues (or a slower backing device) reaches fullness routinely β€” that is exactly the regime the striped-lock design was written for.
  • Instrumentation attempt (ulmon.c) to read plex->usedlocks via VINUM_PLEXCONFIG failed on the ioctl size-encoding probe; not pursued further within budget.

Conclusion

Not reproduced in budget β€” the interleaving is real (every ingredient is individually present: token-dropping sleeps, interrupt-context decrements, equality checks, the unconditional pos = lock) but requires β‰₯256 in-flight RAID-5 groups at a wake boundary, which this guest's I/O stack does not sustain. The finding stands as a race-gated heap OOB write with permanent repeat-after-first-hit semantics (usedlocks overrun). Severity Medium, confidence likely.

Fix

fix.diff (same file as DF-5109's fix): ==β†’>= both fullness checks, post-tsleep goto restart recheck, defensive end-of-table guard before commit, crit_enter/crit_exit around unlockrange's update. Validated to build and boot (see DF-5109 fix validation); no behavioral regression in the hammer runs' I/O path (runs completed identically on the patched kernel during validation).

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

fix.diff builds and boots (validated on the DF-5109 rebuilt kernel); no behavioral regression in the hammer I/O path. Baseline race not reproduced in budget so no patched-vs-baseline behavioral delta was capturable.

fix.diff (git-style); rebuild/boot validation recorded in findings/poc/DF-5109/VERDICT.md fix-validation section
↓ fix.diffper-fix-DF-5108

Confirmed kernel references

Detail

Exploit chain

concurrent conflicting + distinct-stripe RAID-5 I/O with >=256 in-flight request groups on one plex -> pos = &lock[PLEX_LOCKS] (one-past-end) -> 16-byte OOB write (attacker-influenced 64-bit stripe + kernel bp pointer) past the 4096-byte lock table + usedlocks overrun defeating the == fullness checks permanently (repeat-after-first-hit). Unprivileged via any filesystem on a vinum volume.

Evidence (decisive lines)

run.log (3 hammer runs, 0 errors - honest negative), VERDICT.md full static chain (token-dropping tsleep :168, interrupt-context usedlocks RMW :190 vs :219, equality-only fullness :139, unconditional pos=lock :181-182, commit write :188-190), fix.diff

PoC changes

initial sketch used direct lockrange calls; final trigger is real RAID-5 I/O (3x vn-backed drives, 32KB stripes, P-stripe pool for same-stripe conflicts) via pwrite from N threads

Verified recommended fix

fix.diff: == to >= both fullness checks, post-tsleep goto restart recheck, end-of-table defensive guard before commit, crit around unlockrange's update

Verdict

not_reproduced (race-gated heap OOB write; code path certain, >=256-in-flight-groups race not reached within guest budget - ~11M ops over 3 completed 300s runs; vn-on-virtio backing completes too fast)