Update RIL safety TODO: reflect current state and future work

01ef2d04caecd3c041c447b1ce05575921475bae778c0cff6d2a1059fd6c7ecf
Rewrite TODO to document what's implemented (Ptr type, Elem, unsafe,
verifier with bounds tracking) and what remains (first-class slices
for static trap-freedom, global data bounds, exokernel regions).
Alexis Sellier committed ago 1 parent 86e83beb
.ai/RIL.TODO.md +101 -304
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)
1 +
# RIL Safety Verification
2 +
3 +
## Done
4 +
5 +
### Ptr type (Phase 1-2)
6 +
`il::Type::Ptr` distinct from `W64`. All memory ops use `Ptr` for base
7 +
registers. Loads/stores of pointer values use `Ptr` as the type.
8 +
Backend treats `Ptr` identically to `W64` for codegen.
9 +
10 +
### Explicit casts (Phase 3)
11 +
`PtrToWord` (`ptw`) and `WordToPtr` (`wtp`) make pointer/integer
12 +
boundary crossings explicit. No silent bitcasts.
13 +
14 +
### Provenance rules (Phase 4)
15 +
Closed set of pointer-producing instructions documented in `il.rad`:
16 +
Reserve, Copy(DataSym/FnAddr), Load with Ptr type, Elem, BinOp::Add
17 +
on Ptr, Call returning Ptr, block params, WordToPtr.
18 +
19 +
### unsafe keyword (Phase 5)
20 +
`unsafe fn` and `unsafe { }` blocks. Integer-to-pointer casts
21 +
(`addr as *T`) only allowed in unsafe context. Resolver enforces.
22 +
23 +
### Elem instruction (Phase 6 partial)
24 +
Bounds-checked element pointer: `elem %dst %base %idx %len stride`.
25 +
Backend lowers to `bltu + ebreak + mul + add`. Verifier checks
26 +
locally without CFG analysis. Used for all user-facing array/slice
27 +
indexing, for-loop iteration, append, delete, fill, byte copy.
28 +
29 +
### Verifier (Phase 7)
30 +
SSA type checker in `il/verify.rad`. Tracks `Ptr(bound)` per register.
31 +
Checks:
32 +
- Memory op bases are Ptr
33 +
- Static offset access within allocation bounds
34 +
- Elem base is Ptr, result is Ptr(stride)
35 +
- WordToPtr counted as provenance escape
36 +
37 +
## Remaining gaps
38 +
39 +
### Slice re-slicing uses unchecked `add ptr`
40 +
`&slice[start..end]` offsets the data pointer via `add ptr` with no
41 +
bounds check. This is safe in practice because the resulting pointer
42 +
is only used as a slice data pointer — all access goes through `Elem`.
43 +
But untrusted IL could forge a slice pointing anywhere by constructing
44 +
an `add ptr` with an arbitrary offset, then wrapping it in a slice
45 +
struct.
46 +
47 +
### Constant-index array access uses unchecked `add ptr`
48 +
When the resolver validates a constant array index at compile time,
49 +
the lowerer emits plain `add ptr`. The verifier checks this statically
50 +
against the allocation bound from `Reserve`. This is sound for known
51 +
allocations but `Ptr(0)` (unknown bound) skips the check.
52 +
53 +
### Pointers from memory/params/calls have unknown bounds
54 +
`load ptr`, function parameters, and call return values produce
55 +
`Ptr(0)`. The verifier can't check accesses through them. All
56 +
indexing goes through `Elem` which bounds-checks at runtime.
168 57
169 58
---
170 59
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.
60 +
## Future: First-class slices in IL
179 61
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 -
---
62 +
The current model relies on runtime bounds checking (`Elem`) as the
63 +
last line of defense. Every element access is checked, so forging a
64 +
slice can't access arbitrary memory — the worst case is a bounds-check
65 +
trap. This is the WebAssembly model.
195 66
196 -
## Phase 6: Bounds Verification (Future)
67 +
For static verification (proving at load time that no trap can occur),
68 +
slices need to be a first-class IL concept:
197 69
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 70
```
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
71 +
SliceNew  %slice %base %len %cap %stride   ;; construct from known alloc
72 +
SlicePtr  %ptr %slice                       ;; extract data pointer
73 +
SliceLen  %len %slice                       ;; extract length
74 +
SliceElem %ptr %slice %idx                  ;; bounds-checked element
227 75
```
228 76
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).
77 +
### Why this helps
263 78
264 -
### 6.4 Explicit region types (exokernel)
79 +
`SliceNew` is the only way to construct a slice. The verifier checks
80 +
that `%base` is `Ptr(B)` and `len * stride <= B` -- the allocation is
81 +
large enough for the claimed length. After construction, `SliceElem`
82 +
is provably safe because the length was validated against the
83 +
allocation.
265 84
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.
85 +
An attacker can't forge a `(ptr, len)` pair by storing to memory --
86 +
the only path to a slice value is `SliceNew`, which the verifier
87 +
validates.
270 88
271 -
These would be additional provenance classes that the kernel
272 -
verifier checks against the domain's capability set.
89 +
### What it costs
273 90
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 -
---
91 +
- Slices stop being plain `{ptr, len, cap}` memory layouts. They
92 +
  become opaque IL values.
93 +
- Every slice operation becomes a dedicated instruction.
94 +
- The lowerer must emit `SliceNew` instead of manual stores.
95 +
- Re-slicing becomes `SliceSlice %new %old %start %end` with the
96 +
  verifier checking `start <= end <= old.len`.
97 +
- More complex IL, simpler verifier.
302 98
303 -
## Implementation Order
99 +
### Trade-off
304 100
305 -
Recommended sequence, each step independently useful:
101 +
With `Elem` on every access, the current model is already safe against
102 +
memory corruption -- the worst outcome of a forged slice is a trap.
103 +
First-class slices would additionally prove no trap can occur, enabling
104 +
trap-free execution in verified domains.
306 105
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.
106 +
Whether this is needed depends on the exokernel's requirements:
107 +
- If domains are allowed to trap (and the kernel handles it), the
108 +
  current model is sufficient.
109 +
- If domains must be proven trap-free, first-class slices are needed.
314 110
315 111
---
316 112
317 -
## Key Invariant
113 +
## Future: Global data bounds
318 114
319 -
After all phases, the following must hold:
115 +
`Copy(DataSym)` currently produces `Ptr(0)`. The verifier could look
116 +
up the `Data` entry in `Program.data` to get the size and produce
117 +
`Ptr(size)`. Straightforward extension, not yet implemented.
320 118
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.
119 +
## Future: Exokernel region types
325 120
326 -
This is the RIL-level encoding of the exokernel's security guarantee.
121 +
`PagePtr(handle, offset)`, `DevicePtr(handle, offset)`, etc. as
122 +
provenance classes checked against the domain's capability set.
123 +
Needs the exokernel's capability model to be defined first.