1#![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
18pub const SCHEMA: &str = "hermit-core-compatibility/v1";
20pub const API: &str = "hermit-cli-static-backend/v1";
22pub const PROBE_SCHEMA: &str = "hermit-executable-probe/v1";
24pub const MAX_PROBE_BYTES: usize = 16 * 1024;
26pub const PROBE_ARG: &str = "--hermit-compatibility-v1";
28pub 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#[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 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 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 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
917pub 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
948#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
949pub enum ExecutableIdentity {
950 Core,
952 Backend { name: String },
954}
955
956impl ExecutableIdentity {
957 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#[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 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 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 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}