code wiki / _hdl_build / nx_deploy_stage_lib.nx
nx_deploy_stage_lib.nx source
↩ module page · 321 lines · 15017 B
1// ELF-magic admission and exact-byte staging into a .new slot.
2// This preserves ordinary source permissions and verifies copied bytes; it does
3// not establish that an ELF is valid for execution. Legacy1024-byte admission
4// remains explicit pending format-aware admission; no64MiB content ceiling.
5// Caller owns trusted stage directories. File and directory sync outcomes are
6// recorded separately from visibility. Promotion and runtime health are separate.
7// license_tier: ORIGINAL
8import "nx_fio.nx"
9const DS_MAGIC_1024: i64 = 1024
10const DS_MAGIC_4096: i64 = 4096
11const DS_MAGIC_2000: i64 = 2000
12
13const DS_MINSZ: i64 = 1024 // smallest plausible ELF; a smaller "binary" is a failed/torn build
14
15func ds_w(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 }
16func ds_len(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
17func ds_cat(d: *u8, off: i64, s: *u8) -> i64 { var i: i64=0; var o: i64=off; while s[i]!=(0 as u8){d[o]=s[i]; o=o+1; i=i+1} return o }
18func ds_catn(d: *u8, off: i64, v: i64) -> i64 {
19 let t: *u8 = sys_mmap(28); var m: i64=v; var o: i64=off; if m<0 { d[o]=45 as u8; o=o+1; m=0-m }
20 var k: i64=0; if m==0 { t[0]=48 as u8; k=1 } while m>0 { t[k]=(48+(m%10)) as u8; m=m/10; k=k+1 }
21 var i: i64=0; while i<k { d[o]=t[k-1-i]; o=o+1; i=i+1 } return o
22}
23
24// dst path must end with ".new" -> refuses any live-binary target. 1=ok, 0=refused.
25func ds_dst_is_new(dst: *u8) -> i64 {
26 let n: i64 = ds_len(dst)
27 if n < 4 { return 0 }
28 if dst[n-4] != (46 as u8) { return 0 } // .
29 if dst[n-3] != (110 as u8) { return 0 } // n
30 if dst[n-2] != (101 as u8) { return 0 } // e
31 if dst[n-1] != (119 as u8) { return 0 } // w
32 return 1
33}
34
35// verify src is a nonzero ELF; returns size, or negative rc. sets magic ok.
36// Staging receipt distinguishes a visible rename from confirmed directory sync.
37struct NxDeployStageResult {
38 code:i64, system_code:i64, visible:i64, durable:i64, bytes:i64, mode:i64, block:i64, cleanup_code:i64, temp_owned:i64, temp_dev:i64, temp_ino:i64, stage:*u8,
39}
40func ds_stat_word(stat:*u8,offset:i64)->i64{
41 return *((stat+offset) as *i64)
42}
43func ds_stage_result_init(r:*NxDeployStageResult)->i64{
44 r.code=0;r.system_code=0;r.visible=0;r.durable=0;r.bytes=0;r.mode=0;r.block=0;r.cleanup_code=0;r.temp_owned=0;r.temp_dev=0;r.temp_ino=0;r.stage="input";return 0
45}
46func ds_source_prepare(src:*u8,region:*NxFileReadRegion,stat:*u8,digest:*u8,r:*NxDeployStageResult)->i64{
47 var block:i64=0
48 r.stage="source-open"
49 if fio_region_open(src,region)!=0 {r.code=0-3;r.system_code=region.code}
50 if r.code==0 {
51 r.stage="source-metadata";r.system_code=sys_fstat_fd(region.fd,stat)
52 if r.system_code<0 {r.code=0-13}
53 }
54 if r.code==0 {
55 let fullmode:i64=(stat[NX_STAT_X64_MODE_OFFSET] as i64)+((stat[NX_STAT_X64_MODE_OFFSET+1] as i64)<<8)
56 if (fullmode & NX_STAT_TYPE_MASK)!=NX_STAT_REGULAR_FILE {r.code=0-4;r.stage="source-not-regular"}
57 r.mode=sys_stat_permissions(stat)
58 r.bytes=region.total
59 if ds_stat_word(stat,NX_STAT_X64_SIZE_OFFSET)!=r.bytes {r.code=0-13;r.stage="source-extent-changed"}
60 if r.bytes<DS_MINSZ {r.code=0-6;r.stage="source-too-small"}
61 // SHA-256 bit-count representation, not an artifact-size admission policy.
62 if r.bytes>9223372036854775807/8 {r.code=0-7;r.stage="digest-length-overflow"}
63 block=ds_stat_word(stat,NX_STAT_X64_BLOCK_OFFSET)
64 if block<=0 {r.code=0-13;r.stage="filesystem-block-size"}
65 if block>r.bytes {block=r.bytes}
66 }
67 if r.code==0 {
68 r.stage="source-elf"
69 var hn:i64=0
70 while hn<4 {
71 let n:i64=sys_read(region.fd,digest+hn,4-hn)
72 if n==FIO_EINTR{continue}
73 if n<=0{r.code=0-4;r.system_code=n;break};hn=hn+n
74 }
75 if r.code==0 && (digest[0]!=(127 as u8)||digest[1]!=(69 as u8)||digest[2]!=(76 as u8)||digest[3]!=(70 as u8)){r.code=0-4}
76 if r.code==0 && sys_lseek(region.fd,0,0)!=0{r.code=0-9;r.stage="source-rewind"}
77 }
78
79 r.block=block;return r.code
80}
81func ds_src_size(src:*u8)->i64{
82 let needed:i64=__size_of(NxFileReadRegion)+__size_of(NxDeployStageResult)+NX_STAT_X64_BYTES+4
83 let mem:*u8=sys_mmap_try(needed);if (mem as i64)<=0{return 0-12}
84 let region:*NxFileReadRegion=mem as *NxFileReadRegion
85 let r:*NxDeployStageResult=(mem+__size_of(NxFileReadRegion)) as *NxDeployStageResult
86 let stat:*u8=(r as *u8)+__size_of(NxDeployStageResult)
87 fio_region_init(region);ds_stage_result_init(r)
88 ds_source_prepare(src,region,stat,stat+NX_STAT_X64_BYTES,r)
89 fio_region_close(region)
90 var result:i64=r.code
91 if result==0{if region.code!=0{result=0-9}else{result=r.bytes}}
92 sys_munmap_direct(mem,needed);return result
93}
94
95func ds_stage_core(src:*u8,dst:*u8,r:*NxDeployStageResult)->i64{
96 ds_stage_result_init(r)
97 if ds_dst_is_new(dst)==0 {r.code=0-2;return r.code}
98 let dn:i64=ds_len(dst)
99 if dn>9223372036854775807-7 {r.code=0-12;r.stage="path-overflow";return r.code}
100 let fixed_n:i64=__size_of(NxFileReadRegion)+__size_of(NxFileWriteResult)+NX_STAT_X64_BYTES+64+sha256_workspace_bytes()
101 let fixed:*u8=sys_mmap_try(fixed_n)
102 if (fixed as i64)<=0 {r.code=0-12;r.stage="workspace-allocation";return r.code}
103 let region:*NxFileReadRegion=fixed as *NxFileReadRegion
104 let sync:*NxFileWriteResult=(fixed+__size_of(NxFileReadRegion)) as *NxFileWriteResult
105 let stat:*u8=(sync as *u8)+__size_of(NxFileWriteResult)
106 let digest:*u8=stat+NX_STAT_X64_BYTES
107 let sha:*u8=digest+64
108 fio_region_init(region)
109 var paths:*u8=0 as *u8;var buffer:*u8=0 as *u8;var block:i64=0;var wf:i64=0-1
110 var tmp:*u8=0 as *u8;var parent:*u8=0 as *u8
111 ds_source_prepare(src,region,stat,digest,r)
112 block=r.block
113 if r.code==0 {
114 // Two complete path buffers, derived from admitted destination length.
115 if dn>9223372036854775807/2-7 {r.code=0-12;r.stage="path-overflow"}
116 else {
117 paths=sys_mmap_try((dn+7)*2);buffer=sys_mmap_try(block)
118 if (paths as i64)<=0 || (buffer as i64)<=0 {r.code=0-12;r.stage="transfer-allocation"}
119 }
120 }
121 if r.code==0 {
122 tmp=paths;parent=paths+dn+7
123 var p:i64=ds_cat(tmp,0,dst);p=ds_cat(tmp,p,".stg");tmp[p]=0 as u8
124 var slash:i64=0-1;var j:i64=0
125 while j<dn {if dst[j]==(47 as u8){slash=j};j=j+1}
126 if slash<0 {parent[0]=46 as u8;parent[1]=0 as u8}
127 else {var end:i64=slash;if end==0{end=1};j=0;while j<end{parent[j]=dst[j];j=j+1};parent[end]=0 as u8}
128 // Exclusive read/write temporary: never truncate a prior owner or follow a symlink.
129 r.stage="temporary-create"
130 wf=__syscall(SYS_OPENAT,AT_FDCWD,tmp,194|O_NOFOLLOW,MODE_0600,0,0)
131 if wf<0 {r.code=0-8;r.system_code=wf}
132 else {
133 r.temp_owned=1
134 r.system_code=sys_fstat_fd(wf,stat)
135 if r.system_code!=0 {r.code=0-13;r.stage="temporary-metadata"}
136 else {r.temp_dev=ds_stat_word(stat,NX_STAT_X64_DEVICE_OFFSET);r.temp_ino=ds_stat_word(stat,NX_STAT_X64_INODE_OFFSET)}
137 }
138 }
139 if r.code==0 {
140 sha256_init_workspace(sha,sha256_workspace_bytes())
141 while r.code==0 && region.read_bytes<region.length {
142 let first:i64=region.read_bytes
143 let got:i64=fio_region_next(region,buffer,block)
144 if got<=0 {r.code=0-9;r.system_code=got;r.stage="source-read-no-progress";break}
145 if first==0 {
146 if got<4 || buffer[0]!=(127 as u8) || buffer[1]!=(69 as u8) || buffer[2]!=(76 as u8) || buffer[3]!=(70 as u8) {
147 r.code=0-4;r.stage="source-not-elf";break
148 }
149 }
150 sha256_update(sha as *Sha256,buffer,got)
151 var sent:i64=0
152 while sent<got {
153 let w:i64=sys_write(wf,buffer+sent,got-sent)
154 if w==FIO_EINTR {continue}
155 if w<=0 {r.code=0-9;r.system_code=w;r.stage="temporary-write";break}
156 sent=sent+w
157 }
158 }
159 if r.code==0 {sha256_final(sha as *Sha256,digest)}
160 }
161 if r.code==0 {
162 r.stage="temporary-verify"
163 if sys_lseek(wf,0,2)!=r.bytes || sys_lseek(wf,0,0)!=0 {r.code=0-11}
164 else {
165 sha256_init_workspace(sha,sha256_workspace_bytes())
166 var got:i64=0
167 while got<r.bytes && r.code==0 {
168 var want:i64=r.bytes-got;if want>block{want=block}
169 let n:i64=sys_read(wf,buffer,want)
170 if n==FIO_EINTR{continue}
171 if n<=0 {r.code=0-11;r.system_code=n;break}
172 sha256_update(sha as *Sha256,buffer,n);got=got+n
173 }
174 if r.code==0 {
175 sha256_final(sha as *Sha256,digest+32)
176 var i:i64=0;while i<32 {if digest[i]!=digest[32+i]{r.code=0-11};i=i+1}
177 }
178 }
179 }
180 if r.code==0 {
181 r.stage="temporary-mode";r.system_code=sys_fchmod_fd(wf,r.mode)
182 if r.system_code!=0 {r.code=0-14}
183 }
184 if r.code==0 {
185 r.stage="temporary-sync"
186 r.system_code=fio_write_sync_fd(wf,buffer,0,sync);wf=0-1
187 if r.system_code!=0 {r.code=0-15}
188 }
189 if wf>=0 {let closed:i64=sys_close(wf);wf=0-1;if r.code==0 && closed!=0{r.code=0-15;r.system_code=closed}}
190 fio_region_close(region)
191 if r.code==0 && region.code!=0 {r.code=0-9;r.system_code=region.code;r.stage="source-close"}
192 if r.code==0 {
193 r.stage="publish-identity"
194 r.system_code=sys_fstatat(tmp,stat)
195 if r.system_code!=0 || ds_stat_word(stat,NX_STAT_X64_DEVICE_OFFSET)!=r.temp_dev || ds_stat_word(stat,NX_STAT_X64_INODE_OFFSET)!=r.temp_ino {r.code=0-17}
196 }
197 if r.code==0 {
198 r.stage="publish";r.system_code=sys_renameat(tmp,dst)
199 if r.system_code!=0 {r.code=0-10}else{r.visible=1;r.temp_owned=0}
200 }
201 if r.code==0 {
202 r.stage="directory-sync";let df:i64=sys_openat_directory(parent)
203 if df<0 {r.code=0-16;r.system_code=df}
204 else {r.system_code=fio_write_sync_fd(df,buffer,0,sync);if r.system_code!=0{r.code=0-16}else{r.durable=1;r.stage="staged"}}
205 }
206 // Only remove this invocation's temporary after matching its retained identity.
207 // Stage directories are trusted; this is not a hostile-directory race guarantee.
208 if r.code!=0 && r.visible==0 && r.temp_owned==1 {
209 r.cleanup_code=sys_fstatat(tmp,stat)
210 if r.cleanup_code==0 {
211 if ds_stat_word(stat,NX_STAT_X64_DEVICE_OFFSET)==r.temp_dev && ds_stat_word(stat,NX_STAT_X64_INODE_OFFSET)==r.temp_ino {
212 r.cleanup_code=fio_unlink(tmp);if r.cleanup_code==0{r.temp_owned=0}
213 } else {r.cleanup_code=0-17}
214 }
215 }
216 if (buffer as i64)>0 {sys_munmap_direct(buffer,block)}
217 if (paths as i64)>0 {sys_munmap_direct(paths,(dn+7)*2)}
218 sys_munmap_direct(fixed,fixed_n)
219 return r.code
220}
221func ds_stage(src:*u8,dst:*u8)->i64{
222 let r:*NxDeployStageResult=sys_mmap_try(__size_of(NxDeployStageResult)) as *NxDeployStageResult
223 if (r as i64)<=0{return 0-12}
224 let rc:i64=ds_stage_core(src,dst,r)
225 sys_munmap_direct(r as *u8,__size_of(NxDeployStageResult));return rc
226}
227
228
229func ds_out_fd(fd:i64,s:*u8,n:i64)->i64{
230 var done:i64=0
231 while done<n{let w:i64=sys_write(fd,s+done,n-done);if w==FIO_EINTR{continue};if w<=0{return 0-1};done=done+w};return 0
232}
233func ds_text_fd(fd:i64,s:*u8)->i64{return ds_out_fd(fd,s,ds_len(s))}
234func ds_json_fd(fd:i64,s:*u8)->i64{
235 if ds_text_fd(fd,"\"")!=0{return 0-1}
236 var i:i64=0;var begin:i64=0
237 let hex:*u8="0123456789abcdef"
238 while s[i]!=(0 as u8){
239 let ch:i64=s[i] as i64
240 if ch<32||ch==34||ch==92{
241 if ds_out_fd(fd,s+begin,i-begin)!=0{return 0-1}
242 if ch==34||ch==92{if ds_text_fd(fd,"\\")!=0||ds_out_fd(fd,s+i,1)!=0{return 0-1}}
243 else{if ds_text_fd(fd,"\\u00")!=0||ds_out_fd(fd,hex+(ch>>4),1)!=0||ds_out_fd(fd,hex+(ch&15),1)!=0{return 0-1}}
244 begin=i+1
245 }
246 i=i+1
247 }
248 if ds_out_fd(fd,s+begin,i-begin)!=0{return 0-1}
249 return ds_text_fd(fd,"\"")
250}
251// At most20 recursive digit frames for an i64, including its minimum value.
252func ds_digits_fd(fd:i64,v:i64)->i64{
253 if v<=0-10||v>=10{if ds_digits_fd(fd,v/10)!=0{return 0-1}}
254 var digit:i64=v%10;if digit<0{digit=0-digit}
255 return ds_out_fd(fd,"0123456789"+digit,1)
256}
257func ds_num_fd(fd:i64,v:i64)->i64{
258 if v<0{if ds_text_fd(fd,"-")!=0{return 0-1}};return ds_digits_fd(fd,v)
259}
260func ds_emit_result(fd:i64,src:*u8,dst:*u8,r:*NxDeployStageResult)->i64{
261 if ds_text_fd(fd,"{\"organ\":\"nx_deploy_stage\",\"src\":")!=0||ds_json_fd(fd,src)!=0{return 0-1}
262 if ds_text_fd(fd,",\"dst\":")!=0||ds_json_fd(fd,dst)!=0{return 0-1}
263 if ds_text_fd(fd,",\"rc\":")!=0||ds_num_fd(fd,r.code)!=0{return 0-1}
264 if ds_text_fd(fd,",\"bytes\":")!=0||ds_num_fd(fd,r.bytes)!=0{return 0-1}
265 if ds_text_fd(fd,",\"mode\":")!=0||ds_num_fd(fd,r.mode)!=0{return 0-1}
266 if ds_text_fd(fd,",\"system_code\":")!=0||ds_num_fd(fd,r.system_code)!=0{return 0-1}
267 if ds_text_fd(fd,",\"visible\":")!=0||ds_num_fd(fd,r.visible)!=0{return 0-1}
268 if ds_text_fd(fd,",\"durable\":")!=0||ds_num_fd(fd,r.durable)!=0{return 0-1}
269 if ds_text_fd(fd,",\"temporary_owned\":")!=0||ds_num_fd(fd,r.temp_owned)!=0{return 0-1}
270 if ds_text_fd(fd,",\"cleanup_code\":")!=0||ds_num_fd(fd,r.cleanup_code)!=0{return 0-1}
271 if ds_text_fd(fd,",\"stage\":")!=0||ds_json_fd(fd,r.stage)!=0{return 0-1}
272 var verdict:*u8="REFUSED";if r.code==0{verdict="STAGED"}else{if r.visible==1{verdict="VISIBLE_UNCONFIRMED"}}
273 if ds_text_fd(fd,",\"note\":")!=0||ds_json_fd(fd,"ELF-magic admission; destination must end .new; staging preserves source permissions and verifies copied bytes; guarded promotion and runtime health are separate.")!=0{return 0-1}
274 if ds_text_fd(fd,",\"verdict\":")!=0||ds_json_fd(fd,verdict)!=0{return 0-1}
275 return ds_text_fd(fd,"}\n")
276}
277// Legacy call contract retained. New callers use ds_emit_result with full state.
278func ds_emit(src:*u8,dst:*u8,rc:i64,sz:i64)->i64{
279 let r:*NxDeployStageResult=sys_mmap_try(__size_of(NxDeployStageResult)) as *NxDeployStageResult
280 if (r as i64)<=0{return 0-12};ds_stage_result_init(r);r.code=rc;r.bytes=sz;r.stage="legacy-state-unavailable"
281 let emitted:i64=ds_emit_result(1,src,dst,r)
282 sys_munmap_direct(r as *u8,__size_of(NxDeployStageResult));return emitted
283}
284
285// selftest: build a fake ELF fixture, stage it to a .new, verify byte-identical + size; refuse non-.new dst;
286// refuse a non-ELF src. 0 = GREEN.
287func ds_selftest() -> i64 {
288 var f: i64 = 0
289 // fixture: \x7fELF + 2000 bytes of pattern
290 let fx: *u8 = sys_mmap(DS_MAGIC_4096)
291 fx[0]=127 as u8; fx[1]=69 as u8; fx[2]=76 as u8; fx[3]=70 as u8
292 var i: i64 = 4
293 while i < DS_MAGIC_2000 { fx[i] = (65 + (i % 26)) as u8; i = i + 1 }
294 let wf: i64 = sys_openat_wr("dskat_src.elf" as *u8, 420)
295 if wf < 0 { return 1 }
296 sys_write(wf, fx, DS_MAGIC_2000); sys_close(wf)
297 // stage to a .new slot
298 let rc: i64 = ds_stage("dskat_src.elf" as *u8, "dskat_dst.elf.new" as *u8)
299 if rc != 0 { f = f + 1 }
300 // verify byte-identical + size
301 let vf: i64 = sys_openat_rd("dskat_dst.elf.new" as *u8)
302 if vf < 0 { f = f + 1 } else {
303 let vb: *u8 = sys_mmap(DS_MAGIC_4096)
304 let vn: i64 = sys_read(vf, vb, DS_MAGIC_4096)
305 sys_close(vf)
306 if vn != DS_MAGIC_2000 { f = f + 1 }
307 var j: i64 = 0
308 var mism: i64 = 0
309 while j < DS_MAGIC_2000 { if vb[j] != fx[j] { mism = 1; j = DS_MAGIC_2000 } else { j = j + 1 } }
310 if mism == 1 { f = f + 1 }
311 }
312 // refuse non-.new dst
313 let r2: i64 = ds_stage("dskat_src.elf" as *u8, "dskat_live.elf" as *u8)
314 if r2 != (0 - 2) { f = f + 1 }
315 // refuse non-ELF src
316 let bf: i64 = sys_openat_wr("dskat_bad.txt" as *u8, 420)
317 if bf >= 0 { sys_write(bf, "not an elf at all, plain text, over 1024 bytes..." as *u8, 48); sys_close(bf) }
318 let r3: i64 = ds_stage("dskat_bad.txt" as *u8, "dskat_bad.elf.new" as *u8)
319 if r3 != (0 - 4) { if r3 != (0 - 6) { f = f + 1 } } // -4 bad magic (or -6 too small); either refuses
320 return f
321}