Skip to main content

tang_expr/int/
wire.rs

1//! Wire format and verifier for untrusted integer graphs.
2//!
3//! A graph that arrives from outside (e.g. a decoder shipped inside a
4//! compressed stream) is read with [`IntGraph::from_bytes`], which rebuilds
5//! it node by node and rejects anything the trusted builder could not have
6//! produced, plus anything over the caller's [`Limits`]:
7//!
8//! - every operand refers to an earlier node (the graph is a DAG in id order);
9//! - types and loop scopes check exactly as in the builder;
10//! - parameters and buffers match the caller's ABI (count and type); buffer
11//!   names come from the caller, never from the bytes;
12//! - every loop carries a trip cap (`≤ max_trip`), nests at most `max_depth`
13//!   deep, and has exactly one state node per state;
14//! - the worst-case work, Σ over nodes of the product of the caps of the
15//!   enclosing loops, is at most `max_cost`;
16//! - outputs are top-level and have the caller's types.
17//!
18//! The op set has no I/O and no unbounded control flow; loads are
19//! bounds-checked by the interpreter and by
20//! [`IntGraph::to_wgsl_body_guarded`]. The only writes go to the caller's
21//! fixed-length scratch buffers ([`IntGraph::from_bytes_rw`]), bounds-checked
22//! the same way (out-of-range stores are dropped) and executed in the shared
23//! schedule order. So a verified graph is a total, deterministic function of
24//! its parameters and buffers (and the scratch contents it starts from).
25//!
26//! Layout (all integers LEB128 varints unless noted):
27//! `"TIR1"`, `nloops`, per loop `parent+1 | 0`, `cap`, `n`, `init[n]`,
28//! `cond`, `next[n]`; `nnodes`, per node a tag byte and its fields (operands
29//! as `self − operand ≥ 1`); `nout`, `out[nout]`. Loop fields are node ids.
30
31use std::collections::HashMap;
32
33use super::{BinOp, CmpOp, Elem, IId, INode, IType, IntGraph, Loop, UnOp};
34
35const MAGIC: &[u8; 4] = b"TIR1";
36
37/// Resource limits for [`IntGraph::from_bytes`].
38#[derive(Clone, Copy, Debug)]
39pub struct Limits {
40    /// Maximum node count.
41    pub max_nodes: usize,
42    /// Maximum loop count.
43    pub max_loops: usize,
44    /// Maximum loop nesting depth.
45    pub max_depth: usize,
46    /// Maximum trip cap of one loop.
47    pub max_trip: u32,
48    /// Maximum Σ nodes × enclosing trip caps (work per evaluation).
49    pub max_cost: u64,
50}
51
52impl Default for Limits {
53    fn default() -> Self {
54        Self {
55            max_nodes: 1 << 14,
56            max_loops: 64,
57            max_depth: 4,
58            max_trip: 1 << 12,
59            max_cost: 1 << 20,
60        }
61    }
62}
63
64/// Why a graph was rejected.
65#[derive(Clone, Debug, PartialEq, Eq)]
66pub struct WireError(pub String);
67
68impl std::fmt::Display for WireError {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        write!(f, "int graph rejected: {}", self.0)
71    }
72}
73
74impl std::error::Error for WireError {}
75
76fn err<T>(m: impl Into<String>) -> Result<T, WireError> {
77    Err(WireError(m.into()))
78}
79
80fn put(o: &mut Vec<u8>, mut v: u64) {
81    loop {
82        let b = (v & 0x7f) as u8;
83        v >>= 7;
84        if v == 0 {
85            o.push(b);
86            return;
87        }
88        o.push(b | 0x80);
89    }
90}
91
92struct Rd<'a> {
93    b: &'a [u8],
94    p: usize,
95}
96
97impl Rd<'_> {
98    fn u8(&mut self) -> Result<u8, WireError> {
99        let v = *self.b.get(self.p).ok_or(WireError("truncated".into()))?;
100        self.p += 1;
101        Ok(v)
102    }
103    fn var(&mut self) -> Result<u64, WireError> {
104        let mut v = 0u64;
105        for s in 0..10 {
106            let b = self.u8()?;
107            v |= ((b & 0x7f) as u64) << (7 * s);
108            if b & 0x80 == 0 {
109                return Ok(v);
110            }
111        }
112        err("varint too long")
113    }
114    fn u32(&mut self) -> Result<u32, WireError> {
115        u32::try_from(self.var()?).or(err("value out of range"))
116    }
117    fn n(&mut self, max: usize, what: &str) -> Result<usize, WireError> {
118        let v = self.var()?;
119        if v > max as u64 {
120            return err(format!("too many {what}"));
121        }
122        Ok(v as usize)
123    }
124}
125
126fn ty_code(t: IType) -> u8 {
127    match t {
128        IType::I32 => 0,
129        IType::U32 => 1,
130        IType::Bool => 2,
131    }
132}
133
134fn ty_of(c: u8) -> Result<IType, WireError> {
135    Ok(match c {
136        0 => IType::I32,
137        1 => IType::U32,
138        2 => IType::Bool,
139        _ => return err("type"),
140    })
141}
142
143const BINS: [BinOp; 13] = [
144    BinOp::Add,
145    BinOp::Sub,
146    BinOp::Mul,
147    BinOp::MulHi,
148    BinOp::And,
149    BinOp::Or,
150    BinOp::Xor,
151    BinOp::Shl,
152    BinOp::Shr,
153    BinOp::Min,
154    BinOp::Max,
155    BinOp::Div,
156    BinOp::Rem,
157];
158const CMPS: [CmpOp; 4] = [CmpOp::Eq, CmpOp::Ne, CmpOp::Lt, CmpOp::Le];
159const UNS: [UnOp; 3] = [UnOp::Neg, UnOp::Not, UnOp::Clz];
160
161impl IntGraph {
162    /// Serialize the whole graph with `outputs` (see the module docs).
163    /// Buffers are not named on the wire; the reader supplies the ABI.
164    pub fn to_bytes(&self, outputs: &[IId]) -> Vec<u8> {
165        let mut o = MAGIC.to_vec();
166        put(&mut o, self.loops.len() as u64);
167        for lp in &self.loops {
168            put(&mut o, lp.parent.map_or(0, |p| p as u64 + 1));
169            put(
170                &mut o,
171                lp.cap.expect("to_bytes: every loop needs a trip cap") as u64,
172            );
173            put(&mut o, lp.init.len() as u64);
174            for &x in &lp.init {
175                put(&mut o, x.0 as u64);
176            }
177            put(&mut o, lp.cond.0 as u64);
178            for &x in &lp.next {
179                put(&mut o, x.0 as u64);
180            }
181        }
182        put(&mut o, self.nodes.len() as u64);
183        for (i, n) in self.nodes.iter().enumerate() {
184            let d = |a: IId| (i - a.index()) as u64;
185            match *n {
186                INode::Const(t, v) => {
187                    o.push(0);
188                    o.push(ty_code(t));
189                    // small negative i32s as zigzag
190                    let z = if t == IType::I32 {
191                        ((v << 1) ^ (((v as i32) >> 31) as u32)) as u64
192                    } else {
193                        v as u64
194                    };
195                    put(&mut o, z);
196                }
197                INode::Param(p, t) => {
198                    o.push(1);
199                    put(&mut o, p as u64);
200                    o.push(ty_code(t));
201                }
202                INode::Bin(op, a, b) => {
203                    o.push(2);
204                    o.push(BINS.iter().position(|&x| x == op).unwrap() as u8);
205                    put(&mut o, d(a));
206                    put(&mut o, d(b));
207                }
208                INode::Un(op, a) => {
209                    o.push(3);
210                    o.push(UNS.iter().position(|&x| x == op).unwrap() as u8);
211                    put(&mut o, d(a));
212                }
213                INode::Cmp(op, a, b) => {
214                    o.push(4);
215                    o.push(CMPS.iter().position(|&x| x == op).unwrap() as u8);
216                    put(&mut o, d(a));
217                    put(&mut o, d(b));
218                }
219                INode::Select(c, a, b) => {
220                    o.push(5);
221                    put(&mut o, d(c));
222                    put(&mut o, d(a));
223                    put(&mut o, d(b));
224                }
225                INode::Bitcast(t, a) => {
226                    o.push(6);
227                    o.push(ty_code(t));
228                    put(&mut o, d(a));
229                }
230                INode::Load(b, a) => {
231                    o.push(7);
232                    put(&mut o, b as u64);
233                    put(&mut o, d(a));
234                }
235                INode::State(l, k) => {
236                    o.push(8);
237                    put(&mut o, l as u64);
238                    put(&mut o, k as u64);
239                }
240                INode::LoopOut(l, k) => {
241                    o.push(9);
242                    put(&mut o, l as u64);
243                    put(&mut o, k as u64);
244                }
245                INode::Tok => o.push(10),
246                INode::LoadRw(b, a, t) => {
247                    o.push(11);
248                    put(&mut o, b as u64);
249                    put(&mut o, d(a));
250                    put(&mut o, d(t));
251                }
252                INode::Store(b, a, v, t) => {
253                    o.push(12);
254                    put(&mut o, b as u64);
255                    put(&mut o, d(a));
256                    put(&mut o, d(v));
257                    put(&mut o, d(t));
258                }
259            }
260        }
261        put(&mut o, outputs.len() as u64);
262        for x in outputs {
263            put(&mut o, x.0 as u64);
264        }
265        o
266    }
267
268    /// Read and verify an untrusted graph (see the module docs). `params`
269    /// gives the type of each parameter, `bufs` the name and element type of
270    /// each buffer, `outs` the output types. Returns the graph and its outputs.
271    pub fn from_bytes(
272        bytes: &[u8],
273        params: &[IType],
274        bufs: &[(&str, Elem)],
275        outs: &[IType],
276        lim: &Limits,
277    ) -> Result<(IntGraph, Vec<IId>), WireError> {
278        Self::from_bytes_rw(bytes, params, bufs, &[], outs, lim)
279    }
280
281    /// [`Self::from_bytes`] with scratch buffers after the read-only ones:
282    /// `(name, elem, length, WGSL base expression)` each, all from the caller.
283    pub fn from_bytes_rw(
284        bytes: &[u8],
285        params: &[IType],
286        bufs: &[(&str, Elem)],
287        rw: &[(&str, Elem, u32, &str)],
288        outs: &[IType],
289        lim: &Limits,
290    ) -> Result<(IntGraph, Vec<IId>), WireError> {
291        let mut r = Rd { b: bytes, p: 0 };
292        if bytes.len() < 4 || &bytes[..4] != MAGIC {
293            return err("magic");
294        }
295        r.p = 4;
296        let mut g = IntGraph::new();
297        for &(name, elem) in bufs {
298            g.buffer(name, elem);
299        }
300        for &(name, elem, len, base) in rw {
301            g.buffer_rw(name, elem, len, base);
302        }
303        let nbufs = g.bufs.len() as u64;
304        let nl = r.n(lim.max_loops, "loops")?;
305        let mut caps = Vec::with_capacity(nl);
306        for l in 0..nl {
307            let parent = match r.var()? {
308                0 => None,
309                p if p - 1 < l as u64 => Some((p - 1) as u32),
310                _ => return err("loop parent must be an earlier loop"),
311            };
312            let cap = r.u32()?;
313            if cap > lim.max_trip {
314                return err("loop trip cap over limit");
315            }
316            caps.push(cap);
317            let k = r.n(256, "loop states")?;
318            let ids = |r: &mut Rd, k: usize| {
319                (0..k)
320                    .map(|_| r.u32().map(IId))
321                    .collect::<Result<Vec<_>, _>>()
322            };
323            let init = ids(&mut r, k)?;
324            let cond = IId(r.u32()?);
325            let next = ids(&mut r, k)?;
326            let lp = Loop {
327                init,
328                cond,
329                next,
330                parent,
331                cap: Some(cap),
332            };
333            if g.depth(parent) + 1 > lim.max_depth {
334                return err("loops nest too deep");
335            }
336            g.loops.push(lp);
337        }
338        let nn = r.n(lim.max_nodes, "nodes")?;
339        let mut state_seen: Vec<Vec<bool>> =
340            g.loops.iter().map(|l| vec![false; l.init.len()]).collect();
341        let mut intern: HashMap<INode, IId> = HashMap::new();
342        for i in 0..nn {
343            let tag = r.u8()?;
344            let opnd = |r: &mut Rd| -> Result<IId, WireError> {
345                let d = r.var()?;
346                if d == 0 || d > i as u64 {
347                    return err("operand must be an earlier node");
348                }
349                Ok(IId((i as u64 - d) as u32))
350            };
351            let lk = |r: &mut Rd, g: &IntGraph| -> Result<(u32, u16), WireError> {
352                let l = r.var()?;
353                let k = r.var()?;
354                if l >= g.loops.len() as u64 || k >= g.loops[l as usize].init.len() as u64 {
355                    return err("loop reference");
356                }
357                Ok((l as u32, k as u16))
358            };
359            let n = match tag {
360                0 => {
361                    let t = ty_of(r.u8()?)?;
362                    let z = r.u32()?;
363                    let v = if t == IType::I32 {
364                        (z >> 1) ^ (z & 1).wrapping_neg()
365                    } else {
366                        z
367                    };
368                    if t == IType::Bool && v > 1 {
369                        return err("bool constant");
370                    }
371                    INode::Const(t, v)
372                }
373                1 => {
374                    let p = r.var()?;
375                    let t = ty_of(r.u8()?)?;
376                    if p >= params.len() as u64 || params[p as usize] != t {
377                        return err("parameter not in the ABI");
378                    }
379                    INode::Param(p as u16, t)
380                }
381                2 => {
382                    let op = *BINS.get(r.u8()? as usize).ok_or(WireError("op".into()))?;
383                    INode::Bin(op, opnd(&mut r)?, opnd(&mut r)?)
384                }
385                3 => {
386                    let op = *UNS.get(r.u8()? as usize).ok_or(WireError("op".into()))?;
387                    INode::Un(op, opnd(&mut r)?)
388                }
389                4 => {
390                    let op = *CMPS.get(r.u8()? as usize).ok_or(WireError("op".into()))?;
391                    INode::Cmp(op, opnd(&mut r)?, opnd(&mut r)?)
392                }
393                5 => INode::Select(opnd(&mut r)?, opnd(&mut r)?, opnd(&mut r)?),
394                6 => {
395                    let t = ty_of(r.u8()?)?;
396                    INode::Bitcast(t, opnd(&mut r)?)
397                }
398                7 => {
399                    let b = r.var()?;
400                    if b >= nbufs {
401                        return err("buffer not in the ABI");
402                    }
403                    INode::Load(b as u16, opnd(&mut r)?)
404                }
405                10 => INode::Tok,
406                11 | 12 => {
407                    let b = r.var()?;
408                    if b >= nbufs {
409                        return err("buffer not in the ABI");
410                    }
411                    let i = opnd(&mut r)?;
412                    if tag == 11 {
413                        INode::LoadRw(b as u16, i, opnd(&mut r)?)
414                    } else {
415                        let v = opnd(&mut r)?;
416                        INode::Store(b as u16, i, v, opnd(&mut r)?)
417                    }
418                }
419                8 => {
420                    let (l, k) = lk(&mut r, &g)?;
421                    if g.loops[l as usize].init[k as usize].index() >= i {
422                        return err("loop init must precede its state");
423                    }
424                    if std::mem::replace(&mut state_seen[l as usize][k as usize], true) {
425                        return err("duplicate loop state");
426                    }
427                    INode::State(l, k)
428                }
429                9 => {
430                    let (l, k) = lk(&mut r, &g)?;
431                    let lp = &g.loops[l as usize];
432                    if lp.cond.index() >= i || lp.next.iter().any(|x| x.index() >= i) {
433                        return err("loop body must precede its result");
434                    }
435                    if !state_seen[l as usize].iter().all(|&s| s) {
436                        return err("loop result before all its states");
437                    }
438                    INode::LoopOut(l, k)
439                }
440                _ => return err("node tag"),
441            };
442            let (ty, scope) = g.try_infer(n).map_err(|e| WireError(e.into()))?;
443            g.nodes.push(n);
444            g.types.push(ty);
445            g.scope.push(scope);
446            intern.entry(n).or_insert(IId(i as u32));
447        }
448        g.intern = intern;
449        let n = g.nodes.len();
450        // loops: closed, typed, scoped
451        for (l, seen) in state_seen.iter().enumerate().take(g.loops.len()) {
452            let lp = g.loops[l].clone();
453            if !seen.iter().all(|&s| s) {
454                return err("loop without all its states");
455            }
456            if lp.cond.index() >= n || lp.next.iter().chain(&lp.init).any(|x| x.index() >= n) {
457                return err("loop reference out of range");
458            }
459            if g.ty(lp.cond) != IType::Bool {
460                return err("loop condition must be bool");
461            }
462            let me = Some(l as u32);
463            let within = |g: &IntGraph, x: IId, s: Option<u32>| {
464                g.try_inner(g.scope[x.index()], s).ok() == Some(s)
465            };
466            if !within(&g, lp.cond, me) || !lp.next.iter().all(|&x| within(&g, x, me)) {
467                return err("loop body uses an unrelated scope");
468            }
469            if !lp.init.iter().all(|&x| within(&g, x, lp.parent)) {
470                return err("loop init outside the enclosing scope");
471            }
472            for (k, &x) in lp.next.iter().enumerate() {
473                if g.ty(x) != g.ty(lp.init[k]) {
474                    return err("loop state changes type");
475                }
476            }
477        }
478        // worst-case work
479        let mut cost = 0u64;
480        for i in 0..n {
481            let mut c = 1u64;
482            let mut s = g.scope[i];
483            while let Some(l) = s {
484                c = c.saturating_mul(caps[l as usize].max(1) as u64);
485                s = g.loops[l as usize].parent;
486            }
487            cost = cost.saturating_add(c);
488        }
489        if cost > lim.max_cost {
490            return err(format!("worst-case cost {cost} over limit"));
491        }
492        let no = r.n(outs.len(), "outputs")?;
493        if no != outs.len() {
494            return err("output count");
495        }
496        let mut o = Vec::with_capacity(no);
497        for &t in outs {
498            let x = r.u32()? as usize;
499            if x >= n || g.scope[x].is_some() || g.types[x] != t {
500                return err("output not a top-level value of the ABI type");
501            }
502            o.push(IId(x as u32));
503        }
504        if r.p != bytes.len() {
505            return err("trailing bytes");
506        }
507        Ok((g, o))
508    }
509}