Skip to main content

hermit/
liteinst_bootstrap.rs

1// Copyright (c) Meta Platforms, Inc. and affiliates.
2//
3// This source code is licensed under the BSD-style license found in the
4// LICENSE file in the root directory of this source tree.
5
6//! The shared CLI logging filter policy and the tool identity, configuration
7//! schema and effective logging directives carried
8//! by the LiteInst bootstrap. The payload is schema-validated and its config
9//! fingerprint is compared; it is not cryptographically authenticated.
10//! The host resolves the environment once;
11//! the guest consumes the same accepted directives without reading its own
12//! environment or reformatting field matchers.
13
14use serde::Deserialize;
15use serde::Serialize;
16use tracing::metadata::LevelFilter;
17use tracing_subscriber::EnvFilter;
18use tracing_subscriber::filter::Directive;
19
20const VERSION: u32 = 2;
21const TOOL: &str = "hermit-detcore-liteinst-v1";
22
23#[derive(Debug)]
24pub struct EffectiveFilter {
25    directives: String,
26    filter: EnvFilter,
27}
28
29impl EffectiveFilter {
30    /// Resolve `RUST_LOG` once, using the same missing/non-Unicode fallback as
31    /// `EnvFilter::from_default_env`. All CLI file and stderr subscribers use
32    /// this constructor so their policy also defines the bootstrap directives.
33    pub fn from_default_env(level: LevelFilter) -> Self {
34        let raw = std::env::var(EnvFilter::DEFAULT_ENV).unwrap_or_default();
35        Self::from_directives_lossy(&raw, level)
36    }
37
38    /// Resolve directives using the CLI's existing lossy environment policy and
39    /// its final Tokio and level overrides. Preserve the accepted source text:
40    /// formatting an EnvFilter is not a lossless encoding of field matchers.
41    pub fn from_directives_lossy(raw: &str, level: LevelFilter) -> Self {
42        let filter = EnvFilter::new(raw)
43            .add_directive("tokio=debug".parse().expect("correct directive"))
44            .add_directive(level.into());
45        let mut accepted: Vec<_> = raw
46            .split(',')
47            .filter(|directive| !directive.is_empty())
48            .filter(|directive| directive.parse::<Directive>().is_ok())
49            .collect();
50        let level_text = level.to_string();
51        accepted.push("tokio=debug");
52        accepted.push(&level_text);
53        Self {
54            directives: accepted.join(","),
55            filter,
56        }
57    }
58
59    pub fn into_filter(self) -> EnvFilter {
60        self.filter
61    }
62}
63
64#[derive(Deserialize, Serialize)]
65#[serde(deny_unknown_fields)]
66struct Payload {
67    version: u32,
68    tool: String,
69    config_wire_fingerprint: String,
70    log_filter: String,
71}
72
73pub fn encode(
74    config_wire_fingerprint: &str,
75    log_filter: &EffectiveFilter,
76) -> Result<Vec<u8>, serde_json::Error> {
77    serde_json::to_vec(&Payload {
78        version: VERSION,
79        tool: TOOL.to_owned(),
80        config_wire_fingerprint: config_wire_fingerprint.to_owned(),
81        log_filter: log_filter.directives.clone(),
82    })
83}
84
85pub fn decode(bytes: &[u8], expected_fingerprint: &str) -> Result<EffectiveFilter, String> {
86    let payload: Payload = serde_json::from_slice(bytes)
87        .map_err(|error| format!("invalid bootstrap payload: {error}"))?;
88    if payload.version != VERSION {
89        return Err("unsupported bootstrap payload version".to_owned());
90    }
91    if payload.tool != TOOL {
92        return Err("bootstrap tool mismatch".to_owned());
93    }
94    if payload.config_wire_fingerprint != expected_fingerprint {
95        return Err("bootstrap config fingerprint mismatch".to_owned());
96    }
97    let filter = EnvFilter::try_new(&payload.log_filter)
98        .map_err(|error| format!("invalid bootstrap log filter: {error}"))?;
99    Ok(EffectiveFilter {
100        directives: payload.log_filter,
101        filter,
102    })
103}
104
105#[cfg(test)]
106mod tests {
107    use serde_json::Value;
108    use serde_json::json;
109
110    use super::*;
111
112    const FINGERPRINT: &str = "fixture-config-fingerprint";
113
114    fn payload() -> Value {
115        json!({
116            "version": VERSION,
117            "tool": TOOL,
118            "config_wire_fingerprint": FINGERPRINT,
119            "log_filter": "info,tokio=debug,detcore[work{task=7}]=trace",
120        })
121    }
122
123    #[test]
124    fn schema_and_filter_round_trip() {
125        let raw = payload()["log_filter"].as_str().unwrap().to_owned();
126        let filter = EffectiveFilter::from_directives_lossy(&raw, LevelFilter::WARN);
127        let encoded = encode(FINGERPRINT, &filter).unwrap();
128        let mut expected = payload();
129        expected["log_filter"] = json!(format!("{raw},tokio=debug,warn"));
130        assert_eq!(serde_json::from_slice::<Value>(&encoded).unwrap(), expected);
131        let decoded = decode(&encoded, FINGERPRINT).unwrap();
132        assert_eq!(decoded.directives, filter.directives);
133        assert_eq!(encode(FINGERPRINT, &decoded).unwrap(), encoded);
134    }
135
136    #[test]
137    fn preserves_accepted_spelling_and_override_order() {
138        let raw = "warn,,detcore[work{task=1.0}]=info,tokio=off,broken=bogus,detcore[work{task=-1.0}]=debug,detcore[work{task=1e0}]=trace";
139        let filter = EffectiveFilter::from_directives_lossy(raw, LevelFilter::WARN);
140        let expected = "warn,detcore[work{task=1.0}]=info,tokio=off,detcore[work{task=-1.0}]=debug,detcore[work{task=1e0}]=trace,tokio=debug,warn";
141        let encoded = encode(FINGERPRINT, &filter).unwrap();
142        assert_eq!(
143            serde_json::from_slice::<Value>(&encoded).unwrap()["log_filter"],
144            expected
145        );
146        let decoded = decode(&encoded, FINGERPRINT).unwrap();
147        assert_eq!(decoded.directives, expected);
148        assert_eq!(encode(FINGERPRINT, &decoded).unwrap(), encoded);
149    }
150
151    use std::io;
152    use std::io::Write;
153    use std::sync::Arc;
154    use std::sync::Mutex;
155
156    #[derive(Clone)]
157    struct Records(Arc<Mutex<Vec<u8>>>);
158
159    impl Write for Records {
160        fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
161            self.0.lock().unwrap().extend_from_slice(bytes);
162            Ok(bytes.len())
163        }
164
165        fn flush(&mut self) -> io::Result<()> {
166            Ok(())
167        }
168    }
169
170    fn capture(filter: EnvFilter) -> Vec<u8> {
171        let records = Records(Arc::new(Mutex::new(Vec::new())));
172        let writer = records.clone();
173        let subscriber = tracing_subscriber::fmt()
174            .with_env_filter(filter)
175            .without_time()
176            .with_ansi(false)
177            .with_writer(move || writer.clone())
178            .finish();
179        tracing::subscriber::with_default(subscriber, || {
180            tracing::info!(target: "bootstrap_fixture", "outside info");
181            tracing::debug!(target: "tokio", "Tokio override");
182            for task in [1.0_f64, -1.0, 2.0] {
183                let span = tracing::info_span!(target: "bootstrap_fixture", "work", task);
184                let _entered = span.enter();
185                tracing::debug!(target: "bootstrap_fixture", "inside debug");
186                tracing::trace!(target: "bootstrap_fixture", "inside trace");
187            }
188            tracing::warn!(target: "bootstrap_fixture", "warning");
189        });
190        records.0.lock().unwrap().clone()
191    }
192
193    #[test]
194    fn decoded_filter_selects_the_same_events_and_span_fields_as_the_host() {
195        for raw in [
196            "",
197            "off,tokio=off",
198            "broken=bogus,bootstrap_fixture=trace",
199            "off,bootstrap_fixture[work{task=1.0}]=trace",
200            "off,bootstrap_fixture[work{task=-1.0}]=debug,bootstrap_fixture[work{task=1e0}]=trace",
201            "bootstrap_fixture=info,bootstrap_fixture=trace,bootstrap_fixture=warn",
202        ] {
203            let host = EffectiveFilter::from_directives_lossy(raw, LevelFilter::WARN);
204            let payload = encode(FINGERPRINT, &host).unwrap();
205            let guest = decode(&payload, FINGERPRINT).unwrap();
206            // Keep the original CLI construction as an independent oracle.
207            let original = EnvFilter::new(raw)
208                .add_directive("tokio=debug".parse().unwrap())
209                .add_directive(LevelFilter::WARN.into());
210            let expected = capture(original);
211            assert_eq!(capture(host.into_filter()), expected, "host: {raw}");
212            assert!(
213                expected
214                    .windows(b"Tokio override".len())
215                    .any(|part| part == b"Tokio override"),
216                "the mandatory Tokio directive must select a real event: {raw}"
217            );
218            if raw.contains("work{task=") {
219                assert!(
220                    expected
221                        .windows(b"inside trace".len())
222                        .any(|part| part == b"inside trace"),
223                    "the dynamic matcher must select an event in its span: {raw}"
224                );
225                assert!(
226                    !expected
227                        .windows(b"outside info".len())
228                        .any(|part| part == b"outside info"),
229                    "the dynamic matcher must not enable the same target outside its span: {raw}"
230                );
231            }
232            assert_eq!(capture(guest.into_filter()), expected, "{raw}");
233        }
234    }
235
236    // Run environment controls in child processes so these tests do not mutate
237    // the process-global environment while other libtest threads are active.
238    #[test]
239    fn default_environment_filter_subprocess() {
240        if std::env::var_os("HERMIT_BOOTSTRAP_FILTER_CHILD").is_none() {
241            return;
242        }
243        let host = EffectiveFilter::from_default_env(LevelFilter::WARN);
244        let encoded = encode(FINGERPRINT, &host).unwrap();
245        let original = EnvFilter::from_default_env()
246            .add_directive("tokio=debug".parse().unwrap())
247            .add_directive(LevelFilter::WARN.into());
248        let expected = capture(original);
249        assert!(
250            expected
251                .windows(b"Tokio override".len())
252                .any(|part| part == b"Tokio override")
253        );
254        assert_eq!(capture(host.into_filter()), expected);
255        assert_eq!(
256            capture(decode(&encoded, FINGERPRINT).unwrap().into_filter()),
257            expected
258        );
259    }
260
261    #[test]
262    fn environment_policy_matches_original_cli_for_missing_invalid_and_valid_values() {
263        use std::ffi::OsString;
264        use std::os::unix::ffi::OsStringExt;
265        use std::process::Command;
266        use std::process::Stdio;
267        use std::time::Duration;
268        use std::time::Instant;
269
270        for value in [
271            None,
272            Some(OsString::new()),
273            Some(OsString::from_vec(vec![0xff])),
274            Some(OsString::from(
275                "broken=bogus,bootstrap_fixture=trace,tokio=off",
276            )),
277            Some(OsString::from(
278                "off,bootstrap_fixture[work{task=1.0}]=trace",
279            )),
280        ] {
281            let mut child = Command::new(std::env::current_exe().unwrap());
282            child.args([
283                "--exact",
284                "liteinst_bootstrap::tests::default_environment_filter_subprocess",
285                "--nocapture",
286            ]);
287            child.env("HERMIT_BOOTSTRAP_FILTER_CHILD", "1");
288            child.env_remove(EnvFilter::DEFAULT_ENV);
289            if let Some(value) = &value {
290                child.env(EnvFilter::DEFAULT_ENV, value);
291            }
292            child.stdout(Stdio::piped()).stderr(Stdio::piped());
293            let mut child = child.spawn().unwrap();
294            let deadline = Instant::now() + Duration::from_secs(10);
295            while child.try_wait().unwrap().is_none() {
296                if Instant::now() >= deadline {
297                    let _ = child.kill();
298                    let output = child.wait_with_output().unwrap();
299                    panic!("filter child exceeded ten seconds: {value:?}: {output:?}");
300                }
301                std::thread::sleep(Duration::from_millis(10));
302            }
303            let output = child.wait_with_output().unwrap();
304            assert!(output.status.success(), "{value:?}: {output:?}");
305            assert!(
306                String::from_utf8_lossy(&output.stdout).contains("1 passed; 0 failed"),
307                "{value:?}: {output:?}"
308            );
309        }
310    }
311
312    #[test]
313    fn refuses_wrong_version_tool_fingerprint_and_filter() {
314        for (field, value, expected) in [
315            ("version", json!(1), "unsupported bootstrap payload version"),
316            ("tool", json!("other-tool"), "bootstrap tool mismatch"),
317            (
318                "config_wire_fingerprint",
319                json!("other-config"),
320                "bootstrap config fingerprint mismatch",
321            ),
322            (
323                "log_filter",
324                json!("detcore=bogus"),
325                "invalid bootstrap log filter:",
326            ),
327        ] {
328            let mut invalid = payload();
329            invalid[field] = value;
330            let error = decode(&serde_json::to_vec(&invalid).unwrap(), FINGERPRINT).unwrap_err();
331            assert!(error.starts_with(expected), "{field}: {error}");
332        }
333    }
334
335    #[test]
336    fn refuses_missing_unknown_and_wrong_typed_fields() {
337        for field in ["version", "tool", "config_wire_fingerprint", "log_filter"] {
338            let mut missing = payload();
339            missing.as_object_mut().unwrap().remove(field);
340            assert!(decode(&serde_json::to_vec(&missing).unwrap(), FINGERPRINT).is_err());
341            let mut wrong_type = payload();
342            wrong_type[field] = json!(false);
343            assert!(decode(&serde_json::to_vec(&wrong_type).unwrap(), FINGERPRINT).is_err());
344        }
345        let mut unknown = payload();
346        unknown["extra"] = json!(0);
347        assert!(decode(&serde_json::to_vec(&unknown).unwrap(), FINGERPRINT).is_err());
348    }
349
350    #[test]
351    fn refuses_old_payload_duplicate_fields_trailing_data_and_invalid_utf8() {
352        let old = json!({"tool": TOOL, "config_wire_fingerprint": FINGERPRINT});
353        assert!(decode(&serde_json::to_vec(&old).unwrap(), FINGERPRINT).is_err());
354        let valid = serde_json::to_string(&payload()).unwrap();
355        let duplicate = format!("{{\"version\":2,{}", &valid[1..]);
356        assert!(decode(duplicate.as_bytes(), FINGERPRINT).is_err());
357        assert!(decode(format!("{valid} null").as_bytes(), FINGERPRINT).is_err());
358        assert!(decode(b"\xff", FINGERPRINT).is_err());
359        assert!(decode(b"{", FINGERPRINT).is_err());
360    }
361}