use crate::spec::types::DataType;
use crate::spec::types::OpSpec;
use crate::spec::OverflowContract;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OverflowFinding {
MissingContract {
op_id: String,
carries_integer_input: bool,
carries_integer_output: bool,
},
ContractViolation {
op_id: String,
declared: OverflowContract,
observed_behavior: ObservedBehavior,
probe_label: &'static str,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ObservedBehavior {
Wrapped,
Saturated,
Checked,
Unrecognized,
}
impl ObservedBehavior {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Wrapped => "wrapped",
Self::Saturated => "saturated",
Self::Checked => "checked",
Self::Unrecognized => "unrecognized",
}
}
#[must_use]
pub const fn satisfies(self, contract: OverflowContract) -> bool {
matches!(
(self, contract),
(Self::Wrapped, OverflowContract::Wrapping)
| (Self::Saturated, OverflowContract::Saturating)
| (Self::Checked, OverflowContract::Checked)
| (_, OverflowContract::Unchecked)
)
}
}
impl std::fmt::Display for OverflowFinding {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MissingContract {
op_id,
carries_integer_input,
carries_integer_output,
} => write!(
f,
"{op_id}: missing overflow_contract. inputs={} output={}. \
Fix: add `.overflow_contract(OverflowContract::Wrapping|Saturating|Checked|Unchecked)` \
to the OpSpec builder.",
carries_integer_input, carries_integer_output,
),
Self::ContractViolation {
op_id,
declared,
observed_behavior,
probe_label,
} => write!(
f,
"{op_id}: declared {declared}, observed {} on probe `{probe_label}`. \
Fix: either change the declared contract to match the \
CPU reference behavior, or change the CPU reference to \
match the declaration.",
observed_behavior.name(),
),
}
}
}
#[must_use]
#[inline]
pub fn run(specs: &[OpSpec]) -> Vec<OverflowFinding> {
let mut findings = Vec::new();
for spec in specs {
let integer_input = has_integer_type(&spec.signature.inputs);
let integer_output = is_integer_type(&spec.signature.output);
if !integer_input && !integer_output {
continue;
}
let Some(declared) = spec.overflow_contract else {
findings.push(OverflowFinding::MissingContract {
op_id: spec.id.to_string(),
carries_integer_input: integer_input,
carries_integer_output: integer_output,
});
continue;
};
if matches!(declared, OverflowContract::Unchecked) {
continue;
}
findings.extend(probe(spec, declared));
}
findings
}
fn probe(spec: &OpSpec, declared: OverflowContract) -> Vec<OverflowFinding> {
let mut findings = Vec::new();
if spec.signature.inputs.len() != 2 {
return findings;
}
let mut input = Vec::with_capacity(8);
input.extend_from_slice(&u32::MAX.to_le_bytes());
input.extend_from_slice(&1u32.to_le_bytes());
let output = (spec.cpu_fn)(&input);
if output.len() != 4 {
return findings;
}
let mut word = [0u8; 4];
word.copy_from_slice(&output[..4]);
let value = u32::from_le_bytes(word);
let observed = match value {
0 => ObservedBehavior::Wrapped,
u32::MAX => ObservedBehavior::Saturated,
_ => return findings,
};
if !observed.satisfies(declared) {
findings.push(OverflowFinding::ContractViolation {
op_id: spec.id.to_string(),
declared,
observed_behavior: observed,
probe_label: "max_plus_one",
});
}
findings
}
fn has_integer_type(types: &[DataType]) -> bool {
types.iter().any(is_integer_type)
}
fn is_integer_type(ty: &DataType) -> bool {
matches!(ty, DataType::U32 | DataType::I32 | DataType::U64)
}
pub struct OverflowContractEnforcer;
impl crate::enforce::EnforceGate for OverflowContractEnforcer {
fn id(&self) -> &'static str {
"overflow_contract"
}
fn name(&self) -> &'static str {
"overflow_contract"
}
fn run(&self, ctx: &crate::enforce::EnforceCtx<'_>) -> Vec<crate::enforce::Finding> {
let findings = run(ctx.specs);
let messages = findings
.into_iter()
.map(|finding| finding.to_string())
.collect::<Vec<_>>();
crate::enforce::finding_result(self.id(), messages)
}
}
pub const REGISTERED: OverflowContractEnforcer = OverflowContractEnforcer;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn probe_missing_overflow_contracts() {
let specs = crate::spec::op_registry::all_specs();
let findings = run(&specs);
let missing: Vec<_> = findings
.iter()
.filter(|f| matches!(f, OverflowFinding::MissingContract { .. }))
.collect();
for f in &missing {
eprintln!("MISSING: {f:?}");
}
assert!(
missing.is_empty(),
"Fix: {} registered integer ops still lack an overflow contract",
missing.len()
);
}
use crate::spec::types::conform::Strictness;
use crate::spec::types::OpSpec;
use crate::spec::types::{DataType, OpSignature};
use crate::spec::AlgebraicLaw;
use vyre_spec::Category;
fn base_builder(
id: &'static str,
inputs: Vec<DataType>,
output: DataType,
cpu: fn(&[u8]) -> Vec<u8>,
) -> OpSpec {
OpSpec::builder(id)
.signature(OpSignature { inputs, output })
.cpu_fn(cpu)
.wgsl_fn(|| "fn main() {}".to_string())
.category(Category::A {
composition_of: vec![id],
})
.laws(vec![AlgebraicLaw::Bounded {
lo: 0,
hi: u32::MAX,
}])
.strictness(Strictness::Strict)
.version(1)
.build()
.unwrap()
}
fn wrapping_add_cpu(input: &[u8]) -> Vec<u8> {
if input.len() < 8 {
return Vec::new();
}
let a = u32::from_le_bytes(input[..4].try_into().unwrap());
let b = u32::from_le_bytes(input[4..8].try_into().unwrap());
a.wrapping_add(b).to_le_bytes().to_vec()
}
fn saturating_add_cpu(input: &[u8]) -> Vec<u8> {
if input.len() < 8 {
return Vec::new();
}
let a = u32::from_le_bytes(input[..4].try_into().unwrap());
let b = u32::from_le_bytes(input[4..8].try_into().unwrap());
a.saturating_add(b).to_le_bytes().to_vec()
}
fn non_integer_cpu(input: &[u8]) -> Vec<u8> {
input.to_vec()
}
#[test]
fn integer_op_with_no_contract_is_flagged() {
let spec = base_builder(
"test.integer.no_contract",
vec![DataType::U32, DataType::U32],
DataType::U32,
wrapping_add_cpu,
);
let findings = run(&[spec]);
assert!(
findings
.iter()
.any(|finding| matches!(finding, OverflowFinding::MissingContract { op_id, .. } if op_id == "test.integer.no_contract")),
"expected MissingContract: {findings:?}"
);
}
#[test]
fn non_integer_op_is_ignored_regardless_of_contract() {
let spec = base_builder(
"test.bytes.identity",
vec![DataType::Bytes],
DataType::Bytes,
non_integer_cpu,
);
let findings = run(&[spec]);
assert!(findings.is_empty(), "{findings:?}");
}
#[test]
fn correctly_declared_wrapping_passes_probes() {
let spec = OpSpec::builder("test.wrapping_add")
.signature(OpSignature {
inputs: vec![DataType::U32, DataType::U32],
output: DataType::U32,
})
.cpu_fn(wrapping_add_cpu)
.wgsl_fn(|| "fn main() {}".to_string())
.category(Category::A {
composition_of: vec!["test.wrapping_add"],
})
.laws(vec![AlgebraicLaw::Bounded {
lo: 0,
hi: u32::MAX,
}])
.strictness(Strictness::Strict)
.version(1)
.overflow_contract(OverflowContract::Wrapping)
.build()
.unwrap();
let findings = run(&[spec]);
assert!(
findings.is_empty(),
"wrapping contract + wrapping cpu_fn should be quiet: {findings:?}"
);
}
#[test]
fn correctly_declared_saturating_passes_probes() {
let spec = OpSpec::builder("test.saturating_add")
.signature(OpSignature {
inputs: vec![DataType::U32, DataType::U32],
output: DataType::U32,
})
.cpu_fn(saturating_add_cpu)
.wgsl_fn(|| "fn main() {}".to_string())
.category(Category::A {
composition_of: vec!["test.saturating_add"],
})
.laws(vec![AlgebraicLaw::Bounded {
lo: 0,
hi: u32::MAX,
}])
.strictness(Strictness::Strict)
.version(1)
.overflow_contract(OverflowContract::Saturating)
.build()
.unwrap();
let findings = run(&[spec]);
assert!(
findings.is_empty(),
"saturating contract + saturating cpu_fn should be quiet: {findings:?}"
);
}
#[test]
fn declared_wrapping_but_cpu_saturates_fires_violation() {
let spec = OpSpec::builder("test.liar")
.signature(OpSignature {
inputs: vec![DataType::U32, DataType::U32],
output: DataType::U32,
})
.cpu_fn(saturating_add_cpu)
.wgsl_fn(|| "fn main() {}".to_string())
.category(Category::A {
composition_of: vec!["test.liar"],
})
.laws(vec![AlgebraicLaw::Bounded {
lo: 0,
hi: u32::MAX,
}])
.strictness(Strictness::Strict)
.version(1)
.overflow_contract(OverflowContract::Wrapping)
.build()
.unwrap();
let findings = run(&[spec]);
assert!(
findings.iter().any(|finding| matches!(
finding,
OverflowFinding::ContractViolation {
declared: OverflowContract::Wrapping,
..
}
)),
"saturating cpu_fn declared as Wrapping must violate: {findings:?}"
);
}
#[test]
fn unchecked_declaration_skips_probes() {
let spec = OpSpec::builder("test.unchecked_add")
.signature(OpSignature {
inputs: vec![DataType::U32, DataType::U32],
output: DataType::U32,
})
.cpu_fn(saturating_add_cpu)
.wgsl_fn(|| "fn main() {}".to_string())
.category(Category::A {
composition_of: vec!["test.unchecked_add"],
})
.laws(vec![AlgebraicLaw::Bounded {
lo: 0,
hi: u32::MAX,
}])
.strictness(Strictness::Strict)
.version(1)
.overflow_contract(OverflowContract::Unchecked)
.build()
.unwrap();
let findings = run(&[spec]);
assert!(findings.is_empty(), "{findings:?}");
}
#[test]
fn observed_behavior_satisfies_table() {
for (observed, contract, expected) in [
(ObservedBehavior::Wrapped, OverflowContract::Wrapping, true),
(
ObservedBehavior::Wrapped,
OverflowContract::Saturating,
false,
),
(
ObservedBehavior::Saturated,
OverflowContract::Saturating,
true,
),
(
ObservedBehavior::Saturated,
OverflowContract::Wrapping,
false,
),
(ObservedBehavior::Checked, OverflowContract::Checked, true),
(ObservedBehavior::Checked, OverflowContract::Wrapping, false),
(ObservedBehavior::Wrapped, OverflowContract::Unchecked, true),
(
ObservedBehavior::Unrecognized,
OverflowContract::Unchecked,
true,
),
(
ObservedBehavior::Unrecognized,
OverflowContract::Wrapping,
false,
),
] {
assert_eq!(
observed.satisfies(contract),
expected,
"mismatch for {observed:?} vs {contract:?}"
);
}
}
#[test]
fn display_finding_is_actionable() {
let finding = OverflowFinding::MissingContract {
op_id: "test.mystery".to_string(),
carries_integer_input: true,
carries_integer_output: true,
};
let rendered = format!("{finding}");
assert!(rendered.contains("test.mystery"), "{rendered}");
assert!(rendered.contains("Fix:"), "{rendered}");
assert!(rendered.contains("overflow_contract"), "{rendered}");
}
}