Add runtime slice bounds checks

c2b370327662856f92ea20fbf12359ab5ed8ddddbbbff4af63b9697fa95cf0f6
Alexis Sellier committed ago 1 parent b168b3e6
lib/std/lang/lower.rad +55 -2
205 205
/// Maximum number of `catch` clauses per `try`.
206 206
constant MAX_CATCH_CLAUSES: u32 = 32;
207 207
208 208
// Slice Layout
209 209
//
210 -
// A slice is a fat pointer consisting of a data pointer and length.
211 -
// `{ ptr: u32, len: u32 }`.
210 +
// A slice is a fat pointer consisting of a data pointer, a length and a capacity.
211 +
// `{ ptr: u32, len: u32, cap: u32 }`.
212 212
213 213
/// Slice data pointer offset.
214 214
constant SLICE_PTR_OFFSET: i32 = 0;
215 215
/// Offset of slice length in slice data structure.
216 216
constant SLICE_LEN_OFFSET: i32 = resolver::PTR_SIZE as i32;
2089 2089
    emit(self, il::Instr::Unreachable);
2090 2090
2091 2091
    try switchToAndSeal(self, passBlock);
2092 2092
}
2093 2093
2094 +
/// Return whether two IL values are known `u32` immediates satisfying `a <= b`.
2095 +
fn isLtEq(a: il::Val, b: il::Val) -> bool {
2096 +
    if a == b {
2097 +
        return true;
2098 +
    }
2099 +
    if a == il::Val::Imm(0) {
2100 +
        return true;
2101 +
    }
2102 +
    let case il::Val::Imm(aa) = a else {
2103 +
        return false;
2104 +
    };
2105 +
    let case il::Val::Imm(bb) = b else {
2106 +
        return false;
2107 +
    };
2108 +
    if aa < 0 or bb < 0 {
2109 +
        return false;
2110 +
    }
2111 +
    return aa <= bb;
2112 +
}
2113 +
2114 +
/// Emit an `ebreak` when `a < b` holds.
2115 +
fn emitTrapIfLt(
2116 +
    self: *mut FnLowerer,
2117 +
    typ: il::Type,
2118 +
    a: il::Val,
2119 +
    b: il::Val
2120 +
) throws (LowerError) {
2121 +
    let trapBlock = try createBlock(self, "guard#trap");
2122 +
    let passBlock = try createBlock(self, "guard#pass");
2123 +
2124 +
    try emitBrCmp(self, il::CmpOp::Ult, typ, a, b, trapBlock, passBlock);
2125 +
    try switchToAndSeal(self, trapBlock);
2126 +
2127 +
    emit(self, il::Instr::Ebreak);
2128 +
    emit(self, il::Instr::Unreachable);
2129 +
2130 +
    try switchToAndSeal(self, passBlock);
2131 +
}
2132 +
2094 2133
/// Emit a conditional branch. Uses fused compare-and-branch for simple scalar
2095 2134
/// comparisons, falls back to separate comparison plus branch otherwise.
2096 2135
fn emitCondBranch(
2097 2136
    self: *mut FnLowerer,
2098 2137
    cond: *ast::Node,
4770 4809
    let mut endVal = containerLen;
4771 4810
    if let end = range.end {
4772 4811
        set endVal = try lowerExpr(self, end);
4773 4812
    }
4774 4813
4814 +
    // Runtime slice bounds checks for dynamic range expressions.
4815 +
    if not isLtEq(startVal, containerLen) {
4816 +
        try emitTrapIfLt(self, il::Type::W32, containerLen, startVal);
4817 +
    }
4818 +
    if not isLtEq(endVal, containerLen) {
4819 +
        try emitTrapIfLt(self, il::Type::W32, containerLen, endVal);
4820 +
    }
4821 +
    if startVal <> il::Val::Imm(0) and endVal <> containerLen and not isLtEq(startVal, endVal) {
4822 +
        try emitTrapIfLt(self, il::Type::W32, endVal, startVal);
4823 +
    }
4824 +
4775 4825
    // If the start value is known to be zero, the count is just the end
4776 4826
    // value. Otherwise, we have to compute it.
4777 4827
    let mut count = endVal;
4778 4828
4779 4829
    // Only compute range offset and count if the start value is not
5984 6034
    let lenVal = try lowerExpr(self, args[1]);
5985 6035
    let mut capVal = lenVal;
5986 6036
    if args.len == 3 {
5987 6037
        set capVal = try lowerExpr(self, args[2]);
5988 6038
    }
6039 +
    if not isLtEq(lenVal, capVal) {
6040 +
        try emitTrapIfLt(self, il::Type::W32, capVal, lenVal);
6041 +
    }
5989 6042
    return try buildSliceValue(self, item, mutable, ptrVal, lenVal, capVal);
5990 6043
}
5991 6044
5992 6045
/// Lower a `try` expression.
5993 6046
fn lowerTry(self: *mut FnLowerer, node: *ast::Node, t: ast::Try) -> il::Val throws (LowerError) {
lib/std/lang/resolver.rad +1 -1
5176 5176
    if let endNode = range.end {
5177 5177
        if let val = constSliceIndex(self, endNode) {
5178 5178
            set endVal = val;
5179 5179
        }
5180 5180
    }
5181 -
    if let val = startVal; val >= length {
5181 +
    if let val = startVal; val > length {
5182 5182
        throw emitError(self, site, ErrorKind::SliceRangeOutOfBounds);
5183 5183
    }
5184 5184
    if let val = endVal; val > length {
5185 5185
        throw emitError(self, site, ErrorKind::SliceRangeOutOfBounds);
5186 5186
    }
lib/std/lang/resolver/tests.rad +1 -1
966 966
        let program = "let xs: [u8; 2] = [1, 2]; let slice = &xs[..3];";
967 967
        let result = try resolveProgramStr(&mut a, program);
968 968
        try expectErrorKind(&result, super::ErrorKind::SliceRangeOutOfBounds);
969 969
    } {
970 970
        let mut a = testResolver();
971 -
        let program = "let xs: [u8; 2] = [1, 2]; let slice = &xs[2..];";
971 +
        let program = "let xs: [u8; 2] = [1, 2]; let slice = &xs[3..];";
972 972
        let result = try resolveProgramStr(&mut a, program);
973 973
        try expectErrorKind(&result, super::ErrorKind::SliceRangeOutOfBounds);
974 974
    } {
975 975
        let mut a = testResolver();
976 976
        let program = "let xs: [u8; 4] = [1, 2, 3, 4]; let slice = &xs[3..2];";
test/tests/array.slice.openend.ril +5 -0
1 1
fn w64 $sliceOpenEnd(w64 %0, w64 %1, w32 %2) {
2 2
  @entry0
3 +
    br.ult w32 4 %2 @guard#trap1 @guard#pass2;
4 +
  @guard#trap1
5 +
    ebreak;
6 +
    unreachable;
7 +
  @guard#pass2
3 8
    mul w64 %3 %2 4;
4 9
    add w64 %4 %1 %3;
5 10
    sub w32 %5 4 %2;
6 11
    reserve %6 16 8;
7 12
    store w64 %4 %6 0;
test/tests/array.slice.openstart.ril +5 -0
1 1
fn w64 $sliceOpenStart(w64 %0, w64 %1, w32 %2) {
2 2
  @entry0
3 +
    br.ult w32 4 %2 @guard#trap1 @guard#pass2;
4 +
  @guard#trap1
5 +
    ebreak;
6 +
    unreachable;
7 +
  @guard#pass2
3 8
    reserve %3 16 8;
4 9
    store w64 %1 %3 0;
5 10
    store w32 %2 %3 8;
6 11
    store w32 %2 %3 12;
7 12
    blit %0 %3 16;
test/tests/builtin.sliceof.invalid.cap.rad added +10 -0
1 +
//! returns: 133
2 +
//! Test @sliceOf runtime validation for len > cap.
3 +
4 +
@default fn main() -> i32 {
5 +
    let mut arr: [i32; 4] = [10, 20, 30, 40];
6 +
    let ptr: *i32 = &arr[0];
7 +
    let slice: *[i32] = @sliceOf(ptr, 4, 3);
8 +
9 +
    return slice.len as i32;
10 +
}
test/tests/slice.basic.ril +5 -0
34 34
    ret %0;
35 35
}
36 36
37 37
fn w64 $sliceOfWithCap(w64 %0, w64 %1, w32 %2, w32 %3) {
38 38
  @entry0
39 +
    br.ult w32 %3 %2 @guard#trap1 @guard#pass2;
40 +
  @guard#trap1
41 +
    ebreak;
42 +
    unreachable;
43 +
  @guard#pass2
39 44
    reserve %4 16 8;
40 45
    store w64 %1 %4 0;
41 46
    store w32 %2 %4 8;
42 47
    store w32 %3 %4 12;
43 48
    blit %0 %4 16;
test/tests/slice.empty.suffix.rad added +30 -0
1 +
//! returns: 0
2 +
3 +
fn checkArraySuffix() {
4 +
    let xs: [u8; 3] = [10, 20, 30];
5 +
    let empty = &xs[xs.len..xs.len];
6 +
7 +
    assert empty.len == 0;
8 +
}
9 +
10 +
fn checkSliceSuffix() {
11 +
    let xs: [u8; 3] = [10, 20, 30];
12 +
    let slice: *[u8] = &xs[..];
13 +
    let empty = &slice[slice.len..slice.len];
14 +
15 +
    assert empty.len == 0;
16 +
}
17 +
18 +
fn checkStringSuffix() {
19 +
    let text = "abc";
20 +
    let empty = &text[text.len..text.len];
21 +
22 +
    assert empty.len == 0;
23 +
}
24 +
25 +
@default fn main() -> i32 {
26 +
    checkArraySuffix();
27 +
    checkSliceSuffix();
28 +
    checkStringSuffix();
29 +
    return 0;
30 +
}
test/tests/slice.range.bounds.check.rad added +14 -0
1 +
//! returns: 133
2 +
//! Test dynamic slice range bounds checking with runtime EBREAK.
3 +
4 +
fn id(n: u32) -> u32 {
5 +
    return n;
6 +
}
7 +
8 +
@default fn main() -> i32 {
9 +
    let xs: [i32; 3] = [1, 2, 3];
10 +
    let slice: *[i32] = &xs[..];
11 +
    let bad = &slice[id(4)..id(4)];
12 +
13 +
    return bad.len as i32;
14 +
}
test/tests/slice.range.dynamic.rad added +42 -0
1 +
//! returns: 0
2 +
3 +
fn id(n: u32) -> u32 {
4 +
    return n;
5 +
}
6 +
7 +
@default fn main() -> i32 {
8 +
    let xs: [u8; 5] = [10, 20, 30, 40, 50];
9 +
    let slice: *[u8] = &xs[..];
10 +
11 +
    let start = id(1);
12 +
    let end = id(4);
13 +
    let mid = &slice[start..end];
14 +
15 +
    assert mid.len == 3;
16 +
    assert mid[0] == 20;
17 +
    assert mid[1] == 30;
18 +
    assert mid[2] == 40;
19 +
20 +
    let suffixStart = id(2);
21 +
    let suffix = &slice[suffixStart..];
22 +
23 +
    assert suffix.len == 3;
24 +
    assert suffix[0] == 30;
25 +
    assert suffix[1] == 40;
26 +
    assert suffix[2] == 50;
27 +
28 +
    let prefixEnd = id(3);
29 +
    let prefix = &slice[..prefixEnd];
30 +
31 +
    assert prefix.len == 3;
32 +
    assert prefix[0] == 10;
33 +
    assert prefix[1] == 20;
34 +
    assert prefix[2] == 30;
35 +
36 +
    let emptyStart = id(slice.len);
37 +
    let emptyEnd = id(slice.len);
38 +
    let empty = &slice[emptyStart..emptyEnd];
39 +
40 +
    assert empty.len == 0;
41 +
    return 0;
42 +
}
test/tests/slice.range.order.check.rad added +14 -0
1 +
//! returns: 133
2 +
//! Test dynamic slice range ordering check with runtime EBREAK.
3 +
4 +
fn id(n: u32) -> u32 {
5 +
    return n;
6 +
}
7 +
8 +
@default fn main() -> i32 {
9 +
    let xs: [i32; 3] = [1, 2, 3];
10 +
    let slice: *[i32] = &xs[..];
11 +
    let bad = &slice[id(2)..id(1)];
12 +
13 +
    return bad.len as i32;
14 +
}
test/tests/slice.range.ril +25 -0
1 1
fn w64 $sliceRange(w64 %0, w64 %1, w32 %2, w32 %3) {
2 2
  @entry0
3 3
    load w64 %4 %1 0;
4 4
    load w32 %5 %1 8;
5 +
    br.ult w32 %5 %2 @guard#trap1 @guard#pass2;
6 +
  @guard#trap1
7 +
    ebreak;
8 +
    unreachable;
9 +
  @guard#pass2
10 +
    br.ult w32 %5 %3 @guard#trap3 @guard#pass4;
11 +
  @guard#trap3
12 +
    ebreak;
13 +
    unreachable;
14 +
  @guard#pass4
15 +
    br.ult w32 %3 %2 @guard#trap5 @guard#pass6;
16 +
  @guard#trap5
17 +
    ebreak;
18 +
    unreachable;
19 +
  @guard#pass6
5 20
    mul w64 %6 %2 4;
6 21
    add w64 %7 %4 %6;
7 22
    sub w32 %8 %3 %2;
8 23
    reserve %9 16 8;
9 24
    store w64 %7 %9 0;
15 30
16 31
fn w64 $sliceRangeOpenEnd(w64 %0, w64 %1, w32 %2) {
17 32
  @entry0
18 33
    load w64 %3 %1 0;
19 34
    load w32 %4 %1 8;
35 +
    br.ult w32 %4 %2 @guard#trap1 @guard#pass2;
36 +
  @guard#trap1
37 +
    ebreak;
38 +
    unreachable;
39 +
  @guard#pass2
20 40
    mul w64 %5 %2 4;
21 41
    add w64 %6 %3 %5;
22 42
    sub w32 %7 %4 %2;
23 43
    reserve %8 16 8;
24 44
    store w64 %6 %8 0;
30 50
31 51
fn w64 $sliceRangeOpenStart(w64 %0, w64 %1, w32 %2) {
32 52
  @entry0
33 53
    load w64 %3 %1 0;
34 54
    load w32 %4 %1 8;
55 +
    br.ult w32 %4 %2 @guard#trap1 @guard#pass2;
56 +
  @guard#trap1
57 +
    ebreak;
58 +
    unreachable;
59 +
  @guard#pass2
35 60
    reserve %5 16 8;
36 61
    store w64 %3 %5 0;
37 62
    store w32 %2 %5 8;
38 63
    store w32 %2 %5 12;
39 64
    blit %0 %5 16;