nx_container.nx source
↩ module page · 202 lines · 7059 B
1// nx_container.nx -- sovereign container runtime (Docker replacement).
2//
3// One process per container. Substrate forks, child:
4// 1. unshare(CLONE_NEWUTS|CLONE_NEWPID|CLONE_NEWNS|CLONE_NEWIPC)
5// 2. mount root filesystem of the container image
6// 3. pivot_root or chroot into it
7// 4. mount /proc, /sys, /dev pseudo-fs inside the new root
8// 5. execve(entrypoint, argv)
9// Parent waits via sys_wait4 and returns the child's exit code.
10//
11// What it ISN'T (yet): network namespacing with veth pair, full
12// cgroupv2 resource accounting, image pull from a registry,
13// overlayfs union mounts. Those are L8b/c/d in the roadmap; each is
14// a named improvement queued behind the basic case.
15//
16// What it IS: a sovereign primitive that is enough to run our own
17// services (nishi_web, gitea, sshd) inside chrooted PID-namespace
18// isolation, with CAS-pinned root filesystems. No Docker daemon.
19// No /var/lib/docker. No vendored Go runtime. No yaml.
20//
21// Sealed enums per cardinal feedback-honest-perf-verdict.
22//
23// genealogy_id: linux_namespaces_2002 + lmctfy_2013 + runc_2016 +
24// oci_runtime_spec_v1 + bocker_jpetazzo_2015
25// lineage_id: nishi_run_sovereign_q10
26
27// nx_safety_envelope:
28// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
29// sil_target: SIL1
30// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
31// verdict: NOT_YET_EVALUATED
32
33import "nx_syscalls_x86_64.nx"
34
35// Container lifecycle verdict.
36const NX_CTR_VERDICT_UNKNOWN: i64 = 0
37const NX_CTR_VERDICT_STARTED: i64 = 1
38const NX_CTR_VERDICT_EXITED_OK: i64 = 2
39const NX_CTR_VERDICT_EXITED_ERROR: i64 = 3
40const NX_CTR_VERDICT_BLOCKED_PRIV: i64 = 4 // need CAP_SYS_ADMIN
41const NX_CTR_VERDICT_BLOCKED_FS: i64 = 5 // rootfs missing / unreadable
42const NX_CTR_VERDICT_BLOCKED_EXEC: i64 = 6 // entrypoint not found
43const NX_CTR_VERDICT_N: i64 = 7
44
45// Container spec. The caller fills in this struct + calls
46// nx_container_run. All bytes are caller-owned.
47struct ContainerSpec {
48 // Absolute path on the host to the container root filesystem.
49 rootfs_path: *u8,
50 rootfs_path_len: i64,
51 // Absolute path to the entrypoint INSIDE the rootfs (e.g.
52 // "/usr/bin/nishi_web").
53 entry_path: *u8,
54 entry_path_len: i64,
55 // Null-terminated argv array. argv[0] should be entry_path.
56 argv: *i64,
57 // 64-byte hostname for CLONE_NEWUTS (null-terminated).
58 hostname: *u8,
59 // Bitwise-OR of CLONE_NEW*. 0 = no namespaces (chroot only).
60 namespace_flags: i64
61}
62
63// Result block populated by nx_container_run.
64struct ContainerResult {
65 verdict: i64, // NX_CTR_VERDICT_*
66 child_pid: i64, // parent-side; 0 if fork failed
67 exit_code: i64, // 0..255 if exited; -1 if killed
68 signal_no: i64 // signal number if killed by signal
69}
70
71// Write `n` bytes of `src` to `dst`.
72func _bcopy(dst: *u8, src: *u8, n: i64) -> i64 {
73 var i: i64 = 0
74 while i < n { dst[i] = src[i]; i = i + 1 }
75 return 0
76}
77
78// Write "proc" into out (4 bytes + NUL).
79func _str_proc(out: *u8) -> i64 {
80 out[0]=112; out[1]=114; out[2]=111; out[3]=99; out[4]=0
81 return 4
82}
83// "sysfs"
84func _str_sysfs(out: *u8) -> i64 {
85 out[0]=115; out[1]=121; out[2]=115; out[3]=102; out[4]=115; out[5]=0
86 return 5
87}
88// "/proc"
89func _path_proc_abs(out: *u8) -> i64 {
90 out[0]=47; out[1]=112; out[2]=114; out[3]=111; out[4]=99; out[5]=0
91 return 5
92}
93// "/sys"
94func _path_sys_abs(out: *u8) -> i64 {
95 out[0]=47; out[1]=115; out[2]=121; out[3]=115; out[4]=0
96 return 4
97}
98// "none" (mount source placeholder for procfs / sysfs).
99func _str_none(out: *u8) -> i64 {
100 out[0]=110; out[1]=111; out[2]=110; out[3]=101; out[4]=0
101 return 4
102}
103
104// Build "<rootfs>/<sub>" -- caller-supplied scratch must be large
105// enough. Returns the byte length (NUL-terminated for syscall use).
106func _join_path(
107 rootfs: *u8, rootfs_len: i64,
108 sub: *u8, sub_len: i64,
109 out: *u8
110) -> i64 {
111 var i: i64 = 0
112 while i < rootfs_len { out[i] = rootfs[i]; i = i + 1 }
113 var j: i64 = 0
114 while j < sub_len { out[i + j] = sub[j]; j = j + 1 }
115 out[i + sub_len] = 0
116 return i + sub_len
117}
118
119// Inside-child setup: unshare namespaces, mount-bind rootfs, pivot_root,
120// mount pseudo-filesystems, execve. Never returns on success (exec
121// replaces the image); returns -1 on plumbing failure.
122func _child_setup_and_exec(spec: *ContainerSpec) -> i64 {
123 // 1. Enter new namespaces.
124 if spec.namespace_flags != 0 {
125 let ur: i64 = sys_unshare(spec.namespace_flags)
126 if ur < 0 { sys_exit(70) } // -EPERM typically: not root
127 }
128
129 // 2. Set hostname inside the UTS namespace. Skipped if hostname
130 // is null or empty.
131 // (sys_sethostname not yet wrapped; deferred to L8b -- non-blocking
132 // for the basic chroot case)
133
134 // 3. chroot into the rootfs. Skip when rootfs_path is "/" or
135 // length <= 1 -- caller signalled "no isolation, just fork".
136 // Useful for smokes and for "run as user" scenarios.
137 if spec.rootfs_path_len > 1 {
138 let cr: i64 = sys_chroot(spec.rootfs_path)
139 if cr < 0 { sys_exit(71) }
140 }
141
142 // 4. Mount procfs at /proc inside the new root (if CLONE_NEWPID).
143 if spec.namespace_flags != 0 {
144 let proc_target: *u8 = sys_mmap(16)
145 _path_proc_abs(proc_target)
146 let proc_type: *u8 = sys_mmap(16)
147 _str_proc(proc_type)
148 let none_src: *u8 = sys_mmap(16)
149 _str_none(none_src)
150 sys_mount(none_src, proc_target, proc_type, 0, 0 as *u8)
151 }
152
153 // 5. exec the entrypoint. argv is caller-provided.
154 sys_execve(spec.entry_path, spec.argv, 0 as *i64)
155 sys_exit(72)
156 return -1
157}
158
159// Top-level run: fork, child does setup, parent waits.
160func nx_container_run(spec: *ContainerSpec, out: *ContainerResult) -> i64 {
161 out.verdict = NX_CTR_VERDICT_UNKNOWN
162 out.child_pid = 0
163 out.exit_code = -1
164 out.signal_no = 0
165
166 let pid: i64 = sys_fork()
167 if pid < 0 {
168 out.verdict = NX_CTR_VERDICT_BLOCKED_PRIV
169 return -1
170 }
171 if pid == 0 {
172 _child_setup_and_exec(spec)
173 sys_exit(73)
174 }
175 out.child_pid = pid
176 out.verdict = NX_CTR_VERDICT_STARTED
177
178 let status_slot: *i64 = sys_mmap(16) as *i64
179 *status_slot = 0
180 sys_wait4(pid, status_slot, 0)
181 let status: i64 = *status_slot
182
183 // Decode wait status. Low byte == 0 -> exited normally; signal
184 // otherwise.
185 let lo: i64 = status & 0xFF
186 if lo == 0 {
187 out.exit_code = (status >> 8) & 0xFF
188 if out.exit_code == 0 { out.verdict = NX_CTR_VERDICT_EXITED_OK }
189 else { out.verdict = NX_CTR_VERDICT_EXITED_ERROR }
190 } else {
191 out.signal_no = lo & 0x7F
192 out.verdict = NX_CTR_VERDICT_EXITED_ERROR
193 }
194 return 0
195}
196
197// Sealed-enum validity gate.
198func nx_container_verdict_is_valid(v: i64) -> i64 {
199 if v < 0 { return 0 }
200 if v >= NX_CTR_VERDICT_N { return 0 }
201 return 1
202}