Skip to main content

hermit/
compatibility.rs

1/* Copyright (c) Meta Platforms, Inc. and affiliates. */
2
3//! Exact compatibility for a separately linked CLI backend executable.
4//!
5//! This is separate from the closed presentation/recording `BuildInfo`
6//! schema. Equal ELF build IDs are neither expected nor used: the executables
7//! differ, but must contain the same measured core source/API and build cohort.
8//! Matching this record is not runtime qualification or publisher attestation.
9
10#![allow(unexpected_cfgs, reason = "fbcode_build is supplied by Buck")]
11
12use anyhow::Result;
13use anyhow::bail;
14use anyhow::ensure;
15use serde::Deserialize;
16use serde::Serialize;
17
18/// Dedicated source/API contract; do not silently accept another version.
19pub const SCHEMA: &str = "hermit-core-compatibility/v1";
20/// The statically linked CLI application/adapter API.
21pub const API: &str = "hermit-cli-static-backend/v1";
22/// Independent framing for the identity-only executable probe.
23pub const PROBE_SCHEMA: &str = "hermit-executable-probe/v1";
24/// Both readers and producers enforce the same finite protocol output bound.
25pub const MAX_PROBE_BYTES: usize = 16 * 1024;
26/// This exact single option is handled before ordinary application startup.
27pub const PROBE_ARG: &str = "--hermit-compatibility-v1";
28/// Same identity response while an owned control socket keeps this executable
29/// mapped. Only its parent owns the deadline/abandonment; it has no self timer.
30pub const HELD_PROBE_ARG: &str = "--hermit-compatibility-held-v1";
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(deny_unknown_fields)]
34struct Source {
35    kind: String,
36    commit: String,
37    tree: String,
38    sha256: String,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(deny_unknown_fields)]
43struct ReverieUnit {
44    package: String,
45    version: String,
46    target: String,
47    kind: Vec<String>,
48    crate_types: Vec<String>,
49    features: Vec<String>,
50    profile: serde_json::Value,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54#[serde(deny_unknown_fields)]
55struct Reverie {
56    commit: String,
57    sha256: String,
58    units: Vec<ReverieUnit>,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62#[serde(deny_unknown_fields)]
63struct BuildOption {
64    name: String,
65    value: String,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69#[serde(deny_unknown_fields)]
70struct RustcInputs {
71    schema: String,
72    normalization: String,
73    shared_units: usize,
74    shared_sha256: String,
75    auxiliary_commands: usize,
76    auxiliary_sha256: String,
77}
78
79/// Immutable complete comparison input measured by the core build producer.
80/// All dimensions compare exactly; there is no tolerance or missing-field
81/// interpretation, and extensions cannot replace the embedded core record.
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83#[serde(deny_unknown_fields)]
84pub struct CoreCompatibility {
85    schema: String,
86    api: String,
87    source: Source,
88    release: String,
89    reverie: Reverie,
90    target: String,
91    host: String,
92    compiler: String,
93    compiler_sha256: String,
94    features: Vec<String>,
95    core_profile: serde_json::Value,
96    rustc_inputs: RustcInputs,
97    configuration: Vec<BuildOption>,
98}
99
100fn full_oid(value: &str) -> bool {
101    value.len() == 40
102        && value
103            .bytes()
104            .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
105}
106
107fn full_digest(value: &str) -> bool {
108    value.len() == 64
109        && value
110            .bytes()
111            .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
112}
113
114fn known(value: &str) -> bool {
115    !value.is_empty() && value != "unknown"
116}
117
118fn validate_profile(value: &serde_json::Value, owner: &str) -> Result<()> {
119    let profile = value
120        .as_object()
121        .ok_or_else(|| anyhow::anyhow!("{owner} has no Cargo artifact profile"))?;
122    for key in [
123        "opt_level",
124        "debuginfo",
125        "debug_assertions",
126        "overflow_checks",
127        "test",
128    ] {
129        ensure!(
130            profile.contains_key(key),
131            "{owner} omits Cargo profile option {key}"
132        );
133    }
134    Ok(())
135}
136
137impl CoreCompatibility {
138    /// Validate every required identity dimension before comparing records.
139    pub fn validate(&self) -> Result<()> {
140        ensure!(
141            self.schema == SCHEMA && self.api == API,
142            "unsupported core compatibility schema/API"
143        );
144        ensure!(
145            self.source.kind == "git",
146            "core compatibility has no measured Git source producer"
147        );
148        ensure!(
149            full_oid(&self.source.commit) && full_oid(&self.source.tree),
150            "core compatibility requires complete source commit and tree IDs"
151        );
152        ensure!(
153            full_digest(&self.source.sha256),
154            "core compatibility has no full source-content SHA256"
155        );
156        ensure!(
157            known(&self.release),
158            "core compatibility has no release identity"
159        );
160        ensure!(
161            full_oid(&self.reverie.commit)
162                && full_digest(&self.reverie.sha256)
163                && !self.reverie.units.is_empty(),
164            "core compatibility has no complete measured Reverie cohort"
165        );
166        let mut unit_keys = Vec::new();
167        for unit in &self.reverie.units {
168            ensure!(
169                known(&unit.package)
170                    && known(&unit.version)
171                    && known(&unit.target)
172                    && !unit.kind.is_empty()
173                    && !unit.crate_types.is_empty(),
174                "core compatibility has an incomplete actual Reverie unit"
175            );
176            ensure!(
177                unit.features
178                    .windows(2)
179                    .all(|features| features[0] < features[1]),
180                "core compatibility repeats or reorders Reverie unit features"
181            );
182            validate_profile(&unit.profile, "Reverie unit")?;
183            unit_keys.push(serde_json::to_string(unit)?);
184        }
185        ensure!(
186            unit_keys.windows(2).all(|keys| keys[0] < keys[1]),
187            "core compatibility repeats or reorders actual Reverie units"
188        );
189        ensure!(
190            known(&self.target) && known(&self.host),
191            "core compatibility has no target/host identity"
192        );
193        ensure!(
194            self.compiler
195                .lines()
196                .any(|line| line.strip_prefix("commit-hash: ").is_some_and(full_oid)),
197            "core compatibility has no exact compiler commit"
198        );
199        ensure!(
200            full_digest(&self.compiler_sha256),
201            "core compatibility has no complete actual compiler frontend digest"
202        );
203        ensure!(
204            self.features
205                .iter()
206                .all(|f| f.starts_with("CARGO_FEATURE_") && f.len() > "CARGO_FEATURE_".len()),
207            "core compatibility has an invalid semantic feature"
208        );
209        ensure!(
210            self.features.windows(2).all(|p| p[0] < p[1]),
211            "core compatibility repeats or reorders semantic features"
212        );
213        validate_profile(&self.core_profile, "core library unit")?;
214        ensure!(
215            self.rustc_inputs.schema == "hermit-actual-rustc-capture/v3"
216                && self.rustc_inputs.normalization == "hermit-shared-rustc-semantics/v3"
217                && self.rustc_inputs.shared_units >= 2
218                && full_digest(&self.rustc_inputs.shared_sha256)
219                && full_digest(&self.rustc_inputs.auxiliary_sha256),
220            "core compatibility has no complete compared actual shared rustc inputs"
221        );
222        ensure!(
223            self.configuration.windows(2).all(|p| p[0].name < p[1].name),
224            "core compatibility repeats or reorders semantic build options"
225        );
226        for key in [
227            "PROFILE",
228            "OPT_LEVEL",
229            "DEBUG",
230            "CARGO_ENCODED_RUSTFLAGS",
231            "RUSTFLAGS",
232            "RUSTC_WRAPPER",
233            "RUSTC_WORKSPACE_WRAPPER",
234            "CARGO_CFG_TARGET_ARCH",
235            "CARGO_CFG_TARGET_OS",
236            "CARGO_CFG_PANIC",
237        ] {
238            ensure!(
239                self.configuration.iter().any(|option| option.name == key),
240                "core compatibility omits build option {key}"
241            );
242        }
243        for key in [
244            "PROFILE",
245            "OPT_LEVEL",
246            "DEBUG",
247            "CARGO_CFG_TARGET_ARCH",
248            "CARGO_CFG_TARGET_OS",
249            "CARGO_CFG_PANIC",
250        ] {
251            ensure!(
252                self.configuration
253                    .iter()
254                    .any(|option| option.name == key && known(&option.value)),
255                "core compatibility has no measured value for {key}"
256            );
257        }
258        Ok(())
259    }
260
261    /// Refuse a mismatch in any complete dimension. A matching source commit
262    /// alone cannot override feature, target, compiler or dependency drift.
263    pub fn require_exact(&self, observed: &Self) -> Result<()> {
264        self.validate()?;
265        observed.validate()?;
266        let dimension = if self.source != observed.source {
267            "core source"
268        } else if self.release != observed.release {
269            "release"
270        } else if self.reverie != observed.reverie {
271            "Reverie cohort"
272        } else if self.target != observed.target || self.host != observed.host {
273            "target/host"
274        } else if self.compiler != observed.compiler
275            || self.compiler_sha256 != observed.compiler_sha256
276        {
277            "compiler"
278        } else if self.features != observed.features {
279            "semantic features"
280        } else if self.core_profile != observed.core_profile {
281            "core Cargo profile"
282        } else if self.rustc_inputs != observed.rustc_inputs {
283            "actual shared rustc semantics"
284        } else if self.configuration != observed.configuration {
285            "semantic build options"
286        } else {
287            return Ok(());
288        };
289        bail!(
290            "backend executable is incompatible in {dimension}; rebuild it against this exact measured core cohort"
291        )
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298
299    fn stamp() -> CoreCompatibility {
300        let mut configuration: Vec<_> = [
301            ("PROFILE", "release"),
302            ("OPT_LEVEL", "3"),
303            ("DEBUG", "false"),
304            ("CARGO_ENCODED_RUSTFLAGS", ""),
305            ("RUSTFLAGS", ""),
306            ("RUSTC_WRAPPER", ""),
307            ("RUSTC_WORKSPACE_WRAPPER", ""),
308            ("CARGO_CFG_TARGET_ARCH", "x86_64"),
309            ("CARGO_CFG_TARGET_OS", "linux"),
310            ("CARGO_CFG_PANIC", "unwind"),
311        ]
312        .into_iter()
313        .map(|(name, value)| BuildOption {
314            name: name.to_owned(),
315            value: value.to_owned(),
316        })
317        .collect();
318        configuration.sort_by(|a, b| a.name.cmp(&b.name));
319        CoreCompatibility {
320            schema: SCHEMA.to_owned(),
321            api: API.to_owned(),
322            source: Source {
323                kind: "git".to_owned(),
324                commit: "1".repeat(40),
325                tree: "2".repeat(40),
326                sha256: "3".repeat(64),
327            },
328            release: "0.5.5".to_owned(),
329            reverie: Reverie {
330                commit: "4".repeat(40),
331                sha256: "7".repeat(64),
332                units: vec![ReverieUnit {
333                    package: "reverie-core".to_owned(),
334                    version: "0.5.5".to_owned(),
335                    target: "reverie".to_owned(),
336                    kind: vec!["lib".to_owned()],
337                    crate_types: vec!["lib".to_owned()],
338                    features: vec![],
339                    profile: serde_json::json!({"opt_level":"3", "debuginfo":0,
340                        "debug_assertions":false,"overflow_checks":false,"test":false}),
341                }],
342            },
343            target: "x86_64-unknown-linux-gnu".to_owned(),
344            host: "x86_64-unknown-linux-gnu".to_owned(),
345            compiler: format!("rustc fixture\ncommit-hash: {}", "5".repeat(40)),
346            compiler_sha256: "a".repeat(64),
347            features: vec!["CARGO_FEATURE_LITEINST".to_owned()],
348            core_profile: serde_json::json!({"opt_level":"3", "debuginfo":0,
349                "debug_assertions":false,"overflow_checks":false,"test":false}),
350            rustc_inputs: RustcInputs {
351                schema: "hermit-actual-rustc-capture/v3".to_owned(),
352                normalization: "hermit-shared-rustc-semantics/v3".to_owned(),
353                shared_units: 2,
354                shared_sha256: "8".repeat(64),
355                auxiliary_commands: 1,
356                auxiliary_sha256: "b".repeat(64),
357            },
358            configuration,
359        }
360    }
361
362    #[test]
363    fn compatibility_requires_every_exact_dimension() {
364        let original = stamp();
365        original.require_exact(&original).unwrap();
366        let changes: Vec<fn(&mut CoreCompatibility)> = vec![
367            |s| s.source.commit = "6".repeat(40),
368            |s| s.source.tree = "6".repeat(40),
369            |s| s.source.sha256 = "6".repeat(64),
370            |s| s.release = "0.5.6".to_owned(),
371            |s| s.reverie.commit = "6".repeat(40),
372            |s| s.reverie.sha256 = "6".repeat(64),
373            |s| s.reverie.units[0].version = "0.5.6".to_owned(),
374            |s| s.reverie.units[0].features.push("changed".to_owned()),
375            |s| s.reverie.units[0].profile["overflow_checks"] = true.into(),
376            |s| s.target = "x86_64-unknown-linux-musl".to_owned(),
377            |s| s.host = "aarch64-unknown-linux-gnu".to_owned(),
378            |s| s.compiler.push_str("\nother compiler input"),
379            |s| s.compiler_sha256 = "b".repeat(64),
380            |s| s.features.clear(),
381            |s| s.core_profile["overflow_checks"] = true.into(),
382            |s| s.rustc_inputs.shared_sha256 = "9".repeat(64),
383            |s| s.rustc_inputs.auxiliary_commands += 1,
384            |s| s.rustc_inputs.auxiliary_sha256 = "c".repeat(64),
385            |s| s.configuration[0].value.push_str("other option"),
386        ];
387        for change in changes {
388            let mut other = original.clone();
389            change(&mut other);
390            assert!(
391                original.require_exact(&other).is_err(),
392                "accepted {other:?}"
393            );
394        }
395    }
396
397    #[test]
398    fn compatibility_refuses_incomplete_unknown_or_duplicate_dimensions() {
399        let mut missing = stamp();
400        missing.configuration.pop();
401        assert!(missing.validate().is_err());
402        let mut duplicate = stamp();
403        duplicate
404            .configuration
405            .insert(0, duplicate.configuration[0].clone());
406        assert!(duplicate.validate().is_err());
407        let mut unknown = stamp();
408        unknown.source.commit = "unknown".to_owned();
409        assert!(unknown.validate().is_err());
410        let mut value = serde_json::to_value(stamp()).unwrap();
411        value
412            .as_object_mut()
413            .unwrap()
414            .insert("fallback".to_owned(), true.into());
415        assert!(serde_json::from_value::<CoreCompatibility>(value).is_err());
416        let mut missing = serde_json::to_value(stamp()).unwrap();
417        missing.as_object_mut().unwrap().remove("core_profile");
418        assert!(serde_json::from_value::<CoreCompatibility>(missing).is_err());
419        let mut missing = serde_json::to_value(stamp()).unwrap();
420        missing.as_object_mut().unwrap().remove("rustc_inputs");
421        assert!(serde_json::from_value::<CoreCompatibility>(missing).is_err());
422    }
423
424    #[test]
425    fn compatibility_reads_actual_schema3_and_refuses_auxiliary_drift() {
426        // Verbatim core_stamp JSON from the actual source366 producer output.
427        // This is a reader-format fixture, never runtime compatibility evidence.
428        // completed-build.json SHA256: 22b3fbc42c64c7e66b111948bac20b6ce1d2ff896a4b673c5ccf6c516886749b
429        let actual: CoreCompatibility = serde_json::from_str(r#"{
430    "api": "hermit-cli-static-backend/v1",
431    "compiler": "rustc 1.99.0-nightly (be8e82435 2026-07-11)\nbinary: rustc\ncommit-hash: be8e82435eb04fbe75ed5286b52735366e160bed\ncommit-date: 2026-07-11\nhost: x86_64-unknown-linux-gnu\nrelease: 1.99.0-nightly\nLLVM version: 22.1.8",
432    "compiler_sha256": "81536ef88a773f0918ac57c79e4638699e3ffe0a203475e7d2e5168e5853bfee",
433    "configuration": [
434      {
435        "name": "CARGO_CFG_DEBUG_ASSERTIONS",
436        "value": ""
437      },
438      {
439        "name": "CARGO_CFG_FEATURE",
440        "value": "default"
441      },
442      {
443        "name": "CARGO_CFG_FMT_DEBUG",
444        "value": "full"
445      },
446      {
447        "name": "CARGO_CFG_OVERFLOW_CHECKS",
448        "value": ""
449      },
450      {
451        "name": "CARGO_CFG_PANIC",
452        "value": "unwind"
453      },
454      {
455        "name": "CARGO_CFG_RELOCATION_MODEL",
456        "value": "pic"
457      },
458      {
459        "name": "CARGO_CFG_TARGET_ABI",
460        "value": ""
461      },
462      {
463        "name": "CARGO_CFG_TARGET_ARCH",
464        "value": "x86_64"
465      },
466      {
467        "name": "CARGO_CFG_TARGET_ENDIAN",
468        "value": "little"
469      },
470      {
471        "name": "CARGO_CFG_TARGET_ENV",
472        "value": "gnu"
473      },
474      {
475        "name": "CARGO_CFG_TARGET_FAMILY",
476        "value": "unix"
477      },
478      {
479        "name": "CARGO_CFG_TARGET_FEATURE",
480        "value": "fxsr,sse,sse2,x87"
481      },
482      {
483        "name": "CARGO_CFG_TARGET_HAS_ATOMIC",
484        "value": "16,32,64,8,ptr"
485      },
486      {
487        "name": "CARGO_CFG_TARGET_HAS_ATOMIC_LOAD_STORE",
488        "value": "16,32,64,8,ptr"
489      },
490      {
491        "name": "CARGO_CFG_TARGET_HAS_ATOMIC_PRIMITIVE_ALIGNMENT",
492        "value": "16,32,64,8,ptr"
493      },
494      {
495        "name": "CARGO_CFG_TARGET_HAS_RELIABLE_F128",
496        "value": ""
497      },
498      {
499        "name": "CARGO_CFG_TARGET_HAS_RELIABLE_F16",
500        "value": ""
501      },
502      {
503        "name": "CARGO_CFG_TARGET_HAS_RELIABLE_F16_MATH",
504        "value": ""
505      },
506      {
507        "name": "CARGO_CFG_TARGET_HAS_THREADS",
508        "value": ""
509      },
510      {
511        "name": "CARGO_CFG_TARGET_OBJECT_FORMAT",
512        "value": "elf"
513      },
514      {
515        "name": "CARGO_CFG_TARGET_OS",
516        "value": "linux"
517      },
518      {
519        "name": "CARGO_CFG_TARGET_POINTER_WIDTH",
520        "value": "64"
521      },
522      {
523        "name": "CARGO_CFG_TARGET_THREAD_LOCAL",
524        "value": ""
525      },
526      {
527        "name": "CARGO_CFG_TARGET_VENDOR",
528        "value": "unknown"
529      },
530      {
531        "name": "CARGO_CFG_UB_CHECKS",
532        "value": ""
533      },
534      {
535        "name": "CARGO_CFG_UNIX",
536        "value": ""
537      },
538      {
539        "name": "CARGO_ENCODED_RUSTFLAGS",
540        "value": ""
541      },
542      {
543        "name": "DEBUG",
544        "value": "true"
545      },
546      {
547        "name": "OPT_LEVEL",
548        "value": "0"
549      },
550      {
551        "name": "PROFILE",
552        "value": "debug"
553      },
554      {
555        "name": "RUSTC_WORKSPACE_WRAPPER",
556        "value": ""
557      },
558      {
559        "name": "RUSTC_WRAPPER",
560        "value": "hermit-actual-rustc-capture/v3"
561      },
562      {
563        "name": "RUSTFLAGS",
564        "value": ""
565      }
566    ],
567    "core_profile": {
568      "debug_assertions": true,
569      "debuginfo": 2,
570      "opt_level": "0",
571      "overflow_checks": true,
572      "test": false
573    },
574    "features": [
575      "CARGO_FEATURE_DEFAULT"
576    ],
577    "host": "x86_64-unknown-linux-gnu",
578    "release": "0.5.4",
579    "reverie": {
580      "commit": "882c91c02b981f56370a2912f0832b04fefc46ec",
581      "sha256": "f28f1a15c9c0ea1b09554bc7cd02d7b8eaadf377d1854c1770f567e524fe97bf",
582      "units": [
583        {
584          "crate_types": [
585            "lib"
586          ],
587          "features": [
588            "default"
589          ],
590          "kind": [
591            "lib"
592          ],
593          "package": "reverie-core",
594          "profile": {
595            "debug_assertions": true,
596            "debuginfo": 2,
597            "opt_level": "0",
598            "overflow_checks": true,
599            "test": false
600          },
601          "target": "reverie",
602          "version": "0.5.4"
603        },
604        {
605          "crate_types": [
606            "lib"
607          ],
608          "features": [
609            "default"
610          ],
611          "kind": [
612            "lib"
613          ],
614          "package": "reverie-kvm",
615          "profile": {
616            "debug_assertions": true,
617            "debuginfo": 2,
618            "opt_level": "0",
619            "overflow_checks": true,
620            "test": false
621          },
622          "target": "reverie_kvm",
623          "version": "0.5.4"
624        },
625        {
626          "crate_types": [
627            "lib"
628          ],
629          "features": [
630            "default"
631          ],
632          "kind": [
633            "lib"
634          ],
635          "package": "reverie-kvm-core",
636          "profile": {
637            "debug_assertions": true,
638            "debuginfo": 2,
639            "opt_level": "0",
640            "overflow_checks": true,
641            "test": false
642          },
643          "target": "reverie_kvm_core",
644          "version": "0.5.4"
645        },
646        {
647          "crate_types": [
648            "bin"
649          ],
650          "features": [
651            "default"
652          ],
653          "kind": [
654            "custom-build"
655          ],
656          "package": "reverie-linuxemu",
657          "profile": {
658            "debug_assertions": true,
659            "debuginfo": 2,
660            "opt_level": "0",
661            "overflow_checks": true,
662            "test": false
663          },
664          "target": "build-script-build",
665          "version": "0.5.4"
666        },
667        {
668          "crate_types": [
669            "lib"
670          ],
671          "features": [
672            "default"
673          ],
674          "kind": [
675            "lib"
676          ],
677          "package": "reverie-linuxemu",
678          "profile": {
679            "debug_assertions": true,
680            "debuginfo": 2,
681            "opt_level": "0",
682            "overflow_checks": true,
683            "test": false
684          },
685          "target": "reverie_linuxemu",
686          "version": "0.5.4"
687        },
688        {
689          "crate_types": [
690            "lib"
691          ],
692          "features": [],
693          "kind": [
694            "lib"
695          ],
696          "package": "reverie-memory",
697          "profile": {
698            "debug_assertions": true,
699            "debuginfo": 2,
700            "opt_level": "0",
701            "overflow_checks": true,
702            "test": false
703          },
704          "target": "reverie_memory",
705          "version": "0.5.4"
706        },
707        {
708          "crate_types": [
709            "lib"
710          ],
711          "features": [
712            "default"
713          ],
714          "kind": [
715            "lib"
716          ],
717          "package": "reverie-process",
718          "profile": {
719            "debug_assertions": true,
720            "debuginfo": 2,
721            "opt_level": "0",
722            "overflow_checks": true,
723            "test": false
724          },
725          "target": "reverie_process",
726          "version": "0.5.4"
727        },
728        {
729          "crate_types": [
730            "bin"
731          ],
732          "features": [
733            "default"
734          ],
735          "kind": [
736            "custom-build"
737          ],
738          "package": "reverie-ptrace",
739          "profile": {
740            "debug_assertions": true,
741            "debuginfo": 2,
742            "opt_level": "0",
743            "overflow_checks": true,
744            "test": false
745          },
746          "target": "build-script-build",
747          "version": "0.5.4"
748        },
749        {
750          "crate_types": [
751            "lib"
752          ],
753          "features": [
754            "default"
755          ],
756          "kind": [
757            "lib"
758          ],
759          "package": "reverie-ptrace",
760          "profile": {
761            "debug_assertions": true,
762            "debuginfo": 2,
763            "opt_level": "0",
764            "overflow_checks": true,
765            "test": false
766          },
767          "target": "reverie_ptrace",
768          "version": "0.5.4"
769        },
770        {
771          "crate_types": [
772            "lib"
773          ],
774          "features": [],
775          "kind": [
776            "lib"
777          ],
778          "package": "reverie-syscalls",
779          "profile": {
780            "debug_assertions": true,
781            "debuginfo": 2,
782            "opt_level": "0",
783            "overflow_checks": true,
784            "test": false
785          },
786          "target": "reverie_syscalls",
787          "version": "0.5.4"
788        },
789        {
790          "crate_types": [
791            "lib"
792          ],
793          "features": [
794            "default",
795            "memory",
796            "notifier",
797            "reverie-memory"
798          ],
799          "kind": [
800            "lib"
801          ],
802          "package": "safeptrace",
803          "profile": {
804            "debug_assertions": true,
805            "debuginfo": 2,
806            "opt_level": "0",
807            "overflow_checks": true,
808            "test": false
809          },
810          "target": "safeptrace",
811          "version": "0.5.4"
812        }
813      ]
814    },
815    "rustc_inputs": {
816      "auxiliary_commands": 9,
817      "auxiliary_sha256": "6d34ed7b4c1e73b2b5d1a54aaa04103f55bd2d898a8cac685d44b795c0835db1",
818      "normalization": "hermit-shared-rustc-semantics/v3",
819      "schema": "hermit-actual-rustc-capture/v3",
820      "shared_sha256": "794a32fc7083160f524eb7fdf7a328986bc31bb7730253f9c1677a0b4a8c765e",
821      "shared_units": 282
822    },
823    "schema": "hermit-core-compatibility/v1",
824    "source": {
825      "commit": "36609459b88ad44904cfb91e90b025cbe07f18e6",
826      "kind": "git",
827      "sha256": "a34c1786a0d6d03a84a876edc71b5d3d39e1a03497b3b010ca5b80c10b754e86",
828      "tree": "e7d0f9b9bb10a5e74579e9c777d74a5993b4e37d"
829    },
830    "target": "x86_64-unknown-linux-gnu"
831  }"#).unwrap();
832        actual.validate().unwrap();
833        assert_eq!(actual.rustc_inputs.shared_units, 282);
834        assert_eq!(actual.rustc_inputs.auxiliary_commands, 9);
835        let bytes = Probe {
836            schema: PROBE_SCHEMA.to_owned(),
837            executable: ExecutableIdentity::Core,
838            core: actual.clone(),
839        }
840        .encode()
841        .unwrap();
842        Probe::require(&bytes, &ExecutableIdentity::Core, &actual).unwrap();
843
844        for field in ["auxiliary_commands", "auxiliary_sha256"] {
845            let mut missing = serde_json::to_value(&actual).unwrap();
846            missing["rustc_inputs"]
847                .as_object_mut()
848                .unwrap()
849                .remove(field);
850            assert!(
851                serde_json::from_value::<CoreCompatibility>(missing).is_err(),
852                "accepted missing {field}"
853            );
854        }
855        for version in ["v1", "v2"] {
856            let mut obsolete = actual.clone();
857            obsolete.rustc_inputs.schema = format!("hermit-actual-rustc-capture/{version}");
858            obsolete.rustc_inputs.normalization =
859                format!("hermit-shared-rustc-semantics/{version}");
860            assert!(
861                obsolete.validate().is_err(),
862                "accepted incomplete capture {version}"
863            );
864        }
865        let mut wrong_semantics = actual.clone();
866        wrong_semantics.rustc_inputs.normalization = "hermit-shared-rustc-semantics/v2".to_owned();
867        assert!(wrong_semantics.validate().is_err());
868        for malformed in ["", "unknown", "1234", &"A".repeat(64)] {
869            let mut invalid = actual.clone();
870            invalid.rustc_inputs.auxiliary_sha256 = malformed.to_owned();
871            assert!(
872                invalid.validate().is_err(),
873                "accepted malformed auxiliary digest"
874            );
875        }
876        let mut invalid_count = serde_json::to_value(&actual).unwrap();
877        invalid_count["rustc_inputs"]["auxiliary_commands"] = (-1).into();
878        assert!(serde_json::from_value::<CoreCompatibility>(invalid_count).is_err());
879
880        let mut changed_count = actual.clone();
881        changed_count.rustc_inputs.auxiliary_commands += 1;
882        changed_count.validate().unwrap();
883        assert!(actual.require_exact(&changed_count).is_err());
884        let mut changed_digest = actual.clone();
885        changed_digest.rustc_inputs.auxiliary_sha256 = "f".repeat(64);
886        changed_digest.validate().unwrap();
887        assert!(actual.require_exact(&changed_digest).is_err());
888    }
889
890    #[test]
891    fn executable_probe_refuses_wrong_identity_trailing_data_and_unbounded_output() {
892        let identity = ExecutableIdentity::backend("dynamorio").unwrap();
893        let core = stamp();
894        let probe = Probe {
895            schema: PROBE_SCHEMA.to_owned(),
896            executable: identity.clone(),
897            core: core.clone(),
898        };
899        let bytes = probe.encode().unwrap();
900        Probe::require(&bytes, &identity, &core).unwrap();
901        assert!(Probe::require(&bytes, &ExecutableIdentity::Core, &core).is_err());
902        let mut trailing = bytes.clone();
903        trailing.extend_from_slice(b"{}\n");
904        assert!(Probe::require(&trailing, &identity, &core).is_err());
905        assert!(Probe::require(&vec![b' '; MAX_PROBE_BYTES + 1], &identity, &core).is_err());
906    }
907}
908
909#[cfg(not(fbcode_build))]
910#[derive(Debug, Serialize, Deserialize)]
911#[serde(tag = "availability", rename_all = "kebab-case", deny_unknown_fields)]
912enum Embedded {
913    Measured { stamp: Box<CoreCompatibility> },
914    Unavailable { reason: String },
915}
916
917/// Return only a complete measured identity. Native/export/registry builds
918/// without an explicit producer remain usable for builtins, but cannot claim
919/// compatibility by substituting BuildInfo, a short revision, or a package name.
920pub fn current() -> Result<CoreCompatibility> {
921    #[cfg(fbcode_build)]
922    bail!("extension compatibility unavailable: native/export producer is not implemented");
923
924    #[cfg(not(fbcode_build))]
925    {
926        let bytes = option_env!("HERMIT_CORE_COMPATIBILITY").ok_or_else(|| {
927            anyhow::anyhow!("extension compatibility unavailable: missing build producer")
928        })?;
929        ensure!(
930            bytes.len() <= MAX_PROBE_BYTES,
931            "embedded compatibility exceeds its protocol bound"
932        );
933        match serde_json::from_str::<Embedded>(bytes)? {
934            Embedded::Measured { stamp } => {
935                stamp.validate()?;
936                Ok(*stamp)
937            }
938            Embedded::Unavailable { reason } => bail!(
939                "extension compatibility unavailable: {reason}; use ci/build-compatible-cli.rs at the actual top-level build workspace with complete clean core source"
940            ),
941        }
942    }
943}
944
945/// The executable's compiled selection, independent of any user option or
946/// environment value. It does not authorize a private runtime role.
947#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
948#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
949pub enum ExecutableIdentity {
950    /// The ordinary first-party CLI executable.
951    Core,
952    /// A separately linked executable with one static adapter.
953    Backend { name: String },
954}
955
956impl ExecutableIdentity {
957    /// Construct the identity that a static backend entry reports.
958    pub fn backend(name: &str) -> Result<Self> {
959        ensure!(
960            !name.is_empty() && name.len() <= 64,
961            "invalid executable backend identity"
962        );
963        ensure!(
964            name.bytes()
965                .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-'),
966            "invalid executable backend identity"
967        );
968        ensure!(
969            name.as_bytes()[0].is_ascii_lowercase() && !name.ends_with('-'),
970            "invalid executable backend identity"
971        );
972        Ok(Self::Backend {
973            name: name.to_owned(),
974        })
975    }
976}
977
978/// Closed identity-only probe response. This is not an execution receipt.
979#[derive(Debug, Clone, Serialize, Deserialize)]
980#[serde(deny_unknown_fields)]
981pub struct Probe {
982    schema: String,
983    executable: ExecutableIdentity,
984    core: CoreCompatibility,
985}
986
987impl Probe {
988    /// Construct from the compiled core and the entry's static adapter name.
989    pub fn current(executable: ExecutableIdentity) -> Result<Self> {
990        Ok(Self {
991            schema: PROBE_SCHEMA.to_owned(),
992            executable,
993            core: current()?,
994        })
995    }
996
997    /// Encode a finite response without entering the application.
998    pub fn encode(&self) -> Result<Vec<u8>> {
999        self.core.validate()?;
1000        let mut bytes = serde_json::to_vec(self)?;
1001        bytes.push(b'\n');
1002        ensure!(
1003            bytes.len() <= MAX_PROBE_BYTES,
1004            "compatibility probe exceeds its output bound"
1005        );
1006        Ok(bytes)
1007    }
1008
1009    /// Decode the complete response and bind both its exact backend identity
1010    /// and every core dimension. Trailing data and unknown fields are refused.
1011    pub fn require(
1012        bytes: &[u8],
1013        expected: &ExecutableIdentity,
1014        core: &CoreCompatibility,
1015    ) -> Result<Self> {
1016        ensure!(
1017            bytes.len() <= MAX_PROBE_BYTES,
1018            "compatibility probe exceeds its output bound"
1019        );
1020        let probe: Self = serde_json::from_slice(bytes)?;
1021        ensure!(
1022            probe.schema == PROBE_SCHEMA,
1023            "unsupported executable probe schema"
1024        );
1025        ensure!(
1026            &probe.executable == expected,
1027            "backend executable reported another identity; recursive/core redispatch is refused"
1028        );
1029        core.require_exact(&probe.core)?;
1030        Ok(probe)
1031    }
1032}