Skip to main content

detcore/
config.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
9//! Detcore configuration and widely used types.
10pub use detcore_model::config::CONFIG_FINGERPRINT_ENV;
11pub use detcore_model::config::LITEINST_CONFIG_FINGERPRINT_ENV;
12pub use detcore_model::config::*;
13
14/// [`detcore_model::config::config_wire_fingerprint`], extended with the
15/// revision of Detcore's global RPC (`crate::tool_global::GLOBAL_RPC_REVISION`).
16///
17/// A runtime that embeds Detcore (SaBRe's plugin, the LiteInst runtime that
18/// in-guest-trap also loads, DBT's) sends `GlobalRequest`s to its coordinator
19/// as positional bincode and decodes the `GlobalResponse`s. Both enums live
20/// in this crate, outside the sources the configuration's fingerprint hashes,
21/// so a variant one build had and the other lacked went unnoticed: a newer
22/// runtime's request failed to decode in the middle of a run, and an older
23/// runtime never sent it (and so, for one, kept the host's inotify rename
24/// cookies). Every such runtime compares this fingerprint with its
25/// coordinator's before it decodes anything, so the pair is refused by name.
26/// The revision stands for the enums' source, which, unlike the
27/// configuration's, is not embedded: these runtimes are loaded into the guest,
28/// where every byte they grow by moves the guest's own mappings.
29pub fn config_wire_fingerprint() -> String {
30    // The material is immutable for this loaded image: the model overrides
31    // environment-dependent defaults, and the RPC revision is compiled in.
32    // Reuse it when a forked in-guest tool reconnects on the guest's clone
33    // stack; rebuilding Clap's canonical Config there can exhaust that stack.
34    // SaBRe's parent compatibility check finishes before an intercepted clone
35    // can run, so its child inherits an initialized value. Each connection
36    // still compares the coordinator's fingerprint with this local value.
37    static FINGERPRINT: std::sync::OnceLock<String> = std::sync::OnceLock::new();
38    FINGERPRINT
39        .get_or_init(|| {
40            fingerprint_with_revision(
41                &detcore_model::config::config_wire_fingerprint(),
42                crate::tool_global::GLOBAL_RPC_REVISION,
43            )
44        })
45        .clone()
46}
47
48/// What differs when a coordinator announced `expected` as its
49/// [`config_wire_fingerprint`] and this build's is another, or `None` when
50/// they are equal. The runtimes report this before they decode the
51/// coordinator's configuration (<https://github.com/rrnewton/hermit/issues/3986>).
52pub fn config_fingerprint_mismatch(expected: &std::ffi::OsStr) -> Option<String> {
53    let ours = config_wire_fingerprint();
54    (expected.to_str() != Some(ours.as_str())).then(|| {
55        format!(
56            "its configuration and clock RPC fingerprint is {ours}, Hermit's is {}",
57            expected.to_string_lossy()
58        )
59    })
60}
61
62/// FNV-1a over `bytes`, each part length-prefixed in a domain of its own,
63/// as `detcore_model`'s fingerprint hashes its material. A mismatch
64/// detector, not a security boundary.
65fn fnv_domains(parts: &[(u8, &[u8])]) -> String {
66    let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
67    for (domain, bytes) in parts {
68        for byte in std::iter::once(domain)
69            .chain((bytes.len() as u64).to_le_bytes().iter())
70            .chain(bytes.iter())
71        {
72            hash ^= u64::from(*byte);
73            hash = hash.wrapping_mul(0x1000_0000_01b3);
74        }
75    }
76    format!("{hash:016x}")
77}
78
79/// The fingerprint of configuration fingerprint `base` at global RPC
80/// revision `revision`.
81fn fingerprint_with_revision(base: &str, revision: u32) -> String {
82    fnv_domains(&[(0, base.as_bytes()), (1, &revision.to_le_bytes())])
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    /// Every revision of the global RPC, oldest first, with the digest of the
90    /// code of its two enums and its `Resources` payload. A change to any of
91    /// these appends a row and sets
92    /// `GLOBAL_RPC_REVISION` to its revision.
93    const GLOBAL_RPC_REVISIONS: &[(u32, &str, &str)] = &[
94        // GlobalRequest::VirtualInotifyCookies and its response, the first
95        // revision the fingerprint carries.
96        (1, "a61c88ecb1abcb5e", "9b7e11bf5f08e6ae"),
97        // GlobalRequest::RegisterDescriptorDirectory and DescriptorDirectory and
98        // their responses (package K slice F1).
99        (2, "caee2c37469869d4", "9b7e11bf5f08e6ae"),
100        // GlobalRequest::RegisterPlaceholder, Placeholder, PlaceholderFlock,
101        // PlaceholderWriteHint and PlaceholderCopies and their responses, in
102        // place of F1's two (package K slice P1).
103        (3, "b3d7410a78b56ae0", "9b7e11bf5f08e6ae"),
104        // Resources::priority_row extends the positional request payload.
105        (4, "b3d7410a78b56ae0", "4e10b2de93d47d64"),
106        // Adds `GlobalRequest::CpuAffinity`, `GlobalRequest::SetCpuAffinity`; adds `GlobalResponse::CpuAffinity` (#4629).
107        (5, "5a4ad14e74501e11", "4e10b2de93d47d64"),
108        // Adds `Resources.slice_expired` (#4653).
109        (6, "5a4ad14e74501e11", "6a1b176897e67771"),
110        // Retypes `GlobalRequest::RecordHostInput`
111        // (https://github.com/rrnewton/hermit/pull/4611).
112        (7, "6b6c7521a6620f94", "6a1b176897e67771"),
113        // Adds `Resources.zero_length_sleep`
114        // (https://github.com/rrnewton/hermit/pull/4658).
115        (8, "6b6c7521a6620f94", "dafaa96d8b8cb3ad"),
116        // Adds `GlobalResponse::WithLoneGrant`
117        // (https://github.com/rrnewton/hermit/pull/4662).
118        (9, "ebfd2b59a97e1d7a", "dafaa96d8b8cb3ad"),
119        // Adds `GlobalRequest::ProcessSignalReceiver`; adds `GlobalResponse::ProcessSignalReceiver`
120        // (https://github.com/rrnewton/hermit/pull/4625, renumbered to 10 at landing).
121        (10, "1899cf7ab14698ae", "dafaa96d8b8cb3ad"),
122        // Adds `GlobalRequest::StoreExclusive` and `GlobalResponse::StoreExclusive`
123        // (https://github.com/rrnewton/hermit/pull/4740, renumbered to 11 at landing).
124        (11, "8bb87305b1bd45a3", "dafaa96d8b8cb3ad"),
125        // Adds `GlobalRequest::CommitFatalDelivery`; removes `GlobalRequest::RecordFatalSignal`; adds `GlobalResponse::CommitFatalDelivery`; removes `GlobalResponse::RecordFatalSignal`
126        // (https://github.com/rrnewton/hermit/pull/4676, renumbered to 12 at landing).
127        (12, "ab01c6abb5daa6d7", "dafaa96d8b8cb3ad"), // Adds `GlobalRequest::SigkillDyingThreads`, `GlobalRequest::RecordSigkillRobustLists`, `GlobalRequest::TakeSigkillRobustList` and the matching responses
128        // (https://github.com/rrnewton/hermit/pull/4760, renumbered to 13 at landing).
129        (13, "78e513d0fe279099", "dafaa96d8b8cb3ad"),
130        (14, "edaba1a0186b437c", "dafaa96d8b8cb3ad"), // Adds `GlobalRequest::NextGuestId`, `GlobalRequest::GaveHostId`; adds `GlobalResponse::NextGuestId`, `GlobalResponse::GaveHostId`
131        // (https://github.com/rrnewton/hermit/pull/4725, renumbered to 14 at integration; digest measured at this head).
132        // Adds `Resources::passes_descriptors_out`, which revokes the scheduler's
133        // guest-held marks
134        // (https://github.com/rrnewton/hermit/pull/4862, renumbered to 15 at
135        // integration; digests measured at this head).
136        (15, "edaba1a0186b437c", "846052997d97693f"),
137        // Retypes `GlobalRequest::TraceSchedEvent`, including its nested
138        // AccessPayload signal observation (Handler/Ignored/Default/Unknown).
139        // The definition digest below covers the outer enums, not nested types.
140        // (https://github.com/rrnewton/hermit/pull/4738, renumbered to 16 at
141        // integration; digests measured at this head).
142        (16, "182b80da8855a429", "846052997d97693f"),
143        // Adds `GlobalRequest::ProcessSignalSent` and
144        // `GlobalRequest::OwnProcessSignalChoice` and their responses
145        // (https://github.com/rrnewton/hermit/pull/4918, allocated revision 17
146        // at integration; digests measured at this head).
147        (17, "5e3c31a8fe54f2a1", "846052997d97693f"),
148        // Adds `Resources.polls_a_named_fifo`
149        // (https://github.com/rrnewton/hermit/pull/4692, allocated revision 18
150        // at integration; digests measured at this head).
151        (18, "c413f205051a27d4", "f013e36743d8d9e6"),
152        // Adds the replay-departure findings to the global request/response
153        // pair (PR 4860 rework, allocated revision 19 at integration;
154        // digests measured at this head).
155        (19, "c19f0246a9e94f4a", "f013e36743d8d9e6"),
156        // Adds `GlobalRequest::DescriptionLocksTaken`, `LockOwners`,
157        // `LedgerLockRows`, `SecondIdentityLocks`, `HoldTable` and
158        // `RecordLocksHeldAsOfd` and their responses, and `HeldTable`, defined
159        // outside the digested enums, which encodes as nothing (package K
160        // slice K8-h of https://github.com/rrnewton/hermit/issues/4213; its
161        // revisions 17 to 19, never on main, became this one when main took
162        // 17 to 19).
163        (20, "3fb2388350b1a2fa", "f013e36743d8d9e6"),
164        // Adds `GlobalRequest::HostDiskstats` and `GlobalResponse::HostDiskstats`:
165        // the host's /proc/diskstats as the run first read it, which every
166        // rendered /proc/diskstats starts from;
167        // `GlobalRequest::UserNamespaceRelation` and
168        // `GlobalRequest::UserNamespaceOwnerMayDiffer` and their responses,
169        // Detcore's record of user namespaces; and
170        // `GlobalRequest::ProcfsSequence` and
171        // `GlobalRequest::StoreProcfsSequence` and their responses, the record
172        // sequence of a per-process file a fork or a send shares where the
173        // tool runs in each guest process. Serialized fields this digest does
174        // not cover: `ProcfsFile::sequence_shared`, which a shared open file
175        // model carries; the open file model's `record_sequence_unknown`; and
176        // `ExecFdState::procfs_records`, which `GlobalRequest::PrepareExec`
177        // carries (the kvm lane's slice N3, revision 17 on its own base,
178        // renumbered to 21 at landing after main's 20; digests measured at
179        // this head).
180        (21, "9d583ef0f9243c61", "f013e36743d8d9e6"),
181    ];
182
183    /// The lines of code, without comments or blank lines, of the type that
184    /// `source` declares on the line `header`, through its closing brace.
185    fn definition(source: &str, header: &str) -> Vec<String> {
186        let mut lines = source.lines().skip_while(|line| *line != header);
187        assert_eq!(lines.next(), Some(header), "{header} is declared");
188        let mut code: Vec<String> = lines
189            .take_while(|line| *line != "}")
190            .map(str::trim)
191            .filter(|line| !line.is_empty() && !line.starts_with("//"))
192            .map(str::to_owned)
193            .collect();
194        code.insert(0, header.to_owned());
195        code
196    }
197
198    /// A digest of the code of `GlobalRequest` and `GlobalResponse` in
199    /// `source`: their variants, payload types, order and attributes, not
200    /// their documentation.
201    fn definition_digest(source: &str) -> String {
202        let request = definition(source, "pub enum GlobalRequest {").join("\n");
203        let response = definition(source, "pub enum GlobalResponse {").join("\n");
204        fnv_domains(&[(0, request.as_bytes()), (1, response.as_bytes())])
205    }
206
207    fn resources_definition_digest(source: &str) -> String {
208        let resources = definition(source, "pub struct Resources {").join("\n");
209        fnv_domains(&[(2, resources.as_bytes())])
210    }
211
212    /// The global RPC's revision names its enums and Resources as they are:
213    /// a variant or field added, removed, reordered or retyped changes the digest,
214    /// and the test then
215    /// fails until a new revision is recorded, which changes the fingerprint
216    /// every runtime compares with its coordinator's.
217    #[test]
218    fn the_global_rpc_revision_matches_its_definitions() {
219        let source = include_str!("tool_global.rs");
220        let digest = definition_digest(source);
221        let resources = include_str!("resources.rs");
222        let resources_digest = resources_definition_digest(resources);
223        let revision = crate::tool_global::GLOBAL_RPC_REVISION;
224        let next = GLOBAL_RPC_REVISIONS
225            .last()
226            .map_or(1, |(last, _, _)| last + 1);
227        assert_eq!(
228            GLOBAL_RPC_REVISIONS.last(),
229            Some(&(revision, digest.as_str(), resources_digest.as_str())),
230            "GlobalRequest, GlobalResponse or Resources changed: append ({next}, \
231             \"{digest}\", \"{resources_digest}\") to GLOBAL_RPC_REVISIONS and set \
232             GLOBAL_RPC_REVISION to {next}, so \
233             that runtimes and coordinators from either side of the change refuse each other"
234        );
235        for pair in GLOBAL_RPC_REVISIONS.windows(2) {
236            assert_eq!(pair[1].0, pair[0].0 + 1, "revisions count up by one");
237            assert_ne!(
238                (pair[1].1, pair[1].2),
239                (pair[0].1, pair[0].2),
240                "each revision changed the wire definitions"
241            );
242        }
243
244        // What the digest sees, and what it does not.
245        let documented = source.replacen(
246            "pub enum GlobalRequest {\n",
247            "pub enum GlobalRequest {\n    /// Documentation only.\n\n",
248            1,
249        );
250        assert_eq!(definition_digest(&documented), digest);
251        let added = source.replacen(
252            "pub enum GlobalRequest {\n",
253            "pub enum GlobalRequest {\n    Added(u32),\n",
254            1,
255        );
256        assert_ne!(definition_digest(&added), digest);
257        // Retype GlobalRequest's VirtualInotifyCookies wherever it sits: later
258        // revisions append variants after it.
259        let retyped = source.replacen(
260            "    VirtualInotifyCookies(Vec<u32>),\n",
261            "    VirtualInotifyCookies(Vec<u64>),\n",
262            1,
263        );
264        assert_ne!(retyped, source);
265        assert_ne!(definition_digest(&retyped), digest);
266
267        let documented = resources.replacen(
268            "pub struct Resources {\n",
269            "pub struct Resources {\n    /// Documentation only.\n\n",
270            1,
271        );
272        assert_ne!(documented, resources);
273        assert_eq!(resources_definition_digest(&documented), resources_digest);
274        let removed = resources.replacen("    pub(crate) priority_row: Option<u64>,\n", "", 1);
275        assert_ne!(removed, resources);
276        assert_ne!(resources_definition_digest(&removed), resources_digest);
277        let retyped =
278            resources.replacen("priority_row: Option<u64>", "priority_row: Option<u32>", 1);
279        assert_ne!(retyped, resources);
280        assert_ne!(resources_definition_digest(&retyped), resources_digest);
281    }
282
283    /// The published fingerprint is the configuration's extended with the
284    /// revision, so another revision is a mismatch, named with both values.
285    #[test]
286    fn the_fingerprint_carries_the_global_rpc_revision() {
287        let model = detcore_model::config::config_wire_fingerprint();
288        let ours = config_wire_fingerprint();
289        let revision = crate::tool_global::GLOBAL_RPC_REVISION;
290        assert_eq!(ours.len(), 16);
291        assert_eq!(ours, fingerprint_with_revision(&model, revision));
292        assert_ne!(ours, model);
293        let other = fingerprint_with_revision(&model, revision + 1);
294        assert_ne!(other, ours);
295        assert_eq!(
296            config_fingerprint_mismatch(std::ffi::OsStr::new(&ours)),
297            None
298        );
299        let mismatch = config_fingerprint_mismatch(std::ffi::OsStr::new(&other))
300            .expect("another revision is a mismatch");
301        assert!(
302            mismatch.contains(&ours) && mismatch.contains(&other),
303            "{mismatch}"
304        );
305    }
306}