Rust · 10540 bytes Raw Blame History
1 //! Sprint 28 determinism guardrails.
2 //!
3 //! Parallel speedups are only safe if they never perturb the final image. This
4 //! test repeatedly links a multi-object executable and requires byte-identical
5 //! output across concurrent runs.
6
7 mod common;
8
9 use std::collections::VecDeque;
10 use std::fs;
11 use std::path::{Path, PathBuf};
12 use std::process::Command;
13 use std::sync::{Arc, Mutex};
14 use std::thread;
15 use std::time::{SystemTime, UNIX_EPOCH};
16
17 use afs_ld::{LinkOptions, Linker, OutputKind};
18 use common::harness::{assemble, have_xcrun, have_xcrun_tool};
19
20 const DEFAULT_RUNS: usize = 100;
21
22 #[test]
23 fn repeated_parallel_links_are_byte_identical() {
24 if !have_xcrun() || !have_xcrun_tool("as") {
25 eprintln!("skipping: xcrun as unavailable");
26 return;
27 }
28
29 let root = unique_temp_dir("determinism").expect("create determinism temp dir");
30 let main_obj = root.join("main.o");
31 assemble(
32 "\
33 .section __TEXT,__text,regular,pure_instructions\n\
34 .globl _main\n\
35 _main:\n\
36 bl _helper\n\
37 adrp x8, _value@GOTPAGE\n\
38 ldr x8, [x8, _value@GOTPAGEOFF]\n\
39 ldr w0, [x8]\n\
40 ret\n\
41 \n\
42 .subsections_via_symbols\n",
43 &main_obj,
44 )
45 .expect("assemble determinism main fixture");
46 let helper_obj = root.join("helper.o");
47 assemble(
48 "\
49 .section __TEXT,__text,regular,pure_instructions\n\
50 .globl _helper\n\
51 _helper:\n\
52 ret\n\
53 \n\
54 .subsections_via_symbols\n",
55 &helper_obj,
56 )
57 .expect("assemble determinism helper fixture");
58 let data_obj = root.join("data.o");
59 assemble(
60 "\
61 .section __DATA,__data\n\
62 .globl _value\n\
63 .p2align 2\n\
64 _value:\n\
65 .long 7\n\
66 \n\
67 .subsections_via_symbols\n",
68 &data_obj,
69 )
70 .expect("assemble determinism data fixture");
71
72 let inputs = vec![main_obj, helper_obj, data_obj];
73 assert_repeated_links_identical(inputs, &root, "objects");
74
75 let _ = fs::remove_dir_all(root);
76 }
77
78 #[test]
79 fn repeated_parallel_archive_fetches_are_byte_identical() {
80 if !have_xcrun() || !have_xcrun_tool("as") {
81 eprintln!("skipping: xcrun as unavailable");
82 return;
83 }
84
85 let root = unique_temp_dir("archive-determinism").expect("create archive determinism temp dir");
86 let main_obj = root.join("main.o");
87 assemble(
88 "\
89 .section __TEXT,__text,regular,pure_instructions\n\
90 .globl _main\n\
91 _main:\n\
92 bl _helper_a\n\
93 bl _helper_b\n\
94 mov w0, #0\n\
95 ret\n\
96 \n\
97 .subsections_via_symbols\n",
98 &main_obj,
99 )
100 .expect("assemble archive determinism main fixture");
101 let helper_a_obj = root.join("helper_a.o");
102 assemble(
103 "\
104 .section __TEXT,__text,regular,pure_instructions\n\
105 .globl _helper_a\n\
106 _helper_a:\n\
107 ret\n\
108 \n\
109 .subsections_via_symbols\n",
110 &helper_a_obj,
111 )
112 .expect("assemble archive determinism helper_a fixture");
113 let helper_b_obj = root.join("helper_b.o");
114 assemble(
115 "\
116 .section __TEXT,__text,regular,pure_instructions\n\
117 .globl _helper_b\n\
118 _helper_b:\n\
119 ret\n\
120 \n\
121 .subsections_via_symbols\n",
122 &helper_b_obj,
123 )
124 .expect("assemble archive determinism helper_b fixture");
125 let unused_obj = root.join("unused.o");
126 assemble(
127 "\
128 .section __TEXT,__text,regular,pure_instructions\n\
129 .globl _unused\n\
130 _unused:\n\
131 ret\n\
132 \n\
133 .subsections_via_symbols\n",
134 &unused_obj,
135 )
136 .expect("assemble archive determinism unused fixture");
137
138 let archive_path = root.join("libhelpers.a");
139 if let Err(error) = archive(&[helper_a_obj, helper_b_obj, unused_obj], &archive_path) {
140 eprintln!("skipping: archive failed: {error}");
141 let _ = fs::remove_dir_all(root);
142 return;
143 }
144
145 assert_repeated_links_identical(vec![main_obj, archive_path], &root, "archive");
146
147 let _ = fs::remove_dir_all(root);
148 }
149
150 #[test]
151 fn relocation_workers_match_single_worker_for_many_atoms() {
152 if !have_xcrun() || !have_xcrun_tool("as") {
153 eprintln!("skipping: xcrun as unavailable");
154 return;
155 }
156
157 let root = unique_temp_dir("reloc-workers").expect("create relocation worker temp dir");
158 let text_obj = root.join("text.o");
159 let data_obj = root.join("data.o");
160
161 let mut asm = String::from(
162 "\
163 .section __TEXT,__text,regular,pure_instructions\n\
164 .globl _main\n\
165 _main:\n",
166 );
167 for index in 0..64 {
168 asm.push_str(&format!(" bl _helper_{index}\n"));
169 }
170 asm.push_str(
171 "\
172 adrp x8, _value@GOTPAGE\n\
173 ldr x8, [x8, _value@GOTPAGEOFF]\n\
174 ldr w0, [x8]\n\
175 ret\n\
176 \n",
177 );
178 for index in 0..64 {
179 asm.push_str(&format!(
180 "\
181 .globl _helper_{index}\n\
182 _helper_{index}:\n\
183 adrp x9, _value@GOTPAGE\n\
184 ldr x9, [x9, _value@GOTPAGEOFF]\n\
185 ldr w9, [x9]\n\
186 ret\n\
187 \n"
188 ));
189 }
190 asm.push_str(" .subsections_via_symbols\n");
191
192 assemble(&asm, &text_obj).expect("assemble relocation worker text fixture");
193 assemble(
194 "\
195 .section __DATA,__data\n\
196 .globl _value\n\
197 .p2align 2\n\
198 _value:\n\
199 .long 11\n\
200 \n\
201 .subsections_via_symbols\n",
202 &data_obj,
203 )
204 .expect("assemble relocation worker data fixture");
205
206 let inputs = vec![text_obj, data_obj];
207 let serial =
208 link_once_with_jobs(&inputs, &root, "reloc-workers-serial", Some(1)).expect("serial link");
209 let parallel = link_once_with_jobs(&inputs, &root, "reloc-workers-parallel", Some(8))
210 .expect("parallel link");
211 assert_eq!(
212 parallel, serial,
213 "parallel relocation workers changed final output bytes"
214 );
215
216 let _ = fs::remove_dir_all(root);
217 }
218
219 fn assert_repeated_links_identical(inputs: Vec<PathBuf>, root: &Path, label: &str) {
220 let baseline = link_once(&inputs, root, &format!("{label}-baseline"))
221 .expect("baseline deterministic link");
222 let serial = link_once_with_jobs(&inputs, root, &format!("{label}-serial"), Some(1))
223 .expect("single-worker deterministic link");
224 assert_eq!(
225 serial, baseline,
226 "{label}: single-worker link differed from default parallel link"
227 );
228 let run_count = determinism_run_count();
229 let jobs = determinism_jobs(run_count);
230 let queue = Arc::new(Mutex::new((0..run_count).collect::<VecDeque<_>>()));
231 let errors = Arc::new(Mutex::new(Vec::new()));
232
233 thread::scope(|scope| {
234 for _ in 0..jobs {
235 let queue = Arc::clone(&queue);
236 let errors = Arc::clone(&errors);
237 let baseline = baseline.clone();
238 let inputs = inputs.clone();
239 scope.spawn(move || loop {
240 let Some(index) = queue
241 .lock()
242 .expect("determinism queue mutex poisoned")
243 .pop_front()
244 else {
245 break;
246 };
247 match link_once(&inputs, root, &format!("{label}-run-{index:03}")) {
248 Ok(bytes) if bytes == baseline => {}
249 Ok(bytes) => errors
250 .lock()
251 .expect("determinism errors mutex poisoned")
252 .push(format!(
253 "run {index} differed: baseline={} bytes, output={} bytes",
254 baseline.len(),
255 bytes.len()
256 )),
257 Err(error) => errors
258 .lock()
259 .expect("determinism errors mutex poisoned")
260 .push(format!("run {index} failed: {error}")),
261 }
262 });
263 }
264 });
265
266 let errors = errors
267 .lock()
268 .expect("determinism errors mutex poisoned")
269 .clone();
270 assert!(
271 errors.is_empty(),
272 "parallel deterministic links diverged:\n{}",
273 errors.join("\n")
274 );
275 }
276
277 fn link_once(inputs: &[PathBuf], root: &Path, run_name: &str) -> Result<Vec<u8>, String> {
278 link_once_with_jobs(inputs, root, run_name, None)
279 }
280
281 fn link_once_with_jobs(
282 inputs: &[PathBuf],
283 root: &Path,
284 run_name: &str,
285 jobs: Option<usize>,
286 ) -> Result<Vec<u8>, String> {
287 let dir = root.join(run_name);
288 fs::create_dir_all(&dir).map_err(|e| format!("create {}: {e}", dir.display()))?;
289 let out = dir.join("deterministic.out");
290 let opts = LinkOptions {
291 inputs: inputs.to_vec(),
292 output: Some(out.clone()),
293 kind: OutputKind::Executable,
294 jobs,
295 ..LinkOptions::default()
296 };
297 Linker::run(&opts).map_err(|e| format!("link {}: {e}", out.display()))?;
298 fs::read(&out).map_err(|e| format!("read {}: {e}", out.display()))
299 }
300
301 fn archive(objects: &[PathBuf], out: &Path) -> Result<(), String> {
302 let output = Command::new("libtool")
303 .arg("-static")
304 .arg("-o")
305 .arg(out)
306 .args(objects)
307 .output()
308 .map_err(|e| format!("spawn libtool: {e}"))?;
309 if !output.status.success() {
310 return Err(format!(
311 "libtool failed: {}",
312 String::from_utf8_lossy(&output.stderr)
313 ));
314 }
315 Ok(())
316 }
317
318 fn determinism_run_count() -> usize {
319 std::env::var("AFS_LD_DETERMINISM_RUNS")
320 .ok()
321 .and_then(|raw| raw.parse::<usize>().ok())
322 .filter(|runs| *runs > 0)
323 .unwrap_or(DEFAULT_RUNS)
324 }
325
326 fn determinism_jobs(run_count: usize) -> usize {
327 std::env::var("AFS_LD_DETERMINISM_JOBS")
328 .ok()
329 .and_then(|raw| raw.parse::<usize>().ok())
330 .filter(|jobs| *jobs > 0)
331 .unwrap_or_else(|| {
332 thread::available_parallelism()
333 .map(usize::from)
334 .unwrap_or(1)
335 })
336 .min(run_count)
337 .max(1)
338 }
339
340 fn unique_temp_dir(name: &str) -> Result<PathBuf, String> {
341 let stamp = SystemTime::now()
342 .duration_since(UNIX_EPOCH)
343 .map_err(|e| format!("clock error: {e}"))?
344 .as_nanos();
345 let dir = std::env::temp_dir().join(format!("afs-ld-{name}-{}-{stamp}", std::process::id()));
346 fs::create_dir_all(&dir).map_err(|e| format!("create {}: {e}", dir.display()))?;
347 Ok(dir)
348 }
349