Skip to main content

vyre_libs/security/
predicate_catalog.rs

1//! Data-backed catalog rows for security bitset predicates.
2//!
3//! The source of truth is `vyre-libs/rules/security_predicates.toml`.
4//! Public security primitives keep their stable Rust functions, while release
5//! gates and inventory witness registration consume these rows for op id,
6//! inputs, soundness, witness fixtures, and external-engine mapping metadata.
7
8use std::collections::{BTreeMap, BTreeSet};
9use std::sync::OnceLock;
10
11const SECURITY_PREDICATES_TOML: &str = include_str!("../../rules/security_predicates.toml");
12const EXPECTED_SCHEMA_VERSION: u32 = 1;
13
14/// Primitive operation used by a data-backed security predicate row.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum SecurityPredicateOperation {
17    /// Per-word `lhs & rhs` bitset intersection.
18    BitsetAnd,
19    /// Per-word `lhs & !rhs` bitset subtraction.
20    BitsetAndNot,
21}
22
23impl SecurityPredicateOperation {
24    /// Return the Tier-B TOML spelling for this operation.
25    #[must_use]
26    pub const fn as_str(self) -> &'static str {
27        match self {
28            Self::BitsetAnd => "bitset_and",
29            Self::BitsetAndNot => "bitset_and_not",
30        }
31    }
32}
33
34/// One security bitset predicate row parsed from Tier-B TOML.
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct SecurityPredicateRow {
37    /// Stable short row id.
38    pub id: String,
39    /// Rust module that owns the public primitive.
40    pub module: String,
41    /// Public function exported by the module.
42    pub function: String,
43    /// Stable VyrE op id registered in the harness inventory.
44    pub op_id: String,
45    /// Bitset operation used by this predicate.
46    pub operation: SecurityPredicateOperation,
47    /// Ordered input buffer names from the public primitive contract.
48    pub inputs: Vec<String>,
49    /// Output buffer name from the public primitive contract.
50    pub output: String,
51    /// Declared soundness lattice value for the predicate.
52    pub soundness: String,
53    /// Stable fixture id for the row's CPU witness vectors.
54    pub witness_fixture: String,
55    /// external/dataflow concept this predicate maps onto.
56    pub external_mapping: String,
57    /// Left-hand witness input words.
58    pub witness_lhs: Vec<u32>,
59    /// Right-hand witness input words.
60    pub witness_rhs: Vec<u32>,
61    /// Expected CPU reference output words.
62    pub witness_expected: Vec<u32>,
63}
64
65static SECURITY_PREDICATE_ROWS: OnceLock<Result<Vec<SecurityPredicateRow>, String>> =
66    OnceLock::new();
67
68/// Parse and return all bundled security predicate rows.
69pub fn try_security_predicate_rows() -> Result<&'static [SecurityPredicateRow], &'static str> {
70    match SECURITY_PREDICATE_ROWS
71        .get_or_init(|| parse_security_predicates(SECURITY_PREDICATES_TOML))
72    {
73        Ok(rows) => Ok(rows.as_slice()),
74        Err(error) => Err(error.as_str()),
75    }
76}
77
78/// Return parsed security predicate rows.
79///
80/// Fails closed: the Tier-B data is compile-embedded and must always parse, so a
81/// parse failure is a broken ship, not a runtime condition, panicking here keeps
82/// a data regression loud instead of silently wiping the security predicate set.
83/// Use [`try_security_predicate_rows`] where recoverable diagnostics are needed.
84///
85/// # Panics
86/// Panics when the bundled Tier-B predicate TOML does not parse. It is compiled in, so
87/// a parse failure is a broken build; use [`try_security_predicate_rows`] for
88/// recoverable diagnostics.
89#[must_use]
90pub fn security_predicate_rows() -> &'static [SecurityPredicateRow] {
91    try_security_predicate_rows().expect("bundled security predicate Tier-B TOML must parse")
92}
93
94/// Find one security predicate row by stable op id.
95#[must_use]
96pub fn security_predicate_row_by_op_id(op_id: &str) -> Option<&'static SecurityPredicateRow> {
97    try_security_predicate_rows()
98        .ok()?
99        .iter()
100        .find(|row| row.op_id == op_id)
101}
102
103pub(crate) fn packed_witness_inputs(op_id: &str) -> Vec<Vec<Vec<u8>>> {
104    security_predicate_row_by_op_id(op_id)
105        .map(|row| {
106            vec![vec![
107                vyre_primitives::wire::pack_u32_slice(&row.witness_lhs),
108                vyre_primitives::wire::pack_u32_slice(&row.witness_rhs),
109                vyre_primitives::wire::pack_u32_slice(&[0]),
110            ]]
111        })
112        .unwrap_or_default()
113}
114
115pub(crate) fn packed_witness_expected(op_id: &str) -> Vec<Vec<Vec<u8>>> {
116    security_predicate_row_by_op_id(op_id)
117        .map(|row| {
118            vec![vec![vyre_primitives::wire::pack_u32_slice(
119                &row.witness_expected,
120            )]]
121        })
122        .unwrap_or_default()
123}
124
125fn parse_security_predicates(source: &str) -> Result<Vec<SecurityPredicateRow>, String> {
126    let mut schema_version = None;
127    let mut current = None::<BTreeMap<String, String>>;
128    let mut raw_rows = Vec::new();
129    for (line_index, raw_line) in source.lines().enumerate() {
130        let line_no = line_index + 1;
131        let line = raw_line
132            .split_once('#')
133            .map_or(raw_line, |(before, _)| before)
134            .trim();
135        if line.is_empty() {
136            continue;
137        }
138        if line == "[[predicate]]" {
139            if let Some(row) = current.take() {
140                raw_rows.push(row);
141            }
142            current = Some(BTreeMap::new());
143            continue;
144        }
145        let (key, value) = line.split_once('=').ok_or_else(|| {
146            format!(
147                "Fix: security predicate Tier-B TOML line {line_no} must be `key = value`, got `{line}`."
148            )
149        })?;
150        let key = key.trim();
151        let value = value.trim().to_string();
152        if let Some(row) = current.as_mut() {
153            if row.insert(key.to_string(), value).is_some() {
154                return Err(format!(
155                    "Fix: security predicate Tier-B TOML line {line_no} duplicates key `{key}` in one [[predicate]] row."
156                ));
157            }
158        } else if key == "schema_version" {
159            schema_version = Some(parse_u32_scalar(&value, key, line_no)?);
160        } else {
161            return Err(format!(
162                "Fix: security predicate Tier-B TOML line {line_no} sets `{key}` before the first [[predicate]] row."
163            ));
164        }
165    }
166    if let Some(row) = current.take() {
167        raw_rows.push(row);
168    }
169    match schema_version {
170        Some(EXPECTED_SCHEMA_VERSION) => {}
171        Some(version) => {
172            return Err(format!(
173                "Fix: security predicate Tier-B TOML schema_version={version}, expected {EXPECTED_SCHEMA_VERSION}."
174            ));
175        }
176        None => {
177            return Err(
178                "Fix: security predicate Tier-B TOML must declare schema_version = 1.".to_string(),
179            );
180        }
181    }
182    if raw_rows.is_empty() {
183        return Err(
184            "Fix: security predicate Tier-B TOML must declare at least one [[predicate]] row."
185                .to_string(),
186        );
187    }
188
189    let mut seen_ids = BTreeSet::new();
190    let mut seen_op_ids = BTreeSet::new();
191    let mut rows = Vec::with_capacity(raw_rows.len());
192    for (index, raw) in raw_rows.into_iter().enumerate() {
193        let row_no = index + 1;
194        let row = SecurityPredicateRow {
195            id: required_string(&raw, "id", row_no)?,
196            module: required_string(&raw, "module", row_no)?,
197            function: required_string(&raw, "function", row_no)?,
198            op_id: required_string(&raw, "op_id", row_no)?,
199            operation: parse_operation(&required_string(&raw, "operation", row_no)?, row_no)?,
200            inputs: required_string_array(&raw, "inputs", row_no)?,
201            output: required_string(&raw, "output", row_no)?,
202            soundness: required_string(&raw, "soundness", row_no)?,
203            witness_fixture: required_string(&raw, "witness_fixture", row_no)?,
204            external_mapping: required_string(&raw, "external_mapping", row_no)?,
205            witness_lhs: required_u32_array(&raw, "witness_lhs", row_no)?,
206            witness_rhs: required_u32_array(&raw, "witness_rhs", row_no)?,
207            witness_expected: required_u32_array(&raw, "witness_expected", row_no)?,
208        };
209        validate_row(&row, row_no)?;
210        if !seen_ids.insert(row.id.clone()) {
211            return Err(format!(
212                "Fix: security predicate Tier-B TOML duplicates id `{}`.",
213                row.id
214            ));
215        }
216        if !seen_op_ids.insert(row.op_id.clone()) {
217            return Err(format!(
218                "Fix: security predicate Tier-B TOML duplicates op_id `{}`.",
219                row.op_id
220            ));
221        }
222        rows.push(row);
223    }
224    rows.sort_by(|left, right| left.op_id.cmp(&right.op_id));
225    Ok(rows)
226}
227
228fn validate_row(row: &SecurityPredicateRow, row_no: usize) -> Result<(), String> {
229    if !row.op_id.starts_with("vyre-libs::security::") {
230        return Err(format!(
231            "Fix: security predicate row {row_no} op_id `{}` must start with `vyre-libs::security::`.",
232            row.op_id
233        ));
234    }
235    if row.inputs.len() != 2 {
236        return Err(format!(
237            "Fix: security predicate row {row_no} `{}` must declare exactly two bitset inputs.",
238            row.id
239        ));
240    }
241    if row.soundness != "Exact" {
242        return Err(format!(
243            "Fix: security predicate row {row_no} `{}` declares soundness `{}`; VX-096 bitset predicates must be Exact.",
244            row.id, row.soundness
245        ));
246    }
247    if row.witness_lhs.len() != row.witness_rhs.len()
248        || row.witness_lhs.len() != row.witness_expected.len()
249    {
250        return Err(format!(
251            "Fix: security predicate row {row_no} `{}` witness_lhs/rhs/expected lengths must match.",
252            row.id
253        ));
254    }
255    if row.witness_fixture.trim().is_empty() || row.external_mapping.trim().is_empty() {
256        return Err(format!(
257            "Fix: security predicate row {row_no} `{}` must declare witness_fixture and external_mapping.",
258            row.id
259        ));
260    }
261    Ok(())
262}
263
264fn required_string(
265    row: &BTreeMap<String, String>,
266    key: &str,
267    row_no: usize,
268) -> Result<String, String> {
269    let value = row.get(key).ok_or_else(|| {
270        format!("Fix: security predicate Tier-B row {row_no} is missing `{key}`.")
271    })?;
272    parse_string_scalar(value, key, row_no)
273}
274
275fn required_string_array(
276    row: &BTreeMap<String, String>,
277    key: &str,
278    row_no: usize,
279) -> Result<Vec<String>, String> {
280    let value = row.get(key).ok_or_else(|| {
281        format!("Fix: security predicate Tier-B row {row_no} is missing `{key}`.")
282    })?;
283    parse_string_array(value, key, row_no)
284}
285
286fn required_u32_array(
287    row: &BTreeMap<String, String>,
288    key: &str,
289    row_no: usize,
290) -> Result<Vec<u32>, String> {
291    let value = row.get(key).ok_or_else(|| {
292        format!("Fix: security predicate Tier-B row {row_no} is missing `{key}`.")
293    })?;
294    parse_u32_array(value, key, row_no)
295}
296
297fn parse_operation(value: &str, row_no: usize) -> Result<SecurityPredicateOperation, String> {
298    match value {
299        "bitset_and" => Ok(SecurityPredicateOperation::BitsetAnd),
300        "bitset_and_not" => Ok(SecurityPredicateOperation::BitsetAndNot),
301        other => Err(format!(
302            "Fix: security predicate Tier-B row {row_no} operation `{other}` is unsupported; expected bitset_and or bitset_and_not."
303        )),
304    }
305}
306
307fn parse_string_scalar(value: &str, key: &str, row_no: usize) -> Result<String, String> {
308    value
309        .strip_prefix('"')
310        .and_then(|value| value.strip_suffix('"'))
311        .map(str::to_string)
312        .filter(|value| !value.trim().is_empty())
313        .ok_or_else(|| {
314            format!(
315                "Fix: security predicate Tier-B row {row_no} field `{key}` must be a non-empty quoted string."
316            )
317        })
318}
319
320fn parse_u32_scalar(value: &str, key: &str, line_no: usize) -> Result<u32, String> {
321    value.parse::<u32>().map_err(|error| {
322        format!(
323            "Fix: security predicate Tier-B TOML line {line_no} field `{key}` must be u32: {error}."
324        )
325    })
326}
327
328fn parse_string_array(value: &str, key: &str, row_no: usize) -> Result<Vec<String>, String> {
329    let body = value
330        .strip_prefix('[')
331        .and_then(|value| value.strip_suffix(']'))
332        .ok_or_else(|| {
333            format!(
334                "Fix: security predicate Tier-B row {row_no} field `{key}` must be a TOML string array."
335            )
336        })?;
337    if body.trim().is_empty() {
338        return Ok(Vec::new());
339    }
340    body.split(',')
341        .map(|item| parse_string_scalar(item.trim(), key, row_no))
342        .collect()
343}
344
345fn parse_u32_array(value: &str, key: &str, row_no: usize) -> Result<Vec<u32>, String> {
346    let body = value
347        .strip_prefix('[')
348        .and_then(|value| value.strip_suffix(']'))
349        .ok_or_else(|| {
350            format!(
351                "Fix: security predicate Tier-B row {row_no} field `{key}` must be a TOML u32 array."
352            )
353        })?;
354    if body.trim().is_empty() {
355        return Ok(Vec::new());
356    }
357    body.split(',')
358        .map(|item| {
359            item.trim().parse::<u32>().map_err(|error| {
360                format!(
361                    "Fix: security predicate Tier-B row {row_no} field `{key}` has non-u32 array item `{}`: {error}.",
362                    item.trim()
363                )
364            })
365        })
366        .collect()
367}
368
369#[cfg(test)]
370mod tests {
371    use super::*;
372    use vyre::soundness::SoundnessTagged;
373
374    const EXPECTED_BITSET_PREDICATE_COUNT: usize = 10;
375
376    #[test]
377    fn tier_b_security_predicate_rows_parse_and_cover_bitset_surface() {
378        let rows = try_security_predicate_rows()
379            .expect("Fix: bundled security predicate Tier-B TOML must parse");
380        assert_eq!(rows.len(), EXPECTED_BITSET_PREDICATE_COUNT);
381        for row in rows {
382            assert_eq!(row.soundness, "Exact");
383            assert_eq!(row.inputs.len(), 2);
384            assert!(row.op_id.starts_with("vyre-libs::security::"));
385            assert!(!row.witness_fixture.trim().is_empty());
386            assert!(!row.external_mapping.trim().is_empty());
387            assert_eq!(row.witness_lhs.len(), row.witness_rhs.len());
388            assert_eq!(row.witness_lhs.len(), row.witness_expected.len());
389        }
390    }
391
392    #[test]
393    fn tier_b_security_rows_match_module_op_ids() {
394        let expected = [
395            super::super::auth_check_dominates::OP_ID,
396            super::super::buffer_size_check::OP_ID,
397            super::super::format_string_check::OP_ID,
398            super::super::lock_dominates::OP_ID,
399            super::super::path_canonical::OP_ID,
400            super::super::sanitizer_dominates::OP_ID,
401            super::super::sql_param_bound::OP_ID,
402            super::super::taint_kill::OP_ID,
403            super::super::unchecked_return::OP_ID,
404            super::super::xss_escape::OP_ID,
405        ];
406        for op_id in expected {
407            let row = security_predicate_row_by_op_id(op_id).unwrap_or_else(|| {
408                panic!("Fix: missing Tier-B security predicate row for {op_id}")
409            });
410            assert_eq!(row.op_id, op_id);
411            assert_eq!(row.function, row.module);
412        }
413    }
414
415    #[test]
416    fn tier_b_witnesses_match_current_cpu_references() {
417        for row in security_predicate_rows() {
418            let expected = match row.op_id.as_str() {
419                super::super::auth_check_dominates::OP_ID => {
420                    super::super::auth_check_dominates::cpu_ref(&row.witness_lhs, &row.witness_rhs)
421                }
422                super::super::buffer_size_check::OP_ID => {
423                    super::super::buffer_size_check::cpu_ref(&row.witness_lhs, &row.witness_rhs)
424                }
425                super::super::format_string_check::OP_ID => {
426                    super::super::format_string_check::cpu_ref(&row.witness_lhs, &row.witness_rhs)
427                }
428                super::super::lock_dominates::OP_ID => {
429                    super::super::lock_dominates::cpu_ref(&row.witness_lhs, &row.witness_rhs)
430                }
431                super::super::path_canonical::OP_ID => {
432                    super::super::path_canonical::cpu_ref(&row.witness_lhs, &row.witness_rhs)
433                }
434                super::super::sanitizer_dominates::OP_ID => {
435                    super::super::sanitizer_dominates::cpu_ref(&row.witness_lhs, &row.witness_rhs)
436                }
437                super::super::sql_param_bound::OP_ID => {
438                    super::super::sql_param_bound::cpu_ref(&row.witness_lhs, &row.witness_rhs)
439                }
440                super::super::taint_kill::OP_ID => {
441                    super::super::taint_kill::cpu_ref(&row.witness_lhs, &row.witness_rhs)
442                }
443                super::super::unchecked_return::OP_ID => {
444                    super::super::unchecked_return::cpu_ref(&row.witness_lhs, &row.witness_rhs)
445                }
446                super::super::xss_escape::OP_ID => {
447                    super::super::xss_escape::cpu_ref(&row.witness_lhs, &row.witness_rhs)
448                }
449                other => panic!("Fix: unknown security predicate Tier-B op id `{other}`"),
450            };
451            assert_eq!(
452                expected, row.witness_expected,
453                "Fix: Tier-B witness fixture {} drifted from CPU ref for {}",
454                row.witness_fixture, row.op_id
455            );
456        }
457    }
458
459    #[test]
460    fn tier_b_soundness_rows_match_marker_types() {
461        let exact = vyre::soundness::Soundness::Exact;
462        for (op_id, soundness) in [
463            (
464                super::super::auth_check_dominates::OP_ID,
465                super::super::auth_check_dominates::AuthCheckDominates.soundness(),
466            ),
467            (
468                super::super::buffer_size_check::OP_ID,
469                super::super::buffer_size_check::BufferSizeCheck.soundness(),
470            ),
471            (
472                super::super::format_string_check::OP_ID,
473                super::super::format_string_check::FormatStringCheck.soundness(),
474            ),
475            (
476                super::super::lock_dominates::OP_ID,
477                super::super::lock_dominates::LockDominates.soundness(),
478            ),
479            (
480                super::super::path_canonical::OP_ID,
481                super::super::path_canonical::PathCanonical.soundness(),
482            ),
483            (
484                super::super::sanitizer_dominates::OP_ID,
485                super::super::sanitizer_dominates::SanitizerDominates.soundness(),
486            ),
487            (
488                super::super::sql_param_bound::OP_ID,
489                super::super::sql_param_bound::SqlParamBound.soundness(),
490            ),
491            (
492                super::super::taint_kill::OP_ID,
493                super::super::taint_kill::TaintKill.soundness(),
494            ),
495            (
496                super::super::unchecked_return::OP_ID,
497                super::super::unchecked_return::UncheckedReturn.soundness(),
498            ),
499            (
500                super::super::xss_escape::OP_ID,
501                super::super::xss_escape::XssEscape.soundness(),
502            ),
503        ] {
504            let row = security_predicate_row_by_op_id(op_id)
505                .unwrap_or_else(|| panic!("Fix: missing Tier-B row for soundness marker {op_id}"));
506            assert_eq!(soundness, exact);
507            assert_eq!(row.soundness, "Exact");
508        }
509    }
510}