code wiki / (root) / nx_container.nx

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}