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:
@@ -218,9 +218,16 @@ fn rangeFor(badge: u32) ?*Range {
|
|||||||
/// Resolve a caller's transfer to an absolute LBA, or null if it falls outside
|
/// 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 caller's confinement. Unconfined callers (no range) pass through against
|
||||||
/// the whole device.
|
/// 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 {
|
fn resolveTransfer(sender: u32, lba: u64, count: u32) ?u64 {
|
||||||
const r = rangeFor(sender) orelse return lba; // unconfined: whole device
|
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;
|
return r.base + lba;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1246,6 +1246,7 @@ CASES = [
|
|||||||
"timeout": 150,
|
"timeout": 150,
|
||||||
"expect": r"(?s)(?=.*block-range: ok in-range-read)"
|
"expect": r"(?s)(?=.*block-range: ok in-range-read)"
|
||||||
r"(?=.*block-range: ok out-of-range-refused)"
|
r"(?=.*block-range: ok out-of-range-refused)"
|
||||||
|
r"(?=.*block-range: ok wrap-refused)"
|
||||||
r"(?=.*block-range: ok geometry-is-confined)"
|
r"(?=.*block-range: ok geometry-is-confined)"
|
||||||
r"(?=.*block-range: ok confined-cannot-redefine)"
|
r"(?=.*block-range: ok confined-cannot-redefine)"
|
||||||
r"(?=.*block-range: VERDICT done)",
|
r"(?=.*block-range: VERDICT done)",
|
||||||
|
|||||||
@@ -133,6 +133,12 @@ pub fn main(init: process.Init) void {
|
|||||||
// range, and must be refused.
|
// range, and must be refused.
|
||||||
verdict(device.read(0, 1, bounce.physical), "in-range-read");
|
verdict(device.read(0, 1, bounce.physical), "in-range-read");
|
||||||
verdict(!device.read(2, 1, bounce.physical), "out-of-range-refused");
|
verdict(!device.read(2, 1, bounce.physical), "out-of-range-refused");
|
||||||
|
// The wrap attack, calibrated to be exploitable against a naive bound: this
|
||||||
|
// process is confined with base 1, so a volume-relative LBA of maxInt(u64)
|
||||||
|
// makes base + lba wrap to absolute block 0 — a real, readable block OUTSIDE
|
||||||
|
// the range (the boot sector). A naive `lba + count > count` check also
|
||||||
|
// wraps to 0 and waves it through; the overflow-safe bound refuses it.
|
||||||
|
verdict(!device.read(std.math.maxInt(u64), 1, bounce.physical), "wrap-refused");
|
||||||
|
|
||||||
// Geometry now reports the CONFINED size, not the device's.
|
// Geometry now reports the CONFINED size, not the device's.
|
||||||
const confined = device.geometry() orelse {
|
const confined = device.geometry() orelse {
|
||||||
|
|||||||
Reference in New Issue
Block a user