| 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 |