use std::collections::HashMap;
pub const MAX_ACCESS_BYTES: u32 = 8;
#[derive(Clone, Debug)]
pub struct DataSegment {
pub linmem_off: u32,
pub bytes: Vec<u8>,
}
#[derive(Clone, Debug)]
pub struct RelocResolution {
pub seg_index: usize,
pub addend: u32,
pub label: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Verdict {
Consistent,
Mismatch(Vec<AddrMismatch>),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AddrMismatch {
pub label: String,
pub seg_index: usize,
pub addend: u32,
pub access_addr: u32,
pub served: u8,
pub runtime: u8,
pub span_byte: u32,
}
impl AddrMismatch {
pub fn describe(&self) -> String {
let span = if self.span_byte == 0 {
String::new()
} else {
format!(
" (span byte +{} of a possibly {}-byte access)",
self.span_byte, MAX_ACCESS_BYTES
)
};
format!(
"{}: __synth_wasm_seg_{}+0x{:x} -> linmem 0x{:x}{span} serves 0x{:02x} but \
the runtime image (segments applied later-wins) owns 0x{:02x}",
self.label, self.seg_index, self.addend, self.access_addr, self.served, self.runtime
)
}
}
fn runtime_image(segments: &[DataSegment]) -> HashMap<u32, u8> {
let mut mem = HashMap::new();
for seg in segments {
for (j, &b) in seg.bytes.iter().enumerate() {
mem.insert(seg.linmem_off + j as u32, b);
}
}
mem
}
pub fn validate_reloc_resolutions(
segments: &[DataSegment],
resolutions: &[RelocResolution],
) -> Verdict {
let runtime = runtime_image(segments);
let mut bad = Vec::new();
for r in resolutions {
let Some(seg) = segments.get(r.seg_index) else {
bad.push(AddrMismatch {
label: r.label.clone(),
seg_index: r.seg_index,
addend: r.addend,
access_addr: 0,
served: 0,
runtime: 0,
span_byte: 0,
});
continue;
};
let access_addr = seg.linmem_off + r.addend;
let Some(&served) = seg.bytes.get(r.addend as usize) else {
bad.push(AddrMismatch {
label: r.label.clone(),
seg_index: r.seg_index,
addend: r.addend,
access_addr,
served: 0,
runtime: 0,
span_byte: 0,
});
continue;
};
let Some(&runtime_byte) = runtime.get(&access_addr) else {
bad.push(AddrMismatch {
label: r.label.clone(),
seg_index: r.seg_index,
addend: r.addend,
access_addr,
served,
runtime: 0,
span_byte: 0,
});
continue;
};
if served != runtime_byte {
bad.push(AddrMismatch {
label: r.label.clone(),
seg_index: r.seg_index,
addend: r.addend,
access_addr,
served,
runtime: runtime_byte,
span_byte: 0,
});
}
}
if bad.is_empty() {
Verdict::Consistent
} else {
Verdict::Mismatch(bad)
}
}
pub fn resolve_owner(segments: &[DataSegment], c: u32, last_wins: bool) -> Option<RelocResolution> {
let hit = |(off, len): (u32, usize)| c >= off && c < off + len as u32;
let idx = if last_wins {
segments
.iter()
.rposition(|s| hit((s.linmem_off, s.bytes.len())))
} else {
segments
.iter()
.position(|s| hit((s.linmem_off, s.bytes.len())))
}?;
Some(RelocResolution {
seg_index: idx,
addend: c - segments[idx].linmem_off,
label: format!("addr 0x{c:x}"),
})
}
#[derive(Clone, Debug)]
pub struct PackedInit<'a> {
pub seg_packed_off: &'a [u32],
pub bytes: &'a [u8],
}
pub fn validate_reloc_resolutions_spanned(
segments: &[DataSegment],
resolutions: &[RelocResolution],
packed: &PackedInit<'_>,
) -> Verdict {
let runtime = runtime_image(segments);
let mut bad = Vec::new();
if let Verdict::Mismatch(m) = validate_reloc_resolutions(segments, resolutions) {
bad.extend(m);
}
for r in resolutions {
let Some(seg) = segments.get(r.seg_index) else {
continue; };
let Some(&poff) = packed.seg_packed_off.get(r.seg_index) else {
continue; };
for j in 0..MAX_ACCESS_BYTES {
let access_addr = seg.linmem_off.wrapping_add(r.addend).wrapping_add(j);
let Some(&runtime_byte) = runtime.get(&access_addr) else {
continue;
};
if j == 0 && seg.bytes.get(r.addend as usize) != Some(&runtime_byte) {
continue;
}
let p = poff as usize + r.addend as usize + j as usize;
let served = packed.bytes.get(p).copied();
if served != Some(runtime_byte) {
bad.push(AddrMismatch {
label: r.label.clone(),
seg_index: r.seg_index,
addend: r.addend,
access_addr,
served: served.unwrap_or(0),
runtime: runtime_byte,
span_byte: j,
});
}
}
}
if bad.is_empty() {
Verdict::Consistent
} else {
Verdict::Mismatch(bad)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ImageVerdict {
Consistent,
Mismatch(Vec<ImageMismatch>),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ImageMismatch {
pub addr: u32,
pub served: u8,
pub runtime: u8,
}
impl ImageMismatch {
pub fn describe(&self) -> String {
format!(
"linmem 0x{:x} serves 0x{:02x} but the runtime image (segments \
applied later-wins) owns 0x{:02x}",
self.addr, self.served, self.runtime
)
}
}
pub fn image_extent(segments: &[DataSegment]) -> u64 {
segments
.iter()
.map(|s| s.linmem_off as u64 + s.bytes.len() as u64)
.max()
.unwrap_or(0)
}
pub fn pack_rom_image(segments: &[DataSegment], last_wins: bool) -> Vec<u8> {
let mut blob = vec![0u8; image_extent(segments) as usize];
let place = |blob: &mut Vec<u8>, s: &DataSegment| {
let at = s.linmem_off as usize;
blob[at..at + s.bytes.len()].copy_from_slice(&s.bytes);
};
if last_wins {
for s in segments {
place(&mut blob, s);
}
} else {
for s in segments.iter().rev() {
place(&mut blob, s);
}
}
blob
}
pub fn validate_served_image(segments: &[DataSegment], image: &[u8]) -> ImageVerdict {
let runtime = runtime_image(segments);
let mut bad = Vec::new();
for (addr, &served) in image.iter().enumerate() {
let owed = runtime.get(&(addr as u32)).copied().unwrap_or(0);
if served != owed {
bad.push(ImageMismatch {
addr: addr as u32,
served,
runtime: owed,
});
}
}
let mut beyond: Vec<(u32, u8)> = runtime
.into_iter()
.filter(|&(addr, owed)| addr as u64 >= image.len() as u64 && owed != 0)
.collect();
beyond.sort_unstable();
for (addr, owed) in beyond {
bad.push(ImageMismatch {
addr,
served: 0,
runtime: owed,
});
}
if bad.is_empty() {
ImageVerdict::Consistent
} else {
ImageVerdict::Mismatch(bad)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn overlapping_segments() -> Vec<DataSegment> {
vec![
DataSegment {
linmem_off: 0x100000,
bytes: vec![0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x20, 0xAA, 0xBB],
},
DataSegment {
linmem_off: 0x100000,
bytes: vec![0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x10],
},
DataSegment {
linmem_off: 0x100000,
bytes: b"gust:os up\n".to_vec(),
},
]
}
#[test]
fn red_on_first_match_green_on_last_match() {
let segs = overlapping_segments();
let c = 0x100008;
assert_eq!(segs[0].bytes[8], 0xAA); assert_eq!(segs[2].bytes[8], b'u');
let wrong = resolve_owner(&segs, c, false).unwrap();
assert_eq!(wrong.seg_index, 0, "first-match must pick seg_0");
let red = validate_reloc_resolutions(&segs, std::slice::from_ref(&wrong));
match red {
Verdict::Mismatch(m) => {
assert_eq!(m.len(), 1);
assert_eq!(m[0].seg_index, 0);
assert_eq!(m[0].access_addr, c);
assert_eq!(m[0].served, 0xAA, "seg_0 serves the stale byte");
assert_eq!(m[0].runtime, b'u', "runtime image (seg_2) owns 'u'");
}
Verdict::Consistent => {
panic!("VACUOUS: validator accepted the #757 wrong-segment resolution")
}
}
let right = resolve_owner(&segs, c, true).unwrap();
assert_eq!(right.seg_index, 2, "last-match must pick seg_2");
assert_eq!(
validate_reloc_resolutions(&segs, std::slice::from_ref(&right)),
Verdict::Consistent,
"the runtime-correct resolution must pass"
);
}
#[test]
fn non_overlapping_both_policies_consistent() {
let segs = vec![
DataSegment {
linmem_off: 0x1000,
bytes: vec![1, 2, 3, 4],
},
DataSegment {
linmem_off: 0x2000,
bytes: vec![5, 6, 7, 8],
},
];
for &c in &[0x1002u32, 0x2003] {
let a = resolve_owner(&segs, c, false).unwrap();
let b = resolve_owner(&segs, c, true).unwrap();
assert_eq!(a.seg_index, b.seg_index);
assert_eq!(validate_reloc_resolutions(&segs, &[a]), Verdict::Consistent);
assert_eq!(validate_reloc_resolutions(&segs, &[b]), Verdict::Consistent);
}
}
#[test]
fn partial_overlap_tail_wins() {
let segs = vec![
DataSegment {
linmem_off: 0x100,
bytes: vec![0x10, 0x11, 0x12, 0x13, 0x14, 0x15],
},
DataSegment {
linmem_off: 0x104,
bytes: vec![0xF4, 0xF5, 0xF6, 0xF7],
},
];
let c = 0x104; let wrong = resolve_owner(&segs, c, false).unwrap();
assert_eq!(wrong.seg_index, 0);
assert!(matches!(
validate_reloc_resolutions(&segs, &[wrong]),
Verdict::Mismatch(_)
));
let right = resolve_owner(&segs, c, true).unwrap();
assert_eq!(right.seg_index, 1);
assert_eq!(
validate_reloc_resolutions(&segs, &[right]),
Verdict::Consistent
);
let head = resolve_owner(&segs, 0x100, false).unwrap();
assert_eq!(head.seg_index, 0);
assert_eq!(
validate_reloc_resolutions(&segs, &[head]),
Verdict::Consistent
);
}
fn mixed_pack(segments: &[DataSegment]) -> (Vec<u32>, Vec<u8>) {
let mut offs = Vec::with_capacity(segments.len());
let mut cur = 0u32;
for s in segments {
cur = cur.next_multiple_of(4);
offs.push(cur);
cur += s.bytes.len() as u32;
}
let mut blob = vec![0u8; cur as usize];
for (s, &o) in segments.iter().zip(offs.iter()) {
blob[o as usize..o as usize + s.bytes.len()].copy_from_slice(&s.bytes);
}
(offs, blob)
}
#[test]
fn phase1_green_but_span_red_on_staggered_overlap() {
let segs = vec![
DataSegment {
linmem_off: 0x10004,
bytes: vec![0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7],
},
DataSegment {
linmem_off: 0x10008,
bytes: vec![0xB0, 0xB1, 0xB2, 0xB3],
},
];
let r = resolve_owner(&segs, 0x10006, true).unwrap();
assert_eq!(r.seg_index, 0, "correct owner of the addend byte is seg_0");
assert_eq!(
validate_reloc_resolutions(&segs, std::slice::from_ref(&r)),
Verdict::Consistent,
"phase 1 must accept the addend byte (it IS runtime-correct)"
);
let (offs, blob) = mixed_pack(&segs);
let packed = PackedInit {
seg_packed_off: &offs,
bytes: &blob,
};
match validate_reloc_resolutions_spanned(&segs, std::slice::from_ref(&r), &packed) {
Verdict::Mismatch(m) => {
assert_eq!(m[0].span_byte, 2, "first divergent byte is +2");
assert_eq!(m[0].access_addr, 0x10008);
assert_eq!(m[0].served, 0xA4, "packed seg_0 serves its stale byte");
assert_eq!(m[0].runtime, 0xB0, "runtime image owns seg_1's byte");
}
Verdict::Consistent => {
panic!("VACUOUS: spanned validator accepted a straddling stale-tail access")
}
}
}
#[test]
fn span_green_on_adjacency_preserving_crossing() {
let segs = vec![
DataSegment {
linmem_off: 0x100,
bytes: vec![1, 2, 3, 4],
},
DataSegment {
linmem_off: 0x104,
bytes: vec![5, 6, 7, 8],
},
];
let (offs, blob) = mixed_pack(&segs);
let packed = PackedInit {
seg_packed_off: &offs,
bytes: &blob,
};
let r = resolve_owner(&segs, 0x102, true).unwrap();
assert_eq!(r.seg_index, 0);
assert_eq!(
validate_reloc_resolutions_spanned(&segs, &[r], &packed),
Verdict::Consistent,
"packed adjacency == linmem adjacency: the crossing serves the right bytes"
);
}
#[test]
fn span_red_on_padding_shifted_crossing() {
let segs = vec![
DataSegment {
linmem_off: 0x100,
bytes: vec![1, 2, 3], },
DataSegment {
linmem_off: 0x103,
bytes: vec![5, 6, 7, 8],
},
];
let (offs, blob) = mixed_pack(&segs);
let packed = PackedInit {
seg_packed_off: &offs,
bytes: &blob,
};
let r = resolve_owner(&segs, 0x101, true).unwrap();
assert_eq!(r.seg_index, 0);
match validate_reloc_resolutions_spanned(&segs, &[r], &packed) {
Verdict::Mismatch(m) => {
assert_eq!(m[0].span_byte, 2);
assert_eq!(m[0].access_addr, 0x103);
assert_eq!(m[0].served, 0, "the pack serves 4-align padding");
assert_eq!(m[0].runtime, 5);
}
Verdict::Consistent => panic!("VACUOUS: padding-shifted crossing accepted"),
}
}
#[test]
fn span_green_on_sparse_tail() {
let segs = vec![
DataSegment {
linmem_off: 0x100,
bytes: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
},
DataSegment {
linmem_off: 0x400,
bytes: vec![0xFF; 4],
},
];
let (offs, blob) = mixed_pack(&segs);
let packed = PackedInit {
seg_packed_off: &offs,
bytes: &blob,
};
let r = resolve_owner(&segs, 0x108, true).unwrap();
assert_eq!(r.seg_index, 0);
assert_eq!(
validate_reloc_resolutions_spanned(&segs, &[r], &packed),
Verdict::Consistent,
"uncovered span bytes are implicit-zero linmem — must not false-red"
);
}
#[test]
fn span_red_on_init_region_escape() {
let segs = vec![
DataSegment {
linmem_off: 0x100,
bytes: vec![0x11; 8], },
DataSegment {
linmem_off: 0x100,
bytes: vec![0x22; 4], },
];
let (offs, blob) = mixed_pack(&segs);
assert_eq!(blob.len(), 12, "seg_1 is the final packed segment");
let packed = PackedInit {
seg_packed_off: &offs,
bytes: &blob,
};
let r = resolve_owner(&segs, 0x102, true).unwrap();
assert_eq!(r.seg_index, 1);
match validate_reloc_resolutions_spanned(&segs, &[r], &packed) {
Verdict::Mismatch(m) => {
assert_eq!(m[0].span_byte, 2);
assert_eq!(m[0].access_addr, 0x104);
assert_eq!(m[0].runtime, 0x11);
}
Verdict::Consistent => panic!("VACUOUS: init-region escape accepted"),
}
}
#[test]
fn span_red_on_blob_fill_corruption_at_addend_byte() {
let segs = vec![DataSegment {
linmem_off: 0x100,
bytes: vec![1, 2, 3, 4],
}];
let (offs, mut blob) = mixed_pack(&segs);
let r = resolve_owner(&segs, 0x102, true).unwrap();
assert_eq!(
validate_reloc_resolutions(&segs, std::slice::from_ref(&r)),
Verdict::Consistent,
"phase 1 (segment bytes) cannot see a blob-fill bug"
);
blob[2] = 0xEE; let packed = PackedInit {
seg_packed_off: &offs,
bytes: &blob,
};
match validate_reloc_resolutions_spanned(&segs, std::slice::from_ref(&r), &packed) {
Verdict::Mismatch(m) => {
assert_eq!(m[0].span_byte, 0);
assert_eq!(m[0].served, 0xEE);
assert_eq!(m[0].runtime, 3);
}
Verdict::Consistent => panic!("VACUOUS: corrupted shipped blob accepted"),
}
}
#[test]
fn rom_image_red_on_first_wins_green_on_last_wins() {
let segs = overlapping_segments();
let wrong = pack_rom_image(&segs, false);
match validate_served_image(&segs, &wrong) {
ImageVerdict::Mismatch(m) => {
let at8 = m.iter().find(|x| x.addr == 0x100008).expect("addr 8");
assert_eq!(at8.served, 0xAA);
assert_eq!(at8.runtime, b'u');
}
ImageVerdict::Consistent => {
panic!("VACUOUS: dense-image validator accepted a first-wins pack")
}
}
let right = pack_rom_image(&segs, true);
assert_eq!(
validate_served_image(&segs, &right),
ImageVerdict::Consistent,
"declaration-order (later-wins) pack must validate"
);
}
#[test]
fn rom_image_gap_garbage_red_zero_tail_green() {
let segs = vec![
DataSegment {
linmem_off: 0,
bytes: vec![1, 2],
},
DataSegment {
linmem_off: 8,
bytes: vec![0, 0, 0, 0],
},
];
let mut img = pack_rom_image(&segs, true);
img[4] = 0xCC;
assert!(matches!(
validate_served_image(&segs, &img),
ImageVerdict::Mismatch(_)
));
assert_eq!(
validate_served_image(&segs, &[1, 2]),
ImageVerdict::Consistent
);
}
#[test]
fn zero_served_image_red_on_nonzero_green_on_zeroed() {
let nonzero = vec![DataSegment {
linmem_off: 16,
bytes: vec![1, 2, 3, 4],
}];
match validate_served_image(&nonzero, &[]) {
ImageVerdict::Mismatch(m) => {
assert_eq!(m[0].addr, 16);
assert_eq!(m[0].served, 0);
assert_eq!(m[0].runtime, 1);
}
ImageVerdict::Consistent => panic!("VACUOUS: dropped nonzero initializer accepted"),
}
let zeroed = vec![
DataSegment {
linmem_off: 16,
bytes: vec![1, 2, 3, 4],
},
DataSegment {
linmem_off: 16,
bytes: vec![0, 0, 0, 0],
},
];
assert_eq!(
validate_served_image(&zeroed, &[]),
ImageVerdict::Consistent
);
}
#[test]
fn out_of_range_is_mismatch() {
let segs = vec![DataSegment {
linmem_off: 0,
bytes: vec![1, 2, 3],
}];
let bad = RelocResolution {
seg_index: 0,
addend: 99,
label: "oob".into(),
};
assert!(matches!(
validate_reloc_resolutions(&segs, &[bad]),
Verdict::Mismatch(_)
));
}
}