1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum SecurityPredicateOperation {
17 BitsetAnd,
19 BitsetAndNot,
21}
22
23impl SecurityPredicateOperation {
24 #[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#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct SecurityPredicateRow {
37 pub id: String,
39 pub module: String,
41 pub function: String,
43 pub op_id: String,
45 pub operation: SecurityPredicateOperation,
47 pub inputs: Vec<String>,
49 pub output: String,
51 pub soundness: String,
53 pub witness_fixture: String,
55 pub external_mapping: String,
57 pub witness_lhs: Vec<u32>,
59 pub witness_rhs: Vec<u32>,
61 pub witness_expected: Vec<u32>,
63}
64
65static SECURITY_PREDICATE_ROWS: OnceLock<Result<Vec<SecurityPredicateRow>, String>> =
66 OnceLock::new();
67
68pub 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#[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#[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_spec::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_spec::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}