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}