nx_argv.nx source
↩ module page · 284 lines · 10545 B
1// nx_argv.nx -- Linux RV64 entry-stack builder.
2//
3// At program entry the Linux kernel hands the runtime a stack
4// laid out per System V ABI:
5//
6// sp -> argc (i64)
7// argv[0] (pointer to NUL-terminated string)
8// argv[1]
9// ...
10// argv[argc-1]
11// NULL
12// envp[0]
13// envp[1]
14// ...
15// envp[K-1]
16// NULL
17// auxv entries (Elf64_auxv_t = i64 a_type, i64 a_un.a_val)
18// AT_NULL terminator (a_type=0, a_un=0)
19// (string area: argv/envp string bodies)
20//
21// Without this layout, a simulated program crashes the moment it
22// reads its first argument. Setting it up correctly lets us pass
23// arguments through nxc_native_wrap to the simulated program, which
24// is required for any meaningful test harness.
25//
26// Pairs with nx_rv64_sim (consumes the stack via sp register) +
27// nxc_native_wrap (wires this in front of nx_rv64_run).
28//
29// Reference: System V ABI for RV64, "Process Initialization" section
30// (https://github.com/riscv-non-isa/riscv-elf-psabi-doc).
31
32// nx_safety_envelope:
33// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
34// sil_target: SIL1
35// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
36// verdict: NOT_YET_EVALUATED
37
38import "syscalls.nx"
39import "runtime.nx"
40const NX_MAGIC_4096: i64 = 4096
41const NX_MAGIC_1048576: i64 = 1048576
42
43// AT_* auxv types. We populate the few that real programs read.
44const NX_AT_NULL: i64 = 0 // end-of-vector marker
45const NX_AT_PHDR: i64 = 3 // address of program headers
46const NX_AT_PHENT: i64 = 4 // size of one phdr entry
47const NX_AT_PHNUM: i64 = 5 // count of phdrs
48const NX_AT_PAGESZ: i64 = 6 // system page size
49const NX_AT_BASE: i64 = 7 // dynamic linker base addr
50const NX_AT_FLAGS: i64 = 8 // (always 0 in our case)
51const NX_AT_ENTRY: i64 = 9 // program entry point
52const NX_AT_UID: i64 = 11
53const NX_AT_EUID: i64 = 12
54const NX_AT_GID: i64 = 13
55const NX_AT_EGID: i64 = 14
56const NX_AT_HWCAP: i64 = 16 // CPU feature bits
57const NX_AT_CLKTCK: i64 = 17 // clock_t per second
58const NX_AT_RANDOM: i64 = 25 // 16-byte random seed (stack canary)
59
60// strlen canonical in runtime.nx.
61
62// Build the entry stack for a simulated RV64 program in `mem` of
63// total size `mem_size`.
64//
65// Returns the simulated `sp` value (an offset into `mem`) the caller
66// should set on the simulated CPU.
67//
68// Inputs:
69// mem -- HOST pointer to the simulated memory region. We
70// write into the TOP of this region (top-down stack).
71// mem_size -- size in bytes of the simulated region.
72// load_base -- virtual address the simulated memory starts at.
73// Returned sp is in the simulated address space, so:
74// sim_sp = load_base + (host_sp - mem)
75// argc -- number of argv entries.
76// argv_host -- HOST array of pointers to NUL-terminated strings
77// (length argc).
78// envc -- number of envp entries.
79// envp_host -- HOST array of pointers to envp strings (length envc).
80// entry -- program entry point (used for AT_ENTRY).
81//
82// Returns -1 on overflow.
83//
84// Layout is built from the TOP of memory downward:
85// 1. Copy argv/envp string bodies (NUL-terminated).
86// 2. 16-byte random seed (NX_AT_RANDOM).
87// 3. Align to 16 bytes.
88// 4. AuxV entries (terminated by AT_NULL).
89// 5. envp pointer array + NULL terminator.
90// 6. argv pointer array + NULL terminator.
91// 7. argc (i64).
92//
93// All pointers stored in the simulated stack are in the simulated
94// virtual address space (load_base + offset_into_mem).
95func nx_argv_build_stack(
96 mem: *u8,
97 mem_size: i64,
98 load_base: i64,
99 argc: i64,
100 argv_host: *i64,
101 envc: i64,
102 envp_host: *i64,
103 entry: i64
104) -> i64 {
105 // Stage 1: copy strings into top of memory. We allocate a
106 // pointer table that holds the SIMULATED address of each string.
107 let argv_sim_raw: *u8 = sys_mmap((argc + 1) * 8)
108 let argv_sim: *i64 = argv_sim_raw as *i64
109 let envp_sim_raw: *u8 = sys_mmap((envc + 1) * 8)
110 let envp_sim: *i64 = envp_sim_raw as *i64
111
112 var top: i64 = mem_size
113 // Walk argv string bodies.
114 var i: i64 = 0
115 while i < argc {
116 let s_host: *u8 = (argv_host[i]) as *u8
117 let n: i64 = strlen(s_host)
118 top = top - (n + 1)
119 if top < 0 { return -1 }
120 // Copy s_host[0..=n] into mem[top..top+n+1].
121 var k: i64 = 0
122 while k <= n {
123 mem[top + k] = s_host[k]
124 k = k + 1
125 }
126 argv_sim[i] = load_base + top
127 i = i + 1
128 }
129 argv_sim[argc] = 0
130 // Walk envp string bodies.
131 var j: i64 = 0
132 while j < envc {
133 let e_host: *u8 = (envp_host[j]) as *u8
134 let n2: i64 = strlen(e_host)
135 top = top - (n2 + 1)
136 if top < 0 { return -1 }
137 var k2: i64 = 0
138 while k2 <= n2 {
139 mem[top + k2] = e_host[k2]
140 k2 = k2 + 1
141 }
142 envp_sim[j] = load_base + top
143 j = j + 1
144 }
145 envp_sim[envc] = 0
146
147 // Stage 2: 16-byte random seed (AT_RANDOM target).
148 top = top - 16
149 if top < 0 { return -1 }
150 var r: i64 = 0
151 while r < 16 {
152 // Deterministic seed for reproducibility under F6. Real
153 // builds plug the kernel-provided seed in here.
154 mem[top + r] = 0
155 r = r + 1
156 }
157 let at_random_addr: i64 = load_base + top
158
159 // Stage 3: pre-compute total down-stack size of the rest, then
160 // pick `top` so that the final sp lands on a 16-byte boundary
161 // (System V ABI requirement).
162 let n_aux: i64 = 4
163 let rest: i64 = n_aux * 16 + (envc + 1) * 8 + (argc + 1) * 8 + 8
164 let target_sp: i64 = (top - rest) & (~15)
165 top = target_sp + rest
166
167 // Stage 4: build auxv entries. Each entry is two i64s. Order
168 // doesn't matter, but AT_NULL must be last.
169 // Entries: PAGESZ, ENTRY, RANDOM, AT_NULL.
170 top = top - n_aux * 16
171 if top < 0 { return -1 }
172 let aux: *i64 = ((mem as i64) + top) as *i64
173 aux[0] = NX_AT_PAGESZ; aux[1] = NX_MAGIC_4096
174 aux[2] = NX_AT_ENTRY; aux[3] = entry
175 aux[4] = NX_AT_RANDOM; aux[5] = at_random_addr
176 aux[6] = NX_AT_NULL; aux[7] = 0
177
178 // Stage 5: envp pointer array + NULL terminator.
179 top = top - (envc + 1) * 8
180 if top < 0 { return -1 }
181 var ev: i64 = 0
182 while ev <= envc {
183 let dst: *i64 = (((mem as i64) + top + ev * 8) as *i64)
184 *dst = envp_sim[ev]
185 ev = ev + 1
186 }
187
188 // Stage 6: argv pointer array + NULL terminator.
189 top = top - (argc + 1) * 8
190 if top < 0 { return -1 }
191 var av: i64 = 0
192 while av <= argc {
193 let dst2: *i64 = (((mem as i64) + top + av * 8) as *i64)
194 *dst2 = argv_sim[av]
195 av = av + 1
196 }
197
198 // Stage 7: argc.
199 top = top - 8
200 if top < 0 { return -1 }
201 let argc_dst: *i64 = (((mem as i64) + top) as *i64)
202 *argc_dst = argc
203
204 return load_base + top
205}
206
207// Convenience: build a single-arg stack from a host string pointer
208// (e.g. the simulated program's argv[0] = path passed to wrap).
209func nx_argv_build_single(mem: *u8, mem_size: i64, load_base: i64, argv0: *u8, entry: i64) -> i64 {
210 let av_raw: *u8 = sys_mmap(8)
211 let av: *i64 = av_raw as *i64
212 av[0] = argv0 as i64
213 let envp_raw: *u8 = sys_mmap(8)
214 let envp: *i64 = envp_raw as *i64
215 return nx_argv_build_stack(mem, mem_size, load_base, 1, av, 0, envp, entry)
216}
217
218// ---- self-test ---------------------------------------------------
219
220func main() -> i64 {
221 // 1 MiB simulated memory at virtual base 0x10000.
222 let mem_size: i64 = NX_MAGIC_1048576
223 let mem: *u8 = sys_mmap(mem_size)
224 var i: i64 = 0
225 while i < mem_size { mem[i] = 0; i = i + 1 }
226
227 // Pretend argv = ["nxc.elf", "-O", "input.nx"].
228 let arg0: *u8 = sys_mmap(16)
229 arg0[0] = 0x6E; arg0[1] = 0x78; arg0[2] = 0x63; arg0[3] = 0x2E
230 arg0[4] = 0x65; arg0[5] = 0x6C; arg0[6] = 0x66; arg0[7] = 0
231 let arg1: *u8 = sys_mmap(8)
232 arg1[0] = 0x2D; arg1[1] = 0x4F; arg1[2] = 0
233 let arg2: *u8 = sys_mmap(16)
234 arg2[0] = 0x69; arg2[1] = 0x6E; arg2[2] = 0x70; arg2[3] = 0x75
235 arg2[4] = 0x74; arg2[5] = 0x2E; arg2[6] = 0x6E; arg2[7] = 0x78; arg2[8] = 0
236 let argv_raw: *u8 = sys_mmap(32)
237 let argv: *i64 = argv_raw as *i64
238 argv[0] = arg0 as i64
239 argv[1] = arg1 as i64
240 argv[2] = arg2 as i64
241
242 // No envp.
243 let envp_raw: *u8 = sys_mmap(8)
244 let envp: *i64 = envp_raw as *i64
245
246 let load_base: i64 = 0x10000
247 let entry: i64 = 0x10100
248 let sim_sp: i64 = nx_argv_build_stack(mem, mem_size, load_base, 3, argv, 0, envp, entry)
249 if sim_sp < 0 { return __syscall(93, 1, 0, 0, 0, 0, 0) }
250
251 // sim_sp should be near the top, well below load_base + mem_size.
252 let max_sp: i64 = load_base + mem_size
253 if sim_sp >= max_sp { return __syscall(93, 2, 0, 0, 0, 0, 0) }
254 if sim_sp < load_base { return __syscall(93, 3, 0, 0, 0, 0, 0) }
255
256 // Read back argc at simulated sp -> host sp.
257 let host_sp: i64 = (mem as i64) + (sim_sp - load_base)
258 let argc_p: *i64 = host_sp as *i64
259 if *argc_p != 3 { return __syscall(93, 4, 0, 0, 0, 0, 0) }
260
261 // argv[0] sim address should resolve to "nxc.elf" in host memory.
262 let argv0_sim: i64 = *((host_sp + 8) as *i64)
263 if argv0_sim < load_base { return __syscall(93, 5, 0, 0, 0, 0, 0) }
264 if argv0_sim >= max_sp { return __syscall(93, 6, 0, 0, 0, 0, 0) }
265 let argv0_host: *u8 = (((mem as i64) + (argv0_sim - load_base)) as *u8)
266 if argv0_host[0] != 0x6E { return __syscall(93, 7, 0, 0, 0, 0, 0) }
267 if argv0_host[6] != 0x66 { return __syscall(93, 8, 0, 0, 0, 0, 0) }
268 if argv0_host[7] != 0 { return __syscall(93, 9, 0, 0, 0, 0, 0) }
269
270 // After argc + 4*argv pointers (3 strings + NULL) + 1*envp NULL
271 // we should hit the auxv entries, the first of which is PAGESZ.
272 let aux_off: i64 = host_sp + 8 + 4 * 8 + 1 * 8
273 let aux: *i64 = aux_off as *i64
274 if aux[0] != NX_AT_PAGESZ { return __syscall(93, 10, 0, 0, 0, 0, 0) }
275 if aux[1] != NX_MAGIC_4096 { return __syscall(93, 11, 0, 0, 0, 0, 0) }
276 if aux[2] != NX_AT_ENTRY { return __syscall(93, 12, 0, 0, 0, 0, 0) }
277 if aux[3] != entry { return __syscall(93, 13, 0, 0, 0, 0, 0) }
278 if aux[6] != NX_AT_NULL { return __syscall(93, 14, 0, 0, 0, 0, 0) }
279
280 // sp must be 16-byte aligned (System V ABI).
281 if (sim_sp & 15) != 0 { return __syscall(93, 15, 0, 0, 0, 0, 0) }
282
283 return 0
284}