Update Phase 6 design: dominance-based bounds verification

f583c4173f8a880dae8c16710003bede648917c1ea02ac365a791d93936dcea3
Replace the earlier vague Phase 6 sketch with a concrete design.

Key decision: bounds checks stay in the lowerer (which has the most
optimization context). The verifier proves safety through dominance
analysis without inserting or moving checks.

Two tiers:
- Static offsets (field access): purely local, check const offset
  against known allocation size. No CFG analysis.
- Dynamic offsets (array indexing): verifier computes dominator tree,
  tracks bounds facts from br.ult instructions, and connects them
  to pointer derivations via provenance table.
Alexis Sellier committed ago 1 parent c22390f5
.ai/RIL.TODO.md added +326 -0
1 +
# RIL Verifiability: Proposed Lowerer and IL Changes
2 +
3 +
Based on `notes/ril-security-requirements.md`. The goal is to make RIL
4 +
statically verifiable for memory safety, supporting the exokernel's
5 +
trusted-domain guarantees.
6 +
7 +
## Current State
8 +
9 +
Today, RIL has four scalar types: `W8`, `W16`, `W32`, `W64`. Pointers are
10 +
`W64` -- indistinguishable from integers. Memory instructions (`Load`,
11 +
`Store`, `Blit`) take plain `Reg` operands as addresses. There is no
12 +
provenance tracking, no bounds information, and nothing preventing an
13 +
integer from being used as a memory base. The `lowerCast` / `ilType`
14 +
functions in the lowerer collapse `resolver::Type::Pointer` to `il::Type::W64`,
15 +
erasing pointer identity entirely.
16 +
17 +
---
18 +
19 +
## Phase 1: Pointer/Integer Type Distinction in IL
20 +
21 +
### 1.1 Add `Ptr` to `il::Type`
22 +
23 +
```
24 +
pub union Type {
25 +
    W8, W16, W32, W64,
26 +
    Ptr,        // New: pointer-width value with provenance
27 +
}
28 +
```
29 +
30 +
`Ptr` is the same physical width as `W64` but carries different semantic
31 +
meaning. The verifier treats them as incompatible types.
32 +
33 +
**Files:** `lib/std/lang/il.rad`
34 +
35 +
- Add `Ptr` variant to `Type`.
36 +
- Update `typeSize` to return 8 for `Ptr`.
37 +
38 +
### 1.2 Update `ilType` in the lowerer
39 +
40 +
Change the lowerer's `ilType` function so `resolver::Type::Pointer`,
41 +
`resolver::Type::Slice` (which contains a pointer), and
42 +
`resolver::Type::Fn` map to `il::Type::Ptr` instead of `il::Type::W64`.
43 +
44 +
**Files:** `lib/std/lang/lower.rad` (function `ilType`, ~line 6974)
45 +
46 +
### 1.3 Update the IL printer
47 +
48 +
Teach the IL printer to emit `ptr` for the new type variant.
49 +
50 +
**Files:** `lib/std/lang/il/printer.rad`
51 +
52 +
### 1.4 Update the backend
53 +
54 +
The RV64 backend (`isel.rad`, `emit.rad`) must treat `Ptr` identically to
55 +
`W64` for code generation -- same register class, same instructions. The
56 +
distinction is purely for the verifier, not for codegen.
57 +
58 +
**Files:** `lib/std/arch/rv64/isel.rad`, `lib/std/arch/rv64/emit.rad`
59 +
60 +
---
61 +
62 +
## Phase 2: Typed Memory Instruction Operands
63 +
64 +
### 2.1 Enforce pointer-typed bases on memory ops
65 +
66 +
Currently:
67 +
```
68 +
Load  { typ, dst: Reg, src: Reg, offset }
69 +
Store { typ, src: Val, dst: Reg, offset }
70 +
Blit  { dst: Reg, src: Reg, size: Val }
71 +
```
72 +
73 +
The `src`/`dst` address registers are untyped. A verifier pass should
74 +
reject any `Load`/`Store`/`Blit` whose base register was not defined by
75 +
a pointer-producing instruction.
76 +
77 +
This does NOT require changing the `Instr` union itself (registers are
78 +
untyped in the encoding). Instead, a **type map** is maintained:
79 +
each SSA register is assigned either a word type or `Ptr`. The verifier
80 +
checks that `Load.src`, `Store.dst`, and `Blit.{dst,src}` registers
81 +
have type `Ptr`.
82 +
83 +
### 2.2 Lowerer must produce consistently typed registers
84 +
85 +
Update the lowerer so that every register holding a pointer is produced by
86 +
a `Ptr`-typed instruction. Key sites:
87 +
88 +
- `emitReserve` -- `Reserve` produces a stack pointer: its result type
89 +
  should be recorded as `Ptr`.
90 +
- `emitDataAddr` / `emitFnAddr` -- `Copy` of a `DataSym` or `FnAddr`
91 +
  produces a `Ptr`.
92 +
- `emitLoad` for pointer-typed fields -- `Load` of a pointer field
93 +
  returns a `Ptr`-typed register.
94 +
- Address arithmetic (field offsets, array indexing via `BinOp::Add` on
95 +
  a pointer base) -- result is `Ptr`.
96 +
- `buildSliceValue` -- the pointer component is `Ptr`, the length/cap
97 +
  components are `W32`.
98 +
99 +
**Files:** `lib/std/lang/lower.rad` (many functions; grep for
100 +
`emitReserve`, `emitDataAddr`, `emitFnAddr`, `emitLoad`, `emitRead`,
101 +
`buildSliceValue`, `lowerIndex`, `lowerFieldAccess`)
102 +
103 +
---
104 +
105 +
## Phase 3: Restrict Integer-to-Pointer Conversion
106 +
107 +
### 3.1 Forbid `int as *T` in the lowerer
108 +
109 +
The lowerer's `lowerCast` currently delegates to `lowerNumericCast` for
110 +
all same-size casts, which is a no-op. For `int -> ptr`, it must instead
111 +
emit a dedicated **`IntToPtr`** IL instruction (or reject it outright).
112 +
113 +
Add to `il::Instr`:
114 +
```
115 +
IntToPtr { dst: Reg, val: Val },   // Explicit, auditable
116 +
PtrToInt { dst: Reg, val: Val },   // Pointer -> integer (one-way)
117 +
```
118 +
119 +
- `PtrToInt` produces a `W64`. The resulting integer CANNOT be fed back
120 +
  into memory ops.
121 +
- `IntToPtr` produces a `Ptr`. The verifier rejects this in user code;
122 +
  it is only permitted in `@trusted` functions (see Phase 5).
123 +
124 +
### 3.2 Update `lowerCast`
125 +
126 +
When source is an integer type and destination is `Pointer`, emit
127 +
`IntToPtr` instead of silently treating the value as compatible.
128 +
When source is `Pointer` and destination is an integer, emit `PtrToInt`.
129 +
130 +
**Files:** `lib/std/lang/lower.rad` (`lowerCast`, `lowerNumericCast`)
131 +
132 +
---
133 +
134 +
## Phase 4: Provenance-Producing Instructions
135 +
136 +
### 4.1 Define the closed set of pointer-producing operations
137 +
138 +
A verifier must be able to enumerate every instruction that can produce
139 +
a `Ptr`-typed result. The exhaustive list:
140 +
141 +
| Instruction       | Provenance source                        |
142 +
|--------------------|------------------------------------------|
143 +
| `Reserve`          | Stack allocation (local provenance)      |
144 +
| `Copy(DataSym)`    | Global/static data address               |
145 +
| `Copy(FnAddr)`     | Function address (code section)          |
146 +
| `Load` of ptr field| Derived from an existing pointer         |
147 +
| `BinOp::Add(ptr,i)`| Pointer arithmetic (derived)             |
148 +
| `Call` returning ptr| Callee-produced pointer                 |
149 +
| Block parameter    | Merges pointer values from predecessors  |
150 +
| `IntToPtr`         | Trusted-only escape hatch                |
151 +
152 +
Any other instruction producing a register used as a memory base is a
153 +
verifier error.
154 +
155 +
### 4.2 Pointer arithmetic constraints
156 +
157 +
When emitting `BinOp::Add` for pointer offset computation (field access,
158 +
array indexing), the lowerer should tag the result as `Ptr`. The verifier
159 +
checks:
160 +
- Exactly one operand is `Ptr`, the other is an integer.
161 +
- The result is `Ptr` with the same provenance as the pointer operand.
162 +
163 +
`BinOp::Sub` between two pointers produces an integer (`W64`), not a
164 +
pointer. This is the `ptr - ptr -> usize` pattern.
165 +
166 +
**Files:** `lib/std/lang/lower.rad` (field/index lowering),
167 +
`lib/std/lang/il.rad` (documentation)
168 +
169 +
---
170 +
171 +
## Phase 5: Trusted Boundary Annotation
172 +
173 +
### 5.1 Add `@trusted` function attribute
174 +
175 +
Functions that must perform raw address manipulation (syscall wrappers,
176 +
allocator internals, kernel glue) are annotated `@trusted`. The verifier
177 +
relaxes pointer-provenance checks inside `@trusted` functions but tracks
178 +
them as trust boundaries.
179 +
180 +
This requires:
181 +
- Scanner/parser support for the `@trusted` attribute.
182 +
- A `trusted: bool` field on `il::Fn`.
183 +
- The verifier skipping `IntToPtr` rejection inside trusted functions.
184 +
185 +
**Files:** `lib/std/lang/scanner.rad`, `lib/std/lang/parser.rad`,
186 +
`lib/std/lang/resolver.rad`, `lib/std/lang/lower.rad`, `lib/std/lang/il.rad`
187 +
188 +
### 5.2 Confine `ecall` to trusted functions
189 +
190 +
`Ecall` passes raw register values to the environment. A verifier should
191 +
require that `Ecall` only appears in `@trusted` functions, since the
192 +
syscall interface is an inherent trust boundary.
193 +
194 +
---
195 +
196 +
## Phase 6: Bounds Verification (Future)
197 +
198 +
The lowerer emits bounds checks as explicit IL (compare + branch to
199 +
trap). The verifier must prove that every memory access is covered by
200 +
a bounds check, without moving or inserting checks itself.
201 +
202 +
### Design Principle
203 +
204 +
Bounds checks stay in the lowerer. The lowerer has the most context
205 +
for optimization: it knows when a check is redundant (constant index
206 +
into a known-size array), when to hoist (loop over `0..len`), when
207 +
two accesses share one check. The verifier is purely an analysis
208 +
pass that confirms the lowerer's output is safe.
209 +
210 +
### 6.1 Static offsets (no CFG analysis needed)
211 +
212 +
Field access produces `add ptr %field %base <const>`. The verifier
213 +
tracks each `Ptr` register's allocation size (from `Reserve`).
214 +
For constant offsets: check `offset + access_size <= alloc_size`.
215 +
This covers record fields, tagged union payloads, slice header
216 +
fields -- the bulk of pointer derivations.
217 +
218 +
### 6.2 Dynamic offsets (requires dominance analysis)
219 +
220 +
Array/slice indexing produces:
221 +
```
222 +
load w32 %len %slice 8            ;; load length
223 +
br.ult %idx %len @ok @trap        ;; bounds check
224 +
@ok
225 +
mul w64 %off %idx %stride
226 +
add ptr %elem %base %off           ;; derive element pointer
227 +
```
228 +
229 +
The verifier must prove that the `add ptr` is dominated by a
230 +
`br.ult` that establishes `%idx < %len`, and that `%base` is
231 +
valid for `%len * %stride` bytes.
232 +
233 +
This requires:
234 +
1. **Dominator tree** -- computed from the block predecessor
235 +
   lists already in the IL. Standard algorithm, linear time.
236 +
2. **Bounds facts** -- at each `br.ult %a %b @pass @fail`, the
237 +
   verifier records that `%a < %b` holds on the @pass edge.
238 +
3. **Provenance table** -- each `Ptr` register carries
239 +
   `(origin, bound)` where origin is a `Reserve`/`DataSym`/
240 +
   parameter ID and bound is the allocation size or a register
241 +
   holding the dynamic length.
242 +
4. **Access check** -- for each `Load`/`Store`/`Blit`, verify
243 +
   that the base pointer's derived offset is covered by a
244 +
   dominating bounds fact against the same bound.
245 +
246 +
For a `Reserve { size: N }`, the bound is the constant `N`.
247 +
For a slice data pointer, the bound is `len * stride` where
248 +
`len` was loaded from the same slice header.
249 +
250 +
### 6.3 Provenance tracking
251 +
252 +
The verifier maintains a side-table mapping each `Ptr` register
253 +
to its provenance:
254 +
- **Origin**: which allocation or parameter it derives from.
255 +
- **Bound**: static size (for `Reserve`/globals) or the register
256 +
  holding the dynamic bound (for slice data pointers).
257 +
- **Offset**: accumulated constant offset from the origin base.
258 +
259 +
Derived pointers inherit their parent's provenance:
260 +
- `add ptr %dst %base <imm>` -- same origin, offset += imm.
261 +
- `add ptr %dst %base %dynoff` -- same origin, offset is dynamic
262 +
  (must be covered by a dominating bounds check).
263 +
264 +
### 6.4 Explicit region types (exokernel)
265 +
266 +
For the exokernel use case, extend the provenance model:
267 +
- `PagePtr(handle, offset)` -- pointer into a granted page.
268 +
- `DevicePtr(handle, offset)` -- pointer to a device mapping.
269 +
- `PortalPtr(handle, field)` -- pointer to a cross-domain portal.
270 +
271 +
These would be additional provenance classes that the kernel
272 +
verifier checks against the domain's capability set.
273 +
274 +
### Implementation cost
275 +
276 +
Dominance computation and dataflow tracking are standard SSA
277 +
algorithms. The main engineering work is connecting bounds facts
278 +
to pointer derivations across blocks. This is a substantial but
279 +
bounded effort -- probably 500-1000 lines of verifier code.
280 +
281 +
---
282 +
283 +
## Phase 7: Verifier Implementation
284 +
285 +
### 7.1 SSA type checker
286 +
287 +
Walk each function's blocks in order. Maintain a type map
288 +
`Reg -> Type` (where `Type` includes `Ptr`). For each instruction:
289 +
1. Record the destination register's type.
290 +
2. For `Load`/`Store`/`Blit`, check that the base register has type `Ptr`.
291 +
3. For `IntToPtr`, check that the function is `@trusted`.
292 +
4. For `BinOp` with a `Ptr` operand, check the arithmetic constraints.
293 +
294 +
### 7.2 Provenance checker (future)
295 +
296 +
Extend the type checker with provenance sets. Each `Ptr` register carries
297 +
a provenance tag. Derived pointers inherit their parent's tag. The
298 +
verifier rejects memory access through a pointer whose provenance is not
299 +
in the current domain's authorized set.
300 +
301 +
---
302 +
303 +
## Implementation Order
304 +
305 +
Recommended sequence, each step independently useful:
306 +
307 +
1. **Phase 1** (type distinction) -- smallest change, enables all later work.
308 +
2. **Phase 2** (typed memory ops) -- makes the lowerer provenance-aware.
309 +
3. **Phase 3** (cast restrictions) -- closes the int-to-ptr hole.
310 +
4. **Phase 7.1** (basic verifier) -- can now check real programs.
311 +
5. **Phase 5** (trusted boundaries) -- needed before restricting stdlib.
312 +
6. **Phase 4** (formalize pointer producers) -- refine verifier rules.
313 +
7. **Phase 6** (bounds/provenance) -- full exokernel support.
314 +
315 +
---
316 +
317 +
## Key Invariant
318 +
319 +
After all phases, the following must hold:
320 +
321 +
> Every `Load`, `Store`, or `Blit` instruction operates through a register
322 +
> of type `Ptr`, whose provenance traces back to an authorized allocation
323 +
> or grant, and whose derived offset is within the bounds of that
324 +
> allocation.
325 +
326 +
This is the RIL-level encoding of the exokernel's security guarantee.