1use super::bitmask::try_bit_is_set;
2use super::graph::{try_prepare_witness_graph, PreparedWitnessGraph, ReverseEdges};
3use super::{
4 ExtractedPath, ExtractedStatement, NodeAttr, PathError, PathSeed, PrepareWitnessGraphError,
5};
6use vyre_primitives::predicate::edge_kind;
7
8const IFDS_PATH_EDGE_MASK: u32 = edge_kind::ASSIGNMENT
9 | edge_kind::CALL_ARG
10 | edge_kind::RETURN
11 | edge_kind::PHI
12 | edge_kind::ALIAS
13 | edge_kind::MEM_STORE
14 | edge_kind::MEM_LOAD
15 | edge_kind::MUT_REF;
16
17pub(super) const MAX_PATH_DEPTH: usize = 1024;
18
19#[derive(Clone, Copy, Debug)]
25pub struct PathExtractionRequest<'a> {
26 pub seed: &'a PathSeed,
28 pub source_reach: &'a [u32],
30 pub sanitizer_mask: &'a [u32],
32}
33
34#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
36pub struct PreparedWitnessBatchStats {
37 pub requests: u64,
39 pub successes: u64,
41 pub errors: u64,
43 pub prepared_graph_reuses: u64,
47 pub prepared_node_count: u64,
49 pub prepared_reverse_edges: u64,
51}
52
53fn path_node_index(node: u32, node_count: u32) -> Result<usize, PathError> {
54 if node >= node_count {
55 return Err(PathError::NodeOutOfBounds { node, node_count });
56 }
57 usize::try_from(node).map_err(|error| PathError::MalformedGraph {
58 reason: PrepareWitnessGraphError::TargetOutOfBounds {
59 edge_index: 0,
60 target: node,
61 node_count,
62 },
63 fix: format!(
64 "Fix: witness path node id {node} cannot fit usize: {error}; shard the ProgramGraph before witness extraction."
65 ),
66 })
67}
68
69fn reverse_offset_to_usize(
70 value: u32,
71 node: usize,
72 label: &'static str,
73) -> Result<usize, PathError> {
74 usize::try_from(value).map_err(|error| PathError::MalformedGraph {
75 reason: PrepareWitnessGraphError::ReverseEdgeCountOverflow { node },
76 fix: format!(
77 "Fix: witness path {label} cannot fit usize: {error}; shard the ProgramGraph before witness extraction."
78 ),
79 })
80}
81
82fn witness_bit_is_set(bitmask: &[u32], node: u32, field: &'static str) -> Result<bool, PathError> {
83 try_bit_is_set(bitmask, node).map_err(|error| PathError::MalformedGraph {
84 reason: PrepareWitnessGraphError::TargetOutOfBounds {
85 edge_index: 0,
86 target: node,
87 node_count: u32::MAX,
88 },
89 fix: format!("Fix: witness {field} bitmask cannot address node {node}: {error}"),
90 })
91}
92
93#[allow(clippy::too_many_arguments)]
125pub fn extract_path(
126 seed: &PathSeed,
127 source_reach: &[u32],
128 sanitizer_mask: &[u32],
129 edge_offsets: &[u32],
130 edge_targets: &[u32],
131 edge_kind_mask: &[u32],
132 pg_node_attrs: &[NodeAttr],
133 file_table: &[String],
134 descriptions: &[String],
135 adapter: &str,
136) -> Result<ExtractedPath, PathError> {
137 let graph = try_prepare_witness_graph(
138 edge_offsets,
139 edge_targets,
140 edge_kind_mask,
141 pg_node_attrs,
142 file_table,
143 descriptions,
144 adapter,
145 )?;
146 extract_path_prepared(seed, source_reach, sanitizer_mask, &graph)
147}
148
149pub fn extract_path_prepared(
151 seed: &PathSeed,
152 source_reach: &[u32],
153 sanitizer_mask: &[u32],
154 graph: &PreparedWitnessGraph<'_>,
155) -> Result<ExtractedPath, PathError> {
156 let node_count = u32::try_from(graph.pg_node_attrs.len()).map_err(|error| {
157 PathError::MalformedGraph {
158 reason: PrepareWitnessGraphError::TargetOutOfBounds {
159 edge_index: 0,
160 target: u32::MAX,
161 node_count: u32::MAX,
162 },
163 fix: format!(
164 "Fix: prepared witness graph has {} node attribute rows, which does not fit u32: {error}. Shard the ProgramGraph before witness extraction.",
165 graph.pg_node_attrs.len()
166 ),
167 }
168 })?;
169 if seed.source_node >= node_count {
171 return Err(PathError::NodeOutOfBounds {
172 node: seed.source_node,
173 node_count,
174 });
175 }
176 if seed.sink_node >= node_count {
177 return Err(PathError::NodeOutOfBounds {
178 node: seed.sink_node,
179 node_count,
180 });
181 }
182 let _source_index = path_node_index(seed.source_node, node_count)?;
183 let sink_index = path_node_index(seed.sink_node, node_count)?;
184
185 if seed.source_node == seed.sink_node {
186 if witness_bit_is_set(sanitizer_mask, seed.source_node, "sanitizer")? {
190 return Err(PathError::NoPath);
191 }
192 let stmt = build_statement(
193 seed.source_node,
194 node_count,
195 graph.pg_node_attrs,
196 graph.file_table,
197 graph.descriptions,
198 graph.adapter,
199 )?;
200 return Ok(ExtractedPath {
201 statements: vec![stmt],
202 });
203 }
204
205 if !witness_bit_is_set(source_reach, seed.sink_node, "source reachability")?
206 || !witness_bit_is_set(source_reach, seed.source_node, "source reachability")?
207 {
208 return Err(PathError::NoPath);
210 }
211 if witness_bit_is_set(sanitizer_mask, seed.sink_node, "sanitizer")?
214 || witness_bit_is_set(sanitizer_mask, seed.source_node, "sanitizer")?
215 {
216 return Err(PathError::NoPath);
217 }
218
219 let mut visited = vec![false; graph.pg_node_attrs.len()];
220 let mut chain: Vec<u32> = Vec::new();
221 let mut current = seed.sink_node;
222 chain.push(current);
223 visited[sink_index] = true;
224
225 for _ in 0..MAX_PATH_DEPTH {
226 if current == seed.source_node {
227 break;
228 }
229 let Some(pred) = find_reached_predecessor(
230 current,
231 node_count,
232 source_reach,
233 sanitizer_mask,
234 &graph.reverse_edges,
235 &visited,
236 )?
237 else {
238 return Err(PathError::NoPath);
239 };
240 chain.push(pred);
241 let pred_index = path_node_index(pred, node_count)?;
242 visited[pred_index] = true;
243 current = pred;
244 }
245
246 if current != seed.source_node {
247 return Err(PathError::DepthExceeded {
252 partial_chain: chain,
253 });
254 }
255
256 chain.reverse();
258 let mut statements =
259 crate::staging_reserve::reserved_vec(chain.len(), "reachability witness statements")
260 .map_err(|error| PathError::ResourceExhausted {
261 field: "reachability witness statements".to_string(),
262 fix: format!(
263 "Fix: {error}; reduce the witness batch size or shard the ProgramGraph before extracting paths."
264 ),
265 })?;
266 for node in chain {
267 statements.push(build_statement(
268 node,
269 node_count,
270 graph.pg_node_attrs,
271 graph.file_table,
272 graph.descriptions,
273 graph.adapter,
274 )?);
275 }
276 Ok(ExtractedPath { statements })
277}
278
279pub fn extract_paths_prepared(
285 requests: &[PathExtractionRequest<'_>],
286 graph: &PreparedWitnessGraph<'_>,
287) -> Result<(Vec<Result<ExtractedPath, PathError>>, PreparedWitnessBatchStats), PathError> {
288 let mut results = crate::staging_reserve::reserved_vec(
289 requests.len(),
290 "reachability prepared witness batch results",
291 )
292 .map_err(|error| PathError::ResourceExhausted {
293 field: "reachability prepared witness batch results".to_string(),
294 fix: format!(
295 "Fix: {error}; reduce the witness batch size or shard the ProgramGraph before extracting paths."
296 ),
297 })?;
298 let stats = extract_paths_prepared_into(requests, graph, &mut results)?;
299 Ok((results, stats))
300}
301
302pub fn extract_paths_prepared_into(
305 requests: &[PathExtractionRequest<'_>],
306 graph: &PreparedWitnessGraph<'_>,
307 results: &mut Vec<Result<ExtractedPath, PathError>>,
308) -> Result<PreparedWitnessBatchStats, PathError> {
309 results.clear();
310 if results.capacity() < requests.len() {
311 results
312 .try_reserve(requests.len() - results.capacity())
313 .map_err(|error| PathError::ResourceExhausted {
314 field: "reachability prepared witness batch results".to_string(),
315 fix: format!(
316 "Fix: could not reserve {} prepared witness result slot(s): {error}; reduce the witness batch size or shard the ProgramGraph before extracting paths.",
317 requests.len()
318 ),
319 })?;
320 }
321
322 let mut successes = 0u64;
323 let mut errors = 0u64;
324 for request in requests {
325 let result = extract_path_prepared(
326 request.seed,
327 request.source_reach,
328 request.sanitizer_mask,
329 graph,
330 );
331 if result.is_ok() {
332 successes = successes.checked_add(1).ok_or_else(|| {
333 PathError::ResourceExhausted {
334 field: "reachability prepared witness batch success counter".to_string(),
335 fix: "Fix: prepared witness success counter overflowed u64; shard the witness request batch.".to_string(),
336 }
337 })?;
338 } else {
339 errors = errors.checked_add(1).ok_or_else(|| {
340 PathError::ResourceExhausted {
341 field: "reachability prepared witness batch error counter".to_string(),
342 fix: "Fix: prepared witness error counter overflowed u64; shard the witness request batch.".to_string(),
343 }
344 })?;
345 }
346 results.push(result);
347 }
348
349 let requests_u64 = u64::try_from(requests.len()).map_err(|error| {
350 PathError::ResourceExhausted {
351 field: "reachability prepared witness batch request counter".to_string(),
352 fix: format!(
353 "Fix: witness request count does not fit u64: {error}; shard the witness request batch."
354 ),
355 }
356 })?;
357 let prepared_node_count = u64::try_from(graph.node_count()).map_err(|error| {
358 PathError::ResourceExhausted {
359 field: "reachability prepared witness graph node counter".to_string(),
360 fix: format!(
361 "Fix: prepared witness node count does not fit u64: {error}; shard the ProgramGraph before extracting paths."
362 ),
363 }
364 })?;
365 let prepared_reverse_edges = u64::try_from(graph.reverse_edge_count()).map_err(|error| {
366 PathError::ResourceExhausted {
367 field: "reachability prepared witness reverse edge counter".to_string(),
368 fix: format!(
369 "Fix: prepared witness reverse edge count does not fit u64: {error}; shard the ProgramGraph before extracting paths."
370 ),
371 }
372 })?;
373 Ok(PreparedWitnessBatchStats {
374 requests: requests_u64,
375 successes,
376 errors,
377 prepared_graph_reuses: requests_u64,
378 prepared_node_count,
379 prepared_reverse_edges,
380 })
381}
382
383fn find_reached_predecessor(
388 current: u32,
389 node_count: u32,
390 source_reach: &[u32],
391 sanitizer_mask: &[u32],
392 reverse_edges: &ReverseEdges,
393 visited: &[bool],
394) -> Result<Option<u32>, PathError> {
395 let current_index = path_node_index(current, node_count)?;
396 let next_index = current_index.checked_add(1).ok_or_else(|| PathError::MalformedGraph {
397 reason: PrepareWitnessGraphError::InvalidOffsetCount {
398 offsets: reverse_edges.offsets.len(),
399 expected: usize::MAX,
400 },
401 fix: format!(
402 "Fix: witness reverse-edge offset index overflowed after node {current}; rebuild the prepared witness graph."
403 ),
404 })?;
405 let start_value =
406 *reverse_edges
407 .offsets
408 .get(current_index)
409 .ok_or_else(|| PathError::MalformedGraph {
410 reason: PrepareWitnessGraphError::InvalidOffsetCount {
411 offsets: reverse_edges.offsets.len(),
412 expected: next_index,
413 },
414 fix: format!(
415 "Fix: witness reverse-edge offsets are missing row start for node {current}."
416 ),
417 })?;
418 let end_value =
419 *reverse_edges
420 .offsets
421 .get(next_index)
422 .ok_or_else(|| PathError::MalformedGraph {
423 reason: PrepareWitnessGraphError::InvalidOffsetCount {
424 offsets: reverse_edges.offsets.len(),
425 expected: match next_index.checked_add(1) {
426 Some(expected) => expected,
427 None => usize::MAX,
428 },
429 },
430 fix: format!(
431 "Fix: witness reverse-edge offsets are missing row end for node {current}."
432 ),
433 })?;
434 let start = reverse_offset_to_usize(start_value, current_index, "reverse row start")?;
435 let end = reverse_offset_to_usize(end_value, current_index, "reverse row end")?;
436 for edge_index in start..end {
437 let pred =
438 *reverse_edges
439 .preds
440 .get(edge_index)
441 .ok_or_else(|| PathError::MalformedGraph {
442 reason: PrepareWitnessGraphError::TerminalOffsetMismatch {
443 terminal: end,
444 edges: reverse_edges.preds.len(),
445 },
446 fix: format!(
447 "Fix: witness reverse predecessor array is shorter than reverse offset {end}."
448 ),
449 })?;
450 let mask =
451 *reverse_edges
452 .kinds
453 .get(edge_index)
454 .ok_or_else(|| PathError::MalformedGraph {
455 reason: PrepareWitnessGraphError::TerminalOffsetMismatch {
456 terminal: end,
457 edges: reverse_edges.kinds.len(),
458 },
459 fix: format!(
460 "Fix: witness reverse edge-kind array is shorter than reverse offset {end}."
461 ),
462 })?;
463 let pred_index = path_node_index(pred, node_count)?;
464 if visited[pred_index] {
465 continue;
466 }
467 if !witness_bit_is_set(source_reach, pred, "source reachability")? {
468 continue;
469 }
470 if witness_bit_is_set(sanitizer_mask, pred, "sanitizer")? {
471 continue;
474 }
475 if mask & IFDS_PATH_EDGE_MASK != 0 {
476 return Ok(Some(pred));
477 }
478 }
479 Ok(None)
480}
481
482fn build_statement(
483 node_id: u32,
484 node_count: u32,
485 pg_node_attrs: &[NodeAttr],
486 file_table: &[String],
487 descriptions: &[String],
488 adapter: &str,
489) -> Result<ExtractedStatement, PathError> {
490 let node_index = path_node_index(node_id, node_count)?;
491 let attr = pg_node_attrs
492 .get(node_index)
493 .ok_or(PathError::NodeOutOfBounds {
494 node: node_id,
495 node_count,
496 })?;
497 let file = file_table
498 .get(usize::try_from(attr.file_idx).ok().unwrap_or(usize::MAX))
499 .cloned()
500 .unwrap_or_else(|| String::from("<unknown>"));
501 let description = descriptions
502 .get(node_index)
503 .cloned()
504 .unwrap_or_else(|| format!("node {node_id}"));
505 Ok(ExtractedStatement {
506 adapter: adapter.to_string(),
507 description,
508 file,
509 node_id,
510 byte_start: attr.byte_start,
511 byte_end: attr.byte_end,
512 })
513}