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}