Skip to main content

cuttlefish_host/
handles.rs

1//! Files the host holds open on a guest's behalf.
2//!
3//! This is the host side of the rule that bulk data never enters guest memory. A
4//! block receives a handle and a length, then pulls bounded windows; the host
5//! seeks and reads each window straight off disk. Neither side ever holds the
6//! whole file, so guest memory tracks the window size a block chose rather than
7//! the size of its input.
8
9use std::collections::HashMap;
10use std::fs::File;
11use std::io::{Read, Seek, SeekFrom};
12use std::path::Path;
13
14/// One file held open for a job.
15struct OpenFile {
16    file: File,
17    len: u64,
18}
19
20/// A job's open files.
21///
22/// Scoping this to a single job is a security property rather than tidiness.
23/// Because the table lives and dies with one job, a handle from another job
24/// names nothing here — which is what lets [`Handles::slice`] skip a capability
25/// check entirely. The check happened once, at [`Handles::open`], and a handle
26/// cannot be forged into a reference to someone else's data.
27#[derive(Default)]
28pub struct Handles {
29    next: u32,
30    open: HashMap<u32, OpenFile>,
31}
32
33/// Why a handle operation failed.
34#[derive(Debug, thiserror::Error)]
35pub enum HandleError {
36    /// The handle does not belong to this job, or never existed.
37    #[error("no such handle: {0}")]
38    BadHandle(u32),
39    /// The requested offset is beyond the end of the file.
40    #[error("offset {offset} is past end of file ({len} bytes)")]
41    OffsetPastEnd {
42        /// The offset that was asked for.
43        offset: u64,
44        /// The file's actual length.
45        len: u64,
46    },
47    /// The window was too small to contain even one whole character.
48    #[error("window of {0} bytes is too small to hold one character")]
49    WindowTooSmall(u64),
50    /// Underlying I/O failure.
51    #[error(transparent)]
52    Io(#[from] std::io::Error),
53}
54
55/// One window of a file.
56pub struct Window {
57    /// The window's contents.
58    pub text: String,
59    /// Where the returned text actually ended; see [`Handles::slice`].
60    pub next_offset: u64,
61}
62
63impl Handles {
64    /// Open a file, returning its handle and length.
65    ///
66    /// The caller is responsible for having capability-checked `path` first —
67    /// this type deliberately knows nothing about capabilities, so that the
68    /// check lives in exactly one place rather than being half-enforced here.
69    pub fn open(&mut self, path: &Path) -> Result<(u32, u64), HandleError> {
70        let file = File::open(path)?;
71        let len = file.metadata()?.len();
72
73        let handle = self.next;
74        self.next += 1;
75        self.open.insert(handle, OpenFile { file, len });
76        Ok((handle, len))
77    }
78
79    /// Read one window, truncated to a UTF-8 character boundary.
80    ///
81    /// The truncation is the subtle part, and the reason [`Window::next_offset`]
82    /// exists at all. A caller walking a file picks window sizes with no idea
83    /// where characters begin, so a naive read splits a multi-byte character at
84    /// nearly every seam and yields mojibake. Instead the window is cut back to
85    /// the last complete character and `next_offset` reports where that landed —
86    /// so a caller resuming from `next_offset`, rather than advancing by the
87    /// length it requested, never observes a split.
88    ///
89    /// Reading past the end is not an error: the window is clamped, because a
90    /// block asking for a full window at the tail of a file is behaving
91    /// correctly. Starting past the end *is* an error, since that indicates the
92    /// caller has lost track of where it is.
93    pub fn slice(&mut self, handle: u32, offset: u64, len: u64) -> Result<Window, HandleError> {
94        let f = self
95            .open
96            .get_mut(&handle)
97            .ok_or(HandleError::BadHandle(handle))?;
98
99        if offset > f.len {
100            return Err(HandleError::OffsetPastEnd { offset, len: f.len });
101        }
102
103        let want = len.min(f.len - offset) as usize;
104        let mut buf = vec![0u8; want];
105        f.file.seek(SeekFrom::Start(offset))?;
106        f.file.read_exact(&mut buf)?;
107
108        let valid = match std::str::from_utf8(&buf) {
109            Ok(_) => buf.len(),
110            Err(e) => e.valid_up_to(),
111        };
112
113        // A window landing entirely inside one character would otherwise return
114        // empty forever, and a caller looping until it reaches the end would
115        // spin making no progress and reporting no problem. Failing is strictly
116        // better than that silence.
117        if valid == 0 && !buf.is_empty() {
118            return Err(HandleError::WindowTooSmall(len));
119        }
120        buf.truncate(valid);
121
122        Ok(Window {
123            text: String::from_utf8(buf).expect("truncated at a validated boundary"),
124            next_offset: offset + valid as u64,
125        })
126    }
127}