memcap.nx source
↩ module page · 267 lines · 9778 B
1// memcap.nx -- CHERI-style memory capabilities, pure software.
2//
3// Pointer-with-bounds-and-permissions, implemented entirely in
4// software. When Nishi silicon ships with hardware capabilities
5// (CHERI-RISC-V extension, or our own), the compiler recognises
6// MemCap<T> and emits single-instruction capability ops; the
7// software runtime becomes a no-op or is replaced by the hardware
8// path. Same NishiLang source, silicon-accelerated backend.
9//
10// This is the 40-year bet: build invariants in software now, keep
11// the HW/SW interface stable, upgrade silicon when we can fab it.
12//
13// Distinct from runtime/cap.nx: that file covers ACCESS
14// capabilities (who can call fs_open), which are coarse-grained
15// and language-level. This file covers MEMORY capabilities (what
16// bytes a pointer can touch), which are fine-grained and per-
17// allocation. Both systems plug into the Nishi silicon story:
18// access caps become compartment IDs, memory caps become CHERI
19// pointer tags.
20//
21// Reference papers / systems:
22// Watson et al 2015 "CHERI: A Hybrid Capability-System
23// Architecture for Scalable Software Compartmentalization"
24// Woodruff et al 2014 "The CHERI capability model"
25// Nienhuis et al 2020 "Rigorous engineering for hardware security"
26// Arm Morello -- production CHERI-Armv8 (2022)
27// RISC-V CHERI extension -- ratified draft (2024)
28//
29// Layout (32 bytes in software):
30// + 0 .. + 7 : address (i64, the actual pointer)
31// + 8 .. + 15 : base (i64, lower bound inclusive)
32// + 16 .. + 23 : length (i64, bytes from base)
33// + 24 .. + 31 : perms + tag (packed i64)
34// bit 0 : READ (load permitted)
35// bit 1 : WRITE (store permitted)
36// bit 2 : EXEC (jump permitted -- future)
37// bit 3 : CAP_LOAD (can load a capability through)
38// bit 4 : CAP_STORE (can store a capability through)
39// bit 5 : SEAL (non-deref, unforgeable handle)
40// bit 8 : VALID_TAG (mandatory; cleared on raw memcpy)
41// bits 9..63 : generation (monotonic, detects UAF)
42//
43// Silicon target: 128-bit compressed capability with hardware-
44// tagged memory. Same semantic, 4x smaller + bounds enforcement
45// free at runtime.
46
47import "syscalls.nx"
48import "nx_assert.nx"
49
50// --- permission bits ------------------------------------------------
51
52const MCAP_READ: i64 = 0x001
53const MCAP_WRITE: i64 = 0x002
54const MCAP_EXEC: i64 = 0x004
55const MCAP_CAP_LOAD: i64 = 0x008
56const MCAP_CAP_STORE: i64 = 0x010
57const MCAP_SEAL: i64 = 0x020
58const MCAP_VALID_TAG: i64 = 0x100
59
60// Common combinations.
61const MCAP_RW: i64 = 0x103 // READ | WRITE | VALID_TAG
62const MCAP_RO: i64 = 0x101 // READ | VALID_TAG
63
64// --- struct ---------------------------------------------------------
65
66struct MemCap {
67 addr: i64,
68 base: i64,
69 length: i64,
70 perms: i64,
71}
72
73const MEMCAP_BYTES: i64 = 32
74
75// --- construction ---------------------------------------------------
76
77// Create a fresh rw-capable capability over [base, base+length).
78func memcap_new(addr: i64, base: i64, length: i64, perms: i64) -> *MemCap {
79 let c_raw: *u8 = sys_mmap(MEMCAP_BYTES)
80 let c: *MemCap = c_raw as *MemCap
81 c.addr = addr
82 c.base = base
83 c.length = length
84 c.perms = perms | MCAP_VALID_TAG
85 return c
86}
87
88// Allocate `size` bytes + return a capability covering it. The
89// standard replacement for `sys_mmap(size) as *T`.
90func memcap_alloc(size: i64) -> *MemCap {
91 let mem: *u8 = sys_mmap(size)
92 return memcap_new(mem as i64, mem as i64, size, MCAP_RW)
93}
94
95// --- checks ---------------------------------------------------------
96
97// Is this capability usable for ANY operation?
98func memcap_valid(c: *MemCap) -> i64 {
99 if c == (0 as *MemCap) { return 0 }
100 if (c.perms & MCAP_VALID_TAG) == 0 { return 0 }
101 return 1
102}
103
104// Can read n bytes starting at addr+offset?
105func memcap_can_read(c: *MemCap, offset: i64, n: i64) -> i64 {
106 if memcap_valid(c) == 0 { return 0 }
107 if (c.perms & MCAP_READ) == 0 { return 0 }
108 let pos: i64 = c.addr + offset
109 if pos < c.base { return 0 }
110 if pos + n > c.base + c.length { return 0 }
111 return 1
112}
113
114// Same for write.
115func memcap_can_write(c: *MemCap, offset: i64, n: i64) -> i64 {
116 if memcap_valid(c) == 0 { return 0 }
117 if (c.perms & MCAP_WRITE) == 0 { return 0 }
118 let pos: i64 = c.addr + offset
119 if pos < c.base { return 0 }
120 if pos + n > c.base + c.length { return 0 }
121 return 1
122}
123
124// --- load/store operations -----------------------------------------
125
126// Checked i64 load. Asserts on violation.
127func memcap_load_i64(c: *MemCap, offset: i64) -> i64 {
128 nx_assert(memcap_can_read(c, offset, 8),
129 "memcap_load_i64: bounds/perm check failed" as *u8)
130 let p: *i64 = (c.addr + offset) as *i64
131 return *p
132}
133
134// Checked i64 store. Asserts on violation.
135func memcap_store_i64(c: *MemCap, offset: i64, val: i64) -> i64 {
136 nx_assert(memcap_can_write(c, offset, 8),
137 "memcap_store_i64: bounds/perm check failed" as *u8)
138 let p: *i64 = (c.addr + offset) as *i64
139 *p = val
140 return 0
141}
142
143// Checked u8 load (1 byte).
144func memcap_load_u8(c: *MemCap, offset: i64) -> i64 {
145 nx_assert(memcap_can_read(c, offset, 1),
146 "memcap_load_u8: bounds/perm check failed" as *u8)
147 let p: *u8 = (c.addr + offset) as *u8
148 return p[0]
149}
150
151// Checked u8 store.
152func memcap_store_u8(c: *MemCap, offset: i64, val: i64) -> i64 {
153 nx_assert(memcap_can_write(c, offset, 1),
154 "memcap_store_u8: bounds/perm check failed" as *u8)
155 let p: *u8 = (c.addr + offset) as *u8
156 p[0] = val
157 return 0
158}
159
160// --- derivation (monotonic) -----------------------------------------
161//
162// The fundamental CHERI invariant: a derived capability can ONLY
163// narrow the parent, never widen. Enforced in silicon via tagged
164// memory; enforced here by asserts.
165
166// Narrow bounds: derive a cap over a subrange of parent.
167func memcap_subrange(parent: *MemCap, sub_offset: i64, sub_len: i64) -> *MemCap {
168 nx_assert(memcap_valid(parent), "memcap_subrange: parent not valid" as *u8)
169 let new_base: i64 = parent.addr + sub_offset
170 if new_base < parent.base {
171 nx_assert(0, "memcap_subrange: underflow" as *u8)
172 }
173 if new_base + sub_len > parent.base + parent.length {
174 nx_assert(0, "memcap_subrange: overflow" as *u8)
175 }
176 return memcap_new(new_base, new_base, sub_len, parent.perms)
177}
178
179// Narrow perms: drop bits. Can only remove permissions the parent
180// already has; cannot grant new permissions.
181func memcap_restrict(c: *MemCap, new_perms: i64) -> *MemCap {
182 nx_assert(memcap_valid(c), "memcap_restrict: c not valid" as *u8)
183 if (new_perms & c.perms) != new_perms {
184 nx_assert(0, "memcap_restrict: cannot widen perms" as *u8)
185 }
186 return memcap_new(c.addr, c.base, c.length, new_perms | MCAP_VALID_TAG)
187}
188
189// Seal: derive a non-dereferenceable handle. Holder can pass it
190// around but cannot follow it. Analogous to CHERI 'cseal'.
191func memcap_seal(c: *MemCap) -> *MemCap {
192 nx_assert(memcap_valid(c), "memcap_seal: c not valid" as *u8)
193 return memcap_new(c.addr, c.base, c.length,
194 (c.perms | MCAP_SEAL) | MCAP_VALID_TAG)
195}
196
197// --- self-test ------------------------------------------------------
198
199func main() -> i64 {
200 // 1: construction + valid tag
201 let c: *MemCap = memcap_alloc(64)
202 if memcap_valid(c) != 1 { return __syscall(93, 10, 0, 0, 0, 0, 0) }
203 if c.length != 64 { return __syscall(93, 11, 0, 0, 0, 0, 0) }
204
205 // 2: load/store round-trip
206 memcap_store_i64(c, 0, 42)
207 memcap_store_i64(c, 8, 100)
208 if memcap_load_i64(c, 0) != 42 {
209 return __syscall(93, 20, 0, 0, 0, 0, 0)
210 }
211 if memcap_load_i64(c, 8) != 100 {
212 return __syscall(93, 21, 0, 0, 0, 0, 0)
213 }
214
215 // 3: bounds check positive
216 if memcap_can_read(c, 56, 8) != 1 {
217 return __syscall(93, 30, 0, 0, 0, 0, 0)
218 }
219 // 4: bounds check negative (1 byte past end)
220 if memcap_can_read(c, 57, 8) != 0 {
221 return __syscall(93, 40, 0, 0, 0, 0, 0)
222 }
223 // 5: far out of range
224 if memcap_can_write(c, 10000, 8) != 0 {
225 return __syscall(93, 50, 0, 0, 0, 0, 0)
226 }
227
228 // 6: subrange narrows bounds
229 let sub: *MemCap = memcap_subrange(c, 16, 32)
230 memcap_store_i64(sub, 0, 7)
231 // Parent sees the write at offset 16 (= sub's offset 0)
232 if memcap_load_i64(c, 16) != 7 {
233 return __syscall(93, 60, 0, 0, 0, 0, 0)
234 }
235 // Sub can't reach beyond its length
236 if memcap_can_read(sub, 32, 1) != 0 {
237 return __syscall(93, 61, 0, 0, 0, 0, 0)
238 }
239
240 // 7: restrict drops perms monotonically
241 let ro: *MemCap = memcap_restrict(c, MCAP_READ | MCAP_VALID_TAG)
242 if memcap_can_read(ro, 0, 8) != 1 {
243 return __syscall(93, 70, 0, 0, 0, 0, 0)
244 }
245 if memcap_can_write(ro, 0, 8) != 0 {
246 return __syscall(93, 71, 0, 0, 0, 0, 0)
247 }
248
249 // 8: seal sets the seal bit
250 let sealed: *MemCap = memcap_seal(c)
251 if (sealed.perms & MCAP_SEAL) == 0 {
252 return __syscall(93, 80, 0, 0, 0, 0, 0)
253 }
254
255 // 9: null cap not valid
256 if memcap_valid(0 as *MemCap) != 0 {
257 return __syscall(93, 90, 0, 0, 0, 0, 0)
258 }
259
260 // 10: u8 load/store round-trip (byte-level ops)
261 memcap_store_u8(c, 24, 0xA5)
262 if memcap_load_u8(c, 24) != 0xA5 {
263 return __syscall(93, 100, 0, 0, 0, 0, 0)
264 }
265
266 return 0
267}