1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
//! VCR-MEM-001 (#707) — co-rebase N aliased `__stack_pointer` globals.
//!
//! A `meld fuse --memory shared` multi-provider node keeps each fused component's
//! own `__stack_pointer` global, so the module carries N mutable i32 globals ALL
//! initialized to the same stack top (sp_init). They alias the ONE shared
//! reservation. Layer-1 (#383) re-based the *unique* global whose `init ==
//! sp_init` and REFUSED when more than one matched ("could not uniquely identify
//! the shadow-stack global"). That refusal gated the gust:os multi-provider OS
//! node (app + time-provider + log-provider = 3 SP globals, all init 0x100000).
//!
//! #707 re-bases the whole equivalence class: every mutable global with `init ==
//! sp_init` slides to the shrunk budget (they point into the same stack). A
//! single-SP module has exactly one match, so this is byte-identical there — the
//! #383 tests still pin that path. The execution differential
//! (`scripts/repro/multi_sp_707_differential.py`) confirms each co-rebased stack
//! roundtrips correctly and restores its own slot to the budget; gale's on-silicon
//! reflash on the real fused node is the final gate.
use std::path::PathBuf;
use std::process::Command;
// #977 RQ-59-FRESHNESS: nothing here parses an artifact until the artifact is
// proven to be THIS invocation's output — see `artifact_guard`. The readers
// below take BYTES, not a path, so no read can outlive its compile.
mod artifact_guard;
use object::{Object, ObjectSection};
fn synth() -> &'static str {
env!("CARGO_BIN_EXE_synth")
}
fn fixture() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../..")
.join("scripts/repro/mem707_multi_sp.wat")
}
fn compile(extra: &[&str], tag: &str) -> Vec<u8> {
let fx = fixture();
// #977: unique per call + remove-first + status/exists/non-empty guards.
let out = artifact_guard::unique_artifact(tag, "o");
let mut args = vec![
"compile",
fx.to_str().unwrap(),
"--target",
"cortex-m3",
"--native-pointer-abi",
"--all-exports",
"--relocatable",
"-o",
out.to_str().unwrap(),
];
args.extend_from_slice(extra);
let mut cmd = Command::new(synth());
cmd.args(&args);
artifact_guard::compile_bytes_or_panic(&mut cmd, &out, tag)
}
fn bss_size(data: &[u8]) -> u64 {
let obj = object::File::parse(data).expect("parse ELF");
obj.sections()
.find(|s| s.name() == Ok(".bss"))
.expect(".bss present")
.size()
}
/// The materialized global slots in `.data`, decoded as little-endian i32 words.
/// The fixture has three mutable SP globals + one immutable constant ⇒ four
/// 4-byte slots (slots 0..2 = sp0/sp1/sp2, slot 3 = the immutable `$konst`).
fn global_slots(bytes: &[u8]) -> Vec<i32> {
let obj = object::File::parse(bytes).expect("parse ELF");
let data = obj
.section_by_name(".data")
.expect(".data present")
.data()
.expect(".data bytes");
data.as_chunks::<4>()
.0
.iter()
.map(|&w| i32::from_le_bytes(w))
.collect()
}
/// RED before the fix / GREEN after: the fused node with THREE `__stack_pointer`
/// globals (all init 4096) was refused ("could not uniquely identify"); #707
/// co-rebases all three to the budget and shrinks the reservation.
#[test]
fn all_aliased_sp_globals_rebase_707() {
let bytes = compile(&["--shadow-stack-size", "512"], "mem707_test");
// Every MUTABLE global whose init == sp_init (the three SP globals) re-bases
// to 512; the immutable `$konst` (slot 3) — which merely COINCIDES with
// sp_init — must stay 4096. Re-basing it would corrupt a program constant.
assert_eq!(
global_slots(&bytes),
vec![512, 512, 512, 4096],
"the three mutable SP globals co-rebase; the immutable constant is untouched"
);
// Reservation shrinks 4096 → budget 512 (no static tail above sp_init).
assert_eq!(bss_size(&bytes), 512);
}
/// Opt-in / frozen-safe: WITHOUT the flag the same node keeps all three SP slots
/// at their declared top and the full reservation (byte-identical to pre-#707).
#[test]
fn no_flag_leaves_multi_sp_full_707() {
let bytes = compile(&[], "mem707_noflag_test");
assert_eq!(
global_slots(&bytes),
vec![4096, 4096, 4096, 4096],
"no flag must leave every slot at its declared value"
);
assert_eq!(bss_size(&bytes), 4096);
}