Skip to main content

vyre_libs/security/
mod.rs

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