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}