code wiki / (root) / cap.nx

cap.nx source

↩ module page · 141 lines · 5130 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 39import "syscalls.nx" 40 41// Capability domain identifiers. Each one controls a bucket of 42// syscalls. Expand as new subsystems land. 43const CAP_FS: i64 = 1 44const CAP_NET: i64 = 2 45const CAP_PROC: i64 = 3 // fork/exec 46const CAP_CLOCK: i64 = 4 // reading wall-clock time 47const CAP_RANDOM: i64 = 5 48const CAP_ENV: i64 = 6 // environment variables 49const CAP_USER: i64 = 7 // setuid/setgid-class ops 50 51// Action masks -- fine-grained controls inside a domain. Per- 52// domain semantics detailed in docs/CAPABILITIES.md (future). 53const CAP_FS_READ: i64 = 0x01 54const CAP_FS_WRITE: i64 = 0x02 55const CAP_FS_CREATE: i64 = 0x04 56const CAP_FS_DELETE: i64 = 0x08 57const CAP_FS_ALL: i64 = 0x0F 58 59const CAP_NET_BIND: i64 = 0x01 60const CAP_NET_CONNECT: i64 = 0x02 61const CAP_NET_LISTEN: i64 = 0x04 62const CAP_NET_ALL: i64 = 0x07 63 64const CAP_ERR_REVOKED: i64 = -1 65const CAP_ERR_INSUFFICIENT: i64 = -2 66 67// Core capability record. Opaque to library consumers; only 68// cap.nx itself pokes the fields directly. 69struct Cap { 70 domain: i64, // CAP_FS / CAP_NET / ... 71 actions: i64, // bitmask of allowed actions 72 valid: i64, // 0 = revoked; 1 = live 73 parent: *Cap, // who delegated this cap to us (0 if root) 74} 75 76// Grant a capability. Only the ROOT init code should call this. 77// Subsequent sandboxed code derives caps via cap_narrow. 78func cap_root_grant(domain: i64, actions: i64) -> *Cap { 79 let raw: *u8 = sys_mmap(32) 80 let c: *Cap = raw as *Cap 81 c.domain = domain 82 c.actions = actions 83 c.valid = 1 84 c.parent = 0 as *Cap 85 return c 86} 87 88// Derive a weaker capability. New cap can only have a subset 89// of the parent's actions. 90func cap_narrow(parent: *Cap, actions: i64) -> *Cap { 91 let raw: *u8 = sys_mmap(32) 92 let c: *Cap = raw as *Cap 93 c.domain = parent.domain 94 c.actions = actions & parent.actions 95 c.valid = 1 96 c.parent = parent 97 return c 98} 99 100// Permanently revoke a capability. Irreversible. 101func cap_revoke(c: *Cap) -> i64 { 102 c.valid = 0 103 c.actions = 0 104 return 0 105} 106 107// Check whether a cap is valid + has the requested actions. 108// Library functions call this at every gate. 109func cap_check(c: *Cap, domain: i64, actions: i64) -> i64 { 110 if c == (0 as *Cap) { return CAP_ERR_INSUFFICIENT } 111 if c.valid == 0 { return CAP_ERR_REVOKED } 112 if c.domain != domain { return CAP_ERR_INSUFFICIENT } 113 if (c.actions & actions) != actions { return CAP_ERR_INSUFFICIENT } 114 return 0 115} 116 117// Compile-only smoke. 118func main() -> i64 { 119 // Root grants full FS access. 120 let root_fs: *Cap = cap_root_grant(CAP_FS, CAP_FS_ALL) 121 if cap_check(root_fs, CAP_FS, CAP_FS_READ) != 0 { return 1 } 122 if cap_check(root_fs, CAP_FS, CAP_FS_WRITE) != 0 { return 2 } 123 124 // Narrow to read-only + check write is denied. 125 let ro: *Cap = cap_narrow(root_fs, CAP_FS_READ) 126 if cap_check(ro, CAP_FS, CAP_FS_READ) != 0 { return 3 } 127 if cap_check(ro, CAP_FS, CAP_FS_WRITE) != CAP_ERR_INSUFFICIENT { 128 return 4 129 } 130 131 // Wrong-domain check. 132 if cap_check(root_fs, CAP_NET, CAP_NET_BIND) != CAP_ERR_INSUFFICIENT { 133 return 5 134 } 135 136 // Revoke + re-check. 137 cap_revoke(ro) 138 if cap_check(ro, CAP_FS, CAP_FS_READ) != CAP_ERR_REVOKED { return 6 } 139 140 return 0 141}