Skip to main content

vyre_libs/security/
mod.rs

1//! Security / taint compositions for program-analysis pipelines.
2//!
3//! Each op registers via `inventory::submit!(OpEntry { … })` and
4//! exports a `fn(...) -> Program`. Program-analysis lowerers emit
5//! against these stable paths directly.
6//!
7//! All security ops compose GPU-parallel graph algorithms over the
8//! vyre IR: forward / backward reachability, dominator walks, and
9//! taint propagation with sanitizer masking.
10//!
11//! ## Module re-export rule
12//!
13//! Every `pub mod foo` in this file re-exports its primary entry
14//! point as `pub use foo::foo;` at parent, alphabetized below.
15//! Callers reach a primitive by `vyre_libs::security::foo(...)`
16//! without learning the file layout. The single intentional
17//! exception is `topology::match_order`  -  per
18//! AUDIT_CLAUDE_2026-04-24 F7, the `match_order` symbol must be
19//! imported from `vyre_libs::range_ordering::match_order`; the
20//! `#[deprecated]` shim in `topology.rs` is a soft-landing for
21//! out-of-tree callers and is intentionally NOT re-exported here
22//! so its deprecation warning fires.
23//!
24//! `flow_composition` is `pub(crate)` because its helpers
25//! (`fuse_security_flow`, `dataflow_hit_program`,
26//! `sanitized_dataflow_hit_program`) are internal building blocks
27//! the public primitives compose; consumers should reach them only
28//! through a stable public op.
29
30macro_rules! define_bitset_and_security_op {
31    (
32        $module:ident,
33        $function:ident,
34        $marker:ident,
35        $op_id:literal,
36        $left:ident,
37        $right:ident,
38        $doc:literal,
39        tests { $($test_name:ident: ($lhs:expr, $rhs:expr) => $expected:expr;)+ }
40    ) => {
41        #[doc = $doc]
42        pub mod $module {
43            use vyre::ir::Program;
44            use vyre_primitives::bitset::and::bitset_and;
45            use vyre_primitives::graph::csr_forward_traverse::bitset_words;
46
47            pub(crate) const OP_ID: &str = $op_id;
48
49            /// Build the canonical security bitset-intersection program.
50            #[must_use]
51            pub fn $function(
52                node_count: u32,
53                $left: &str,
54                $right: &str,
55                out: &str,
56            ) -> Program {
57                let words = bitset_words(node_count);
58                crate::region::tag_program(OP_ID, bitset_and($left, $right, out, words))
59            }
60
61            /// CPU oracle for this security bitset-intersection predicate.
62            #[must_use]
63            #[cfg(test)]
64            pub(crate) fn cpu_ref($left: &[u32], $right: &[u32]) -> Vec<u32> {
65                vyre_primitives::bitset::and::cpu_ref($left, $right)
66            }
67
68            #[doc = concat!("Soundness marker for [`", stringify!($function), "`].")]
69            pub struct $marker;
70
71            impl vyre::soundness::SoundnessTagged for $marker {
72                fn soundness(&self) -> vyre::soundness::Soundness {
73                    vyre::soundness::Soundness::Exact
74                }
75            }
76
77            #[cfg(test)]
78            mod tests {
79                use super::*;
80
81                $(
82                    #[test]
83                    fn $test_name() {
84                        assert_eq!(cpu_ref($lhs, $rhs), $expected);
85                    }
86                )+
87            }
88        }
89    };
90}
91
92macro_rules! define_bitset_and_not_security_op {
93    (
94        $module:ident,
95        $function:ident,
96        $marker:ident,
97        $op_id:literal,
98        $left:ident,
99        $right:ident,
100        $doc:literal,
101        tests { $($test_name:ident: ($lhs:expr, $rhs:expr) => $expected:expr;)+ }
102    ) => {
103        #[doc = $doc]
104        pub mod $module {
105            use vyre::ir::Program;
106            use vyre_primitives::bitset::and_not::bitset_and_not;
107            use vyre_primitives::graph::csr_forward_traverse::bitset_words;
108
109            pub(crate) const OP_ID: &str = $op_id;
110
111            /// Build the canonical security bitset-subtraction program.
112            #[must_use]
113            pub fn $function(
114                node_count: u32,
115                $left: &str,
116                $right: &str,
117                out: &str,
118            ) -> Program {
119                let words = bitset_words(node_count);
120                crate::region::tag_program(OP_ID, bitset_and_not($left, $right, out, words))
121            }
122
123            /// CPU oracle for this security bitset-subtraction predicate.
124            #[must_use]
125            #[cfg(test)]
126            pub(crate) fn cpu_ref($left: &[u32], $right: &[u32]) -> Vec<u32> {
127                vyre_primitives::bitset::and_not::cpu_ref($left, $right)
128            }
129
130            #[doc = concat!("Soundness marker for [`", stringify!($function), "`].")]
131            pub struct $marker;
132
133            impl vyre::soundness::SoundnessTagged for $marker {
134                fn soundness(&self) -> vyre::soundness::Soundness {
135                    vyre::soundness::Soundness::Exact
136                }
137            }
138
139            #[cfg(test)]
140            mod tests {
141                use super::*;
142
143                $(
144                    #[test]
145                    fn $test_name() {
146                        assert_eq!(cpu_ref($lhs, $rhs), $expected);
147                    }
148                )+
149            }
150        }
151    };
152}
153
154pub mod aliases_dataflow;
155define_bitset_and_security_op!(
156    auth_check_dominates,
157    auth_check_dominates,
158    AuthCheckDominates,
159    "vyre-libs::security::auth_check_dominates",
160    auth_doms,
161    sensitive_op_set,
162    "`auth_check_dominates` - authorization check dominates sensitive operation.",
163    tests {
164        protected_op_returns_set: (&[0b1100], &[0b0100]) => vec![0b0100];
165        unprotected_op_returns_empty: (&[0b0001], &[0b1110]) => vec![0];
166        no_sensitive_ops: (&[0xFFFF], &[0]) => vec![0];
167        no_auth_checks: (&[0], &[0xFFFF]) => vec![0];
168    }
169);
170pub mod bounded_by_comparison;
171define_bitset_and_security_op!(
172    buffer_size_check,
173    buffer_size_check,
174    BufferSizeCheck,
175    "vyre-libs::security::buffer_size_check",
176    size_compared,
177    user_input_set,
178    "`buffer_size_check` - buffer size is compared to user input.",
179    tests {
180        checked_size_returns_set: (&[0b1010], &[0b1100]) => vec![0b1000];
181        unchecked_size_returns_empty: (&[0b0001], &[0b1110]) => vec![0];
182        no_user_input_yields_empty: (&[0xFFFF], &[0]) => vec![0];
183        full_overlap: (&[0xDEAD], &[0xDEAD]) => vec![0xDEAD];
184    }
185);
186mod catalog;
187pub mod dominator_tree;
188pub mod facts;
189pub(crate) mod flow_composition;
190pub mod flows_to;
191pub mod flows_to_to_sink;
192pub mod flows_to_with_sanitizer;
193// `external_ifds` is an INCOMPLETE integration: it `use`s a crate
194// `external_dataflow_engine` that is wired into no Cargo.toml and exists nowhere
195// on the tree, so it does not compile under `--features security` and broke every
196// downstream consumer the moment a cache invalidation forced a
197// vyre-libs rebuild. Gated behind `cfg(feature = "external_ifds_engine")`, which
198// is deliberately NOT a Cargo feature: the engine crate depends on the vyre
199// platform, and `xtask platform-boundary` forbids the platform from depending
200// back on a consumer, so this bridge cannot compile here at all. The cfg is
201// declared to the compiler in the workspace lint table so no build warns, and
202// nothing can turn it on. The bridge belongs on the consumer side, which is
203// BACKLOG R47. Gating it keeps the workspace building WITHOUT deleting the WIP. To finish the
204// integration: add the `external_dataflow_engine` crate to the workspace + this
205// crate's deps, then restore these guards to `#[cfg(feature = "security")]`.
206#[cfg(feature = "external_ifds_engine")]
207pub mod external_ifds;
208define_bitset_and_not_security_op!(
209    format_string_check,
210    format_string_check,
211    FormatStringCheck,
212    "vyre-libs::security::format_string_check",
213    format_arg_pts,
214    non_literal_set,
215    "`format_string_check` - format argument is reachable only from literals.",
216    tests {
217        literal_only_returns_full: (&[0xFFFF], &[0]) => vec![0xFFFF];
218        user_input_present_subtracts: (&[0xFFFF], &[0xFF00]) => vec![0x00FF];
219        fully_user_input_returns_empty: (&[0xDEAD], &[0xFFFF]) => vec![0];
220        distributes: (&[0xFFFF, 0x0F0F], &[0xFF00, 0x0000]) => vec![0x00FF, 0x0F0F];
221    }
222);
223pub mod integer_overflow_arith;
224pub mod label_by_family;
225define_bitset_and_security_op!(
226    lock_dominates,
227    lock_dominates,
228    LockDominates,
229    "vyre-libs::security::lock_dominates",
230    lock_doms,
231    shared_access_set,
232    "`lock_dominates` - lock acquisition dominates shared-state access.",
233    tests {
234        locked_access: (&[0b1110], &[0b0010]) => vec![0b0010];
235        unlocked_access: (&[0b0001], &[0b0010]) => vec![0];
236        no_accesses: (&[0xFFFF], &[0]) => vec![0];
237        empty_lock_set: (&[0], &[0xFFFF]) => vec![0];
238    }
239);
240define_bitset_and_security_op!(
241    path_canonical,
242    path_canonical,
243    PathCanonical,
244    "vyre-libs::security::path_canonical",
245    canonicalizer_dominates,
246    fs_op_set,
247    "`path_canonical` - path string was canonicalized before a filesystem operation.",
248    tests {
249        canonicalized_op: (&[0b1110], &[0b0010]) => vec![0b0010];
250        uncanonicalized_op: (&[0b0001], &[0b0010]) => vec![0];
251        no_fs_ops: (&[0xFFFF], &[0]) => vec![0];
252        distributes: (&[0xFF00, 0x00FF], &[0xFFFF, 0xFFFF]) => vec![0xFF00, 0x00FF];
253    }
254);
255pub mod path_reconstruct;
256pub mod predicate_catalog;
257pub mod relation_analyzer;
258pub mod reporter;
259pub mod sanitized_by;
260define_bitset_and_security_op!(
261    sanitizer_dominates,
262    sanitizer_dominates,
263    SanitizerDominates,
264    "vyre-libs::security::sanitizer_dominates",
265    sanitizer_doms,
266    sink_set,
267    "`sanitizer_dominates` - sanitizer dominates the queried sink.",
268    tests {
269        dominated_sink_returns_set: (&[0b1111], &[0b0010]) => vec![0b0010];
270        non_dominated_sink_returns_empty: (&[0b0001], &[0b0010]) => vec![0];
271        no_sinks_returns_empty: (&[0xFFFF], &[0]) => vec![0];
272        distributes_per_word: (&[0xFF00, 0x00FF], &[0x0FF0, 0x0FF0]) => vec![0x0F00, 0x00F0];
273    }
274);
275pub mod sink_intersection;
276define_bitset_and_security_op!(
277    sql_param_bound,
278    sql_param_bound,
279    SqlParamBound,
280    "vyre-libs::security::sql_param_bound",
281    param_binding_set,
282    sql_query_set,
283    "`sql_param_bound` - SQL query is built through parameter binding.",
284    tests {
285        parameterized_query: (&[0b1100], &[0b0100]) => vec![0b0100];
286        raw_concat_query: (&[0b0001], &[0b0010]) => vec![0];
287        no_queries: (&[0xFFFF], &[0]) => vec![0];
288        distributes: (&[0xFF00, 0xF0F0], &[0x0FF0, 0x0F0F]) => vec![0x0F00, 0x0000];
289    }
290);
291pub mod taint_flow;
292pub mod taint_kill;
293pub mod taint_pollution;
294pub mod topology;
295define_bitset_and_not_security_op!(
296    unchecked_return,
297    unchecked_return,
298    UncheckedReturn,
299    "vyre-libs::security::unchecked_return",
300    use_set,
301    check_dominates,
302    "`unchecked_return` - sensitive return-value use lacks a dominating check.",
303    tests {
304        use_without_check_returns_set: (&[0b1100], &[0b0001]) => vec![0b1100];
305        use_with_dominating_check_returns_empty: (&[0b0010], &[0b0010]) => vec![0];
306        no_uses_returns_empty: (&[0], &[0xFFFF]) => vec![0];
307        distributes: (&[0xFFFF, 0x0F0F], &[0x00FF, 0xF000]) => vec![0xFF00, 0x0F0F];
308    }
309);
310define_bitset_and_security_op!(
311    xss_escape,
312    xss_escape,
313    XssEscape,
314    "vyre-libs::security::xss_escape",
315    escape_dominates,
316    render_set,
317    "`xss_escape` - HTML output escaping dominates render sites.",
318    tests {
319        escaped_render: (&[0b1100], &[0b0100]) => vec![0b0100];
320        unescaped_render: (&[0b0001], &[0b0010]) => vec![0];
321        no_renders: (&[0xFFFF], &[0]) => vec![0];
322        no_escape_dominators: (&[0], &[0xFFFF]) => vec![0];
323    }
324);
325
326pub use aliases_dataflow::{aliases_dataflow, try_aliases_dataflow};
327pub use auth_check_dominates::auth_check_dominates;
328pub use bounded_by_comparison::bounded_by_comparison;
329pub use buffer_size_check::buffer_size_check;
330pub use dominator_tree::dominator_tree;
331pub use facts::{
332    AnalysisFact, AnalysisFactColumns, AnalysisFactError, AnalysisFactTable, AnalysisSourceSpan,
333    FactId, FactKind, FindingProofBundle, FindingProofStep,
334};
335pub use flows_to::flows_to;
336pub use flows_to_to_sink::flows_to_to_sink;
337pub use flows_to_with_sanitizer::flows_to_with_sanitizer;
338// Gated off with `external_ifds` above (incomplete integration; missing the
339// `external_dataflow_engine` crate). Restore to `#[cfg(feature = "security")]`
340// once that crate is wired into the workspace.
341#[cfg(feature = "external_ifds_engine")]
342pub use external_ifds::{
343    route_security_taint_through_external_ifds, security_witness_path_from_external_path,
344    ExternalIfdsSecurityBuffers, ExternalIfdsSecurityDispatch, ExternalIfdsSecurityRouteError,
345    SecurityFindingWitnessPath, SecurityWitnessPathError, SecurityWitnessStatement,
346    EXTERNAL_IFDS_SECURITY_BACKEND_ID,
347};
348pub use format_string_check::format_string_check;
349pub use integer_overflow_arith::integer_overflow_arith;
350pub use label_by_family::label_by_family;
351pub use lock_dominates::lock_dominates;
352pub use path_canonical::path_canonical;
353pub use path_reconstruct::path_reconstruct;
354pub use predicate_catalog::{
355    security_predicate_row_by_op_id, security_predicate_rows, try_security_predicate_rows,
356    SecurityPredicateOperation, SecurityPredicateRow,
357};
358pub use relation_analyzer::{
359    generated_relation_finding_fact_ids, run_generated_security_relation_analyzer,
360    GeneratedSecurityRelationAnalyzerEvidence, GeneratedSecurityRelationAnalyzerReport,
361    GeneratedSecurityRelationAnalyzerRunStats, GeneratedSecurityRelationAnalyzerSpec,
362    SecurityRelationAnalyzerError, SecurityRelationQueryFamily,
363    SECURITY_RELATION_ANALYZER_SCHEMA_VERSION,
364};
365pub use reporter::{
366    render_security_reporter_output, SecurityReporterError, SecurityReporterFinding,
367    SecurityReporterOutputBytes, SecurityReporterPlannerPath, SecurityReporterSourceFile,
368    SECURITY_REPORTER_SCHEMA_VERSION,
369};
370pub use sanitized_by::sanitized_by;
371pub use sanitizer_dominates::sanitizer_dominates;
372pub use sink_intersection::sink_intersection;
373pub use sql_param_bound::sql_param_bound;
374pub use taint_flow::taint_flow;
375pub use taint_kill::taint_kill;
376pub use taint_pollution::taint_pollution;
377pub use unchecked_return::unchecked_return;
378pub use xss_escape::xss_escape;
379
380/// Validate that a security composition's input shape + buffer names
381/// are non-degenerate. Panics with a `Fix:` message on violation so
382/// downstream substrate errors don't surface as cryptic OOB indices.
383///
384/// The contract is: every security op rejects degenerate input rather
385/// than building a Program that traps inside the reference interpreter
386/// (or worse, runs to completion and emits silently-wrong taint sets).
387pub(crate) fn assert_security_inputs(op: &str, node_count: u32, buffers: &[(&str, &str)]) {
388    assert!(
389        node_count > 0,
390        "Fix: {op} node_count must be positive; got 0. \
391         A taint analysis over an empty program graph has no meaningful \
392         result  -  callers must skip empty translation units before lowering."
393    );
394    for (role, name) in buffers {
395        assert!(
396            !name.is_empty(),
397            "Fix: {op} requires non-empty buffer name for {role}. \
398             Empty buffer names alias to the zero-length lookup key in the \
399             validator and produce silent miscompiles. Pass a stable \
400             non-empty buffer identifier."
401        );
402    }
403}