use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::path::{Path, PathBuf};
use crate::wasm_op::WasmOp;
pub const SAFE_ACCESSES_SCHEMA: &str = "scry/safe-accesses/v1";
pub const ELISION_ATTESTATION_SCHEMA: &str = "synth-proven-safe-elisions-v1";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SafeSite {
pub func: u32,
pub pc: u32,
#[serde(default)]
pub op: String,
pub width: u32,
}
#[derive(Debug, Clone, Deserialize)]
struct RawDocument {
schema: String,
#[serde(default)]
scry_version: String,
module_sha256: String,
memory_min_bytes: u64,
#[serde(default)]
proven_safe: Vec<SafeSite>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ProvenSafeIngest {
pub accepted: bool,
pub refusal: Option<String>,
pub scry_version: String,
pub declared_module_sha256: String,
pub actual_module_sha256: String,
pub declared_memory_min_bytes: u64,
pub offered: Vec<SafeSite>,
pub diagnostics: Vec<String>,
}
impl ProvenSafeIngest {
pub fn refused(reason: impl Into<String>) -> Self {
Self::refuse(reason)
}
fn refuse(reason: impl Into<String>) -> Self {
let reason = reason.into();
Self {
accepted: false,
diagnostics: vec![reason.clone()],
refusal: Some(reason),
..Self::default()
}
}
pub fn offered_for_func(&self, func: u32) -> Vec<&SafeSite> {
let mut v: Vec<&SafeSite> = self.offered.iter().filter(|s| s.func == func).collect();
v.sort_by_key(|s| s.pc);
v
}
pub fn validate_function(
&self,
func: u32,
ops: &[WasmOp],
notes: &mut Vec<String>,
) -> Vec<usize> {
let mut marks = Vec::new();
for site in self.offered_for_func(func) {
let idx = site.pc as usize;
let Some(op) = ops.get(idx) else {
notes.push(format!(
"func {func} pc {} — out of range (function has {} operators); \
entry DROPPED, guard retained. If the producer emitted wasm BYTE \
OFFSETS, the key space is wrong: `pc` is the 0-based OPERATOR index",
site.pc,
ops.len()
));
continue;
};
let Some(actual_width) = access_width(op) else {
notes.push(format!(
"func {func} pc {} — the operator there is {op:?}, not a linear-memory \
access; entry DROPPED, guard retained (claimed op '{}')",
site.pc, site.op
));
continue;
};
if actual_width != site.width {
notes.push(format!(
"func {func} pc {} — declared width {} B disagrees with the decoded \
operator {op:?} ({actual_width} B); entry DROPPED, guard retained",
site.pc, site.width
));
continue;
}
marks.push(idx);
}
marks.sort_unstable();
marks.dedup();
marks
}
}
pub fn access_width(op: &WasmOp) -> Option<u32> {
Some(match op {
WasmOp::I32Load8S { .. }
| WasmOp::I32Load8U { .. }
| WasmOp::I32Store8 { .. }
| WasmOp::I64Load8S { .. }
| WasmOp::I64Load8U { .. }
| WasmOp::I64Store8 { .. } => 1,
WasmOp::I32Load16S { .. }
| WasmOp::I32Load16U { .. }
| WasmOp::I32Store16 { .. }
| WasmOp::I64Load16S { .. }
| WasmOp::I64Load16U { .. }
| WasmOp::I64Store16 { .. } => 2,
WasmOp::I32Load { .. }
| WasmOp::I32Store { .. }
| WasmOp::I64Load32S { .. }
| WasmOp::I64Load32U { .. }
| WasmOp::I64Store32 { .. }
| WasmOp::F32Load { .. }
| WasmOp::F32Store { .. } => 4,
WasmOp::I64Load { .. }
| WasmOp::I64Store { .. }
| WasmOp::F64Load { .. }
| WasmOp::F64Store { .. } => 8,
WasmOp::V128Load { .. } | WasmOp::V128Store { .. } => 16,
_ => return None,
})
}
pub fn ingest(path: &Path, module_bytes: &[u8], memory_min_bytes: u32) -> ProvenSafeIngest {
let actual = hex_sha256(module_bytes);
let text = match std::fs::read_to_string(path) {
Ok(t) => t,
Err(e) => {
return ProvenSafeIngest {
actual_module_sha256: actual,
..ProvenSafeIngest::refuse(format!(
"--proven-safe {}: cannot read the file ({e}); NO bounds guard is \
elided (fail closed) and the compile continues unchanged",
path.display()
))
};
}
};
let doc: RawDocument = match serde_json::from_str(&text) {
Ok(d) => d,
Err(e) => {
return ProvenSafeIngest {
actual_module_sha256: actual,
..ProvenSafeIngest::refuse(format!(
"--proven-safe {}: not a well-formed `{SAFE_ACCESSES_SCHEMA}` document \
({e}); NO bounds guard is elided (fail closed) and the compile \
continues unchanged",
path.display()
))
};
}
};
if doc.schema != SAFE_ACCESSES_SCHEMA {
return ProvenSafeIngest {
actual_module_sha256: actual,
scry_version: doc.scry_version.clone(),
declared_module_sha256: doc.module_sha256.clone(),
declared_memory_min_bytes: doc.memory_min_bytes,
..ProvenSafeIngest::refuse(format!(
"--proven-safe {}: schema is '{}', expected '{SAFE_ACCESSES_SCHEMA}'; \
NO bounds guard is elided (fail closed)",
path.display(),
doc.schema
))
};
}
if !doc.module_sha256.eq_ignore_ascii_case(&actual) {
return ProvenSafeIngest {
actual_module_sha256: actual.clone(),
scry_version: doc.scry_version.clone(),
declared_module_sha256: doc.module_sha256.clone(),
declared_memory_min_bytes: doc.memory_min_bytes,
..ProvenSafeIngest::refuse(format!(
"--proven-safe {}: REFUSED — module_sha256 mismatch. The file was produced \
for {}, but this compile's module hashes to {actual}. Eliding a bounds \
check on a stale analysis is a memory-safety hole, not a stale \
optimization, so NO guard is elided. (The hash covers the bytes handed to \
the decoder — after .wat parsing, after loom, and after the #418 \
arena-bind rewrite — so a module rewrite that shifts operator indices \
lands here too.)",
path.display(),
if doc.module_sha256.is_empty() {
"<empty>"
} else {
&doc.module_sha256
}
))
};
}
if doc.memory_min_bytes != u64::from(memory_min_bytes) {
return ProvenSafeIngest {
actual_module_sha256: actual,
scry_version: doc.scry_version.clone(),
declared_module_sha256: doc.module_sha256.clone(),
declared_memory_min_bytes: doc.memory_min_bytes,
..ProvenSafeIngest::refuse(format!(
"--proven-safe {}: REFUSED — memory_min_bytes disagreement. The verdicts \
were proven against a {} B floor; synth's declared linear-memory minimum \
for this module is {memory_min_bytes} B. The module_sha256 MATCHED, so \
these should be equal — a disagreement means the producer is broken, and \
verdicts from a broken prover are not trusted. NO guard is elided.",
path.display(),
doc.memory_min_bytes
))
};
}
let mut diagnostics = Vec::new();
if doc.proven_safe.is_empty() {
diagnostics.push(format!(
"--proven-safe {}: accepted, but the document proves ZERO access sites — \
nothing to elide (every guard is retained)",
path.display()
));
}
ProvenSafeIngest {
accepted: true,
refusal: None,
scry_version: doc.scry_version,
declared_module_sha256: doc.module_sha256,
actual_module_sha256: actual,
declared_memory_min_bytes: doc.memory_min_bytes,
offered: doc.proven_safe,
diagnostics,
}
}
pub fn hex_sha256(bytes: &[u8]) -> String {
let digest = Sha256::digest(bytes);
let mut s = String::with_capacity(64);
for b in digest {
s.push_str(&format!("{b:02x}"));
}
s
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AttestedElision {
pub func: u32,
pub pc: u32,
pub op: String,
pub width: u32,
pub authority: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ElisionAttestation {
pub schema: String,
pub synth_version: String,
pub scry_version: String,
pub module_sha256: String,
pub declared_module_sha256: String,
pub memory_min_bytes: Option<u32>,
pub declared_memory_min_bytes: u64,
pub safety_bounds: String,
pub accepted: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub refusal: Option<String>,
pub sites_offered: usize,
pub sites_elided: usize,
pub sites_not_elided: usize,
pub elisions: Vec<AttestedElision>,
pub diagnostics: Vec<String>,
}
impl ElisionAttestation {
pub fn to_json(&self) -> String {
serde_json::to_string_pretty(self).expect("ElisionAttestation serializes")
}
pub fn sidecar_path(elf_path: &Path) -> PathBuf {
let mut p = elf_path.to_path_buf();
let stem = elf_path
.file_stem()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| "out".to_string());
p.set_file_name(format!("{stem}.proven-safe-elisions.json"));
p
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn write_tmp(name: &str, body: &str) -> PathBuf {
let dir = std::env::temp_dir().join("proven_safe_901_unit");
std::fs::create_dir_all(&dir).expect("mk tempdir");
let p = dir.join(name);
let mut f = std::fs::File::create(&p).expect("create");
f.write_all(body.as_bytes()).expect("write");
p
}
const MODULE: &[u8] = b"\0asm\x01\0\0\0 pretend this is a module";
fn doc(hash: &str, min: u64, sites: &str) -> String {
format!(
r#"{{ "schema": "scry/safe-accesses/v1", "scry_version": "3.2.4",
"module_sha256": "{hash}", "memory_min_bytes": {min},
"premises": {{ "bounded_memory": true }},
"counts": {{ "access_sites": 9, "proven_safe": 1 }},
"proven_safe": [{sites}] }}"#
)
}
#[test]
fn hash_mismatch_refuses_everything_901() {
let stale = "0".repeat(64);
let p = write_tmp(
"stale.json",
&doc(
&stale,
65536,
r#"{"func":0,"pc":1,"op":"i32.load","width":4}"#,
),
);
let r = ingest(&p, MODULE, 65536);
assert!(!r.accepted, "a stale analysis must never be accepted");
assert!(
r.offered.is_empty(),
"a refused document is not trusted piecemeal"
);
let why = r.refusal.expect("a refusal names its reason");
assert!(why.contains("module_sha256 mismatch"), "{why}");
assert!(why.contains(&stale), "{why}");
assert!(why.contains(&hex_sha256(MODULE)), "{why}");
}
#[test]
fn matching_hash_is_accepted_and_case_insensitive_901() {
let h = hex_sha256(MODULE).to_uppercase();
let p = write_tmp(
"good.json",
&doc(&h, 65536, r#"{"func":0,"pc":1,"op":"i32.load","width":4}"#),
);
let r = ingest(&p, MODULE, 65536);
assert!(r.accepted, "{:?}", r.refusal);
assert_eq!(r.offered.len(), 1);
assert_eq!(r.scry_version, "3.2.4");
}
#[test]
fn one_flipped_module_byte_refuses_901() {
let p = write_tmp(
"flip.json",
&doc(
&hex_sha256(MODULE),
65536,
r#"{"func":0,"pc":1,"op":"i32.load","width":4}"#,
),
);
let mut rewritten = MODULE.to_vec();
rewritten.push(0x00);
let r = ingest(&p, &rewritten, 65536);
assert!(!r.accepted);
assert!(r.refusal.unwrap().contains("module_sha256 mismatch"));
}
#[test]
fn memory_min_bytes_disagreement_refuses_901() {
let p = write_tmp(
"floor.json",
&doc(
&hex_sha256(MODULE),
131072,
r#"{"func":0,"pc":1,"op":"i32.load","width":4}"#,
),
);
let r = ingest(&p, MODULE, 65536);
assert!(!r.accepted);
let why = r.refusal.unwrap();
assert!(why.contains("memory_min_bytes disagreement"), "{why}");
assert!(why.contains("131072") && why.contains("65536"), "{why}");
}
#[test]
fn malformed_missing_and_wrong_schema_all_refuse_without_erroring_901() {
let h = hex_sha256(MODULE);
let cases = vec![
("missing", None),
("garbage.json", Some("this is not json {{{".to_string())),
("empty.json", Some(String::new())),
(
"wrongschema.json",
Some(doc(&h, 65536, "").replace(SAFE_ACCESSES_SCHEMA, "scry/safe-accesses/v2")),
),
(
"nohash.json",
Some(r#"{"schema":"scry/safe-accesses/v1","memory_min_bytes":65536}"#.to_string()),
),
(
"sitegarbage.json",
Some(doc(&h, 65536, r#"{"func":"four","pc":1,"width":4}"#)),
),
];
for (name, body) in cases {
let p = match body {
Some(b) => write_tmp(name, &b),
None => std::env::temp_dir().join("proven_safe_901_unit/definitely-absent.json"),
};
let r = ingest(&p, MODULE, 65536);
assert!(!r.accepted, "'{name}' must not be accepted");
assert!(r.offered.is_empty(), "'{name}' offered sites");
assert!(r.refusal.is_some(), "'{name}' refused without a reason");
assert!(!r.diagnostics.is_empty(), "'{name}' refused silently");
}
}
#[test]
fn unknown_fields_are_tolerated_901() {
let p = write_tmp(
"future.json",
&format!(
r#"{{ "schema": "scry/safe-accesses/v1", "scry_version": "9.9.9",
"module_sha256": "{}", "memory_min_bytes": 65536,
"brand_new_field": {{ "nested": [1,2,3] }},
"proven_safe": [{{"func":0,"pc":1,"op":"i32.load","width":4,
"confidence":"high"}}] }}"#,
hex_sha256(MODULE)
),
);
let r = ingest(&p, MODULE, 65536);
assert!(r.accepted, "{:?}", r.refusal);
assert_eq!(r.offered.len(), 1);
}
#[test]
fn accepted_but_empty_is_diagnosed_901() {
let p = write_tmp("none.json", &doc(&hex_sha256(MODULE), 65536, ""));
let r = ingest(&p, MODULE, 65536);
assert!(r.accepted);
assert!(r.offered.is_empty());
assert!(
r.diagnostics
.iter()
.any(|d| d.contains("ZERO access sites")),
"an accepted-but-vacuous document must say so: {:?}",
r.diagnostics
);
}
fn ops() -> Vec<WasmOp> {
vec![
WasmOp::LocalGet(0), WasmOp::I32Load {
offset: 0,
align: 2,
}, WasmOp::LocalGet(0), WasmOp::I32Load8U {
offset: 1,
align: 0,
}, WasmOp::I32Add, WasmOp::I64Store {
offset: 8,
align: 3,
}, ]
}
#[test]
fn valid_sites_become_marks_901() {
let p = write_tmp(
"marks.json",
&doc(
&hex_sha256(MODULE),
65536,
r#"{"func":0,"pc":5,"op":"i64.store","width":8},
{"func":0,"pc":1,"op":"i32.load","width":4},
{"func":0,"pc":3,"op":"i32.load8_u","width":1}"#,
),
);
let r = ingest(&p, MODULE, 65536);
let mut notes = Vec::new();
assert_eq!(r.validate_function(0, &ops(), &mut notes), vec![1, 3, 5]);
assert!(notes.is_empty(), "{notes:?}");
}
#[test]
fn byte_offsets_instead_of_op_indices_elide_nothing_loudly_901() {
let p = write_tmp(
"byteoffsets.json",
&doc(
&hex_sha256(MODULE),
65536,
r#"{"func":0,"pc":41,"op":"i32.load","width":4},
{"func":0,"pc":137,"op":"i32.load8_u","width":1}"#,
),
);
let r = ingest(&p, MODULE, 65536);
assert!(
r.accepted,
"the FILE is well formed — only the keys are wrong"
);
let mut notes = Vec::new();
assert_eq!(
r.validate_function(0, &ops(), &mut notes),
Vec::<usize>::new()
);
assert_eq!(notes.len(), 2);
assert!(
notes.iter().all(|n| n.contains("out of range")),
"{notes:?}"
);
assert!(notes[0].contains("OPERATOR index"), "{notes:?}");
}
#[test]
fn non_access_operator_is_dropped_901() {
let p = write_tmp(
"nonaccess.json",
&doc(
&hex_sha256(MODULE),
65536,
r#"{"func":0,"pc":4,"op":"i32.load","width":4}"#,
),
);
let mut notes = Vec::new();
let marks = ingest(&p, MODULE, 65536).validate_function(0, &ops(), &mut notes);
assert_eq!(marks, Vec::<usize>::new());
assert!(notes[0].contains("not a linear-memory access"), "{notes:?}");
}
#[test]
fn width_disagreement_is_dropped_901() {
let p = write_tmp(
"width.json",
&doc(
&hex_sha256(MODULE),
65536,
r#"{"func":0,"pc":3,"op":"i32.load","width":4},
{"func":0,"pc":1,"op":"i32.load","width":4}"#,
),
);
let mut notes = Vec::new();
let marks = ingest(&p, MODULE, 65536).validate_function(0, &ops(), &mut notes);
assert_eq!(
marks,
vec![1],
"the sound entry survives, the skewed one does not"
);
assert_eq!(notes.len(), 1);
assert!(
notes[0].contains("disagrees with the decoded operator"),
"{notes:?}"
);
}
#[test]
fn sites_are_keyed_per_function_901() {
let p = write_tmp(
"perfunc.json",
&doc(
&hex_sha256(MODULE),
65536,
r#"{"func":7,"pc":1,"op":"i32.load","width":4}"#,
),
);
let r = ingest(&p, MODULE, 65536);
let mut notes = Vec::new();
assert_eq!(
r.validate_function(0, &ops(), &mut notes),
Vec::<usize>::new()
);
assert!(notes.is_empty());
assert_eq!(r.validate_function(7, &ops(), &mut notes), vec![1]);
}
#[test]
fn access_width_covers_the_bytes_touched_not_the_value_width_901() {
assert_eq!(
access_width(&WasmOp::I64Load32U {
offset: 0,
align: 2
}),
Some(4)
);
assert_eq!(
access_width(&WasmOp::I64Store8 {
offset: 0,
align: 0
}),
Some(1)
);
assert_eq!(
access_width(&WasmOp::I32Load16S {
offset: 0,
align: 1
}),
Some(2)
);
assert_eq!(
access_width(&WasmOp::F64Load {
offset: 0,
align: 3
}),
Some(8)
);
assert_eq!(access_width(&WasmOp::I32Add), None);
assert_eq!(access_width(&WasmOp::LocalGet(0)), None);
}
#[test]
fn attestation_sidecar_path_mirrors_the_safety_manifest_901() {
assert_eq!(
ElisionAttestation::sidecar_path(Path::new("/tmp/foo.elf")),
PathBuf::from("/tmp/foo.proven-safe-elisions.json")
);
assert_eq!(
ElisionAttestation::sidecar_path(Path::new("out")),
PathBuf::from("out.proven-safe-elisions.json")
);
}
#[test]
fn refusal_is_attested_not_hidden_901() {
let a = ElisionAttestation {
schema: ELISION_ATTESTATION_SCHEMA.to_string(),
synth_version: "0.55.0".to_string(),
scry_version: "3.2.4".to_string(),
module_sha256: "aa".repeat(32),
declared_module_sha256: "bb".repeat(32),
memory_min_bytes: Some(65536),
declared_memory_min_bytes: 65536,
safety_bounds: "software".to_string(),
accepted: false,
refusal: Some("module_sha256 mismatch".to_string()),
sites_offered: 8,
sites_elided: 0,
sites_not_elided: 8,
elisions: Vec::new(),
diagnostics: vec!["refused".to_string()],
};
let json = a.to_json();
assert!(json.contains("\"accepted\": false"));
assert!(json.contains("module_sha256 mismatch"));
assert!(json.contains("\"sites_offered\": 8"));
assert!(json.contains("\"sites_elided\": 0"));
let back: ElisionAttestation = serde_json::from_str(&json).expect("round-trips");
assert_eq!(back, a);
}
#[test]
fn no_floor_attests_null_not_zero_rq57() {
let a = ElisionAttestation {
schema: ELISION_ATTESTATION_SCHEMA.to_string(),
synth_version: "0.56.1".to_string(),
scry_version: "3.2.4".to_string(),
module_sha256: "aa".repeat(32),
declared_module_sha256: "aa".repeat(32),
memory_min_bytes: None,
declared_memory_min_bytes: 65536,
safety_bounds: "software".to_string(),
accepted: false,
refusal: Some("no memory floor can be established".to_string()),
sites_offered: 1,
sites_elided: 0,
sites_not_elided: 1,
elisions: Vec::new(),
diagnostics: Vec::new(),
};
let json = a.to_json();
assert!(
json.contains("\"memory_min_bytes\": null"),
"absence must be an explicit null, got:\n{json}"
);
assert!(
!json.contains("\"memory_min_bytes\": 0"),
"the invented-0 floor must be unrepresentable, got:\n{json}"
);
let back: ElisionAttestation = serde_json::from_str(&json).expect("round-trips");
assert_eq!(back, a);
}
}