compiler: Reject overflowing arena allocations

db9890711300e893e28e6e066a13f043a628a4fdc1ba5bb7c55bfd1b2ba33d4e
Check alignment and allocation-size arithmetic before updating an arena offset.
Reject element-count multiplication overflow in typed slice allocation. The
allocator now preserves its state when a request cannot fit.
Alexis Sellier committed ago 1 parent 85034f8f
lib/std/arch/rv64/asm/parser.rad +2 -11
27 27
    while a.scan.current.kind <> scanner::TokenKind::Eof {
28 28
        try parseItem(a);
29 29
    }
30 30
}
31 31
32 -
/// Align `value` upward to `alignment`, returning nil on u32 overflow.
33 -
fn checkedAlignUp(value: u32, alignment: u32) -> ?u32 {
34 -
    let padding = alignment - 1;
35 -
    if value > parser::U32_MAX - padding {
36 -
        return nil;
37 -
    }
38 -
    return mem::alignUp(value, alignment);
39 -
}
40 -
41 32
/// Advance the parser by one token, preserving the previous token.
42 33
unsafe fn advance(a: &mut super::Assembler) {
43 34
    set a.scan.previous = a.scan.current;
44 35
    set a.scan.current = scanner::next(&mut a.scan);
45 36
}
596 587
        case super::Section::Text => {
597 588
            if amount % rv64::INSTR_SIZE as u32 <> 0 {
598 589
                throw fail(a, "text alignment must be a multiple of 4");
599 590
            }
600 591
            let bytes = a.text.len * rv64::INSTR_SIZE as u32;
601 -
            let aligned = checkedAlignUp(bytes, amount) else {
592 +
            let aligned = mem::checkedAlignUp(bytes, amount) else {
602 593
                throw super::Error::TextOverflow;
603 594
            };
604 595
            let words = (aligned - bytes) / rv64::INSTR_SIZE as u32;
605 596
            try emit::emitTextPadding(a, words);
606 597
        }
607 598
        case super::Section::Data => {
608 -
            let aligned = checkedAlignUp(a.data.len, amount) else {
599 +
            let aligned = mem::checkedAlignUp(a.data.len, amount) else {
609 600
                throw super::Error::DataOverflow;
610 601
            };
611 602
            for _ in a.data.len..aligned {
612 603
                try emit::emitByte(a, 0);
613 604
            }
lib/std/lang/alloc.rad +9 -4
37 37
/// appropriate type and initializing the memory.
38 38
export fn alloc(arena: &mut Arena, size: u32, alignment: u32) -> *mut opaque throws (AllocError) {
39 39
    assert alignment > 0;
40 40
    assert size > 0;
41 41
42 -
    let aligned = mem::alignUp(arena.offset, alignment);
43 -
    let newOffset = aligned + size;
44 -
45 -
    if newOffset > arena.data.len as u32 {
42 +
    let aligned = mem::checkedAlignUp(arena.offset, alignment) else {
43 +
        throw AllocError::OutOfMemory;
44 +
    };
45 +
    if aligned > arena.data.len or size > arena.data.len - aligned {
46 46
        throw AllocError::OutOfMemory;
47 47
    }
48 +
    let newOffset = aligned + size;
49 +
48 50
    let base: *mut u8 = &mut arena.data[aligned];
49 51
    set arena.offset = newOffset;
50 52
51 53
    return base as *mut opaque;
52 54
}
97 99
/// Throws `AllocError` if the arena is exhausted.
98 100
export unsafe fn allocSlice(arena: &mut Arena, size: u32, alignment: u32, count: u32) -> *mut [opaque] throws (AllocError) {
99 101
    if count == 0 {
100 102
        return &mut [];
101 103
    }
104 +
    if size > mem::MAX_SIZE / count {
105 +
        throw AllocError::OutOfMemory;
106 +
    }
102 107
    let ptr = try alloc(arena, size * count, alignment);
103 108
104 109
    return @sliceOf(ptr, count);
105 110
}
106 111
lib/std/mem.rad +17 -0
1 +
//! Memory operations and alignment helpers.
2 +
3 +
/// Maximum byte size or offset that fits in the 32-bit memory interface.
4 +
export constant MAX_SIZE: u32 = 0xffffffff;
5 +
1 6
/// Memory error.
2 7
union MemoryError: Copy {
3 8
    /// Buffer is too small.
4 9
    BufferTooSmall,
5 10
}
35 40
/// Align value up to alignment boundary.
36 41
export fn alignUp(value: u32, alignment: u32) -> u32 {
37 42
    return (value + alignment - 1) & ~(alignment - 1);
38 43
}
39 44
45 +
/// Align `value` upward, or return nil if the result exceeds `MAX_SIZE`.
46 +
/// `alignment` must be a nonzero power of two.
47 +
export fn checkedAlignUp(value: u32, alignment: u32) -> ?u32 {
48 +
    assert alignment > 0;
49 +
    let padding = alignment - 1;
50 +
    assert (alignment & padding) == 0;
51 +
    if value > MAX_SIZE - padding {
52 +
        return nil;
53 +
    }
54 +
    return alignUp(value, alignment);
55 +
}
56 +
40 57
/// Align signed value up to alignment boundary.
41 58
export fn alignUpI32(value: i32, alignment: i32) -> i32 {
42 59
    return (value + alignment - 1) & ~(alignment - 1);
43 60
}
44 61
lib/std/tests.rad +14 -0
217 217
    try testing::expect(buffer[2] == 0);
218 218
}
219 219
220 220
// mem /////////////////////////////////////////////////////////////////////////
221 221
222 +
/// Checked alignment accepts representable results and rejects overflow.
223 +
@test fn testCheckedAlignUp() throws (testing::TestError) {
224 +
    let alignment: u32 = 16;
225 +
    let lastAligned = mem::MAX_SIZE - (alignment - 1);
226 +
    assert mem::checkedAlignUp(0, alignment) == 0;
227 +
    assert mem::checkedAlignUp(1, alignment) == alignment;
228 +
    assert mem::checkedAlignUp(alignment, alignment) == alignment;
229 +
    assert mem::checkedAlignUp(lastAligned - 1, alignment) == lastAligned;
230 +
    assert mem::checkedAlignUp(lastAligned, alignment) == lastAligned;
231 +
    assert mem::checkedAlignUp(lastAligned + 1, alignment) == nil;
232 +
    assert mem::checkedAlignUp(mem::MAX_SIZE, alignment) == nil;
233 +
    assert mem::checkedAlignUp(mem::MAX_SIZE, 1) == mem::MAX_SIZE;
234 +
}
235 +
222 236
@test fn testCopyFullSlice() throws (testing::TestError) {
223 237
    let mut xs: [u8; 3] = [1, 2, 3];
224 238
    let mut ys: [u8; 3] = [4, 5, 6];
225 239
226 240
    try! mem::copy(&mut xs[..], &ys[..]);