code wiki / (root) / nx_surface_compatibility_expanded_t207.nx

nx_surface_compatibility_expanded_t207.nx source

↩ module page · 4943 lines · 262144 B

1func gpe_surface_sources_t132(cast_path:*u8,cast_sha:*u8,adapter_path:*u8,adapter_sha:*u8)->i64{ 2 return gsb_artifact_matches(cast_path,cast_sha,135458)&gsb_artifact_matches(adapter_path,adapter_sha,7728) 3} 4// Generated capture closure: changes to either deformation source refuse emission until requalified. 5func gpe_surface_runtime_t132(out:*u8,pos:i64)->i64{ 6 if gpe_surface_sources_t132("buildroot/runtime/nx_cast_shader_src.nx","1049ef22208253d6cfbddf3d1ad6a1d4d08f6cd0d714a3a2a3e89199f66fcc8c","buildroot/runtime/nx_cast_surface_src.nx","b93fc53053eadbee8d6bee38ded29532e5dae9471bd1fbbe695357bf4c9494f9")!=1{rk_err("surface source closure mismatch: qualify canonical cast and adapter before emitting retained surface shaders\n");gsb_failed=1;return 0} 7 let compute:*u8="struct U{\nuSc:f32,\nuIP:array<vec3f,12>,\nuIH:array<vec4f,12>,\nuSB:array<vec3f,72>,\nuOF:array<vec4f,3>,\nuGB:array<vec4f,3>,\nuGT:array<vec4f,3>,\nuGK:i32,\nuGN:array<vec4f,3>,\nuTm:f32,\nuWnd:f32,\nuZMN:f32,\nuH:f32,\nuNIP:f32,\nuDNL:vec4f,\nuDNR:vec4f,\nuDNB:vec4f,\nuDNG:vec4f,\nuDNTl:vec4f,\nuDNTr:vec4f,\nuCamM:vec3f,\nuYPM:vec4f,\nuResM:vec2f,\nuSunM:vec3f,\nuHF2:array<vec3f,84>,\nuHM:i32,\nuGar:i32,\nuAuth:i32,\nuShd:i32,\nuShdP:vec4f,\nuSurfaceVertices:i32,\nuSurfaceActors:i32,\n}\n@group(0)@binding(0)var<uniform> u:U;\nvar<private> aP:vec3f;\nvar<private> aJ:vec4f;\nvar<private> aW:vec4f;\nvar<private> aN:vec3f;\nvar<private> aUV:vec2f;\nvar<private> aHS:f32;\n@group(0)@binding(1)var uJT:texture_2d<f32>;\n@group(0)@binding(2)var uJT_s:sampler;\n@group(0)@binding(3)var uGD:texture_2d<f32>;\n@group(0)@binding(4)var uGD_s:sampler;\nvar<private> vB:vec3f;\nvar<private> vW:vec3f;\nvar<private> vN:vec3f;\nvar<private> vII:i32;\nvar<private> vSh:f32;\nvar<private> vUV:vec2f;\nvar<private> vSt:f32;\n@group(0)@binding(5)var<storage,read> surfaceIn:array<vec4f>;\n@group(0)@binding(6)var<storage,read_write> surfaceOut:array<vec4f>;\nfn hsh(s:f32)->f32{\nreturn fract((sin((s*127.1))*43758.5453));\n}\n@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) gid:vec3u){\nvar svIndex:i32=i32(gid.x);\nvar ii:i32=i32(gid.y);\nif((((svIndex>=u.uSurfaceVertices)||(ii>=u.uSurfaceActors))||((u.uGar!=0)||(u.uHM!=1)))){\nreturn;\n}\naP=surfaceIn[((svIndex*4)+0)].xyz;\naN=surfaceIn[((svIndex*4)+1)].xyz;\naJ=surfaceIn[((svIndex*4)+2)];\naW=surfaceIn[((svIndex*4)+3)];\naHS=0.0;\nvSh=1.0;\nvar jit:vec3f=vec3f(0.0);\nvar rb:i32=i32((u.uIH[ii].w+0.5));\nvar rd2:i32=(rb+1);\nvar r0:vec4f=textureLoad(uJT,vec2i(i32((aJ.x+0.5)),rb),0);\nvar d0:vec4f=textureLoad(uJT,vec2i(i32((aJ.x+0.5)),rd2),0);\nvar r1:vec4f=textureLoad(uJT,vec2i(i32((aJ.y+0.5)),rb),0);\nvar d1:vec4f=textureLoad(uJT,vec2i(i32((aJ.y+0.5)),rd2),0);\nvar r2:vec4f=textureLoad(uJT,vec2i(i32((aJ.z+0.5)),rb),0);\nvar d2:vec4f=textureLoad(uJT,vec2i(i32((aJ.z+0.5)),rd2),0);\nvar r3:vec4f=textureLoad(uJT,vec2i(i32((aJ.w+0.5)),rb),0);\nvar d3:vec4f=textureLoad(uJT,vec2i(i32((aJ.w+0.5)),rd2),0);\nvar s1:f32=select(1.0,(-1.0),(dot(r0,r1)<0.0));\nvar s2:f32=select(1.0,(-1.0),(dot(r0,r2)<0.0));\nvar s3:f32=select(1.0,(-1.0),(dot(r0,r3)<0.0));\nvar br:vec4f=((((aW.x*r0)+((aW.y*s1)*r1))+((aW.z*s2)*r2))+((aW.w*s3)*r3));\nvar bd:vec4f=((((aW.x*d0)+((aW.y*s1)*d1))+((aW.z*s2)*d2))+((aW.w*s3)*d3));\nvar nr:f32=max(length(br),1e-6);\nbr=(br/nr);\nbd=(bd/nr);\nvar aP2:vec3f=(aP+jit);\nif((u.uGar==1)){\naP2=(aP2+textureLoad(uGD,vec2i(svIndex,ii),0).xyz);\n}\nif((u.uGar==2)){\nvar hf9:vec3f=u.uHF2[((ii*7)+i32((aHS+0.5)))];\nvar hw9:f32=clamp((aHS/6.0),0.0,1.0);\nhw9=(hw9*hw9);\naP2=(aP2+(vec3f(hf9.x,hf9.y,hf9.z)*hw9));\n}\nif(((u.uHM==1)&&(u.uAuth==0))){\nvar hb1:f32=((aP.z-u.uZMN)/u.uH);\nvar w1:f32=(smoothstep(0.885,0.905,hb1)*(1.0-smoothstep(0.930,0.9448,hb1)));\nif((aP.y>(-1200.0))){\nw1=0.0;\n}\naP2.z=(aP2.z+(704.0*w1));\nvar w2:f32=(((1.0-smoothstep(0.915,0.925,hb1))*smoothstep(0.885,0.895,hb1))*smoothstep((u.uH*0.015),(u.uH*0.03),abs(aP.x)));\nif((aP.y>(-1200.0))){\nw2=0.0;\n}\naP2.x=(aP2.x*(1.0+(0.08*w2)));\n}\nif(((u.uAuth==0)&&(u.uGar==0))){\nvar fh9:f32=((aP.z-u.uZMN)/u.uH);\nif((((fh9>0.885)&&(fh9<0.985))&&(aP.y<(-1200.0)))){\nvar HH7:f32=(0.115*u.uH);\nvar hc7:vec3f=vec3f(0.0,0.0,(u.uZMN+(0.94*u.uH)));\nvar no7:vec3f=(aN*sign(dot(aN,(aP-hc7))));\nvar ez7:f32=((fh9-0.9420)*u.uH);\nvar bz7:f32=((fh9-0.954)*u.uH);\nvar mz7:f32=((fh9-0.908)*u.uH);\nvar ax7:f32=abs(aP.x);\nvar bo7:f32=(ax7/HH7);\nvar bt7:f32=clamp(((bo7-0.085)/0.16),0.0,1.0);\nvar bc7:f32=(((0.024*sin((3.14159*pow(bt7,1.45))))-(0.006*bt7))*HH7);\nvar br7:f32=(((1.0-smoothstep(0.0,(0.020*HH7),abs((bz7-bc7))))*smoothstep(0.0,0.10,bt7))*(1.0-smoothstep(0.90,1.0,bt7)));\nvar ex7:f32=(ax7-(0.16*HH7));\nvar ed7:f32=sqrt(((ex7*ex7)+((ez7*ez7)*1.4)));\nvar so7:f32=(1.0-smoothstep(0.0,(0.085*HH7),ed7));\nvar lp7:f32=((1.0-smoothstep(0.0,(0.018*HH7),abs(mz7)))*(1.0-smoothstep((0.030*HH7),(0.062*HH7),ax7)));\nvar d7:f32=((((br7*0.0100)*HH7)-((so7*0.0250)*HH7))+((lp7*0.0100)*HH7));\naP2=(aP2+(no7*d7));\n}\n}\nvar sp:vec3f=((aP2+(2.0*cross(br.xyz,(cross(br.xyz,aP2)+(br.w*aP2)))))+(2.0*(((br.w*bd.xyz)-(bd.w*br.xyz))+cross(br.xyz,bd.xyz))));\nvar sn:vec3f=(aN+(2.0*cross(br.xyz,(cross(br.xyz,aN)+(br.w*aN)))));\nvar nyup:vec3f=vec3f(sn.x,sn.z,(-sn.y));\nvN=vec3f(((nyup.x*u.uIH[ii].y)-(nyup.z*u.uIH[ii].x)),nyup.y,((nyup.x*u.uIH[ii].x)+(nyup.z*u.uIH[ii].y)));\nvB=aP;\nvar yup:vec3f=vec3f(sp.x,sp.z,(-sp.y));\nvar rot:vec3f=vec3f(((yup.x*u.uIH[ii].y)-(yup.z*u.uIH[ii].x)),yup.y,((yup.x*u.uIH[ii].x)+(yup.z*u.uIH[ii].y)));\nvar wp:vec3f=(u.uIP[ii]+(rot*(u.uSc*u.uIH[ii].z)));\nvar hb2:f32=((aP.z-u.uZMN)/u.uH);\nvar sbo:vec3f=vec3f(0.0);\nif(((u.uHM>1)||(u.uGar==2))){\nvar tw9:f32=clamp(((1.02-hb2)*1.7),0.0,1.0);\nsbo=(u.uSB[(ii*6)]*tw9);\nvar wp9:f32=((u.uTm*3.1)+(vSh*7.9));\nsbo=(sbo+(vec3f(0.7,(0.15*sin((wp9*1.7))),0.7)*((sin(wp9)*u.uWnd)*tw9)));\n}\nelse{\nif((u.uGar==1)){\nsbo=(u.uSB[((ii*6)+2)]*(1.0-smoothstep(0.30,0.55,hb2)));\nif((u.uDNL.w>0.5)){\nvar wLg:f32=(1.0-smoothstep(0.0,(u.uDNL.w*1.4),length((aP-u.uDNL.xyz))));\nvar wRg:f32=(1.0-smoothstep(0.0,(u.uDNR.w*1.4),length((aP-u.uDNR.xyz))));\nsbo=(sbo+((u.uSB[((ii*6)+1)]*max(wLg,wRg))*(1.0-smoothstep((-1000.0),(-14000.0),aP.y))));\n}\nif((u.uDNB.w>0.5)){\nsbo=(sbo+(u.uSB[((ii*6)+3)]*(1.0-smoothstep(0.0,(u.uDNB.w*1.6),length((aP-u.uDNB.xyz))))));\n}\nif((u.uDNG.w>0.5)){\nsbo=(sbo+(u.uSB[((ii*6)+4)]*(1.0-smoothstep(0.0,(u.uDNG.w*1.6),length((aP-u.uDNG.xyz))))));\n}\nif((u.uDNTl.w>0.5)){\nvar wT9:f32=max((1.0-smoothstep(0.0,(u.uDNTl.w*1.6),length((aP-u.uDNTl.xyz)))),(1.0-smoothstep(0.0,(u.uDNTr.w*1.6),length((aP-u.uDNTr.xyz)))));\nsbo=(sbo+(u.uSB[((ii*6)+5)]*wT9));\n}\n}\nelse{\nvar wB:f32;\nif((u.uDNL.w>0.5)){\nvar wL:f32=(1.0-smoothstep(0.0,(u.uDNL.w*1.4),length((aP-u.uDNL.xyz))));\nvar wR:f32=(1.0-smoothstep(0.0,(u.uDNR.w*1.4),length((aP-u.uDNR.xyz))));\nwB=(max(wL,wR)*(1.0-smoothstep((-1000.0),(-14000.0),aP.y)));\n}\nelse{\nwB=(((1.0-smoothstep(0.0,0.085,abs((hb2-u.uNIP))))*(1.0-smoothstep(6000.0,26000.0,abs(aP.x))))*(1.0-smoothstep((-1000.0),(-14000.0),aP.y)));\n}\nsbo=(u.uSB[((ii*6)+1)]*wB);\nif((u.uDNB.w>0.5)){\nsbo=(sbo+(u.uSB[((ii*6)+3)]*(1.0-smoothstep(0.0,(u.uDNB.w*1.6),length((aP-u.uDNB.xyz))))));\n}\nif((u.uDNG.w>0.5)){\nsbo=(sbo+(u.uSB[((ii*6)+4)]*(1.0-smoothstep(0.0,(u.uDNG.w*1.6),length((aP-u.uDNG.xyz))))));\n}\nif((u.uDNTl.w>0.5)){\nvar wT8:f32=max((1.0-smoothstep(0.0,(u.uDNTl.w*1.6),length((aP-u.uDNTl.xyz)))),(1.0-smoothstep(0.0,(u.uDNTr.w*1.6),length((aP-u.uDNTr.xyz)))));\nsbo=(sbo+(u.uSB[((ii*6)+5)]*wT8));\n}\n}\n}\nwp=(wp+sbo);\nvar surfaceIndex:i32=(((ii*u.uSurfaceVertices)+svIndex)*2);\nsurfaceOut[surfaceIndex]=vec4f(wp.x,wp.y,wp.z,1.0);\nsurfaceOut[(surfaceIndex+1)]=vec4f(vN.x,vN.y,vN.z,0.0);\n}\n" as *u8 8 let vertex:*u8="struct U{\nuSc:f32,\nuIP:array<vec3f,12>,\nuIH:array<vec4f,12>,\nuSB:array<vec3f,72>,\nuOF:array<vec4f,3>,\nuGB:array<vec4f,3>,\nuGT:array<vec4f,3>,\nuGK:i32,\nuGN:array<vec4f,3>,\nuTm:f32,\nuWnd:f32,\nuZMN:f32,\nuH:f32,\nuNIP:f32,\nuDNL:vec4f,\nuDNR:vec4f,\nuDNB:vec4f,\nuDNG:vec4f,\nuDNTl:vec4f,\nuDNTr:vec4f,\nuCamM:vec3f,\nuYPM:vec4f,\nuResM:vec2f,\nuSunM:vec3f,\nuHF2:array<vec3f,84>,\nuHM:i32,\nuGar:i32,\nuAuth:i32,\nuShd:i32,\nuShdP:vec4f,\nuSurfaceVertices:i32,\nuSurfaceActors:i32,\n}\n@group(0)@binding(0)var<uniform> u:U;\n@group(0)@binding(1)var uJT:texture_2d<f32>;\n@group(0)@binding(2)var uJT_s:sampler;\n@group(0)@binding(3)var uGD:texture_2d<f32>;\n@group(0)@binding(4)var uGD_s:sampler;\nstruct VIO{@builtin(position) bpos:vec4f,@location(0) vB:vec3f,@location(1) vW:vec3f,@location(2) vN:vec3f,@location(3) @interpolate(flat) vII:i32,@location(4) @interpolate(flat) vSh:f32,@location(5) vUV:vec2f,@location(6) vSt:f32,}\n@group(0)@binding(6)var<storage,read> surfaceOut:array<vec4f>;\n@vertex fn main(@builtin(vertex_index) vi:u32,@builtin(instance_index) iidx:u32,@location(0) aP:vec3f,@location(1) aJ:vec4f,@location(2) aW:vec4f,@location(3) aN:vec3f,@location(4) aUV:vec2f,@location(5) aHS:f32)->VIO{\nvar vo:VIO;\nvar ii:i32=i32(iidx);\nvo.vII=ii;\nvo.vB=aP;\nvo.vUV=aUV;\nvo.vSh=1.0;\nvar surfaceNormal:vec3f=(((surfaceOut[((((ii*u.uSurfaceVertices)+i32(aJ.x))*2)+1)].xyz*aW.x)+(surfaceOut[((((ii*u.uSurfaceVertices)+i32(aJ.y))*2)+1)].xyz*aW.y))+(surfaceOut[((((ii*u.uSurfaceVertices)+i32(aJ.z))*2)+1)].xyz*aW.z));\nvo.vN=vec3f(0.0);\nif((dot(surfaceNormal,surfaceNormal)>0.0)){\nvo.vN=normalize(surfaceNormal);\n}\nelse{\nvar surfaceGeometric:vec3f=cross((surfaceOut[((((ii*u.uSurfaceVertices)+i32(aJ.y))*2)+0)].xyz-surfaceOut[((((ii*u.uSurfaceVertices)+i32(aJ.x))*2)+0)].xyz),(surfaceOut[((((ii*u.uSurfaceVertices)+i32(aJ.z))*2)+0)].xyz-surfaceOut[((((ii*u.uSurfaceVertices)+i32(aJ.x))*2)+0)].xyz));\nif((dot(surfaceGeometric,surfaceGeometric)>0.0)){\nvo.vN=normalize(surfaceGeometric);\n}\n}\nvar wp:vec3f=((((surfaceOut[((((ii*u.uSurfaceVertices)+i32(aJ.x))*2)+0)].xyz*aW.x)+(surfaceOut[((((ii*u.uSurfaceVertices)+i32(aJ.y))*2)+0)].xyz*aW.y))+(surfaceOut[((((ii*u.uSurfaceVertices)+i32(aJ.z))*2)+0)].xyz*aW.z))+(vo.vN*(aW.w*(u.uSc*u.uIH[ii].z))));\nvar gbm:i32=i32((u.uGB[(ii)/4][(ii)%4]+0.5));\nif(((u.uGar==1)&&(!((((gbm==4)||(gbm==5))||(gbm==8))||(gbm==9))))){\nwp=vec3f(0.0,(-999.0),0.0);\n}\nif((u.uGar==3)){\nvar gk9:i32=u.uGK;\nvar hv9:i32=select(i32((u.uGB[(ii)/4][(ii)%4]+0.5)),i32((u.uGT[(ii)/4][(ii)%4]+0.5)),((gk9==1)||(gk9==6)));\nvar wr9:bool=(hv9==gk9);\nif((gk9==8)){\nwr9=((((hv9==4)||(hv9==5))||(hv9==8))||(hv9==9));\n}\nif((!wr9)){\nwp=vec3f(0.0,(-999.0),0.0);\n}\n}\nvo.vSt=0.0;\nif(((u.uShd==1)&&(wp.y>(-900.0)))){\nvar gY9:f32=(u.uIP[ii].y+u.uShdP.x);\nvar sd9:vec3f=normalize(u.uSunM);\nvar sy9:f32=max(sd9.y,u.uShdP.y);\nvo.vSt=((wp.y-gY9)/sy9);\nwp=vec3f((wp.x-(sd9.x*vo.vSt)),gY9,(wp.z-(sd9.z*vo.vSt)));\n}\nvo.vW=wp;\nvar rel:vec3f=(wp-u.uCamM);\nvar fw:vec3f=vec3f((u.uYPM.x*u.uYPM.w),u.uYPM.z,(u.uYPM.y*u.uYPM.w));\nvar rt:vec3f=vec3f(u.uYPM.y,0.0,(-u.uYPM.x));\nvar up:vec3f=vec3f(((-u.uYPM.x)*u.uYPM.z),u.uYPM.w,((-u.uYPM.y)*u.uYPM.z));\nvar zv:f32=max(dot(rel,fw),0.081);\nvo.bpos=vec4f(dot(rel,rt),(dot(rel,up)*(u.uResM.x/u.uResM.y)),((zv*(300.08/299.92))-(48.0/299.92)),zv);\nvo.bpos.z=(vo.bpos.z+vo.bpos.w)*0.5;\nreturn vo;\n}\n" as *u8 9 let digest:*u8=sys_mmap(65) 10 gpa_sha(compute,gpe_slen(compute),digest) 11 if fsx_seq(digest,"8c2ed6f1fb5b1c02a807531c5affd4ae76791c80cf8c3b4edb637c5c53e96543" as *u8)!=1{gsb_failed=1;sys_munmap(digest,65);return 0} 12 gpa_sha(vertex,gpe_slen(vertex),digest) 13 if fsx_seq(digest,"11268cd64433ea762cdcd6d1091627eb4ea8d72caf491cca218de011df4edbb1" as *u8)!=1{gsb_failed=1;sys_munmap(digest,65);return 0} 14 sys_munmap(digest,65) 15 var p:i64=gpe_ap(out,pos,"const NX_SURFACE_COMPUTE=" as *u8) 16 p=rk_js_str(out,p,compute,gpe_slen(compute)) 17 p=gpe_ap(out,p,";\nconst NX_ATTACHMENT_VERTEX=" as *u8) 18 p=rk_js_str(out,p,vertex,gpe_slen(vertex)) 19 p=gpe_ap(out,p,";\n" as *u8) 20 p=gpe_ap(out,p,"\nfunction nxGarmentFamilyWorn(kind,id){return id===kind||(kind===8&&(id===4||id===5||id===8||id===9));}\nconst NX_SURFACE_LAYOUT={uSc:0,uIP:16,uIH:208,uSB:400,uOF:1552,uGB:1600,uGT:1648,uGK:1696,uGN:1712,uTm:1760,uWnd:1764,uZMN:1768,uH:1772,uNIP:1776,uDNL:1792,uDNR:1808,uDNB:1824,uDNG:1840,uDNTl:1856,uDNTr:1872,uCamM:1888,uYPM:1904,uResM:1920,uSunM:1936,uHF2:1952,uHM:3296,uGar:3300,uAuth:3304,uShd:3308,uShdP:3312,};\nfunction nxSurfaceLegacyProjection(layout,bytes,shader){\n const prefixBytes=3328,extendedBytes=3344,extra={uSkinMode:3328,uAssetBasis:3332};\n const extended=bytes===extendedBytes;\n const expected=extended?{...NX_SURFACE_LAYOUT,...extra}:NX_SURFACE_LAYOUT;\n const signature=\"uSc:f32,uIP:array<vec3f,12>,uIH:array<vec4f,12>,uSB:array<vec3f,72>,uOF:array<vec4f,3>,uGB:array<vec4f,3>,uGT:array<vec4f,3>,uGK:i32,uGN:array<vec4f,3>,uTm:f32,uWnd:f32,uZMN:f32,uH:f32,uNIP:f32,uDNL:vec4f,uDNR:vec4f,uDNB:vec4f,uDNG:vec4f,uDNTl:vec4f,uDNTr:vec4f,uCamM:vec3f,uYPM:vec4f,uResM:vec2f,uSunM:vec3f,uHF2:array<vec3f,84>,uHM:i32,uGar:i32,uAuth:i32,uShd:i32,uShdP:vec4f,\";\n const uniform=shader.match(/\\bstruct\\s+U\\s*\\{([^}]*)\\}/);\n if((bytes!==prefixBytes&&!extended)||!layout||Object.keys(layout).length!==Object.keys(expected).length||Object.keys(expected).some(k=>layout[k]!==expected[k])||!uniform||uniform[1].replace(/\\s+/g,'')!==signature+(extended?'uSkinMode:i32,uAssetBasis:i32,':''))throw Error('surface-source-layout-identity');\n let lastSource=null,view=null,prefix=null;\n return function(source,target){\n if(!(source instanceof Uint8Array)||source.byteLength!==bytes||!(target instanceof Uint8Array)||target.byteLength!==extendedBytes)throw Error('surface-uniform-packet');\n if(source!==lastSource){lastSource=source;view=new DataView(source.buffer,source.byteOffset,source.byteLength);prefix=new Uint8Array(source.buffer,source.byteOffset,prefixBytes);}\n if(extended&&(view.getInt32(extra.uSkinMode,true)!==0||view.getInt32(extra.uAssetBasis,true)!==0))throw Error('surface-legacy-mode-required');\n target.set(prefix);\n };\n}\nasync function nxGpuSurfaceCreate(dev,G,R,P,mkg,owned){\n const projectLegacy=nxSurfaceLegacyProjection(NXCV,NXCV_SIZE,WGSL_CASTV);\n const packet=P.attachment,h=new Uint32Array(packet,0,16),nr=h[2],nt=h[3],nv=h[4],kind=h[5];\n if(h[0]!==827343175||h[1]!==1||nv!==P.nv||kind<1||kind>9||h[10]!==packet.byteLength||h[6]!==64||h[7]!==64+nr*12||h[8]!==h[7]+nr*16||h[9]!==h[8]+nr*16||h[9]+nt*12!==packet.byteLength)throw Error('native-attachment-packet-contract');\n const rows=12,inputBytes=nv*64,outputBytes=nv*32*rows,uniformBytes=3344,L=dev.limits;\n if(!Number.isSafeInteger(outputBytes)||nv>Math.floor(2147483647/(2*rows))||inputBytes>L.maxStorageBufferBindingSize||inputBytes>L.maxBufferSize||outputBytes>L.maxStorageBufferBindingSize||outputBytes>L.maxBufferSize||Math.ceil(nv/64)>L.maxComputeWorkgroupsPerDimension||rows>L.maxComputeWorkgroupsPerDimension)throw Error('surface-device-budget');\n const input=new Float32Array(nv*16);for(let i=0;i<nv;i++){input.set(P.pos.subarray(i*3,i*3+3),i*16);input.set(P.nrm.subarray(i*3,i*3+3),i*16+4);input.set(P.aj.subarray(i*4,i*4+4),i*16+8);input.set(P.aw3.subarray(i*4,i*4+4),i*16+12);}\n const mk=(size,usage)=>{const b=dev.createBuffer({size,usage:usage|GPUBufferUsage.COPY_DST});owned.push(b);return b;};\n const source=mkg(input,GPUBufferUsage.STORAGE),surface=mk(outputBytes,GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_SRC),cu=mk(uniformBytes,GPUBufferUsage.UNIFORM),vu=mk(uniformBytes,GPUBufferUsage.UNIFORM),fu=mk(NXCF_SIZE,GPUBufferUsage.UNIFORM);\n const dummy=dev.createTexture({size:[1,1],format:'rgba8unorm',usage:GPUTextureUsage.TEXTURE_BINDING|GPUTextureUsage.COPY_DST});owned.push(dummy);dev.queue.writeTexture({texture:dummy},new Uint8Array([128,128,255,255]),{bytesPerRow:4},[1,1]);\n const module=async code=>{const m=dev.createShaderModule({code}),info=await m.getCompilationInfo();const error=info.messages.find(x=>x.type==='error');if(error)throw Error(error.message);return m;};\n const cm=await module(NX_SURFACE_COMPUTE),vm=await module(NX_ATTACHMENT_VERTEX),fm=await module(WGSL_CASTF);\n const cp=await dev.createComputePipelineAsync({layout:'auto',compute:{module:cm,entryPoint:'main'}}),vb=(arrayStride,shaderLocation,format)=>({arrayStride,attributes:[{shaderLocation,offset:0,format}]});\n const pl=await dev.createRenderPipelineAsync({layout:'auto',depthStencil:{format:'depth24plus',depthWriteEnabled:true,depthCompare:'less'},vertex:{module:vm,entryPoint:'main',buffers:[vb(12,0,'float32x3'),vb(16,1,'float32x4'),vb(16,2,'float32x4'),vb(12,3,'float32x3'),vb(8,4,'float32x2'),vb(4,5,'float32')]},fragment:{module:fm,entryPoint:'main',targets:[{format:navigator.gpu.getPreferredCanvasFormat()}]},primitive:{topology:'triangle-list'}});\n const cb=dev.createBindGroup({layout:cp.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:cu}},{binding:1,resource:R.jt.createView()},{binding:3,resource:dummy.createView()},{binding:5,resource:{buffer:source}},{binding:6,resource:{buffer:surface}}]});\n const vg=dev.createBindGroup({layout:pl.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:vu}},{binding:6,resource:{buffer:surface}}]});\n const fg=dev.createBindGroup({layout:pl.getBindGroupLayout(1),entries:[{binding:0,resource:{buffer:fu}},{binding:1,resource:G.t3.createView()},{binding:2,resource:(R.atl||dummy).createView()},{binding:3,resource:R.samp},{binding:4,resource:(R.nrm||dummy).createView()},{binding:5,resource:R.samp}]});\n const vbs=[new Float32Array(packet,h[6],nr*3),new Float32Array(packet,h[7],nr*4),new Float32Array(packet,h[8],nr*4),new Float32Array(nr*3),new Float32Array(nr*2),new Float32Array(nr)].map(x=>mkg(x,GPUBufferUsage.VERTEX));const ib=mkg(new Uint32Array(packet,h[9],nt*3),GPUBufferUsage.INDEX);\n const cv=new ArrayBuffer(uniformBytes),cf=new ArrayBuffer(NXCF_SIZE),cvBytes=new Uint8Array(cv),cfBytes=new Uint8Array(cf),ci=new Int32Array(cv),fi=new Int32Array(cf),ids=new Int32Array(rows);\n const report=window.__nx_surface={state:'ready',kind,source_vertices:nv,attachment_vertices:nr,triangles:nt,input_bytes:inputBytes,output_bytes:outputBytes,dispatches:0,draws:0,rows:0,ids,reason:null};\n let surfaceFrame=null;\n const state={kind,rows:0,prepare(enc,G,N,count,nm){state.rows=0;report.rows=0;if(!Number.isInteger(count)||count<1||count>rows||!N.nativeIds)throw Error('surface-actor-rows');let worn=false;\n for(let i=0;i<count;i++){const id=N.nativeIds[i];if(!Number.isInteger(id)||id<0||id>=nm)throw Error('surface-actor-id');for(let j=0;j<i;j++)if(ids[j]===id)throw Error('surface-actor-duplicate');ids[i]=id;const wardrobe=(kind===1||kind===6)?N.gt:N.gb;if(wardrobe&&nxGarmentFamilyWorn(kind,Math.round(wardrobe[i])))worn=true;}\n if(!worn)return;projectLegacy(G.castCVBytes,cvBytes);ci[3328/4]=nv;ci[3332/4]=count;if(ci[NXCV.uGar/4]!==0||ci[NXCV.uHM/4]!==1)throw Error('surface-mode');dev.queue.writeBuffer(cu,0,cv);const pass=enc.beginComputePass();pass.setPipeline(cp);pass.setBindGroup(0,cb);pass.dispatchWorkgroups(Math.ceil(nv/64),count);pass.end();\n ci[NXCV.uGar/4]=3;ci[NXCV.uGK/4]=kind;dev.queue.writeBuffer(vu,0,cv);cfBytes.set(G.castCFBytes);fi[NXCF.uGar/4]=3;fi[NXCF.uGK/4]=kind;dev.queue.writeBuffer(fu,0,cf);state.rows=count;report.rows=count;report.dispatches++;report.state='prepared';surfaceFrame={dispatch:report.dispatches,rows:count,ids:Array.from(ids.subarray(0,count)),observed_ms:performance.now(),simulation_time:new Float32Array(cv)[NXCV.uTm/4]};\n },draw(rp,mesh){fi[NXCF.uGM/4]=mesh.m;dev.queue.writeBuffer(fu,0,cf);rp.setPipeline(pl);rp.setBindGroup(0,vg);rp.setBindGroup(1,fg);for(let k=0;k<6;k++)rp.setVertexBuffer(k,vbs[k]);rp.setIndexBuffer(ib,'uint32');rp.drawIndexed(nt*3,state.rows);rp.setPipeline(G.cpl);report.draws++;report.state='submitted';}};\n let observationPending=false;\n report.observe=async function(request){\n if(!request||!Array.isArray(request.vertices)||!Array.isArray(request.actors)||!request.vertices.length||!request.actors.length)throw Error('surface-observe-request');\n const vertices=request.vertices.slice(),actors=request.actors.slice();\n if(vertices.length>nv||actors.length>(surfaceFrame?.rows||0)||vertices.some((v,i)=>!Number.isInteger(v)||v<0||v>=nv||vertices.indexOf(v)!==i)||actors.some((id,i)=>!Number.isInteger(id)||actors.indexOf(id)!==i))throw Error('surface-observe-range');\n if(observationPending)throw Error('surface-observe-busy');\n if(GPU!==G||G.npc!==R||!nxResidentCurrent(P.residentToken)||!surfaceFrame)throw Error('surface-observe-stale-owner');\n const sourceRows=actors.map(id=>surfaceFrame.ids.indexOf(id));if(sourceRows.some(row=>row<0))throw Error('surface-observe-actor');\n const bytes=actors.length*vertices.length*32;if(!Number.isSafeInteger(bytes)||bytes>dev.limits.maxBufferSize)throw Error('surface-observe-device-budget');\n const frame={dispatch:surfaceFrame.dispatch,simulation_time:surfaceFrame.simulation_time,evidence_age_ms:performance.now()-surfaceFrame.observed_ms,source_vertices:nv,rows:surfaceFrame.rows,actors,vertices,source:'shared-generated-surface',normal:'shared-transformed-source-vertex-normal'};\n const read=dev.createBuffer({size:bytes,usage:GPUBufferUsage.COPY_DST|GPUBufferUsage.MAP_READ});let mapped=false;observationPending=true;\n try{const enc=dev.createCommandEncoder();for(let a=0;a<actors.length;a++)for(let v=0;v<vertices.length;v++)enc.copyBufferToBuffer(surface,(sourceRows[a]*nv+vertices[v])*32,read,(a*vertices.length+v)*32,32);dev.queue.submit([enc.finish()]);await read.mapAsync(GPUMapMode.READ);mapped=true;const data=new Float32Array(read.getMappedRange());\n if(GPU!==G||G.npc!==R||!nxResidentCurrent(P.residentToken))throw Error('surface-observe-owner-replaced');\n return {...frame,readback_bytes:bytes,values:actors.map((id,a)=>({id,vertices:vertices.map((index,v)=>({index,position:Array.from(data.slice((a*vertices.length+v)*8,(a*vertices.length+v)*8+3)),normal:Array.from(data.slice((a*vertices.length+v)*8+4,(a*vertices.length+v)*8+7))}))}))};\n }finally{if(mapped)read.unmap();read.destroy();observationPending=false;}\n };\nreturn state;\n}\n" as *u8) 21 return p 22} 23// ORIGINAL. Private generated-page observation; never calls either publishing main. 24// Private page-fit composition only; no publisher main. Presentation helpers copied exactly from existing nx_craft_integrated_emit_t73 owner. The fit supplies already compiled, hash-verified bytecode. 25// nx_game_page_emit.nx -- the SOVEREIGN game-page emitter. Until now the browser page for a wasm 26// game was assembled by printf blocks in _ops/nx_emit_game.sh -- real emission logic living in 27// SHELL, against the doctrine (shell only LAUNCHES sovereign ELFs). This organ owns it: reads the 28// .wasm, base64-encodes it (composes nx_base64 -- one encoder, not a coreutils dependency), and 29// emits the complete self-contained page (marker, shim, input forwarding, persistence bytes-mover). 30// usage: nx_game_page_emit <wasm> <out.html> <title> <native-client-name> <controls-html> 31// 32// RECOVERED 2026-08-02 from buildroot/_build/nx_craft_emit.s (the 14:07 build's data section) 33// after a failed in-place rewrite TRUNCATED this file to 0 bytes and the damage was copied to 34// both trees. Every emitted chunk below is the exact byte sequence that build shipped, so the 35// recovered emitter is byte-equivalent to the last one that passed both gates. 36// license_tier: ORIGINAL 37// syscalls.nx -- thin __syscall wrappers used across modules. 38// 39// Sovereign path: no libc. Every memory allocation, file op, and 40// clock read in the rest of the runtime routes through one of these 41// helpers. Numbers match Linux RV64; NishiOS uses the same set. 42// 43// Extracted from runtime.nx and ir.nx's copy-pasted helpers so the 44// module-import build doesn't produce duplicate symbols. 45 46// Tier aliases (nx_size / nx_idx / nx_fd / ...) ride along with the 47// syscall shelf: 141 runtime files use `as nx_size` etc. and only 48// compiled historically because the old parser silently void-cast 49// unknown type names (T#nx-int-alias-size-0 closed that hole LOUDLY, 50// which exposed the missing import). nx_tier.nx is pure type 51// aliases (0 funcs); prepass_register_aliases skips duplicates, so 52// modules that also import it directly stay fine. 53// nx_tier.nx -- substrate-wide tier configuration. 54// 55// Single point of edit for scale-agnostic substrate. Per user 56// directive 2026-05-13: "with the i64 it looks hardcoded everywhere 57// if we really want this dynamic dont we want that to be a changeable 58// value everywhere so it can switch to i128 and i256 etc." 59// 60// Per cardinals: 61// - feedback-numeric-tier-ladder.md (N0..N9 swap) 62// - feedback-scale-agnostic-substrate.md (MCU..HPC swap) 63// - feedback-substrate-additive-not-restrictive.md (declare cost) 64// 65// SEMANTIC ALIASES (not all should swap simultaneously): 66// 67// nx_int -- DEFAULT ARITHMETIC integer. Swappable across the 68// numeric tier ladder. Swap this to i128 to make the 69// entire substrate compute in 128-bit integers. 70// 71// nx_size -- MEMORY-SIZE integer. Always platform-pointer-width. 72// Used for buffer sizes, mmap byte counts, struct 73// sizes. Does NOT swap with nx_int -- changing this 74// would break pointer arithmetic. Stays i64 on RV64. 75// 76// nx_idx -- ARRAY-INDEX integer. Same width as nx_size on 77// flat-memory targets. Distinct alias so future 78// GPU/distributed targets can change indexing without 79// touching arithmetic. 80// 81// nx_byte -- The byte type. Stays u8. Distinct alias so MCU 82// targets that emulate u16-byte memory could rebind. 83// 84// HARDWARE-TIER BUFFER SIZES (declare cost, don't restrict): 85// 86// NX_BUF_TINY -- 64 B (MCU-friendly; stack-safe) 87// NX_BUF_SMALL -- 256 B (MCU heap-friendly) 88// NX_BUF_MEDIUM -- 4096 B (page-size; workstation default) 89// NX_BUF_LARGE -- 64 KiB (server-friendly) 90// NX_BUF_HUGE -- 1 MiB (HPC; assumes virtual memory) 91// 92// Use these instead of `sys_mmap(4096)` etc. so the substrate 93// announces its memory footprint and tier-incompatible code can 94// be flagged by audit. 95// 96// HARDWARE TIER (informational; downstream code may branch): 97// 98// NX_TIER_MCU = 0 -- microcontroller, kilobytes RAM 99// NX_TIER_SOVEREIGN_CHIP = 1 -- custom silicon, ~MB RAM 100// NX_TIER_FAMILY_DEVICE = 2 -- phone/router, ~GB RAM 101// NX_TIER_WORKSTATION = 3 -- laptop/desktop, ~10-100 GB RAM 102// NX_TIER_SERVER = 4 -- server-class, ~TB RAM 103// NX_TIER_HPC = 5 -- cluster, distributed 104// 105// COMPILE-TIME SWAP for nx_int (uncomment exactly one line): 106 107// THIS FILE IS THE SINGLE DEFINITION SITE for substrate-wide types. 108// Per user directive 2026-05-13: only this file (and platform-ABI 109// definition files like nx_syscalls.nx) should declare bare i64. 110// Every other substrate module uses the aliases below. 111 112// ===== arithmetic-tier aliases (swappable per nx_int tier ladder) ===== 113 114type nx_int = i64 // N1 -- default; 9 quintillion, fits all physical scales 115// type nx_int = i32 // N0 -- MCU / embedded 116// type nx_int = i128 // N2 -- queued; needs nx_i128 backend ops 117// type nx_int = i256 // N3 -- shipped (nx_i256.nx); cosmology / crypto 118 119// ===== platform-width aliases (stay at pointer width) ================= 120 121type nx_size = i64 // memory-size / byte-count 122type nx_idx = i64 // array-index 123type nx_byte = u8 // single-byte unit 124 125// ===== POSIX/Linux platform-ABI aliases (mandated 64-bit on RV64) ==== 126// 127// Each is a 64-bit integer by Linux RV64 ABI. Renamed here so substrate 128// code never writes bare `i64` for these semantic types. 129 130type nx_fd = i64 // file descriptor (kernel-mandated width) 131type nx_exit = i64 // exit / status code (main() return) 132type nx_pid = i64 // process id 133type nx_uid = i64 // user id 134type nx_gid = i64 // group id 135type nx_syscall_num = i64 // Linux syscall number 136type nx_off = i64 // file offset (off_t) 137type nx_errno = i64 // errno (negative on syscall failure) 138 139// ===== SEMANTIC TYPE GENEALOGY (added 2026-05-20) ====================== 140// 141// Per cardinal [[feedback-type-genealogy-math-cardinal-not-script]] 142// AND its immediate refinement (same session): every alias collapsing 143// to i64 is "y2k incestuous" -- relabeling, not genealogy. Real 144// semantic types pick the APPROPRIATE underlying width based on 145// the physics of the values they represent: 146// 147// - Small sealed enums (15 outcomes, 18 probe kinds) -> u8 148// - Display pixel coords (~32M max realistic) -> i32 149// - Q10 / Q14 fixed-point (values * 1024 / 16384) -> i32 150// - 32-bit color packs (RGBA8888) -> u32 151// - Q20 fixed-point (values * 1048576) -> i64 152// - Wide color packs (RGBA16161616, PRESERVE_ALL) -> u64 153// - Timestamps (ns / us / ms / cycles) -> i64 (2038 Y2K38) 154// - 64-bit hash digests -> u64 155// - Cryptographic hashes (SHA-256, SHA-512) -> STRUCT (multi-word; queued) 156// - Virtual addresses on 64-bit ISA -> u64 157// 158// Each type is a child of its PHYSICALLY-APPROPRIATE parent 159// (i8/u8/i32/u32/i64/u64), not blanket-i64. This breaks the 160// y2k-incestuous trap where renaming i64 N ways pretends to be 161// type discipline while every value silently shares one width. 162 163// ----- TIME family (all i64; ns/us/ms/cycles legitimately need it) ----- 164// 2038 Y2K38 lurks for 32-bit time_t; i64 is the substrate-honest 165// choice. ms/us/ns + cycles all i64. s_q14 needs only i32 range 166// (val*16384 fits comfortably in i32 for typical second scales) but 167// we stay at i64 to compose cleanly with the i64 time arithmetic 168// across the substrate. 169type nx_ns = i64 // nanoseconds (since boot, monotonic) 170type nx_us = i64 // microseconds (since boot, monotonic) 171type nx_ms = i64 // milliseconds (since epoch, wall) 172type nx_s_q14 = i64 // seconds in Q14 fixed-point 173type nx_cycles = i64 // CPU cycle count 174 175// ----- HASH family (non-cryptographic 64-bit; crypto = STRUCT) ----- 176// FNV-1a / xxhash digest is u64 by spec. SHA-256 / SHA-512 / BLAKE 177// hashes are MULTI-WORD; they're declared as structs in 178// nx_sha256.nx / nx_sha512.nx / nx_blake2b.nx (each carries its own 179// fixed-size byte array; NOT i64). 180type nx_hash64 = u64 // FNV-1a / xxhash / truncated SHA -- 64-bit digest 181 182// ----- ETG family (sealed enums; small value space -> u8) ----- 183// nx_outcome_id sealed enum has 11 values; u8 fits 256 184// nx_probe_kind sealed enum has 18 values; u8 fits 256 185// nx_claim_source sealed enum has 13 values; u8 fits 256 186// nx_silicon_serial is a content-addressed identity HASH; u64. 187type nx_outcome_id = u8 // NX_ETG_OUTCOME_* (11 values; u8 fits) 188type nx_probe_kind = u8 // NX_ETG_PROBE_* (18 values; u8 fits) 189type nx_claim_source = u8 // NX_ETG_CLAIM_* (13 values; u8 fits) 190type nx_silicon_serial = u64 // per-die identity hash (cryptographic-strength width) 191 192// ----- PERF family (sealed enums) ----- 193type nx_pathology_id = u8 // NX_PERF_PATH_* (15 values; u8 fits) 194type nx_flow_state_id = u8 // NX_FLOW_STATE_* (6 values; u8 fits) 195 196// ----- FIXED-POINT family (width chosen by precision*range) ----- 197// Q10: value * 1024. Typical seed values are 0..255 so q10 max is 198// ~261K; i32 holds up to ~2.1B -> plenty of headroom. 199// Q14: value * 16384. Typical max around 16K of seed -> q14 ~ 2.6e8; 200// i32 holds up to 2.1e9 -> headroom for a few decimal seconds. 201// Q20: value * 1048576. Wider precision; needs i64 to avoid wrap. 202type nx_q10 = i32 // val * 1024; ~0.001 precision 203type nx_q14 = i32 // val * 16384; ~6e-5 precision 204type nx_q20 = i64 // val * 1048576; ~1e-6 precision 205 206// ----- GRAPHICS family (display coords + color packs at real widths) ----- 207// Modern displays are well within 32-bit pixel addressing. 208// 8K display = 7680x4320 pixels. i32 holds 2.1B -> plenty. 209// nx_color_rgba8 = 32-bit packed RGBA (the common case) 210// nx_color_rgba16 = 64-bit packed RGBA16161616 (HDR / wide gamut) 211type nx_pixel_x = i32 // screen X in pixels 212type nx_pixel_y = i32 // screen Y in pixels 213type nx_color_rgba8 = u32 // RGBA8888 packed 214type nx_color_rgba16 = u64 // RGBA16161616 packed (HDR / preserve-all) 215 216// ----- PERCEPTUAL family (sealed enum; small value space) ----- 217// nx_perceptual_profile has ~40 declared values up through 218// NX_PERCEPT_PRESERVE_ALL = 9999. Sentinel value 9999 needs i16, 219// not u8. i16 fits -32768..32767 with room for sentinels. 220type nx_perceptual_profile = i16 // NX_PERCEPT_* (~40 values + 9999 sentinel) 221 222// ----- ADDRESS family (virtual addresses on 64-bit ISA) ----- 223// Pointer-width is u64 on all our supported 64-bit targets 224// (RV64 / x86_64 / AArch64 / ppc64le / loongarch64 / mips64 / 225// s390x / RV32 uses u32 -- TODO: tier-conditional). 226type nx_addr = u64 // raw virtual address (caller casts to *u8) 227 228// nx_capability_manifest: 229// variant_class: tier_config 230// variant_id: tier_config_v1_global 231// requires_isa: [rv32i, rv32imac, rv64imac, rv64imacv, x86_64, aarch64, armv7a, cortex_m, avr, xtensa, wasm32] 232// requires_syscalls: [] 233// requires_ram_min_b: 0 // pure-const + typedef module, no runtime cost 234// tier_floor: NX_TIER_MCU 235// tier_ceiling: NX_TIER_HPC 236// cost_model: 237// flops_per_n: 0.0 238// bytes_per_n: 0.0 239// syscalls_per_n: 0.0 240// adversary_class: THREAT_OPPORTUNISTIC 241// 242// Note: This file is the substrate's TIER ENUM SOURCE OF TRUTH. It 243// has no variants by design (it IS the variant_class taxonomy that 244// other primitives' tier_floor / tier_ceiling reference). Manifest 245// declared for hygiene completeness; selector will skip it. 246 247// ---- buffer-size constants (use instead of bare numbers) ------- 248 249const NX_BUF_TINY: nx_size = 64 250const NX_BUF_SMALL: nx_size = 256 251const NX_BUF_MEDIUM: nx_size = 4096 252const NX_BUF_LARGE: nx_size = 65536 253const NX_BUF_HUGE: nx_size = 1048576 254 255// ---- hardware tier sentinels ----------------------------------- 256 257const NX_TIER_MCU: nx_int = 0 258const NX_TIER_SOVEREIGN_CHIP: nx_int = 1 259const NX_TIER_FAMILY_DEVICE: nx_int = 2 260const NX_TIER_WORKSTATION: nx_int = 3 261const NX_TIER_SERVER: nx_int = 4 262const NX_TIER_HPC: nx_int = 5 263 264// ---- numeric tier sentinels (informational) -------------------- 265 266const NX_NUM_N0_I32: nx_int = 0 267const NX_NUM_N1_I64: nx_int = 1 268const NX_NUM_N2_I128: nx_int = 2 269const NX_NUM_N3_I256: nx_int = 3 270const NX_NUM_N4_I512: nx_int = 4 271const NX_NUM_N5_BIGINT: nx_int = 5 272 273// ---- byte-width of substrate types (replace bare `8` / `4`) ---- 274// 275// Use these wherever you need the byte count of a substrate type -- 276// e.g., sys_mmap(N * NX_SIZEOF_NX_SIZE) to allocate N nx_size slots. 277// Swap nx_int's underlying type and ONLY this constant changes. 278 279const NX_SIZEOF_NX_INT: nx_size = 8 // nx_int currently i64 -> 8 bytes 280const NX_SIZEOF_NX_SIZE: nx_size = 8 // nx_size always pointer-width 281const NX_SIZEOF_NX_IDX: nx_size = 8 // nx_idx alias of nx_size 282 283// ---- POSIX stdio file descriptors (replace bare 0/1/2) --------- 284 285const NX_FD_STDIN: nx_fd = 0 286const NX_FD_STDOUT: nx_fd = 1 287const NX_FD_STDERR: nx_fd = 2 288 289const SYS_MAGIC_1024: i64 = 1024 290const SYS_MAGIC_1000000: i64 = 1000000 291const SYS_MAGIC_4294967296: i64 = 4294967296 292// first read window for a size-UNKNOWABLE file (lseek END <= 0); doubles while it fills -- see sys_read_file 293const SYS_READ_GROW_INIT: i64 = 65536 294const SYS_MAGIC_100000: i64 = 100000 295 296// ---- syscall numbers (per-target) ---- 297// 298// Cross-target via the macro processor (cardinal landed 2026-05-20: 299// feedback-hardware-agnostic-is-robustness -- the substrate must 300// compile + run on every silicon we point it at). Default path 301// (TARGET_X86_64 not defined) carries Linux RV64 numbers used by 302// qemu-RV64 + NishiOS. When nxc2 is invoked with --target x86_64 303// main.c pre-defines @macro TARGET_X86_64 1 so this file resolves 304// to x86_64 Linux ABI numbers. 305// 306// nx_syscalls_x86_64.nx remains the dedicated x86_64-only mirror 307// for files that want explicit single-target imports (e.g., bench 308// smokes built only for x86_64). This block makes nx_syscalls.nx 309// itself dual-target so substrate primitives compile portably. 310 311@ifdef TARGET_X86_64 312const SYS_READ: i64 = 0 313const SYS_WRITE: i64 = 1 314const SYS_CLOSE: i64 = 3 315const SYS_LSEEK: i64 = 8 316const SYS_OPENAT: i64 = 257 317const SYS_EXIT: i64 = 60 318const SYS_MMAP: i64 = 9 319const SYS_CLOCK_GETTIME: i64 = 228 320const SYS_IOCTL: i64 = 16 321const SYS_CLOCK_NANOSLEEP: i64 = 230 322// Namespace/container family, x86 branch (debt 1785528831). Moved here from 323// nx_syscalls_x86_64.nx so ONE module owns the wrapper set -- a TU reaching both 324// modules used to hold every wrapper TWICE, resolved silently by definition ORDER. 325const SYS_CHROOT: i64 = 161 326const SYS_MOUNT: i64 = 165 327const SYS_UNSHARE: i64 = 272 328const SYS_GETUID: i64 = 102 329const SYS_GETGID: i64 = 104 330const SYS_POLL: i64 = 7 331@endif 332 333@ifndef TARGET_X86_64 334const SYS_READ: i64 = 63 335const SYS_WRITE: i64 = 64 336const SYS_CLOSE: i64 = 57 337const SYS_LSEEK: i64 = 62 338const SYS_OPENAT: i64 = 56 339const SYS_EXIT: i64 = 93 340const SYS_MMAP: i64 = 222 341const SYS_CLOCK_GETTIME: i64 = 113 342const SYS_IOCTL: i64 = 29 343const SYS_CLOCK_NANOSLEEP: i64 = 115 344// Namespace/container family, RV64 branch (debt 1785528831). This is the branch actually 345// KEPT (TARGET_X86_64 is hard-pinned undefined), so these are the numbers the x86 backend 346// translates at emit: 51->161 chroot, 40->165 mount, 97->272 unshare, 174->102 getuid, 347// 176->104 getgid. The 40 and 51 rows were added to x86ctx_rv64_to_x86_64_syscall and 348// shipped FIRST -- without them both would pass through to the WRONG x86 syscall 349// (sendfile / getsockname), silently, because that translator's default is `return num`. 350const SYS_CHROOT: i64 = 51 351const SYS_MOUNT: i64 = 40 352const SYS_UNSHARE: i64 = 97 353const SYS_GETUID: i64 = 174 354const SYS_GETGID: i64 = 176 355const SYS_POLL: i64 = 73 356@endif 357 358func sys_ioctl(fd: i64, request: i64, arg: i64) -> i64 { 359 return __syscall(SYS_IOCTL, fd, request, arg, 0, 0, 0) 360} 361 362// poll(2): wait for events on fds. fds points to an array of `nfds` 363// struct pollfd { i32 fd; i16 events; i16 revents } (8 bytes each). 364// timeout_ms < 0 = block forever, 0 = return immediately. Returns the 365// count of ready fds (>0), 0 on timeout, or -errno. Used by the 366// substrate's own network diagnostics (bounded non-blocking connect) 367// instead of reaching for external tools. (rv64 const = ppoll; this 368// wrapper only runs on the x86_64 target.) 369func sys_poll(fds: *u8, nfds: i64, timeout_ms: i64) -> i64 { 370 return __syscall(SYS_POLL, fds, nfds, timeout_ms, 0, 0, 0) 371} 372 373// ---- core wrappers ---- 374 375func sys_write(fd: i64, buf: *u8, count: i64) -> i64 { 376 return __syscall(SYS_WRITE, fd, buf, count, 0, 0, 0) 377} 378 379func sys_read(fd: i64, buf: *u8, count: i64) -> i64 { 380 return __syscall(SYS_READ, fd, buf, count, 0, 0, 0) 381} 382 383func sys_close(fd: i64) -> i64 { 384 return __syscall(SYS_CLOSE, fd, 0, 0, 0, 0, 0) 385} 386 387// chdir. The compiler only rv64->x86 translates CONSTANT syscall numbers (x86ctx_emit_syscall: 388// VK_CONST_INT); chdir is absent from that table, so a constant 49 falls through to x86_64 bind and a 389// constant 80 is mapped to fstat -- BOTH gave EBADF (PROBE-PROVEN by test_chdir). The documented escape 390// (nx_x86_64_ctx.nx:1004 "Runtime-computed syscall number -- load as-is") is to make op0 RUNTIME: a memory 391// load can't be folded to VK_CONST_INT, so the raw x86_64 number 80 passes through untranslated = real 392// chdir. Used by the supervisor to set a spawned daemon's CWD before execve. 0 on success, -errno on fail. 393func sys_chdir(path: *u8) -> i64 { 394 let nbox: *i64 = sys_mmap(16) as *i64 395 nbox[0] = 80 // x86_64 chdir, forced runtime so the rv64->x86 xlate is skipped 396 return __syscall(nbox[0], path as i64, 0, 0, 0, 0, 0) 397} 398 399// getcwd -- SAME runtime-number escape as sys_chdir directly above, for the same documented reason: the 400// rv64->x86 translator only rewrites CONSTANT syscall numbers, and getcwd is absent from that table, so a 401// constant would be mangled exactly as chdir's was. A memory load cannot be folded to VK_CONST_INT, so the 402// raw x86_64 number passes through untranslated. 403// WHY THIS EXISTS (2026-08-14): the shim had sys_chdir but NOTHING to ask where we are. Every organ that 404// resolves a path against the CWD could therefore only print a RELATIVE path -- a claim whose truth depends 405// on invisible state. Three separate working-directory faults in one session stayed invisible until they 406// bit, and in each the reader could not tell "the file is missing" from "I am standing somewhere else". 407// ★★★AN ORGAN THAT CANNOT REPORT WHERE IT IS CANNOT WRITE AN HONEST PATH. 408// Returns the byte length written INCLUDING the terminator, or -errno (notably -ERANGE if cap is short). 409// SYS_PATH_MAX is exported so a caller never hand-writes the size: the FIRST consumer of sys_getcwd (this 410// author, minutes after adding it) wrote `sys_mmap(4096)` and `sys_getcwd(buf, 4096)` on consecutive 411// lines -- a bare literal AND a duplicate-authored pair, the exact shape being removed elsewhere the same 412// day. ★★A NEW PRIMITIVE THAT DOES NOT EXPORT ITS OWN SIZE INVITES EVERY CALLER TO INVENT ONE. 413const SYS_PATH_MAX: i64 = 4096 // Linux PATH_MAX; getcwd returns -ERANGE below it 414// The DIRECTORY sibling of MODE_0644, added on the same evidence: `0x1ed` appears at 569 sites in 415// buildroot/runtime (nx_shelltool, corpus_complete=1), i.e. the estate scatters TWO file-mode constants, 416// not one. Named here so the pair lives together and a reader meets both at the same place. 417const MODE_0755: i64 = 0x1ed // rwxr-xr-x : default mode for a created directory 418func sys_getcwd(buf: *u8, cap: i64) -> i64 { 419 let nbox: *i64 = sys_mmap(16) as *i64 420 nbox[0] = 79 // x86_64 getcwd, forced runtime so the rv64->x86 xlate is skipped 421 return __syscall(nbox[0], buf as i64, cap, 0, 0, 0, 0) 422} 423 424// ⚠AT_FDCWD MOVED UP 2026-07-20 -- IT WAS A LIVE MISCOMPILE. This const was declared ~60 lines BELOW 425// (in the openat block) while sys_unlinkat and sys_fchmodat immediately below REFERENCE it. A module 426// const referenced ABOVE its declaration does not resolve, and nx_cc silently substituted CONSTANT 0 427// -- so both wrappers passed dirfd=0 (stdin) instead of -100. Absolute paths survive that (openat 428// ignores dirfd when the path is absolute), RELATIVE paths do not, which is exactly why unlinkat was 429// long recorded as flaky and "passing only by luck". Surfaced by the new unknown-identifier 430// diagnostic, which turned a silent 0 into a compile error. LAW (already banked, now enforced): 431// module-wide consts/statics go ABOVE every possible reader. 432const AT_FDCWD: i64 = -100 433 434// unlinkat(AT_FDCWD, path, 0) -- delete a file. x86_64 263 is a PROVEN pass-through (not an rv64 key), 435// but this is THE canonical home: 5+ organs hand-rolled `__syscall(263,...)` before this landed (DRY, 436// 2026-07-20). 0 on success, -errno on fail. 437func sys_unlinkat(path: *u8) -> i64 { 438 return __syscall(263, AT_FDCWD, path as i64, 0, 0, 0, 0) 439} 440 441// fchmodat(AT_FDCWD, path, mode) -- chmod by path. ⚠a CONSTANT 268 gets rv64->x86 TRANSLATED to the 442// wrong syscall (silent no-op chmod -- cost a vacuous-permission-test debug cycle, 2026-07-20), so the 443// number is forced RUNTIME via the sys_chdir nbox pattern. 0 on success, -errno on fail. 444func sys_fchmodat(path: *u8, mode: i64) -> i64 { 445 let nbox: *i64 = sys_mmap(16) as *i64 446 nbox[0] = 268 // x86_64 fchmodat, forced runtime so the xlate is skipped 447 return __syscall(nbox[0], AT_FDCWD, path as i64, mode, 0, 0, 0) 448} 449 450// exit_group(2) -- terminate ALL tasks in the thread group. Raw x86_64 231 451// (231 is NOT an rv64 key in the compiler's swap table, so it passes through 452// untranslated -- the munmap-11 precedent). THE explicit program-exit call 453// once a process holds live nx_thread_pool workers: CLONE_VM tasks are 454// separate PIDs, so plain sys_exit (93 -> x86 60, single task) leaves them 455// running, holding stdout open and wedging any pipeline that waits for EOF 456// (found 2026-07-07: the shared-pool matmul dispatcher hung the build lane 457// this way). Return-from-main already exit_groups via the _start trampoline; 458// use THIS for explicit early program exit. Per-THREAD exit stays sys_exit 459// (see nx_thread_exit). 460func sys_exit_group(code: i64) -> i64 { 461 return __syscall(231, code, 0, 0, 0, 0, 0) 462} 463 464// setpriority(PRIO_PROCESS=0, who=0 -> SELF, prio) -- x86_64 syscall 141. 465// Lower priority = larger nice value; 19 is the maximum yield. 466// WHY A WRAPPER AND NOT AN OPERATOR STEP (measured 2026-07-30): a bulk media 467// migration walk saturated the NAS; every forked organ queued behind its I/O so 468// EVERY agent MCP call 503'd for minutes -- the control plane went blind while a 469// background job did exactly what it was told. `renice 19` on the running pid 470// restored interactive service at once. 471// LAW: a long-running BULK job must yield to the interactive control plane BY 472// CONSTRUCTION at its own launch, not when an operator notices. Bind it to the 473// one act every bulk job performs (its startup) and nothing has to remember it. 474// WARN: `ionice` does NOT exist on the Synology busybox, so the I/O-class lever 475// is unavailable; CPU nice sufficed because the walk is SHA-256-bound over 476// cached reads (state R, not D, once niced). 477func sys_setpriority(prio: i64) -> i64 { 478 return __syscall(141, 0, 0, prio, 0, 0, 0) 479} 480 481// ADDITIVE TWIN 2026-08-04 (nx_resgov): re-nice ANOTHER process by pid. The incumbent above pins 482// who=0 = "me", so it cannot deprioritise a runaway -- and a governor that can only slow ITSELF has 483// no graceful rung between "observe" and "kill". PRIO_PROCESS=0, who=pid. Existing callers untouched 484// (rule 19: add the new entry point, never re-shape the one in service). 485func sys_setpriority_of(pid: i64, prio: i64) -> i64 { 486 return __syscall(141, 0, pid, prio, 0, 0, 0) 487} 488 489// munmap -- free a region from sys_mmap. x86_64 munmap = 11; 11 is NOT an rv64 number in the compiler's 490// swap table, so the literal passes through untranslated = real munmap (unlike chdir, where rv64 80=fstat 491// intercepted it). CRITICAL for long-running loops: the supervisor's per-poll proc_* scans mmap 64KB+ each; 492// unfreed, the leak hits DSM's RLIMIT_AS -> mmap returns -12 -> the code writes through it -> SEGFAULT 493// (dmesg-proven: nx_hostctl segfault at 0xfffffffffffffff4). Free scan buffers to keep the supervisor alive. 494// ===== SMALL-ALLOCATION BUMP ARENA (2026-08-06, debt 1785516350 / 1786055008) ===================== 495// MEASURED FIRST, THEN BUILT. nx_arena_probe: 20,000 x sys_mmap(32) -> VmSize 80,172 kB, 496// VmRSS 80,024 kB. 640 KB of requested data cost 78 MB of RESIDENT memory -- 4096 bytes per 32-byte 497// request, exactly one page and one kernel VMA each. Across the corpus nx_mmapbal deep counts 17,157 498// functions / 43,498 sites that allocate and never return, so this multiplier is the actual shape of 499// the leak: the call sites are not individually wrong so much as individually EXPENSIVE. 500// 501// One VMA per call is also a HARD CORRECTNESS CEILING, not just a memory cost: vm.max_map_count 502// defaults to 65530, after which mmap returns -ENOMEM and callers write through the failed pointer. 503// That is precisely the dmesg-proven nx_hostctl SEGFAULT at 0xfffffffffffffff4 described below. 504// 505// SO: requests <= NXA_SMALL_MAX are bump-allocated out of a 256 KiB chunk (one VMA per ~5,400 small 506// allocations instead of one per allocation). Larger requests take the ORIGINAL path untouched -- 507// they are the ones plausibly relying on page alignment, and they are not where the leak lives. 508// 509// THE ZEROING CONTRACT IS LOAD-BEARING AND IS PRESERVED BY NEVER RECYCLING. Callers rely on mmap 510// returning zeroed memory (nx_mmapbal: "mmap zeroes, so an untouched slot reads empty with no init 511// loop"). Bytes handed out here come from a freshly mmapped chunk and are NEVER handed out twice, so 512// every region is zero-filled exactly as before. LIFO give-back on munmap was deliberately REJECTED: 513// it would recover memory but hand back dirty bytes, silently breaking every caller that trusts the 514// zero -- a correctness regression traded for a memory win, which is the wrong trade. 515// 516// KNOWN TRADE-OFF, stated rather than hidden: small allocations are now ADJACENT within a chunk 517// instead of isolated in their own pages. An overrun that today walks off the end of a page and 518// SIGSEGVs loudly may instead corrupt a neighbouring allocation quietly. NXA_GAP puts slack between 519// allocations and NXA_SMALL_MAX is kept deliberately low to bound the exposure, but the risk is real 520// and is the reason this starts at 256 rather than a page. 521// ---- MEMORY ORDERING, THE ONE DEFINITION ------------------------------------------------------- 522// Moved here from nx_atom.nx on 2026-08-25 and DELETED from its two other copies 523// (nx_atomic_intrinsic_test, nx_simd_i32x8_test). Measured before the move, corpus_complete=1: 524// THREE files each declared NX_MO_SEQ_CST = 5 independently. A constant written in three places is 525// three rulers that agree until one of them does not. 526// 527// They live at THIS layer because the arena allocator below needs an ordering value for its own 528// lock, and this file cannot import nx_atom.nx -- nx_atom imports THIS file, so that direction is a 529// cycle. Everything that had these constants still has them: nx_atom.nx imports this file, and so 530// does every consumer of nx_atom. 531// 532// The __atomic_* forms these feed are COMPILER INTRINSICS, not library calls, so this file can use 533// them with no import at all. Verified in nx_x86_64_ctx rather than assumed: __atomic_cas_i64 emits 534// `lock cmpxchgq`, __atomic_faa_i64 emits `lock xaddq`, __atomic_fence emits `mfence`. On x86-64 the 535// ordering operand is not consulted by the emitter because those instructions are full barriers 536// regardless; it is carried for the RV64A backend, where it selects the aq/rl bits. 537const NX_MO_RELAXED: i64 = 0 538const NX_MO_CONSUME: i64 = 1 539const NX_MO_ACQUIRE: i64 = 2 540const NX_MO_RELEASE: i64 = 3 541const NX_MO_ACQ_REL: i64 = 4 542const NX_MO_SEQ_CST: i64 = 5 543 544const NXA_SMALL_MAX: i64 = 256 545const NXA_CHUNK: i64 = 262144 546const NXA_ALIGN: i64 = 16 547const NXA_GAP: i64 = 16 548const NXA_STATE: i64 = 4096 549// RING CANARY (temporary diagnostic): the single-slot canary checked only the immediately 550// previous allocation and reported ZERO overruns -- but the bisection proved the write is 551// DELAYED, landing after later allocations have been served. Track the last NXA_RING 552// allocations and re-verify every one of them on each call. Lives at i64 slot NXA_RBASE in 553// the state page; the reporter borrows bytes 64/128, so 512 is clear of it. 554const NXA_RING: i64 = 128 555const NXA_RBASE: i64 = 64 556// ---- ARENA MARK/RESET (2026-08-12, additive; the durable fix for bump-without-reset). The arena 557// abandons a full chunk on rollover, so a long-running accept loop accumulates chunks into one giant 558// coalesced VMA (hub_gw MEASURED 3.4GB over 64k requests). A daemon marks the arena AFTER startup and 559// resets at its accept-loop's quiescent point; reset munmaps every chunk allocated since the mark and 560// zeroes the marked chunk's reclaimed tail, so per-request small allocations reuse a bounded slab. 561// State slots (state page is 512 i64): [3]=chunk_count [4]=mark_valid [5]=mark_bump [6]=mark_chunk_end 562// [7]=mark_chunk_count; the chunk-base list lives at slots NXA_CHUNKBASE..+NXA_CHUNKMAX (clear of the 563// ring at 64..320 and the reporter scratch below 64). CONTRACT: the caller guarantees NO arena 564// allocation made after the mark is still referenced at reset (the accept-loop top, where the previous 565// request's frames have all returned -- the same quiescent point ss_cache_reap already uses). LARGE 566// (>NXA_SMALL_MAX) allocations take their own VMA and are NOT tracked here; a per-request large mmap 567// still needs its own munmap. Untracked-overflow (>NXA_CHUNKMAX chunks between resets) degrades to the 568// old leak for the excess, never corrupts. 569// ---- ARENA MUTUAL EXCLUSION (2026-08-25) ------------------------------------------------------- 570// THE DEFECT: the bump-pointer advance below was a plain read-modify-write -- 571// let p: i64 = nxa_st[0] 572// nxa_st[0] = p + need 573// -- so two threads that read nxa_st[0] before either wrote it BOTH RECEIVE THE SAME POINTER and 574// then write over each other. The chunk refill, the ring-canary scan and the nxa_st[2] counter have 575// the same shape. MEASURED while shipping structured concurrency: eight pool workers calling a 576// helper that allocates a 16-byte timespec raced this cursor and produced ARENA-OVERRUN 577// prev_alloc_size=16 followed by SIGSEGV. It generalises to EVERY small allocation from more than 578// one thread, which is why the scoped-spawn child body was written to allocate nothing at all. 579// 580// WHY A LOCK AND NOT A LOCK-FREE BUMP. A fetch-and-add on the cursor fixes only the fast path; two 581// threads can still both observe the chunk exhausted and both refill, and the canary ring and the 582// counter would still race. One lock over the whole mutable region is correct by inspection, which 583// on the allocator that every organ in the estate calls is worth more than a clever fast path. 584// THE COST IS NOT THE DOMINANT COST HERE: this function ALREADY walks all NXA_RING canary slots on 585// every allocation, so one uncontended `lock cmpxchgq` is far below the noise of work already done. 586// 587// SLOT 4 IS FREE BY THE LAYOUT ABOVE: [0] cursor, [1] limit, [2] ring counter, [3] chunk count, and 588// the ring starts at NXA_RBASE=64. It is also clear of the byte-64 and byte-128 scratch that 589// nxa_report_overrun formats digits into (slots 8 and 16), which slot 4 (bytes 32-39) does not touch. 590const NXA_LOCK: i64 = 4 591// A BOUND ON AN UNKNOWABLE WAIT, DERIVED RATHER THAN PICKED, AND ITS EXHAUSTION ANNOUNCES. The 592// longest thing the critical section can do is the NXA_RING canary scan plus one mmap, so a spin far 593// beyond that is not contention -- it is a holder that is never coming back. Eight times the ring 594// gives an order of magnitude of headroom over the longest legitimate hold; on reaching it the 595// allocator SAYS SO on stderr once and keeps waiting, because hanging visibly is recoverable and 596// corrupting silently is not, and dying inside the allocator would take down a process that may be 597// merely slow. 598const NXA_LOCK_WARN: i64 = NXA_RING * 8 599// Slot 5: "the contention hint has already been printed by this process". Also free by the layout 600// above and clear of every scratch region. It is a FLAG, not a counter, and it is set through a CAS 601// so the once-ness is itself race-free rather than depending on the lock it reports about. 602const NXA_LOCK_WARNED: i64 = 5 603 604const NXA_CHUNKBASE: i64 = 320 605const NXA_CHUNKMAX: i64 = 192 606 607// [0] = next free byte, [1] = one past the end of the current chunk. A static POINTER to a real 608// mmapped page rather than scalar statics, matching the idiom the corpus already proves; the state 609// page is taken through __syscall directly so this can never recurse into itself. 610static nxa_st: *i64 611 612// munmap -- free a region from sys_mmap. x86_64 munmap = 11; 11 is NOT an rv64 number in the compiler's 613// swap table, so the literal passes through untranslated = real munmap (unlike chdir, where rv64 80=fstat 614// intercepted it). CRITICAL for long-running loops: the supervisor's per-poll proc_* scans mmap 64KB+ each; 615// unfreed, the leak hits DSM's RLIMIT_AS -> mmap returns -12 -> the code writes through it -> SEGFAULT 616// (dmesg-proven: nx_hostctl segfault at 0xfffffffffffffff4). Free scan buffers to keep the supervisor alive. 617// 618// A small len means the region came from the bump arena above, because sys_mmap routes by the SAME 619// threshold. Unmapping an interior pointer would tear a hole in a chunk still holding other callers' 620// live allocations, so it is a no-op here. Balanced small callers therefore no longer return memory -- 621// but they now cost ~48 bytes instead of 4096, so the arena wins by two orders of magnitude even 622// against code that was already correct. 623// Matching release for sys_mmap_try and other whole kernel mappings. 624// Never pass an arena allocation from sys_mmap: its small pointers may be interior. 625// Preserve the requested mapping length; the kernel applies its page rounding. 626const NXA_MAP_INVALID:i64=0-22 // Linux EINVAL, a protocol value rather than a resource budget. 627func sys_munmap_direct(addr:*u8,len:i64)->i64{ 628 if (addr as i64)<=0||len<=0{return NXA_MAP_INVALID} 629 return __syscall(11,addr as i64,len,0,0,0,0) 630} 631 632func sys_munmap(addr: *u8, len: i64) -> i64 { 633 if len <= NXA_SMALL_MAX { return 0 } 634 return __syscall(11, addr as i64, len, 0, 0, 0, 0) 635} 636 637// Seek within a file. whence: 0=SEEK_SET, 1=SEEK_CUR, 2=SEEK_END. 638// Returns new file offset on success, -errno on failure. 639func sys_lseek(fd: i64, offset: i64, whence: i64) -> i64 { 640 return __syscall(SYS_LSEEK, fd, offset, whence, 0, 0, 0) 641} 642 643// ---- FILESYSTEM SPACE: THE AXIS THE ESTATE DID NOT HAVE (2026-08-28) ----------------------------- 644// WHY THIS IS HERE AND NOT LEFT WHERE IT WAS. On 2026-08-28 a 100%-FULL DISK truncated a sibling seat's 645// MEMORY.md to 0 bytes -- open(path,"w") truncates before it writes, so a full volume does not refuse a 646// write, it DESTROYS the file. Nothing in the estate saw it coming: nx_resmon is "the resource axis 647// nx_health lacks" for MEMORY and SWAP, and a search for the disk primitive returned matches=0 for BOTH 648// sys_statfs and statvfs with corpus_complete=1. nx_res_census records the same absence in its own header. 649// The capability was not missing, it was DARK: nx_system_triage.tr_free_gb has read filesystem space since 650// 2026-06-10, in an _hdl_build organ that is NOT REGISTERED (nx_job_run refuses it as "not an unpinned 651// GREEN tool"), so the one instrument that could have warned was unreachable by any caller. 652// A CAPABILITY THAT EXISTS IN ONE UNREACHABLE ORGAN IS INDISTINGUISHABLE FROM ONE NOBODY BUILT. 653// 654// WHY THE RAW 137 AND NOT A SYS_ CONST. This file's dual-arch blocks are gated on TARGET_X86_64, which is 655// HARD-PINNED UNDEFINED, so the RV64 branch is what compiles and the x86 backend translates each number at 656// emit through x86ctx_rv64_to_x86_64_syscall -- whose default is `return num`. There is NO row for RV64 43 657// (statfs), so a SYS_STATFS=43 const would pass through unmapped to x86_64 43 = ACCEPT: a different 658// syscall, silently, on a path pointer. That is not a hypothesis -- nx_system_triage PROBE-PROVED it on 659// 2026-06-10: "rv64 43 returns -9 through the translation table; 137 raw matches df exactly." So 137 is 660// the MEASURED-CORRECT number for the target we actually emit, and it is named here ONCE instead of 661// sitting as a bare literal at each call site. 662// ⚠NAMED FOLLOW-UP, conflict-checked and deliberately NOT taken here: adding `if num == 43 { return 137 }` 663// to x86ctx_rv64_to_x86_64_syscall would make the arch-correct const work too. Nothing passes 43 as an x86 664// number (43 appears only as a translation TARGET, from RV64 202 accept), so the row is safe -- but it is a 665// COMPILER change that activates only on the next nx_cc self-host rebuild, and the working path needs none. 666// 667// struct statfs (x86_64) as i64 slots: 0 f_type, 1 f_bsize, 2 f_blocks, 3 f_bfree, 4 f_bavail, 5 f_files. 668// f_bavail (not f_bfree) is the honest number for "will my write succeed": it excludes the root reserve, so 669// it reports FULLER than root would see. Wrong in the safe direction, and said out loud rather than implied. 670// ⚠THE IMPRECISION, MEASURED AND NAMED SO NOBODY LATER "FIXES" IT INTO AGREEING WITH df: this permil is 671// NOT df's Use%. df computes Used/(Used+Available), which EXCLUDES the root-reserved blocks from its 672// denominator; this computes (blocks-bavail)/blocks, which counts the reserve as used. VERIFIED against df 673// on 2026-08-28: avail_bytes came back 958449582080, which is EXACTLY df's Available of 935985920 KiB, while 674// the same volume read 113 permil here and 7% there -- both correct, measuring different things. Both reach 675// their maximum at the SAME event (bavail = 0), so a threshold calibrated against THIS metric alarms at the 676// same moment a writer actually hits the wall; it simply sits higher below that. Calibrate thresholds to 677// this definition, and do not import a df-derived number as if it were the same quantity. 678const SYS_STATFS_X86_MEASURED: i64 = 137 679const STATFS_BUF_BYTES: i64 = 144 680const STATFS_I_BSIZE: i64 = 1 681const STATFS_I_BLOCKS: i64 = 2 682const STATFS_I_BAVAIL: i64 = 4 683const STATFS_PERMIL: i64 = 1000 684const STATFS_ERR: i64 = 0 - 1 685 686// raw statfs into a caller-supplied 144-byte buffer. 0 = ok, non-zero = the kernel's negative errno. 687func sys_statfs(path: *u8, buf: *i64) -> i64 { 688 return __syscall(SYS_STATFS_X86_MEASURED, path, buf, 0, 0, 0, 0) 689} 690 691// bytes available to a non-root writer on the filesystem holding `path`; STATFS_ERR if statfs failed. 692func sys_fs_avail_bytes(path: *u8) -> i64 { 693 let buf: *i64 = sys_mmap(STATFS_BUF_BYTES) as *i64 694 if sys_statfs(path, buf) != 0 { return STATFS_ERR } 695 return buf[STATFS_I_BSIZE] * buf[STATFS_I_BAVAIL] 696} 697 698// USED per-mille of the filesystem holding `path`, counted against what a non-root writer can reach: 699// (blocks - bavail) * 1000 / blocks. STATFS_ERR if statfs failed or the volume reports zero blocks -- 700// an UNMEASURABLE volume must never read as 0 permil used, which is the most flattering possible lie. 701func sys_fs_used_permil(path: *u8) -> i64 { 702 let buf: *i64 = sys_mmap(STATFS_BUF_BYTES) as *i64 703 if sys_statfs(path, buf) != 0 { return STATFS_ERR } 704 let blocks: i64 = buf[STATFS_I_BLOCKS] 705 if blocks <= 0 { return STATFS_ERR } 706 let avail: i64 = buf[STATFS_I_BAVAIL] 707 return ((blocks - avail) * STATFS_PERMIL) / blocks 708} 709 710func sys_exit(code: i64) -> i64 { 711 return __syscall(SYS_EXIT, code, 0, 0, 0, 0, 0) 712} 713 714// mmap anonymous R/W memory; returns raw bytes. Fixed flags: 715// PROT_READ|PROT_WRITE = 3, MAP_PRIVATE|MAP_ANONYMOUS = 0x22, fd=-1. 716// FAIL-CLOSED ON A REFUSED MAPPING (2026-08-07). MEASURED: the corpus has 90,817 sys_mmap call sites 717// and SIX of them check the result -- all six in test probes whose response is sys_exit anyway. So 718// 90,811 sites take whatever this returns and write through it. When the kernel refuses, that value is 719// -errno, and the write lands at 0xfffffffffffffff4 (-12, ENOMEM). That is not a hypothetical: dmesg 720// on this host recorded it hourly in nx_web_shard_compact, and 18 times in nx_web_crawl_step. 721// Returning a poisoned pointer to 90,811 unguarded callers is the defect. Dying here is strictly safer 722// than dying there: the process ends either way, but this way there is no memory corruption first and 723// the failure is NAMED instead of arriving as a bare segfault address an operator has to decode. 724// This is the never-brick shape -- fail-safe BY CONSTRUCTION, not by every caller remembering. 725// KNOWN COST, stated: nx_mmap_probe / test_munmap deliberately provoke a refusal to observe it. They 726// now exit here with code 12 rather than printing their own verdict. Six probes lose a diagnostic; 727// 90,811 sites stop corrupting memory. 728// ===== TEMPORARY DIAGNOSTIC -- ARENA OVERRUN CANARY (2026-08-07) ===================================== 729// ⛔DO NOT BLESS A COMPILER BUILT WITH THIS. The canary writes 0xC7 into the NXA_GAP slack that a 730// caller could otherwise legitimately read as zeros, so it changes observable behaviour for any code 731// that reads past its declared size -- which is precisely the code being hunted. 732// PURPOSE: at NXA_SMALL_MAX=256 the compiler produces 14 SPURIOUS type diagnostics (it reports 733// `arg 2 is an INTEGER but the parameter is a POINTER` against a parameter DECLARED `j: *u8`), i.e. 734// something writes past its allocation and corrupts the parser's type table. At threshold 64 the same 735// requests each get a 4096-byte page whose slack absorbs it. Reading the source found nothing: the 736// two obvious suspects (nx_ir.nx:70 sys_mmap(104), nx_parse.nx:868 sys_mmap(256)) are both correctly 737// sized and bounded. So stop reading and MEASURE: stamp each small allocation's gap, verify the 738// PREVIOUS one on the next call, and print the size of whichever allocation was overrun. 739// Writes to fd 2 without allocating -- it borrows scratch inside the arena state page, because a 740// reporter that called sys_mmap would recurse into the thing it is instrumenting. 741// Dump n bytes at src to fd 2, unprintables as '.', using scratch at state+256 (the ring starts at 742// state+512 and the decimal scratch sits at +64/+128, so this cannot collide with either). n is 743// capped by callers at 48 so the buffer stays clear of the ring. 744func nxa_dump_printable(src: i64, n: i64) -> i64 { 745 let o: *u8 = ((nxa_st as i64) + 256) as *u8 746 var i: i64 = 0 747 while i < n { 748 let sp: *u8 = (src + i) as *u8 749 var c: i64 = sp[0] as i64 750 if c < 32 { c = 46 } 751 if c > 126 { c = 46 } 752 o[i] = c as u8 753 i = i + 1 754 } 755 o[n] = 10 as u8 756 sys_write(2, o, n + 1) 757 return 0 758} 759 760// FINGERPRINT (2026-08-12): the size alone + all-zeros byte dump never named the site. The ring already 761// records each allocation's REQUESTED size in counter order, so the recent size SEQUENCE fingerprints the 762// code path that was running when the overrun landed (a distinctive run of sizes is near-unique to a 763// function). Writes to fd 2 borrowing state-page scratch at bytes 320/340 (clear of the ring at byte 512, 764// the reporter decimals at 64/128, and the byte-dump at 256). No allocation -- must not recurse into sys_mmap. 765func nxa_dump_sizes() -> i64 { 766 sys_write(2, " ring_sizes(old->recent): " as *u8, 27) 767 let scr: *u8 = ((nxa_st as i64) + 320) as *u8 768 let out2: *u8 = ((nxa_st as i64) + 340) as *u8 769 let cnt: i64 = nxa_st[2] 770 var start: i64 = cnt - 32 771 if start < 0 { start = 0 } 772 var idx: i64 = start 773 while idx < cnt { 774 let slot: i64 = idx % NXA_RING 775 let szv: i64 = nxa_st[NXA_RBASE + slot * 2 + 1] 776 var m: i64 = szv 777 var k: i64 = 0 778 if m == 0 { scr[0] = 48 as u8; k = 1 } 779 while m > 0 { scr[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 780 var j: i64 = 0 781 while j < k { out2[j] = scr[k - 1 - j]; j = j + 1 } 782 out2[k] = 44 as u8 783 sys_write(2, out2, k + 1) 784 idx = idx + 1 785 } 786 sys_write(2, "\n" as *u8, 1) 787 return 0 788} 789 790func nxa_report_overrun(sz: i64, gs: i64) -> i64 { 791 let msg: *u8 = "ARENA-OVERRUN prev_alloc_size=" as *u8 792 var n: i64 = 0 793 while msg[n] != (0 as u8) { n = n + 1 } 794 sys_write(2, msg, n) 795 let b: *u8 = ((nxa_st as i64) + 64) as *u8 796 let o: *u8 = ((nxa_st as i64) + 128) as *u8 797 var m: i64 = sz 798 var k: i64 = 0 799 if m == 0 { b[0] = 48 as u8; k = 1 } 800 while m > 0 { b[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 801 var i: i64 = 0 802 while i < k { o[i] = b[k - 1 - i]; i = i + 1 } 803 o[k] = 10 as u8 804 sys_write(2, o, k + 1) 805 // The SIZE alone did not name the site (four 80-byte victims, and the two unbounded 80-byte 806 // buffers in nx_parse.nx were sized from their inputs with no effect). So show the DATA: the 807 // victim's own bytes identify the buffer, and the bytes written past its end identify the WRITER. 808 let algn: i64 = (sz + NXA_ALIGN - 1) / NXA_ALIGN * NXA_ALIGN 809 let base: i64 = gs - algn 810 var dn: i64 = sz 811 if dn > 48 { dn = 48 } 812 sys_write(2, " own : " as *u8, 8) 813 nxa_dump_printable(base, dn) 814 sys_write(2, " over: " as *u8, 8) 815 nxa_dump_printable(gs, 16) 816 nxa_dump_sizes() 817 return 0 818} 819 820func nxa_die(msg: *u8) -> i64 { 821 var n: i64 = 0 822 while msg[n] != (0 as u8) { n = n + 1 } 823 sys_write(2, msg, n) 824 sys_exit(12) 825 return 0 826} 827 828// Address of the arena lock word. Valid only once nxa_st exists; every caller below has already 829// ensured that, and the state-page creation itself is discussed at the take site. 830func nxa_lock_addr() -> *i64 { 831 return ((nxa_st as i64) + NXA_LOCK * 8) as *i64 832} 833 834// __atomic_cas_i64 returns 1 when it wrote and 0 when it did not, so the spin condition is == 0. 835// It is a COMPILER INTRINSIC, not a call into nx_atom -- that module imports THIS file, so importing 836// it back would be a cycle. Verified in nx_x86_64_ctx rather than assumed: it lowers to a genuine 837// `lock cmpxchgq` followed by sete, which is a full barrier on x86-64 whatever ordering is passed. 838func nxa_lock_take() -> i64 { 839 var spins: i64 = 0 840 while __atomic_cas_i64(nxa_lock_addr(), 0, 1, NX_MO_ACQUIRE) == 0 { 841 spins = spins + 1 842 // Fires EXACTLY ONCE, on equality rather than on exceeding, so a genuinely long wait reports 843 // itself without turning the allocator into a log generator. 844 if spins == NXA_LOCK_WARN { 845 // ONCE PER PROCESS, not once per acquisition. MEASURED 2026-08-25 and this is a 846 // correction to the first cut of this very function: it fired on equality per CALL, and 847 // eight workers contending LEGITIMATELY produced hundreds of identical lines in a single 848 // gate run. A DIAGNOSTIC THAT FIRES CONSTANTLY IS ONE EVERY READER LEARNS TO IGNORE, and 849 // this one writes to the stderr of every organ in the estate. 850 // The threshold was derived from the longest the critical section can run, which bounds 851 // ONE hold and says nothing about QUEUE DEPTH: with N threads waiting, a legitimate wait 852 // is N holds and can exceed any per-section derivation. So this is a NOISE FLOOR for a 853 // hint, never a correctness bound -- it never fails, never delays, and never repeats. 854 // The flag is set through a CAS so the once-ness cannot itself race. 855 let wflag: *i64 = ((nxa_st as i64) + NXA_LOCK_WARNED * 8) as *i64 856 if __atomic_cas_i64(wflag, 0, 1, NX_MO_ACQ_REL) == 1 { 857 let m: *u8 = "ARENA-LOCK: sustained allocator contention seen (reported once per process; a hint, not an error -- allocation proceeds normally).\n" as *u8 858 var mn: i64 = 0 859 while m[mn] != (0 as u8) { mn = mn + 1 } 860 sys_write(2, m, mn) 861 } 862 } 863 } 864 return 0 865} 866 867func nxa_lock_give() -> i64 { 868 // nx_cc refuses a bare intrinsic statement ("computes a value and never uses it") and an atomic 869 // store has no result worth using, so it is bound and discarded -- the same shape nx_atom uses 870 // for exactly this reason. The contract is unchanged: this returns 0 either way. 871 let discarded: i64 = __atomic_store_i64(nxa_lock_addr(), 0, NX_MO_RELEASE) 872 if discarded != 0 { return 0 } 873 return 0 874} 875 876// Optional mapping for request boundaries that must report allocation refusal. 877// Unlike sys_mmap, this never aborts the process and never consumes arena storage. 878// Release successful mappings with sys_munmap_direct, not the arena-aware sys_munmap. 879// A successful reservation can still fail on later physical-memory pressure; callers 880// must not describe virtual address admission as guaranteed resident RAM. 881func sys_mmap_try(size:i64)->*u8 { 882 if size<=0 { return 0 as *u8 } 883 let mapped:i64=__syscall(SYS_MMAP,0,size,3,0x22,-1,0) 884 if mapped<=0 { return 0 as *u8 } 885 return mapped as *u8 886} 887 888func sys_mmap(size: i64) -> *u8 { 889 // Large requests keep the EXACT original behaviour, byte for byte: page-aligned, own VMA. Any 890 // caller that depends on page alignment is allocating at least a page, so the arena cannot reach 891 // it. Every failure path below also falls back to this same call, so an exhausted arena degrades 892 // to the old allocator rather than returning a bad pointer. 893 if size > NXA_SMALL_MAX { 894 let big: i64 = __syscall(SYS_MMAP, 0, size, 3, 0x22, -1, 0) 895 if big <= 0 { nxa_die("FATAL sys_mmap: kernel refused a large mapping (ENOMEM). Refusing to return a poisoned pointer -- a write through it would corrupt memory.\n" as *u8) } 896 return big as *u8 897 } 898 if (nxa_st as i64) == 0 { 899 let s: i64 = __syscall(SYS_MMAP, 0, NXA_STATE, 3, 0x22, -1, 0) 900 if s <= 0 { 901 // arena state page refused -- degrade to the plain allocator, and only die if THAT fails too 902 let f1: i64 = __syscall(SYS_MMAP, 0, size, 3, 0x22, -1, 0) 903 if f1 <= 0 { nxa_die("FATAL sys_mmap: kernel refused the arena state page AND the fallback mapping (ENOMEM).\n" as *u8) } 904 return f1 as *u8 905 } 906 nxa_st = s as *i64 907 } 908 // EVERYTHING FROM HERE TO THE RETURN TOUCHES SHARED STATE: the cursor, the limit, the chunk 909 // table, the canary ring and the ring counter. It is ONE critical section because the refill 910 // decision and the bump that depends on it cannot be separated without reintroducing the race. 911 // The state page itself is created ABOVE this point, unlocked: two threads arriving there 912 // together would each map a page and one would win the static, leaking the other's 4 KiB but 913 // corrupting nothing, and in practice the arena is warm long before any thread is spawned 914 // because spawning one allocates. That residual is NAMED here rather than papered over. 915 nxa_lock_take() 916 var need: i64 = size 917 if need <= 0 { need = 1 } 918 need = (need + NXA_ALIGN - 1) / NXA_ALIGN * NXA_ALIGN + NXA_GAP 919 if nxa_st[0] + need > nxa_st[1] { 920 let c: i64 = __syscall(SYS_MMAP, 0, NXA_CHUNK, 3, 0x22, -1, 0) 921 if c <= 0 { 922 // chunk refused -- degrade to the plain allocator, and only die if THAT fails too. 923 // RELEASE FIRST: this is the one path that leaves the critical section early, and a lock 924 // held across a degraded return would wedge every other allocator in the process. 925 nxa_lock_give() 926 let f2: i64 = __syscall(SYS_MMAP, 0, size, 3, 0x22, -1, 0) 927 if f2 <= 0 { nxa_die("FATAL sys_mmap: kernel refused an arena chunk AND the fallback mapping (ENOMEM).\n" as *u8) } 928 return f2 as *u8 929 } 930 nxa_st[0] = c 931 nxa_st[1] = c + NXA_CHUNK 932 // track the chunk base so arena_reset can munmap post-mark chunks (additive; guarded at cap). 933 if nxa_st[3] < NXA_CHUNKMAX { nxa_st[NXA_CHUNKBASE + nxa_st[3]] = c; nxa_st[3] = nxa_st[3] + 1 } 934 } 935 // ---- RING CANARY (temporary diagnostic) ---- 936 var rk: i64 = 0 937 while rk < NXA_RING { 938 let gs0: i64 = nxa_st[NXA_RBASE + rk * 2] 939 if gs0 != 0 { 940 var bi: i64 = 0 941 var bad: i64 = 0 942 while bi < 8 { 943 let bp: *u8 = (gs0 + bi) as *u8 944 if bp[0] != (199 as u8) { bad = 1; bi = 8 } else { bi = bi + 1 } 945 } 946 if bad == 1 { 947 nxa_report_overrun(nxa_st[NXA_RBASE + rk * 2 + 1], gs0) 948 nxa_st[NXA_RBASE + rk * 2] = 0 949 } 950 } 951 rk = rk + 1 952 } 953 let p: i64 = nxa_st[0] 954 nxa_st[0] = p + need 955 let gs: i64 = p + need - NXA_GAP 956 var gj: i64 = 0 957 while gj < NXA_GAP { let q: *u8 = (gs + gj) as *u8; q[0] = 199 as u8; gj = gj + 1 } 958 let slot: i64 = nxa_st[2] % NXA_RING 959 nxa_st[NXA_RBASE + slot * 2] = gs 960 nxa_st[NXA_RBASE + slot * 2 + 1] = size 961 nxa_st[2] = nxa_st[2] + 1 962 // The ONLY other exit from the critical section is the degraded chunk-refill path above, which 963 // releases before it returns. Every shared write is now behind this pair. 964 nxa_lock_give() 965 return p as *u8 966} 967 968// arena_mark: force the arena warm (so a first chunk + state page exist), then record the current 969// position as the reset barrier. Returns 1. A daemon calls this ONCE after startup, before its loop. 970func sys_arena_mark() -> i64 { 971 let warm: *u8 = sys_mmap(1) // ensures nxa_st + chunk[0] exist; the 1 byte is itself arena scratch 972 if (warm as i64) == 0 { return 0 } 973 nxa_st[4] = 1 974 nxa_st[5] = nxa_st[0] 975 nxa_st[6] = nxa_st[1] 976 nxa_st[7] = nxa_st[3] 977 return 1 978} 979 980// arena_reset: reclaim everything allocated since the mark. munmap post-mark chunks, restore the bump 981// to the mark, ZERO the marked chunk's reclaimed tail (preserves the mmap-returns-zeroed contract for 982// recycled bytes), and CLEAR the ring canary (its stamps may point into a just-munmap'd chunk, and a 983// stale deref on the next alloc would SEGV). Returns 1 on reset, 0 if no mark was set. 984func sys_arena_reset() -> i64 { 985 if (nxa_st as i64) == 0 { return 0 } 986 if nxa_st[4] != 1 { return 0 } 987 var i: i64 = nxa_st[7] 988 while i < nxa_st[3] { 989 let cb: i64 = nxa_st[NXA_CHUNKBASE + i] 990 if cb != 0 { __syscall(11, cb, NXA_CHUNK, 0, 0, 0, 0); nxa_st[NXA_CHUNKBASE + i] = 0 } 991 i = i + 1 992 } 993 nxa_st[3] = nxa_st[7] 994 nxa_st[0] = nxa_st[5] 995 nxa_st[1] = nxa_st[6] 996 var z: i64 = nxa_st[0] 997 while z < nxa_st[1] { let q: *u8 = z as *u8; q[0] = 0 as u8; z = z + 1 } 998 var r: i64 = 0 999 while r < NXA_RING * 2 { nxa_st[NXA_RBASE + r] = 0; r = r + 1 } 1000 nxa_st[2] = 0 1001 return 1 1002} 1003 1004// mmap anonymous SHARED R/W memory -- ONE region that survives fork() so all 1005// children see each other's writes (MAP_SHARED|MAP_ANONYMOUS = 0x21). Allocate 1006// in the PARENT before fork. Foundation for the fork-per-connection video relay 1007// (peers in separate children share the per-room frame table). 1008func sys_mmap_shared(size: i64) -> *u8 { 1009 let r: i64 = __syscall(SYS_MMAP, 0, size, 3, 0x21, -1, 0) 1010 return r as *u8 1011} 1012 1013// madvise(2) -- prefetch/advice hints for mapped ranges. MADV_WILLNEED=3 batches page-ins so a 1014// serial fault loop over a cold file-backed mmap becomes parallel disk readahead (the dp-web-pub 1015// stage-2 p95 fix, 2026-08-12). RAW x86_64 NUMBER 28 ON PURPOSE (sys_exit_group's raw-231 pattern): 1016// the portable rv64/asm-generic number is 233 and x86ctx_rv64_to_x86_64_syscall has no 233 row in 1017// the DEPLOYED compiler, so a portable const would emit x86_64 233 = epoll_ctl (the wrong-syscall- 1018// not-an-error class; see the setpgid/flock rows). The 233->28 row is staged in nx_x86_64_ctx.nx and 1019// activates on the next nx_cc self-host rebuild; flip this to the portable const AFTER that lands. 1020// Signature bite-proven by nx_madvise_probe (0 / -12 ENOMEM / -22 EINVAL). Advisory contract: callers 1021// may ignore the return value -- a failed hint costs nothing but the cold-read behaviour it hints away. 1022func sys_madvise(addr: *u8, len: i64, advice: i64) -> i64 { 1023 return __syscall(28, addr, len, advice, 0, 0, 0) 1024} 1025 1026// openat flavors used by the compiler driver. AT_FDCWD = -100 (declared ABOVE, next to its first 1027// reader -- see the miscompile note there; do NOT move it back down). 1028// O_RDONLY = 0; O_CREAT|O_WRONLY|O_TRUNC = 0x241 on Linux RV64. 1029const O_RDONLY: i64 = 0 1030const O_WRONLY_CT: i64 = 0x241 // O_CREAT | O_WRONLY | O_TRUNC 1031const O_WRONLY_CA: i64 = 0x441 // O_CREAT | O_WRONLY | O_APPEND 1032 1033func sys_openat_rd(path: *u8) -> i64 { 1034 return __syscall(SYS_OPENAT, AT_FDCWD, path, O_RDONLY, 0, 0, 0) 1035} 1036 1037// O_RDWR|O_CREAT (NO truncate) -- for offset-addressed persistent files like the metrics ring TSDB 1038// (create if missing, then lseek+read/write records in place, never truncating existing history). 1039const O_RDWR_CREATE: i64 = 0x42 1040func sys_openat_rdwr(path: *u8, mode: i64) -> i64 { 1041 return __syscall(SYS_OPENAT, AT_FDCWD, path, O_RDWR_CREATE, mode, 0, 0) 1042} 1043 1044// ★★★THE FILE MODE IS THE HALF OF THIS INTERFACE THAT WAS NEVER NAMED. The O_ flags above are named 1045// consts in hex WITH a decoding comment; the mode passed beside them is a bare literal at every call 1046// site. MEASURED 2026-08-14 (coverage_complete=1 corpus_complete=1 over 23,053 files): 1047// - 29 organs passed the mode as a bare DECIMAL literal, which no reader decodes as rw-r--r-- 1048// without stopping to convert it. ⚠THE FIRST COUNT PUBLISHED HERE WAS 26: the scan was scoped to 1049// runtime/_hdl_build/ and the SUBDIRECTORY's count was published as the estate figure -- three 1050// more (nx_forge_rag, nx_gpu_export, nx_bvhfk) sat one level up in runtime/. 1051// ★A COUNT INHERITS THE SCOPE OF ITS SCAN, AND THE SCOPE IS THE PART NOBODY PRINTS BESIDE IT. 1052// ⚠The offending call is deliberately NOT spelled out literally in this comment: prose is source 1053// bytes, so writing the pattern here would make every future grep for it match this note; 1054// - 10 MORE each define their OWN private 0644 const (IP_ VR_ VP_ LIVE_ FD_ FP_ WL_ PUB_ REG_ HFF_), 1055// nine written 0x1a4 and one written 420 -- THE SAME CONSTANT IN TWO DIFFERENT BASES. 1056// Ten seats each solved this privately and none put the answer where the next one would look. That is 1057// the duplicate-ruler defect precisely: changing the estate's default artifact mode today means finding 1058// 39 sites in two notations and hoping none was missed. One name, in the shim every organ already 1059// imports, is the entire fix -- and it belongs HERE, beside the flags, not in a 40th private copy. 1060const MODE_0644: i64 = 0x1a4 // rw-r--r-- : default mode for a generated artifact 1061// rwxr-xr-x : default mode for a created DIRECTORY. A directory without the execute bit cannot be 1062// traversed, so MODE_0644 is not merely stricter here -- it is wrong, and the failure surfaces later 1063// as an unopenable path rather than as a refused mkdir. Named beside its sibling so the choice is a 1064// lookup rather than a recollection; the estate otherwise spells this as a raw 0x1ed at every site. 1065const MODE_0755: i64 = 0x1ed 1066// Seconds of ZERO PROGRESS on one socket operation before an accepted connection is abandoned. 1067// A single-threaded accept-loop daemon that loop-reads to Content-Length can be starved FOREVER by one 1068// peer that declares a body it never finishes sending -- a one-request DoS, hostile OR merely buggy. 1069// nx_dos_timeout_scan supervises the class and named 16 daemons carrying no timeout at all; the cure is 1070// sys_set_socket_timeout(cfd, ACCEPT_TMO_S) folded in right after accept. 1071// WHY 30 AND NOT THE 5 THE LOGIN DAEMONS USE: this bound must be wrong in the direction of SERVING, not 1072// of dropping. The attack is an UNBOUNDED wait, so ANY finite bound closes it; a short one additionally 1073// risks aborting a legitimate slow client. 30s of zero progress on a single recv/send is pathological 1074// for every daemon in the class -- including the streaming ones, where data is flowing and the timer 1075// never approaches its bound -- while still converting an infinite starvation into a bounded one. 1076// It is the calibration nx_galx_bridge already uses for an accepted cfd; named here rather than copied 1077// into a 16th private literal, exactly as MODE_0644 above. 1078const ACCEPT_TMO_S: i64 = 30 1079func sys_openat_wr(path: *u8, mode: i64) -> i64 { 1080 return __syscall(SYS_OPENAT, AT_FDCWD, path, O_WRONLY_CT, mode, 0, 0) 1081} 1082 1083// Linux O_WRONLY | O_CREAT | O_EXCL. An existing final component, including 1084// a symlink, is a conflict; callers acquire ownership only on success. 1085const O_WRONLY_CREATE_EXCLUSIVE: i64 = 0x1 | 0x40 | 0x80 1086func sys_openat_exclusive(path: *u8, mode: i64) -> i64 { 1087 return __syscall(SYS_OPENAT, AT_FDCWD, path, O_WRONLY_CREATE_EXCLUSIVE, mode, 0, 0) 1088} 1089 1090// Linux O_DIRECTORY: require a directory, rather than merely an openable node. 1091const O_DIRECTORY: i64 = 0x10000 1092func sys_openat_directory(path: *u8) -> i64 { 1093 return __syscall(SYS_OPENAT, AT_FDCWD, path, O_RDONLY | O_DIRECTORY, 0, 0, 0) 1094} 1095 1096// Open path for append (create if missing). Used by append-only 1097// journals such as .race_telemetry.tsv. RV64 syscall numbers; the 1098// x86_64 mirror lives in nx_syscalls_x86_64.nx. 1099func sys_openat_append(path: *u8, mode: i64) -> i64 { 1100 return __syscall(SYS_OPENAT, AT_FDCWD, path, O_WRONLY_CA, mode, 0, 0) 1101} 1102 1103// Linux open ABI flags: acquire close-on-exec atomically and refuse a final 1104// symlink. Nonblocking also prevents an unexpected FIFO from stalling admission. 1105const O_CLOEXEC: i64 = 0x80000 1106const O_NOFOLLOW: i64 = 0x20000 1107const O_NONBLOCK: i64 = 0x800 1108const MODE_0600: i64 = 0x180 1109func sys_openat_lock(path: *u8) -> i64 { 1110 return __syscall(SYS_OPENAT, AT_FDCWD, path, O_WRONLY_CA | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK, MODE_0600, 0, 0) 1111} 1112 1113// symlinkat(target, AT_FDCWD, linkpath) -- raw x86_64 266 forced RUNTIME (the chdir escape, same as 1114// readlinkat below). THE atomic-repoint primitive for release management: create releases/current.new -> 1115// sys_renameat over releases/current = an atomic symlink swap (golive/rollback are instant + crash-safe). 1116// 0 on success, -errno (notably -EEXIST=-17 if linkpath exists -- create the .new name, then rename). 1117func sys_symlinkat(target: *u8, linkpath: *u8) -> i64 { 1118 let nbox: *i64 = sys_mmap(16) as *i64 1119 nbox[0] = 266 1120 let r: i64 = __syscall(nbox[0], target as i64, AT_FDCWD, linkpath as i64, 0, 0, 0) 1121 sys_munmap(nbox as *u8, 16) 1122 return r 1123} 1124 1125// readlinkat(AT_FDCWD, path, buf, cap) -- raw x86_64 267 forced RUNTIME (the chdir escape: keep the 1126// number out of the rv64->x86 constant-translate path). Returns link length (NO NUL appended), -errno 1127// on fail. nbox is munmap'd before return: the daemon supervisor calls this hundreds of times PER CYCLE 1128// (exe-identity sweeps), and a leaked page per call is exactly the VSZ-balloon class that broke fork. 1129func sys_readlinkat(path: *u8, buf: *u8, cap: i64) -> i64 { 1130 let nbox: *i64 = sys_mmap(16) as *i64 1131 nbox[0] = 267 1132 let r: i64 = __syscall(nbox[0], AT_FDCWD, path as i64, buf as i64, cap, 0, 0) 1133 sys_munmap(nbox as *u8, 16) 1134 return r 1135} 1136 1137// Atomically replace newpath with oldpath (rename(2) on one filesystem: a concurrent reader sees the 1138// whole old file or the whole new file, never a torn read). The S-class content-publish primitive: 1139// write the new page to a temp file, then sys_renameat(tmp, live) -> hot-swap, NO rm+ln race. 1140// renameat2: rv64=276, x86_64=316, flags=0. The known-good compiler translates most rv64 syscall 1141// numbers to the x86_64 target but its table MISSES 276 -- verified 2026-06-14 via nx_rename_probe: 1142// raw 276 -> -EINVAL (lands on x86_64 `tee`), raw 316 -> renames OK. That silently broke every 1143// cst_write_atomic publish (page.html.new written, never swapped in). Try the x86_64 number first 1144// (works on every x86_64 build incl. known-good); fall back to the rv64 number for native-rv64 or 1145// translating compilers that do map it. flags=0 so renameat2 == renameat semantics. 1146func sys_renameat(oldpath: *u8, newpath: *u8) -> i64 { 1147 let r: i64 = __syscall(316, AT_FDCWD, oldpath, AT_FDCWD, newpath, 0, 0) 1148 if r == 0 { return 0 } 1149 return __syscall(276, AT_FDCWD, oldpath, AT_FDCWD, newpath, 0, 0) 1150} 1151 1152// fsync(2): flush file (or directory) data+metadata to stable storage. 1153// PROBE-PROVEN 2026-06-10 (_fsync_probe): rv64 82 is NOT in the compiler's 1154// translation table (lands on x86 rename -> -EFAULT both ways); direct 1155// x86_64 74 passes through raw (the unlinkat-263 precedent) and behaves as 1156// fsync (0 on a valid fd, -9 EBADF on a bad one). Storage commit points 1157// fsync the data files AND their directory around rename(2) so a committed 1158// segment survives power loss, not just process death. 1159func sys_fsync(fd: i64) -> i64 { 1160 return __syscall(74, fd, 0, 0, 0, 0, 0) 1161} 1162 1163// flock(2): BSD-style whole-file ADVISORY lock. rv64 32 -> x86_64 73 via the compiler's 1164// x86ctx_rv64_to_x86_64_syscall table (nx_x86_64_ctx.nx:961, PROVEN LIVE in flock_deploy.log). 1165// op: SYS_LOCK_SH=1 / SYS_LOCK_EX=2 / SYS_LOCK_NB=4 (OR) / SYS_LOCK_UN=8. Returns 0 on success, 1166// -errno on failure. Used by the framed-append durability floor to serialize the write-until- 1167// complete loop so a partial/short write under contention can NEVER misalign a concurrent appender 1168// (O_APPEND single-write atomicity is necessary but not sufficient on every fs -- the lock makes 1169// the whole framed record write atomic against other lockers). Additive: no existing caller in 1170// this file changes. NOTE: nx_flock.nx is a separate organ importing the LEGACY "syscalls.nx" 1171// name; this wrapper lives HERE so organs already on nx_syscalls.nx (e.g. nx_framed_append) get 1172// flock without a second import (double-import rc=6 trap). 1173const SYS_LOCK_SH: i64 = 1 1174const SYS_LOCK_EX: i64 = 2 1175const SYS_LOCK_NB: i64 = 4 1176const SYS_LOCK_UN: i64 = 8 1177func sys_flock(fd: i64, op: i64) -> i64 { 1178 return __syscall(32, fd, op, 0, 0, 0, 0) 1179} 1180 1181// newfstatat(2): stat `path` into a 144-byte x86-64 struct stat at `statbuf`. x86_64 nr 262 is passed 1182// DIRECTLY (the unlinkat-263 / fsync-74 precedent: stat-family rv64 numbers aren't in the compiler's 1183// translation table, so a raw x86_64 number passes through untranslated). Returns 0 on success, <0 1184// (e.g. -2 ENOENT) on error. st_mtim.tv_sec @ offset 88, st_mtim.tv_nsec @ 96 (the freshness channel). 1185func sys_fstatat(path: *u8, statbuf: *u8) -> i64 { 1186 return __syscall(262, AT_FDCWD, path, statbuf, 0, 0, 0) 1187} 1188 1189// utimensat(2): set `path` atime+mtime from `times` (a struct timespec[2] = [atime.sec,atime.nsec, 1190// mtime.sec,mtime.nsec]). x86_64 nr 280 passed DIRECTLY. A sovereign `touch`; also makes freshness 1191// tests deterministic. Returns 0 on success, <0 on error. 1192func sys_utimensat(path: *u8, times: *i64) -> i64 { 1193 return __syscall(280, AT_FDCWD, path, times as i64, 0, 0, 0) 1194} 1195 1196// ---- sovereign host control-plane syscalls (x86_64; single unconditional consts, 1197// per the known-good-compiler @ifdef finding). The Nishi supervisor uses these to 1198// manage the daemon lifecycle WITHOUT any shell (no pkill / mkdir / chmod glue). ---- 1199 1200// COMPILER NOTE: the known-good compiler BAKES whole function bodies by NAME for some syscalls 1201// (proven via emitted .s: a function literally named sys_kill emits number 8, sys_chmod emits 155 1202// -- both wrong, regardless of the const referenced). So these wrappers use NON-baked names 1203// (nx_kill / nx_chmod). sys_mkdir / sys_renameat are not baked, so those keep the sys_ name. 1204 1205// DESIGN: __syscall takes the RV64/generic number; the compiler's x86ctx_rv64_to_x86_64_syscall table 1206// (nx_x86_64_ctx.nx) translates it to the build target. So pass the RV64 number. These four were added 1207// to that sovereign table 2026-06-06 (kill 129->62, mkdirat 34->258, fchmodat 53->268, renameat2 1208// 276->316); x86 kill(62) had collided with rv64 lseek(62), x86 fchmodat(268) with rv64 pivot_root(268). 1209 1210// kill(pid, sig) -- rv64 129 -> x86_64 62. SIGTERM=15 / SIGKILL=9. Host control plane. 1211func nx_kill(pid: i64, sig: i64) -> i64 { return __syscall(129, pid, sig, 0, 0, 0, 0) } 1212 1213// setpgid(pid, pgid) -- put a process in its own PROCESS GROUP so a killer can reach its whole 1214// subtree. nx_kill(0 - pgid, sig) signals every member, not just the one process you forked. 1215// A BOUND THAT ONLY REACHES THE PROCESS YOU FORKED IS NOT A BOUND ON THE WORK IT STARTED. 1216// Per-target const, NOT a bare generic number: x86ctx_rv64_to_x86_64_syscall translates only the 1217// numbers it knows and FALLS THROUGH for the rest. MEASURED on the laptop lane 2026-08-10: a bare 1218// generic 154 reached x86_64 as 154 and returned -38 (ENOSYS), silently -- and a fix built on it 1219// reproduced the original bug exactly. Callers must treat setpgid as BEST-EFFORT. 1220@ifdef TARGET_X86_64 1221const SYS_SETPGID: i64 = 109 1222@endif 1223@ifndef TARGET_X86_64 1224const SYS_SETPGID: i64 = 154 1225@endif 1226func sys_setpgid(pid: i64, pgid: i64) -> i64 { return __syscall(SYS_SETPGID, pid, pgid, 0, 0, 0, 0) } 1227 1228// prlimit64(pid, resource, new_limit, old_limit) -- the Linux RESOURCE-LIMIT primitive = 1229// the Job-Object ActiveProcessLimit / memory-limit analog for the sovereign supervisor (M5). 1230// x86_64 prlimit64 = 302 (PASSED DIRECTLY, the unlinkat-263 / fsync-74 / fstatat-262 1231// precedent: a raw x86_64 number not in the compiler's rv64->x86 swap table passes through 1232// untranslated). NOTE: rv64 prlimit64 IS 261 but x86_64 261 = futimesat -- so the naive 1233// "261 is the same on both" is WRONG (PROBE-PROVEN: 261 returned EFAULT/EINVAL because it 1234// hit futimesat); the build target here is x86_64, so we emit 302 directly. pid=0 => the 1235// calling process (a forked child caps ITSELF before running its payload). new_limit / 1236// old_limit each point at a struct rlimit64 { rlim_cur: i64, rlim_max: i64 } (16 bytes); 1237// pass 0 for old_limit to skip read-back. Returns 0 on success, -errno (e.g. -1 EPERM if 1238// raising a hard limit unprivileged) on failure. NON-baked name (the compiler bakes some 1239// sys_* bodies by name; the nx_ prefix avoids that trap). 1240func nx_prlimit(pid: i64, resource: i64, new_limit: *u8, old_limit: *u8) -> i64 { 1241 return __syscall(302, pid, resource, new_limit as i64, old_limit as i64, 0, 0) 1242} 1243 1244// RLIMIT resource ids (Linux generic; identical rv64/x86_64). RLIMIT_AS = address-space 1245// (virtual memory) cap -- the cleanest userspace-settable "memory budget" for a supervised 1246// job. RLIMIT_CPU = CPU-seconds cap. WNOHANG=1 = wait4 non-blocking liveness poll option. 1247const RLIMIT_CPU: i64 = 0 1248const RLIMIT_AS: i64 = 9 1249const WNOHANG: i64 = 1 1250 1251// mkdirat -- rv64 34 -> x86_64 258. Create a doc-root directory. mode e.g. 0x1ed (0755). 1252func sys_mkdir(path: *u8, mode: i64) -> i64 { return __syscall(34, AT_FDCWD, path, mode, 0, 0, 0) } 1253 1254// fchmodat -- rv64 53 -> x86_64 268. +x a freshly-deployed daemon binary (mode 0x1ed). flags=0. 1255func nx_chmod(path: *u8, mode: i64) -> i64 { return __syscall(53, AT_FDCWD, path, mode, 0, 0, 0) } 1256 1257// setsid -- x86_64 = 112 (not in the rv64->x86 table, so the literal passes through). Detach a forked 1258// process into a NEW session so it survives the SSH/parent close -- sovereign daemonization (no shell setsid). 1259func nx_setsid() -> i64 { return __syscall(112, 0, 0, 0, 0, 0, 0) } 1260 1261// CLOCK_MONOTONIC = 1. ts is 16 bytes {sec: i64, nsec: i64}. 1262// Returns 0 / -errno. 1263func sys_clock_gettime_mono(ts: *i64) -> i64 { 1264 return __syscall(SYS_CLOCK_GETTIME, 1, ts, 0, 0, 0, 0) 1265} 1266 1267// CLOCK_REALTIME = 0 -- wall-clock seconds since the Unix epoch. Use 1268// this (NOT monotonic) for anything that must match calendar time: 1269// X.509 notBefore/notAfter, logs, TLS timestamps. Monotonic returns 1270// time-since-boot, which encodes as ~1970 when (mis)used as an epoch. 1271func sys_clock_gettime_real(ts: *i64) -> i64 { 1272 return __syscall(SYS_CLOCK_GETTIME, 0, ts, 0, 0, 0, 0) 1273} 1274 1275// Wall-clock seconds since the Unix epoch. 1276func sys_now_realtime_sec() -> i64 { 1277 let ts: *i64 = sys_mmap(16) as *i64 1278 sys_clock_gettime_real(ts) 1279 return ts[0] 1280} 1281 1282// Wall-clock milliseconds since the Unix epoch. 1283func sys_now_realtime_ms() -> i64 { 1284 let ts: *i64 = sys_mmap(16) as *i64 1285 sys_clock_gettime_real(ts) 1286 return ts[0] * 1000 + ts[1] / SYS_MAGIC_1000000 1287} 1288 1289// Wall-clock MICROSECONDS since the Unix epoch -- the CROSS-MACHINE stamp. 1290// ★ Use this, never sys_now_us(), for any value one machine writes and ANOTHER machine judges 1291// (fleet beats, lease expiry, telemetry rows). Monotonic counts from each machine's OWN boot, so 1292// subtracting one node's monotonic stamp from another's monotonic now yields the difference of two 1293// unrelated boot epochs -- the remote row then reads as ancient (or future-forged) and a freshness 1294// guard rejects every honest remote node while looking like it is working. 1295func sys_now_realtime_us() -> i64 { 1296 let ts: *i64 = sys_mmap(16) as *i64 1297 sys_clock_gettime_real(ts) 1298 return ts[0] * SYS_MAGIC_1000000 + ts[1] / 1000 1299} 1300 1301// Convenience: monotonic time in milliseconds. Caller does not own 1302// the timespec buffer -- it is mmap'd once per call (cheap; the 1303// underlying syscall already costs more than the page fault). 1304func sys_now_ms() -> i64 { 1305 let ts: *i64 = sys_mmap(16) as *i64 1306 sys_clock_gettime_mono(ts) 1307 let sec_part: i64 = ts[0] * 1000 1308 let nsec_part: i64 = ts[1] / SYS_MAGIC_1000000 1309 return sec_part + nsec_part 1310} 1311 1312// Convenience: monotonic time in microseconds. Used by per-request 1313// elapsed-time tracking in search engines + benches where ms is too 1314// coarse. Same caller-ownership rules as sys_now_ms. 1315func sys_now_us() -> i64 { 1316 let ts: *i64 = sys_mmap(16) as *i64 1317 sys_clock_gettime_mono(ts) 1318 let sec_part: i64 = ts[0] * SYS_MAGIC_1000000 1319 let nsec_part: i64 = ts[1] / 1000 1320 return sec_part + nsec_part 1321} 1322 1323// Alias used by nx_search_onsite_engine etc. Matches `_us` naming 1324// convention. Substrate-canonical name is sys_now_us; this alias 1325// preserves existing call sites without churn. 1326func sys_clock_now_us() -> i64 { 1327 return sys_now_us() 1328} 1329 1330// Read the entire file at `path` into a fresh mmap'd buffer. Returns 1331// a null-terminated *u8 plus writes the byte count to *out_len. On 1332// error (open failure, oversize) returns null and leaves out_len = 0. 1333// Uses a fixed 1 MiB buffer for the first pass; larger sources need a 1334// growth loop. 1335// ---- process control (Linux RV64) ---------------------------- 1336// 1337// Lets NishiLang programs spawn other processes -- prerequisite 1338// for replacing shell scripts (f6_gate.sh) with .nx equivalents. 1339// NishiOS will expose a different process model (capability-based); 1340// these wrappers are the Linux-host compatibility layer. 1341 1342@ifdef TARGET_X86_64 1343const SYS_CLONE: i64 = 56 1344const SYS_EXECVE: i64 = 59 1345const SYS_WAIT4: i64 = 61 1346const SYS_PIPE2: i64 = 293 1347const SYS_DUP3: i64 = 292 1348@endif 1349 1350@ifndef TARGET_X86_64 1351const SYS_CLONE: i64 = 220 1352const SYS_EXECVE: i64 = 221 1353const SYS_WAIT4: i64 = 260 1354const SYS_PIPE2: i64 = 59 1355const SYS_DUP3: i64 = 24 1356@endif 1357 1358// Clone flags (subset). CLONE_VFORK blocks parent until child 1359// exec's or exits, matching fork() semantics closely enough for 1360// our spawn-then-wait patterns. 1361const CLONE_VM: i64 = 0x00000100 1362const CLONE_VFORK: i64 = 0x00004000 1363const SIGCHLD: i64 = 17 1364 1365// Create a child process via Linux clone(). Returns: 1366// > 0 in the parent: child PID 1367// == 0 in the child: child should exec or exit 1368// < 0 on error: -errno 1369// Uses SIGCHLD as the signal that parent receives on child exit 1370// (the libc fork() default); no shared memory or thread flags. 1371// ---- namespace / container family (debt 1785528831) ---------------- 1372// Moved here from nx_syscalls_x86_64.nx so ONE module owns the wrapper set. Their 1373// absence here is why nx_container.nx had to import that module as a SECOND syscall 1374// layer, which put every wrapper in the TU twice and let definition ORDER pick the 1375// winner, silently, until the duplicate-definition guard made it fail closed. 1376func sys_unshare(flags: i64) -> i64 { 1377 return __syscall(SYS_UNSHARE, flags, 0, 0, 0, 0, 0) 1378} 1379func sys_mount(source: *u8, target: *u8, fs_type: *u8, mountflags: i64, data: *u8) -> i64 { 1380 return __syscall(SYS_MOUNT, source, target, fs_type, mountflags, data, 0) 1381} 1382func sys_chroot(path: *u8) -> i64 { 1383 return __syscall(SYS_CHROOT, path, 0, 0, 0, 0, 0) 1384} 1385func sys_getuid() -> i64 { 1386 return __syscall(SYS_GETUID, 0, 0, 0, 0, 0, 0) 1387} 1388func sys_getgid() -> i64 { 1389 return __syscall(SYS_GETGID, 0, 0, 0, 0, 0, 0) 1390} 1391 1392func sys_fork() -> i64 { 1393 return __syscall(SYS_CLONE, SIGCHLD, 0, 0, 0, 0, 0) 1394} 1395 1396// Replace the current process image. `path` is the executable 1397// (absolute or in $PATH if the child first does a fresh clone). 1398// `argv` is a null-terminated array of *u8 (already-marshalled). 1399// `envp` same shape, or null for "inherit parent's env". 1400// Only returns on failure (-errno). 1401// EXEC WITH A CLEAN FD TABLE (seq1785451144). A child inherits every fd its parent held, INCLUDING 1402// listen sockets, across fork AND execve. That is how nx_opaque_login came to hold mgmt s :18098 1403// alongside mgmt itself -- two listeners on one port, connections split between them, a VALID route 1404// answering 404 on some requests. There is no error anywhere in that state, which is why it was 1405// filed as a transport flake for months. 1406// ADDITIVE ON PURPOSE: sys_execve is left byte-identical (910 call sites across 719 files -- a 1407// global change there is unverifiable in one session). Spawners opt in by calling THIS instead. 1408// AUDIT THAT MAKES IT SAFE: zero call sites in the tree dup3 to a target fd above 2, so no exec d 1409// child is deliberately handed a high fd; 0/1/2 are preserved untouched. 1410// Linux child lifetime binding: call in the freshly forked child, before exec. 1411// The expected parent PID is captured before fork, closing the pre-arm death race. 1412// Kernel semantics bind to the creating thread; privileged exec can clear this. 1413const NX_SYS_PRCTL: i64 = 167 1414const NX_PR_SET_PDEATHSIG: i64 = 1 1415const NX_PR_SET_CHILD_SUBREAPER: i64 = 36 1416func sys_prctl(option: i64, arg: i64) -> i64 { 1417 return __syscall(NX_SYS_PRCTL,option,arg,0,0,0,0) 1418} 1419func sys_bind_parent_lifetime(expected_parent: i64, signal: i64) -> i64 { 1420 if expected_parent <= 0 || signal <= 0 { return 0-22 } 1421 let armed: i64=sys_prctl(NX_PR_SET_PDEATHSIG,signal) 1422 if armed < 0 { return armed } 1423 let parent: i64=__syscall(173,0,0,0,0,0,0) 1424 if parent != expected_parent { return 0-10 } 1425 return 0 1426} 1427 1428// Linux waitid observes termination without releasing the child's PID when WNOWAIT is set. 1429// Portable syscall 95 requires the matching x86 backend translation to 247. 1430const SYS_WAITID_PORTABLE: i64 = 95 1431const NX_WAIT_P_PID: i64 = 1 1432const NX_WAIT_EXITED: i64 = 4 1433const NX_WAIT_NOWAIT: i64 = 0x01000000 1434const NX_WAIT_SIGINFO_BYTES: i64 = 128 1435func sys_waitid(idtype: i64, id: i64, info: *u8, options: i64) -> i64 { 1436 return __syscall(SYS_WAITID_PORTABLE,idtype,id,info as i64,options,0,0) 1437} 1438 1439// Post-fork only: the child owns its descriptor table. The buffer bounds a 1440// getdents batch, never the descriptor numbers or number of open handles. 1441const NX_FD_DENT_BUFFER: i64 = 4096 1442const NX_SYS_CLOSE_RANGE: i64 = 436 // Linux x86_64 and asm-generic ABI 1443const NX_FD_UINT_MAX: i64 = 4294967295 1444func sys_close_inherited_proc(first: i64) -> i64 { 1445 let directory: i64=sys_openat_rd("/proc/self/fd") 1446 if directory < 0 { return directory } 1447 let buf: *u8=sys_mmap(NX_FD_DENT_BUFFER) 1448 var result: i64=0 1449 var running: i64=1 1450 while running == 1 { 1451 let n: i64=sys_getdents64(directory,buf,NX_FD_DENT_BUFFER) 1452 if n == (0-4) { continue } 1453 if n <= 0 { result=n; break } 1454 var off: i64=0 1455 while off < n { 1456 if n-off < 20 { result=0-5; running=0; break } 1457 let rec: *u8=buf+off 1458 let size: i64=dirent_reclen(rec) 1459 if size < 20 || size > n-off { result=0-5; running=0; break } 1460 var i: i64=19 1461 var fd: i64=0 1462 var valid: i64=1 1463 while i < size { 1464 let c: i64=rec[i] as i64 1465 if c == 0 { break } 1466 if c < 48 || c > 57 { valid=0; break } 1467 if fd > (2147483647-(c-48))/10 { valid=0; break } 1468 fd=fd*10+c-48; i=i+1 1469 } 1470 if i == 19 || i == size { valid=0 } 1471 if valid == 1 && fd >= first && fd != directory { 1472 // Linux releases the descriptor even when close reports a late 1473 // I/O error; never retry close and risk a reused descriptor. 1474 let closed: i64=sys_close(fd) 1475 if closed < 0 && closed != (0-9) { result=closed; running=0; break } 1476 } 1477 off=off+size 1478 } 1479 } 1480 let closedir: i64=sys_close(directory) 1481 sys_munmap(buf,NX_FD_DENT_BUFFER) 1482 if result == 0 && closedir < 0 { result=closedir } 1483 return result 1484} 1485func sys_close_inherited(first: i64) -> i64 { 1486 if first < 0 { return 0-22 } 1487 let rc: i64=__syscall(NX_SYS_CLOSE_RANGE,first,NX_FD_UINT_MAX,0,0,0,0) 1488 if rc == (0-38) { return sys_close_inherited_proc(first) } 1489 return rc 1490} 1491func sys_execve_clean(path: *u8, argv: *i64, envp: *i64) -> i64 { 1492 let rc: i64=sys_close_inherited(3) 1493 if rc < 0 { return rc } 1494 return sys_execve(path,argv,envp) 1495} 1496 1497func sys_execve(path: *u8, argv: *i64, envp: *i64) -> i64 { 1498 return __syscall(SYS_EXECVE, path, argv, envp, 0, 0, 0) 1499} 1500 1501// Wait for a child to exit. `pid` = -1 waits for ANY child, 1502// otherwise waits for that specific PID. `status` is a caller- 1503// mmapped i64 slot: on exit the low 16 bits carry Linux's w* status 1504// flags (WIFEXITED / WEXITSTATUS). Returns the reaped child's PID 1505// or -errno. 1506func sys_wait4(pid: i64, status: *i64, options: i64) -> i64 { 1507 return __syscall(SYS_WAIT4, pid, status, options, 0, 0, 0) 1508} 1509 1510// Extract exit code from a wait4 status word. Matches the glibc 1511// WEXITSTATUS macro: bits 8-15 of the low 16. 1512func wait_exit_code(status: i64) -> i64 { 1513 return (status >> 8) & 0xFF 1514} 1515 1516// Terminating signal from a wait4 status (0 when the child exited normally). Sibling of 1517// wait_exit_code; RESTORED 2026-07-30 after a stale whole-tree push erased both it and 1518// sys_ignore_sigpipe below, while three files still CALLED them (nx_http_server, nx_sigpipe_gate, 1519// nx_tools_api_serve) -- so the tree could not build until they came back. 1520func wait_term_signal(status: i64) -> i64 { 1521 return status & 0x7f 1522} 1523 1524// THE ONE RULER for "what result code did this process actually produce". Use this, not 1525// wait_exit_code, anywhere the answer becomes a VERDICT. 1526// 1527// WHY IT EXISTS, MEASURED 2026-08-25. wait_exit_code is WEXITSTATUS and is correctly named: 1528// bits 8-15 of the status word. But a child KILLED BY A SIGNAL has no exit status at all, and 1529// those bits are ZERO -- so a SEGFAULTING process is indistinguishable from a clean exit 0 to 1530// every caller that reads only wait_exit_code. Measured live: a gate that SIGSEGV'd mid-run was 1531// served by /api/gate_run as exit_code 0, verdict GREEN. A CRASHED GATE WORE A PASS. 1532// 1533// This is not a new discovery in this estate -- and that is the point. nx_gatekit_lib's 1534// gk_wait_code already carried exactly this rule, with its own measurement recorded (two gates 1535// the 60 s watchdog KILLED journaled `GREEN exit=0 ms=60443`). It was fixed THERE in August and 1536// left unfixed in nx_tool_run, which is the shared exec primitive sitting behind /api/gate_run, 1537// /api/build and 51 other consumers. A LAW APPLIED IN ONE ORGAN AND NOT ITS SIBLING IS HALF A 1538// LAW, AND THE HALF LEFT UNDONE IS THE ONE ON THE PRODUCTION PATH. So the rule now lives HERE, 1539// beside the two accessors it is composed of, and gk_wait_code delegates to it: one ruler. 1540// 1541// Shell convention 128+signal (137 SIGKILL, 139 SIGSEGV) is deliberate: it makes the death both 1542// VISIBLE and NON-ZERO, so every existing caller that branches on rc != 0 sees it with no change. 1543// wait_exit_code is left EXACTLY as it was -- 85 call sites across the corpus (corpus_complete=1) 1544// read it, and silently redefining WEXITSTATUS under them would be the cure being worse. 1545func wait_status_rc(status: i64) -> i64 { 1546 let sig: i64 = wait_term_signal(status) 1547 if sig != 0 { return 128 + sig } 1548 return wait_exit_code(status) 1549} 1550 1551// Ignore SIGPIPE process-wide, so writing to a socket the peer already closed returns -EPIPE 1552// instead of KILLING the process. SIGPIPE default action is TERMINATE, which for a daemon means 1553// every client that walks away mid-response is an outage -- this one call at the listen primitive 1554// is inherited by all 52 consumers of nx_http_server_listen. 1555// rt_sigaction(SIGPIPE, {handler=SIG_IGN}, NULL, 8): syscall 13 on x86-64, which happens to equal 1556// the signal number. SA_RESTORER is deliberately NOT set -- the kernel consults it only when it 1557// DELIVERS a handler frame, and SIG_IGN never delivers one. 1558// PROVEN, not asserted: nx_sigpipe_gate forks a child that writes to a closed pipe and demands 1559// death-by-signal-13 WITHOUT this call and a clean -EPIPE WITH it. 1560// Restore a signal to its DEFAULT disposition. THE INVERSE OF sys_ignore_sigpipe, and it exists 1561// because SIG_IGN is inherited across BOTH fork and execve: a daemon that ignores SIGPIPE hands 1562// that ignore to every child it spawns, FOREVER. That silently corrupted verification -- the 1563// sigpipe gate reported 4/5 RED under /api/gate_run and 5/5 GREEN under a shell, same binary, 1564// same minute, because its DISEASE control (writing to a closed peer must KILL) could not be 1565// observed inside an environment where the kill was already disabled (seq1463). A harness must 1566// not change the state it is verifying; where it must, it has to hand back a clean slate. 1567// ⚠the same inheritance can also produce a FALSE GREEN, which is the far more dangerous half. 1568func sys_default_signal(sig: i64) -> i64 { 1569 let act: *i64 = sys_mmap(64) as *i64 1570 act[0] = 0 1571 act[1] = 0 1572 act[2] = 0 1573 act[3] = 0 1574 return __syscall(13, sig, act as i64, 0, 8, 0, 0) 1575} 1576 1577func sys_ignore_sigpipe() -> i64 { 1578 let act: *i64 = sys_mmap(64) as *i64 1579 act[0] = 1 1580 act[1] = 0 1581 act[2] = 0 1582 act[3] = 0 1583 return __syscall(13, 13, act as i64, 0, 8, 0, 0) 1584} 1585 1586// Create a pipe. `fds` must point at 8+ writable bytes; the kernel 1587// packs BOTH int32 fds into fds[0]: read end = low 32 bits, write end 1588// = HIGH 32 bits (fds[1] is never written -- the old comment claiming 1589// fds[1]=write-end caused a false-pass KAT + a hung gate, 2026-07-16). 1590// Extract: rfd = fds[0] & 0xffffffff; wfd = (fds[0] / 4294967296) & 1591// 0xffffffff. Returns 0 on success, -errno on failure. 1592func sys_pipe2(fds: *i64, flags: i64) -> i64 { 1593 return __syscall(SYS_PIPE2, fds, flags, 0, 0, 0, 0) 1594} 1595 1596// Duplicate `oldfd` onto `newfd`, closing `newfd` first if open. 1597// Used to wire child stdout to a pipe: dup3(pipe_write_end, 1). 1598func sys_dup3(oldfd: i64, newfd: i64, flags: i64) -> i64 { 1599 return __syscall(SYS_DUP3, oldfd, newfd, flags, 0, 0, 0) 1600} 1601 1602// ---- directory listing (Linux RV64 getdents64) --------------- 1603// 1604// Foundation for ls / glob / dir-walk helpers. Linux returns 1605// linux_dirent64 records: 1606// u64 d_ino (inode, ignored here) 1607// s64 d_off (next-record offset) 1608// u16 d_reclen (this record's byte length) 1609// u8 d_type (file type; DT_DIR=4, DT_REG=8, DT_LNK=10) 1610// char d_name[] (null-terminated name, padded so d_reclen 1611// carries us to the next record boundary) 1612// Total struct header: 19 bytes, then name up to d_reclen - 19. 1613 1614@ifdef TARGET_X86_64 1615const SYS_GETDENTS64: i64 = 217 1616@endif 1617@ifndef TARGET_X86_64 1618const SYS_GETDENTS64: i64 = 61 1619@endif 1620 1621const DT_UNKNOWN: i64 = 0 1622const DT_FIFO: i64 = 1 1623const DT_CHR: i64 = 2 1624const DT_DIR: i64 = 4 1625const DT_BLK: i64 = 6 1626const DT_REG: i64 = 8 1627const DT_LNK: i64 = 10 1628const DT_SOCK: i64 = 12 1629 1630// Raw syscall. Returns bytes written on success (0 = end-of-dir), 1631// or -errno on failure. 1632func sys_getdents64(fd: i64, buf: *u8, buf_len: i64) -> i64 { 1633 return __syscall(SYS_GETDENTS64, fd, buf, buf_len, 0, 0, 0) 1634} 1635 1636// Extract fields from a linux_dirent64 record. `rec` points at 1637// the start of the record; fields are at fixed offsets. 1638func dirent_reclen(rec: *u8) -> i64 { 1639 // d_reclen is u16 at offset 16. Read as two bytes little-endian. 1640 let lo: i64 = rec[16] 1641 let hi: i64 = rec[17] 1642 return lo | (hi << 8) 1643} 1644 1645func dirent_type(rec: *u8) -> i64 { 1646 return rec[18] 1647} 1648 1649// Pointer to the null-terminated name inside the record. 1650func dirent_name(rec: *u8) -> *u8 { 1651 let base: i64 = rec as i64 1652 return (base + 19) as *u8 1653} 1654 1655// ---- content-addressed file reader --------------------------- 1656 1657func sys_read_file(path: *u8, out_len: *i64) -> *u8 { 1658 let fd: i64 = sys_openat_rd(path) 1659 if fd < 0 { 1660 *out_len = 0 1661 return 0 as *u8 1662 } 1663 // DEBT-EATEN 2026-07-15: the old fixed 4 GiB cap SILENTLY TRUNCATED bigger files (a 9 GB gguf would 1664 // short-read into plausible-garbage tensors -- the worst failure class). Now the buffer is sized from 1665 // the file itself (lseek END), so ANY size reads fully. Physical pages still allocate on-demand. For 1666 // zero-copy any-size READ-ONLY access prefer sys_map_file (below). 1667 // DEBT-EATEN 2026-08-19 (1787076780): when the size is UNKNOWABLE (lseek END <= 0: /proc files, pipes 1668 // -- AND every empty regular file, which reports 0 just the same) this used to reserve 1669 // SYS_MAGIC_4294967296 of address space per call. Untouched pages were never resident, but the 1670 // mapping WAS: a daemon that read an empty registry every sweep ballooned its VmSize by 4 GiB per 1671 // read (measured: smoke instances at a 4.2 GB base), the leak screens flagged it, and sys_free_file 1672 // could only release what was read. The size-unknowable path now GROWS: start at SYS_READ_GROW_INIT, 1673 // double while the window fills, and hand back an EXACT mapping (total + 16) so sys_free_file 1674 // releases all of it. An empty file costs one small read and a 16-byte arena cell; /proc/stat fits 1675 // the first window; a pipe of any length still reads whole. The known-size path is unchanged. 1676 let fsz: i64 = sys_lseek(fd, 0, 2) 1677 sys_lseek(fd, 0, 0) 1678 var cap: i64 = SYS_READ_GROW_INIT 1679 var grow: i64 = 1 1680 if fsz > 0 { cap = fsz; grow = 0 } 1681 var buf: *u8 = sys_mmap(cap + 16) 1682 var total: i64 = 0 1683 var go: i64 = 1 1684 while go == 1 { 1685 let base: i64 = buf as i64 1686 let tail: *u8 = (base + total) as *u8 1687 let n: i64 = sys_read(fd, tail, cap - total) 1688 if n <= 0 { go = 0 } 1689 if n > 0 { total = total + n } 1690 if total >= cap { 1691 if grow == 0 { go = 0 } else { 1692 // the window filled and the size is unknown: double it, copy, release the old mapping 1693 let ncap: i64 = cap * 2 1694 let nb: *u8 = sys_mmap(ncap + 16) 1695 var ci: i64 = 0 1696 let obase: i64 = buf as i64 1697 let nbase: i64 = nb as i64 1698 while ci < total { let src: *u8 = (obase + ci) as *u8; let dst: *u8 = (nbase + ci) as *u8; dst[0] = src[0]; ci = ci + 1 } 1699 sys_munmap(buf, cap + 16) 1700 buf = nb 1701 cap = ncap 1702 } 1703 } 1704 } 1705 sys_close(fd) 1706 if grow == 1 { 1707 // hand back an EXACT mapping so the paired free releases everything (the doubled window would 1708 // otherwise leave its slack mapped forever -- the address-space leak this change exists to end) 1709 let xb: *u8 = sys_mmap(total + 16) 1710 var xi: i64 = 0 1711 let gbase: i64 = buf as i64 1712 let xbase: i64 = xb as i64 1713 while xi < total { let gsrc: *u8 = (gbase + xi) as *u8; let xdst: *u8 = (xbase + xi) as *u8; xdst[0] = gsrc[0]; xi = xi + 1 } 1714 sys_munmap(buf, cap + 16) 1715 buf = xb 1716 } 1717 // Null-terminate for the lexer. 1718 let bbase: i64 = buf as i64 1719 let term: *u8 = (bbase + total) as *u8 1720 term[0] = 0 1721 *out_len = total 1722 return buf 1723} 1724 1725// PAIRED FREE FOR sys_read_file (2026-08-17). sys_read_file mmaps `cap + 16` where cap is the FILE SIZE 1726// and returns only the pointer -- so any caller that frees it must know the padding, and a caller that 1727// unmaps `len` alone leaks the tail page whenever the file size sits just under a page boundary. 1728// ★A CALLER FORCED TO KNOW ITS ALLOCATOR'S PADDING IS A COUPLING THAT WILL DRIFT -- so the +16 lives 1729// HERE, beside the +16 it mirrors, instead of being retyped at every call site. 1730// Pass the length sys_read_file reported through out_len; this re-derives the mapping from it. 1731// Null-safe by construction: sys_read_file returns 0 on failure, so callers need no extra guard -- 1732// ★A FREE THAT REFUSES NULL IS A FREE NOBODY HAS TO WRAP IN AN IF. 1733// EXACT for every path since 2026-08-19: the size-unknowable fallback (lseek <= 0: /proc, pipes, empty 1734// regular files) now returns a mapping of exactly total + 16, so this releases ALL of it. (It used to 1735// map SYS_MAGIC_4294967296 of address space and release only what was read -- stated then, ended now.) 1736// WHY IT EXISTS: nx_sites_daemon serves /wiki/roadmap by calling sys_read_file PER REQUEST inside a loop 1737// that runs up to NX_SD_MAX_REQ_PER_CONN (64) times per connection and never released it -- an 8,408 B 1738// file became 3 fresh pages and a fresh kernel VMA on every hit, held until the child exited. 1739func sys_free_file(buf: *u8, len: i64) -> i64 { 1740 if (buf as i64) == 0 { return 0 } 1741 if len < 0 { return 0 } 1742 return sys_munmap(buf, len + 16) 1743} 1744 1745// Read-only FILE-BACKED map of the whole file (PROT_READ=1, MAP_PRIVATE=2): any size, zero-copy -- only 1746// touched pages become resident (the lazy-MoE shape: a 9 GB model serves in ~active-set RSS, and load 1747// time is ~0 because nothing is copied). NO NUL pad (a file mapping cannot be extended) -- BINARY 1748// consumers only; text/lexer callers keep sys_read_file. Returns 0 on failure; *out_len = file size. 1749// Read-only by construction (PROT_READ; writes fault -- Rule 26-friendly). 1750func sys_map_file(path: *u8, out_len: *i64) -> *u8 { 1751 *out_len = 0 1752 let fd: i64 = sys_openat_rd(path) 1753 if fd < 0 { return 0 as *u8 } 1754 let fsz: i64 = sys_lseek(fd, 0, 2) 1755 if fsz <= 0 { sys_close(fd); return 0 as *u8 } 1756 let r: i64 = __syscall(SYS_MMAP, 0, fsz, 1, 2, fd, 0) 1757 sys_close(fd) 1758 if r <= 0 { return 0 as *u8 } 1759 *out_len = fsz 1760 return r as *u8 1761} 1762 1763// Sleep for `ms` milliseconds against CLOCK_MONOTONIC (relative). 1764// Returns 0 on success, negative errno on failure. Caller-supplied 1765// budget: ms <= 0 is a no-op; very large values are accepted as-is 1766// (the kernel will saturate to its own clamp). Defined at the bottom 1767// of this file so sys_mmap is in scope (single-pass parser). 1768func sys_sleep_ms(ms: i64) -> i64 { 1769 if ms <= 0 { return 0 } 1770 // struct timespec { sec: i64, nsec: i64 } -- 16 bytes RV64. 1771 let req: *u8 = sys_mmap(16) 1772 let rem: *u8 = sys_mmap(16) 1773 let secs: i64 = ms / 1000 1774 let nsec: i64 = (ms - secs * 1000) * SYS_MAGIC_1000000 // remainder ms -> ns 1775 let req_sec: *i64 = req as *i64 1776 let req_nsec: *i64 = ((req as i64) + 8) as *i64 1777 req_sec[0] = secs 1778 req_nsec[0] = nsec 1779 // clock_nanosleep(CLOCK_MONOTONIC=1, flags=0, req, rem). On EINTR (-4) a signal (e.g. SIGCHLD from a 1780 // reaped child) cut the sleep short and wrote the leftover into rem -- RESUME it, otherwise a caller 1781 // that uses the sleep as a timer (the torrent pool's 2s tick) gets spun into a busy loop by child 1782 // deaths and any tick-based budget collapses to milliseconds. A sleep must sleep its full duration. 1783 var r: i64 = __syscall(SYS_CLOCK_NANOSLEEP, 1, 0, req as i64, rem as i64, 0, 0) 1784 var guard: i64 = 0 1785 while r == (0 - 4) { 1786 if guard > SYS_MAGIC_100000 { r = 0 } else { 1787 let rs: *i64 = rem as *i64 1788 let rn: *i64 = ((rem as i64) + 8) as *i64 1789 req_sec[0] = rs[0] 1790 req_nsec[0] = rn[0] 1791 r = __syscall(SYS_CLOCK_NANOSLEEP, 1, 0, req as i64, rem as i64, 0, 0) 1792 guard = guard + 1 1793 } 1794 } 1795 sys_munmap(req, 16); sys_munmap(rem, 16) // FREE the timespec pages -- every call mmap'd 2 pages; in a 1796 // long-running poll loop (the supervisor's 15s tick) that leaked ~8KB/iter until mmap -> -12 -> SEGFAULT. 1797 return r 1798} 1799 1800// ---- sockets (RV64 generic syscall numbers) ---------------------- 1801// 1802// Source uses RV64 numbers; the x86_64 backend's 1803// x86ctx_rv64_to_x86_64_syscall table translates at codegen time. 1804// Numbers from arch/arm64/include/asm/unistd.h (RV64 inherits the 1805// generic ABI). 1806 1807// Socket-family syscall numbers via @ifdef macro -- mirrors the 1808// pattern already used for SYS_READ/WRITE/MMAP/etc. above. Without 1809// this gate, --target x86_64 compiled the RV64 numbers as literals 1810// into the `syscall` instruction (e.g. 198 = sched_setaffinity on 1811// x86_64, not socket) and any daemon using sys_socket() died with 1812// ENOSYS before printing its banner -- caught by the nx_signaling 1813// stone S2 deploy on 2026-05-20 (see [[project-cross-isa-syscall- 1814// unification-gap-2026-05-20]]). 1815@ifdef TARGET_X86_64 1816const SYS_SOCKET: i64 = 41 1817const SYS_BIND: i64 = 49 1818const SYS_LISTEN: i64 = 50 1819const SYS_ACCEPT: i64 = 43 1820const SYS_CONNECT: i64 = 42 1821const SYS_SETSOCKOPT: i64 = 54 1822const SYS_SENDTO: i64 = 44 1823const SYS_RECVFROM: i64 = 45 1824const SYS_SHUTDOWN: i64 = 48 1825@endif 1826 1827@ifndef TARGET_X86_64 1828const SYS_SOCKET: i64 = 198 1829const SYS_BIND: i64 = 200 1830const SYS_LISTEN: i64 = 201 1831const SYS_ACCEPT: i64 = 202 1832const SYS_CONNECT: i64 = 203 1833const SYS_SETSOCKOPT: i64 = 208 1834const SYS_SENDTO: i64 = 206 1835const SYS_RECVFROM: i64 = 207 1836const SYS_SHUTDOWN: i64 = 210 1837@endif 1838 1839// Socket-option constants used by nx_http_server / nx_https_server. 1840const SOL_SOCKET: i64 = 1 1841const SO_REUSEADDR: i64 = 2 1842// Receive/send timeouts (Linux x86_64). optval is a struct timeval 1843// {tv_sec: i64, tv_usec: i64} (16 bytes). Essential on PUBLIC sockets: 1844// without them, a single silent/slow client hangs a blocking read 1845// forever -> trivial DoS on a single-threaded accept loop. 1846const SO_SNDTIMEO: i64 = 21 1847const SO_RCVTIMEO: i64 = 20 1848 1849// setsockopt(2) -- set a socket option. Defined BEFORE its first caller 1850// (sys_set_socket_timeout, below): NishiLang forbids forward references, 1851// so the definition must precede every use. 1852func sys_setsockopt(fd: i64, level: i64, optname: i64, 1853 optval: *u8, optlen: i64) -> i64 { 1854 return __syscall(SYS_SETSOCKOPT, fd, level, optname, optval, optlen, 0) 1855} 1856 1857// Set a receive+send timeout (in whole seconds) on a socket fd. 1858// tv is munmap'd before return (LEAK FIXED 2026-07-16): this is called once per PROBE by the daemon 1859// supervisor (35/cycle forever -> ~800MB VSZ/day) and once per CONNECTION by fork-per-connection daemons. 1860// The unfreed page-per-call ballooned VSZ until heuristic overcommit made fork() return -ENOMEM (the 1861// proven pid=-12 failure class) -- likely the historical VSZ pressure behind the vsz_watchdog. 1862func sys_set_socket_timeout(fd: i64, secs: i64) -> i64 { 1863 let tv: *i64 = (sys_mmap(16)) as *i64 1864 tv[0] = secs // tv_sec 1865 tv[1] = 0 // tv_usec 1866 sys_setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, tv as *u8, 16) 1867 sys_setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, tv as *u8, 16) 1868 sys_munmap(tv as *u8, 16) 1869 return 0 1870} 1871 1872// alarm(2): deliver SIGALRM after `secs` seconds (0 cancels a pending alarm). No SIGALRM handler is installed, so 1873// the default action TERMINATES the process. Used as a per-request watchdog inside a forked request-child: a 1874// pathologically-slow page can then never hang the child forever (which would leak its buffers + pile up procs). 1875const SYS_ALARM: i64 = 37 1876func sys_alarm(secs: i64) -> i64 { return __syscall(SYS_ALARM, secs, 0, 0, 0, 0, 0) } 1877 1878const AF_INET: i64 = 2 1879const SOCK_STREAM: i64 = 1 1880const SOCK_DGRAM: i64 = 2 1881 1882func sys_socket(domain: i64, sock_type: i64, protocol: i64) -> i64 { 1883 return __syscall(SYS_SOCKET, domain, sock_type, protocol, 0, 0, 0) 1884} 1885// Pack an AF_INET any-address sockaddr_in (16 bytes) for `port` at `addr`. 1886// RESTORED INTO THE OWNER 2026-08-19: this lived in the old full nx_syscalls_x86_64.nx and was the 1887// one wrapper WITH LIVE CALLERS (nx_nishipages_serve, nx_udp) that the 2026-07-31 alias-stub 1888// consolidation dropped -- both lanes sat NAS-unbuildable ("I do not know the name") until the 1889// rebuild-drain surfaced them. Body verbatim from the old file, including its documented 1890// workaround: NO `as u8` casts on the byte stores -- the array-element-store already truncates 1891// when the lvalue is *u8, and casts on this path once tripped a codegen defect. 1892// (The old file's other two uncalled orphans, sys_pivot_root/sys_umount2, were left dead on a 1893// zero-caller full-tree grep -- restoring an uncalled wrapper is inventory, not capability.) 1894func sockaddr_in_init(addr: *u8, port: i64) -> i64 { 1895 addr[0] = 2 // AF_INET low byte 1896 addr[1] = 0 1897 // Port in network byte order (big-endian). 1898 let hi: i64 = (port >> 8) & 0xFF 1899 let lo: i64 = port & 0xFF 1900 addr[2] = hi 1901 addr[3] = lo 1902 addr[4] = 0 1903 addr[5] = 0 1904 addr[6] = 0 1905 addr[7] = 0 1906 addr[8] = 0 1907 addr[9] = 0 1908 addr[10] = 0 1909 addr[11] = 0 1910 addr[12] = 0 1911 addr[13] = 0 1912 addr[14] = 0 1913 addr[15] = 0 1914 return 0 1915} 1916 1917func sys_bind(fd: i64, addr: *u8, addr_len: i64) -> i64 { 1918 return __syscall(SYS_BIND, fd, addr, addr_len, 0, 0, 0) 1919} 1920func sys_listen(fd: i64, backlog: i64) -> i64 { 1921 return __syscall(SYS_LISTEN, fd, backlog, 0, 0, 0, 0) 1922} 1923// accept(2) -- accept the next pending connection on a listening socket. 1924// Single-arg form (kernel ignores NULL addr/addr_len writes). Existing 1925// nx_http_server callers use this signature; the 3-arg form is provided 1926// as sys_accept_with_addr for outliers needing peer address. 1927func sys_accept(fd: i64) -> i64 { 1928 return __syscall(SYS_ACCEPT, fd, 0, 0, 0, 0, 0) 1929} 1930func sys_accept_with_addr(fd: i64, addr: *u8, addr_len: *i64) -> i64 { 1931 return __syscall(SYS_ACCEPT, fd, addr, addr_len, 0, 0, 0) 1932} 1933// shutdown(2) -- half-close a socket. how: 0=RD, 1=WR, 2=RDWR. 1934func sys_shutdown(fd: i64, how: i64) -> i64 { 1935 return __syscall(SYS_SHUTDOWN, fd, how, 0, 0, 0, 0) 1936} 1937func sys_connect(fd: i64, addr: *u8, addr_len: i64) -> i64 { 1938 return __syscall(SYS_CONNECT, fd, addr, addr_len, 0, 0, 0) 1939} 1940func sys_sendto(fd: i64, buf: *u8, n: i64, flags: i64, 1941 dest_addr: *u8, addr_len: i64) -> i64 { 1942 return __syscall(SYS_SENDTO, fd, buf, n, flags, dest_addr, addr_len) 1943} 1944func sys_recvfrom(fd: i64, buf: *u8, n: i64, flags: i64, 1945 src_addr: *u8, addr_len: *i64) -> i64 { 1946 return __syscall(SYS_RECVFROM, fd, buf, n, flags, src_addr, addr_len) 1947} 1948 1949// ---- SCM_RIGHTS DESCRIPTOR PASSING (sendmsg/recvmsg over AF_UNIX) ----------------------------- 1950// ADDED 2026-08-21 for /compare/trafficsafety TS1. Until now sys_sendmsg was ABSENT-PROVEN from the 1951// whole tree (corpus_complete=1), so the mechanism nginx, HAProxy and Envoy all use for hitless 1952// replacement -- MOVING the listening descriptor rather than re-binding it -- could not be written 1953// at all. SO_REUSEPORT co-binding is an ACCEPT-DISTRIBUTION primitive, NOT a handoff primitive: 1954// LWN documents that changing the set of listening sockets on a port drops connections during the 1955// three-way handshake, so co-binding proves two binders and can never prove zero drops. 1956// 1957// EVERY OFFSET BELOW IS MEASURED, NOT RECALLED. They were read out of the platform's own headers 1958// with offsetof/sizeof/CMSG_LEN compiled for x86_64: 1959// msghdr 56 = name 0 | namelen 8 (u32) | iov 16 | iovlen 24 | control 32 | controllen 40 | flags 48 (u32) 1960// iovec 16 = base 0 | len 8 1961// cmsghdr 16 = len 0 (u64) | level 8 (u32) | type 12 (u32), data at 16 1962// CMSG_LEN(4)=20 CMSG_SPACE(4)=24 sendmsg=46 recvmsg=47 socketpair=53 1963// AF_UNIX=1 SOL_SOCKET=1 SCM_RIGHTS=1 MSG_CMSG_CLOEXEC=1073741824 1964// A WRONG LAYOUT HERE DOES NOT FAIL LOUD. The syscall still returns a positive byte count and 1965// simply transfers no descriptor, which is why the gate for this proves the property by passing a 1966// REAL descriptor between two REAL processes and then USING it, never by reading a return code. 1967// x86_64 Linux numbers, DELIBERATELY UNGUARDED, and the reason is a measurement rather than a 1968// preference. The first draft of this block wrapped these three in the same 1969// @ifdef TARGET_X86_64 / @ifndef pair every other syscall number in this file uses. On an x86 build 1970// that made every call ENOSYS, and the probe that caught it printed why: 1971// CONSTS SYS_SENDMSG=211 SYS_RECVMSG=212 SYS_SOCKETPAIR=199 SYS_WRITE=64 1972// N sendmsg PLAIN via the CONST rc=-38 (211 is unassigned on x86_64) 1973// N2 sendmsg PLAIN via the LITERAL rc=1 1974// SYS_WRITE reading 64 is the tell and it is NOT MINE: the file's own original guarded block 1975// resolves to its RV64 branch when the constant is referenced, on a build whose sys_write plainly 1976// works. So a constant inside these guards is not reliably the value the guard appears to select. 1977// !! A GUARD THAT SILENTLY SELECTS THE OTHER TARGET'S NUMBER IS WORSE THAN NO GUARD: the call still 1978// compiles, still returns, and dispatches a DIFFERENT SYSCALL. Syscall 199 on x86_64 is 1979// fremovexattr, which is why socketpair appeared to answer EFAULT for every input including a NULL 1980// vector and an unsupported domain -- varying the ARGUMENTS can never reveal that the NUMBER is 1981// wrong, because every variant was equally wrong. 1982// => RV64 support for these three is an OPEN, NAMED requirement, blocked on that toolchain 1983// behaviour. It is left undone and stated rather than papered over with a guard measured not to 1984// work. The estate already keeps nx_syscalls_x86_64.nx as the explicit single-target mirror for 1985// exactly this class of problem. 1986const SYS_SENDMSG: i64 = 46 1987const SYS_RECVMSG: i64 = 47 1988const SYS_SOCKETPAIR: i64 = 53 1989const SCM_AF_UNIX: i64 = 1 1990const SCM_SOL_SOCKET: i64 = 1 1991const SCM_RIGHTS_TYPE: i64 = 1 1992const SCM_MSG_CMSG_CLOEXEC: i64 = 1073741824 1993const SCM_MSGHDR_BYTES: i64 = 56 1994const SCM_MSGHDR_OFF_IOV: i64 = 16 1995const SCM_MSGHDR_OFF_IOVLEN: i64 = 24 1996const SCM_MSGHDR_OFF_CTRL: i64 = 32 1997const SCM_MSGHDR_OFF_CTRLLEN: i64 = 40 1998const SCM_IOVEC_BYTES: i64 = 16 1999const SCM_IOVEC_OFF_BASE: i64 = 0 2000const SCM_IOVEC_OFF_LEN: i64 = 8 2001const SCM_CMSG_OFF_LEN: i64 = 0 2002const SCM_CMSG_OFF_LEVEL: i64 = 8 2003const SCM_CMSG_OFF_TYPE: i64 = 12 2004const SCM_CMSG_OFF_DATA: i64 = 16 2005const SCM_CMSG_LEN_1FD: i64 = 20 2006const SCM_CMSG_SPACE_1FD: i64 = 24 2007const SCM_IOV_COUNT_ONE: i64 = 1 2008const SCM_U32_BYTES: i64 = 4 2009const SCM_BYTE_RADIX: i64 = 256 2010const SCM_FDPAIR_BYTES: i64 = 8 2011// One real data byte travels with the ancillary data ON PURPOSE: a sendmsg carrying SCM_RIGHTS and 2012// NO ordinary payload is the classic silent no-transfer, and it returns 0 rather than an error. 2013const SCM_PAYLOAD_BYTES: i64 = 1 2014const SCM_PAYLOAD_BYTE: i64 = 70 2015// Distinguishable refusals, each naming WHICH conjunct failed -- a compound assertion that will not 2016// name its failing conjunct is a false-alarm generator. All are negative and all sit far outside the 2017// errno range, so no caller can confuse one with a kernel error or with a valid descriptor. 2018const SCM_ERR_NO_CMSG: i64 = 0 - 901 2019const SCM_ERR_CMSG_LEN: i64 = 0 - 902 2020const SCM_ERR_CMSG_LEVEL: i64 = 0 - 903 2021const SCM_ERR_CMSG_TYPE: i64 = 0 - 904 2022 2023func scm_zero(base: *u8, n: i64) -> i64 { var i: i64 = 0; while i < n { base[i] = 0; i = i + 1 } return 0 } 2024func scm_put_i64(base: *u8, off: i64, v: i64) -> i64 { 2025 let p: *i64 = ((base as i64) + off) as *i64 2026 p[0] = v 2027 return 0 2028} 2029func scm_get_i64(base: *u8, off: i64) -> i64 { 2030 let p: *i64 = ((base as i64) + off) as *i64 2031 return p[0] 2032} 2033// The two cmsg header fields and the descriptor slot itself are 4-byte ints, so they are packed and 2034// unpacked byte by byte in little-endian order. Radix arithmetic rather than bit shifts, matching 2035// sockaddr_in_init's documented style on this exact path. 2036func scm_put_u32(base: *u8, off: i64, v: i64) -> i64 { 2037 var i: i64 = 0 2038 var m: i64 = v 2039 while i < SCM_U32_BYTES { 2040 base[off + i] = m % SCM_BYTE_RADIX 2041 m = m / SCM_BYTE_RADIX 2042 i = i + 1 2043 } 2044 return 0 2045} 2046func scm_get_u32(base: *u8, off: i64) -> i64 { 2047 var v: i64 = 0 2048 var mult: i64 = 1 2049 var i: i64 = 0 2050 while i < SCM_U32_BYTES { 2051 v = v + (base[off + i] as i64) * mult 2052 mult = mult * SCM_BYTE_RADIX 2053 i = i + 1 2054 } 2055 return v 2056} 2057 2058func sys_sendmsg(fd: i64, msg: *u8, flags: i64) -> i64 { 2059 return __syscall(SYS_SENDMSG, fd, msg, flags, 0, 0, 0) 2060} 2061func sys_recvmsg(fd: i64, msg: *u8, flags: i64) -> i64 { 2062 return __syscall(SYS_RECVMSG, fd, msg, flags, 0, 0, 0) 2063} 2064// socketpair(2). sv receives TWO 4-byte descriptors, so it is a *u8 read with scm_get_u32 -- a 2065// single *i64 read would splice both descriptors into one number and the second would vanish. 2066// !! THIS NUMBER IS NOT REACHING socketpair, AND THE FIRST DIAGNOSIS OF THAT WAS WRONG. 2067// Measured 2026-08-21: every call returns -14 (EFAULT) -- with a valid pointer, with a NULL vector, 2068// and with an UNSUPPORTED DOMAIN alike. The first reading of that evidence was "the host refuses 2069// this call for every input", and it was REFUTED by measuring the emitted constants instead of the 2070// arguments. TARGET_X86_64 is hard-pinned UNDEFINED in this toolchain (see nx_syscalls_x86_64.nx 2071// and nx_tokenizer.nx), so the @ifndef branch is what compiles and the x86 backend TRANSLATES RV64 2072// syscall numbers at emit time. Under that translation 53 is RV64 fchmodat, whose SECOND argument 2073// is a path pointer -- and SOCK_STREAM==1 as a path pointer is exactly EFAULT, every time, 2074// regardless of the other arguments. 2075// * VARYING THE ARGUMENTS CAN NEVER REVEAL THAT THE SYSCALL NUMBER IS WRONG: every variant is 2076// equally wrong, so a set of controls that all agree reads as a confident finding about the host. 2077// The control that actually discriminated was PRINTING THE CONSTANT the binary emits. 2078// => The likely correct value here is the RV64 number 199, exactly as sendmsg/recvmsg above needed 2079// their own numbers rather than the guarded pair. That is NOT asserted: it is UNTESTED, and this 2080// comment says so rather than shipping a plausible number with a confident sentence. 2081// => NOTHING DEPENDS ON IT. The descriptor-passing lane uses a NAMED AF_UNIX rendezvous 2082// (sys_unix_listen + sys_unix_connect_fd below), which is proven end to end by nx_scm_rights_gate 2083// and is also what nginx, HAProxy and systemd actually use to move a listener between processes. 2084// socketpair was only ever the convenience. 2085func sys_socketpair(domain: i64, sock_type: i64, protocol: i64, sv: *u8) -> i64 { 2086 return __syscall(SYS_SOCKETPAIR, domain, sock_type, protocol, sv, 0, 0) 2087} 2088 2089// Bind+listen a NAMED AF_UNIX stream socket -- the accepting half of the rendezvous whose 2090// connecting half is nx_unix_connect. Returns the listening fd, or a negative errno. 2091// The caller owns the path: unlink it first (a stale node makes bind return EADDRINUSE) and unlink 2092// it after, because an AF_UNIX bind leaves a filesystem entry that outlives the process. 2093const SCM_SUN_PATH_OFF: i64 = 2 // sockaddr_un = [sa_family: u16][sun_path: 108] 2094const SCM_SUN_BYTES: i64 = 110 2095const SCM_SUN_PATH_MAX: i64 = 107 2096func sys_unix_listen(path: *u8, backlog: i64) -> i64 { 2097 let fd: i64 = sys_socket(SCM_AF_UNIX, SOCK_STREAM, 0) 2098 if fd < 0 { return fd } 2099 let sa: *u8 = sys_mmap(SCM_SUN_BYTES) 2100 var i: i64 = 0 2101 while i < SCM_SUN_BYTES { sa[i] = 0; i = i + 1 } 2102 sa[0] = SCM_AF_UNIX 2103 sa[1] = 0 2104 var p: i64 = 0 2105 while path[p] != (0 as u8) { 2106 if p >= SCM_SUN_PATH_MAX { sys_close(fd); return 0 - 36 } 2107 sa[SCM_SUN_PATH_OFF + p] = path[p] 2108 p = p + 1 2109 } 2110 let br: i64 = sys_bind(fd, sa, SCM_SUN_PATH_OFF + p + 1) 2111 if br < 0 { sys_close(fd); return br } 2112 let lr: i64 = sys_listen(fd, backlog) 2113 if lr < 0 { sys_close(fd); return lr } 2114 return fd 2115} 2116 2117// The CONNECTING half of the same rendezvous. Returns the connected fd or a negative errno. 2118// RESIDUAL NAMED RATHER THAN LEFT SILENT: nx_unix_socket.nx already carries an nx_unix_connect with 2119// this exact body. It is not composed here because that file also defines a main(), so importing it 2120// would inject a second main into every one of the 52 daemons that reach nx_http_server -- a 2121// resolution-by-definition-order hazard this tree has already been bitten by. The primitive belongs 2122// in the shim; the older standalone file should be reduced to a caller of this one, and that is a 2123// separate change to a file with its own consumers rather than something to fold in silently here. 2124func sys_unix_connect_fd(path: *u8) -> i64 { 2125 let fd: i64 = sys_socket(SCM_AF_UNIX, SOCK_STREAM, 0) 2126 if fd < 0 { return fd } 2127 let sa: *u8 = sys_mmap(SCM_SUN_BYTES) 2128 var i: i64 = 0 2129 while i < SCM_SUN_BYTES { sa[i] = 0; i = i + 1 } 2130 sa[0] = SCM_AF_UNIX 2131 sa[1] = 0 2132 var p: i64 = 0 2133 while path[p] != (0 as u8) { 2134 if p >= SCM_SUN_PATH_MAX { sys_close(fd); return 0 - 36 } 2135 sa[SCM_SUN_PATH_OFF + p] = path[p] 2136 p = p + 1 2137 } 2138 let cr: i64 = sys_connect(fd, sa, SCM_SUN_PATH_OFF + p + 1) 2139 if cr < 0 { sys_close(fd); return cr } 2140 return fd 2141} 2142 2143// Send ONE open descriptor over a connected AF_UNIX socket. Returns the sendmsg result: the number 2144// of ordinary data bytes sent (SCM_PAYLOAD_BYTES on success) or a negative errno. The descriptor 2145// itself is NOT closed here -- both ends legitimately hold it until the sender chooses to let go, 2146// and that overlap is the entire point: there must be no instant at which zero processes hold the 2147// listening socket. 2148func sys_send_fd(sock: i64, fd: i64) -> i64 { 2149 let msg: *u8 = sys_mmap(SCM_MSGHDR_BYTES) 2150 let iov: *u8 = sys_mmap(SCM_IOVEC_BYTES) 2151 let cbuf: *u8 = sys_mmap(SCM_CMSG_SPACE_1FD) 2152 let data: *u8 = sys_mmap(SCM_PAYLOAD_BYTES) 2153 scm_zero(msg, SCM_MSGHDR_BYTES) 2154 scm_zero(cbuf, SCM_CMSG_SPACE_1FD) 2155 data[0] = SCM_PAYLOAD_BYTE 2156 scm_put_i64(iov, SCM_IOVEC_OFF_BASE, data as i64) 2157 scm_put_i64(iov, SCM_IOVEC_OFF_LEN, SCM_PAYLOAD_BYTES) 2158 scm_put_i64(msg, SCM_MSGHDR_OFF_IOV, iov as i64) 2159 scm_put_i64(msg, SCM_MSGHDR_OFF_IOVLEN, SCM_IOV_COUNT_ONE) 2160 scm_put_i64(msg, SCM_MSGHDR_OFF_CTRL, cbuf as i64) 2161 scm_put_i64(msg, SCM_MSGHDR_OFF_CTRLLEN, SCM_CMSG_SPACE_1FD) 2162 scm_put_i64(cbuf, SCM_CMSG_OFF_LEN, SCM_CMSG_LEN_1FD) 2163 scm_put_u32(cbuf, SCM_CMSG_OFF_LEVEL, SCM_SOL_SOCKET) 2164 scm_put_u32(cbuf, SCM_CMSG_OFF_TYPE, SCM_RIGHTS_TYPE) 2165 scm_put_u32(cbuf, SCM_CMSG_OFF_DATA, fd) 2166 let r: i64 = sys_sendmsg(sock, msg, 0) 2167 sys_munmap(msg, SCM_MSGHDR_BYTES) 2168 sys_munmap(iov, SCM_IOVEC_BYTES) 2169 sys_munmap(cbuf, SCM_CMSG_SPACE_1FD) 2170 sys_munmap(data, SCM_PAYLOAD_BYTES) 2171 return r 2172} 2173 2174// Receive ONE descriptor from a connected AF_UNIX socket. Returns the NEW descriptor number in this 2175// process (>= 0), a negative errno from recvmsg, or one of the SCM_ERR_* codes above. 2176// flags: 0, or SCM_MSG_CMSG_CLOEXEC so the arriving descriptor is not leaked into grandchildren -- 2177// the estate has already lost a port for six days to exactly that inheritance (nx_cloexec_gate). 2178// THE VALIDATION IS THE WHOLE POINT. recvmsg happily returns a positive byte count having delivered 2179// no ancillary data at all, so the kernel's REWRITTEN msg_controllen is read back rather than the 2180// value we asked for, and each of the three cmsg header fields is checked separately so a failure 2181// says which one. 2182func sys_recv_fd(sock: i64, flags: i64) -> i64 { 2183 let msg: *u8 = sys_mmap(SCM_MSGHDR_BYTES) 2184 let iov: *u8 = sys_mmap(SCM_IOVEC_BYTES) 2185 let cbuf: *u8 = sys_mmap(SCM_CMSG_SPACE_1FD) 2186 let data: *u8 = sys_mmap(SCM_PAYLOAD_BYTES) 2187 scm_zero(msg, SCM_MSGHDR_BYTES) 2188 scm_zero(cbuf, SCM_CMSG_SPACE_1FD) 2189 scm_put_i64(iov, SCM_IOVEC_OFF_BASE, data as i64) 2190 scm_put_i64(iov, SCM_IOVEC_OFF_LEN, SCM_PAYLOAD_BYTES) 2191 scm_put_i64(msg, SCM_MSGHDR_OFF_IOV, iov as i64) 2192 scm_put_i64(msg, SCM_MSGHDR_OFF_IOVLEN, SCM_IOV_COUNT_ONE) 2193 scm_put_i64(msg, SCM_MSGHDR_OFF_CTRL, cbuf as i64) 2194 scm_put_i64(msg, SCM_MSGHDR_OFF_CTRLLEN, SCM_CMSG_SPACE_1FD) 2195 let r: i64 = sys_recvmsg(sock, msg, flags) 2196 var out: i64 = r 2197 if r >= 0 { 2198 out = SCM_ERR_NO_CMSG 2199 if scm_get_i64(msg, SCM_MSGHDR_OFF_CTRLLEN) >= SCM_CMSG_LEN_1FD { 2200 out = SCM_ERR_CMSG_LEN 2201 if scm_get_i64(cbuf, SCM_CMSG_OFF_LEN) == SCM_CMSG_LEN_1FD { 2202 out = SCM_ERR_CMSG_LEVEL 2203 if scm_get_u32(cbuf, SCM_CMSG_OFF_LEVEL) == SCM_SOL_SOCKET { 2204 out = SCM_ERR_CMSG_TYPE 2205 if scm_get_u32(cbuf, SCM_CMSG_OFF_TYPE) == SCM_RIGHTS_TYPE { 2206 out = scm_get_u32(cbuf, SCM_CMSG_OFF_DATA) 2207 } 2208 } 2209 } 2210 } 2211 } 2212 sys_munmap(msg, SCM_MSGHDR_BYTES) 2213 sys_munmap(iov, SCM_IOVEC_BYTES) 2214 sys_munmap(cbuf, SCM_CMSG_SPACE_1FD) 2215 sys_munmap(data, SCM_PAYLOAD_BYTES) 2216 return out 2217} 2218 2219// base64.nx -- RFC 4648 base64 encoder + decoder. 2220// 2221// Canonical: this is the substrate-wide canonical Base64 (RFC 4648 2222// §4 standard alphabet) per [[feedback-no-tool-proliferation-bit- 2223// level]]. Variants that need URL-safe alphabet (RFC 4648 §5) are 2224// candidates for ONE distinct sibling primitive nx_base64_url.nx 2225// (queued) that imports THIS file's encode/decode skeleton; all 2226// other consumers compose THIS file's encode/decode primitives. 2227// Re-implementing the Base64 alphabet or quantum-loop inline is 2228// refused. 2229// 2230// Used for: 2231// - PEM decoding of X.509 certs (thin ASCII wrapper around DER) 2232// - TLS 1.3 pre-shared key encoding 2233// - HTTP Basic auth, OAuth tokens, JWT 2234// - Web content (data: URIs, JSON-embedded bytes) 2235// 2236// Standard alphabet (RFC 4648 §4): 2237// 0-25 : A-Z 2238// 26-51 : a-z 2239// 52-61 : 0-9 2240// 62 : + 2241// 63 : / 2242// pad : = 2243// 2244// URL-safe alphabet variant (§5) swaps +/ for -_; provided as 2245// b64url_encode / b64url_decode. 2246// 2247// Invariants: 2248// B1 Input/output lengths are predictable: 2249// encode(n bytes) -> 4 * ceil(n / 3) chars 2250// decode(n chars) -> 3 * (n / 4) - padding bytes 2251// B2 Decoder rejects invalid input (non-alphabet chars) by 2252// returning a negative length. No silent skip. 2253// B3 Decoder is tolerant of missing padding (RFC 4648 §3.2 2254// permits this as "unpadded" variant). 2255// B4 Encoder is deterministic; same input -> same output. No 2256// trailing whitespace, no line breaks inserted. Callers 2257// that want MIME-style 76-char wrap do it outside. 2258// 2259// license_tier: INDEPENDENT_REDERIVE 2260// genealogy_id: international-research-sources/ietf/rfc_8446 2261// 2262 2263// nx_safety_envelope: 2264// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 2265// sil_target: SIL1 2266// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 2267// verdict: NOT_YET_EVALUATED 2268// 2269// F-meta-4 refactor 2026-05-17: removed `import "nx_syscalls.nx"`. 2270// This file is pure -- all functions operate on caller-provided 2271// buffers + stack-local counters. Composing with any syscall layer 2272// (nx_syscalls.nx OR nx_syscalls_x86_64.nx) no longer hits the 2273// duplicate-symbol bug class. Unblocks crypto chain (nx_jwt -> 2274// nx_jose / jwk / acme / csr) for x86_64 native runtime testing. 2275 2276const B64_PAD: i64 = 0x3D // '=' 2277 2278// Encode one 6-bit index to an ASCII char. Standard alphabet. 2279func b64_enc_char(n: i64) -> i64 { 2280 let v: i64 = n & 0x3F 2281 if v < 26 { return 0x41 + v } // 'A'..'Z' 2282 if v < 52 { return 0x61 + (v - 26) } // 'a'..'z' 2283 if v < 62 { return 0x30 + (v - 52) } // '0'..'9' 2284 if v == 62 { return 0x2B } // '+' 2285 return 0x2F // '/' 2286} 2287 2288// URL-safe variant: replace + / with - _ 2289func b64url_enc_char(n: i64) -> i64 { 2290 let v: i64 = n & 0x3F 2291 if v < 26 { return 0x41 + v } 2292 if v < 52 { return 0x61 + (v - 26) } 2293 if v < 62 { return 0x30 + (v - 52) } 2294 if v == 62 { return 0x2D } // '-' 2295 return 0x5F // '_' 2296} 2297 2298// Decode one ASCII char to 6-bit value; returns -1 if invalid. 2299// Accepts either standard (+,/) or URL-safe (-,_) variants. 2300func b64_dec_char(c: i64) -> i64 { 2301 if c >= 0x41 { if c <= 0x5A { return c - 0x41 } } // A-Z 2302 if c >= 0x61 { if c <= 0x7A { return c - 0x61 + 26 } } // a-z 2303 if c >= 0x30 { if c <= 0x39 { return c - 0x30 + 52 } } // 0-9 2304 if c == 0x2B { return 62 } // + 2305 if c == 0x2F { return 63 } // / 2306 if c == 0x2D { return 62 } // - (URL-safe) 2307 if c == 0x5F { return 63 } // _ (URL-safe) 2308 return -1 2309} 2310 2311// Encode `n` bytes from `in_bytes` to `out`; returns written length. 2312// Output size: 4 * ceil(n / 3) chars. Pads with '=' to full groups. 2313func b64_encode(in_bytes: *u8, n: i64, out: *u8) -> i64 { 2314 var pos: i64 = 0 2315 var out_pos: i64 = 0 2316 while pos + 3 <= n { 2317 let b0: i64 = in_bytes[pos] 2318 let b1: i64 = in_bytes[pos + 1] 2319 let b2: i64 = in_bytes[pos + 2] 2320 out[out_pos + 0] = b64_enc_char((b0 >> 2) & 0x3F) 2321 out[out_pos + 1] = b64_enc_char(((b0 << 4) | (b1 >> 4)) & 0x3F) 2322 out[out_pos + 2] = b64_enc_char(((b1 << 2) | (b2 >> 6)) & 0x3F) 2323 out[out_pos + 3] = b64_enc_char(b2 & 0x3F) 2324 pos = pos + 3 2325 out_pos = out_pos + 4 2326 } 2327 let remain: i64 = n - pos 2328 if remain == 1 { 2329 let b0: i64 = in_bytes[pos] 2330 out[out_pos + 0] = b64_enc_char((b0 >> 2) & 0x3F) 2331 out[out_pos + 1] = b64_enc_char((b0 << 4) & 0x3F) 2332 out[out_pos + 2] = B64_PAD 2333 out[out_pos + 3] = B64_PAD 2334 out_pos = out_pos + 4 2335 } 2336 if remain == 2 { 2337 let b0: i64 = in_bytes[pos] 2338 let b1: i64 = in_bytes[pos + 1] 2339 out[out_pos + 0] = b64_enc_char((b0 >> 2) & 0x3F) 2340 out[out_pos + 1] = b64_enc_char(((b0 << 4) | (b1 >> 4)) & 0x3F) 2341 out[out_pos + 2] = b64_enc_char((b1 << 2) & 0x3F) 2342 out[out_pos + 3] = B64_PAD 2343 out_pos = out_pos + 4 2344 } 2345 return out_pos 2346} 2347 2348// Grab one sextet: return 0..63 on valid, -1 on '=' or end-of-input, 2349// -2 on any other invalid char. Advances *pos on success. 2350func b64_grab(in_chars: *u8, n: i64, pos: *i64) -> i64 { 2351 let p: i64 = *pos 2352 if p >= n { return -1 } 2353 let c: i64 = in_chars[p] 2354 if c == B64_PAD { 2355 *pos = n + 1 2356 return -1 2357 } 2358 let v: i64 = b64_dec_char(c) 2359 if v < 0 { return -2 } 2360 *pos = p + 1 2361 return v 2362} 2363 2364// Decode `n` base64 chars into raw bytes. Returns bytes written on 2365// success or -1 on invalid input. Tolerant of missing padding (B3). 2366// Whitespace is NOT skipped. 2367// 2368// F-meta-4 refactor: position counter is a stack-local; no sys_mmap. 2369func b64_decode(in_chars: *u8, n: i64, out: *u8) -> i64 { 2370 var pos: i64 = 0 2371 var out_pos: i64 = 0 2372 while pos < n { 2373 let s0: i64 = b64_grab(in_chars, n, &pos) 2374 if s0 == -2 { return -1 } 2375 if s0 < 0 { return out_pos } 2376 let s1: i64 = b64_grab(in_chars, n, &pos) 2377 if s1 == -2 { return -1 } 2378 if s1 < 0 { return -1 } // single lonely char invalid 2379 out[out_pos] = ((s0 << 2) | (s1 >> 4)) & 0xFF 2380 out_pos = out_pos + 1 2381 let s2: i64 = b64_grab(in_chars, n, &pos) 2382 if s2 == -2 { return -1 } 2383 if s2 < 0 { return out_pos } 2384 out[out_pos] = ((s1 << 4) | (s2 >> 2)) & 0xFF 2385 out_pos = out_pos + 1 2386 let s3: i64 = b64_grab(in_chars, n, &pos) 2387 if s3 == -2 { return -1 } 2388 if s3 < 0 { return out_pos } 2389 out[out_pos] = ((s2 << 6) | s3) & 0xFF 2390 out_pos = out_pos + 1 2391 } 2392 return out_pos 2393} 2394 2395// Self-test main() removed by F-meta-4 refactor (used sys_mmap; this 2396// file is now syscall-free). Round-trip smoke lives in a separate 2397// nx_base64_test.nx that imports a caller-chosen syscall layer. 2398 2399// Native packaging by artifact reference; no publication or engine-acceptance claim. 2400// nx_fsops_lib.nx -- CONSOLIDATED filesystem tool (MCP name: nx_fs, tool #4 of the 15), LIBRARY half. 2401// (Source is named nx_fsops because nx_fs.nx is the safety-enveloped file-I/O STDLIB -- a different thing.) 2402// READ-ONLY first increment: `read` (bounded file read) + `ls` (typed dir listing). Retires ssh-cat for 2403// remote reads per rule 27 (api-first, no shell plumbing). 2404// 2405// BOUNDARY DEFENSE (rule 12 -- MCP callers are EXTERNAL input): `read` REFUSES any path that matches the 2406// secret DENY-LIST: compiled-in default needles (secret/key/token/passw/.pem, matched case-insensitively 2407// against the WHOLE path) plus data-driven extras from fs_read_deny.conf (one lowercase needle per line, 2408// CWD-relative -- rule 11: policy in data, not code). The tools-api runs where key material lives; an 2409// arbitrary-read tool that could return opaque_keys.bin or tools_cap_secret.key would convert a read-cap 2410// into a key-theft primitive. Over-blocking is the SAFE failure direction for v1. 2411// WRITE/EDIT increment (2026-07-16): fsx_write (ATOMIC tmp+fsync+rename) + fsx_edit (exact-string replace 2412// with the Claude-Edit UNIQUENESS contract). Exposed as the SEPARATE tools-api name `nx_fs_write` (its own 2413// cap class per knowledge/mcp/exposure_policy.txt: read=broad, write=cap) -- the `nx_fs` name stays read-only. 2414// The write DENY is a superset of the read deny (never clobber key material) PLUS the OS device/kernel/ 2415// firmware namespace via the nx_os_fs seam (rule 26 never-brick BY CONSTRUCTION -- not config-disableable) 2416// PLUS the tool-registry escalation surface ("allowlist") PLUS data-driven extras (fs_write_deny.conf). 2417// license_tier: ORIGINAL 2418 2419// nx_fio.nx -- canonical sovereign file operations: unlink (delete) + existence check. Importable (no main). 2420// Retires Remove-Item / rm. rename is already canonical (sys_renameat in nx_syscalls). unlinkat x86_64=263 is passed 2421// DIRECTLY (the fsync-74 / fstatat-262 / unlinkat-263 precedent: a raw x86_64 number not in the rv64->x86 swap table 2422// passes through untranslated). AT_FDCWD=-100, flags=0. Returns 0 on success, -errno on failure. license_tier: ORIGINAL 2423 2424// sha256.nx -- SHA-256 in pure NishiLang (Phase G9, FIPS 180-4). 2425// 2426// Canonical: this is the substrate-wide canonical SHA-256 2427// implementation per [[feedback-no-tool-proliferation-bit-level]]. 2428// HMAC-SHA256 / HKDF-SHA256 / DRBG-SHA256 etc. compose THIS file's 2429// sha256 primitive; they're distinct primitives (different specs: 2430// FIPS 198-1 HMAC, RFC 5869 HKDF, NIST SP 800-90A DRBG) but all 2431// share THIS sha256 as their SHA-256 backbone. Re-implementing 2432// the SHA-256 K-table or round function inline is refused. 2433// 2434// license_tier: INDEPENDENT_REDERIVE 2435// genealogy_id: international-research-sources/nist/fips_180_4 2436// 2437// Used for: content-addressed build artifacts (F6), session tokens 2438// (rand.nx + sha256 = HMAC), TLS 1.3 handshake (G15), Git-style 2439// object addressing, reproducible-build attestation. 2440// 2441// Implementation follows FIPS 180-4 section 6.2 exactly -- no 2442// precomputed tables beyond the standard K[0..63] round constants. 2443// Pure i64 arithmetic; all 32-bit ops masked with 0xFFFFFFFF. 2444// 2445// API: 2446// sha256_init(*ctx) — reset a fresh Sha256 context 2447// sha256_update(*ctx, *u8 bytes, len) — feed input chunks 2448// sha256_final(*ctx, *u8 out32) — write 32-byte digest 2449// sha256_digest(*u8 bytes, len, *u8 out32) — one-shot convenience 2450// 2451// The context is ~128 bytes: 8 words of hash state + 64-byte partial 2452// block buffer + 8-byte length counter + an index. Caller allocates 2453// (stack or heap) and passes pointer. 2454// 2455// nx_safety_envelope: (schema: nishi-library/seeds/safety-critical-standards.toml) 2456// intended_use: "SHA-256 cryptographic hash -- HMAC + HKDF 2457// + content-addressed storage + digital 2458// signatures + Wheeler-DDC integrity chain" 2459// sil_target: SIL3 (integrity primitive; collision or 2460// preimage attack = signature forgery) 2461// asil_target: QM 2462// dal_target: DAL B 2463// iec_62304_class: B 2464// evidence: [no_floating_point, no_table_lookup, 2465// bit_equal_reproducible, 2466// FIPS_180-4_Sec_5_3_3_init_vector, 2467// NIST_CAVP_test_vectors_VERIFIED, 2468// constant_time_by_construction, 2469// license_tier_INDEPENDENT_REDERIVE] 2470// hazard_register: [bug-tape-length-extension-attack, 2471// bug-tape-implementation-skipping-final-block, 2472// bug-tape-state-not-cleared-after-use] 2473// residual_risk: "Length-extension attack applies to raw 2474// SHA-256. Callers MUST use HMAC-SHA-256 2475// (nx_hmac) for keyed scenarios; never raw 2476// SHA-256(key || msg). Substrate cannot 2477// enforce this from the hash primitive's 2478// boundary; it's a composition responsibility." 2479// verdict: NOT_YET_EVALUATED 2480 2481 2482// nx_bits.nx -- bit-manipulation primitives, dispatching to hardware 2483// intrinsics on supported backends with portable software fallbacks. 2484// 2485// Inspired by Hacker's Delight (Henry S. Warren Jr.) -- the canonical 2486// reference for bit-twiddling. Every soft path is BRANCHLESS or 2487// minimally-branched, FIXED-CYCLE, and CROSS-ARCH PORTABLE. 2488// 2489// Dispatch model: 2490// nx_bits_popcount64 / nx_bits_clz32 / nx_bits_ctz32 -> backend 2491// intrinsic on x86_64 (popcntq/bsrl+xor/bsfl) and rv64 with Zbb 2492// (cpop/clzw/ctzw). One machine instruction. Used by hot paths 2493// (sketches, hashing, bitmap iteration). 2494// 2495// nx_bits_popcount64_soft / nx_bits_clz32_soft / nx_bits_ctz32_soft 2496// -- pure-NishiLang SWAR + binary-search variants. Cross-arch 2497// portable to backends without bit-count opcodes. Used by paired 2498// correctness oracles and any caller targeting an exotic ISA. 2499// 2500// Substrate "get off C" trajectory: this module is pure NishiLang. 2501 2502// syscalls.nx -- alias stub. 2503// 2504// nx_syscalls.nx is the canonical syscall surface: same 15 funcs 2505// this file used to define (sys_write/read/mmap/openat/close/exit/ 2506// brk/lseek/getpid/kill/fork/execve/wait4/clone/ioctl) plus 9 2507// additions for sockets and time (sys_socket/bind/listen/accept/ 2508// connect/clock_gettime_mono/now_ms/sleep_ms/openat_append). 2509// 2510// Consumers using `import "syscalls.nx"` resolve via NishiLang's 2511// textual import splicing with path-dedup (runtime/import.nx I1): 2512// this stub splices nx_syscalls.nx once, and any other file that 2513// also imports "nx_syscalls.nx" directly gets deduped to the same 2514// canonical splice. No duplicate symbols. This fixed the 2515// nxasm_main.nx duplicate-symbol error on first link 2026-05-19. 2516// 2517// Deprecation: when every "syscalls.nx" consumer migrates to the 2518// nx_-prefixed name, delete this stub. bench/nx_import_closure.sh 2519// will catch any straggler before the build ships. 2520 2521 2522 2523 2524// === popcount FAST: dispatches to backend intrinsic ============== 2525 2526func nx_bits_popcount64(x: i64) -> i64 { 2527 return __popcnt64(x) 2528} 2529 2530func nx_bits_popcount32(x: i64) -> i64 { 2531 return __popcnt64(x & 0xFFFFFFFF) 2532} 2533 2534// === clz32 / ctz32 FAST: backend intrinsic ======================= 2535// __clz32(0) and __ctz32(0) both return 32 on both backends (x86 2536// uses a tested fallback to set the result; rv64 Zbb returns 32 by 2537// spec) so the wrapper is a thin pass-through. 2538 2539func nx_bits_clz32(x: i64) -> i64 { 2540 return __clz32(x) 2541} 2542 2543func nx_bits_ctz32(x: i64) -> i64 { 2544 return __ctz32(x) 2545} 2546 2547// 64-bit clz / ctz: composed from two 32-bit intrinsics. Until the 2548// backend grows OP_CLZ64 / OP_CTZ64 these are still ~3-instruction 2549// hot paths vs the legacy 64-iteration loops, so they replace those 2550// substrate-wide. clz(0) = 64; ctz(0) = 64. 2551 2552func nx_bits_clz64(x: i64) -> i64 { 2553 let hi: i64 = (x >> 32) & 0xFFFFFFFF 2554 if hi != 0 { return __clz32(hi) } 2555 return 32 + __clz32(x & 0xFFFFFFFF) 2556} 2557 2558func nx_bits_ctz64(x: i64) -> i64 { 2559 let lo: i64 = x & 0xFFFFFFFF 2560 if lo != 0 { return __ctz32(lo) } 2561 if x == 0 { return 64 } 2562 return 32 + __ctz32((x >> 32) & 0xFFFFFFFF) 2563} 2564 2565// === rotate left / right FAST: backend intrinsic ================ 2566// Hardware native via rolq/rorq (x86_64, 1985) and rol/ror (rv64 2567// Zbb). Mask the count to 0..63 so the substrate exposes 2568// "rotate-mod-64" semantics on both ISAs (x86_64 already masks; rv64 2569// behaviour is identical with the explicit mask). 2570 2571func nx_bits_rotl64(x: i64, n: i64) -> i64 { 2572 return __rotl64(x, n & 63) 2573} 2574 2575func nx_bits_rotr64(x: i64, n: i64) -> i64 { 2576 return __rotr64(x, n & 63) 2577} 2578 2579// 32-bit rotate (no native intrinsic emitted; we pre-mask the value 2580// to its low 32 bits so the i64 arithmetic shift right doesn't 2581// contaminate with sign bits, then mask the result back to 32 bits). 2582// ~3 ops vs 5-7 in inline rotr32/rotl32 callsites scattered across 2583// crypto modules (SHA-256, ChaCha20, MurmurHash, etc.). 2584 2585func nx_bits_rotl32(x: i64, n: i64) -> i64 { 2586 let v: i64 = x & 0xFFFFFFFF 2587 let nn: i64 = n & 31 2588 if nn == 0 { return v } 2589 return ((v << nn) | (v >> (32 - nn))) & 0xFFFFFFFF 2590} 2591 2592func nx_bits_rotr32(x: i64, n: i64) -> i64 { 2593 let v: i64 = x & 0xFFFFFFFF 2594 let nn: i64 = n & 31 2595 if nn == 0 { return v } 2596 return ((v >> nn) | (v << (32 - nn))) & 0xFFFFFFFF 2597} 2598 2599// === byte-reverse FAST: backend intrinsic ========================= 2600// bswapq (x86_64, i486 1989+, universal) and rev8 (rv64 Zbb). 1 2601// cycle vs the 13-op SWAR phrasing. Used by every endian flip, 2602// every network/header parse, SHA-256 big-endian word loads. 2603 2604func nx_bits_bswap64(x: i64) -> i64 { 2605 return __bswap64(x) 2606} 2607 2608// 32-bit byte-reverse: mask to low 32 (zero-extends the i64), bswap 2609// the whole register -- the four low bytes get reversed into the top 2610// half -- then shift down to recover them. Mask after shift to 2611// discard the sign extension on inputs where bit 31 of the bswapped 2612// low half is set (which becomes bit 63 of the 64-bit register). 2613func nx_bits_bswap32(x: i64) -> i64 { 2614 return (__bswap64(x & 0xFFFFFFFF) >> 32) & 0xFFFFFFFF 2615} 2616 2617// === SOFT fallbacks: pure NishiLang, cross-arch portable ========= 2618 2619func nx_bits_popcount64_soft(x: i64) -> i64 { 2620 var v: i64 = x 2621 v = v - ((v >> 1) & 0x5555555555555555) 2622 v = (v & 0x3333333333333333) + ((v >> 2) & 0x3333333333333333) 2623 v = (v + (v >> 4)) & 0x0F0F0F0F0F0F0F0F 2624 return ((v * 0x0101010101010101) >> 56) & 0xFF 2625} 2626 2627func nx_bits_popcount32_soft(x: i64) -> i64 { 2628 var v: i64 = x & 0xFFFFFFFF 2629 v = v - ((v >> 1) & 0x55555555) 2630 v = (v & 0x33333333) + ((v >> 2) & 0x33333333) 2631 v = (v + (v >> 4)) & 0x0F0F0F0F 2632 return ((v * 0x01010101) >> 24) & 0xFF 2633} 2634 2635func nx_bits_clz32_soft(x: i64) -> i64 { 2636 let lo: i64 = x & 0xFFFFFFFF 2637 if lo == 0 { return 32 } 2638 var t: i64 = lo 2639 var n: i64 = 0 2640 if (t & 0xFFFF0000) == 0 { n = n + 16; t = t << 16; t = t & 0xFFFFFFFF } 2641 if (t & 0xFF000000) == 0 { n = n + 8; t = t << 8; t = t & 0xFFFFFFFF } 2642 if (t & 0xF0000000) == 0 { n = n + 4; t = t << 4; t = t & 0xFFFFFFFF } 2643 if (t & 0xC0000000) == 0 { n = n + 2; t = t << 2; t = t & 0xFFFFFFFF } 2644 if (t & 0x80000000) == 0 { n = n + 1 } 2645 return n 2646} 2647 2648// 64-bit rotate soft fallback (pure NishiLang -- shift+or, ~5 ops). 2649// Used by paired correctness oracle and by backends without rotate 2650// opcodes. Note: shifting by 0 is the identity; explicit branch 2651// avoids the undefined-behaviour case of `x >> 64` on some ISAs. 2652 2653// The signed >> arithmetic-shifts sign bits in for negative x, so the 2654// shifted-right half must be masked to the actual m / (64-m) low bits 2655// to discard the sign extension. 2656 2657func nx_bits_rotl64_soft(x: i64, n: i64) -> i64 { 2658 let m: i64 = n & 63 2659 if m == 0 { return x } 2660 let top: i64 = (x >> (64 - m)) & ((1 << m) - 1) 2661 return (x << m) | top 2662} 2663 2664func nx_bits_rotr64_soft(x: i64, n: i64) -> i64 { 2665 let m: i64 = n & 63 2666 if m == 0 { return x } 2667 let low: i64 = (x >> m) & ((1 << (64 - m)) - 1) 2668 return low | (x << (64 - m)) 2669} 2670 2671// bswap SOFT (Hacker's Delight 7-1, 13-op SWAR). Used by paired 2672// oracle and exotic backends. 2673 2674func nx_bits_bswap64_soft(x: i64) -> i64 { 2675 var v: i64 = x 2676 v = ((v & 0x00FF00FF00FF00FF) << 8) | ((v >> 8) & 0x00FF00FF00FF00FF) 2677 v = ((v & 0x0000FFFF0000FFFF) << 16) | ((v >> 16) & 0x0000FFFF0000FFFF) 2678 v = ((v & 0x00000000FFFFFFFF) << 32) | ((v >> 32) & 0x00000000FFFFFFFF) 2679 return v 2680} 2681 2682func nx_bits_bswap32_soft(x: i64) -> i64 { 2683 let v: i64 = x & 0xFFFFFFFF 2684 let b0: i64 = (v >> 24) & 0xFF 2685 let b1: i64 = (v >> 16) & 0xFF 2686 let b2: i64 = (v >> 8) & 0xFF 2687 let b3: i64 = (v ) & 0xFF 2688 return (b3 << 24) | (b2 << 16) | (b1 << 8) | b0 2689} 2690 2691// 32-bit rotate SOFT (identical body to FAST; no separate intrinsic 2692// path) -- kept as the named-soft for the consolidation paired-oracle 2693// convention. 2694func nx_bits_rotl32_soft(x: i64, n: i64) -> i64 { 2695 return nx_bits_rotl32(x, n) 2696} 2697func nx_bits_rotr32_soft(x: i64, n: i64) -> i64 { 2698 return nx_bits_rotr32(x, n) 2699} 2700 2701// 64-bit soft fallbacks (Knuth TAOCP 4A linear-scan). O(64) iterations 2702// in the worst case; used by the paired oracle and by exotic backends. 2703 2704func nx_bits_clz64_soft(x: i64) -> i64 { 2705 if x == 0 { return 64 } 2706 var v: i64 = x 2707 var n: i64 = 0 2708 var mask: i64 = 0x8000000000000000 2709 var done: i64 = 0 2710 while done == 0 { 2711 if (v & mask) != 0 { done = 1 } 2712 if done == 0 { 2713 n = n + 1 2714 mask = mask >> 1 2715 if mask == 0 { done = 1 } 2716 } 2717 } 2718 return n 2719} 2720 2721func nx_bits_ctz64_soft(x: i64) -> i64 { 2722 if x == 0 { return 64 } 2723 var v: i64 = x 2724 var n: i64 = 0 2725 var done: i64 = 0 2726 while done == 0 { 2727 if (v & 1) != 0 { done = 1 } 2728 if done == 0 { 2729 n = n + 1 2730 v = v >> 1 2731 if n >= 64 { done = 1 } 2732 } 2733 } 2734 return n 2735} 2736 2737func nx_bits_ctz32_soft(x: i64) -> i64 { 2738 let lo: i64 = x & 0xFFFFFFFF 2739 if lo == 0 { return 32 } 2740 var t: i64 = lo 2741 var n: i64 = 0 2742 if (t & 0x0000FFFF) == 0 { n = n + 16; t = t >> 16 } 2743 if (t & 0x000000FF) == 0 { n = n + 8; t = t >> 8 } 2744 if (t & 0x0000000F) == 0 { n = n + 4; t = t >> 4 } 2745 if (t & 0x00000003) == 0 { n = n + 2; t = t >> 2 } 2746 if (t & 0x00000001) == 0 { n = n + 1 } 2747 return n 2748} 2749 2750// === isolate lowest set bit (Hacker's Delight 2-1) ================ 2751// 2752// x & -x selects only the lowest 1-bit of x. Useful for iterating 2753// set bits in a bitmap (faster than testing each bit). 2754// for bitmap != 0: 2755// bit = nx_bits_lowest(bitmap) 2756// // process bit 2757// bitmap = bitmap ^ bit // clear it 2758 2759func nx_bits_lowest(x: i64) -> i64 { 2760 return x & (0 - x) 2761} 2762 2763// === reset lowest set bit (Hacker's Delight 2-1) ================== 2764// 2765// x & (x-1) clears the lowest 1-bit. When combined with popcount, 2766// gives O(popcount) bit-traversal loops -- faster than O(width) 2767// when the bitmap is sparse. 2768 2769func nx_bits_clear_lowest(x: i64) -> i64 { 2770 return x & (x - 1) 2771} 2772 2773// === is power of 2 (Hacker's Delight 2-1) ========================= 2774// 2775// x > 0 AND (x & (x-1)) == 0. One subtract + one and + one compare. 2776 2777func nx_bits_is_pow2(x: i64) -> i64 { 2778 if x <= 0 { return 0 } 2779 if (x & (x - 1)) == 0 { return 1 } 2780 return 0 2781} 2782 2783// === next power of 2 (Hacker's Delight 3-2) ======================= 2784// 2785// Round up to next power of 2. For x already pow2, returns x. 2786// For x = 0, returns 1. Standard "smear high bit" pattern. 2787 2788func nx_bits_next_pow2_32(x: i64) -> i64 { 2789 if x <= 1 { return 1 } 2790 var v: i64 = (x - 1) & 0xFFFFFFFF 2791 v = v | (v >> 1) 2792 v = v | (v >> 2) 2793 v = v | (v >> 4) 2794 v = v | (v >> 8) 2795 v = v | (v >> 16) 2796 return (v + 1) & 0xFFFFFFFF 2797} 2798 2799// === parity (Hacker's Delight 5-1) ================================ 2800// 2801// Returns 1 if odd number of set bits, 0 if even. Two-and-XOR 2802// reduction, branchless. 2803 2804func nx_bits_parity64(x: i64) -> i64 { 2805 var v: i64 = x 2806 v = v ^ (v >> 32) 2807 v = v ^ (v >> 16) 2808 v = v ^ (v >> 8) 2809 v = v ^ (v >> 4) 2810 return (0x6996 >> (v & 15)) & 1 2811} 2812 2813// === floor(log2(x)) =============================================== 2814// 2815// Equivalent to (31 - clz(x)) for x > 0. Returns -1 for x <= 0. 2816 2817func nx_bits_floor_log2(x: i64) -> i64 { 2818 if x <= 0 { return -1 } 2819 if x <= 0xFFFFFFFF { 2820 return 31 - nx_bits_clz32(x) 2821 } 2822 // High 32 bits set: 32 + log2(x >> 32) 2823 return 63 - nx_bits_clz32(x >> 32) 2824} 2825 2826// === bit-field extract (BMI BEXTR semantics) ====================== 2827// 2828// Extract `len` bits starting at `start` from x. 2829// Equivalent to (x >> start) & ((1 << len) - 1). 2830 2831func nx_bits_bextr(x: i64, start: i64, len: i64) -> i64 { 2832 if len <= 0 { return 0 } 2833 if len >= 64 { return x >> start } 2834 let mask: i64 = (1 << len) - 1 2835 return (x >> start) & mask 2836} 2837 2838const K_MAGIC_536870912: i64 = 536870912 2839 2840struct Sha256 { 2841 // Hash state H[0..7] as i64 (low 32 bits used). 2842 h0: i64, h1: i64, h2: i64, h3: i64, 2843 h4: i64, h5: i64, h6: i64, h7: i64, 2844 2845 // Legacy byte-packed block fields (retained for struct-size 2846 // compatibility; no longer the active buffer -- see bufptr below). 2847 b0: i64, b1: i64, b2: i64, b3: i64, 2848 b4: i64, b5: i64, b6: i64, b7: i64, 2849 2850 // Byte index into the 64-byte block (0..63) and total bits 2851 // processed (for final padding). 2852 idx: i64, 2853 bit_len: i64, 2854 2855 // Scratch buffers allocated ONCE per context in sha256_init (the perf rewrite, 2856 // 2026-06-10: kills the per-block mmap syscall + the per-round K if-chain + 2857 // the linear-scan byte access). ALL three are indexed only by PUBLIC counters 2858 // (byte position / round number 0..63 / schedule index) -- never by secret data -- 2859 // so constant_time_by_construction is preserved (no secret-indexed table access). 2860 bufptr: i64, // -> 64-byte contiguous block buffer (O(1) byte access) 2861 kptr: i64, // -> 64 round constants, materialized once from sha256_k() 2862 wptr: i64, // -> 64-word message schedule, reused every block 2863 2864 // Hardware SHA-NI scratch (the perf path, 2026-07-02). Allocated once per ctx. 2865 // k32ptr -> 64 CONTIGUOUS i32 round constants (the SHA-NI intrinsic wants packed 32-bit 2866 // K, whereas kptr above is 64 i64 for the software loop). st8ptr -> 8 CONTIGUOUS i32 2867 // working state a..h, marshalled from h0..h7 around each __sha256_ni_block call. ni_ok 2868 // caches the CPUID SHA-feature probe (1=use hardware, 0=software fallback) so cpuid runs 2869 // once per hash, not once per block. All three are indexed only by PUBLIC counters -> 2870 // constant_time_by_construction is preserved. 2871 k32ptr: i64, // -> 64 i32 round constants (packed), for the SHA-NI intrinsic 2872 st8ptr: i64, // -> 8 i32 working state a..h, marshalled around __sha256_ni_block 2873 ni_ok: i64, // 1 = CPU has SHA-NI (cpuid(7,0):EBX bit-29) -> hardware compress; 0 = software 2874} 2875 2876// Mask utilities. 2877const M32: i64 = 0xFFFFFFFF 2878 2879// Delegated to nx_bits_rotr32. SHA-256 round does 6 rotates per 2880// word * 64 rounds = 384 rotates per block. 2881func rotr32(x: i64, n: i64) -> i64 { 2882 return nx_bits_rotr32(x, n) 2883} 2884 2885func shr32(x: i64, n: i64) -> i64 { 2886 return (x >> n) & M32 2887} 2888 2889// SHA-256 round constants K[0..63]. Standard cube-roots-of-primes. 2890// We encode as a simple index -> constant lookup; each returns the 2891// i64 with the 32-bit constant in the low bits. 2892func sha256_k(i: i64) -> i64 { 2893 if i == 0 { return 0x428a2f98 } 2894 if i == 1 { return 0x71374491 } 2895 if i == 2 { return 0xb5c0fbcf } 2896 if i == 3 { return 0xe9b5dba5 } 2897 if i == 4 { return 0x3956c25b } 2898 if i == 5 { return 0x59f111f1 } 2899 if i == 6 { return 0x923f82a4 } 2900 if i == 7 { return 0xab1c5ed5 } 2901 if i == 8 { return 0xd807aa98 } 2902 if i == 9 { return 0x12835b01 } 2903 if i == 10 { return 0x243185be } 2904 if i == 11 { return 0x550c7dc3 } 2905 if i == 12 { return 0x72be5d74 } 2906 if i == 13 { return 0x80deb1fe } 2907 if i == 14 { return 0x9bdc06a7 } 2908 if i == 15 { return 0xc19bf174 } 2909 if i == 16 { return 0xe49b69c1 } 2910 if i == 17 { return 0xefbe4786 } 2911 if i == 18 { return 0x0fc19dc6 } 2912 if i == 19 { return 0x240ca1cc } 2913 if i == 20 { return 0x2de92c6f } 2914 if i == 21 { return 0x4a7484aa } 2915 if i == 22 { return 0x5cb0a9dc } 2916 if i == 23 { return 0x76f988da } 2917 if i == 24 { return 0x983e5152 } 2918 if i == 25 { return 0xa831c66d } 2919 if i == 26 { return 0xb00327c8 } 2920 if i == 27 { return 0xbf597fc7 } 2921 if i == 28 { return 0xc6e00bf3 } 2922 if i == 29 { return 0xd5a79147 } 2923 if i == 30 { return 0x06ca6351 } 2924 if i == 31 { return 0x14292967 } 2925 if i == 32 { return 0x27b70a85 } 2926 if i == 33 { return 0x2e1b2138 } 2927 if i == 34 { return 0x4d2c6dfc } 2928 if i == 35 { return 0x53380d13 } 2929 if i == 36 { return 0x650a7354 } 2930 if i == 37 { return 0x766a0abb } 2931 if i == 38 { return 0x81c2c92e } 2932 if i == 39 { return 0x92722c85 } 2933 if i == 40 { return 0xa2bfe8a1 } 2934 if i == 41 { return 0xa81a664b } 2935 if i == 42 { return 0xc24b8b70 } 2936 if i == 43 { return 0xc76c51a3 } 2937 if i == 44 { return 0xd192e819 } 2938 if i == 45 { return 0xd6990624 } 2939 if i == 46 { return 0xf40e3585 } 2940 if i == 47 { return 0x106aa070 } 2941 if i == 48 { return 0x19a4c116 } 2942 if i == 49 { return 0x1e376c08 } 2943 if i == 50 { return 0x2748774c } 2944 if i == 51 { return 0x34b0bcb5 } 2945 if i == 52 { return 0x391c0cb3 } 2946 if i == 53 { return 0x4ed8aa4a } 2947 if i == 54 { return 0x5b9cca4f } 2948 if i == 55 { return 0x682e6ff3 } 2949 if i == 56 { return 0x748f82ee } 2950 if i == 57 { return 0x78a5636f } 2951 if i == 58 { return 0x84c87814 } 2952 if i == 59 { return 0x8cc70208 } 2953 if i == 60 { return 0x90befffa } 2954 if i == 61 { return 0xa4506ceb } 2955 if i == 62 { return 0xbef9a3f7 } 2956 if i == 63 { return 0xc67178f2 } 2957 return 0 2958} 2959 2960// Access byte n (0..63) of the current block buffer. O(1) -- the buffer is contiguous 2961// (was a linear scan over 8 byte-packed fields per access; n is a public position). 2962func blk_byte(c: *Sha256, n: i64) -> i64 { 2963 let p: *u8 = c.bufptr as *u8 2964 return p[n] as i64 2965} 2966 2967// Set byte n (0..63) of the current block buffer. O(1) contiguous store. 2968func blk_set_byte(c: *Sha256, n: i64, v: i64) -> i64 { 2969 let p: *u8 = c.bufptr as *u8 2970 p[n] = (v & 0xFF) as u8 2971 return 0 2972} 2973 2974// Pack bytes [4*i .. 4*i+4) of the current block into a 32-bit 2975// big-endian word (SHA-256 spec is big-endian). 2976func blk_word(c: *Sha256, i: i64) -> i64 { 2977 let off: i64 = i * 4 2978 let b0: i64 = blk_byte(c, off + 0) 2979 let b1: i64 = blk_byte(c, off + 1) 2980 let b2: i64 = blk_byte(c, off + 2) 2981 let b3: i64 = blk_byte(c, off + 3) 2982 return ((b0 << 24) | (b1 << 16) | (b2 << 8) | b3) & M32 2983} 2984 2985// Hardware SHA-NI block compression: marshal the working state h0..h7 into the 8-word i32 2986// buffer, run one full SHA-256 block via the fused __sha256_ni_block intrinsic (which reads 2987// the 64 raw big-endian bytes at bufptr and the packed i32 K table), marshal the updated 2988// state back. Bit-identical to sha256_compress_sw (validated by nx_shani_block_probe against 2989// the software oracle for many blocks + the NIST KAT). ~hardware speed vs the ~40 MB/s soft path. 2990func sha256_compress_ni(c: *Sha256) -> i64 { 2991 let st: *i32 = c.st8ptr as *i32 2992 st[0] = (c.h0 & M32) as i32; st[1] = (c.h1 & M32) as i32 2993 st[2] = (c.h2 & M32) as i32; st[3] = (c.h3 & M32) as i32 2994 st[4] = (c.h4 & M32) as i32; st[5] = (c.h5 & M32) as i32 2995 st[6] = (c.h6 & M32) as i32; st[7] = (c.h7 & M32) as i32 2996 let _r: i64 = __sha256_ni_block(c.st8ptr as *u8, c.bufptr as *u8, c.k32ptr as *u8) 2997 c.h0 = (st[0] as i64) & M32; c.h1 = (st[1] as i64) & M32 2998 c.h2 = (st[2] as i64) & M32; c.h3 = (st[3] as i64) & M32 2999 c.h4 = (st[4] as i64) & M32; c.h5 = (st[5] as i64) & M32 3000 c.h6 = (st[6] as i64) & M32; c.h7 = (st[7] as i64) & M32 3001 return 0 3002} 3003 3004// MULTI-BLOCK SHA-NI (2026-07-02, organ-level, NO new intrinsic): marshal state -> i32 buffer ONCE, 3005// run __sha256_ni_block over `nblk` consecutive 64-byte blocks read DIRECTLY from `blocks` (state 3006// stays resident in st8ptr between calls), marshal back ONCE. Eliminates the per-block marshal AND 3007// the per-byte blk_set_byte buffering of the byte-at-a-time path -> the SHA-NI GB/s lever. Each 3008// __sha256_ni_block is the identical proven compression -> bit-identical to N separate compresses. 3009func sha256_compress_ni_blocks(c: *Sha256, blocks: i64, nblk: i64) -> i64 { 3010 let st: *i32 = c.st8ptr as *i32 3011 st[0] = (c.h0 & M32) as i32; st[1] = (c.h1 & M32) as i32 3012 st[2] = (c.h2 & M32) as i32; st[3] = (c.h3 & M32) as i32 3013 st[4] = (c.h4 & M32) as i32; st[5] = (c.h5 & M32) as i32 3014 st[6] = (c.h6 & M32) as i32; st[7] = (c.h7 & M32) as i32 3015 var b: i64 = 0 3016 while b < nblk { 3017 let blkp: i64 = blocks + b * 64 3018 let _r: i64 = __sha256_ni_block(c.st8ptr as *u8, blkp as *u8, c.k32ptr as *u8) 3019 b = b + 1 3020 } 3021 c.h0 = (st[0] as i64) & M32; c.h1 = (st[1] as i64) & M32 3022 c.h2 = (st[2] as i64) & M32; c.h3 = (st[3] as i64) & M32 3023 c.h4 = (st[4] as i64) & M32; c.h5 = (st[5] as i64) & M32 3024 c.h6 = (st[6] as i64) & M32; c.h7 = (st[7] as i64) & M32 3025 return 0 3026} 3027 3028// One compression function call: process the 64 bytes currently in the block buffer. 3029// Mutates c.h0..c.h7. Routes to hardware SHA-NI when the CPU supports it (probed once in 3030// sha256_init -> c.ni_ok); the pure-integer software path below stays the ORACLE/fallback. 3031func sha256_compress(c: *Sha256) -> i64 { 3032 if c.ni_ok == 1 { return sha256_compress_ni(c) } 3033 // Message schedule W[0..63] + round constants K[0..63] -- both per-ctx scratch 3034 // (allocated once in sha256_init), so no per-block mmap syscall and no K if-chain. 3035 let w: *i64 = c.wptr as *i64 3036 let k: *i64 = c.kptr as *i64 3037 var i: i64 = 0 3038 while i < 16 { 3039 w[i] = blk_word(c, i) 3040 i = i + 1 3041 } 3042 i = 16 3043 while i < 64 { 3044 let x15: i64 = w[i - 15] 3045 let x2: i64 = w[i - 2] 3046 // sigma0/sigma1 with the rotates inlined (was 4 rotr32 calls/iter -> pure arithmetic) 3047 let s0: i64 = (((x15 >> 7) | (x15 << 25)) ^ ((x15 >> 18) | (x15 << 14)) ^ (x15 >> 3)) & M32 3048 let s1: i64 = (((x2 >> 17) | (x2 << 15)) ^ ((x2 >> 19) | (x2 << 13)) ^ (x2 >> 10)) & M32 3049 w[i] = (w[i - 16] + s0 + w[i - 7] + s1) & M32 3050 i = i + 1 3051 } 3052 var a: i64 = c.h0 3053 var b: i64 = c.h1 3054 var cc: i64 = c.h2 3055 var d: i64 = c.h3 3056 var e: i64 = c.h4 3057 var ff: i64 = c.h5 3058 var g: i64 = c.h6 3059 var h: i64 = c.h7 3060 i = 0 3061 while i < 64 { 3062 // Sigma1(e), Sigma0(a) with rotates inlined (was 6 rotr32 calls/round) 3063 let S1: i64 = (((e >> 6) | (e << 26)) ^ ((e >> 11) | (e << 21)) ^ ((e >> 25) | (e << 7))) & M32 3064 let ch: i64 = ((e & ff) ^ ((e ^ M32) & g)) & M32 3065 let t1: i64 = (h + S1 + ch + k[i] + w[i]) & M32 3066 let S0: i64 = (((a >> 2) | (a << 30)) ^ ((a >> 13) | (a << 19)) ^ ((a >> 22) | (a << 10))) & M32 3067 let mj: i64 = ((a & b) ^ (a & cc) ^ (b & cc)) & M32 3068 let t2: i64 = (S0 + mj) & M32 3069 h = g 3070 g = ff 3071 ff = e 3072 e = (d + t1) & M32 3073 d = cc 3074 cc = b 3075 b = a 3076 a = (t1 + t2) & M32 3077 i = i + 1 3078 } 3079 c.h0 = (c.h0 + a) & M32 3080 c.h1 = (c.h1 + b) & M32 3081 c.h2 = (c.h2 + cc) & M32 3082 c.h3 = (c.h3 + d) & M32 3083 c.h4 = (c.h4 + e) & M32 3084 c.h5 = (c.h5 + ff) & M32 3085 c.h6 = (c.h6 + g) & M32 3086 c.h7 = (c.h7 + h) & M32 3087 return 0 3088} 3089 3090// Initialise state. H[0..7] values from FIPS 180-4 section 5.3.3 3091// (first 32 bits of fractional parts of square roots of first 8 3092// primes). 3093// Shared allocation-free initializer: all five scratch pointers are supplied by the owning path. 3094func sha256_seed_allocated(c: *Sha256) -> i64 { 3095 c.h0 = 0x6a09e667; c.h1 = 0xbb67ae85; c.h2 = 0x3c6ef372; c.h3 = 0xa54ff53a 3096 c.h4 = 0x510e527f; c.h5 = 0x9b05688c; c.h6 = 0x1f83d9ab; c.h7 = 0x5be0cd19 3097 // Per-ctx scratch, allocated once (amortized over every block of this hash): 3098 // Materialize the canonical K table once (sha256_k stays the single source of the 3099 // constants -- DRY; the if-chain now runs 64x per HASH, not 64x per BLOCK). 3100 let kp: *i64 = c.kptr as *i64 3101 var i: i64 = 0 3102 while i < 64 { kp[i] = sha256_k(i); i = i + 1 } 3103 let bp: *u8 = c.bufptr as *u8 3104 i = 0 3105 while i < 64 { bp[i] = 0 as u8; i = i + 1 } 3106 c.idx = 0 3107 c.bit_len = 0 3108 3109 // ---- Hardware SHA-NI setup (additive; software path is the oracle/fallback) ---- 3110 // Packed i32 K table for the intrinsic + an 8-word i32 state marshalling buffer. 3111 let k32: *i32 = c.k32ptr as *i32 3112 i = 0 3113 while i < 64 { k32[i] = (sha256_k(i) & M32) as i32; i = i + 1 } 3114 // Probe CPU SHA support ONCE per context: cpuid(leaf=7, subleaf=0):EBX bit-29 = SHA. 3115 // 1<<29 = 0x20000000 = 536870912. Gate the compress path on this; a CPU without SHA-NI 3116 // transparently uses the software compression (byte-identical result, just slower). 3117 if (__cpuid_ebx(7, 0) & K_MAGIC_536870912) != 0 { c.ni_ok = 1 } else { c.ni_ok = 0 } 3118 return 0 3119} 3120 3121func sha256_init(c: *Sha256) -> i64 { 3122 c.bufptr = sys_mmap(64) as i64 3123 c.kptr = sys_mmap(64 * 8) as i64 3124 c.wptr = sys_mmap(64 * 8) as i64 3125 c.k32ptr = sys_mmap(64 * 4) as i64 3126 c.st8ptr = sys_mmap(8 * 4) as i64 3127 return sha256_seed_allocated(c) 3128} 3129 3130// Feed `n` bytes. Buffers partial blocks; compresses full blocks 3131// as soon as they fill. 3132func sha256_update(c: *Sha256, bytes: *u8, n: i64) -> i64 { 3133 var i: i64 = 0 3134 // BULK FAST PATH: when block-aligned (idx==0) and SHA-NI is available, process all full 64-byte 3135 // blocks straight from the input via the resident-state multi-block compress -- skipping both the 3136 // byte-at-a-time blk_set_byte buffering and the per-block state marshalling. Bit-identical. 3137 if c.idx == 0 { 3138 if c.ni_ok == 1 { 3139 let nblk: i64 = n / 64 3140 if nblk > 0 { 3141 sha256_compress_ni_blocks(c, (bytes as i64) + i, nblk) 3142 c.bit_len = c.bit_len + nblk * 512 3143 i = i + nblk * 64 3144 } 3145 } 3146 } 3147 while i < n { 3148 blk_set_byte(c, c.idx, bytes[i]) 3149 c.idx = c.idx + 1 3150 c.bit_len = c.bit_len + 8 3151 if c.idx == 64 { 3152 sha256_compress(c) 3153 c.idx = 0 3154 } 3155 i = i + 1 3156 } 3157 return 0 3158} 3159 3160// Finalise: append 0x80, pad with zeros, append 8-byte bit length, 3161// then do one or two final compressions. Writes 32 bytes to `out`. 3162func sha256_final(c: *Sha256, out: *u8) -> i64 { 3163 // Remember total bit length before padding. 3164 let total_bits: i64 = c.bit_len 3165 // Append 0x80. 3166 blk_set_byte(c, c.idx, 0x80) 3167 c.idx = c.idx + 1 3168 // If not enough room for 8-byte length in this block, pad rest 3169 // with zeros + compress. 3170 if c.idx > 56 { 3171 while c.idx < 64 { 3172 blk_set_byte(c, c.idx, 0) 3173 c.idx = c.idx + 1 3174 } 3175 sha256_compress(c) 3176 c.idx = 0 3177 } 3178 // Pad zeros up to byte 56. 3179 while c.idx < 56 { 3180 blk_set_byte(c, c.idx, 0) 3181 c.idx = c.idx + 1 3182 } 3183 // Write 64-bit big-endian length in bytes 56..63. 3184 blk_set_byte(c, 56, (total_bits >> 56) & 0xFF) 3185 blk_set_byte(c, 57, (total_bits >> 48) & 0xFF) 3186 blk_set_byte(c, 58, (total_bits >> 40) & 0xFF) 3187 blk_set_byte(c, 59, (total_bits >> 32) & 0xFF) 3188 blk_set_byte(c, 60, (total_bits >> 24) & 0xFF) 3189 blk_set_byte(c, 61, (total_bits >> 16) & 0xFF) 3190 blk_set_byte(c, 62, (total_bits >> 8) & 0xFF) 3191 blk_set_byte(c, 63, total_bits & 0xFF) 3192 sha256_compress(c) 3193 // Emit H[0..7] as big-endian 4-byte words. 3194 out[0] = (c.h0 >> 24) & 0xFF 3195 out[1] = (c.h0 >> 16) & 0xFF 3196 out[2] = (c.h0 >> 8) & 0xFF 3197 out[3] = c.h0 & 0xFF 3198 out[4] = (c.h1 >> 24) & 0xFF 3199 out[5] = (c.h1 >> 16) & 0xFF 3200 out[6] = (c.h1 >> 8) & 0xFF 3201 out[7] = c.h1 & 0xFF 3202 out[8] = (c.h2 >> 24) & 0xFF 3203 out[9] = (c.h2 >> 16) & 0xFF 3204 out[10] = (c.h2 >> 8) & 0xFF 3205 out[11] = c.h2 & 0xFF 3206 out[12] = (c.h3 >> 24) & 0xFF 3207 out[13] = (c.h3 >> 16) & 0xFF 3208 out[14] = (c.h3 >> 8) & 0xFF 3209 out[15] = c.h3 & 0xFF 3210 out[16] = (c.h4 >> 24) & 0xFF 3211 out[17] = (c.h4 >> 16) & 0xFF 3212 out[18] = (c.h4 >> 8) & 0xFF 3213 out[19] = c.h4 & 0xFF 3214 out[20] = (c.h5 >> 24) & 0xFF 3215 out[21] = (c.h5 >> 16) & 0xFF 3216 out[22] = (c.h5 >> 8) & 0xFF 3217 out[23] = c.h5 & 0xFF 3218 out[24] = (c.h6 >> 24) & 0xFF 3219 out[25] = (c.h6 >> 16) & 0xFF 3220 out[26] = (c.h6 >> 8) & 0xFF 3221 out[27] = c.h6 & 0xFF 3222 out[28] = (c.h7 >> 24) & 0xFF 3223 out[29] = (c.h7 >> 16) & 0xFF 3224 out[30] = (c.h7 >> 8) & 0xFF 3225 out[31] = c.h7 & 0xFF 3226 return 0 3227} 3228 3229// One-shot: hash `n` bytes, write 32-byte digest to `out`. 3230// Release only scratch owned by this initialized context; the caller owns c. 3231// Reset pointers so explicit cleanup is safe to repeat after completion/failure. 3232func sha256_destroy(c: *Sha256) -> i64 { 3233 if c.bufptr!=0 { sys_munmap(c.bufptr as *u8,64);c.bufptr=0 } 3234 if c.kptr!=0 { sys_munmap(c.kptr as *u8,64*8);c.kptr=0 } 3235 if c.wptr!=0 { sys_munmap(c.wptr as *u8,64*8);c.wptr=0 } 3236 if c.k32ptr!=0 { sys_munmap(c.k32ptr as *u8,64*4);c.k32ptr=0 } 3237 if c.st8ptr!=0 { sys_munmap(c.st8ptr as *u8,8*4);c.st8ptr=0 } 3238 return 0 3239} 3240 3241func sha256_digest(bytes: *u8, n: i64, out: *u8) -> i64 { 3242 let ctx_raw: *u8 = sys_mmap(__size_of(Sha256)) 3243 let ctx: *Sha256 = ctx_raw as *Sha256 3244 sha256_init(ctx) 3245 sha256_update(ctx, bytes, n) 3246 sha256_final(ctx, out) 3247 sha256_destroy(ctx) 3248 sys_munmap(ctx_raw,__size_of(Sha256)) 3249 return 0 3250} 3251 3252// Native Linux x86-64 checked observation path using the existing shared allocator owner. 3253// No cross-backend portability claim: sys_munmap currently uses the native x86-64 release ABI. 3254// Synchronous caller-owned scratch: do not publish it or pass it to forked children. 3255const SHA256_WORD_ALIGN: i64 = 8 3256const SHA256_BLOCK_BYTES: i64 = 64 3257const SHA256_ROUND_WORDS: i64 = 64 3258const SHA256_WIDE_WORD: i64 = 8 3259const SHA256_PACKED_WORD: i64 = 4 3260const SHA256_STATE_WORDS: i64 = 8 3261const SHA256_DIGEST_BYTES: i64 = 32 3262const SHA256_SIGNED_MAX: i64 = 9223372036854775807 3263const SHA256_BITS_PER_BYTE: i64 = 8 3264const SHA256_E_INPUT: i64 = 0-1 3265const SHA256_E_WORKSPACE: i64 = 0-2 3266const SHA256_E_MAPPING: i64 = 0-3 3267const SHA256_E_RELEASE: i64 = 0-4 3268 3269func sha256_context_aligned_bytes() -> i64 { 3270 return ((__size_of(Sha256)+SHA256_WORD_ALIGN-1)/SHA256_WORD_ALIGN)*SHA256_WORD_ALIGN 3271} 3272func sha256_workspace_bytes() -> i64 { 3273 return sha256_context_aligned_bytes()+SHA256_BLOCK_BYTES+2*SHA256_ROUND_WORDS*SHA256_WIDE_WORD+SHA256_ROUND_WORDS*SHA256_PACKED_WORD+SHA256_STATE_WORDS*SHA256_PACKED_WORD 3274} 3275func sha256_checked_input(bytes: *u8, n: i64, out: *u8) -> i64 { 3276 if n < 0 || n > SHA256_SIGNED_MAX/SHA256_BITS_PER_BYTE { return 0 } 3277 let source: i64=bytes as i64; let target: i64=out as i64 3278 if source < 0 || (n > 0 && source == 0) || source > SHA256_SIGNED_MAX-n { return 0 } 3279 if target <= 0 || target > SHA256_SIGNED_MAX-SHA256_DIGEST_BYTES { return 0 } 3280 return 1 3281} 3282func sha256_ranges_overlap(a: i64, an: i64, b: i64, bn: i64) -> i64 { 3283 if an == 0 || bn == 0 { return 0 }; return a < b+bn && b < a+an 3284} 3285// Borrowed scratch. Never call sha256_destroy: the buffers share one allocation. 3286// No allocation/release occurs here. Refused boundary inputs leave output unchanged. 3287// Initialize caller-owned scratch for incremental update/final; never call destroy on it. 3288func sha256_init_workspace(workspace: *u8, capacity: i64) -> i64 { 3289 let base: i64=workspace as i64; let needed: i64=sha256_workspace_bytes() 3290 if base <= 0 || capacity < needed || base > SHA256_SIGNED_MAX-needed || base%SHA256_WORD_ALIGN != 0 { return SHA256_E_WORKSPACE } 3291 let ctx: *Sha256=workspace as *Sha256; var p: i64=base+sha256_context_aligned_bytes() 3292 ctx.bufptr=p; p=p+SHA256_BLOCK_BYTES 3293 ctx.kptr=p; p=p+SHA256_ROUND_WORDS*SHA256_WIDE_WORD 3294 ctx.wptr=p; p=p+SHA256_ROUND_WORDS*SHA256_WIDE_WORD 3295 ctx.k32ptr=p; p=p+SHA256_ROUND_WORDS*SHA256_PACKED_WORD 3296 ctx.st8ptr=p 3297 return sha256_seed_allocated(ctx) 3298} 3299func sha256_digest_workspace(bytes: *u8, n: i64, out: *u8, workspace: *u8, capacity: i64) -> i64 { 3300 if sha256_checked_input(bytes,n,out) != 1 { return SHA256_E_INPUT } 3301 let base: i64=workspace as i64; let needed: i64=sha256_workspace_bytes() 3302 if base <= 0 || capacity < needed || base > SHA256_SIGNED_MAX-needed || base%SHA256_WORD_ALIGN != 0 { return SHA256_E_WORKSPACE } 3303 if sha256_ranges_overlap(base,needed,bytes as i64,n) == 1 || sha256_ranges_overlap(base,needed,out as i64,SHA256_DIGEST_BYTES) == 1 { return SHA256_E_WORKSPACE } 3304 let initialized:i64=sha256_init_workspace(workspace,capacity) 3305 if initialized != 0 { return initialized } 3306 let ctx:*Sha256=workspace as *Sha256 3307 sha256_update(ctx,bytes,n); sha256_final(ctx,out) 3308 return 0 3309} 3310// Takes ownership of an actual whole sys_mmap_shared(workspace_bytes()) result. 3311// A failed mapping leaves output unchanged. A release failure may follow computed output; 3312// callers must accept output only on0. Never supply an arena pointer or undersized mapping. 3313func sha256_digest_mapping_native(bytes: *u8, n: i64, out: *u8, mapping: i64) -> i64 { 3314 if mapping <= 0 { return SHA256_E_MAPPING } 3315 let size: i64=sha256_workspace_bytes() 3316 let result: i64=sha256_digest_workspace(bytes,n,out,mapping as *u8,size) 3317 let released: i64=sys_munmap(mapping as *u8,size) 3318 if result != 0 { return result } 3319 if released != 0 { return SHA256_E_RELEASE } 3320 return 0 3321} 3322func sha256_digest_checked_native(bytes: *u8, n: i64, out: *u8) -> i64 { 3323 if sha256_checked_input(bytes,n,out) != 1 { return SHA256_E_INPUT } 3324 // The existing shared wrapper returns errno; sys_mmap's failure policy is fatal. 3325 let mapping: i64=sys_mmap_shared(sha256_workspace_bytes()) as i64 3326 return sha256_digest_mapping_native(bytes,n,out,mapping) 3327} 3328 3329 3330// Linux syscall ABI results, not admission or retry policy. 3331const FIO_EINTR: i64 = 0 - 4 3332const FIO_EIO: i64 = 0 - 5 3333const FIO_EINVAL: i64 = 0 - 22 3334const FIO_EEXIST: i64 = 0 - 17 3335const FIO_EBADMSG: i64 = 0 - 74 // Linux ABI: artifact digest mismatch. 3336 3337struct NxFileWriteResult { 3338 stage: *u8, 3339 code: i64, 3340 written: i64, 3341 close_code: i64 3342} 3343 3344// Owns fd until close. Preserve the first failure and the independent close 3345// result; never retry close because Linux may already have released the fd. 3346func fio_write_sync_fd(fd: i64, body: *u8, n: i64, result: *NxFileWriteResult) -> i64 { 3347 result.stage = "write" as *u8 3348 result.code = 0 3349 result.written = 0 3350 result.close_code = 0 3351 if n < 0 || ((body as i64) == 0 && n > 0) { 3352 result.stage = "input" as *u8 3353 result.code = FIO_EINVAL 3354 } 3355 while result.code == 0 && result.written < n { 3356 let w: i64 = sys_write(fd, body + result.written, n - result.written) 3357 if w == FIO_EINTR { continue } 3358 if w < 0 { result.code = w; break } 3359 if w == 0 { result.code = FIO_EIO; break } 3360 result.written = result.written + w 3361 } 3362 if result.code == 0 { 3363 result.stage = "fsync" as *u8 3364 var synced: i64 = sys_fsync(fd) 3365 while synced == FIO_EINTR { synced = sys_fsync(fd) } 3366 result.code = synced 3367 } 3368 result.close_code = sys_close(fd) 3369 if result.code == 0 { 3370 result.stage = "close" as *u8 3371 result.code = result.close_code 3372 } 3373 if result.code == 0 { result.stage = "complete" as *u8 } 3374 return result.code 3375} 3376 3377// Persist the directory entry after rename. A failure here occurs after the 3378// visible update: the caller must retain that publication state in its receipt. 3379func fio_sync_parent(path: *u8, result: *NxFileWriteResult) -> i64 { 3380 var length: i64=0 3381 var slash: i64=0-1 3382 while path[length]!=(0 as u8) { if path[length]==(47 as u8) { slash=length }; length=length+1 } 3383 let parent: *u8=sys_mmap(length+2) 3384 if slash<0 { parent[0]=46 as u8; parent[1]=0 as u8 } 3385 else { 3386 var end: i64=slash 3387 if end==0 { end=1 } 3388 var i: i64=0 3389 while i<end { parent[i]=path[i]; i=i+1 } 3390 parent[end]=0 as u8 3391 } 3392 result.stage="directory-open" as *u8 3393 result.close_code=0 3394 let fd: i64=sys_openat_directory(parent) 3395 sys_munmap(parent,length+2) 3396 if fd<0 { result.code=fd; return fd } 3397 result.stage="directory-fsync" as *u8 3398 var synced: i64=sys_fsync(fd) 3399 while synced==FIO_EINTR { synced=sys_fsync(fd) } 3400 result.close_code=sys_close(fd) 3401 result.code=synced 3402 if synced==0 { result.stage="directory-close" as *u8; result.code=result.close_code } 3403 if result.code==0 { result.stage="complete" as *u8 } 3404 return result.code 3405} 3406 3407// delete a file (unlinkat). Returns 0 on success. 3408func fio_unlink(path: *u8) -> i64 { return __syscall(263, 0 - 100, path as i64, 0, 0, 0, 0) } 3409 3410// 1 if `path` exists (fstatat succeeds), else 0. 3411func fio_exists(path: *u8) -> i64 { 3412 let st: *u8 = sys_mmap(160) 3413 if sys_fstatat(path, st) == 0 { return 1 } 3414 return 0 3415} 3416 3417// A caller-owned, single-use read session. Initialize once before open; never 3418// reinitialize an open session. Atomic pathname replacement does not change its fd. 3419struct NxFileReadRegion { 3420 fd: i64, 3421 total: i64, 3422 start: i64, 3423 length: i64, 3424 read_bytes: i64, 3425 last_read: i64, 3426 stage: *u8, 3427 code: i64, 3428 close_code: i64, 3429} 3430func fio_region_init(r: *NxFileReadRegion) -> i64 { 3431 r.fd=0-1;r.total=0;r.start=0;r.length=0;r.read_bytes=0;r.last_read=0 3432 r.stage="initialized";r.code=0;r.close_code=0 3433 return 0 3434} 3435// Linux close consumes ownership even when it reports an error; do not retry it. 3436func fio_region_close(r: *NxFileReadRegion) -> i64 { 3437 if r.fd>=0 { 3438 let fd: i64=r.fd;r.fd=0-1 3439 r.close_code=sys_close(fd) 3440 if r.code==0 && r.close_code<0 { r.code=r.close_code;r.stage="close" } 3441 } 3442 return r.code 3443} 3444func fio_region_fail(r: *NxFileReadRegion,stage: *u8,code: i64) -> i64 { 3445 r.stage=stage;r.code=code 3446 fio_region_close(r) 3447 return code 3448} 3449func fio_region_open(path: *u8,r: *NxFileReadRegion) -> i64 { 3450 if r.fd>=0 { return FIO_EEXIST } 3451 fio_region_init(r) 3452 if (path as i64)==0 { return fio_region_fail(r,"path",FIO_EINVAL) } 3453 if path[0]==(0 as u8) { return fio_region_fail(r,"path",FIO_EINVAL) } 3454 r.stage="open";r.fd=sys_openat_rd(path) 3455 if r.fd<0 { r.code=r.fd;return r.code } 3456 // SEEK_END/SEEK_SET are platform ABI selectors, not transfer-size policy. 3457 let size: i64=sys_lseek(r.fd,0,2) 3458 if size<0 { return fio_region_fail(r,"size-seek",size) } 3459 r.total=size;r.length=size 3460 let back: i64=sys_lseek(r.fd,0,0) 3461 if back!=0 { if back<0 { return fio_region_fail(r,"initial-seek",back) };return fio_region_fail(r,"initial-seek",FIO_EIO) } 3462 r.stage="ready";return 0 3463} 3464// Bounds use subtraction, so start+length can never wrap before validation. 3465func fio_region_select(r: *NxFileReadRegion,start: i64,length: i64) -> i64 { 3466 if r.fd<0 || r.code!=0 || r.read_bytes!=0 { return FIO_EINVAL } 3467 if start<0 || length<0 || start>r.total { return FIO_EINVAL } 3468 if length>r.total-start { return FIO_EINVAL } 3469 let at: i64=sys_lseek(r.fd,start,0) 3470 if at!=start { if at<0 { return fio_region_fail(r,"region-seek",at) };return fio_region_fail(r,"region-seek",FIO_EIO) } 3471 r.start=start;r.length=length;r.stage="ready";return 0 3472} 3473// The caller supplies its reusable transport buffer. No allocation depends on 3474// file size; each read is at most min(buffer capacity, remaining region bytes). 3475func fio_region_next(r: *NxFileReadRegion,out: *u8,cap: i64) -> i64 { 3476 r.last_read=0 3477 if r.code!=0 { return r.code } 3478 if r.fd<0 { if r.read_bytes==r.length { return 0 };return FIO_EINVAL } 3479 if cap<=0 || (out as i64)==0 { return FIO_EINVAL } 3480 let remaining: i64=r.length-r.read_bytes 3481 if remaining==0 { r.stage="complete";return fio_region_close(r) } 3482 var want: i64=remaining;if want>cap { want=cap } 3483 r.stage="read" 3484 while r.last_read<want { 3485 let got: i64=sys_read(r.fd,out+r.last_read,want-r.last_read) 3486 if got==FIO_EINTR { continue } 3487 if got<0 { return fio_region_fail(r,"read",got) } 3488 if got==0 { return fio_region_fail(r,"read-premature-eof",FIO_EIO) } 3489 r.last_read=r.last_read+got;r.read_bytes=r.read_bytes+got 3490 } 3491 if r.read_bytes==r.length { 3492 r.stage="complete" 3493 if fio_region_close(r)<0 { return r.code } 3494 } else { r.stage="ready" } 3495 return r.last_read 3496} 3497 3498 3499// Preparation never replaces a pathname. The caller owns an exclusive candidate 3500// path and keeps it for diagnosis on failure; publication is a separate operation. 3501struct NxFilePrepareResult { 3502 stage: *u8, 3503 code: i64, 3504 copied: i64, 3505 created: i64, 3506 source_close: i64, 3507 destination_close: i64, 3508 durable: i64, 3509} 3510func fio_prepare_copy(source: *u8,candidate: *u8,mode: i64,buffer: *u8,capacity: i64,out: *NxFilePrepareResult) -> i64 { 3511 out.stage="input";out.code=FIO_EINVAL;out.copied=0;out.created=0 3512 out.source_close=0;out.destination_close=0;out.durable=0 3513 if (source as i64)==0 || (candidate as i64)==0 || (buffer as i64)==0 || capacity<=0 { return out.code } 3514 if source[0]==(0 as u8) || candidate[0]==(0 as u8) || mode<0 || mode>0x1ff { return out.code } 3515 let input: *NxFileReadRegion=sys_mmap(__size_of(NxFileReadRegion)) as *NxFileReadRegion 3516 fio_region_init(input) 3517 out.code=fio_region_open(source,input);out.stage="source-open" 3518 var fd: i64=0-1 3519 if out.code==0 { 3520 out.stage="candidate-create" 3521 fd=sys_openat_exclusive(candidate,mode) 3522 if fd<0 { out.code=fd } else { out.created=1 } 3523 } 3524 while out.code==0 && input.read_bytes<input.length { 3525 let n: i64=fio_region_next(input,buffer,capacity) 3526 if n<0 { out.stage="source-read";out.code=n;break } 3527 var sent: i64=0 3528 out.stage="candidate-write" 3529 while sent<n { 3530 let w: i64=sys_write(fd,buffer+sent,n-sent) 3531 if w==FIO_EINTR { continue } 3532 if w<0 { out.code=w;break } 3533 if w==0 { out.code=FIO_EIO;break } 3534 sent=sent+w;out.copied=out.copied+w 3535 } 3536 } 3537 fio_region_close(input);out.source_close=input.close_code 3538 if out.code==0 && input.code!=0 { out.code=input.code;out.stage="source-close" } 3539 sys_munmap(input as *u8,__size_of(NxFileReadRegion)) 3540 if out.code==0 { 3541 out.stage="candidate-mode" 3542 out.code=nx_chmod(candidate,mode) 3543 } 3544 if out.code==0 { 3545 out.stage="candidate-fsync";out.code=sys_fsync(fd) 3546 while out.code==FIO_EINTR { out.code=sys_fsync(fd) } 3547 } 3548 if fd>=0 { 3549 out.destination_close=sys_close(fd) 3550 if out.code==0 && out.destination_close!=0 { out.code=out.destination_close;out.stage="candidate-close" } 3551 } 3552 if out.code==0 { 3553 let sync: *NxFileWriteResult=sys_mmap(__size_of(NxFileWriteResult)) as *NxFileWriteResult 3554 out.code=fio_sync_parent(candidate,sync) 3555 if out.code!=0 { out.stage=sync.stage } 3556 sys_munmap(sync as *u8,__size_of(NxFileWriteResult)) 3557 } 3558 if out.code==0 { out.stage="prepared";out.durable=1 } 3559 return out.code 3560} 3561 3562 3563struct NxFilePublishResult { 3564 stage: *u8, 3565 code: i64, 3566 visible: i64, 3567 durable: i64, 3568} 3569// Caller owns the prepared candidate and target's mutation lock. Rename failure 3570// leaves live intact; sync failure AFTER rename must retain visible=1. 3571func fio_publish_candidate(candidate: *u8,live: *u8,out: *NxFilePublishResult) -> i64 { 3572 out.stage="publish-input";out.code=FIO_EINVAL;out.visible=0;out.durable=0 3573 if (candidate as i64)==0 || (live as i64)==0 { return out.code } 3574 if candidate[0]==(0 as u8) || live[0]==(0 as u8) { return out.code } 3575 out.stage="publish-rename";out.code=sys_renameat(candidate,live) 3576 if out.code!=0 { return out.code } 3577 out.visible=1 3578 let sync: *NxFileWriteResult=sys_mmap(__size_of(NxFileWriteResult)) as *NxFileWriteResult 3579 out.stage="live-directory-sync";out.code=fio_sync_parent(live,sync) 3580 // Both directory entries change if preparation used another directory. 3581 if out.code==0 { out.stage="candidate-directory-sync";out.code=fio_sync_parent(candidate,sync) } 3582 sys_munmap(sync as *u8,__size_of(NxFileWriteResult)) 3583 if out.code==0 { out.stage="published";out.durable=1 } 3584 return out.code 3585} 3586 3587 3588// Stable lock inode: never unlink the lockfile. All cooperating callers must use 3589// the same canonical live pathname in an estate-owned directory. This excludes 3590// arbitrary writers and pathname aliases from the guarantee. 3591struct NxFileTargetLock { 3592 fd: i64, 3593 stage: *u8, 3594 code: i64, 3595 unlock_code: i64, 3596 close_code: i64, 3597} 3598func fio_target_lock_init(lock: *NxFileTargetLock) -> i64 { 3599 lock.fd=0-1;lock.stage="not-started";lock.code=0;lock.unlock_code=0;lock.close_code=0 3600 return 0 3601} 3602func fio_target_lock_release(lock: *NxFileTargetLock) -> i64 { 3603 if lock.fd>=0 { 3604 let fd: i64=lock.fd;lock.fd=0-1 3605 lock.unlock_code=sys_flock(fd,SYS_LOCK_UN) 3606 lock.close_code=sys_close(fd) 3607 if lock.code==0 && lock.unlock_code!=0 { lock.code=lock.unlock_code;lock.stage="target-unlock" } 3608 if lock.code==0 && lock.close_code!=0 { lock.code=lock.close_code;lock.stage="target-lock-close" } 3609 if lock.code==0 { lock.stage="released" } 3610 } 3611 return lock.code 3612} 3613func fio_target_lock_acquire(live: *u8,lock: *NxFileTargetLock) -> i64 { 3614 if lock.fd>=0 { return FIO_EEXIST } 3615 fio_target_lock_init(lock) 3616 lock.stage="target-lock-input";lock.code=FIO_EINVAL 3617 if (live as i64)==0 { return lock.code } 3618 var n: i64=0;while live[n]!=(0 as u8) { n=n+1 } 3619 if n==0 { return lock.code } 3620 let suffix: *u8=".install.lock" 3621 var extra: i64=0;while suffix[extra]!=(0 as u8) { extra=extra+1 } 3622 let bytes: i64=n+extra+1 3623 if bytes<=n { return lock.code } 3624 let path: *u8=sys_mmap(bytes) 3625 if (path as i64)<0 { lock.stage="target-lock-allocation";lock.code=path as i64;return lock.code } 3626 var i: i64=0;while i<n { path[i]=live[i];i=i+1 } 3627 i=0;while i<extra { path[n+i]=suffix[i];i=i+1 };path[n+extra]=0 as u8 3628 lock.stage="target-lock-open";lock.fd=sys_openat_lock(path) 3629 sys_munmap(path,bytes) 3630 if lock.fd<0 { lock.code=lock.fd;return lock.code } 3631 lock.stage="target-lock-acquire";lock.code=sys_flock(lock.fd,SYS_LOCK_EX | SYS_LOCK_NB) 3632 if lock.code!=0 { 3633 let fd: i64=lock.fd;lock.fd=0-1;lock.close_code=sys_close(fd) 3634 return lock.code 3635 } 3636 lock.stage="held";return 0 3637} 3638struct NxFileReplaceResult { 3639 stage: *u8, 3640 code: i64, 3641 candidate: NxFilePrepareResult, 3642 backup: NxFilePrepareResult, 3643 publication: NxFilePublishResult, 3644 lock: NxFileTargetLock, 3645} 3646// Internal locked body. Source remains an immutable artifact; history paths 3647// are exclusive. The public replacement entry points acquire the target lock. 3648func fio_replace_owned(source: *u8,live: *u8,candidate: *u8,backup: *u8,mode: i64,buffer: *u8,capacity: i64,expected_candidate: *u8,expected_live: *u8,out: *NxFileReplaceResult) -> i64 { 3649 out.stage="prepare-candidate" 3650 out.backup.created=0;out.backup.durable=0;out.backup.copied=0;out.backup.code=0 3651 out.backup.stage="not-started";out.backup.source_close=0;out.backup.destination_close=0 3652 out.publication.stage="not-started";out.publication.code=0;out.publication.visible=0;out.publication.durable=0 3653 out.code=fio_prepare_copy(source,candidate,mode,buffer,capacity,&out.candidate) 3654 if out.code!=0 { return out.code } 3655 if (expected_candidate as i64)!=0 { 3656 out.stage="candidate-identity" 3657 out.code=fio_verify_sha256(candidate,expected_candidate,buffer,capacity) 3658 if out.code!=0 { return out.code } 3659 } 3660 out.stage="prepare-backup" 3661 out.code=fio_prepare_copy(live,backup,mode,buffer,capacity,&out.backup) 3662 if out.code!=0 { return out.code } 3663 if (expected_live as i64)!=0 { 3664 out.stage="live-identity" 3665 out.code=fio_verify_sha256(backup,expected_live,buffer,capacity) 3666 if out.code!=0 { return out.code } 3667 } 3668 out.stage="publish" 3669 out.code=fio_publish_candidate(candidate,live,&out.publication) 3670 if out.code==0 { out.stage="complete" } 3671 return out.code 3672} 3673 3674func fio_replace_init(out: *NxFileReplaceResult) -> i64 { 3675 let raw: *u8=out as *u8;var i: i64=0 3676 while i<__size_of(NxFileReplaceResult) { raw[i]=0 as u8;i=i+1 } 3677 out.stage="not-started";out.candidate.stage="not-started" 3678 out.backup.stage="not-started";out.publication.stage="not-started" 3679 fio_target_lock_init(&out.lock);return 0 3680} 3681func fio_replace_core(source: *u8,live: *u8,candidate: *u8,backup: *u8,mode: i64,buffer: *u8,capacity: i64,expected_candidate: *u8,expected_live: *u8,out: *NxFileReplaceResult) -> i64 { 3682 fio_replace_init(out) 3683 out.code=fio_target_lock_acquire(live,&out.lock) 3684 if out.code!=0 { out.stage=out.lock.stage;return out.code } 3685 fio_replace_owned(source,live,candidate,backup,mode,buffer,capacity,expected_candidate,expected_live,out) 3686 let released: i64=fio_target_lock_release(&out.lock) 3687 if out.code==0 && released!=0 { out.code=released;out.stage=out.lock.stage } 3688 return out.code 3689} 3690func fio_replace_with_backup(source: *u8,live: *u8,candidate: *u8,backup: *u8,mode: i64,buffer: *u8,capacity: i64,out: *NxFileReplaceResult) -> i64 { 3691 return fio_replace_core(source,live,candidate,backup,mode,buffer,capacity,0 as *u8,0 as *u8,out) 3692} 3693 3694func fio_replace_verified(source: *u8,live: *u8,candidate: *u8,backup: *u8,mode: i64,buffer: *u8,capacity: i64,expected_candidate: *u8,expected_live: *u8,out: *NxFileReplaceResult) -> i64 { 3695 if (expected_candidate as i64)==0 || (expected_live as i64)==0 { 3696 fio_replace_init(out) 3697 out.stage="identity-input";out.code=FIO_EINVAL 3698 out.candidate.stage="not-started";out.backup.stage="not-started";out.publication.stage="not-started" 3699 return out.code 3700 } 3701 return fio_replace_core(source,live,candidate,backup,mode,buffer,capacity,expected_candidate,expected_live,out) 3702} 3703 3704 3705// Digest comparison is over the prepared bytes, before any live replacement. 3706// Expected points to a SHA-256 digest (32 bytes), not a filename or size. 3707func fio_verify_sha256(path: *u8,expected: *u8,buffer: *u8,capacity: i64) -> i64 { 3708 if (expected as i64)==0 || (buffer as i64)==0 || capacity<=0 { return FIO_EINVAL } 3709 let input: *NxFileReadRegion=sys_mmap(__size_of(NxFileReadRegion)) as *NxFileReadRegion 3710 fio_region_init(input) 3711 var rc: i64=fio_region_open(path,input) 3712 let ctx: *Sha256=sys_mmap(__size_of(Sha256)) as *Sha256 3713 sha256_init(ctx) 3714 while rc==0 && input.read_bytes<input.length { 3715 let n: i64=fio_region_next(input,buffer,capacity) 3716 if n<0 { rc=n;break } 3717 sha256_update(ctx,buffer,n) 3718 } 3719 fio_region_close(input) 3720 if rc==0 { rc=input.code } 3721 if rc==0 { 3722 let actual: *u8=sys_mmap(32) 3723 sha256_final(ctx,actual) 3724 var i: i64=0;var differs: i64=0 3725 while i<32 { differs=differs | ((actual[i] as i64) ^ (expected[i] as i64));i=i+1 } 3726 if differs!=0 { rc=FIO_EBADMSG } 3727 sys_munmap(actual,32) 3728 } 3729 sha256_destroy(ctx);sys_munmap(ctx as *u8,__size_of(Sha256)) 3730 sys_munmap(input as *u8,__size_of(NxFileReadRegion)) 3731 return rc 3732} 3733 3734// nx_itoa_lib.nx -- THE shared integer->decimal emitter. ONE copy, so the corpus stops retyping it. 3735// 3736// LIFTED, NEVER COPIED (2026-07-31, debt 1785557603). ccz_cat_num was already correct, already 3737// MSB-first, already zero-allocation, and already had 10+ callers -- it was simply IMPRISONED inside 3738// nx_crashresume_census_core.nx, a crash-resume census organ. Seven files imported an entire census 3739// just to print an integer. That import cost, NOT ignorance of the primitive, is why ~87 sites 3740// hand-rolled their own. LAW: WHEN A CORRECT PRIMITIVE IS RETYPED, MEASURE ITS IMPORT COST BEFORE 3741// BLAMING DISCOVERABILITY -- people do not retype what is CHEAP to reach. 3742// 3743// THE LEAK WAS NEVER IN THE PRIMITIVE, IT WAS IN THE MISSING WRAPPER. ccz_cat_num allocates nothing. 3744// What every clone hand-rolled was the fd shim around it, e.g. nx_lock_reap_gate.g_putn: 3745// let b: *u8 = sys_mmap(32); let e: i64 = ccz_cat_num(b, 0, v); sys_write(1, b, e); return 0 3746// -- one mmap per call, never freed. nxi_fd below is that shim, written ONCE and always freeing. 3747// 3748// The census now imports THIS file; NishiLang import is transitive (verified: nx_lock_reap_gate 3749// imports only nx_syscalls + nx_lock_reap_core, and resolves ccz_cat_num through the core), so all 3750// existing callers keep resolving with no edit. 3751// 3752// LAYERING: lives in runtime/ so BOTH runtime/ and _hdl_build/ can import it. 3753// license_tier: ORIGINAL No hw writes (Rule 26). 3754 3755// MSB-FIRST (2026-07-31, debt 1785516350): the previous body built digits LEAST-significant first, 3756// which comes out BACKWARDS and therefore needed a sys_mmap(32) scratch buffer to reverse through -- 3757// and never freed it, leaking a page per call across 12+ importers. Emitting MOST-significant first 3758// needs no buffer at all, so this now ALLOCATES NOTHING. Output bytes and the NUL-terminate contract 3759// are unchanged; this is a rewrite of the algorithm, not a sprinkled munmap (rule 3). 3760const CCZ_ASCII_0: i64 = 48 3761const CCZ_MINUS: i64 = 45 3762const CCZ_DEC: i64 = 10 3763func ccz_cat_num(buf: *u8, off: i64, v: i64) -> i64 { 3764 var o: i64 = off 3765 var m: i64 = v 3766 if m == 0 { buf[o] = CCZ_ASCII_0 as u8; o = o + 1; buf[o] = 0 as u8; return o } 3767 if m < 0 { buf[o] = CCZ_MINUS as u8; o = o + 1; m = 0 - m } 3768 // i64 MIN negates to itself and stays negative; clamp rather than loop forever on the digit walk. 3769 if m < 0 { m = 0 } 3770 var pw: i64 = 1 3771 while m / pw >= CCZ_DEC { pw = pw * CCZ_DEC } 3772 while pw > 0 { 3773 buf[o] = (CCZ_ASCII_0 + ((m / pw) % CCZ_DEC)) as u8 3774 o = o + 1 3775 pw = pw / CCZ_DEC 3776 } 3777 buf[o] = 0 as u8 3778 return o 3779} 3780 3781// max i64 is 19 digits + sign + the NUL ccz_cat_num writes; 24 leaves slack, well under one page. 3782const NXI_BUF: i64 = 24 3783const NXI_STDOUT: i64 = 1 3784const NXI_STDERR: i64 = 2 3785 3786// Write v as decimal to fd. ONE buffer, ALWAYS freed -- the balanced shape nx_mmapbal certifies. 3787// This is the drop-in for every hand-rolled putn/gn/wn/pn clone. Returns bytes written. 3788func nxi_fd(fd: i64, v: i64) -> i64 { 3789 let b: *u8 = sys_mmap(NXI_BUF) 3790 let n: i64 = ccz_cat_num(b, 0, v) 3791 sys_write(fd, b, n) 3792 sys_munmap(b, NXI_BUF) 3793 return n 3794} 3795 3796func nxi_out(v: i64) -> i64 { return nxi_fd(NXI_STDOUT, v) } 3797func nxi_err(v: i64) -> i64 { return nxi_fd(NXI_STDERR, v) } 3798 3799// NUL-FREE buffer form (2026-07-31). ccz_cat_num NUL-terminates -- it writes dst[ret]=0 -- which is 3800// right for its own callers but WRONG as a drop-in for the large clone family whose contract is 3801// "append digits, touch nothing else, return the new offset". Pointing those at ccz_cat_num would 3802// write one byte past the returned offset, and a clone that patches a number into the MIDDLE of an 3803// already-built buffer would have the next byte clobbered. nx_office_serve.of_catn alone has 60 3804// call sites, none of them audited for that. 3805// So the lib carries BOTH contracts explicitly rather than making every migrator guess: 3806// ccz_cat_num -> digits + NUL, returns the offset BEFORE the NUL 3807// nxi_buf -> digits only, returns the offset AFTER them, ZERO bytes touched beyond 3808// Both are MSB-first and allocate NOTHING. Constants are the CCZ_ ones lifted with ccz_cat_num. 3809func nxi_buf(dst: *u8, off: i64, v: i64) -> i64 { 3810 var p: i64 = off 3811 var m: i64 = v 3812 if m < 0 { 3813 dst[p] = CCZ_MINUS as u8 3814 p = p + 1 3815 m = 0 - m 3816 } 3817 // i64 MIN negates to ITSELF and stays negative. Clamp to 0 rather than looping forever or 3818 // emitting garbage -- a documented bound, never a silent wrong number. 3819 if m < 0 { m = 0 } 3820 var pw: i64 = 1 3821 while m / pw >= CCZ_DEC { pw = pw * CCZ_DEC } 3822 while pw > 0 { 3823 dst[p] = (CCZ_ASCII_0 + ((m / pw) % CCZ_DEC)) as u8 3824 p = p + 1 3825 pw = pw / CCZ_DEC 3826 } 3827 return p 3828} 3829 3830// nx_vsz_watchdog_core.nx -- importable CORE of the VSZ watchdog (the permanent fix for outage MODE 2: 3831// mmap-per-request daemons never munmap -> VSZ balloons (mgmt hit ~160GB) -> fork() fails -> child-exec 3832// SILENTLY EMPTY while /api/health stays 200; see reference-mgmt-api-outage-tmp-log-rootcause-2026-07-12). 3833// The watchdog DECIDES DEATH ONLY: it kills a conf-listed daemon whose VSZ crossed its threshold; RESPAWN 3834// stays 100% the nx_hostctl guard's job (single responsibility, no dueling supervisors). FAIL-SAFE BY 3835// CONSTRUCTION: no conf file -> INERT; unreadable /proc -> skip; cooldown suppresses kill-storms; pid<=300 3836// and self are never killed. Pure decision funcs here (gate-locked); the /proc walk + kill live in the CLI. 3837// license_tier: ORIGINAL 3838 3839 3840const VW_PROC_PATH_CAP: i64 = 256 // /proc/<pid>/status path buffer 3841const VW_STATUS_BUF: i64 = 8192 // /proc status read buffer 3842const VW_PTR_CELL: i64 = 16 // 2-i64 scratch cell (vw_num_at end-pointer out-param) 3843 3844func vw_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 3845 3846// substring containment (hostctl's hc_contains idiom; needle has no NUL so cmdline NUL separators are safe). 3847func vw_contains(hay: *u8, hn: i64, needle: *u8, nn: i64) -> i64 { 3848 if nn == 0 { return 0 } 3849 var i: i64 = 0 3850 while i + nn <= hn { 3851 var k: i64 = 0 3852 var ok: i64 = 1 3853 while k < nn { if hay[i+k] != needle[k] { ok = 0; k = nn } k = k + 1 } 3854 if ok == 1 { return 1 } 3855 i = i + 1 3856 } 3857 return 0 3858} 3859 3860// parse leading unsigned decimal from s[off..n): value, or -1 if no digit at off. end offset in endp[0]. 3861func vw_num_at(s: *u8, n: i64, off: i64, endp: *i64) -> i64 { 3862 var v: i64 = 0 3863 var any: i64 = 0 3864 var i: i64 = off 3865 var go: i64 = 1 3866 while go == 1 { 3867 go = 0 3868 if i < n { let c: i64 = s[i] as i64; if c >= 48 { if c <= 57 { v = v*10 + (c-48); any = 1; i = i + 1; go = 1 } } } 3869 } 3870 endp[0] = i 3871 if any == 0 { return 0 - 1 } 3872 return v 3873} 3874 3875// parse one conf line buf[ls..le): "<needle> <max_gb>". Writes NUL after the needle IN PLACE, returns gb 3876// (>=1) with needle start in outp[0], or -1 for comment/blank/malformed (row dropped, fail-safe). 3877func vw_parse_row(buf: *u8, ls: i64, le: i64, outp: *i64) -> i64 { 3878 var i: i64 = ls 3879 var go: i64 = 1 3880 while go == 1 { go = 0; if i < le { let c: i64 = buf[i] as i64; if c == 32 { i = i + 1; go = 1 } else { if c == 9 { i = i + 1; go = 1 } } } } 3881 if i >= le { return 0 - 1 } 3882 if buf[i] == (35 as u8) { return 0 - 1 } // '#' comment 3883 let nstart: i64 = i 3884 go = 1 3885 while go == 1 { go = 0; if i < le { let c: i64 = buf[i] as i64; if c != 32 { if c != 9 { i = i + 1; go = 1 } } } } 3886 if i >= le { return 0 - 1 } // no separator -> malformed 3887 let nend: i64 = i 3888 let ep: *i64 = sys_mmap(VW_PTR_CELL) as *i64 3889 var j: i64 = i 3890 go = 1 3891 while go == 1 { go = 0; if j < le { let c: i64 = buf[j] as i64; if c == 32 { j = j + 1; go = 1 } else { if c == 9 { j = j + 1; go = 1 } } } } 3892 let gb: i64 = vw_num_at(buf, le, j, ep) 3893 sys_munmap(ep as *u8, VW_PTR_CELL) // leak-free: ep (the end-ptr out-param) was leaked per row (the ep-out-param class my leak-checker flagged) 3894 if gb < 1 { return 0 - 1 } // gb<1 -> inert row (never a 0-threshold kill-everything) 3895 buf[nend] = 0 as u8 // NUL-terminate the needle in place 3896 outp[0] = nstart 3897 return gb 3898} 3899 3900// parse the kB value of an arbitrary "<Label>:" row out of a /proc status text. -1 absent/unreadable. 3901// Generalized so VmSize (address space) and VmRSS (resident -- the heap-leak meter VSZ can hide) share ONE 3902// parser (DRY; NEVER kill on parse failure). 3903func vw_status_kb(buf: *u8, n: i64, pat: *u8) -> i64 { 3904 let pl: i64 = vw_slen(pat) 3905 var i: i64 = 0 3906 while i + pl <= n { 3907 var k: i64 = 0 3908 var ok: i64 = 1 3909 while k < pl { if buf[i+k] != pat[k] { ok = 0; k = pl } k = k + 1 } 3910 if ok == 1 { 3911 var j: i64 = i + pl 3912 var go: i64 = 1 3913 while go == 1 { go = 0; if j < n { let c: i64 = buf[j] as i64; if c == 32 { j = j + 1; go = 1 } else { if c == 9 { j = j + 1; go = 1 } } } } 3914 let ep: *i64 = sys_mmap(VW_PTR_CELL) as *i64 3915 let r: i64 = vw_num_at(buf, n, j, ep) 3916 sys_munmap(ep as *u8, VW_PTR_CELL) // leak-free: ep was mmap'd-and-leaked per call (the ep-out-param leak class) 3917 return r 3918 } 3919 i = i + 1 3920 } 3921 return 0 - 1 3922} 3923 3924// THE kill decision. 1 only when: threshold sane (gb>=1) AND vsz known (kb>0) AND over threshold AND the 3925// per-row cooldown expired. Everything else -> 0 (fail-safe). 3926func vw_should_kill(vsz_kb: i64, max_gb: i64, last_kill_s: i64, now_s: i64, cooldown_s: i64) -> i64 { 3927 if max_gb < 1 { return 0 } 3928 if vsz_kb <= 0 { return 0 } 3929 if vsz_kb <= max_gb * 1048576 { return 0 } 3930 if now_s - last_kill_s < cooldown_s { return 0 } 3931 return 1 3932} 3933 3934// bounded whole-file read. -1 absent. 3935func vw_read(path: *u8, buf: *u8, cap: i64) -> i64 { 3936 let fd: i64 = sys_openat_rd(path) 3937 if fd < 0 { return 0 - 1 } 3938 var tot: i64 = 0 3939 var n: i64 = sys_read(fd, buf, cap) 3940 while n > 0 { tot = tot + n; if tot >= cap { n = 0 } else { n = sys_read(fd, (buf as i64 + tot) as *u8, cap - tot) } } 3941 sys_close(fd) 3942 return tot 3943} 3944 3945// monotonic seconds (persists across one-shot runs within a boot -- exactly the cooldown scope we want). 3946func vw_now_s() -> i64 { let ts: *i64 = sys_mmap(16) as *i64; sys_clock_gettime_mono(ts); return ts[0] } 3947 3948// self pid via /proc/self/stat leading digits (no getpid syscall-number risk). 3949func vw_selfpid() -> i64 { 3950 let b: *u8 = sys_mmap(VW_PROC_PATH_CAP) 3951 let n: i64 = vw_read("/proc/self/stat" as *u8, b, VW_PROC_PATH_CAP - 1) 3952 if n <= 0 { sys_munmap(b, VW_PROC_PATH_CAP); return 0 - 1 } 3953 let ep: *i64 = sys_mmap(VW_PTR_CELL) as *i64 3954 let r: i64 = vw_num_at(b, n, 0, ep) 3955 sys_munmap(b, VW_PROC_PATH_CAP); sys_munmap(ep, VW_PTR_CELL) // leak-free (b + ep were leaked per call) 3956 return r 3957} 3958 3959// back-compat: the VmSize row (a sibling gate + vw_status_kb_of call this by name) 3960func vw_vmsize_parse(buf: *u8, n: i64) -> i64 { return vw_status_kb(buf, n, "VmSize:" as *u8) } 3961// kB of an arbitrary "<label>:" status row for a /proc entry named by DIRECTORY STRING (pid or "self"). 3962// -1 unreadable. LEAK-FREE (munmaps path+b on every return). Generalized so VmSize (address space) and 3963// VmRSS (resident heap -- the leak class an arena hides from VSZ) share ONE reader. 3964func vw_status_kb_of(dirname: *u8, label: *u8) -> i64 { 3965 let path: *u8 = sys_mmap(VW_PROC_PATH_CAP) 3966 var o: i64 = 0 3967 let pre: *u8 = "/proc/" as *u8 3968 var a: i64 = 0 3969 while pre[a] != (0 as u8) { path[o] = pre[a]; o = o + 1; a = a + 1 } 3970 a = 0 3971 while dirname[a] != (0 as u8) { path[o] = dirname[a]; o = o + 1; a = a + 1 } 3972 let suf: *u8 = "/status" as *u8 3973 a = 0 3974 while suf[a] != (0 as u8) { path[o] = suf[a]; o = o + 1; a = a + 1 } 3975 path[o] = 0 as u8 3976 let b: *u8 = sys_mmap(VW_STATUS_BUF) 3977 let n: i64 = vw_read(path, b, VW_STATUS_BUF - 1) 3978 if n <= 0 { sys_munmap(path, VW_PROC_PATH_CAP); sys_munmap(b, VW_STATUS_BUF); return 0 - 1 } 3979 let r: i64 = vw_status_kb(b, n, label) 3980 sys_munmap(path, VW_PROC_PATH_CAP) 3981 sys_munmap(b, VW_STATUS_BUF) 3982 return r 3983} 3984// VmSize kB (back-compat; leak_check's memory meter). -1 unreadable. 3985func vw_vmsize_kb_of(dirname: *u8) -> i64 { return vw_status_kb_of(dirname, "VmSize:" as *u8) } 3986// VmRSS kB (resident set -- the heap-leak meter VmSize can hide in an arena). 3987func vw_rss_kb_of(dirname: *u8) -> i64 { return vw_status_kb_of(dirname, "VmRSS:" as *u8) } 3988 3989// nx_os_fs.nx -- OS FILESYSTEM-NAMESPACE SEAM (the write-safety half; sibling of nx_os_proc.nx). 3990// Answers ONE question for the IO layer: is this path in the OS's device/kernel/firmware namespace, 3991// where a file write could touch hardware or kernel state? Rule 26 (never-brick) demands the answer 3992// be BY CONSTRUCTION -- compiled in, not config-disableable -- so the deny lives here, in code, and 3993// callers cannot toggle it off with a conf line. 3994// 3995// LINUX BACKEND (current): the kernel exposes devices/firmware knobs as FILES under /dev, /sys, /proc 3996// (e.g. /sys/firmware/efi/efivars -- an errant write there can brick a board; /dev/sda -- raw disk). 3997// A path is write-forbidden iff it IS or is UNDER one of those roots. 3998// 3999// NISHIOS-NATIVE (target): NishiOS has no ambient device files -- device access is capability-routed 4000// through typed channels, so the ambient-namespace hazard class does not exist; the native backend 4001// returns forbid only for its reserved kernel-object namespace. This file is the SOURCE-SWAP seam 4002// (same contract, swapped backend), exactly like nx_os_proc.nx. license_tier: ORIGINAL 4003 4004 4005const OSF_SLASH: i64 = 47 // '/' -- path separator (namespace-boundary test) 4006 4007// is path EXACTLY root or UNDER root/ ? (blocks "/dev" and "/dev/null", not "/devdata") 4008func osf_under(path: *u8, root: *u8) -> i64 { 4009 var i: i64 = 0 4010 while root[i] != (0 as u8) { 4011 if path[i] != root[i] { return 0 } 4012 i = i + 1 4013 } 4014 if path[i] == (0 as u8) { return 1 } // exactly the root 4015 if path[i] == (OSF_SLASH as u8) { return 1 } // inside the root 4016 return 0 4017} 4018// WRITE-FORBIDDEN check: 1 = the OS device/kernel/firmware namespace, never writable through the IO layer. 4019func osf_write_forbidden(path: *u8) -> i64 { 4020 if osf_under(path, "/dev" as *u8) == 1 { return 1 } 4021 if osf_under(path, "/sys" as *u8) == 1 { return 1 } 4022 if osf_under(path, "/proc" as *u8) == 1 { return 1 } 4023 return 0 4024} 4025 4026// nx_os_proc.nx -- OS PROCESS-INTROSPECTION abstraction (the PORTABILITY SEAM). THE ONE place OS-specific 4027// process access lives, so every tool above it (nx_heal, ...) stays OS-AGNOSTIC + portable. Interop by 4028// construction: ONE source compiles to BOTH backends via the @ifdef target guard -- 4029// LINUX backend = procfs (/proc/<pid>/{stat,cmdline}, getdents on /proc) [current NAS deploy] 4030// NISHIOS backend = native process table (@ifdef TARGET_NISHI seam) -- superior: a direct kernel 4031// query, NO text-parsing of /proc, NO USER_HZ guesswork. FAILS LOUD until wired, 4032// so a NishiOS build never silently inherits Linux assumptions. 4033// LAW (portability): NEVER scatter raw /proc, /sys, /dev, or hardcoded syscall numbers through the LOGIC 4034// layer -- put OS-specifics behind an nx_os_* seam like this one. The sovereign core (seg_store, tool 4035// logic) already only touches nx_syscalls (ABI-abstracted); this extends the same discipline to OS features. 4036// license_tier: ORIGINAL 4037 4038 4039const OSP_HZ_LINUX: i64 = 100 // Linux USER_HZ: /proc/<pid>/stat starttime ticks/sec 4040const OSP_PATH_CAP: i64 = 256 4041const OSP_RD_CAP: i64 = 4096 4042const OSP_STAT_CAP: i64 = 262144 // /proc/stat whole-file read cap (btime scan) 4043const OSP_DENT_BUF: i64 = 65536 // getdents64 batch buffer (proven sizing) 4044const OSP_F_PPID: i64 = 2 // /proc/<pid>/stat field after ')': state=1 ppid=2 ... utime=12 stime=13 ... starttime=20 4045const OSP_F_START: i64 = 20 4046const OSP_F_UTIME: i64 = 12 // user-mode CPU ticks (cumulative) 4047const OSP_F_STIME: i64 = 13 // kernel-mode CPU ticks (cumulative) 4048const OSP_ASCII_0: i64 = 48 4049const OSP_ASCII_9: i64 = 57 4050const OSP_SP: i64 = 32 4051const OSP_NL: i64 = 10 4052const OSP_RP: i64 = 41 // ')' 4053const OSP_SLASH: i64 = 47 4054const OSP_NUL: i64 = 0 4055const OSP_SENTINEL: i64 = 0 - 1 // "not available on this OS backend" 4056 4057func osp_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (OSP_NUL as u8) { n = n + 1 } return n } 4058// pid integer -> decimal string in out; return len 4059func osp_itoa(v: i64, out: *u8) -> i64 { 4060 if v == 0 { out[0] = OSP_ASCII_0 as u8; out[1] = 0 as u8; return 1 } 4061 let t: *u8 = sys_mmap(24); var m: i64 = v; var k: i64 = 0 4062 while m > 0 { t[k] = (OSP_ASCII_0 + (m % 10)) as u8; m = m / 10; k = k + 1 } 4063 var i: i64 = 0 4064 while i < k { out[i] = t[k-1-i]; i = i + 1 } 4065 out[k] = 0 as u8 4066 return k 4067} 4068// parse leading integer at buf[off..]; endp[0] = position of the FIRST non-digit (NOT n) so a caller 4069// walking fields can resume there. BUG-FIX 2026-07-16: the old `i = n` break jumped to end-of-buffer, 4070// so osp_stat_field skipped every field after the first non-numeric one (the state char) and never 4071// reached starttime (field 20) -- diagnose then dropped every real process. 4072func osp_num(buf: *u8, n: i64, off: i64, endp: *i64) -> i64 { 4073 var v: i64 = 0; var i: i64 = off; var any: i64 = 0; var go: i64 = 1 4074 while go == 1 { 4075 go = 0 4076 if i < n { let c: i64 = buf[i] as i64; if c >= OSP_ASCII_0 { if c <= OSP_ASCII_9 { v = v*(10 as i64)+(c-OSP_ASCII_0); any = 1; i = i + 1; go = 1 } } } 4077 } 4078 endp[0] = i 4079 if any == 0 { return OSP_SENTINEL } 4080 return v 4081} 4082// basename (after last '/') of NUL-terminated s 4083func osp_basename(s: *u8) -> *u8 { 4084 var i: i64 = 0; var last: i64 = 0 4085 while s[i] != (OSP_NUL as u8) { if s[i] == (OSP_SLASH as u8) { last = i + 1 } i = i + 1 } 4086 return (s as i64 + last) as *u8 4087} 4088 4089// ============================ LINUX BACKEND (procfs) ============================ 4090// NOTE: the OS-target guard is currently a SOURCE-SWAP seam, not a compile-time @ifdef -- nx_cc's 4091// preprocessor is arch-guard-only today (TARGET_X86_64); a custom TARGET_NISHI compiled BOTH branches 4092// and the stub won (proven 2026-07-16). When nx_cc gains OS-target guards, wrap this in @ifdef 4093// TARGET_LINUX and the NishiOS backend (spec at bottom) in @ifdef TARGET_NISHI. The portability WIN 4094// stands regardless: every raw /proc lives HERE and nowhere else, so swapping the backend is a 4095// single-file change with the whole LOGIC layer (nx_heal) untouched. 4096// bounded read of a whole (small) proc/file into buf; returns len (0 on empty/absent) 4097func osp_bread(path: *u8, buf: *u8, cap: i64) -> i64 { 4098 let fd: i64 = sys_openat_rd(path) 4099 if fd < 0 { return 0 } 4100 var got: i64 = 0; var go: i64 = 1 4101 while go == 1 { let r: i64 = sys_read(fd, (buf as i64 + got) as *u8, cap - got); if r > 0 { got = got + r; if got >= cap { go = 0 } } else { go = 0 } } 4102 sys_close(fd) 4103 return got 4104} 4105// build "/proc/<pid><leaf>" (leaf NUL-terminated, e.g. "/stat") into out 4106func osp_ppath(out: *u8, pid: i64, leaf: *u8) -> i64 { 4107 var o: i64 = 0 4108 let pre: *u8 = "/proc/" as *u8 4109 var i: i64 = 0 4110 while pre[i] != (OSP_NUL as u8) { out[o] = pre[i]; o = o + 1; i = i + 1 } 4111 o = o + osp_itoa(pid, (out as i64 + o) as *u8) 4112 i = 0 4113 while leaf[i] != (OSP_NUL as u8) { out[o] = leaf[i]; o = o + 1; i = i + 1 } 4114 out[o] = 0 as u8 4115 return o 4116} 4117// enumerate live pids into pids[0..cap); returns count 4118func osp_list_pids(pids: *i64, cap: i64) -> i64 { 4119 let fd: i64 = sys_openat_rd("/proc" as *u8) 4120 if fd < 0 { return 0 } 4121 let dbuf: *u8 = sys_mmap(OSP_DENT_BUF) 4122 let ep: *i64 = sys_mmap(16) as *i64 4123 var cnt: i64 = 0; var run: i64 = 1 4124 while run == 1 { 4125 let n: i64 = sys_getdents64(fd, dbuf, OSP_DENT_BUF) 4126 if n <= 0 { run = 0 } else { 4127 var off: i64 = 0 4128 while off < n { 4129 let rec: *u8 = ((dbuf as i64 + off) as *u8) 4130 let reclen: i64 = dirent_reclen(rec) 4131 if reclen <= 0 { off = n } else { 4132 let name: *u8 = dirent_name(rec) 4133 if name[0] >= (OSP_ASCII_0 as u8) { if name[0] <= (OSP_ASCII_9 as u8) { 4134 if cnt < cap { let pid: i64 = osp_num(name, osp_slen(name), 0, ep); if pid > 0 { pids[cnt] = pid; cnt = cnt + 1 } } 4135 } } 4136 off = off + reclen 4137 } 4138 } 4139 } 4140 } 4141 sys_close(fd) 4142 return cnt 4143} 4144// count OPEN FILE DESCRIPTORS of pid = numeric entries in /proc/<pid>/fd. -1 if unreadable (gone/no perm). 4145// The fd METER for anomaly detection: a socket/file-descriptor leak trends up HERE while VmSize can stay 4146// flat (the fd table is not the address space) -- a leak shows in whatever resource disappears. Leak-free. 4147func osp_fd_count_from_fd_result(fd: i64, cause: *i64) -> i64 { 4148 if (cause as i64) != 0 { cause[0]=0 } 4149 let dbuf: *u8=sys_mmap(OSP_DENT_BUF) 4150 var count: i64=0 4151 var running: i64=1 4152 var failure: i64=0 4153 while running == 1 { 4154 let n: i64=sys_getdents64(fd,dbuf,OSP_DENT_BUF) 4155 if n < 0 { failure=n; running=0 } else { 4156 if n == 0 { running=0 } else { 4157 var off: i64=0 4158 while off < n { 4159 let rec: *u8=((dbuf as i64)+off) as *u8 4160 let reclen: i64=dirent_reclen(rec) 4161 if reclen <= 0 { failure=OSP_SENTINEL; running=0; break } 4162 let name: *u8=dirent_name(rec) 4163 if name[0] >= OSP_ASCII_0 as u8 && name[0] <= OSP_ASCII_9 as u8 { count=count+1 } 4164 off=off+reclen 4165 } 4166 } 4167 } 4168 } 4169 sys_munmap(dbuf,OSP_DENT_BUF) 4170 if failure < 0 { if (cause as i64) != 0 { cause[0]=failure }; return OSP_SENTINEL } 4171 return count 4172} 4173func osp_fd_count_from_fd(fd: i64) -> i64 { 4174 return osp_fd_count_from_fd_result(fd,0 as *i64) 4175} 4176func osp_fd_count(pid: i64) -> i64 { 4177 let path: *u8=sys_mmap(OSP_PATH_CAP) 4178 osp_ppath(path,pid,"/fd") 4179 let fd: i64=sys_openat_rd(path) 4180 sys_munmap(path,OSP_PATH_CAP) 4181 if fd < 0 { return OSP_SENTINEL } 4182 let count: i64=osp_fd_count_from_fd(fd) 4183 sys_close(fd) 4184 return count 4185} 4186 4187// field after the last ')' in /proc/<pid>/stat: OSP_F_PPID or OSP_F_START; SENTINEL on fail 4188func osp_stat_field(pid: i64, fidx: i64) -> i64 { 4189 let p: *u8 = sys_mmap(OSP_PATH_CAP) 4190 osp_ppath(p, pid, "/stat" as *u8) 4191 let b: *u8 = sys_mmap(OSP_RD_CAP) 4192 let n: i64 = osp_bread(p, b, OSP_RD_CAP - 1) 4193 if n <= 0 { return OSP_SENTINEL } 4194 var rp: i64 = 0 - 1; var i: i64 = 0 4195 while i < n { if b[i] == (OSP_RP as u8) { rp = i } i = i + 1 } 4196 if rp < 0 { return OSP_SENTINEL } 4197 var f: i64 = 0; i = rp + 1 4198 let ep: *i64 = sys_mmap(16) as *i64 4199 while i < n { 4200 if b[i] == (OSP_SP as u8) { i = i + 1 } else { 4201 f = f + 1 4202 let v: i64 = osp_num(b, n, i, ep) 4203 if f == fidx { return v } 4204 i = ep[0] 4205 var go: i64 = 1 4206 while go == 1 { go = 0; if i < n { if b[i] != (OSP_SP as u8) { i = i + 1; go = 1 } } } 4207 } 4208 } 4209 return OSP_SENTINEL 4210} 4211func osp_ppid(pid: i64) -> i64 { return osp_stat_field(pid, OSP_F_PPID) } 4212func osp_starttime_ticks(pid: i64) -> i64 { return osp_stat_field(pid, OSP_F_START) } 4213// cumulative CPU ticks consumed by pid = utime + stime. The CPU METER for anomaly detection: sampled over 4214// time, its RATE (Theil-Sen slope of the per-interval deltas) = the burn = "power disappearing" (a busy-loop 4215// pegs a core). SENTINEL if unreadable. HZ ticks/sec via osp_hz(). 4216func osp_cpu_ticks(pid: i64) -> i64 { 4217 let u: i64 = osp_stat_field(pid, OSP_F_UTIME) 4218 let s: i64 = osp_stat_field(pid, OSP_F_STIME) 4219 if u == OSP_SENTINEL { return OSP_SENTINEL } 4220 if s == OSP_SENTINEL { return OSP_SENTINEL } 4221 return u + s 4222} 4223// argv0 basename of /proc/<pid>/cmdline into out; return len (0 if none) 4224func osp_cmd_argv0(pid: i64, out: *u8, cap: i64) -> i64 { 4225 let p: *u8 = sys_mmap(OSP_PATH_CAP) 4226 osp_ppath(p, pid, "/cmdline" as *u8) 4227 let cl: *u8 = sys_mmap(cap + 1) 4228 let n: i64 = osp_bread(p, cl, cap) 4229 if n <= 0 { out[0] = 0 as u8; return 0 } 4230 cl[n] = 0 as u8 // argv0 = bytes up to the first NUL (already there) 4231 let bn: *u8 = osp_basename(cl) 4232 var o: i64 = 0 4233 while bn[o] != (OSP_NUL as u8) { out[o] = bn[o]; o = o + 1 } 4234 out[o] = 0 as u8 4235 return o 4236} 4237func osp_hz() -> i64 { return OSP_HZ_LINUX } 4238// where the HOST SUPERVISOR writes its log -- a deployment/OS question, so it lives in the seam. 4239// Linux/NAS deploy: /tmp/supervisor.log (nx_hostctl supervise). NishiOS: its native supervisor journal. 4240func osp_supervisor_log() -> *u8 { return "/tmp/supervisor.log" as *u8 } 4241func osp_uptime_s() -> i64 { let ts: *i64 = sys_mmap(16) as *i64; sys_clock_gettime_mono(ts); return ts[0] } 4242func osp_selfpid() -> i64 { 4243 let b: *u8 = sys_mmap(OSP_RD_CAP) 4244 let n: i64 = osp_bread("/proc/self/stat" as *u8, b, OSP_RD_CAP - 1) 4245 if n <= 0 { return OSP_SENTINEL } 4246 let ep: *i64 = sys_mmap(16) as *i64 4247 return osp_num(b, n, 0, ep) 4248} 4249// wallclock epoch = /proc/stat btime + monotonic-since-boot 4250func osp_boot_epoch() -> i64 { 4251 let b: *u8 = sys_mmap(OSP_STAT_CAP) 4252 let n: i64 = osp_bread("/proc/stat" as *u8, b, OSP_STAT_CAP - 1) 4253 if n <= 0 { return 0 } 4254 let needle: *u8 = "btime " as *u8 4255 var i: i64 = 0 4256 let ep: *i64 = sys_mmap(16) as *i64 4257 while i < n { 4258 var m: i64 = 1; var k: i64 = 0 4259 while needle[k] != (OSP_NUL as u8) { if i+k >= n { m = 0 } else { if b[i+k] != needle[k] { m = 0 } } k = k + 1 } 4260 if m == 1 { return osp_num(b, n, i + k, ep) } 4261 i = i + 1 4262 } 4263 return 0 4264} 4265 4266// ======================= NISHIOS BACKEND SPEC (the swap-in seam) ======================= 4267// When NishiOS's native process API lands, REPLACE the Linux backend above (or @ifdef-branch it once 4268// nx_cc has OS-target guards) with these ~8 functions over NishiOS's DIRECT kernel process table -- 4269// superior to procfs: no /proc text-parsing, no USER_HZ, a real syscall query. The interface the LOGIC 4270// layer depends on (and ALL it depends on) is exactly: 4271// osp_list_pids(pids,cap)->count nishi_proc_enumerate (live pids) 4272// osp_ppid(pid)->ppid nishi_proc_parent 4273// osp_starttime_ticks(pid)->ticks nishi_proc_starttime (osp_hz() ticks/sec) 4274// osp_cmd_argv0(pid,out,cap)->len nishi_proc_argv0 (basename of argv0) 4275// osp_hz()->ticks_per_sec native rate (not the Linux-100 assumption) 4276// osp_uptime_s()->secs sys_clock_gettime_mono (already OS-neutral) 4277// osp_selfpid()->pid nishi_getpid 4278// osp_boot_epoch()->epoch nishi_boot_epoch 4279// Interop: NishiOS ships the superior backend; Linux stays supported for the current NAS deploy; the 4280// LOGIC (nx_heal) compiles unchanged on both. THAT is the point of this file. 4281 4282// Direct-child enumeration does not depend on CONFIG_CHECKPOINT_RESTORE's 4283// optional /proc/<pid>/task/<pid>/children file. The caller owns the output 4284// capacity; overflow is an error, never a plausible partial child set. 4285const OSP_DIRENT_NAME_OFFSET: i64 = 19 // Linux linux_dirent64 ABI 4286func osp_stat_parent(buf: *u8, n: i64) -> i64 { 4287 var last: i64=0-1;var i: i64=0 4288 while i < n { if buf[i] == OSP_RP as u8 { last=i };i=i+1 } 4289 if last < 0 { return 0-5 } 4290 i=last+1 4291 while i < n && buf[i] == OSP_SP as u8 { i=i+1 } 4292 while i < n && buf[i] != OSP_SP as u8 { i=i+1 } 4293 while i < n && buf[i] == OSP_SP as u8 { i=i+1 } 4294 let first: i64=i;var parent: i64=0 4295 while i < n && buf[i] >= OSP_ASCII_0 as u8 && buf[i] <= OSP_ASCII_9 as u8 { 4296 parent=parent*10+(buf[i] as i64)-OSP_ASCII_0;i=i+1 4297 } 4298 if i == first || i == n || buf[i] != OSP_SP as u8 { return 0-5 } 4299 return parent 4300} 4301func osp_children(parent: i64, children: *i64, capacity: i64) -> i64 { 4302 if parent <= 0 || capacity < 0 { return 0-22 } 4303 let directory: i64=sys_openat_directory("/proc") 4304 if directory < 0 { return directory } 4305 let batch: *u8=sys_mmap(OSP_DENT_BUF) 4306 let path: *u8=sys_mmap(OSP_PATH_CAP) 4307 let stat: *u8=sys_mmap(OSP_RD_CAP) 4308 var count: i64=0;var failure: i64=0;var running: i64=1 4309 while running == 1 { 4310 let n: i64=sys_getdents64(directory,batch,OSP_DENT_BUF) 4311 if n == (0-4) { continue } 4312 if n <= 0 { failure=n;break } 4313 var off: i64=0 4314 while off < n { 4315 if n-off <= OSP_DIRENT_NAME_OFFSET { failure=0-5;running=0;break } 4316 let rec: *u8=batch+off 4317 let size: i64=dirent_reclen(rec) 4318 if size <= OSP_DIRENT_NAME_OFFSET || size > n-off { failure=0-5;running=0;break } 4319 var i: i64=OSP_DIRENT_NAME_OFFSET;var pid: i64=0 4320 while i < size && rec[i] >= OSP_ASCII_0 as u8 && rec[i] <= OSP_ASCII_9 as u8 { 4321 pid=pid*10+(rec[i] as i64)-OSP_ASCII_0;i=i+1 4322 } 4323 if pid > 0 && i < size && rec[i] == OSP_NUL as u8 { 4324 osp_ppath(path,pid,"/stat") 4325 let fd: i64=sys_openat_rd(path) 4326 // A process may disappear during enumeration; other failures 4327 // make completeness unproven and must propagate. 4328 if fd < 0 && fd != (0-2) { failure=fd;running=0;break } 4329 if fd >= 0 { 4330 var used: i64=0;var readrc: i64=1 4331 while readrc > 0 && used < OSP_RD_CAP { 4332 readrc=sys_read(fd,stat+used,OSP_RD_CAP-used) 4333 if readrc == (0-4) { readrc=1;continue } 4334 if readrc > 0 { used=used+readrc } 4335 } 4336 let closed: i64=sys_close(fd) 4337 if readrc < 0 && readrc != (0-3) { failure=readrc;running=0;break } 4338 if closed < 0 { failure=closed;running=0;break } 4339 if used == OSP_RD_CAP { failure=0-75;running=0;break } 4340 if used > 0 { 4341 let observed: i64=osp_stat_parent(stat,used) 4342 if observed < 0 { failure=observed;running=0;break } 4343 if observed == parent { 4344 if count >= capacity { failure=0-28;running=0;break } 4345 children[count]=pid;count=count+1 4346 } 4347 } 4348 } 4349 } 4350 off=off+size 4351 } 4352 } 4353 let closed: i64=sys_close(directory) 4354 sys_munmap(batch,OSP_DENT_BUF);sys_munmap(path,OSP_PATH_CAP);sys_munmap(stat,OSP_RD_CAP) 4355 if failure < 0 { return failure } 4356 if closed < 0 { return closed } 4357 return count 4358} 4359 4360const FSX_MAGIC_4095: i64 = 4095 4361 4362const FSX_READ_CAP: i64 = 1048576 // max bytes returned by `read` (truncation is MARKED, never silent) 4363const FSX_DENY_CAP: i64 = 8192 // fs_read_deny.conf read cap 4364const FSX_PATH_CAP: i64 = 1024 // lowercased path work buffer 4365const FSX_DENT_BUF: i64 = 65536 // getdents64 batch buffer (matches the proven vsz/heal sizing) 4366const FSX_LS_CAP: i64 = 200 // scale-law: max ls entries EMITTED; true total ALWAYS declared (65KB-dump fix) 4367const FSX_RC_ABSENT: i64 = 3 // exit: path absent/unreadable (mirrors nx_fileop's exists convention) 4368const FSX_RC_DENIED: i64 = 5 // exit: deny-list refused the read 4369const FSX_UPPER_A: i64 = 65 // 'A' (ASCII lowercasing) 4370const FSX_UPPER_Z: i64 = 90 // 'Z' 4371const FSX_CASE_OFF: i64 = 32 // 'a' - 'A' 4372const FSX_ASCII_0: i64 = 48 // '0' (decimal print) 4373 4374func fsx_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 4375// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer 4376// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the 4377// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls). 4378// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign. 4379func fsx_putn(v: i64) -> i64 { nxi_out(v); return 0 } 4380// lowercase copy of s into out (bounded), returns length 4381func fsx_lower(s: *u8, out: *u8, cap: i64) -> i64 { 4382 var i: i64 = 0 4383 while s[i] != (0 as u8) { 4384 if i >= cap - 1 { out[i] = 0 as u8; return i } 4385 var c: i64 = s[i] as i64 4386 if c >= FSX_UPPER_A { if c <= FSX_UPPER_Z { c = c + FSX_CASE_OFF } } 4387 out[i] = c as u8 4388 i = i + 1 4389 } 4390 out[i] = 0 as u8 4391 return i 4392} 4393// exact NUL-terminated string equality 4394func fsx_seq(a: *u8, b: *u8) -> i64 { var i: i64 = 0; while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 } if b[i] != (0 as u8) { return 0 } return 1 } 4395// is `needle` (NUL-terminated, lowercase) contained in lowercase path lp[0..ln)? 4396// ---------- compare-and-swap decision (seq1422/seq1456) ---------- 4397// 4398// PURE, and in the LIB on purpose: the decision used to live inside the CLI's 4399// main(), where a gate cannot reach it -- which is exactly how it shipped 4400// refusing every correct expectation (seq1422). A rule nothing can drive is a 4401// rule nothing can prove. 4402// 4403// tok is the raw argv token (`expect=<n>` / `expect=any` / a bare number); 4404// cur is the file's real size. Returns 1 = ALLOW, 0 = REFUSE. 4405func fsx_cas_val(tok: *u8) -> *u8 { 4406 var i: i64 = 0 4407 while tok[i] != (0 as u8) { 4408 if tok[i] == (61 as u8) { return ((tok as i64) + i + 1) as *u8 } 4409 i = i + 1 4410 } 4411 return tok 4412} 4413func fsx_cas_ok(cur: i64, tok: *u8) -> i64 { 4414 let v: *u8 = fsx_cas_val(tok) 4415 if fsx_seq(v, "any" as *u8) == 1 { return 1 } 4416 var n: i64 = 0 4417 var i: i64 = 0 4418 var got: i64 = 0 4419 while v[i] != (0 as u8) { 4420 let c: i64 = v[i] as i64 4421 if c >= 48 { if c <= 57 { n = n * 10 + (c - 48); got = 1 } } 4422 i = i + 1 4423 } 4424 if got == 0 { return 0 } 4425 if n == cur { return 1 } 4426 return 0 4427} 4428 4429func fsx_deny_hit(lp: *u8, ln: i64, needle: *u8) -> i64 { 4430 let nl: i64 = vw_slen(needle) 4431 if nl == 0 { return 0 } 4432 return vw_contains(lp, ln, needle, nl) 4433} 4434// data-driven deny extras: one lowercase needle per line in `conf`; 1 = some line matches the path. 4435// Factored out so the read deny (fs_read_deny.conf) and write deny (fs_write_deny.conf) share ONE scanner. 4436func fsx_conf_deny(lp: *u8, ln: i64, conf: *u8) -> i64 { 4437 let cb: *u8 = sys_mmap(FSX_DENY_CAP) 4438 let cn: i64 = vw_read(conf, cb, FSX_DENY_CAP - 1) 4439 if cn > 0 { 4440 var ls: i64 = 0 4441 var i: i64 = 0 4442 while i <= cn { 4443 var eol: i64 = 0 4444 if i == cn { eol = 1 } else { if cb[i] == (10 as u8) { eol = 1 } } 4445 if eol == 1 { 4446 if i > ls { 4447 cb[i] = 0 as u8 // terminate the line in place 4448 if fsx_deny_hit(lp, ln, (cb as i64 + ls) as *u8) == 1 { return 1 } 4449 } 4450 ls = i + 1 4451 } 4452 i = i + 1 4453 } 4454 } 4455 return 0 4456} 4457const FSX_SNIFF_CAP: i64 = 4096 4458 4459func fsx_isalnum(c: i64) -> i64 { 4460 if c >= 48 { if c <= 57 { return 1 } } 4461 if c >= 97 { if c <= 122 { return 1 } } 4462 if c >= 65 { if c <= 90 { return 1 } } 4463 return 0 4464} 4465 4466func fsx_ends_with(lp: *u8, ln: i64, suf: *u8) -> i64 { 4467 let sl: i64 = vw_slen(suf) 4468 if sl == 0 { return 0 } 4469 if sl > ln { return 0 } 4470 var i: i64 = 0 4471 while i < sl { 4472 if lp[ln - sl + i] != suf[i] { return 0 } 4473 i = i + 1 4474 } 4475 return 1 4476} 4477 4478func fsx_basename_is(lp: *u8, ln: i64, name: *u8) -> i64 { 4479 let nl: i64 = vw_slen(name) 4480 if nl == 0 { return 0 } 4481 if nl > ln { return 0 } 4482 if fsx_ends_with(lp, ln, name) == 0 { return 0 } 4483 if nl == ln { return 1 } 4484 let c: i64 = lp[ln - nl - 1] as i64 4485 if c == 47 { return 1 } 4486 if c == 92 { return 1 } 4487 return 0 4488} 4489 4490// Whole-word containment: bounded by non-alphanumeric on BOTH sides, so `api_secret.txt` is denied and 4491// `secretary_notes.md` is not. 4492func fsx_word_has(lp: *u8, ln: i64, w: *u8) -> i64 { 4493 let wl: i64 = vw_slen(w) 4494 if wl == 0 { return 0 } 4495 if wl > ln { return 0 } 4496 var i: i64 = 0 4497 while i + wl <= ln { 4498 var eq: i64 = 1 4499 var k: i64 = 0 4500 while k < wl { if lp[i + k] != w[k] { eq = 0; k = wl } else { k = k + 1 } } 4501 if eq == 1 { 4502 var lb: i64 = 1 4503 if i > 0 { if fsx_isalnum(lp[i - 1] as i64) == 1 { lb = 0 } } 4504 var rb: i64 = 1 4505 if i + wl < ln { if fsx_isalnum(lp[i + wl] as i64) == 1 { rb = 0 } } 4506 if lb == 1 { if rb == 1 { return 1 } } 4507 } 4508 i = i + 1 4509 } 4510 return 0 4511} 4512 4513// CONTENT LEG: sniff the leading bytes for what a secret actually IS. This is the half a name-only list 4514// can never do -- it denies a private key no matter what it is called, including `notes.txt`. 4515// A CERTIFICATE is deliberately NOT denied: certs are public by definition, and denying them is the same 4516// category error as denying the tokenizer. 4517func fsx_content_secret(path: *u8) -> i64 { 4518 let fd: i64 = sys_openat_rd(path) 4519 if fd < 0 { return 0 } 4520 let b: *u8 = sys_mmap(FSX_SNIFF_CAP) 4521 let n: i64 = sys_read(fd, b, FSX_SNIFF_CAP - 1) 4522 sys_close(fd) 4523 if n <= 0 { return 0 } 4524 if vw_contains(b, n, "PRIVATE KEY-----" as *u8, 16) == 1 { return 1 } 4525 if vw_contains(b, n, "OPENSSH PRIVATE KEY" as *u8, 19) == 1 { return 1 } 4526 if vw_contains(b, n, "PGP PRIVATE KEY BLOCK" as *u8, 21) == 1 { return 1 } 4527 if vw_contains(b, n, "PuTTY-User-Key-File" as *u8, 19) == 1 { return 1 } 4528 return 0 4529} 4530 4531// DENY check: 1 = refuse this path. SOTA-2026 REWRITE (2026-07-31). 4532// 4533// THE OLD RULE WAS WRONG IN BOTH DIRECTIONS, measured on real paths: 4534// OVER-BLOCKED substring "token" denied runtime/nx_tokenizer.nx -- the compiler's own tokenizer, which 4535// contains no secret -- and blocked BOTH nx_fs read AND nx_fs_write on it, while 4536// nx_shelltool grep returned the same bytes freely. It cost real work and bought nothing. 4537// substring "key" likewise denies monkey / keyword / keyboard. 4538// UNDER-BLOCKED `id_rsa`, the canonical SSH private key filename, contains NONE of 4539// secret/key/token/passw/.pem and sailed straight through. 4540// A denylist that blocks source and passes private keys is not a security control -- it is a rename away 4541// from useless in one direction and a permanent nuisance in the other. 4542// 4543// REPLACEMENT -- two INDEPENDENT legs, either one denies: 4544// (1) PATH leg: real secret-bearing EXTENSIONS and exact BASENAMES, matched at a true suffix/segment 4545// boundary, plus whole-word `secret`/`password`. No substring-anywhere matching survives. 4546// (2) CONTENT leg: PEM/OpenSSH/PGP/PuTTY private-key armour, which catches a secret regardless of name. 4547// Net effect: strictly MORE secrets denied (id_rsa, a renamed key, a key with no extension) and strictly 4548// FEWER ordinary sources blocked. 4549func fsx_denied(path: *u8) -> i64 { 4550 let lp: *u8 = sys_mmap(FSX_PATH_CAP) 4551 let ln: i64 = fsx_lower(path, lp, FSX_PATH_CAP) 4552 4553 if fsx_ends_with(lp, ln, ".pem" as *u8) == 1 { return 1 } 4554 if fsx_ends_with(lp, ln, ".key" as *u8) == 1 { return 1 } 4555 if fsx_ends_with(lp, ln, ".cap" as *u8) == 1 { return 1 } 4556 if fsx_ends_with(lp, ln, ".p12" as *u8) == 1 { return 1 } 4557 if fsx_ends_with(lp, ln, ".pfx" as *u8) == 1 { return 1 } 4558 if fsx_ends_with(lp, ln, ".jks" as *u8) == 1 { return 1 } 4559 if fsx_ends_with(lp, ln, ".ppk" as *u8) == 1 { return 1 } 4560 if fsx_ends_with(lp, ln, "_rsa" as *u8) == 1 { return 1 } 4561 if fsx_ends_with(lp, ln, "_dsa" as *u8) == 1 { return 1 } 4562 if fsx_ends_with(lp, ln, "_ecdsa" as *u8) == 1 { return 1 } 4563 if fsx_ends_with(lp, ln, "_ed25519" as *u8) == 1 { return 1 } 4564 4565 if fsx_basename_is(lp, ln, ".env" as *u8) == 1 { return 1 } 4566 if fsx_basename_is(lp, ln, "credentials" as *u8) == 1 { return 1 } 4567 if fsx_basename_is(lp, ln, "shadow" as *u8) == 1 { return 1 } 4568 if fsx_basename_is(lp, ln, "opaque_keys.bin" as *u8) == 1 { return 1 } 4569 4570 // CALIBRATED BY WORD FREQUENCY, not by one uniform rule -- the gate proved a uniform rule wrong in 4571 // BOTH directions within minutes. `secret` and `passw` are high-signal and essentially absent from 4572 // ordinary source, so SUBSTRING matching is correct for them and catches mysecret_key.bin. `key` and 4573 // `token` are common English fragments (tokenizer, monkey, keyword, keyboard) and must NEVER be 4574 // substring-matched -- that is what denied the compiler's own tokenizer. They are covered instead by 4575 // the extension/suffix rules above and by the content leg below. 4576 if fsx_deny_hit(lp, ln, "secret" as *u8) == 1 { return 1 } 4577 if fsx_deny_hit(lp, ln, "passw" as *u8) == 1 { return 1 } 4578 if fsx_deny_hit(lp, ln, "credential" as *u8) == 1 { return 1 } 4579 4580 if fsx_content_secret(path) == 1 { return 1 } 4581 4582 return fsx_conf_deny(lp, ln, "fs_read_deny.conf" as *u8) 4583} 4584// read: emit up to `cap` bytes of path to stdout. Returns bytes emitted; -1 absent; -2 DENIED. 4585// deniedp/absent are ALSO visible in the CLI exit code. Truncation is marked with a trailing banner. 4586// Failure reporter that KEEPS THE ERRNO. sys_openat_rd returns -errno, and the old message printed 4587// "ABSENT" for every negative -- so EACCES (-13, EXISTS but unopenable) read as "missing", which are 4588// OPPOSITE remedies. Cost a real hour on 2026-08-01: knowledge/foundation existed with mode 0100 and 4589// every instrument in the stack called it absent (the mkdirp read-back that printed the errno cracked 4590// the case in one call). rc>=0 means a probe re-open SUCCEEDED: the earlier read failed for a 4591// non-open reason (an empty file), so say THAT. Always returns -1 (callers' contract unchanged; 4592// the -2 DENIED sentinel stays distinct). 4593func fsx_fail(path: *u8, rc: i64) -> i64 { 4594 if rc >= 0 { sys_close(rc); fsx_puts("NX-FS EMPTY: 0 bytes: " as *u8); fsx_puts(path); fsx_puts("\n" as *u8); return 0 - 1 } 4595 if rc == 0 - 13 { 4596 fsx_puts("NX-FS PERMISSION (EACCES): exists but this process may not open it: " as *u8) 4597 fsx_puts(path); fsx_puts("\n" as *u8) 4598 return 0 - 1 4599 } 4600 if rc == 0 - 2 { fsx_puts("NX-FS ABSENT: " as *u8); fsx_puts(path); fsx_puts("\n" as *u8); return 0 - 1 } 4601 fsx_puts("NX-FS ERROR rc=" as *u8); fsx_putn(rc) 4602 fsx_puts(": " as *u8); fsx_puts(path); fsx_puts("\n" as *u8) 4603 return 0 - 1 4604} 4605 4606const FSX_SEEK_END: i64 = 2 // lseek whence: EOF offset = size, WITHOUT reading a single byte 4607 4608// TRUE SIZE -- the one thing no other read verb in this lib can give you (2026-08-07, debt 1786054029). 4609// read/lines/outline all report BYTES THEY READ against FSX_READ_CAP/FSX_LINES_SCAN, and they DO honestly 4610// declare the cap -- but an honest floor is still not a measurement: "bytes=1048576 (covers first 1048576 4611// bytes only)" is the IDENTICAL answer for a 1.05MB file and a 30MB one. 4612// MEASURED COST OF NOT HAVING IT: bounding ONE 1.38MB journal took TWELVE probe reads at hand-chosen 4613// offsets, because the only way to learn a big file size was to binary-search EOF by hand. 4614// lseek(SEEK_END) reads ZERO bytes, so the answer is exact at ANY size for one syscall. 4615// Deny-list still applies: consistency with every other verb beats a special case for a metadata read. 4616// CONTRACT DIFFERS FROM fsx_read ON PURPOSE: an EMPTY file returns 0, never -1. Size is the one caller for 4617// which "absent" and "zero bytes" are DIFFERENT FACTS, so fsx_fail -- which folds both to -1 -- is not used 4618// here. (Same distinction lt_read_tail needed: -1 ABSENT vs 0 EMPTY. A reader that conflates them cannot 4619// tell a lane that never wrote from a lane whose file vanished.) 4620// A DECLARED FLOOR IS HONEST BUT IT IS NOT A MEASUREMENT -- IF THE NUMBER IS CHEAP, EMIT THE NUMBER. 4621func fsx_size(path: *u8) -> i64 { 4622 if fsx_denied(path) == 1 { 4623 fsx_puts("NX-FS-SIZE DENIED: path matches the secret deny-list. WHY: this tool never returns key material.\n" as *u8) 4624 return 0 - (2 as i64) 4625 } 4626 let fd: i64 = sys_openat_rd(path) 4627 if fd < 0 { 4628 fsx_puts("NX-FS-SIZE ABSENT: cannot open " as *u8); fsx_puts(path) 4629 fsx_puts(" . FIX: confirm the path with `nx_fs ls <dir>`.\n" as *u8) 4630 return 0 - 1 4631 } 4632 let sz: i64 = sys_lseek(fd, 0, FSX_SEEK_END) 4633 sys_close(fd) 4634 if sz < 0 { 4635 fsx_puts("NX-FS-SIZE UNSEEKABLE: " as *u8); fsx_puts(path) 4636 fsx_puts(" (a pipe/char device has no size; this is NOT a zero-byte file)\n" as *u8) 4637 return 0 - 1 4638 } 4639 fsx_puts("NX-FS-SIZE " as *u8); fsx_puts(path) 4640 fsx_puts(" bytes=" as *u8); fsx_putn(sz) 4641 fsx_puts(" exact=1 read_bytes=0\n" as *u8) 4642 return sz 4643} 4644 4645func fsx_read(path: *u8, cap: i64) -> i64 { 4646 if fsx_denied(path) == 1 { 4647 fsx_puts("NX-FS DENIED: path matches the secret deny-list (defaults + fs_read_deny.conf)\n" as *u8) 4648 return 0 - (2 as i64) // DENIED sentinel (distinct from -1 absent) 4649 } 4650 var want: i64 = cap 4651 if want <= 0 { want = FSX_READ_CAP } 4652 if want > FSX_READ_CAP { want = FSX_READ_CAP } 4653 let buf: *u8 = sys_mmap(want + 1) 4654 let n: i64 = vw_read(path, buf, want) 4655 // vw_read flattens the errno (-1 for every failure); re-probe the open ONLY on the failure path 4656 // so the message can distinguish absent / permission / empty. Zero cost on success. 4657 if n <= 0 { return fsx_fail(path, sys_openat_rd(path)) } 4658 sys_write(1, buf, n) 4659 if n == want { 4660 fsx_puts("\n[NX-FS TRUNCATED at " as *u8); fsx_putn(n); fsx_puts(" bytes]\n" as *u8) 4661 } 4662 return n 4663} 4664// WINDOWED read (eats debt seq222: the tools-call transport caps ~64KB, so files past the cap were 4665// unreadable over MCP): emit up to `cap` bytes starting at byte `off`. Same deny-list as fsx_read. 4666// A separate function (NOT an fsx_read arity change) so every existing caller keeps its exact contract. 4667func fsx_read_at(path: *u8, cap: i64, off: i64) -> i64 { 4668 if fsx_denied(path) == 1 { 4669 fsx_puts("NX-FS DENIED: path matches the secret deny-list (defaults + fs_read_deny.conf)\n" as *u8) 4670 return 0 - (2 as i64) 4671 } 4672 var want: i64 = cap 4673 if want <= 0 { want = FSX_READ_CAP } 4674 if want > FSX_READ_CAP { want = FSX_READ_CAP } 4675 let fd: i64 = sys_openat_rd(path) 4676 if fd < 0 { return fsx_fail(path, fd) } 4677 if off > 0 { if sys_lseek(fd, off, 0) < 0 { sys_close(fd); fsx_puts("NX-FS ABSENT: seek failed " as *u8); fsx_puts(path); fsx_puts("\n" as *u8); return 0 - 1 } } 4678 let buf: *u8 = sys_mmap(want + 1) 4679 var got: i64 = 0 4680 var sc: i64 = 1 4681 while sc == 1 { 4682 let r: i64 = sys_read(fd, ((buf as i64 + got) as *u8), want - got) 4683 if r <= 0 { sc = 0 } else { got = got + r; if got >= want { sc = 0 } } 4684 } 4685 sys_close(fd) 4686 if got <= 0 { fsx_puts("NX-FS EOF: no bytes at offset " as *u8); fsx_putn(off); fsx_puts(" in " as *u8); fsx_puts(path); fsx_puts("\n" as *u8); return 0 - 1 } 4687 sys_write(1, buf, got) 4688 if got == want { 4689 fsx_puts("\n[NX-FS WINDOW off=" as *u8); fsx_putn(off); fsx_puts(" n=" as *u8); fsx_putn(got); fsx_puts(" -- more remains]\n" as *u8) 4690 } 4691 return got 4692} 4693const FSX_LINES_SCAN: i64 = 1048576 // line-addressing scan window (matches the proven read cap) 4694const FSX_LINES_MAXOUT: i64 = 262144 // max bytes emitted by one `lines` call (transport-friendly) 4695const FSX_LINES_DEFN: i64 = 40 // default line count when the caller omits it 4696const FSX_LINES_MAXN: i64 = 400 // max lines per call 4697 4698// LINE-ADDRESSED read -- THE MISSING PRIMITIVE (measured 2026-07-20): `grep` reports file:LINE but `read` 4699// takes BYTES, so the two did NOT compose -- locating one function in a remote file meant hand 4700// binary-searching byte offsets (cost one subagent 70K tokens + 22 calls for a single extraction). 4701// Emits lines [start, start+count) 1-based, then a DECLARED envelope banner (scale-law: a caller can 4702// NEVER be silently windowed -- scanned bytes, scan cap, over-window and clip flags are all stated). 4703// Same deny-list as fsx_read. Returns bytes emitted; -1 absent; -2 DENIED. 4704func fsx_read_lines(path: *u8, start: i64, count: i64) -> i64 { 4705 if fsx_denied(path) == 1 { 4706 fsx_puts("NX-FS DENIED: path matches the secret deny-list (defaults + fs_read_deny.conf)\n" as *u8) 4707 return 0 - (2 as i64) 4708 } 4709 var s: i64 = start 4710 if s < 1 { s = 1 } 4711 var c: i64 = count 4712 if c <= 0 { c = FSX_LINES_DEFN } 4713 if c > FSX_LINES_MAXN { c = FSX_LINES_MAXN } 4714 let buf: *u8 = sys_mmap(FSX_LINES_SCAN + 1) 4715 let n: i64 = vw_read(path, buf, FSX_LINES_SCAN) 4716 if n <= 0 { return fsx_fail(path, sys_openat_rd(path)) } 4717 // walk to the first byte of line `s`; cur > s afterwards means we ran off the end (fail-loud, not empty) 4718 var i: i64 = 0 4719 var cur: i64 = 1 4720 while cur < s { 4721 if i >= n { cur = s + 1 } else { 4722 if buf[i] == (10 as u8) { cur = cur + 1 } 4723 i = i + 1 4724 } 4725 } 4726 if cur > s { 4727 fsx_puts("NX-FS LINES: start line " as *u8); fsx_putn(s) 4728 fsx_puts(" is beyond EOF (scanned " as *u8); fsx_putn(n); fsx_puts(" bytes)\n" as *u8) 4729 return 0 4730 } 4731 let from: i64 = i 4732 var lines_out: i64 = 0 4733 var j: i64 = i 4734 var go: i64 = 1 4735 while go == 1 { 4736 if j >= n { go = 0 } else { 4737 if buf[j] == (10 as u8) { 4738 lines_out = lines_out + 1 4739 j = j + 1 4740 if lines_out >= c { go = 0 } 4741 } else { j = j + 1 } 4742 } 4743 } 4744 var outn: i64 = j - from 4745 var clipped: i64 = 0 4746 if outn > FSX_LINES_MAXOUT { outn = FSX_LINES_MAXOUT; clipped = 1 } 4747 if outn > 0 { sys_write(1, ((buf as i64 + from) as *u8), outn) } 4748 fsx_puts("\n[NX-FS LINES start=" as *u8); fsx_putn(s) 4749 fsx_puts(" lines=" as *u8); fsx_putn(lines_out) 4750 fsx_puts(" next=" as *u8); fsx_putn(s + lines_out) 4751 fsx_puts(" bytes=" as *u8); fsx_putn(outn) 4752 fsx_puts(" scanned=" as *u8); fsx_putn(n) 4753 fsx_puts(" scan_cap=" as *u8); fsx_putn(FSX_LINES_SCAN) 4754 if n >= FSX_LINES_SCAN { fsx_puts(" FILE-EXCEEDS-SCAN-WINDOW" as *u8) } 4755 if clipped == 1 { fsx_puts(" BYTE-CLIPPED" as *u8) } 4756 fsx_puts("]\n" as *u8) 4757 return outn 4758} 4759// ==== WRITE/EDIT half (cap class: write; tools-api name nx_fs_write) ========================= 4760// ★ONE DEFINITION, TWO NAMES: this const KEEPS its name so no caller changes, but its VALUE now comes 4761// from the shim's MODE_0644 instead of a second literal. This line already called itself "the ecosystem's 4762// file-create mode idiom" -- and it was right, which is why adding MODE_0644 to nx_syscalls without 4763// finding it created a 64th copy rather than a single ruler. 4764// ★★★SEARCHING BY NAME FINDS ONLY WHAT SHARES YOUR NAMING CONVENTION. TO FIND A DUPLICATE CONSTANT YOU 4765// MUST SEARCH BY VALUE: a grep for `_MODE_0644` returned 10, a grep for `= 0x1a4` returned 66. 4766const FSX_MODE_RW: i64 = MODE_0644 // 0644 -- the ecosystem's file-create mode idiom 4767const FSX_DEC: i64 = 10 // decimal base (pid rendering in the tmp suffix) 4768const FSX_EDIT_OUT: i64 = 2097152 // edit output buffer (2x read cap: bounded replacement growth) 4769const FSX_TMP_ROOM: i64 = 32 // reserved room for ".nxw" + pid digits + NUL in the tmp name 4770const FSX_RC_IO: i64 = 4 // exit: io failure (open/short-write/rename) 4771const FSX_RC_NOMATCH: i64 = 6 // exit: edit found 0 occurrences (file UNCHANGED) 4772const FSX_RC_AMBIG: i64 = 7 // exit: edit found >1 occurrences without `all` (file UNCHANGED) 4773 4774// write-DENY: read deny (never clobber key material) + OS device/firmware namespace (rule 26, seam, 4775// BY CONSTRUCTION) + registry-escalation needle + fs_write_deny.conf extras (data-driven). 4776// TAIL -- the "WHERE DOES THIS FILE END" primitive, answered from the file's own end in ONE call. 4777// The documented recipe was `size`, then `read <path> <n> <size-n>`: two calls and an offset the caller 4778// carries by hand. What actually happened (measured 2026-09-03): a caller chose a `lines` start from an 4779// EARLIER run's size, read a window that landed mid-file, and published the window's last line as the 4780// file's last line -- while the envelope on that very read said next=212. Two false mechanisms and a 4781// false scope claim followed. ★A WINDOW READ IS NOT A TAIL READ. This verb cannot be pointed at the 4782// middle: it seeks to the end, walks BACKWARD for the last `count` line starts, and declares its window. 4783// Same deny-list as every read verb. Returns bytes emitted; 0 for an empty file (banner, never silence); 4784// -1 absent/unseekable; -2 DENIED. 4785func fsx_tail(path: *u8, count: i64) -> i64 { 4786 if fsx_denied(path) == 1 { 4787 fsx_puts("NX-FS DENIED: path matches the secret deny-list (defaults + fs_read_deny.conf)\n" as *u8) 4788 return 0 - (2 as i64) 4789 } 4790 var c: i64 = count 4791 if c <= 0 { c = FSX_LINES_DEFN } 4792 if c > FSX_LINES_MAXN { c = FSX_LINES_MAXN } 4793 let fd: i64 = sys_openat_rd(path) 4794 if fd < 0 { return fsx_fail(path, fd) } 4795 let sz: i64 = sys_lseek(fd, 0, FSX_SEEK_END) 4796 if sz < 0 { 4797 sys_close(fd) 4798 fsx_puts("NX-FS TAIL UNSEEKABLE: " as *u8); fsx_puts(path) 4799 fsx_puts(" (a pipe/char device has no end to seek to)\n" as *u8) 4800 return 0 - 1 4801 } 4802 if sz == 0 { 4803 sys_close(fd) 4804 fsx_puts("[NX-FS TAIL lines=0 total_bytes=0 window_off=0 scanned=0 EMPTY-FILE]\n" as *u8) 4805 return 0 4806 } 4807 // read the LAST scan-window of the file, never the first: a log past the window still yields its end 4808 var off: i64 = 0 4809 if sz > FSX_LINES_SCAN { off = sz - FSX_LINES_SCAN } 4810 if sys_lseek(fd, off, 0) < 0 { 4811 sys_close(fd) 4812 fsx_puts("NX-FS ABSENT: seek failed " as *u8); fsx_puts(path); fsx_puts("\n" as *u8) 4813 return 0 - 1 4814 } 4815 let buf: *u8 = sys_mmap(FSX_LINES_SCAN + 1) 4816 var n: i64 = 0 4817 var sc: i64 = 1 4818 while sc == 1 { 4819 let r: i64 = sys_read(fd, ((buf as i64 + n) as *u8), FSX_LINES_SCAN - n) 4820 if r <= 0 { sc = 0 } else { n = n + r; if n >= FSX_LINES_SCAN { sc = 0 } } 4821 } 4822 sys_close(fd) 4823 if n <= 0 { return fsx_fail(path, 0 - 1) } 4824 // a single trailing newline terminates the last line; it is not an empty extra line 4825 var lim: i64 = n 4826 var terminated: i64 = 0 4827 if buf[n - 1] == (10 as u8) { lim = n - 1; terminated = 1 } 4828 // walk backward for `c` line starts 4829 var p: i64 = lim 4830 var seen: i64 = 0 4831 var start: i64 = 0 4832 var go: i64 = 1 4833 while go == 1 { 4834 if p <= 0 { start = 0; go = 0 } else { 4835 p = p - 1 4836 if buf[p] == (10 as u8) { 4837 seen = seen + 1 4838 if seen >= c { start = p + 1; go = 0 } 4839 } 4840 } 4841 } 4842 var lines_out: i64 = seen + 1 4843 if seen >= c { lines_out = c } 4844 // count the window's lines once so a caller can address the whole file with `lines` afterwards 4845 var wl: i64 = 0 4846 var q: i64 = 0 4847 while q < lim { if buf[q] == (10 as u8) { wl = wl + 1 } q = q + 1 } 4848 wl = wl + 1 4849 let outn: i64 = n - start 4850 if outn > 0 { sys_write(1, ((buf as i64 + start) as *u8), outn) } 4851 if terminated == 0 { fsx_puts("\n" as *u8) } 4852 fsx_puts("[NX-FS TAIL lines=" as *u8); fsx_putn(lines_out) 4853 fsx_puts(" bytes=" as *u8); fsx_putn(outn) 4854 fsx_puts(" total_bytes=" as *u8); fsx_putn(sz) 4855 fsx_puts(" window_off=" as *u8); fsx_putn(off) 4856 fsx_puts(" scanned=" as *u8); fsx_putn(n) 4857 fsx_puts(" window_lines=" as *u8); fsx_putn(wl) 4858 fsx_puts(" last_line_terminated=" as *u8); fsx_putn(terminated) 4859 if off > 0 { fsx_puts(" WINDOW-IS-TAIL-OF-FILE" as *u8) } 4860 if off > 0 { if start == 0 { fsx_puts(" FIRST-LINE-MAY-BE-PARTIAL" as *u8) } } 4861 fsx_puts("]\n" as *u8) 4862 return outn 4863} 4864 4865func fsx_write_denied(path: *u8) -> i64 { 4866 if fsx_denied(path) == 1 { return 1 } 4867 if osf_write_forbidden(path) == 1 { return 1 } 4868 let lp: *u8 = sys_mmap(FSX_PATH_CAP) 4869 let ln: i64 = fsx_lower(path, lp, FSX_PATH_CAP) 4870 if fsx_deny_hit(lp, ln, "allowlist" as *u8) == 1 { return 1 } 4871 return fsx_conf_deny(lp, ln, "fs_write_deny.conf" as *u8) 4872} 4873// ---------- APPEND-ONLY write for journals and boards (2026-09-02) ---------- 4874// ONE O_APPEND write under an exclusive flock: the row lands whole and AFTER every row already there, and 4875// there is no read-modify-write window for a sibling seat to lose it in. MEASURED the same day: a `log|` 4876// row appended to lang.plan by anchored CAS edit (receipt OK bytes=51180) was gone minutes later -- a 4877// sibling's whole-file write had rebuilt the file from its own stale read. A BOARD IS A JOURNAL; JOURNALS 4878// ARE APPENDED, NEVER REWRITTEN. The write deny-list applies unchanged (a new write path must never become 4879// a way into the secret or device namespace). 4880// CONTRACT: body must end in '\n' (a row that does not terminate glues itself to the next seat's row -> 4881// FSX_APP_NONL, file unchanged); an empty body is refused (FSX_APP_EMPTY); when the file's LAST byte is not 4882// a newline (a rewrite left an unterminated tail) one newline is prepended INSIDE the same locked write, so 4883// the caller sees bytes-written == blen + 1 and can announce the heal. Returns bytes written; -2 DENIED; 4884// -3 io (open/lock/short write). 4885const FSX_NL: i64 = 10 // '\n' -- the row terminator this verb requires and heals 4886const FSX_APP_EMPTY: i64 = 0 - 4 // append refused: nothing to append 4887const FSX_APP_NONL: i64 = 0 - 5 // append refused: body does not end in a newline 4888const FSX_SEEK_SET: i64 = 0 // lseek whence: absolute offset (the tail probe) 4889// 1 = the file exists, is non-empty and its last byte is NOT a newline (an unterminated tail); else 0. 4890func fsx_tail_unterminated(path: *u8) -> i64 { 4891 let fd: i64 = sys_openat_rd(path) 4892 if fd < 0 { return 0 } 4893 let sz: i64 = sys_lseek(fd, 0, FSX_SEEK_END) 4894 var unterminated: i64 = 0 4895 if sz > 0 { 4896 if sys_lseek(fd, sz - 1, FSX_SEEK_SET) == sz - 1 { 4897 let lb: *u8 = sys_mmap(16) 4898 if sys_read(fd, lb, 1) == 1 { if lb[0] != (FSX_NL as u8) { unterminated = 1 } } 4899 } 4900 } 4901 sys_close(fd) 4902 return unterminated 4903} 4904func fsx_append(path: *u8, body: *u8, blen: i64) -> i64 { 4905 if fsx_write_denied(path) == 1 { 4906 fsx_puts("NX-FS DENIED: append refused (secret/device-namespace/allowlist deny)\n" as *u8) 4907 return 0 - (2 as i64) 4908 } 4909 if blen <= 0 { return FSX_APP_EMPTY } 4910 if body[blen - 1] != (FSX_NL as u8) { return FSX_APP_NONL } 4911 let heal: i64 = fsx_tail_unterminated(path) 4912 let fd: i64 = sys_openat_append(path, FSX_MODE_RW) 4913 if fd < 0 { return 0 - (3 as i64) } 4914 sys_flock(fd, SYS_LOCK_EX) 4915 let total: i64 = blen + heal 4916 let buf: *u8 = sys_mmap(total + 1) 4917 var i: i64 = 0 4918 if heal == 1 { buf[0] = FSX_NL as u8; i = 1 } 4919 var j: i64 = 0 4920 while j < blen { buf[i] = body[j]; i = i + 1; j = j + 1 } 4921 var off: i64 = 0 4922 while off < total { 4923 let w: i64 = sys_write(fd, ((buf as i64 + off) as *u8), total - off) 4924 if w <= 0 { sys_flock(fd, SYS_LOCK_UN); sys_close(fd); return 0 - (3 as i64) } 4925 off = off + w 4926 } 4927 sys_fsync(fd) 4928 sys_flock(fd, SYS_LOCK_UN) 4929 sys_close(fd) 4930 return total 4931} 4932// ATOMIC full-file write: content lands via <path>.nxw<pid> + fsync + rename, so a reader NEVER sees a 4933// torn file and concurrent writers each land whole (last rename wins; pid suffix = no shared tmp). 4934// Returns bytes written; -2 DENIED; -3 io error (path too long / open / short write / rename). 4935func fsx_write(path: *u8, body: *u8, blen: i64) -> i64 { 4936 if fsx_write_denied(path) == 1 { 4937 fsx_puts("NX-FS DENIED: write refused (secret/device-namespace/allowlist deny)\n" as *u8) 4938 return 0 - (2 as i64) 4939 } 4940 let plen: i64 = vw_slen(path) 4941 if plen + FSX_TMP_ROOM >= FSX_PATH_CAP { return 0 - (3 as i64) } 4942 let tmp: *u8 = sys_mmap(FSX_PATH_CAP) 4943 var i: i64 = 0 4944 while i < plen { tmp[i] = path[