Skip to main content

vyre_libs/dataflow/
mod.rs

1//! Compatibility facade for shared dataflow soundness contracts.
2//!
3//! `vyre-libs::dataflow` remains as a stable import path for older consumers,
4//! but platform crates must not re-export downstream analysis engines. Concrete
5//! IFDS, SSA, reaching-definition, callgraph, slicing, range, and related
6//! analyses live in their owning engine crates and consume these shared
7//! contracts from `vyre-foundation`.
8
9use serde::{Deserialize, Serialize};
10
11pub use vyre_foundation::soundness::{
12    validate_dynamic_pipeline, validate_dynamic_primitive, validate_pipeline, validate_primitive,
13    DynamicPrimitiveSoundness, DynamicSoundnessViolation, PrecisionContract, PrimitiveSoundness,
14    Soundness, SoundnessTagged, SoundnessViolation,
15};
16
17/// Shared fact-schema version for security, borrowck, and external/Vyre bridges.
18pub const SHARED_FACT_SCHEMA_VERSION: u16 = 1;
19
20/// Cross-engine fact families accepted by the shared dataflow schema.
21#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
22pub enum SharedFactKind {
23    /// Attacker-controlled or analysis source fact.
24    Source,
25    /// Security sink fact.
26    Sink,
27    /// Taint/dataflow reachability fact.
28    Taint,
29    /// Sanitizer or kill-set fact.
30    Sanitizer,
31    /// Program graph edge or call/control edge fact.
32    GraphEdge,
33    /// Rust borrow loan fact.
34    BorrowLoan,
35    /// Rust origin/region fact.
36    BorrowOrigin,
37    /// Rust origin subset/outlives fact.
38    BorrowSubset,
39    /// Dominance or authorization-guard fact.
40    Dominance,
41    /// Numeric range or bounds fact.
42    Range,
43    /// Source-to-sink witness/path fact.
44    Witness,
45}
46
47impl SharedFactKind {
48    /// Stable wire tag used by columnar schemas and release evidence.
49    #[must_use]
50    pub const fn wire_tag(self) -> &'static str {
51        match self {
52            Self::Source => "source",
53            Self::Sink => "sink",
54            Self::Taint => "taint",
55            Self::Sanitizer => "sanitizer",
56            Self::GraphEdge => "graph_edge",
57            Self::BorrowLoan => "borrow_loan",
58            Self::BorrowOrigin => "borrow_origin",
59            Self::BorrowSubset => "borrow_subset",
60            Self::Dominance => "dominance",
61            Self::Range => "range",
62            Self::Witness => "witness",
63        }
64    }
65}
66
67/// Minimal cross-engine fact header.
68#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
69pub struct SharedFactHeader {
70    /// Schema version, currently [`SHARED_FACT_SCHEMA_VERSION`].
71    pub schema_version: u16,
72    /// Producer id such as `c-c11`, `rustc-nll`, or `external-dataflow`.
73    pub producer: String,
74    /// Shared fact family.
75    pub kind: SharedFactKind,
76    /// Stable producer-local fact id.
77    pub fact_id: u64,
78    /// Primary subject id.
79    pub subject: u64,
80    /// Optional object id.
81    pub object: Option<u64>,
82    /// Optional auxiliary id, usually a point, edge kind, or relation id.
83    pub aux: Option<u64>,
84    /// Stable file id, or zero when not source-spanned.
85    pub file_id: u32,
86    /// Start byte offset, inclusive.
87    pub start_byte: u32,
88    /// End byte offset, exclusive.
89    pub end_byte: u32,
90    /// Soundness label for the fact.
91    pub soundness: Soundness,
92}
93
94impl SharedFactHeader {
95    /// Build one shared fact header.
96    #[must_use]
97    pub fn new(
98        producer: impl Into<String>,
99        kind: SharedFactKind,
100        fact_id: u64,
101        subject: u64,
102        soundness: Soundness,
103    ) -> Self {
104        Self {
105            schema_version: SHARED_FACT_SCHEMA_VERSION,
106            producer: producer.into(),
107            kind,
108            fact_id,
109            subject,
110            object: None,
111            aux: None,
112            file_id: 0,
113            start_byte: 0,
114            end_byte: 0,
115            soundness,
116        }
117    }
118
119    /// Attach an object id.
120    #[must_use]
121    pub const fn with_object(mut self, object: u64) -> Self {
122        self.object = Some(object);
123        self
124    }
125
126    /// Attach an auxiliary relation id.
127    #[must_use]
128    pub const fn with_aux(mut self, aux: u64) -> Self {
129        self.aux = Some(aux);
130        self
131    }
132
133    /// Attach a byte span.
134    #[must_use]
135    pub const fn with_span(mut self, file_id: u32, start_byte: u32, end_byte: u32) -> Self {
136        self.file_id = file_id;
137        self.start_byte = start_byte;
138        self.end_byte = end_byte;
139        self
140    }
141
142    /// Render the stable compact header used by schema contract tests.
143    ///
144    /// Absent optional fields use `-` as their sentinel so that `object=None`
145    /// and `object=Some(0)` produce distinct tokens (`object=-` vs `object=0`).
146    /// Since Polonius origin/loan/point ids are dense `u32` starting from 0,
147    /// `Some(0)` is a valid, common value and must not be conflated with absence.
148    #[must_use]
149    pub fn wire_header(&self) -> String {
150        let object_token = self
151            .object
152            .map_or_else(|| "-".to_string(), |v| v.to_string());
153        let aux_token = self.aux.map_or_else(|| "-".to_string(), |v| v.to_string());
154        format!(
155            "schema=v{};producer={};kind={};fact_id={};subject={};object={};aux={};file={};start={};end={};soundness={:?}",
156            self.schema_version,
157            self.producer,
158            self.kind.wire_tag(),
159            self.fact_id,
160            self.subject,
161            object_token,
162            aux_token,
163            self.file_id,
164            self.start_byte,
165            self.end_byte,
166            self.soundness
167        )
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn c_security_source_fact_header_is_exact() {
177        let header =
178            SharedFactHeader::new("c-c11", SharedFactKind::Source, 1, 42, Soundness::Exact)
179                .with_span(7, 100, 120);
180
181        // object and aux are absent (None): wire token is "-", not "0".
182        // "0" is a valid Polonius id (first interned origin/loan) and must not
183        // be conflated with absence.
184        assert_eq!(
185            header.wire_header(),
186            "schema=v1;producer=c-c11;kind=source;fact_id=1;subject=42;object=-;aux=-;file=7;start=100;end=120;soundness=Exact"
187        );
188    }
189
190    #[test]
191    fn rust_borrow_subset_fact_header_is_exact() {
192        let header = SharedFactHeader::new(
193            "rustc-nll",
194            SharedFactKind::BorrowSubset,
195            9,
196            3,
197            Soundness::Exact,
198        )
199        .with_object(5)
200        .with_aux(11);
201
202        assert_eq!(
203            header.wire_header(),
204            "schema=v1;producer=rustc-nll;kind=borrow_subset;fact_id=9;subject=3;object=5;aux=11;file=0;start=0;end=0;soundness=Exact"
205        );
206    }
207
208    /// Regression: `object=None` and `object=Some(0)` must produce distinct wire
209    /// tokens.  Before the fix both produced `object=0`; now they produce
210    /// `object=-` and `object=0` respectively.
211    #[test]
212    fn wire_header_distinguishes_absent_object_from_zero_object() {
213        let no_object = SharedFactHeader::new(
214            "rustc-nll",
215            SharedFactKind::BorrowLoan,
216            1,
217            5,
218            Soundness::Exact,
219        );
220        let object_zero = no_object.clone().with_object(0);
221
222        // Semantic difference must be preserved on the wire.
223        assert_ne!(
224            no_object.wire_header(),
225            object_zero.wire_header(),
226            "wire_header must distinguish object=None from object=Some(0)"
227        );
228        assert!(
229            no_object.wire_header().contains("object=-"),
230            "absent object must encode as 'object=-', got: {}",
231            no_object.wire_header()
232        );
233        assert!(
234            object_zero.wire_header().contains("object=0"),
235            "object=Some(0) must encode as 'object=0', got: {}",
236            object_zero.wire_header()
237        );
238    }
239
240    /// Same injectivity requirement for the aux field.
241    #[test]
242    fn wire_header_distinguishes_absent_aux_from_zero_aux() {
243        let no_aux = SharedFactHeader::new(
244            "rustc-nll",
245            SharedFactKind::BorrowLoan,
246            2,
247            7,
248            Soundness::Exact,
249        );
250        let aux_zero = no_aux.clone().with_aux(0);
251
252        assert_ne!(
253            no_aux.wire_header(),
254            aux_zero.wire_header(),
255            "wire_header must distinguish aux=None from aux=Some(0)"
256        );
257        assert!(
258            no_aux.wire_header().contains("aux=-"),
259            "absent aux must encode as 'aux=-', got: {}",
260            no_aux.wire_header()
261        );
262        assert!(
263            aux_zero.wire_header().contains("aux=0"),
264            "aux=Some(0) must encode as 'aux=0', got: {}",
265            aux_zero.wire_header()
266        );
267    }
268
269    #[test]
270    fn external_witness_fact_header_is_exact() {
271        let header = SharedFactHeader::new(
272            "external-dataflow",
273            SharedFactKind::Witness,
274            13,
275            21,
276            Soundness::Exact,
277        )
278        .with_object(34)
279        .with_aux(55);
280
281        assert_eq!(
282            header.wire_header(),
283            "schema=v1;producer=external-dataflow;kind=witness;fact_id=13;subject=21;object=34;aux=55;file=0;start=0;end=0;soundness=Exact"
284        );
285    }
286}