Skip to main content

vyre_foundation/
soundness.rs

1//! Soundness regime markers for dataflow primitives.
2//!
3//! Rules with zero-FP precision contracts MUST only compose primitives
4//! whose marker is [`Soundness::Exact`], or [`Soundness::MayOver`]
5//! primitives gated by an explicit sanitizer filter downstream.
6//!
7//! Lives in `vyre-foundation` because soundness is a primitive lattice
8//! over IR-level analyses; dataflow engines and composition crates both
9//! consume it. Per the LEGO discipline (consumers call vyre, vyre never calls
10//! consumers) the canonical definition must live in vyre.
11
12/// Soundness regime of a dataflow primitive.
13///
14/// Rules with zero-FP precision contracts MUST only compose primitives
15/// whose marker is `Exact`, or `MayOver` primitives gated by an explicit
16/// sanitizer filter downstream.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19pub enum Soundness {
20    /// Over-approximates: may report taint where none exists. Safe for
21    /// recall-driven rules paired with a downstream filter.
22    MayOver,
23    /// Under-approximates: may miss taint that exists. Safe only when
24    /// the rule semantics explicitly accept false negatives.
25    MustUnder,
26    /// Exact: reports taint iff taint exists on the given CFG. No false
27    /// positives, no false negatives, given a correct input AST.
28    Exact,
29}
30
31/// Precision contract requested by a consumer pipeline.
32///
33/// This is the policy layer above individual primitive markers. A
34/// pipeline that promises zero false positives cannot freely compose
35/// every `MayOver` analysis; it must either stay `Exact` end to end or
36/// prove that a downstream sanitizer filter bounds the over-approximate
37/// primitive before the result escapes.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40pub enum PrecisionContract {
41    /// Results must not contain false positives.
42    ZeroFalsePositive,
43    /// Results must not contain false negatives.
44    RecallDriven,
45    /// The consumer explicitly accepts false negatives.
46    FalseNegativesAccepted,
47}
48
49/// Soundness evidence for one primitive in a composed pipeline.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
51#[cfg_attr(feature = "serde", derive(serde::Serialize))]
52pub struct PrimitiveSoundness {
53    /// Stable primitive id, normally the `vyre_harness::OpEntry::id`.
54    pub op_id: &'static str,
55    /// Primitive soundness marker.
56    pub soundness: Soundness,
57    /// Whether a downstream sanitizer/filter makes a `MayOver` primitive
58    /// safe for a zero-false-positive consumer.
59    pub sanitizer_filter: bool,
60}
61
62/// Serializable soundness evidence for one primitive in a finding or release
63/// artifact.
64#[derive(Debug, Clone, PartialEq, Eq, Hash)]
65#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
66pub struct DynamicPrimitiveSoundness {
67    /// Stable primitive id, normally the `vyre_harness::OpEntry::id`.
68    pub op_id: String,
69    /// Primitive soundness marker.
70    pub soundness: Soundness,
71    /// Whether a downstream sanitizer/filter makes a `MayOver` primitive
72    /// safe for a zero-false-positive consumer.
73    pub sanitizer_filter: bool,
74}
75
76impl PrimitiveSoundness {
77    /// Construct primitive soundness evidence with no sanitizer filter.
78    #[must_use]
79    pub const fn new(op_id: &'static str, soundness: Soundness) -> Self {
80        Self {
81            op_id,
82            soundness,
83            sanitizer_filter: false,
84        }
85    }
86
87    /// Mark this primitive as bounded by an explicit downstream filter.
88    #[must_use]
89    pub const fn with_sanitizer_filter(mut self) -> Self {
90        self.sanitizer_filter = true;
91        self
92    }
93}
94
95impl DynamicPrimitiveSoundness {
96    /// Construct serializable primitive soundness evidence with no sanitizer
97    /// filter.
98    #[must_use]
99    pub fn new(op_id: impl Into<String>, soundness: Soundness) -> Self {
100        Self {
101            op_id: op_id.into(),
102            soundness,
103            sanitizer_filter: false,
104        }
105    }
106
107    /// Mark this primitive as bounded by an explicit downstream filter.
108    #[must_use]
109    pub fn with_sanitizer_filter(mut self) -> Self {
110        self.sanitizer_filter = true;
111        self
112    }
113}
114
115/// Mechanical rejection reason for an invalid soundness composition.
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
117#[cfg_attr(feature = "serde", derive(serde::Serialize))]
118pub struct SoundnessViolation {
119    /// Primitive that violates the requested consumer contract.
120    pub op_id: &'static str,
121    /// Primitive soundness marker.
122    pub soundness: Soundness,
123    /// Consumer policy that rejected the primitive.
124    pub contract: PrecisionContract,
125    /// Human-readable fix direction.
126    pub fix: &'static str,
127}
128
129/// Mechanical rejection reason for an invalid dynamic soundness composition.
130#[derive(Debug, Clone, PartialEq, Eq, Hash)]
131#[cfg_attr(feature = "serde", derive(serde::Serialize))]
132pub struct DynamicSoundnessViolation {
133    /// Primitive that violates the requested consumer contract.
134    pub op_id: String,
135    /// Primitive soundness marker.
136    pub soundness: Soundness,
137    /// Consumer policy that rejected the primitive.
138    pub contract: PrecisionContract,
139    /// Human-readable fix direction.
140    pub fix: &'static str,
141}
142
143impl Soundness {
144    /// Conservative join of two soundness markers.
145    ///
146    /// The join is the least precise soundness that soundly describes
147    /// the composition of two primitives.
148    #[must_use]
149    pub const fn join(self, other: Soundness) -> Soundness {
150        match (self, other) {
151            (Soundness::MayOver, _) | (_, Soundness::MayOver) => Soundness::MayOver,
152            (Soundness::MustUnder, Soundness::MustUnder) => Soundness::MustUnder,
153            (Soundness::MustUnder, Soundness::Exact) | (Soundness::Exact, Soundness::MustUnder) => {
154                Soundness::MustUnder
155            }
156            (Soundness::Exact, Soundness::Exact) => Soundness::Exact,
157        }
158    }
159}
160
161/// Validate that a primitive pipeline can satisfy a consumer precision
162/// contract, returning the composed soundness marker on success.
163///
164/// This is intentionally conservative. A `ZeroFalsePositive` pipeline
165/// rejects `MustUnder` because under-approximation can hide required
166/// sanitizer evidence, and rejects unfiltered `MayOver` because that
167/// can leak false positives to the consumer. A `RecallDriven` pipeline
168/// rejects `MustUnder` because false negatives break recall.
169///
170/// # Errors
171///
172/// Returns [`SoundnessViolation`] when any primitive is incompatible with the
173/// requested `contract`.
174pub fn validate_pipeline(
175    contract: PrecisionContract,
176    primitives: &[PrimitiveSoundness],
177) -> Result<Soundness, SoundnessViolation> {
178    let mut joined = Soundness::Exact;
179    for primitive in primitives {
180        validate_primitive(contract, *primitive)?;
181        joined = joined.join(primitive.soundness);
182    }
183    Ok(joined)
184}
185
186/// Validate a serializable primitive pipeline against a consumer precision
187/// contract, returning the composed soundness marker on success.
188///
189/// This has the same semantics as [`validate_pipeline`] but accepts owned
190/// primitive ids for finding evidence, release artifacts, and decoded manifests.
191///
192/// # Errors
193///
194/// Returns [`DynamicSoundnessViolation`] when any primitive is incompatible
195/// with the requested `contract`.
196pub fn validate_dynamic_pipeline(
197    contract: PrecisionContract,
198    primitives: &[DynamicPrimitiveSoundness],
199) -> Result<Soundness, DynamicSoundnessViolation> {
200    let mut joined = Soundness::Exact;
201    for primitive in primitives {
202        validate_dynamic_primitive(contract, primitive)?;
203        joined = joined.join(primitive.soundness);
204    }
205    Ok(joined)
206}
207
208/// Validate one primitive against a consumer precision contract.
209///
210/// # Errors
211///
212/// Returns [`SoundnessViolation`] when `primitive` cannot soundly satisfy
213/// `contract`.
214pub fn validate_primitive(
215    contract: PrecisionContract,
216    primitive: PrimitiveSoundness,
217) -> Result<(), SoundnessViolation> {
218    match violation_fix(contract, primitive.soundness, primitive.sanitizer_filter) {
219        None => Ok(()),
220        Some(fix) => Err(SoundnessViolation {
221            op_id: primitive.op_id,
222            soundness: primitive.soundness,
223            contract,
224            fix,
225        }),
226    }
227}
228
229/// Validate one serializable primitive against a consumer precision contract.
230///
231/// # Errors
232///
233/// Returns [`DynamicSoundnessViolation`] when `primitive` cannot soundly
234/// satisfy `contract`.
235pub fn validate_dynamic_primitive(
236    contract: PrecisionContract,
237    primitive: &DynamicPrimitiveSoundness,
238) -> Result<(), DynamicSoundnessViolation> {
239    match violation_fix(contract, primitive.soundness, primitive.sanitizer_filter) {
240        None => Ok(()),
241        Some(fix) => Err(DynamicSoundnessViolation {
242            op_id: primitive.op_id.clone(),
243            soundness: primitive.soundness,
244            contract,
245            fix,
246        }),
247    }
248}
249
250fn violation_fix(
251    contract: PrecisionContract,
252    soundness: Soundness,
253    sanitizer_filter: bool,
254) -> Option<&'static str> {
255    match (contract, soundness, sanitizer_filter) {
256        (PrecisionContract::ZeroFalsePositive, Soundness::Exact, _)
257        | (PrecisionContract::ZeroFalsePositive, Soundness::MayOver, true)
258        | (PrecisionContract::RecallDriven, Soundness::Exact | Soundness::MayOver, _)
259        | (PrecisionContract::FalseNegativesAccepted, _, _) => None,
260        (PrecisionContract::ZeroFalsePositive, Soundness::MayOver, false) => {
261            Some("Fix: add an explicit sanitizer filter or use only Exact primitives.")
262        }
263        (PrecisionContract::ZeroFalsePositive, Soundness::MustUnder, _) => {
264            Some("Fix: zero-FP pipelines require Exact primitives or filtered MayOver primitives.")
265        }
266        (PrecisionContract::RecallDriven, Soundness::MustUnder, _) => {
267            Some("Fix: recall-driven pipelines cannot include under-approximating primitives.")
268        }
269    }
270}
271
272/// Trait for types that carry a soundness marker.
273pub trait SoundnessTagged {
274    /// Return the soundness regime of this primitive.
275    fn soundness(&self) -> Soundness;
276}
277
278#[cfg(test)]
279mod tests {
280    use super::{
281        validate_dynamic_pipeline, DynamicPrimitiveSoundness, PrecisionContract, Soundness,
282    };
283
284    #[test]
285    fn dynamic_pipeline_rejects_zero_false_positive_unfiltered_mayover() {
286        let error = validate_dynamic_pipeline(
287            PrecisionContract::ZeroFalsePositive,
288            &[DynamicPrimitiveSoundness::new(
289                "vyre-libs::security::flows_to",
290                Soundness::MayOver,
291            )],
292        )
293        .expect_err("unfiltered MayOver must not satisfy zero false positive contracts");
294
295        assert_eq!(error.op_id, "vyre-libs::security::flows_to");
296        assert_eq!(error.soundness, Soundness::MayOver);
297        assert_eq!(error.contract, PrecisionContract::ZeroFalsePositive);
298        assert!(error.fix.contains("explicit sanitizer filter"));
299    }
300
301    #[test]
302    fn dynamic_pipeline_accepts_zero_false_positive_filtered_mayover() {
303        let soundness = validate_dynamic_pipeline(
304            PrecisionContract::ZeroFalsePositive,
305            &[DynamicPrimitiveSoundness::new(
306                "vyre-libs::security::flows_to_with_sanitizer",
307                Soundness::MayOver,
308            )
309            .with_sanitizer_filter()],
310        )
311        .expect("filtered MayOver should satisfy zero false positive contracts");
312
313        assert_eq!(soundness, Soundness::MayOver);
314    }
315}