Skip to main content

iris_source/
window.rs

1//! A file view that slides inside a fixed reservation of address space.
2//!
3//! # What this is for
4//!
5//! A decoder in the sandbox asks for byte ranges. The host has to put those bytes at an address the
6//! decoder can read, and the obvious way to do that is to map the whole file, which is what the
7//! prior art does. Mapping the whole file is fast and it costs address space proportional to the
8//! dataset, which is fine until the dataset is larger than the address space or until there are a
9//! thousand of them open at once.
10//!
11//! A window is the other trade. It reserves a fixed span of addresses once, maps a piece of the file
12//! into it, and moves that piece when a request falls outside it. The address space cost is constant
13//! and chosen by the host rather than by the data. The cost is that a request which does not fall in
14//! the current view pays for a remap, so the win depends entirely on requests being clustered, which
15//! for a columnar scan they are.
16//!
17//! # The property that matters
18//!
19//! When the view moves, everything the old view pointed at has to stop being readable. Not stop
20//! being correct, stop being readable. A decoder that reads through a stale pointer and gets bytes
21//! from the part of the file that used to be there produces an answer that is wrong and looks right,
22//! and there is nothing downstream that can catch it. So the vacated range does not become free
23//! memory and does not become zeroes: it goes back to being reserved and unreadable, and a read
24//! through a stale pointer faults.
25//!
26//! Rust's own rules cover the safe path already, because [`Window::range`] borrows the window
27//! mutably and hands back a slice tied to that borrow, so a slice cannot outlive the view it came
28//! from. That is not the case the gate is about. The case the gate is about is a raw address that
29//! crossed into a sandbox, where the borrow checker is not present, and the test for it is in
30//! `tests/window.rs`.
31//!
32//! # Alignment
33//!
34//! Offsets and lengths are rounded by two different numbers, which is a Windows distinction that
35//! Unix does not have and is the easiest thing here to get wrong on a machine where they happen to
36//! be equal. A mapping offset has to be a multiple of the allocation granularity, sixty four
37//! kibibytes on Windows and the page size everywhere else. A mapping length has to be a multiple of
38//! the page size. Rounding a length up by the allocation granularity instead looks correct on Unix
39//! and fails on Windows at the end of a file, because the pages past the end of the section are not
40//! part of it.
41//!
42//! A view is therefore rounded out at both ends and is usually larger than what was asked for. The
43//! slice handed back is not: it is exactly the requested range, cut out of the middle.
44
45use std::fmt;
46use std::fs::File;
47use std::io;
48use std::path::Path;
49
50use crate::sys;
51
52/// How much address space a window reserves when the caller does not say.
53///
54/// Large enough that a column chunk in a scan lands inside it and no remap happens, small enough
55/// that a host can hold many of them. It is a default rather than a tuned number, because the
56/// measurement that would tune it needs a real scan and there is not one yet.
57pub const DEFAULT_SPAN: usize = 4 * 1024 * 1024;
58
59/// What can go wrong when opening a window or moving it.
60#[derive(Debug, thiserror::Error)]
61#[non_exhaustive]
62pub enum WindowError {
63    /// The operating system refused a reservation, a mapping or a file operation.
64    #[error("{operation} failed: {source}")]
65    Os {
66        /// Which step failed, so the error says where in the sequence it stopped.
67        operation: &'static str,
68        /// What the operating system said.
69        #[source]
70        source: io::Error,
71    },
72
73    /// The requested range runs past the end of the file.
74    #[error("bytes {at}..{end} were asked for and the file is {len} bytes long")]
75    OutOfBounds {
76        /// Where the request started.
77        at: u64,
78        /// One past where it ended.
79        end: u64,
80        /// How long the file is.
81        len: u64,
82    },
83
84    /// The request cannot be covered by one view, so this window cannot serve it at all.
85    ///
86    /// This is a configuration error rather than a data error: the window was opened with a span
87    /// smaller than the largest range it was going to be asked for.
88    ///
89    /// `wanted` can be larger than the length that was requested, and the difference is not a
90    /// mistake. A view has to start on an alignment boundary, so a request that starts part of the
91    /// way into one needs the bytes before it mapped as well, and `wanted` is what the mapping has
92    /// to cover rather than what the caller asked to read.
93    #[error("covering that range needs {wanted} bytes in one view and this window reserved {span}")]
94    TooLarge {
95        /// How many bytes a single view would have to cover, including alignment.
96        wanted: usize,
97        /// How many the window can map at once.
98        span: usize,
99    },
100}
101
102type Result<T> = std::result::Result<T, WindowError>;
103
104fn os(operation: &'static str) -> impl FnOnce(io::Error) -> WindowError {
105    move |source| WindowError::Os { operation, source }
106}
107
108/// Where the current view sits. Both fields are already rounded to the platform's alignments.
109#[derive(Clone, Copy, Debug, PartialEq, Eq)]
110struct View {
111    at: u64,
112    len: u64,
113}
114
115impl View {
116    /// Whether the half open range `at..at + len` is entirely inside this view.
117    fn covers(self, at: u64, len: u64) -> bool {
118        let Some(want_end) = at.checked_add(len) else {
119            return false;
120        };
121        let Some(have_end) = self.at.checked_add(self.len) else {
122            return false;
123        };
124        at >= self.at && want_end <= have_end
125    }
126}
127
128/// A read only view of part of a file, at a fixed address, that can be moved.
129///
130/// See the [module documentation](self) for what this is for and what it guarantees. In short: the
131/// address the window lives at is reserved once and held until the window is dropped, and moving the
132/// view makes the bytes that used to be there unreadable rather than stale.
133pub struct Window {
134    // Declaration order is drop order, and these three have to come apart in this order. The
135    // reservation unmaps the view, which has to happen before the section it was mapped from is
136    // closed, which has to happen before the file underneath it is. Both platforms tolerate the
137    // other order, because a mapping keeps its own reference to what it maps, but relying on that
138    // means the code is correct for a reason that is not visible where the code is.
139    reservation: sys::Reservation,
140    /// None for an empty file, which has nothing to map and no section to map it from.
141    backing: Option<sys::Backing>,
142    /// Held because the mapping refers to it. On Unix the mapping was made from a descriptor number
143    /// rather than from a reference, so dropping the file early would close it out from under the
144    /// next slide.
145    file: File,
146    len: u64,
147    view: Option<View>,
148    slides: u64,
149}
150
151impl Window {
152    /// Opens `path` read only and reserves [`DEFAULT_SPAN`] bytes of address space for it.
153    ///
154    /// # Errors
155    ///
156    /// If the file cannot be opened or measured, or the reservation is refused.
157    pub fn open(path: &Path) -> Result<Self> {
158        let file = File::open(path).map_err(os("opening the file"))?;
159        Self::with_span(file, DEFAULT_SPAN)
160    }
161
162    /// Reserves `span` bytes of address space for `file`, rounded up to what the platform allows.
163    ///
164    /// The span bounds the largest range this window can serve in one piece, so it has to be at
165    /// least as large as the largest request plus the alignment slack in front of it. A view starts
166    /// on an allocation boundary, so in the worst case that slack is one whole unit of allocation
167    /// granularity, which is sixty four kibibytes on Windows and the page size elsewhere. A span of
168    /// exactly one unit therefore serves a request only when it happens not to straddle a boundary,
169    /// and a span for a largest request of `n` wants to be at least `n` plus one unit.
170    ///
171    /// A span of zero is rounded up to one unit of allocation granularity rather than rejected,
172    /// because a window over an empty file is a reasonable thing to ask for and reserving nothing is
173    /// not.
174    ///
175    /// # Errors
176    ///
177    /// If the file cannot be measured, or the reservation is refused.
178    pub fn with_span(file: File, span: usize) -> Result<Self> {
179        let len = file.metadata().map_err(os("measuring the file"))?.len();
180
181        let unit = sys::granularity();
182        let span = round_up(span.max(1), unit).ok_or(WindowError::TooLarge {
183            wanted: span,
184            span: usize::MAX,
185        })?;
186
187        let reservation = sys::Reservation::new(span).map_err(os("reserving address space"))?;
188
189        // An empty file has no section to map from. Windows refuses to create one and Unix would
190        // accept it and produce a mapping nothing may read, so neither platform gains anything from
191        // having one, and every path below already handles a window with no view.
192        let backing = if len == 0 {
193            None
194        } else {
195            Some(sys::Backing::new(&file).map_err(os("preparing the file for mapping"))?)
196        };
197
198        Ok(Self {
199            reservation,
200            backing,
201            file,
202            len,
203            view: None,
204            slides: 0,
205        })
206    }
207
208    /// How long the file is.
209    #[must_use]
210    pub fn len(&self) -> u64 {
211        self.len
212    }
213
214    /// Whether the file is empty.
215    #[must_use]
216    pub fn is_empty(&self) -> bool {
217        self.len == 0
218    }
219
220    /// The file this window reads from.
221    #[must_use]
222    pub fn file(&self) -> &File {
223        &self.file
224    }
225
226    /// How much address space this window holds.
227    #[must_use]
228    pub fn span(&self) -> usize {
229        self.reservation.span()
230    }
231
232    /// How many times the view has moved since the window was opened.
233    ///
234    /// A scan whose requests are clustered slides rarely. One that slides on nearly every request is
235    /// either reading in an order the window is the wrong structure for or was opened with too small
236    /// a span, and this is how a host tells those apart from the outside.
237    #[must_use]
238    pub fn slides(&self) -> u64 {
239        self.slides
240    }
241
242    /// The bytes of the file from `at`, `len` of them.
243    ///
244    /// If the range is already inside the current view this costs a comparison. If it is not, the
245    /// view moves first, which unmaps the old one and maps a new one, and every address the old view
246    /// covered stops being readable.
247    ///
248    /// The returned slice borrows the window, so it cannot outlive the view it came from. That is
249    /// the safe half of the guarantee in the [module documentation](self); the other half is about
250    /// raw addresses and is a property of the mapping rather than of this signature.
251    ///
252    /// # Errors
253    ///
254    /// [`WindowError::OutOfBounds`] if the range runs past the end of the file,
255    /// [`WindowError::TooLarge`] if no single view could cover it, and [`WindowError::Os`] if the
256    /// remap is refused.
257    pub fn range(&mut self, at: u64, len: usize) -> Result<&[u8]> {
258        let wide = len as u64;
259        let end = at.saturating_add(wide);
260        if end > self.len {
261            return Err(WindowError::OutOfBounds {
262                at,
263                end,
264                len: self.len,
265            });
266        }
267
268        // What a view has to cover is the request plus however far into an alignment unit it starts,
269        // because a view cannot start anywhere else. A request of exactly the span therefore does
270        // not fit unless it happens to be aligned, which is worth an error that says so rather than
271        // a mapping that quietly comes up short.
272        let unit = sys::granularity() as u64;
273        let skew = usize::try_from(at % unit).unwrap_or(usize::MAX);
274        let needed = skew.saturating_add(len);
275        if needed > self.span() {
276            return Err(WindowError::TooLarge {
277                wanted: needed,
278                span: self.span(),
279            });
280        }
281
282        if !self.view.is_some_and(|view| view.covers(at, wide)) {
283            self.slide_to(at)?;
284        }
285
286        let Some(view) = self.view else {
287            // No view means an empty file, which the bounds check only lets through for a zero
288            // length request. There is nothing mapped and nothing to hand back.
289            debug_assert_eq!(len, 0);
290            return Ok(&[]);
291        };
292
293        // The view starts at or before `at` by construction, so this does not go negative, and it is
294        // smaller than the span, so it fits.
295        let offset = usize::try_from(at - view.at).unwrap_or(0);
296
297        // SAFETY: the view is mapped and readable across view.at..view.at + view.len, which the
298        // check above established contains at..at + len, so offset..offset + len is inside one
299        // mapping. The slice borrows self for its lifetime and nothing can move the view without
300        // &mut self, so the mapping outlives the slice.
301        let bytes = unsafe {
302            std::slice::from_raw_parts(self.reservation.base().as_ptr().add(offset), len)
303        };
304        Ok(bytes)
305    }
306
307    /// Moves the view so that it starts at the alignment boundary at or below `at`.
308    ///
309    /// The view runs from there for as much of the span as the file has left, rounded up to a page
310    /// so that the last view of a file covers the tail. Starting the view at the request rather than
311    /// centred on it is deliberate: a scan reads forwards, so the bytes worth having mapped are the
312    /// ones after the request and not the ones before it.
313    ///
314    /// The caller has already established that the request fits, so this does not check again.
315    fn slide_to(&mut self, at: u64) -> Result<()> {
316        let Some(backing) = self.backing.as_ref() else {
317            return Ok(());
318        };
319
320        let unit = sys::granularity() as u64;
321        let start = at - (at % unit);
322        let remaining = self.len - start;
323
324        let span = self.reservation.span();
325        let available = usize::try_from(remaining).unwrap_or(usize::MAX);
326        // Round up to a page so the last view reaches the end of the file. The bytes between the end
327        // of the file and the end of that page read as zero on both platforms and are never inside a
328        // slice this hands out, because every slice is bounded by the file length.
329        let view_len = round_up(available.min(span), sys::page())
330            .unwrap_or(span)
331            .min(span);
332
333        // A zero length request at exactly the end of the file has nothing after it to map, and a
334        // mapping of no bytes is refused by both platforms with an error that says nothing useful.
335        // The window is left with no view, which is the state it is already in for an empty file and
336        // which the caller path below already handles by handing back an empty slice.
337        //
338        // The fuzzer found this in the first minute it ran, on the input `range(len, 0)`, which is
339        // the shape a decoder produces when it asks for a column that happens to be empty and sits
340        // last in the file. Nothing about it looks like an edge case from inside the arithmetic.
341        if view_len == 0 {
342            self.view = None;
343            return self
344                .reservation
345                .unmap()
346                .map_err(os("unmapping the previous view"));
347        }
348
349        // The old view goes first even though Unix could replace it in one call, so that a failed
350        // map leaves the window with nothing mapped rather than with a view it thinks has moved.
351        // Windows has to do it in this order anyway, and a state machine with one shape on both
352        // platforms is worth one extra call on the platform that could have skipped it.
353        self.view = None;
354        self.reservation
355            .unmap()
356            .map_err(os("unmapping the previous view"))?;
357        self.reservation
358            .map(backing, start, view_len)
359            .map_err(os("mapping the file into the reservation"))?;
360
361        self.view = Some(View {
362            at: start,
363            len: view_len as u64,
364        });
365        self.slides += 1;
366        Ok(())
367    }
368
369    /// The address the reservation starts at.
370    ///
371    /// This is the address a host would hand to a sandbox, and it does not move for the life of the
372    /// window, which is the point of reserving rather than mapping. Reading through it is only
373    /// defined for the part of the reservation the current view covers. It is public so that the
374    /// stale read tests can hold an address across a slide and check that it stopped being readable,
375    /// which is a property no safe signature can express.
376    #[must_use]
377    pub fn address(&self) -> *const u8 {
378        self.reservation.base().as_ptr()
379    }
380
381    /// Where the current view sits in the file and how long it is, or `None` if nothing is mapped.
382    ///
383    /// The length is the mapped length, which is rounded out to a page and so is usually longer than
384    /// the part of the file it covers.
385    #[must_use]
386    pub fn mapped(&self) -> Option<(u64, usize)> {
387        self.view
388            .map(|view| (view.at, usize::try_from(view.len).unwrap_or(usize::MAX)))
389    }
390}
391
392impl fmt::Debug for Window {
393    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
394        // The reservation, the section and the file are left out on purpose. Their debug output is
395        // a raw address and two handle numbers, which say nothing a reader can use and change on
396        // every run, and this is the type somebody prints when a slide went wrong.
397        f.debug_struct("Window")
398            .field("len", &self.len)
399            .field("span", &self.span())
400            .field("view", &self.view)
401            .field("slides", &self.slides)
402            .finish_non_exhaustive()
403    }
404}
405
406/// `value` rounded up to the next multiple of `unit`, or `None` if that overflows.
407fn round_up(value: usize, unit: usize) -> Option<usize> {
408    debug_assert!(
409        unit != 0,
410        "an alignment of zero is not something a platform reports"
411    );
412    let over = value % unit;
413    if over == 0 {
414        return Some(value);
415    }
416    value.checked_add(unit - over)
417}
418
419#[cfg(test)]
420mod tests {
421    use super::*;
422
423    #[test]
424    fn rounding_up_leaves_exact_multiples_alone() {
425        assert_eq!(round_up(0, 4096), Some(0));
426        assert_eq!(round_up(4096, 4096), Some(4096));
427        assert_eq!(round_up(1, 4096), Some(4096));
428        assert_eq!(round_up(4097, 4096), Some(8192));
429        assert_eq!(round_up(usize::MAX, 4096), None);
430    }
431
432    #[test]
433    fn a_view_covers_a_range_inside_it_and_nothing_else() {
434        let view = View { at: 100, len: 50 };
435        assert!(view.covers(100, 50));
436        assert!(view.covers(120, 10));
437        assert!(view.covers(150, 0));
438        assert!(!view.covers(99, 1));
439        assert!(!view.covers(150, 1));
440        assert!(!view.covers(140, 11));
441        // A length that would overflow the addition is not covered by anything.
442        assert!(!view.covers(u64::MAX, 2));
443    }
444}