iris_guard/lib.rs
1//! Structural validation of Arrow arrays crossing the sandbox boundary.
2//!
3//! A sandbox stops a decoder from reading the host's memory. It does nothing at all about the
4//! numbers the decoder hands back, and those numbers are offsets, lengths and buffer indices that
5//! the host is about to use to read its own memory. That gap is the difference between a security
6//! claim and a security property, and this crate is where the gap gets closed.
7//!
8//! ```
9//! use arrow_schema::{DataType, Field, Schema};
10//! use iris_abi::Node;
11//!
12//! let schema = Schema::new(vec![Field::new("a", DataType::Int64, false)]);
13//! let nodes = [Node { length: 2, null_count: 0 }];
14//! let buffers: Vec<Vec<u8>> = vec![Vec::new(), 1i64.to_le_bytes().into_iter().chain(2i64.to_le_bytes()).collect()];
15//!
16//! iris_guard::check(&schema, 2, &nodes, &buffers)?;
17//! # Ok::<(), iris_guard::Violation>(())
18//! ```
19//!
20//! # The property
21//!
22//! If [`check`] returns `Ok` then every offset in the batch is inside the buffer it indexes, every
23//! buffer is long enough for the number of slots its array claims, and every child array is long
24//! enough for the parent that points into it. Arrays that pass can be read without a read leaving
25//! the bytes it was given.
26//!
27//! That is a bounds property and nothing more. Whether a `Utf8` column holds well formed UTF-8 is a
28//! correctness question rather than a bounds question, because reading a badly encoded string
29//! cannot leave the buffer, so it is left to Arrow. Keeping the line there is what keeps the surface
30//! that gets fuzzed the surface whose failure is silent.
31//!
32//! # Why the checks are here and not left to Arrow
33//!
34//! Arrow validates an `ArrayData` when it is built, and that validation is good. It is also the
35//! wrong place for two of these checks and the wrong shape for the rest.
36//!
37//! The wrong place, because a schema nested a hundred thousand deep and a length that overflows when
38//! it is multiplied by a width both have to be refused *before* anything walks the schema or
39//! allocates against the length. By the time there is an `ArrayData` to validate, the recursion has
40//! already happened.
41//!
42//! The wrong shape, because what comes back is a message. This crate returns [`Invariant`], so a
43//! host can count refusals by rule, alert on one kind and not another, and say which rule failed
44//! without matching on prose.
45//!
46//! So the batch is checked here first, and then Arrow validates what it is handed as an independent
47//! second opinion. That means the structural checks run twice, which is a real cost, measured rather
48//! than assumed, and written down against a decision rule that was committed before the number was
49//! known. Removing the second pass means building arrays with validation skipped, which is an
50//! `unsafe` call in the runtime that this crate's fuzzer would have to be the only thing standing
51//! behind. That trade is worth making when there is a number saying it matters and not before.
52//!
53//! # What it does not carry
54//!
55//! Unions, dictionaries, run end encoding and the view types are refused by name. Each needs
56//! something the batch cannot carry yet, and the two whose checks are the hard part have those
57//! checks written and tested anyway, in [`check_dictionary`] and [`check_views`].
58
59#![forbid(unsafe_code)]
60
61pub mod corpus;
62
63mod check;
64mod error;
65mod indirect;
66mod layout;
67
68pub use check::{MAX_DEPTH, check, check_schema};
69pub use error::{Invariant, Result, Violation};
70pub use indirect::{VIEW_INLINE, VIEW_WIDTH, check_dictionary, check_views};
71pub use layout::{Layout, layout};
72
73/// The version of this crate, as reported by build metadata.
74pub const VERSION: &str = env!("CARGO_PKG_VERSION");