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}