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}