block: close the range-clamp overflow — a confined caller could wrap into the neighbour

The naive bound `lba + count > r.count` wraps for an lba near u64 max: the
sum overflows to a small value, sails under the check, and `base + lba`
wraps to an absolute block OUTSIDE the range. Calibrated, it is a real
confinement escape — a process confined to [1,3) reads absolute block 0
(the boot sector) with lba = maxInt(u64), since base + lba wraps to 0.

The bound is rewritten as two subtractions that cannot overflow: lba within
the range, and count within what remains. block-range gains a wrap-refused
assertion calibrated to be exploitable against the naive form — it FAILS
against the old bound (reads block 0) and passes against the fix (verified
by reverting the clamp). Caught pre-emptively before the V2 boundary review.
This commit is contained in:
Daniel Samson
2026-08-09 17:40:45 +01:00
parent 7af65697cc
commit 89d4592777
3 changed files with 15 additions and 1 deletions
+8 -1
View File
@@ -218,9 +218,16 @@ fn rangeFor(badge: u32) ?*Range {
/// Resolve a caller's transfer to an absolute LBA, or null if it falls outside
/// the caller's confinement. Unconfined callers (no range) pass through against
/// the whole device.
///
/// The bound is written to survive a hostile confined caller: `lba + count`
/// would WRAP for an `lba` near u64 max, sail under a naive `> r.count` check,
/// and translate to a wild absolute block — so the check is phrased as two
/// subtractions that cannot overflow (`lba` within the range, and `count`
/// within what remains). `r.base + lba` cannot overflow once `lba <= r.count`,
/// because the volume manager sets `base + count` inside the device.
fn resolveTransfer(sender: u32, lba: u64, count: u32) ?u64 {
const r = rangeFor(sender) orelse return lba; // unconfined: whole device
if (lba + count > r.count) return null; // past the volume's end
if (lba > r.count or r.count - lba < count) return null; // past the volume's end
return r.base + lba;
}