code wiki / (root) / nx_cap.nx

nx_cap.nx source

↩ module page · 147 lines · 5243 B

1// cap.nx -- capability-based security primitives. 2// 3// EFFICIENCY_ROADMAP ยง5.4. Today every .nx file can call any 4// syscall (sys_execve, sys_openat, etc.) because the function 5// symbols are linker-resolved with no access control. That's 6// fine for a single-author project; it's a disaster once we 7// import third-party code. 8// 9// This module ships capability tokens that can be REQUIRED at 10// function-signature level. A function that wants to open a 11// file takes a `*CapFS` argument; if the caller didn't get that 12// capability from the root process, they can't forge one (we 13// don't expose a fake constructor). 14// 15// Three delivery phases: 16// 17// A (this file): capability types + a root-only constructor. 18// Library code adopting `fs_open(cap: *CapFS, path: *u8)` 19// becomes sandbox-aware immediately; old `fs_open(path)` 20// callers keep working until migration is complete. 21// 22// B (parse.nx change, pending): `@requires(CapFS)` function 23// attribute lifts this into the type system. Compiler 24// refuses to compile a function that calls fs_open without 25// a CapFS in scope. 26// 27// C (import.nx change, pending): every import statement 28// declares what caps it needs. main.c picks them up + 29// ensures the top-level main has the superset granted. 30// 31// Invariants: 32// CAP1 Capabilities can NOT be forged -- only created through 33// cap_root_grant which the root/init code holds. 34// CAP2 A capability can be narrowed (cap_narrow returns a 35// strictly weaker capability). 36// CAP3 Capabilities can be revoked; revoked caps reject all 37// operations + can't be re-enabled. 38 39// nx_safety_envelope: 40// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 41// sil_target: SIL1 42// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 43// verdict: NOT_YET_EVALUATED 44 45import "nx_syscalls.nx" 46 47// Capability domain identifiers. Each one controls a bucket of 48// syscalls. Expand as new subsystems land. 49const CAP_FS: i64 = 1 50const CAP_NET: i64 = 2 51const CAP_PROC: i64 = 3 // fork/exec 52const CAP_CLOCK: i64 = 4 // reading wall-clock time 53const CAP_RANDOM: i64 = 5 54const CAP_ENV: i64 = 6 // environment variables 55const CAP_USER: i64 = 7 // setuid/setgid-class ops 56 57// Action masks -- fine-grained controls inside a domain. Per- 58// domain semantics detailed in docs/CAPABILITIES.md (future). 59const CAP_FS_READ: i64 = 0x01 60const CAP_FS_WRITE: i64 = 0x02 61const CAP_FS_CREATE: i64 = 0x04 62const CAP_FS_DELETE: i64 = 0x08 63const CAP_FS_ALL: i64 = 0x0F 64 65const CAP_NET_BIND: i64 = 0x01 66const CAP_NET_CONNECT: i64 = 0x02 67const CAP_NET_LISTEN: i64 = 0x04 68const CAP_NET_ALL: i64 = 0x07 69 70const CAP_ERR_REVOKED: i64 = -1 71const CAP_ERR_INSUFFICIENT: i64 = -2 72 73// Core capability record. Opaque to library consumers; only 74// cap.nx itself pokes the fields directly. 75struct Cap { 76 domain: i64, // CAP_FS / CAP_NET / ... 77 actions: i64, // bitmask of allowed actions 78 valid: i64, // 0 = revoked; 1 = live 79 parent: *Cap, // who delegated this cap to us (0 if root) 80} 81 82// Grant a capability. Only the ROOT init code should call this. 83// Subsequent sandboxed code derives caps via cap_narrow. 84func cap_root_grant(domain: i64, actions: i64) -> *Cap { 85 let raw: *u8 = sys_mmap(32) 86 let c: *Cap = raw as *Cap 87 c.domain = domain 88 c.actions = actions 89 c.valid = 1 90 c.parent = 0 as *Cap 91 return c 92} 93 94// Derive a weaker capability. New cap can only have a subset 95// of the parent's actions. 96func cap_narrow(parent: *Cap, actions: i64) -> *Cap { 97 let raw: *u8 = sys_mmap(32) 98 let c: *Cap = raw as *Cap 99 c.domain = parent.domain 100 c.actions = actions & parent.actions 101 c.valid = 1 102 c.parent = parent 103 return c 104} 105 106// Permanently revoke a capability. Irreversible. 107func cap_revoke(c: *Cap) -> i64 { 108 c.valid = 0 109 c.actions = 0 110 return 0 111} 112 113// Check whether a cap is valid + has the requested actions. 114// Library functions call this at every gate. 115func cap_check(c: *Cap, domain: i64, actions: i64) -> i64 { 116 if c == (0 as *Cap) { return CAP_ERR_INSUFFICIENT } 117 if c.valid == 0 { return CAP_ERR_REVOKED } 118 if c.domain != domain { return CAP_ERR_INSUFFICIENT } 119 if (c.actions & actions) != actions { return CAP_ERR_INSUFFICIENT } 120 return 0 121} 122 123// Compile-only smoke. 124func main() -> i64 { 125 // Root grants full FS access. 126 let root_fs: *Cap = cap_root_grant(CAP_FS, CAP_FS_ALL) 127 if cap_check(root_fs, CAP_FS, CAP_FS_READ) != 0 { return 1 } 128 if cap_check(root_fs, CAP_FS, CAP_FS_WRITE) != 0 { return 2 } 129 130 // Narrow to read-only + check write is denied. 131 let ro: *Cap = cap_narrow(root_fs, CAP_FS_READ) 132 if cap_check(ro, CAP_FS, CAP_FS_READ) != 0 { return 3 } 133 if cap_check(ro, CAP_FS, CAP_FS_WRITE) != CAP_ERR_INSUFFICIENT { 134 return 4 135 } 136 137 // Wrong-domain check. 138 if cap_check(root_fs, CAP_NET, CAP_NET_BIND) != CAP_ERR_INSUFFICIENT { 139 return 5 140 } 141 142 // Revoke + re-check. 143 cap_revoke(ro) 144 if cap_check(ro, CAP_FS, CAP_FS_READ) != CAP_ERR_REVOKED { return 6 } 145 146 return 0 147}