code wiki / (root) / memcap.nx

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}