verify: track pointer bounds for static offset checking

6a6120933bb30c5baa72e3fcd1a74f2ed9e4dfca08a88f98100b1e15852aabf2
Extend the verifier's RegType::Ptr to carry an allocation bound.
Reserve with a constant size produces Ptr(size). Pointer arithmetic
with a constant offset derives Ptr(bound - offset). Load/Store/Blit
check that offset + access_size <= bound when the bound is known.

Pointers from unknown sources (parameters, memory loads, calls,
DataSym, WordToPtr, Elem) get Ptr(0) meaning unknown bound, which
skips the check.

This catches out-of-bounds field access at constant offsets into
stack allocations without any CFG analysis.

Also refactors verifyInstr to use a VerifyCtx record, reducing
parameter count on helper functions.
Alexis Sellier committed ago 1 parent c1192d4f
lib/std/lang/il/verify.rad +149 -82
7 7
//! 2. Load/Store/Blit base registers must have type Ptr.
8 8
//! 3. WordToPtr is flagged as a provenance violation.
9 9
//! 4. BinOp::Add with Ptr type requires exactly one Ptr operand.
10 10
//! 5. Copy(DataSym)/Copy(FnAddr) produces Ptr; Copy(Imm) produces W64.
11 11
//! 6. Reserve always produces Ptr.
12 +
//! 7. Static offset checks: field access at constant offsets is validated
13 +
//!    against the known allocation size when available.
12 14
13 15
use std::fmt;
14 16
use std::io;
15 17
use std::lang::alloc;
16 18
17 19
/// Maximum SSA registers per function (must match regalloc).
18 20
const MAX_REGS: u32 = 8192;
19 21
22 +
/// Unknown bound sentinel. When a pointer's bound is zero, bounds
23 +
/// checking is skipped (the allocation size is not statically known).
24 +
const UNKNOWN_BOUND: u32 = 0;
25 +
20 26
/// A verification error found in an IL function.
21 27
pub record VerifyError {
22 28
    /// Name of the function containing the error.
23 29
    fnName: *[u8],
24 30
    /// Block index where the error occurred.
60 66
union RegType {
61 67
    /// Not yet defined.
62 68
    Undef,
63 69
    /// Word type (W8, W16, W32, W64).
64 70
    Word(super::Type),
65 -
    /// Pointer type.
66 -
    Ptr,
71 +
    /// Pointer type with known bound. Bound of 0 means unknown.
72 +
    Ptr(u32),
67 73
}
68 74
69 -
/// Check if a register type is Ptr.
75 +
/// Check if a register type is Ptr (any bound).
70 76
fn isPtr(t: RegType) -> bool {
71 -
    if let case RegType::Ptr = t { return true; }
77 +
    if let case RegType::Ptr(_) = t { return true; }
72 78
    return false;
73 79
}
74 80
81 +
/// Get the bound of a Ptr register type, or 0 if not Ptr.
82 +
fn ptrBound(t: RegType) -> u32 {
83 +
    if let case RegType::Ptr(b) = t { return b; }
84 +
    return UNKNOWN_BOUND;
85 +
}
86 +
87 +
/// Create a Ptr RegType from an IL type. Non-Ptr types produce Word.
88 +
fn regTypeFromIl(typ: super::Type) -> RegType {
89 +
    if let case super::Type::Ptr = typ {
90 +
        return RegType::Ptr(UNKNOWN_BOUND);
91 +
    }
92 +
    return RegType::Word(typ);
93 +
}
94 +
75 95
/// Infer the type of a Val without looking at the register map.
76 96
/// Returns nil for Reg values (caller must look up the map).
77 97
fn valType(val: super::Val) -> ?RegType {
78 98
    match val {
79 99
        case super::Val::Imm(_) => return RegType::Word(super::Type::W64),
80 100
        case super::Val::DataSym(_),
81 -
             super::Val::FnAddr(_) => return RegType::Ptr,
101 +
             super::Val::FnAddr(_) => return RegType::Ptr(UNKNOWN_BOUND),
82 102
        case super::Val::Undef => return RegType::Word(super::Type::W64),
83 103
        case super::Val::Reg(_) => return nil,
84 104
    }
85 105
}
86 106
94 114
        return regs[r.n];
95 115
    }
96 116
    return RegType::Undef;
97 117
}
98 118
119 +
/// Extract a constant i64 from a Val, if it is an immediate.
120 +
fn immVal(val: super::Val) -> ?i64 {
121 +
    if let case super::Val::Imm(v) = val { return v; }
122 +
    return nil;
123 +
}
124 +
99 125
/// Verify a single function.
100 126
fn verifyFn(
101 127
    func: *super::Fn,
102 128
    errors: *mut *mut [VerifyError],
103 129
    wtpCount: *mut u32,
121 147
            setReg(&mut regTypes[..], param.value, regTypeFromIl(param.type));
122 148
        }
123 149
124 150
        // Check each instruction.
125 151
        for instr, instrIdx in block.instrs {
126 -
            verifyInstr(
127 -
                func, blockIdx as u32, instrIdx as u32,
128 -
                instr, &mut regTypes[..], errors, wtpCount, a,
129 -
            );
152 +
            let mut ctx = VerifyCtx {
153 +
                func,
154 +
                blockIdx: blockIdx as u32,
155 +
                instrIdx: instrIdx as u32,
156 +
                regs: &mut regTypes[..],
157 +
                errors,
158 +
                a,
159 +
            };
160 +
            verifyInstr(&mut ctx, instr, wtpCount);
130 161
        }
131 162
    }
132 163
}
133 164
134 165
/// Verify a single instruction: assign dst type and check operands.
135 -
fn verifyInstr(
136 -
    func: *super::Fn,
137 -
    blockIdx: u32,
138 -
    instrIdx: u32,
139 -
    instr: super::Instr,
140 -
    regs: *mut [RegType],
141 -
    errors: *mut *mut [VerifyError],
142 -
    wtpCount: *mut u32,
143 -
    a: alloc::Allocator,
144 -
) {
166 +
fn verifyInstr(ctx: *mut VerifyCtx, instr: super::Instr, wtpCount: *mut u32) {
145 167
    match instr {
146 -
        case super::Instr::Reserve { dst, .. } => {
147 -
            // Reserve always produces a pointer.
148 -
            setReg(regs, dst, RegType::Ptr);
168 +
        case super::Instr::Reserve { dst, size, .. } => {
169 +
            let bound = immVal(size) else {
170 +
                setReg(ctx.regs, dst, RegType::Ptr(UNKNOWN_BOUND));
171 +
                return;
172 +
            };
173 +
            setReg(ctx.regs, dst, RegType::Ptr(bound as u32));
149 174
        }
150 -
        case super::Instr::Load { typ, dst, src, .. } => {
151 -
            checkPtrBase(func, blockIdx, instrIdx, regs, src, errors, a, "load base");
152 -
            setReg(regs, dst, regTypeFromIl(typ));
175 +
        case super::Instr::Load { typ, dst, src, offset } => {
176 +
            checkPtrBase(ctx, src, "load base");
177 +
            checkAccess(ctx, src, offset, super::typeSize(typ));
178 +
            setReg(ctx.regs, dst, regTypeFromIl(typ));
153 179
        }
154 -
        case super::Instr::Sload { typ, dst, src, .. } => {
155 -
            checkPtrBase(func, blockIdx, instrIdx, regs, src, errors, a, "sload base");
156 -
            setReg(regs, dst, regTypeFromIl(typ));
180 +
        case super::Instr::Sload { typ, dst, src, offset } => {
181 +
            checkPtrBase(ctx, src, "sload base");
182 +
            checkAccess(ctx, src, offset, super::typeSize(typ));
183 +
            setReg(ctx.regs, dst, regTypeFromIl(typ));
157 184
        }
158 -
        case super::Instr::Store { dst, .. } => {
159 -
            checkPtrBase(func, blockIdx, instrIdx, regs, dst, errors, a, "store base");
185 +
        case super::Instr::Store { typ, dst, offset, .. } => {
186 +
            checkPtrBase(ctx, dst, "store base");
187 +
            checkAccess(ctx, dst, offset, super::typeSize(typ));
160 188
        }
161 -
        case super::Instr::Blit { dst, src, .. } => {
162 -
            checkPtrBase(func, blockIdx, instrIdx, regs, dst, errors, a, "blit dst");
163 -
            checkPtrBase(func, blockIdx, instrIdx, regs, src, errors, a, "blit src");
189 +
        case super::Instr::Blit { dst, src, size } => {
190 +
            checkPtrBase(ctx, dst, "blit dst");
191 +
            checkPtrBase(ctx, src, "blit src");
192 +
            if let sz = immVal(size) {
193 +
                checkAccess(ctx, dst, 0, sz as u32);
194 +
                checkAccess(ctx, src, 0, sz as u32);
195 +
            }
164 196
        }
165 197
        case super::Instr::Copy { dst, val } => {
166 -
            // Type depends on the value being copied.
167 -
            let vt = resolveValType(val, regs);
168 -
            setReg(regs, dst, vt);
198 +
            setReg(ctx.regs, dst, resolveValType(val, ctx.regs));
169 199
        }
170 200
        case super::Instr::BinOp { op, typ, dst, a: va, b: vb } => {
171 201
            if let case super::Type::Ptr = typ {
172 -
                // Pointer arithmetic: result is Ptr.
173 -
                // Check that exactly one operand is Ptr.
174 -
                let aIsPtr = isPtr(resolveValType(va, regs));
175 -
                let bIsPtr = isPtr(resolveValType(vb, regs));
202 +
                let aIsPtr = isPtr(resolveValType(va, ctx.regs));
203 +
                let bIsPtr = isPtr(resolveValType(vb, ctx.regs));
176 204
177 205
                if let case super::BinOp::Add = op {
178 206
                    if not (aIsPtr or bIsPtr) {
179 -
                        emitError(func, blockIdx, instrIdx, errors, a,
207 +
                        emitError(ctx.func, ctx.blockIdx, ctx.instrIdx, ctx.errors, ctx.a,
180 208
                            "ptr add: neither operand is Ptr");
181 209
                    }
182 210
                }
183 -
                setReg(regs, dst, RegType::Ptr);
211 +
                setReg(ctx.regs, dst, RegType::Ptr(derivePtrAdd(va, vb, ctx.regs)));
184 212
            } else {
185 -
                setReg(regs, dst, RegType::Word(typ));
213 +
                setReg(ctx.regs, dst, RegType::Word(typ));
186 214
            }
187 215
        }
188 -
        case super::Instr::UnOp { typ, dst, .. } => {
189 -
            setReg(regs, dst, RegType::Word(typ));
190 -
        }
191 -
        case super::Instr::Zext { dst, .. } => {
192 -
            setReg(regs, dst, RegType::Word(super::Type::W64));
193 -
        }
194 -
        case super::Instr::Sext { dst, .. } => {
195 -
            setReg(regs, dst, RegType::Word(super::Type::W64));
196 -
        }
197 -
        case super::Instr::PtrToWord { dst, .. } => {
198 -
            setReg(regs, dst, RegType::Word(super::Type::W64));
199 -
        }
216 +
        case super::Instr::UnOp { typ, dst, .. } =>
217 +
            setReg(ctx.regs, dst, RegType::Word(typ)),
218 +
        case super::Instr::Zext { dst, .. } =>
219 +
            setReg(ctx.regs, dst, RegType::Word(super::Type::W64)),
220 +
        case super::Instr::Sext { dst, .. } =>
221 +
            setReg(ctx.regs, dst, RegType::Word(super::Type::W64)),
222 +
        case super::Instr::PtrToWord { dst, .. } =>
223 +
            setReg(ctx.regs, dst, RegType::Word(super::Type::W64)),
200 224
        case super::Instr::WordToPtr { dst, .. } => {
201 -
            // Count provenance escapes.
202 225
            *wtpCount += 1;
203 -
            setReg(regs, dst, RegType::Ptr);
226 +
            setReg(ctx.regs, dst, RegType::Ptr(UNKNOWN_BOUND));
204 227
        }
205 228
        case super::Instr::Call { retTy, dst, .. } => {
206 229
            if let d = dst {
207 -
                setReg(regs, d, regTypeFromIl(retTy));
230 +
                setReg(ctx.regs, d, regTypeFromIl(retTy));
208 231
            }
209 232
        }
210 -
        case super::Instr::Ecall { dst, .. } => {
211 -
            // Ecall returns a word value.
212 -
            setReg(regs, dst, RegType::Word(super::Type::W64));
213 -
        }
233 +
        case super::Instr::Ecall { dst, .. } =>
234 +
            setReg(ctx.regs, dst, RegType::Word(super::Type::W64)),
214 235
        case super::Instr::Elem { dst, base, .. } => {
215 -
            // Bounds-checked element pointer: base must be Ptr, result is Ptr.
216 -
            checkPtrBase(func, blockIdx, instrIdx, regs, base, errors, a, "elem base");
217 -
            setReg(regs, dst, RegType::Ptr);
236 +
            checkPtrBase(ctx, base, "elem base");
237 +
            setReg(ctx.regs, dst, RegType::Ptr(UNKNOWN_BOUND));
218 238
        }
219 239
        case super::Instr::Ret { .. },
220 240
             super::Instr::Jmp { .. },
221 241
             super::Instr::Br { .. },
222 242
             super::Instr::Switch { .. },
223 243
             super::Instr::Unreachable,
224 244
             super::Instr::Ebreak => {}
225 245
    }
226 246
}
227 247
248 +
/// Derive the remaining bound after `ptr + offset`.
249 +
/// If the pointer operand has a known bound and the offset is a constant,
250 +
/// returns `bound - offset`. Otherwise returns UNKNOWN_BOUND.
251 +
fn derivePtrAdd(va: super::Val, vb: super::Val, regs: *[RegType]) -> u32 {
252 +
    // Find which operand is the pointer and which is the offset.
253 +
    let aType = resolveValType(va, regs);
254 +
    let bType = resolveValType(vb, regs);
255 +
256 +
    let mut bound: u32 = UNKNOWN_BOUND;
257 +
    let mut offset: ?i64 = nil;
258 +
259 +
    if isPtr(aType) {
260 +
        bound = ptrBound(aType);
261 +
        offset = immVal(vb);
262 +
    } else if isPtr(bType) {
263 +
        bound = ptrBound(bType);
264 +
        offset = immVal(va);
265 +
    }
266 +
    if bound == UNKNOWN_BOUND {
267 +
        return UNKNOWN_BOUND;
268 +
    }
269 +
    let off = offset else return UNKNOWN_BOUND;
270 +
    if off < 0 or off as u32 >= bound {
271 +
        return UNKNOWN_BOUND;
272 +
    }
273 +
    return bound - (off as u32);
274 +
}
275 +
228 276
/// Set a register's type in the map.
229 277
fn setReg(regs: *mut [RegType], reg: super::Reg, typ: RegType) {
230 278
    if reg.n < regs.len {
231 279
        regs[reg.n] = typ;
232 280
    }
233 281
}
234 282
235 -
/// Convert an IL type to a RegType.
236 -
fn regTypeFromIl(typ: super::Type) -> RegType {
237 -
    if let case super::Type::Ptr = typ {
238 -
        return RegType::Ptr;
283 +
/// Check that a register used as a memory base has type Ptr.
284 +
fn checkPtrBase(ctx: *mut VerifyCtx, reg: super::Reg, context: *[u8]) {
285 +
    if reg.n >= ctx.regs.len {
286 +
        return;
287 +
    }
288 +
    if not isPtr(ctx.regs[reg.n]) {
289 +
        emitError(ctx.func, ctx.blockIdx, ctx.instrIdx, ctx.errors, ctx.a, context);
239 290
    }
240 -
    return RegType::Word(typ);
241 291
}
242 292
243 -
/// Check that a register used as a memory base has type Ptr.
244 -
fn checkPtrBase(
293 +
/// Check that a memory access at `base + offset` of `size` bytes is
294 +
/// within the known bounds of the pointer. Skipped when the bound is
295 +
/// unknown (UNKNOWN_BOUND).
296 +
fn checkAccess(
297 +
    ctx: *mut VerifyCtx,
298 +
    base: super::Reg,
299 +
    offset: i32,
300 +
    size: u32,
301 +
) {
302 +
    if base.n >= ctx.regs.len {
303 +
        return;
304 +
    }
305 +
    let bound = ptrBound(ctx.regs[base.n]);
306 +
    if bound == UNKNOWN_BOUND {
307 +
        return;
308 +
    }
309 +
    if offset < 0 {
310 +
        emitError(ctx.func, ctx.blockIdx, ctx.instrIdx, ctx.errors, ctx.a, "negative offset");
311 +
        return;
312 +
    }
313 +
    let end = (offset as u32) + size;
314 +
    if end > bound {
315 +
        emitError(ctx.func, ctx.blockIdx, ctx.instrIdx, ctx.errors, ctx.a, "access out of bounds");
316 +
    }
317 +
}
318 +
319 +
/// Context passed to verification helpers to avoid parameter bloat.
320 +
record VerifyCtx {
245 321
    func: *super::Fn,
246 322
    blockIdx: u32,
247 323
    instrIdx: u32,
248 -
    regs: *[RegType],
249 -
    reg: super::Reg,
324 +
    regs: *mut [RegType],
250 325
    errors: *mut *mut [VerifyError],
251 326
    a: alloc::Allocator,
252 -
    context: *[u8],
253 -
) {
254 -
    if reg.n >= regs.len {
255 -
        return;
256 -
    }
257 -
    if not isPtr(regs[reg.n]) {
258 -
        emitError(func, blockIdx, instrIdx, errors, a, context);
259 -
    }
260 327
}
261 328
262 329
/// Record a verification error.
263 330
fn emitError(
264 331
    func: *super::Fn,