diff --git a/system/drivers/usb-storage/usb-storage.zig b/system/drivers/usb-storage/usb-storage.zig index 99c1b20..1351378 100644 --- a/system/drivers/usb-storage/usb-storage.zig +++ b/system/drivers/usb-storage/usb-storage.zig @@ -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; } diff --git a/test/qemu_test.py b/test/qemu_test.py index 4754310..6e91485 100644 --- a/test/qemu_test.py +++ b/test/qemu_test.py @@ -1246,6 +1246,7 @@ CASES = [ "timeout": 150, "expect": r"(?s)(?=.*block-range: ok in-range-read)" 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 confined-cannot-redefine)" r"(?=.*block-range: VERDICT done)", diff --git a/test/system/services/block-range-test/block-range-test.zig b/test/system/services/block-range-test/block-range-test.zig index f5ab054..585cd8a 100644 --- a/test/system/services/block-range-test/block-range-test.zig +++ b/test/system/services/block-range-test/block-range-test.zig @@ -133,6 +133,12 @@ pub fn main(init: process.Init) void { // range, and must be refused. verdict(device.read(0, 1, bounce.physical), "in-range-read"); 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. const confined = device.geometry() orelse {