Skip to main content

detcore_model/
fd.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9use serde::Deserialize;
10use serde::Serialize;
11
12use crate::pid::DetTid;
13
14/// For now we use the definiton of `RawFd` from `std::os`.
15// (Workaround: reexporting this type directly triggers a rust-anlazer glitch.)
16pub type RawFd = std::os::unix::io::RawFd;
17
18/// Nondeterministic "physical" inode: a host file's identity, its device and
19/// its inode number on that device.
20///
21/// An inode number alone does not name a file: each filesystem numbers its own
22/// inodes, so a file in `/sys` and one on the root filesystem can share a
23/// number. Keyed by the number alone, Hermit gave two such files ONE
24/// deterministic inode, so a guest comparing inode numbers could take them for
25/// the same file, and which files merged depended on how the host happened to
26/// number them.
27#[derive(
28    Debug,
29    Clone,
30    Copy,
31    PartialEq,
32    Eq,
33    Hash,
34    PartialOrd,
35    Ord,
36    Serialize,
37    Deserialize
38)]
39pub struct RawInode {
40    /// The host `st_dev`.
41    pub dev: u64,
42    /// The host `st_ino` on that device.
43    pub ino: u64,
44}
45
46impl RawInode {
47    /// The host file identified by device `dev` and inode number `ino`.
48    pub const fn new(dev: u64, ino: u64) -> Self {
49        Self { dev, ino }
50    }
51}
52
53impl std::fmt::Display for RawInode {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        write!(f, "{}:{}", self.dev, self.ino)
56    }
57}
58
59/// Deterministic "virtual" inode.
60///
61/// Deliberately a newtype rather than an alias for [`RawInode`]. As an alias
62/// the two were the same type to the compiler, so a host inode could be used
63/// wherever a deterministic one was required and nothing diagnosed it; that is
64/// how raw host inodes reached guest-visible `ResourceID`s.
65///
66/// There is intentionally no `From<RawInode>` impl. The only supported way to
67/// turn a host inode into a `DetInode` is the determinization boundary in
68/// `tool_global` (`determinize_inode` -> `add_inode`), which mints values from
69/// a monotonic counter. [`DetInode::mint`] exists for that boundary and for the
70/// handful of compile-time constants; every call site is a deliberate,
71/// auditable assertion that the value is already deterministic.
72#[derive(
73    Debug,
74    Clone,
75    Copy,
76    PartialEq,
77    Eq,
78    Hash,
79    PartialOrd,
80    Ord,
81    Serialize,
82    Deserialize
83)]
84pub struct DetInode(u64);
85
86impl DetInode {
87    /// Assert that `value` is a deterministic inode.
88    ///
89    /// Reserved for the determinization boundary and for compile-time
90    /// constants. Passing a host inode here reintroduces the leak this newtype
91    /// exists to prevent.
92    pub const fn mint(value: u64) -> Self {
93        Self(value)
94    }
95
96    /// The underlying integer, for writing into guest-visible stat buffers.
97    pub const fn as_raw(self) -> u64 {
98        self.0
99    }
100}
101
102impl std::fmt::Display for DetInode {
103    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104        write!(f, "{}", self.0)
105    }
106}
107
108/// Identity of a Linux descriptor table (`files_struct`).
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
110pub struct FilesId {
111    creator: DetTid,
112    generation: u64,
113}
114
115impl FilesId {
116    /// Task that created this model descriptor-table incarnation.
117    /// This is data for equality checks, not kernel table attestation.
118    pub const fn creator(self) -> DetTid {
119        self.creator
120    }
121
122    /// Exact model exec generation of this descriptor table.
123    pub const fn generation(self) -> u64 {
124        self.generation
125    }
126
127    /// Create the first descriptor table owned by a task.
128    pub const fn initial(creator: DetTid) -> Self {
129        Self {
130            creator,
131            generation: 0,
132        }
133    }
134
135    /// Create a copied descriptor table for a newly created task.
136    pub const fn forked(creator: DetTid) -> Self {
137        Self::initial(creator)
138    }
139
140    /// Create the replacement table installed by exec.
141    pub fn for_exec(self, creator: DetTid) -> Self {
142        let generation = if self.creator == creator {
143            self.generation + 1
144        } else {
145            0
146        };
147        Self {
148            creator,
149            generation,
150        }
151    }
152}
153
154/// Identity of one numeric descriptor slot within a descriptor table.
155#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
156pub struct FdSlot {
157    /// Descriptor table containing the slot.
158    pub files: FilesId,
159    /// Numeric descriptor within the table.
160    pub fd: RawFd,
161}
162
163/// Identity of a Linux open file description (`struct file`).
164#[derive(
165    Debug,
166    Clone,
167    Copy,
168    PartialEq,
169    Eq,
170    PartialOrd,
171    Ord,
172    Hash,
173    Serialize,
174    Deserialize
175)]
176pub struct OpenFileId {
177    creator: DetTid,
178    sequence: u64,
179}
180
181const SOCKET_SEQUENCE_DOMAIN: u64 = 1 << 63;
182
183impl OpenFileId {
184    /// Create an identity from the task that observed the open and its local sequence.
185    pub const fn new(creator: DetTid, sequence: u64) -> Self {
186        assert!(sequence < SOCKET_SEQUENCE_DOMAIN);
187        Self { creator, sequence }
188    }
189
190    /// Create a socket identity from a backend-independent socket-open sequence.
191    pub const fn new_socket(creator: DetTid, sequence: u64) -> Self {
192        assert!(sequence < SOCKET_SEQUENCE_DOMAIN);
193        Self {
194            creator,
195            sequence: SOCKET_SEQUENCE_DOMAIN | sequence,
196        }
197    }
198
199    /// Whether this identity came from the socket-specific allocation domain.
200    pub const fn is_socket(self) -> bool {
201        self.sequence & SOCKET_SEQUENCE_DOMAIN != 0
202    }
203
204    // TODO-HUMAN-REVIEW(PR-886): Review stable socket-cookie identity encoding.
205    /// Encode the per-task socket-open sequence as a deterministic socket cookie.
206    ///
207    /// Linux promises that live socket cookies are unique and that descriptor aliases
208    /// for one open file description share a cookie. Detcore's virtual task IDs and a
209    /// socket-specific sequence provide those same properties for realistic descriptor
210    /// counts while avoiding the kernel's host-global cookie allocator. The sequence is
211    /// independent of regular-file opens because backend loaders do not expose the same
212    /// dynamic-linker file operations to Detcore.
213    pub fn deterministic_socket_cookie(self) -> u64 {
214        let creator = self.creator.as_raw() as u32 as u64;
215        (creator << 32) | (self.sequence & u32::MAX as u64)
216    }
217}
218
219/// Names the identity for diagnostics, without the socket-domain bit.
220impl std::fmt::Display for OpenFileId {
221    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
222        let kind = if self.is_socket() { "socket" } else { "file" };
223        write!(
224            f,
225            "{kind} {} opened by thread {}",
226            self.sequence & !SOCKET_SEQUENCE_DOMAIN,
227            self.creator.as_raw()
228        )
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    #[test]
237    fn deterministic_socket_cookies_track_open_file_identity() {
238        let first = OpenFileId::new_socket(DetTid::from_raw(3), 7);
239        let alias = first;
240        let next = OpenFileId::new_socket(DetTid::from_raw(3), 8);
241        let other_task = OpenFileId::new_socket(DetTid::from_raw(4), 7);
242
243        assert_ne!(first.deterministic_socket_cookie(), 0);
244        assert!(first.is_socket());
245        assert!(!OpenFileId::new(DetTid::from_raw(3), 7).is_socket());
246        assert_eq!(
247            first.deterministic_socket_cookie(),
248            alias.deterministic_socket_cookie()
249        );
250        assert_ne!(
251            first.deterministic_socket_cookie(),
252            next.deterministic_socket_cookie()
253        );
254        assert_ne!(
255            first.deterministic_socket_cookie(),
256            other_task.deterministic_socket_cookie()
257        );
258        assert_ne!(first, OpenFileId::new(DetTid::from_raw(3), 7));
259        assert_eq!(first.deterministic_socket_cookie(), (3_u64 << 32) | 7);
260    }
261
262    #[test]
263    fn open_file_ids_display_their_kind_sequence_and_creator() {
264        assert_eq!(
265            OpenFileId::new_socket(DetTid::from_raw(3), 0).to_string(),
266            "socket 0 opened by thread 3"
267        );
268        assert_eq!(
269            OpenFileId::new(DetTid::from_raw(4), 7).to_string(),
270            "file 7 opened by thread 4"
271        );
272    }
273}