Skip to main content

weir/
escape.rs

1//! DF-8  -  escape analysis.
2//!
3//! Does this pointer leave its defining frame / cross a trust
4//! boundary? Closure over assignments to parameters, return values,
5//! heap fields reachable from globals, and indirect-call arguments.
6//!
7//! # Implementation
8//!
9//! Escape is a reachability fixpoint in bitset space:
10//!   * `escapes[v]` is set iff any of:
11//!     - `v` is a root (written to global, parameter, return value,
12//!        indirect-call argument  -  seeded by caller),
13//!     - `v` points to an escaped object (propagated through
14//!        `points_to`),
15//!     - `v` is passed to an escaped callee (propagated through
16//!        `callgraph`).
17//!
18//! Per invocation we OR-union both channels and AND-mask against the
19//! current escape-status to emit the new escape set. The caller
20//! drives the fixpoint via `bitset_fixpoint` and stops when the
21//! bitset is unchanged.
22//!
23//! Soundness: [`MayOver`](super::Soundness::MayOver).
24//!
25//! Required for C03 (double-free on concurrent path), C17 (fd
26//! passing confused-deputy).
27
28use std::sync::Arc;
29
30use vyre::ir::{BufferAccess, BufferDecl, DataType, Expr, Ident, Node, Program};
31use vyre_primitives::bitset::bitset_words;
32
33pub(crate) const OP_ID: &str = "weir::escape";
34
35/// One escape-propagation step. Output bit `v` is set iff any of
36/// `points_to_in[v]`, `callgraph_in[v]`, or prior `escape_out[v]`
37/// was set. Host runs this in a fixpoint until the bitset is
38/// unchanged.
39#[must_use]
40pub fn escape_analyze(points_to_in: &str, callgraph_in: &str, escape_out: &str) -> Program {
41    escape_analyze_with_count(points_to_in, callgraph_in, escape_out, 64)
42}
43
44/// Version that takes the lane count explicitly.
45#[must_use]
46pub fn escape_analyze_with_count(
47    points_to_in: &str,
48    callgraph_in: &str,
49    escape_out: &str,
50    node_count: u32,
51) -> Program {
52    let words = bitset_words(node_count).max(1);
53    let w = Expr::InvocationId { axis: 0 };
54
55    let body = vec![
56        Node::let_bind("pts", Expr::load(points_to_in, w.clone())),
57        Node::let_bind("cg", Expr::load(callgraph_in, w.clone())),
58        Node::let_bind("prev", Expr::load(escape_out, w.clone())),
59        Node::store(
60            escape_out,
61            w.clone(),
62            Expr::bitor(
63                Expr::bitor(Expr::var("pts"), Expr::var("cg")),
64                Expr::var("prev"),
65            ),
66        ),
67    ];
68
69    let buffers = vec![
70        BufferDecl::storage(points_to_in, 0, BufferAccess::ReadOnly, DataType::U32)
71            .with_count(words),
72        BufferDecl::storage(callgraph_in, 1, BufferAccess::ReadOnly, DataType::U32)
73            .with_count(words),
74        BufferDecl::storage(escape_out, 2, BufferAccess::ReadWrite, DataType::U32)
75            .with_count(words),
76    ];
77
78    Program::wrapped(
79        buffers,
80        [256, 1, 1],
81        vec![Node::Region {
82            generator: Ident::from(OP_ID),
83            source_region: None,
84            body: Arc::new(vec![Node::if_then(
85                Expr::lt(w.clone(), Expr::u32(words)),
86                body,
87            )]),
88        }],
89    )
90}
91
92/// Marker type for the escape-analysis dataflow primitive.
93#[derive(Clone, Copy, Debug, PartialEq, Eq)]
94pub struct Escape;
95
96impl super::soundness::SoundnessTagged for Escape {
97    fn soundness(&self) -> super::soundness::Soundness {
98        super::soundness::Soundness::MayOver
99    }
100}
101
102inventory::submit! {
103    vyre_harness::OpEntry::new(
104        OP_ID,
105        || escape_analyze_with_count("pts", "cg", "out", 64),
106        Some(|| {
107            let u32s = crate::dispatch_decode::pack_u32;
108            vec![vec![
109                u32s(&[0b0011, 0]),
110                u32s(&[0b0100, 0b1000]),
111                u32s(&[0b1000, 0b0001]),
112            ]]
113        }),
114        Some(|| {
115            let u32s = crate::dispatch_decode::pack_u32;
116            vec![vec![u32s(&[0b1111, 0b1001])]]
117        }),
118    )
119}
120
121#[cfg(test)]
122#[allow(deprecated)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn escape_analyze_emits_program_with_op_id() {
128        let p = escape_analyze_with_count("pts", "cg", "out", 64);
129        let has_region = p.entry().iter().any(|n| {
130            matches!(
131                n,
132                Node::Region { generator, .. } if generator.as_str() == OP_ID
133            )
134        });
135        assert!(
136            has_region,
137            "weir::escape: Program body must contain a Region tagged `{OP_ID}`"
138        );
139    }
140
141    #[test]
142    fn escape_analyze_declares_three_buffers() {
143        let p = escape_analyze_with_count("pts", "cg", "out", 64);
144        assert_eq!(
145            p.buffers().len(),
146            3,
147            "weir::escape: Program must declare exactly 3 buffers (pts, cg, out)"
148        );
149    }
150
151    #[test]
152    fn escape_analyze_buffer_widths_match_node_count() {
153        // 64 nodes → 2 words (32-bit-per-word bitset).
154        let p = escape_analyze_with_count("pts", "cg", "out", 64);
155        for buf in p.buffers() {
156            assert_eq!(
157                buf.count(),
158                2,
159                "weir::escape: buffer `{}` must have count=2 for 64 lanes",
160                buf.name()
161            );
162        }
163    }
164
165    #[test]
166    fn escape_soundness_is_mayover() {
167        use super::super::soundness::SoundnessTagged;
168        assert_eq!(
169            Escape.soundness(),
170            super::super::soundness::Soundness::MayOver,
171            "weir::escape: closure-over-pointers is over-approximate (MayOver)"
172        );
173    }
174
175    #[test]
176    fn escape_analyze_handles_minimum_node_count() {
177        // node_count=0 must still produce a valid Program (no panic, words.max(1)).
178        let p = escape_analyze_with_count("pts", "cg", "out", 0);
179        assert_eq!(
180            p.buffers().len(),
181            3,
182            "weir::escape: zero-node case must still emit a valid 3-buffer Program"
183        );
184    }
185}