code wiki / _hdl_build / nx_stage_alias.nx

nx_stage_alias.nx source

↩ module page · 232 lines · 10024 B

1// nx_stage_alias.nx -- the missing RENAME BRIDGE in the build-over-API loop (seq983). 2// 3// THE GAP: /api/build stages an artifact as <target>.sov.elf.new, but the deploy plane expects 4// per-target names -- cmd_selfswap renames a hardcoded nx_hostctl.new, and the deploy rows name 5// things like nx_mgmt_api.elf.new. Nothing reachable over the API could bridge those two names: 6// nx_fs_write is text-only (a binary would be corrupted), md_promote_staged only maps <n>.new -> <n>, 7// /api/promote maps <t>.sov.elf.new -> <t>.elf and refuses daemon/oracle names outright, and 8// /api/unpack writes only under buildroot/runtime at mode 0644. So the documented sequence 9// "build then deploy" could never actually stage a deployable artifact, and the established 10// workaround was to build off-hub and upload -- i.e. leave the sovereign API. 11// 12// WHY THIS IS NOT A GENERAL FILE-COPY TOOL: a general copy primitive exposed over the API would be a 13// serious hazard. This is deliberately the narrowest thing that closes the gap: 14// - the target name is sanitized to [A-Za-z0-9_] only, so a path separator or .. can never appear; 15// - the source is ALWAYS <target>.sov.elf.new and the destination ALWAYS <target>.<suffix>, both in 16// the daemon cwd -- neither is caller-supplied as a path; 17// - the suffix is an allowlist of exactly {new, elf.new}; anything else is refused; 18// - the source must exist, be non-empty, and begin with the ELF magic \x7fELF -- so a failed build 19// that staged 0 bytes, or any non-ELF, is REFUSED rather than propagated into a deploy; 20// - the destination is written fresh and chmod 0755, because a promoted binary must be executable. 21// Refusals are LOUD and return non-zero; nothing is written on any refusal path. 22// 23// license_tier: ORIGINAL No hw writes (Rule 26). 24import "nx_syscalls.nx" 25import "nx_gate_verdict.nx" 26 27const SA_NAMEMAX: i64 = 120 28const SA_PATHMAX: i64 = 256 29const SA_COPYBUF: i64 = 262144 30const SA_MODE_X: i64 = 0x1ed // 0755 -- a promoted binary must stay executable 31const SA_MODE_RW: i64 = 0x1a4 // 0644 -- creation mode before the chmod 32const SA_ELF_0: i64 = 127 // \x7f 33const SA_ELF_1: i64 = 69 // 'E' 34const SA_ELF_2: i64 = 76 // 'L' 35const SA_ELF_3: i64 = 70 // 'F' 36 37func sa_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 38 39// NUL-terminated equality. Local rather than borrowed: streq_n lives in the parser's scope and is 40// not reachable from a plain organ, and a whole string lib is not worth importing for one compare. 41func sa_streq(a: *u8, b: *u8) -> i64 { 42 var i: i64 = 0 43 while a[i] != (0 as u8) { 44 if b[i] != a[i] { return 0 } 45 i = i + 1 46 } 47 if b[i] != (0 as u8) { return 0 } 48 return 1 49} 50 51func sa_cat(dst: *u8, o: i64, src: *u8) -> i64 { 52 var i: i64 = 0 53 var p: i64 = o 54 while src[i] != (0 as u8) { dst[p] = src[i]; p = p + 1; i = i + 1 } 55 return p 56} 57 58// 1 when every byte is [A-Za-z0-9_] and the name is non-empty and bounded. A name that passes this 59// cannot contain '/' or '.', so it can never escape the daemon cwd. 60func sa_name_ok(nm: *u8) -> i64 { 61 var i: i64 = 0 62 while nm[i] != (0 as u8) { 63 let c: i64 = nm[i] as i64 64 var ok: i64 = 0 65 if c >= 48 { if c <= 57 { ok = 1 } } 66 if c >= 65 { if c <= 90 { ok = 1 } } 67 if c >= 97 { if c <= 122 { ok = 1 } } 68 if c == 95 { ok = 1 } 69 if ok == 0 { return 0 } 70 i = i + 1 71 if i >= SA_NAMEMAX { return 0 } 72 } 73 if i == 0 { return 0 } 74 return 1 75} 76 77func sa_suffix_ok(sfx: *u8) -> i64 { 78 if sa_streq(sfx, "new" as *u8) == 1 { return 1 } 79 if sa_streq(sfx, "elf.new" as *u8) == 1 { return 1 } 80 return 0 81} 82 83// Copy src -> dst, but ONLY if src is a real, non-empty ELF. Returns bytes copied, or -1 refused. 84func sa_copy_elf(src: *u8, dst: *u8) -> i64 { 85 let sf: i64 = sys_openat_rd(src) 86 if sf < 0 { return 0 - 1 } 87 let buf: *u8 = sys_mmap(SA_COPYBUF) 88 let first: i64 = sys_read(sf, buf, SA_COPYBUF) 89 if first < 4 { sys_close(sf); return 0 - 1 } 90 if (buf[0] as i64) != SA_ELF_0 { sys_close(sf); return 0 - 1 } 91 if (buf[1] as i64) != SA_ELF_1 { sys_close(sf); return 0 - 1 } 92 if (buf[2] as i64) != SA_ELF_2 { sys_close(sf); return 0 - 1 } 93 if (buf[3] as i64) != SA_ELF_3 { sys_close(sf); return 0 - 1 } 94 let df: i64 = sys_openat_wr(dst, SA_MODE_RW) 95 if df < 0 { sys_close(sf); return 0 - 1 } 96 var total: i64 = 0 97 var n: i64 = first 98 var go: i64 = 1 99 while go == 1 { 100 if n <= 0 { go = 0 } else { 101 let w: i64 = sys_write(df, buf, n) 102 if w != n { sys_close(sf); sys_close(df); return 0 - 1 } 103 total = total + n 104 n = sys_read(sf, buf, SA_COPYBUF) 105 } 106 } 107 sys_close(sf) 108 sys_close(df) 109 nx_chmod(dst, SA_MODE_X) 110 return total 111} 112 113func sa_selftest() -> i64 { 114 let ctr: *i64 = gv_ctr() 115 gv_head("nx_stage_alias -- fail-closed rename bridge for staged build artifacts (seq983)" as *u8) 116 117 var t1: i64 = 0 118 if sa_name_ok("nx_hostctl" as *u8) == 1 { t1 = 1 } 119 gv_check("T1 a plain target name is accepted" as *u8, t1, ctr) 120 121 var t2: i64 = 0 122 if sa_name_ok("../etc/passwd" as *u8) == 0 { 123 if sa_name_ok("a/b" as *u8) == 0 { 124 if sa_name_ok("a.b" as *u8) == 0 { t2 = 1 } 125 } 126 } 127 gv_check("T2 traversal, separators and dots are REFUSED (no path can escape the cwd)" as *u8, t2, ctr) 128 129 var t3: i64 = 0 130 if sa_name_ok("" as *u8) == 0 { t3 = 1 } 131 gv_check("T3 the empty name is refused" as *u8, t3, ctr) 132 133 var t4: i64 = 0 134 if sa_suffix_ok("new" as *u8) == 1 { 135 if sa_suffix_ok("elf.new" as *u8) == 1 { 136 if sa_suffix_ok("sh" as *u8) == 0 { t4 = 1 } 137 } 138 } 139 gv_check("T4 the destination suffix is an allowlist, not caller-chosen" as *u8, t4, ctr) 140 141 // The explicit-destination arg must not become an escape hatch: it is sanitized by the SAME rule 142 // as the target, so the added expressiveness cannot add authority. 143 var t4b: i64 = 0 144 if sa_name_ok("sites" as *u8) == 1 { 145 if sa_name_ok("../sites" as *u8) == 0 { 146 if sa_name_ok("sites.elf" as *u8) == 0 { t4b = 1 } 147 } 148 } 149 gv_check("T4b an explicit destination name is sanitized identically (no path, no dot)" as *u8, t4b, ctr) 150 151 // A non-ELF source must be refused, so a 0-byte failed build can never be staged for deploy. 152 let bad: *u8 = "/tmp/nx_stage_alias_notelf" as *u8 153 let bf: i64 = sys_openat_wr(bad, SA_MODE_RW) 154 if bf >= 0 { sys_write(bf, "not an elf at all" as *u8, 17); sys_close(bf) } 155 var t5: i64 = 0 156 if sa_copy_elf(bad, "/tmp/nx_stage_alias_out" as *u8) == (0 - 1) { t5 = 1 } 157 gv_check("T5 a non-ELF source is REFUSED (a 0-byte failed build never reaches a deploy)" as *u8, t5, ctr) 158 159 var t6: i64 = 0 160 if sa_copy_elf("/tmp/nx_stage_alias_absent_xyz" as *u8, "/tmp/nx_stage_alias_out" as *u8) == (0 - 1) { t6 = 1 } 161 gv_check("T6 an absent source is REFUSED, not silently treated as success" as *u8, t6, ctr) 162 163 let rc: i64 = gv_verdict("STAGE-ALIAS-GATE" as *u8, ctr, 164 "name sanitation, suffix allowlist, ELF-magic and absence refusals" as *u8) 165 return rc 166} 167 168func main(argc: i64, argv: *i64) -> i64 { 169 if argc > 1 { 170 let v: *u8 = argv[1] as *u8 171 if sa_streq(v, "selftest" as *u8) == 1 { let rc: i64 = sa_selftest(); sys_exit(rc); return rc } 172 } 173 if argc < 3 { 174 sa_puts("usage: nx_stage_alias alias <target> [new|elf.new] [dstname] (src is always <target>.sov.elf.new; dstname defaults to <target> and is sanitized identically)\n" as *u8) 175 sys_exit(2) 176 return 2 177 } 178 let nm: *u8 = argv[2] as *u8 179 if sa_name_ok(nm) == 0 { 180 sa_puts("STAGE-ALIAS REFUSED: target name must be [A-Za-z0-9_] and non-empty\n" as *u8) 181 sys_exit(3) 182 return 3 183 } 184 var sfx: *u8 = "new" as *u8 185 if argc > 3 { sfx = argv[3] as *u8 } 186 if sa_suffix_ok(sfx) == 0 { 187 sa_puts("STAGE-ALIAS REFUSED: suffix must be new or elf.new\n" as *u8) 188 sys_exit(4) 189 return 4 190 } 191 let src: *u8 = sys_mmap(SA_PATHMAX) 192 var o: i64 = 0 193 o = sa_cat(src, o, nm) 194 o = sa_cat(src, o, ".sov.elf.new" as *u8) 195 src[o] = 0 as u8 196 // OPTIONAL EXPLICIT DESTINATION BASENAME (2026-07-30). The bridge assumed the deployed binary 197 // shares the BUILD TARGET's name. The public edge disproves that: it builds as 198 // nx_sites_daemon_v2 and runs as sites.elf, so /api/restart service=sites (which promotes 199 // sites.elf.new) could never be fed by /api/build -- the same build-name-vs-deploy-name trap that 200 // left the hostctl reader-keep breaker staged-but-unlandable for days. EVERY safety invariant is 201 // preserved: the destination basename goes through the SAME sa_name_ok sanitation (so it still 202 // cannot contain '/' or '.', still cannot escape the daemon cwd), the suffix is still the same 203 // two-entry allowlist, and the source is still ALWAYS <target>.sov.elf.new with the ELF-magic 204 // check. What is added is expressiveness, not authority. 205 var dnm: *u8 = nm 206 if argc > 4 { 207 dnm = argv[4] as *u8 208 if sa_name_ok(dnm) == 0 { 209 sa_puts("STAGE-ALIAS REFUSED: destination name must be [A-Za-z0-9_] and non-empty\n" as *u8) 210 sys_exit(3) 211 return 3 212 } 213 } 214 let dst: *u8 = sys_mmap(SA_PATHMAX) 215 var p: i64 = 0 216 p = sa_cat(dst, p, dnm) 217 p = sa_cat(dst, p, "." as *u8) 218 p = sa_cat(dst, p, sfx) 219 dst[p] = 0 as u8 220 let n: i64 = sa_copy_elf(src, dst) 221 if n < 0 { 222 sa_puts("STAGE-ALIAS REFUSED: " as *u8); sa_puts(src) 223 sa_puts(" is absent, empty, or not an ELF -- nothing written\n" as *u8) 224 sys_exit(5) 225 return 5 226 } 227 sa_puts("STAGE-ALIAS OK " as *u8); sa_puts(src) 228 sa_puts(" -> " as *u8); sa_puts(dst) 229 sa_puts(" bytes=" as *u8); gv_num(n) 230 sa_puts(" mode=0755\n" as *u8) 231 return 0 232}