nx_pe_job_self_test.nx source
↩ module page · 278 lines · 13154 B
1// nx_pe_job_self_test.nx -- W2a: sovereign Windows Job Object, native, no HCS.
2//
3// Docker-replacement ladder rung W2a (isolation model = sovereign Job Objects,
4// operator decision 2026-06-13). The substrate emits a native Windows PE that:
5// 1. CreateJobObjectW(NULL, NULL) -> hJob (our sovereign control boundary)
6// 2. AssignProcessToJobObject(hJob, self) -> put THIS process under the job
7// 3. IsProcessInJob(self, hJob, &inJob) -> VERIFY membership (no-false-green)
8// 4. ExitProcess(inJob ? 42 : 99)
9// self = GetCurrentProcess() = (HANDLE)-1 (constant pseudo-handle, no import needed).
10//
11// PROOF: exit 42 iff the process is genuinely inside OUR job (governed). If the
12// assignment did not take (e.g. nesting unavailable), inJob=FALSE -> exit 99 -> RED.
13// This is the cgroups-analog BOUNDARY on Windows -- the thing Docker needs a Linux
14// VM (WSL2/Hyper-V) for. We do it as a native NT process. No HCS daemon, no MinGW,
15// no MSVC, no VM. 4 kernel32 imports.
16//
17// HONEST SCOPE: this proves Job CREATION + MEMBERSHIP (the boundary exists & governs).
18// It does NOT yet enforce a resource LIMIT -- that is W2b (SetInformationJobObject
19// memory.max/ActiveProcessLimit + tamper proof). Child-spawn-into-job = W2c (reuse
20// nx_pe_emit_spawn42 machinery). Founded on nx_pe_writer.nx (hand-authored byte-layout,
21// tutor-scaffold lineage); back-fill = nxc2 PE/MS-x64 backend (W3).
22//
23// genealogy_id: win32_job_objects_2000 + lmctfy_2013 + runc_cgroups_2016
24// lineage_id: substrate_pe_jobself_v1
25
26import "nx_syscalls.nx"
27import "nx_hal.nx"
28import "nx_pe_writer.nx"
29
30const PE_JOBSELF_FILE_SIZE: i64 = 0x600 // 1536 bytes: headers + .text + .idata
31
32// idata base (file 0x400, RVA 0x2000). 4 imports:
33// slot 0 CreateJobObjectW IAT RVA 0x2050 byname RVA 0x2078
34// slot 1 AssignProcessToJobObject IAT RVA 0x2058 byname RVA 0x208C
35// slot 2 IsProcessInJob IAT RVA 0x2060 byname RVA 0x20A8
36// slot 3 ExitProcess IAT RVA 0x2068 byname RVA 0x20BA
37// dll name "kernel32.dll" RVA 0x20C8
38func nx_pe_emit_job_self(buf: *u8) -> i64 {
39 if (buf as i64) == 0 { return 0 - NX_PE_BAD_INPUT }
40
41 // ===== DOS Header =====
42 _w16(buf, 0, 0x5A4D)
43 _w32(buf, 0x3C, FOFF_PE_SIG)
44
45 // ===== PE Signature =====
46 _w32(buf, FOFF_PE_SIG, 0x00004550)
47
48 // ===== COFF File Header =====
49 _w16(buf, FOFF_COFF + 0, PE_MACHINE_AMD64)
50 _w16(buf, FOFF_COFF + 2, 2) // 2 sections (.text + .idata)
51 _w16(buf, FOFF_COFF + 16, 0xF0)
52 _w16(buf, FOFF_COFF + 18, PE_CHAR_EXEC | PE_CHAR_LARGE_ADDR)
53
54 // ===== Optional Header (PE32+) =====
55 _w16(buf, FOFF_OPT + 0, PE_OH_MAGIC_PEPLUS)
56 _w8(buf, FOFF_OPT + 2, 1)
57 _w32(buf, FOFF_OPT + 4, 0x200) // SizeOfCode
58 _w32(buf, FOFF_OPT + 8, 0x200) // SizeOfInitializedData (.idata)
59 _w32(buf, FOFF_OPT + 16, RVA_TEXT) // AddressOfEntryPoint
60 _w32(buf, FOFF_OPT + 20, RVA_TEXT) // BaseOfCode
61 _w64(buf, FOFF_OPT + 24, IMG_BASE_LO, IMG_BASE_HI)
62 _w32(buf, FOFF_OPT + 32, 0x1000) // SectionAlignment
63 _w32(buf, FOFF_OPT + 36, 0x200) // FileAlignment
64 _w16(buf, FOFF_OPT + 40, 6) // MajorOSVersion
65 _w16(buf, FOFF_OPT + 48, 6) // MajorSubsystemVersion
66 _w32(buf, FOFF_OPT + 56, 0x3000) // SizeOfImage (headers + 2 sections)
67 _w32(buf, FOFF_OPT + 60, 0x200) // SizeOfHeaders
68 _w16(buf, FOFF_OPT + 68, PE_SUBSYSTEM_CONSOLE)
69 _w64(buf, FOFF_OPT + 72, 0x100000, 0) // StackReserve
70 _w64(buf, FOFF_OPT + 80, 0x1000, 0) // StackCommit
71 _w64(buf, FOFF_OPT + 88, 0x100000, 0) // HeapReserve
72 _w64(buf, FOFF_OPT + 96, 0x1000, 0) // HeapCommit
73 _w32(buf, FOFF_OPT + 108, 16) // NumberOfRvaAndSizes
74 // DataDirectory[1] = Import Table -> .idata RVA 0x2000, 2 descriptors (0x28)
75 _w32(buf, FOFF_OPT + 112 + 8, RVA_IDATA)
76 _w32(buf, FOFF_OPT + 112 + 12, 0x28)
77
78 // ===== Section Headers =====
79 // .text vsize 0x5E (94 bytes of code)
80 _emit_section_header(buf, FOFF_SECT_TBL, 46, 116, 101, 120, 116, 0, 0, 0, 0x5E, RVA_TEXT, 0x200, FOFF_TEXT, PE_SECT_CODE_X_R)
81 // .idata vsize 0xD5 (213 bytes)
82 _emit_section_header(buf, FOFF_SECT_TBL + 40, 46, 105, 100, 97, 116, 97, 0, 0, 0xD5, RVA_IDATA, 0x200, FOFF_IDATA, PE_SECT_DATA_R)
83
84 // ===== .text (MS x64) =====
85 let t: i64 = FOFF_TEXT
86 // sub rsp, 0x38 (48 83 EC 38) off 0
87 _w8(buf, t + 0, 0x48); _w8(buf, t + 1, 0x83); _w8(buf, t + 2, 0xEC); _w8(buf, t + 3, 0x38)
88 // --- CreateJobObjectW(NULL, NULL) ---
89 // xor ecx, ecx (31 C9) off 4
90 _w8(buf, t + 4, 0x31); _w8(buf, t + 5, 0xC9)
91 // xor edx, edx (31 D2) off 6
92 _w8(buf, t + 6, 0x31); _w8(buf, t + 7, 0xD2)
93 // call [rip+disp] IAT_CJO 0x2050; next RVA 0x100E; disp=0x1042 off 8
94 _w8(buf, t + 8, 0xFF); _w8(buf, t + 9, 0x15); _w32(buf, t + 10, 0x1042)
95 // mov [rsp+0x28], rax (hJob) (48 89 44 24 28) off 14 (0x0E)
96 _w8(buf, t + 14, 0x48); _w8(buf, t + 15, 0x89); _w8(buf, t + 16, 0x44); _w8(buf, t + 17, 0x24); _w8(buf, t + 18, 0x28)
97 // --- AssignProcessToJobObject(hJob, (HANDLE)-1) ---
98 // mov rcx, [rsp+0x28] (48 8B 4C 24 28) off 19 (0x13)
99 _w8(buf, t + 19, 0x48); _w8(buf, t + 20, 0x8B); _w8(buf, t + 21, 0x4C); _w8(buf, t + 22, 0x24); _w8(buf, t + 23, 0x28)
100 // mov rdx, -1 (48 C7 C2 FF FF FF FF) off 24 (0x18)
101 _w8(buf, t + 24, 0x48); _w8(buf, t + 25, 0xC7); _w8(buf, t + 26, 0xC2)
102 _w8(buf, t + 27, 0xFF); _w8(buf, t + 28, 0xFF); _w8(buf, t + 29, 0xFF); _w8(buf, t + 30, 0xFF)
103 // call [rip+disp] IAT_APTJ 0x2058; next RVA 0x1025; disp=0x1033 off 31 (0x1F)
104 _w8(buf, t + 31, 0xFF); _w8(buf, t + 32, 0x15); _w32(buf, t + 33, 0x1033)
105 // --- IsProcessInJob((HANDLE)-1, hJob, &inJob) ---
106 // mov dword [rsp+0x30], 0 (init inJob) (C7 44 24 30 00000000) off 37 (0x25)
107 _w8(buf, t + 37, 0xC7); _w8(buf, t + 38, 0x44); _w8(buf, t + 39, 0x24); _w8(buf, t + 40, 0x30); _w32(buf, t + 41, 0)
108 // mov rcx, -1 (48 C7 C1 FF FF FF FF) off 45 (0x2D)
109 _w8(buf, t + 45, 0x48); _w8(buf, t + 46, 0xC7); _w8(buf, t + 47, 0xC1)
110 _w8(buf, t + 48, 0xFF); _w8(buf, t + 49, 0xFF); _w8(buf, t + 50, 0xFF); _w8(buf, t + 51, 0xFF)
111 // mov rdx, [rsp+0x28] (hJob) (48 8B 54 24 28) off 52 (0x34)
112 _w8(buf, t + 52, 0x48); _w8(buf, t + 53, 0x8B); _w8(buf, t + 54, 0x54); _w8(buf, t + 55, 0x24); _w8(buf, t + 56, 0x28)
113 // lea r8, [rsp+0x30] (&inJob) (4C 8D 44 24 30) off 57 (0x39)
114 _w8(buf, t + 57, 0x4C); _w8(buf, t + 58, 0x8D); _w8(buf, t + 59, 0x44); _w8(buf, t + 60, 0x24); _w8(buf, t + 61, 0x30)
115 // call [rip+disp] IAT_IPIJ 0x2060; next RVA 0x1044; disp=0x101C off 62 (0x3E)
116 _w8(buf, t + 62, 0xFF); _w8(buf, t + 63, 0x15); _w32(buf, t + 64, 0x101C)
117 // --- exit = inJob ? 42 : 99 ---
118 // mov ecx, 42 (B9 2A 00 00 00) off 68 (0x44)
119 _w8(buf, t + 68, 0xB9); _w32(buf, t + 69, 42)
120 // mov edx, 99 (BA 63 00 00 00) off 73 (0x49)
121 _w8(buf, t + 73, 0xBA); _w32(buf, t + 74, 99)
122 // mov eax, [rsp+0x30] (inJob) (8B 44 24 30) off 78 (0x4E)
123 _w8(buf, t + 78, 0x8B); _w8(buf, t + 79, 0x44); _w8(buf, t + 80, 0x24); _w8(buf, t + 81, 0x30)
124 // test eax, eax (85 C0) off 82 (0x52)
125 _w8(buf, t + 82, 0x85); _w8(buf, t + 83, 0xC0)
126 // cmovz ecx, edx (if inJob==0 -> 99) (0F 44 CA) off 84 (0x54)
127 _w8(buf, t + 84, 0x0F); _w8(buf, t + 85, 0x44); _w8(buf, t + 86, 0xCA)
128 // call [rip+disp] IAT_EXIT 0x2068; next RVA 0x105D; disp=0x100B off 87 (0x57)
129 _w8(buf, t + 87, 0xFF); _w8(buf, t + 88, 0x15); _w32(buf, t + 89, 0x100B)
130 // int3 (CC) off 93 (0x5D)
131 _w8(buf, t + 93, 0xCC)
132
133 // ===== .idata =====
134 let d: i64 = FOFF_IDATA
135 // Import Descriptor for kernel32
136 _w32(buf, d + 0, 0x2028) // OriginalFirstThunk = INT RVA
137 _w32(buf, d + 12, 0x20C8) // Name = "kernel32.dll" RVA
138 _w32(buf, d + 16, 0x2050) // FirstThunk = IAT RVA
139 // (null descriptor d+0x14.. already zero)
140
141 // INT at 0x28 (RVA 0x2028)
142 _w64(buf, d + 0x28, 0x2078, 0) // CreateJobObjectW
143 _w64(buf, d + 0x30, 0x208C, 0) // AssignProcessToJobObject
144 _w64(buf, d + 0x38, 0x20A8, 0) // IsProcessInJob
145 _w64(buf, d + 0x40, 0x20BA, 0) // ExitProcess
146 _w64(buf, d + 0x48, 0, 0) // null
147
148 // IAT at 0x50 (RVA 0x2050) -- loader overwrites with real addrs
149 _w64(buf, d + 0x50, 0x2078, 0)
150 _w64(buf, d + 0x58, 0x208C, 0)
151 _w64(buf, d + 0x60, 0x20A8, 0)
152 _w64(buf, d + 0x68, 0x20BA, 0)
153 _w64(buf, d + 0x70, 0, 0)
154
155 // IMAGE_IMPORT_BY_NAME: CreateJobObjectW @ 0x78 (RVA 0x2078)
156 _w16(buf, d + 0x78, 0)
157 _w8(buf, d + 0x7A, 67) // C
158 _w8(buf, d + 0x7B, 114) // r
159 _w8(buf, d + 0x7C, 101) // e
160 _w8(buf, d + 0x7D, 97) // a
161 _w8(buf, d + 0x7E, 116) // t
162 _w8(buf, d + 0x7F, 101) // e
163 _w8(buf, d + 0x80, 74) // J
164 _w8(buf, d + 0x81, 111) // o
165 _w8(buf, d + 0x82, 98) // b
166 _w8(buf, d + 0x83, 79) // O
167 _w8(buf, d + 0x84, 98) // b
168 _w8(buf, d + 0x85, 106) // j
169 _w8(buf, d + 0x86, 101) // e
170 _w8(buf, d + 0x87, 99) // c
171 _w8(buf, d + 0x88, 116) // t
172 _w8(buf, d + 0x89, 87) // W
173 _w8(buf, d + 0x8A, 0) // null
174
175 // AssignProcessToJobObject @ 0x8C (RVA 0x208C)
176 _w16(buf, d + 0x8C, 0)
177 _w8(buf, d + 0x8E, 65) // A
178 _w8(buf, d + 0x8F, 115) // s
179 _w8(buf, d + 0x90, 115) // s
180 _w8(buf, d + 0x91, 105) // i
181 _w8(buf, d + 0x92, 103) // g
182 _w8(buf, d + 0x93, 110) // n
183 _w8(buf, d + 0x94, 80) // P
184 _w8(buf, d + 0x95, 114) // r
185 _w8(buf, d + 0x96, 111) // o
186 _w8(buf, d + 0x97, 99) // c
187 _w8(buf, d + 0x98, 101) // e
188 _w8(buf, d + 0x99, 115) // s
189 _w8(buf, d + 0x9A, 115) // s
190 _w8(buf, d + 0x9B, 84) // T
191 _w8(buf, d + 0x9C, 111) // o
192 _w8(buf, d + 0x9D, 74) // J
193 _w8(buf, d + 0x9E, 111) // o
194 _w8(buf, d + 0x9F, 98) // b
195 _w8(buf, d + 0xA0, 79) // O
196 _w8(buf, d + 0xA1, 98) // b
197 _w8(buf, d + 0xA2, 106) // j
198 _w8(buf, d + 0xA3, 101) // e
199 _w8(buf, d + 0xA4, 99) // c
200 _w8(buf, d + 0xA5, 116) // t
201 _w8(buf, d + 0xA6, 0) // null
202
203 // IsProcessInJob @ 0xA8 (RVA 0x20A8)
204 _w16(buf, d + 0xA8, 0)
205 _w8(buf, d + 0xAA, 73) // I
206 _w8(buf, d + 0xAB, 115) // s
207 _w8(buf, d + 0xAC, 80) // P
208 _w8(buf, d + 0xAD, 114) // r
209 _w8(buf, d + 0xAE, 111) // o
210 _w8(buf, d + 0xAF, 99) // c
211 _w8(buf, d + 0xB0, 101) // e
212 _w8(buf, d + 0xB1, 115) // s
213 _w8(buf, d + 0xB2, 115) // s
214 _w8(buf, d + 0xB3, 73) // I
215 _w8(buf, d + 0xB4, 110) // n
216 _w8(buf, d + 0xB5, 74) // J
217 _w8(buf, d + 0xB6, 111) // o
218 _w8(buf, d + 0xB7, 98) // b
219 _w8(buf, d + 0xB8, 0) // null
220
221 // ExitProcess @ 0xBA (RVA 0x20BA)
222 _w16(buf, d + 0xBA, 0)
223 _w8(buf, d + 0xBC, 69) // E
224 _w8(buf, d + 0xBD, 120) // x
225 _w8(buf, d + 0xBE, 105) // i
226 _w8(buf, d + 0xBF, 116) // t
227 _w8(buf, d + 0xC0, 80) // P
228 _w8(buf, d + 0xC1, 114) // r
229 _w8(buf, d + 0xC2, 111) // o
230 _w8(buf, d + 0xC3, 99) // c
231 _w8(buf, d + 0xC4, 101) // e
232 _w8(buf, d + 0xC5, 115) // s
233 _w8(buf, d + 0xC6, 115) // s
234 _w8(buf, d + 0xC7, 0) // null
235
236 // "kernel32.dll\0" @ 0xC8 (RVA 0x20C8)
237 _w8(buf, d + 0xC8, 107) // k
238 _w8(buf, d + 0xC9, 101) // e
239 _w8(buf, d + 0xCA, 114) // r
240 _w8(buf, d + 0xCB, 110) // n
241 _w8(buf, d + 0xCC, 101) // e
242 _w8(buf, d + 0xCD, 108) // l
243 _w8(buf, d + 0xCE, 51) // 3
244 _w8(buf, d + 0xCF, 50) // 2
245 _w8(buf, d + 0xD0, 46) // .
246 _w8(buf, d + 0xD1, 100) // d
247 _w8(buf, d + 0xD2, 108) // l
248 _w8(buf, d + 0xD3, 108) // l
249 _w8(buf, d + 0xD4, 0) // null
250
251 return NX_PE_OK
252}
253
254func main() -> i64 {
255 let buf: *u8 = nx_hal_alloc_pages(PE_JOBSELF_FILE_SIZE)
256 if (buf as i64) == 0 { return 1 }
257
258 let rc: i64 = nx_pe_emit_job_self(buf)
259 if rc != NX_PE_OK { return 2 }
260
261 // Spot-checks (catch layout drift before native run).
262 if buf[0] != 0x4D { return 10 } // 'M'
263 if buf[0x80] != 0x50 { return 11 } // 'P' (PE sig)
264 if buf[0x86] != 0x02 { return 12 } // 2 sections
265 if buf[0x200] != 0x48 { return 13 } // .text: sub rsp
266 if buf[0x203] != 0x38 { return 14 } // imm8 0x38
267 if buf[0x47A] != 0x43 { return 15 } // 'C' of CreateJobObjectW (d+0x7A=0x47A)
268 if buf[0x48E] != 0x41 { return 16 } // 'A' of AssignProcessToJobObject (d+0x8E=0x48E)
269 if buf[0x4C8] != 0x6B { return 17 } // 'k' of kernel32.dll (d+0xC8=0x4C8)
270
271 let out_path: *u8 = "/mnt/c/Users/elder/nishi-core/nxc2/_offc/nx_pe_job_self.exe" as *u8
272 let wrc: i64 = nx_pe_write_to_file(out_path, buf, PE_JOBSELF_FILE_SIZE)
273 if wrc != NX_PE_OK { return 70 }
274
275 let msg: *u8 = "[substrate] Job-Object PE32+ written (nx_pe_job_self.exe, 1536 bytes)\n" as *u8
276 sys_write(1, msg, 68)
277 return 0
278}