il: add RIL type verifier

c22390f5142ec35e3f54d529d045a019efc3de85fa36e56410d6af0402ec82ba
Phase 7.1: implement a basic SSA type checker for pointer provenance.

The verifier walks each IL function and maintains a per-register type
map (Word or Ptr). It checks:
- Load/Store/Blit base registers must have type Ptr
- Reserve always produces Ptr
- Copy(DataSym/FnAddr) produces Ptr; Copy(Imm) produces W64
- Load/Sload with Ptr type produces Ptr
- BinOp::Add with Ptr type requires at least one Ptr operand
- PtrToWord produces W64; WordToPtr produces Ptr
- Call return type flows through
- WordToPtr instructions are counted as provenance escapes

The verifier is a passive analysis pass (il/verify.rad) that can be
invoked on any IL Program to check safety properties.
Alexis Sellier committed ago 1 parent 985a1b92
lib/std/lang/il.rad +1 -0
85 85
86 86
// TODO: Labels should have their own type.
87 87
// TODO: Blocks should have an instruction in `Instr`.
88 88
89 89
pub mod printer;
90 +
pub mod verify;
90 91
91 92
use std::mem;
92 93
use std::lang::alloc;
93 94
94 95
/// Source location for debug info.
lib/std/lang/il/verify.rad added +316 -0
1 +
//! RIL type verifier.
2 +
//!
3 +
//! Walks each function's blocks and checks pointer-provenance rules:
4 +
//!
5 +
//! 1. Every register is assigned a type (W8/W16/W32/W64/Ptr) based on
6 +
//!    its defining instruction.
7 +
//! 2. Load/Store/Blit base registers must have type Ptr.
8 +
//! 3. WordToPtr is flagged as a provenance violation.
9 +
//! 4. BinOp::Add with Ptr type requires exactly one Ptr operand.
10 +
//! 5. Copy(DataSym)/Copy(FnAddr) produces Ptr; Copy(Imm) produces W64.
11 +
//! 6. Reserve always produces Ptr.
12 +
13 +
use std::fmt;
14 +
use std::io;
15 +
use std::lang::alloc;
16 +
17 +
/// Maximum SSA registers per function (must match regalloc).
18 +
const MAX_REGS: u32 = 8192;
19 +
20 +
/// A verification error found in an IL function.
21 +
pub record VerifyError {
22 +
    /// Name of the function containing the error.
23 +
    fnName: *[u8],
24 +
    /// Block index where the error occurred.
25 +
    blockIdx: u32,
26 +
    /// Instruction index within the block.
27 +
    instrIdx: u32,
28 +
    /// Description of the error.
29 +
    message: *[u8],
30 +
}
31 +
32 +
/// Result of verifying a program.
33 +
pub record VerifyResult {
34 +
    /// Errors found during verification.
35 +
    errors: *mut [VerifyError],
36 +
    /// Number of WordToPtr instructions found (provenance escapes).
37 +
    wtpCount: u32,
38 +
}
39 +
40 +
/// Verify pointer-type safety of an IL program.
41 +
///
42 +
/// Returns a list of errors. An empty list means the program passes
43 +
/// all checks. WordToPtr instructions are counted but not treated as
44 +
/// errors (they are legal in unsafe code).
45 +
pub fn verifyProgram(program: *super::Program, arena: *mut alloc::Arena) -> VerifyResult {
46 +
    let a = alloc::arenaAllocator(arena);
47 +
    let mut errors: *mut [VerifyError] = &mut [];
48 +
    let mut wtpCount: u32 = 0;
49 +
50 +
    for func in program.fns {
51 +
        if func.isExtern {
52 +
            continue;
53 +
        }
54 +
        verifyFn(func, &mut errors, &mut wtpCount, a);
55 +
    }
56 +
    return VerifyResult { errors, wtpCount };
57 +
}
58 +
59 +
/// Inferred type of an SSA register: either a word width or pointer.
60 +
union RegType {
61 +
    /// Not yet defined.
62 +
    Undef,
63 +
    /// Word type (W8, W16, W32, W64).
64 +
    Word(super::Type),
65 +
    /// Pointer type.
66 +
    Ptr,
67 +
}
68 +
69 +
/// Check if a register type is Ptr.
70 +
fn isPtr(t: RegType) -> bool {
71 +
    if let case RegType::Ptr = t { return true; }
72 +
    return false;
73 +
}
74 +
75 +
/// Infer the type of a Val without looking at the register map.
76 +
/// Returns nil for Reg values (caller must look up the map).
77 +
fn valType(val: super::Val) -> ?RegType {
78 +
    match val {
79 +
        case super::Val::Imm(_) => return RegType::Word(super::Type::W64),
80 +
        case super::Val::DataSym(_),
81 +
             super::Val::FnAddr(_) => return RegType::Ptr,
82 +
        case super::Val::Undef => return RegType::Word(super::Type::W64),
83 +
        case super::Val::Reg(_) => return nil,
84 +
    }
85 +
}
86 +
87 +
/// Look up the type of a Val, consulting the register map for Reg values.
88 +
fn resolveValType(val: super::Val, regs: *[RegType]) -> RegType {
89 +
    if let vt = valType(val) {
90 +
        return vt;
91 +
    }
92 +
    let case super::Val::Reg(r) = val else return RegType::Word(super::Type::W64);
93 +
    if r.n < regs.len {
94 +
        return regs[r.n];
95 +
    }
96 +
    return RegType::Undef;
97 +
}
98 +
99 +
/// Verify a single function.
100 +
fn verifyFn(
101 +
    func: *super::Fn,
102 +
    errors: *mut *mut [VerifyError],
103 +
    wtpCount: *mut u32,
104 +
    a: alloc::Allocator,
105 +
) {
106 +
    // Type map: regId -> RegType.
107 +
    let mut regTypes: [RegType; MAX_REGS] = undefined;
108 +
    for i in 0..MAX_REGS {
109 +
        regTypes[i] = RegType::Undef;
110 +
    }
111 +
112 +
    // Type function parameters.
113 +
    for param in func.params {
114 +
        if param.value.n < MAX_REGS {
115 +
            if let case super::Type::Ptr = param.type {
116 +
                regTypes[param.value.n] = RegType::Ptr;
117 +
            } else {
118 +
                regTypes[param.value.n] = RegType::Word(param.type);
119 +
            }
120 +
        }
121 +
    }
122 +
123 +
    // Walk blocks.
124 +
    for block, blockIdx in func.blocks {
125 +
        // Type block parameters.
126 +
        for param in block.params {
127 +
            if param.value.n < MAX_REGS {
128 +
                if let case super::Type::Ptr = param.type {
129 +
                    regTypes[param.value.n] = RegType::Ptr;
130 +
                } else {
131 +
                    regTypes[param.value.n] = RegType::Word(param.type);
132 +
                }
133 +
            }
134 +
        }
135 +
136 +
        // Check each instruction.
137 +
        for instr, instrIdx in block.instrs {
138 +
            verifyInstr(
139 +
                func, blockIdx as u32, instrIdx as u32,
140 +
                instr, &mut regTypes[..], errors, wtpCount, a,
141 +
            );
142 +
        }
143 +
    }
144 +
}
145 +
146 +
/// Verify a single instruction: assign dst type and check operands.
147 +
fn verifyInstr(
148 +
    func: *super::Fn,
149 +
    blockIdx: u32,
150 +
    instrIdx: u32,
151 +
    instr: super::Instr,
152 +
    regs: *mut [RegType],
153 +
    errors: *mut *mut [VerifyError],
154 +
    wtpCount: *mut u32,
155 +
    a: alloc::Allocator,
156 +
) {
157 +
    match instr {
158 +
        case super::Instr::Reserve { dst, .. } => {
159 +
            // Reserve always produces a pointer.
160 +
            setReg(regs, dst, RegType::Ptr);
161 +
        }
162 +
        case super::Instr::Load { typ, dst, src, .. } => {
163 +
            // Base must be Ptr.
164 +
            checkPtrBase(func, blockIdx, instrIdx, regs, src, errors, a, "load base");
165 +
            // Result type depends on the load type.
166 +
            if let case super::Type::Ptr = typ {
167 +
                setReg(regs, dst, RegType::Ptr);
168 +
            } else {
169 +
                setReg(regs, dst, RegType::Word(typ));
170 +
            }
171 +
        }
172 +
        case super::Instr::Sload { typ, dst, src, .. } => {
173 +
            checkPtrBase(func, blockIdx, instrIdx, regs, src, errors, a, "sload base");
174 +
            if let case super::Type::Ptr = typ {
175 +
                setReg(regs, dst, RegType::Ptr);
176 +
            } else {
177 +
                setReg(regs, dst, RegType::Word(typ));
178 +
            }
179 +
        }
180 +
        case super::Instr::Store { dst, .. } => {
181 +
            checkPtrBase(func, blockIdx, instrIdx, regs, dst, errors, a, "store base");
182 +
        }
183 +
        case super::Instr::Blit { dst, src, .. } => {
184 +
            checkPtrBase(func, blockIdx, instrIdx, regs, dst, errors, a, "blit dst");
185 +
            checkPtrBase(func, blockIdx, instrIdx, regs, src, errors, a, "blit src");
186 +
        }
187 +
        case super::Instr::Copy { dst, val } => {
188 +
            // Type depends on the value being copied.
189 +
            let vt = resolveValType(val, regs);
190 +
            setReg(regs, dst, vt);
191 +
        }
192 +
        case super::Instr::BinOp { op, typ, dst, a: va, b: vb } => {
193 +
            if let case super::Type::Ptr = typ {
194 +
                // Pointer arithmetic: result is Ptr.
195 +
                // Check that exactly one operand is Ptr.
196 +
                let aIsPtr = isPtr(resolveValType(va, regs));
197 +
                let bIsPtr = isPtr(resolveValType(vb, regs));
198 +
199 +
                if let case super::BinOp::Add = op {
200 +
                    if not (aIsPtr or bIsPtr) {
201 +
                        emitError(func, blockIdx, instrIdx, errors, a,
202 +
                            "ptr add: neither operand is Ptr");
203 +
                    }
204 +
                }
205 +
                setReg(regs, dst, RegType::Ptr);
206 +
            } else {
207 +
                setReg(regs, dst, RegType::Word(typ));
208 +
            }
209 +
        }
210 +
        case super::Instr::UnOp { typ, dst, .. } => {
211 +
            setReg(regs, dst, RegType::Word(typ));
212 +
        }
213 +
        case super::Instr::Zext { dst, .. } => {
214 +
            setReg(regs, dst, RegType::Word(super::Type::W64));
215 +
        }
216 +
        case super::Instr::Sext { dst, .. } => {
217 +
            setReg(regs, dst, RegType::Word(super::Type::W64));
218 +
        }
219 +
        case super::Instr::PtrToWord { dst, .. } => {
220 +
            setReg(regs, dst, RegType::Word(super::Type::W64));
221 +
        }
222 +
        case super::Instr::WordToPtr { dst, .. } => {
223 +
            // Count provenance escapes.
224 +
            *wtpCount += 1;
225 +
            setReg(regs, dst, RegType::Ptr);
226 +
        }
227 +
        case super::Instr::Call { retTy, dst, .. } => {
228 +
            if let d = dst {
229 +
                if let case super::Type::Ptr = retTy {
230 +
                    setReg(regs, d, RegType::Ptr);
231 +
                } else {
232 +
                    setReg(regs, d, RegType::Word(retTy));
233 +
                }
234 +
            }
235 +
        }
236 +
        case super::Instr::Ecall { dst, .. } => {
237 +
            // Ecall returns a word value.
238 +
            setReg(regs, dst, RegType::Word(super::Type::W64));
239 +
        }
240 +
        case super::Instr::Ret { .. },
241 +
             super::Instr::Jmp { .. },
242 +
             super::Instr::Br { .. },
243 +
             super::Instr::Switch { .. },
244 +
             super::Instr::Unreachable,
245 +
             super::Instr::Ebreak => {}
246 +
    }
247 +
}
248 +
249 +
/// Set a register's type in the map.
250 +
fn setReg(regs: *mut [RegType], reg: super::Reg, typ: RegType) {
251 +
    if reg.n < regs.len {
252 +
        regs[reg.n] = typ;
253 +
    }
254 +
}
255 +
256 +
/// Check that a register used as a memory base has type Ptr.
257 +
fn checkPtrBase(
258 +
    func: *super::Fn,
259 +
    blockIdx: u32,
260 +
    instrIdx: u32,
261 +
    regs: *[RegType],
262 +
    reg: super::Reg,
263 +
    errors: *mut *mut [VerifyError],
264 +
    a: alloc::Allocator,
265 +
    context: *[u8],
266 +
) {
267 +
    if reg.n >= regs.len {
268 +
        return;
269 +
    }
270 +
    if not isPtr(regs[reg.n]) {
271 +
        emitError(func, blockIdx, instrIdx, errors, a, context);
272 +
    }
273 +
}
274 +
275 +
/// Record a verification error.
276 +
fn emitError(
277 +
    func: *super::Fn,
278 +
    blockIdx: u32,
279 +
    instrIdx: u32,
280 +
    errors: *mut *mut [VerifyError],
281 +
    a: alloc::Allocator,
282 +
    message: *[u8],
283 +
) {
284 +
    errors.append(VerifyError {
285 +
        fnName: func.name,
286 +
        blockIdx,
287 +
        instrIdx,
288 +
        message,
289 +
    }, a);
290 +
}
291 +
292 +
/// Print verification results to stderr.
293 +
pub fn printResult(result: *VerifyResult) {
294 +
    if result.errors.len == 0 and result.wtpCount == 0 {
295 +
        io::printError("verify: ok\n");
296 +
        return;
297 +
    }
298 +
    let mut buf: [u8; 10] = undefined;
299 +
300 +
    if result.wtpCount > 0 {
301 +
        io::printError("verify: ");
302 +
        io::printError(fmt::formatU32(result.wtpCount, &mut buf[..]));
303 +
        io::printError(" WordToPtr instruction(s) found\n");
304 +
    }
305 +
    for err in result.errors {
306 +
        io::printError("verify: ");
307 +
        io::printError(err.fnName);
308 +
        io::printError(" block ");
309 +
        io::printError(fmt::formatU32(err.blockIdx, &mut buf[..]));
310 +
        io::printError(" instr ");
311 +
        io::printError(fmt::formatU32(err.instrIdx, &mut buf[..]));
312 +
        io::printError(": ");
313 +
        io::printError(err.message);
314 +
        io::printError("\n");
315 +
    }
316 +
}
std.lib +1 -0
23 23
lib/std/lang/ast/printer.rad
24 24
lib/std/lang/scanner.rad
25 25
lib/std/lang/parser.rad
26 26
lib/std/lang/il.rad
27 27
lib/std/lang/il/printer.rad
28 +
lib/std/lang/il/verify.rad
28 29
lib/std/lang/resolver.rad
29 30
lib/std/lang/resolver/printer.rad
30 31
lib/std/lang/lower.rad
31 32
lib/std/lang/module.rad
32 33
lib/std/lang/module/printer.rad