fortrangoingonforty/afs-ld / 01ed08d

Browse files

Gate deterministic link output

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
01ed08d7ed504f8ae9d92f8d3b673e477fceb2ff
Parents
d1bd8f6
Tree
98336a2

2 changed files

StatusFile+-
M .github/workflows/parity-matrix.yml 3 0
A tests/determinism.rs 147 0
.github/workflows/parity-matrix.ymlmodified
@@ -23,6 +23,9 @@ jobs:
23
       - name: Run parity harness proof tests
23
       - name: Run parity harness proof tests
24
         run: cargo test --test diff_harness_tolerates_known_linkedit --test parity_harness --test parity_canary -- --nocapture
24
         run: cargo test --test diff_harness_tolerates_known_linkedit --test parity_harness --test parity_canary -- --nocapture
25
 
25
 
26
+      - name: Run determinism gate
27
+        run: cargo test --test determinism -- --nocapture
28
+
26
       - name: Run parity matrix
29
       - name: Run parity matrix
27
         env:
30
         env:
28
           PARITY_MATRIX_ARTIFACT_DIR: ${{ github.workspace }}/parity-matrix-artifacts
31
           PARITY_MATRIX_ARTIFACT_DIR: ${{ github.workspace }}/parity-matrix-artifacts
tests/determinism.rsadded
@@ -0,0 +1,147 @@
1
+//! Sprint 28 determinism guardrails.
2
+//!
3
+//! Parallel speedups are only safe if they never perturb the final image. This
4
+//! test repeatedly links one synthetic-section-heavy executable and requires
5
+//! byte-identical 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::sync::{Arc, Mutex};
13
+use std::thread;
14
+use std::time::{SystemTime, UNIX_EPOCH};
15
+
16
+use afs_ld::{LinkOptions, Linker, OutputKind};
17
+use common::harness::{assemble, have_xcrun, have_xcrun_tool};
18
+
19
+const DEFAULT_RUNS: usize = 100;
20
+
21
+#[test]
22
+fn repeated_parallel_links_are_byte_identical() {
23
+    if !have_xcrun() || !have_xcrun_tool("as") {
24
+        eprintln!("skipping: xcrun as unavailable");
25
+        return;
26
+    }
27
+
28
+    let root = unique_temp_dir("determinism").expect("create determinism temp dir");
29
+    let obj = root.join("input.o");
30
+    assemble(
31
+        "\
32
+        .section __TEXT,__text,regular,pure_instructions\n\
33
+        .globl _main\n\
34
+        _main:\n\
35
+            adrp x8, _value@GOTPAGE\n\
36
+            ldr x8, [x8, _value@GOTPAGEOFF]\n\
37
+            ldr w0, [x8]\n\
38
+            ret\n\
39
+\n\
40
+        .section __DATA,__data\n\
41
+        .globl _value\n\
42
+        .p2align 2\n\
43
+        _value:\n\
44
+            .long 7\n\
45
+        .subsections_via_symbols\n",
46
+        &obj,
47
+    )
48
+    .expect("assemble determinism fixture");
49
+
50
+    let baseline = link_once(&obj, &root, "baseline").expect("baseline deterministic link");
51
+    let run_count = determinism_run_count();
52
+    let jobs = determinism_jobs(run_count);
53
+    let queue = Arc::new(Mutex::new((0..run_count).collect::<VecDeque<_>>()));
54
+    let errors = Arc::new(Mutex::new(Vec::new()));
55
+
56
+    thread::scope(|scope| {
57
+        for _ in 0..jobs {
58
+            let queue = Arc::clone(&queue);
59
+            let errors = Arc::clone(&errors);
60
+            let baseline = baseline.clone();
61
+            let root = root.clone();
62
+            let obj = obj.clone();
63
+            scope.spawn(move || loop {
64
+                let Some(index) = queue
65
+                    .lock()
66
+                    .expect("determinism queue mutex poisoned")
67
+                    .pop_front()
68
+                else {
69
+                    break;
70
+                };
71
+                match link_once(&obj, &root, &format!("run-{index:03}")) {
72
+                    Ok(bytes) if bytes == baseline => {}
73
+                    Ok(bytes) => errors
74
+                        .lock()
75
+                        .expect("determinism errors mutex poisoned")
76
+                        .push(format!(
77
+                            "run {index} differed: baseline={} bytes, output={} bytes",
78
+                            baseline.len(),
79
+                            bytes.len()
80
+                        )),
81
+                    Err(error) => errors
82
+                        .lock()
83
+                        .expect("determinism errors mutex poisoned")
84
+                        .push(format!("run {index} failed: {error}")),
85
+                }
86
+            });
87
+        }
88
+    });
89
+
90
+    let errors = errors
91
+        .lock()
92
+        .expect("determinism errors mutex poisoned")
93
+        .clone();
94
+    assert!(
95
+        errors.is_empty(),
96
+        "parallel deterministic links diverged:\n{}",
97
+        errors.join("\n")
98
+    );
99
+
100
+    let _ = fs::remove_dir_all(root);
101
+}
102
+
103
+fn link_once(obj: &Path, root: &Path, run_name: &str) -> Result<Vec<u8>, String> {
104
+    let dir = root.join(run_name);
105
+    fs::create_dir_all(&dir).map_err(|e| format!("create {}: {e}", dir.display()))?;
106
+    let out = dir.join("deterministic.out");
107
+    let opts = LinkOptions {
108
+        inputs: vec![obj.to_path_buf()],
109
+        output: Some(out.clone()),
110
+        kind: OutputKind::Executable,
111
+        ..LinkOptions::default()
112
+    };
113
+    Linker::run(&opts).map_err(|e| format!("link {}: {e}", out.display()))?;
114
+    fs::read(&out).map_err(|e| format!("read {}: {e}", out.display()))
115
+}
116
+
117
+fn determinism_run_count() -> usize {
118
+    std::env::var("AFS_LD_DETERMINISM_RUNS")
119
+        .ok()
120
+        .and_then(|raw| raw.parse::<usize>().ok())
121
+        .filter(|runs| *runs > 0)
122
+        .unwrap_or(DEFAULT_RUNS)
123
+}
124
+
125
+fn determinism_jobs(run_count: usize) -> usize {
126
+    std::env::var("AFS_LD_DETERMINISM_JOBS")
127
+        .ok()
128
+        .and_then(|raw| raw.parse::<usize>().ok())
129
+        .filter(|jobs| *jobs > 0)
130
+        .unwrap_or_else(|| {
131
+            thread::available_parallelism()
132
+                .map(usize::from)
133
+                .unwrap_or(1)
134
+        })
135
+        .min(run_count)
136
+        .max(1)
137
+}
138
+
139
+fn unique_temp_dir(name: &str) -> Result<PathBuf, String> {
140
+    let stamp = SystemTime::now()
141
+        .duration_since(UNIX_EPOCH)
142
+        .map_err(|e| format!("clock error: {e}"))?
143
+        .as_nanos();
144
+    let dir = std::env::temp_dir().join(format!("afs-ld-{name}-{}-{stamp}", std::process::id()));
145
+    fs::create_dir_all(&dir).map_err(|e| format!("create {}: {e}", dir.display()))?;
146
+    Ok(dir)
147
+}