1#[cfg(target_os = "linux")]
36use std::path::Path;
37use std::path::PathBuf;
38
39#[cfg(target_os = "linux")]
40use crate::interpreter_runtime::{DerivedInterpreterRuntime, Facts, InterpreterKind};
41#[cfg(target_os = "linux")]
42use crate::murmur_home::FileStamp;
43#[cfg(target_os = "linux")]
44use crate::sandbox::ElfDependencies;
45
46#[cfg(target_os = "linux")]
49pub(crate) const HOST_FACTS_FORMAT: u32 = 1;
50
51#[cfg(target_os = "linux")]
54const MAX_ENTRY_BYTES: u64 = 4 * 1024 * 1024;
55
56#[derive(Debug, Clone)]
58#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
59pub struct HostFactsCache {
60 dir: Option<PathBuf>,
62}
63
64impl HostFactsCache {
65 pub fn in_murmur_home() -> Self {
67 Self { dir: None }
68 }
69
70 pub fn at(dir: impl Into<PathBuf>) -> Self {
73 Self {
74 dir: Some(dir.into()),
75 }
76 }
77
78 #[cfg(target_os = "linux")]
86 pub(crate) fn open(&self, roots: &[PathBuf]) -> Option<Shelf> {
87 let reachable = |dir: &Path| {
88 roots
89 .iter()
90 .any(|root| crate::murmur_home::reachable_from(dir, root))
91 };
92 let (home, name) = match &self.dir {
93 None => (
94 crate::murmur_home::ensure_murmur_home().ok()?,
95 crate::murmur_home::HomeEntryKind::HostFacts
96 .file_name()?
97 .to_string(),
98 ),
99 Some(dir) => (
100 dir.parent()?.to_path_buf(),
101 dir.file_name()?.to_str()?.to_string(),
102 ),
103 };
104 let (dir, emptied) = crate::murmur_home::private_subdir(&home, &name, true, reachable)?;
105 Some(Shelf { dir, emptied })
106 }
107}
108
109#[derive(Debug, Clone, PartialEq, Eq)]
111pub struct HostFactsUse {
112 pub binary: PathBuf,
114 pub source: HostFactsSource,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq)]
118pub enum HostFactsSource {
119 Kept,
121 Derived {
125 reason: HostFactsReason,
126 changed_path: Option<PathBuf>,
127 },
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub enum HostFactsReason {
133 Absent,
135 Changed,
138 Unreadable,
141 NotKept,
143}
144
145impl HostFactsReason {
146 pub fn wire_name(self) -> &'static str {
148 match self {
149 Self::Absent => "absent",
150 Self::Changed => "changed",
151 Self::Unreadable => "unreadable",
152 Self::NotKept => "not-kept",
153 }
154 }
155}
156
157#[cfg(target_os = "linux")]
159pub(crate) struct Shelf {
160 dir: PathBuf,
161 emptied: bool,
163}
164
165#[cfg(target_os = "linux")]
167pub(crate) struct RuntimeOf {
168 pub(crate) runtime: Option<DerivedInterpreterRuntime>,
170 pub(crate) source: HostFactsSource,
171 pub(crate) held: bool,
174}
175
176#[cfg(target_os = "linux")]
181pub(crate) fn runtime_of(
182 shelf: Option<&Shelf>,
183 binary: &Path,
184 kinds: &[InterpreterKind<'_>],
185 roots: &[PathBuf],
186 timeout: std::time::Duration,
187 report: &mut dyn FnMut(&str),
188) -> RuntimeOf {
189 let derived = |reason, changed_path| HostFactsSource::Derived {
190 reason,
191 changed_path,
192 };
193 let Some(shelf) = shelf.filter(|shelf| !shelf.emptied) else {
194 let fresh = derive(binary, kinds, roots, timeout, report);
195 let held = match (shelf, fresh.entry) {
196 (Some(shelf), Some(entry)) => store(&shelf.dir.join(entry_name(binary)), &entry),
197 _ => false,
198 };
199 return RuntimeOf {
200 runtime: fresh.runtime,
201 source: derived(HostFactsReason::NotKept, None),
202 held,
203 };
204 };
205 let path = shelf.dir.join(entry_name(binary));
206 let source = match read_entry(&path) {
207 Read::Absent => derived(HostFactsReason::Absent, None),
208 Read::Unreadable => derived(HostFactsReason::Unreadable, None),
209 Read::Entry(entry) => match reuse(&entry, binary, kinds, roots, report) {
210 Ok(runtime) => {
211 return RuntimeOf {
212 runtime,
213 source: HostFactsSource::Kept,
214 held: true,
215 }
216 }
217 Err(Stale::Unreadable) => derived(HostFactsReason::Unreadable, None),
218 Err(Stale::Changed(changed)) => derived(HostFactsReason::Changed, Some(changed)),
219 },
220 };
221 let fresh = derive(binary, kinds, roots, timeout, report);
222 let held = fresh.entry.is_some_and(|entry| store(&path, &entry));
223 RuntimeOf {
224 runtime: fresh.runtime,
225 source,
226 held,
227 }
228}
229
230#[cfg(target_os = "linux")]
232fn entry_name(binary: &Path) -> String {
233 use std::os::unix::ffi::OsStrExt;
234 format!(
235 "{}.json",
236 murmur_artifact::sha256_hex(binary.as_os_str().as_bytes())
237 )
238}
239
240#[cfg(target_os = "linux")]
242#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
243#[serde(deny_unknown_fields)]
244struct Entry {
245 format: u32,
247 runtime_version: String,
249 kinds: Vec<KindRecord>,
251 search_dirs: Vec<String>,
253 binary: Stamped,
255 context: Vec<Stamped>,
257 kind: Option<String>,
260 reported: Vec<String>,
262 entries: Vec<ReportedEntry>,
264 dirs: Vec<ListedDir>,
266 elf: Vec<ElfFacts>,
268}
269
270#[cfg(target_os = "linux")]
271#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
272#[serde(deny_unknown_fields)]
273struct KindRecord {
274 name: String,
275 marker_symbol: String,
276 args: Vec<String>,
277}
278
279#[cfg(target_os = "linux")]
280impl KindRecord {
281 fn table(kinds: &[InterpreterKind<'_>]) -> Vec<Self> {
282 kinds
283 .iter()
284 .map(|kind| Self {
285 name: kind.name.to_string(),
286 marker_symbol: kind.marker_symbol.to_string(),
287 args: kind.args.iter().map(|arg| arg.to_string()).collect(),
288 })
289 .collect()
290 }
291}
292
293#[cfg(target_os = "linux")]
295#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
296#[serde(deny_unknown_fields)]
297struct Stamped {
298 path: PathBuf,
299 stamp: String,
300}
301
302#[cfg(target_os = "linux")]
304#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
305#[serde(deny_unknown_fields)]
306struct MaybeStamped {
307 path: PathBuf,
308 stamp: Option<String>,
309}
310
311#[cfg(target_os = "linux")]
313#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
314#[serde(deny_unknown_fields)]
315struct ReportedEntry {
316 reported: String,
317 canonical: Option<PathBuf>,
318 stamp: Option<String>,
319}
320
321#[cfg(target_os = "linux")]
325#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
326#[serde(deny_unknown_fields)]
327struct ListedDir {
328 path: PathBuf,
329 stamp: Option<String>,
330 seeds: Vec<Stamped>,
331 others: Vec<MaybeStamped>,
332}
333
334#[cfg(target_os = "linux")]
337#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
338#[serde(deny_unknown_fields)]
339struct ElfFacts {
340 path: PathBuf,
341 stamp: Option<String>,
342 dependencies: Option<ElfDependencies>,
343}
344
345#[cfg(target_os = "linux")]
348fn stamp_of(path: &Path) -> Option<String> {
349 std::fs::metadata(path)
350 .ok()
351 .map(|metadata| FileStamp::of(&metadata).to_string())
352}
353
354#[cfg(target_os = "linux")]
356fn context_of(binary: &Path) -> Option<Vec<Stamped>> {
357 binary
358 .ancestors()
359 .skip(1)
360 .take(2)
361 .map(|dir| {
362 Some(Stamped {
363 path: dir.to_path_buf(),
364 stamp: stamp_of(dir)?,
365 })
366 })
367 .collect()
368}
369
370#[cfg(target_os = "linux")]
373fn resolve_reported(reported: &str) -> ReportedEntry {
374 let resolved = Some(Path::new(reported))
375 .filter(|path| path.is_absolute())
376 .and_then(|path| std::fs::canonicalize(path).ok())
377 .and_then(|canonical| {
378 let metadata = std::fs::metadata(&canonical).ok()?;
379 (metadata.is_dir() || metadata.is_file())
380 .then(|| (canonical, FileStamp::of(&metadata).to_string()))
381 });
382 let (canonical, stamp) = resolved.unzip();
383 ReportedEntry {
384 reported: reported.to_string(),
385 canonical,
386 stamp,
387 }
388}
389
390#[cfg(target_os = "linux")]
393struct Fresh {
394 runtime: Option<DerivedInterpreterRuntime>,
395 entry: Option<Entry>,
396}
397
398#[cfg(target_os = "linux")]
402fn derive(
403 binary: &Path,
404 kinds: &[InterpreterKind<'_>],
405 roots: &[PathBuf],
406 timeout: std::time::Duration,
407 report: &mut dyn FnMut(&str),
408) -> Fresh {
409 use crate::interpreter_runtime::{query, recognise, report_not_derived, runtime_from_report};
410
411 let binary_stamp = stamp_of(binary);
414 let context = context_of(binary);
415 let header = |kind: Option<&str>| {
416 Some(Entry {
417 format: HOST_FACTS_FORMAT,
418 runtime_version: env!("CARGO_PKG_VERSION").to_string(),
419 kinds: KindRecord::table(kinds),
420 search_dirs: search_dirs(),
421 binary: Stamped {
422 path: binary.to_path_buf(),
423 stamp: binary_stamp.clone()?,
424 },
425 context: context.clone()?,
426 kind: kind.map(str::to_string),
427 reported: Vec::new(),
428 entries: Vec::new(),
429 dirs: Vec::new(),
430 elf: Vec::new(),
431 })
432 };
433 let Some(kind) = recognise(binary, kinds) else {
434 return Fresh {
435 runtime: None,
436 entry: header(None),
437 };
438 };
439 let reported = match query(binary, kind, timeout) {
440 Ok(reported) => reported,
441 Err(reason) => {
442 report_not_derived(binary, kind, &reason, report);
443 return Fresh {
444 runtime: None,
445 entry: None,
446 };
447 }
448 };
449 let entries = reported
450 .iter()
451 .map(|entry| resolve_reported(entry))
452 .collect();
453 let mut recorder = Recorder::default();
454 let Ok(runtime) = runtime_from_report(binary, kind, &reported, roots, &mut recorder, report);
455 let unmoved = stamp_of(binary) == binary_stamp && context_of(binary) == context;
457 let entry = header(Some(kind.name))
458 .filter(|_| unmoved)
459 .map(|entry| Entry {
460 reported,
461 entries,
462 dirs: recorder.dirs,
463 elf: recorder.elf,
464 ..entry
465 });
466 Fresh { runtime, entry }
467}
468
469#[cfg(target_os = "linux")]
470fn search_dirs() -> Vec<String> {
471 crate::sandbox::DEFAULT_LIBRARY_SEARCH_DIRS
472 .iter()
473 .map(|dir| dir.to_string())
474 .collect()
475}
476
477#[cfg(target_os = "linux")]
479#[derive(Default)]
480struct Recorder {
481 dirs: Vec<ListedDir>,
482 elf: Vec<ElfFacts>,
483}
484
485#[cfg(target_os = "linux")]
486impl Facts for Recorder {
487 type Changed = std::convert::Infallible;
488
489 fn shared_objects_in(&mut self, dir: &Path) -> Result<Vec<PathBuf>, Self::Changed> {
490 use crate::sandbox::{elf_is_shared_object, ElfFile};
491
492 let stamp = stamp_of(dir);
493 let mut seeds = Vec::new();
494 let mut others = Vec::new();
495 for entry in std::fs::read_dir(dir).into_iter().flatten().flatten() {
496 let path = entry.path();
497 if std::fs::symlink_metadata(&path).is_ok_and(|metadata| metadata.is_dir()) {
499 continue;
500 }
501 let metadata = std::fs::metadata(&path).ok();
502 if metadata.as_ref().is_some_and(std::fs::Metadata::is_file) {
503 if let Ok(file) = ElfFile::open_host(&path) {
504 if elf_is_shared_object(&file) {
505 seeds.push(Stamped {
506 stamp: FileStamp::of(file.metadata()).to_string(),
507 path,
508 });
509 continue;
510 }
511 }
512 }
513 others.push(MaybeStamped {
514 stamp: metadata.map(|metadata| FileStamp::of(&metadata).to_string()),
515 path,
516 });
517 }
518 let found = seeds.iter().map(|seed| seed.path.clone()).collect();
519 self.dirs.push(ListedDir {
520 path: dir.to_path_buf(),
521 stamp,
522 seeds,
523 others,
524 });
525 Ok(found)
526 }
527
528 fn dependencies(&mut self, path: &Path) -> Result<Option<ElfDependencies>, Self::Changed> {
529 let (stamp, dependencies) = match crate::sandbox::ElfFile::open_host(path) {
530 Ok(file) => (
531 Some(FileStamp::of(file.metadata()).to_string()),
532 crate::sandbox::parse_elf_dependencies(&file),
533 ),
534 Err(_) => (stamp_of(path), None),
535 };
536 self.elf.push(ElfFacts {
537 path: path.to_path_buf(),
538 stamp,
539 dependencies: dependencies.clone(),
540 });
541 Ok(dependencies)
542 }
543}
544
545#[cfg(target_os = "linux")]
547struct Recorded<'a> {
548 dirs: std::collections::HashMap<&'a Path, &'a ListedDir>,
549 elf: std::collections::HashMap<&'a Path, &'a ElfFacts>,
550}
551
552#[cfg(target_os = "linux")]
553impl<'a> Recorded<'a> {
554 fn of(entry: &'a Entry) -> Self {
555 Self {
556 dirs: entry
557 .dirs
558 .iter()
559 .map(|dir| (dir.path.as_path(), dir))
560 .collect(),
561 elf: entry
562 .elf
563 .iter()
564 .map(|file| (file.path.as_path(), file))
565 .collect(),
566 }
567 }
568}
569
570#[cfg(target_os = "linux")]
571impl Facts for Recorded<'_> {
572 type Changed = PathBuf;
573
574 fn shared_objects_in(&mut self, dir: &Path) -> Result<Vec<PathBuf>, PathBuf> {
575 let changed = |path: &Path| path.to_path_buf();
576 let listed = self.dirs.get(dir).ok_or_else(|| changed(dir))?;
577 if stamp_of(dir) != listed.stamp {
578 return Err(changed(dir));
579 }
580 for seed in &listed.seeds {
581 if stamp_of(&seed.path).as_ref() != Some(&seed.stamp) {
582 return Err(changed(&seed.path));
583 }
584 }
585 for other in &listed.others {
586 if stamp_of(&other.path) != other.stamp {
587 return Err(changed(&other.path));
588 }
589 }
590 Ok(listed.seeds.iter().map(|seed| seed.path.clone()).collect())
591 }
592
593 fn dependencies(&mut self, path: &Path) -> Result<Option<ElfDependencies>, PathBuf> {
594 match self.elf.get(path) {
595 Some(file) if stamp_of(path) == file.stamp => Ok(file.dependencies.clone()),
596 _ => Err(path.to_path_buf()),
597 }
598 }
599}
600
601#[cfg(target_os = "linux")]
602enum Read {
603 Absent,
604 Unreadable,
605 Entry(Box<Entry>),
606}
607
608#[cfg(target_os = "linux")]
611fn read_entry(path: &Path) -> Read {
612 use std::io::Read as _;
613
614 if std::fs::symlink_metadata(path).is_err_and(|err| err.kind() == std::io::ErrorKind::NotFound)
615 {
616 return Read::Absent;
617 }
618 let Some((file, metadata)) = crate::murmur_home::open_private(path) else {
619 return Read::Unreadable;
620 };
621 if metadata.len() > MAX_ENTRY_BYTES {
622 return Read::Unreadable;
623 }
624 let mut bytes = Vec::new();
625 if file.take(MAX_ENTRY_BYTES).read_to_end(&mut bytes).is_err() {
626 return Read::Unreadable;
627 }
628 match serde_json::from_slice(&bytes) {
629 Ok(entry) => Read::Entry(Box::new(entry)),
630 Err(_) => Read::Unreadable,
631 }
632}
633
634#[cfg(target_os = "linux")]
636enum Stale {
637 Unreadable,
638 Changed(PathBuf),
639}
640
641#[cfg(target_os = "linux")]
644fn reuse(
645 entry: &Entry,
646 binary: &Path,
647 kinds: &[InterpreterKind<'_>],
648 roots: &[PathBuf],
649 report: &mut dyn FnMut(&str),
650) -> Result<Option<DerivedInterpreterRuntime>, Stale> {
651 let foreign = entry.format != HOST_FACTS_FORMAT
652 || entry.runtime_version != env!("CARGO_PKG_VERSION")
653 || entry.kinds != KindRecord::table(kinds)
654 || entry.search_dirs != search_dirs()
655 || entry.binary.path != binary
656 || entry.entries.len() != entry.reported.len()
657 || entry
658 .entries
659 .iter()
660 .zip(&entry.reported)
661 .any(|(resolved, reported)| resolved.reported != *reported);
662 let context: Vec<&Path> = binary.ancestors().skip(1).take(2).collect();
663 let context_paths: Vec<&Path> = entry.context.iter().map(|dir| dir.path.as_path()).collect();
664 if foreign || context_paths != context {
665 return Err(Stale::Unreadable);
666 }
667 if stamp_of(binary).as_ref() != Some(&entry.binary.stamp) {
668 return Err(Stale::Changed(binary.to_path_buf()));
669 }
670 for dir in &entry.context {
671 if stamp_of(&dir.path).as_ref() != Some(&dir.stamp) {
672 return Err(Stale::Changed(dir.path.clone()));
673 }
674 }
675 let Some(name) = &entry.kind else {
676 let empty = entry.reported.is_empty() && entry.dirs.is_empty() && entry.elf.is_empty();
677 return if empty {
678 Ok(None)
679 } else {
680 Err(Stale::Unreadable)
681 };
682 };
683 let kind = *kinds
684 .iter()
685 .find(|kind| kind.name == name)
686 .ok_or(Stale::Unreadable)?;
687 for recorded in &entry.entries {
688 let live = resolve_reported(&recorded.reported);
689 if live != *recorded {
690 let changed = recorded
691 .canonical
692 .clone()
693 .or(live.canonical)
694 .unwrap_or_else(|| PathBuf::from(&recorded.reported));
695 return Err(Stale::Changed(changed));
696 }
697 }
698 crate::interpreter_runtime::runtime_from_report(
699 binary,
700 kind,
701 &entry.reported,
702 roots,
703 &mut Recorded::of(entry),
704 report,
705 )
706 .map_err(Stale::Changed)
707}
708
709#[cfg(target_os = "linux")]
712fn store(path: &Path, entry: &Entry) -> bool {
713 serde_json::to_vec(entry)
714 .is_ok_and(|bytes| crate::murmur_home::write_private_file(path, &bytes).is_ok())
715}
716
717#[cfg(all(test, target_os = "linux"))]
718#[allow(clippy::print_stdout)]
719mod tests {
720 use super::*;
721 use crate::interpreter_runtime::tests::{
722 cache_in, canonical_tempdir, dash_copy, dash_kind, pad_with_zeros, thread_rchar,
723 };
724 use crate::interpreter_runtime::{derive_in, InterpreterRuntimes, QUERY_TIMEOUT};
725 use std::os::unix::fs::{MetadataExt, PermissionsExt};
726
727 const EIGHT_MIB: u64 = 8 * 1024 * 1024;
728
729 struct StandIn {
735 _temp: tempfile::TempDir,
736 root: PathBuf,
737 binary: PathBuf,
738 marker: PathBuf,
739 script: String,
740 }
741
742 impl StandIn {
743 fn new(reported: &[&Path]) -> Option<Self> {
744 let (temp, root) = canonical_tempdir();
745 let binary = dash_copy(&root.join("prefix"), "standin")?;
746 let marker = root.join("ran");
747 let printed: Vec<String> = reported
748 .iter()
749 .map(|path| path.display().to_string())
750 .collect();
751 let script = format!(
752 ": > {}; printf '{}'",
753 marker.display(),
754 printed.join("\\000")
755 );
756 Some(Self {
757 _temp: temp,
758 root,
759 binary,
760 marker,
761 script,
762 })
763 }
764
765 fn derive_with(&self, roots: &[&Path], cache: &HostFactsCache) -> InterpreterRuntimes {
767 let args = ["-c", self.script.as_str()];
768 let kinds = [dash_kind(&args)];
769 derive_in(
770 &["standin".to_string()],
771 &[],
772 roots,
773 &[self.binary.parent().unwrap().to_path_buf()],
774 &kinds,
775 QUERY_TIMEOUT,
776 cache,
777 )
778 }
779
780 fn derive(&self, roots: &[&Path]) -> InterpreterRuntimes {
781 self.derive_with(roots, &cache_in(&self.root))
782 }
783
784 fn derive_uncached(&self, roots: &[&Path]) -> InterpreterRuntimes {
786 let not_a_dir = self.root.join("not-a-cache");
787 std::fs::write(¬_a_dir, b"").unwrap();
788 let derived = self.derive_with(roots, &HostFactsCache::at(¬_a_dir));
789 assert_eq!(self.source(&derived), not_kept());
790 derived
791 }
792
793 fn entry(&self) -> PathBuf {
794 self.root.join("host_facts").join(entry_name(&self.binary))
795 }
796
797 fn source(&self, derived: &InterpreterRuntimes) -> HostFactsSource {
798 assert_eq!(derived.host_facts.len(), 1, "{derived:?}");
799 assert_eq!(derived.host_facts[0].binary, self.binary);
800 derived.host_facts[0].source.clone()
801 }
802
803 fn forget_run(&self) {
805 let _ = std::fs::remove_file(&self.marker);
806 }
807
808 fn ran(&self) -> bool {
809 self.marker.exists()
810 }
811 }
812
813 fn derived(reason: HostFactsReason, changed_path: Option<PathBuf>) -> HostFactsSource {
814 HostFactsSource::Derived {
815 reason,
816 changed_path,
817 }
818 }
819
820 fn absent() -> HostFactsSource {
821 derived(HostFactsReason::Absent, None)
822 }
823
824 fn not_kept() -> HostFactsSource {
825 derived(HostFactsReason::NotKept, None)
826 }
827
828 fn shared_object(path: &Path, needed: &[&str], runpath: Option<&str>) {
830 std::fs::write(path, crate::sandbox::build_elf64(None, needed, runpath)).unwrap();
831 }
832
833 fn lib_with_one_module(root: &Path) -> PathBuf {
836 let lib = root.join("lib");
837 std::fs::create_dir_all(&lib).unwrap();
838 shared_object(&lib.join("ext.so"), &["libc.so.6"], None);
839 lib
840 }
841
842 fn holds_a_valid_entry(path: &Path) -> bool {
844 let metadata = std::fs::symlink_metadata(path).unwrap();
845 metadata.is_file()
846 && metadata.mode() & 0o777 == 0o600
847 && matches!(read_entry(path), Read::Entry(_))
848 }
849
850 #[test]
853 fn a_kept_entry_runs_nothing_and_reads_no_binary_in_full() {
854 let (_guard, root) = canonical_tempdir();
855 let lib = lib_with_one_module(&root);
856 let Some(stand_in) = StandIn::new(&[&lib]) else {
857 return;
858 };
859 pad_with_zeros(&stand_in.binary, 64 << 20);
860
861 let first = stand_in.derive(&[]);
862 assert_eq!(stand_in.source(&first), absent());
863 assert!(stand_in.ran());
864 assert!(holds_a_valid_entry(&stand_in.entry()));
865 assert_eq!(first.runtimes.len(), 1, "{first:?}");
866 assert_eq!(first.runtimes[0].dirs, vec![lib.clone()]);
867
868 stand_in.forget_run();
869 let before = thread_rchar();
870 let second = stand_in.derive(&[]);
871 let read = thread_rchar() - before;
872 assert_eq!(stand_in.source(&second), HostFactsSource::Kept);
873 assert_eq!(second.runtimes, first.runtimes);
874 assert!(!stand_in.ran(), "a kept entry ran the interpreter");
875 assert!(read < EIGHT_MIB, "the kept staging read {read} bytes");
876 println!("a kept staging read {read} bytes");
877 }
878
879 #[test]
881 fn a_replaced_binary_is_derived_again() {
882 let (_guard, root) = canonical_tempdir();
883 let lib = lib_with_one_module(&root);
884 let Some(stand_in) = StandIn::new(&[&lib]) else {
885 return;
886 };
887 let first = stand_in.derive(&[]);
888 assert_eq!(
889 stand_in.source(&stand_in.derive(&[])),
890 HostFactsSource::Kept
891 );
892 let stored = std::fs::metadata(stand_in.entry()).unwrap().ino();
893
894 let replacement = stand_in.binary.with_file_name(".standin.new");
895 std::fs::copy(&stand_in.binary, &replacement).unwrap();
896 std::fs::rename(&replacement, &stand_in.binary).unwrap();
897 stand_in.forget_run();
898 let again = stand_in.derive(&[]);
899
900 assert_eq!(
901 stand_in.source(&again),
902 derived(HostFactsReason::Changed, Some(stand_in.binary.clone()))
903 );
904 assert!(stand_in.ran());
905 assert_eq!(again.runtimes, first.runtimes);
906 assert_ne!(std::fs::metadata(stand_in.entry()).unwrap().ino(), stored);
907 assert!(holds_a_valid_entry(&stand_in.entry()));
908 assert_eq!(
909 stand_in.source(&stand_in.derive(&[])),
910 HostFactsSource::Kept
911 );
912 }
913
914 #[test]
916 fn a_module_added_to_a_kept_directory_is_derived_again() {
917 let (_guard, root) = canonical_tempdir();
918 let lib = lib_with_one_module(&root);
919 let deps = root.join("deps");
920 std::fs::create_dir(&deps).unwrap();
921 std::fs::write(deps.join("libextra.so.1"), b"not elf").unwrap();
922 let Some(stand_in) = StandIn::new(&[&lib]) else {
923 return;
924 };
925 let first = stand_in.derive(&[]);
926 assert_eq!(first.runtimes[0].libraries, Vec::<PathBuf>::new());
927 assert_eq!(
928 stand_in.source(&stand_in.derive(&[])),
929 HostFactsSource::Kept
930 );
931
932 shared_object(
933 &lib.join("_extra.so"),
934 &["libextra.so.1"],
935 Some("$ORIGIN/../deps"),
936 );
937 stand_in.forget_run();
938 let again = stand_in.derive(&[]);
939
940 assert_eq!(
941 stand_in.source(&again),
942 derived(HostFactsReason::Changed, Some(lib.clone()))
943 );
944 assert!(stand_in.ran());
945 assert_eq!(
946 again.runtimes[0].libraries,
947 vec![deps.join("libextra.so.1")]
948 );
949 assert_eq!(again.runtimes, stand_in.derive_uncached(&[]).runtimes);
950 assert_eq!(
951 stand_in.source(&stand_in.derive(&[])),
952 HostFactsSource::Kept
953 );
954 }
955
956 fn an_entry_damaged_by(damage: impl FnOnce(&Path)) {
959 let (_guard, root) = canonical_tempdir();
960 let lib = lib_with_one_module(&root);
961 let Some(stand_in) = StandIn::new(&[&lib]) else {
962 return;
963 };
964 let first = stand_in.derive(&[]);
965 assert!(holds_a_valid_entry(&stand_in.entry()));
966
967 damage(&stand_in.entry());
968 stand_in.forget_run();
969 let again = stand_in.derive(&[]);
970
971 assert_eq!(
972 stand_in.source(&again),
973 derived(HostFactsReason::Unreadable, None)
974 );
975 assert!(stand_in.ran(), "a damaged entry was trusted");
976 assert_eq!(again.runtimes, first.runtimes);
977 assert_eq!(again.runtimes, stand_in.derive_uncached(&[]).runtimes);
978 assert!(holds_a_valid_entry(&stand_in.entry()));
979 assert_eq!(
980 stand_in.source(&stand_in.derive(&[])),
981 HostFactsSource::Kept
982 );
983 }
984
985 fn edit_entry(path: &Path, edit: impl FnOnce(&mut serde_json::Value)) {
987 let mut value: serde_json::Value =
988 serde_json::from_slice(&std::fs::read(path).unwrap()).unwrap();
989 edit(&mut value);
990 crate::murmur_home::write_private_file(path, value.to_string().as_bytes()).unwrap();
991 }
992
993 #[test]
994 fn an_entry_of_random_bytes_is_derived_again() {
995 an_entry_damaged_by(|path| {
996 let mut state: u64 = 0x9e37_79b9_7f4a_7c15;
997 let bytes: Vec<u8> = (0..4096)
998 .map(|_| {
999 state ^= state << 13;
1000 state ^= state >> 7;
1001 state ^= state << 17;
1002 state as u8
1003 })
1004 .collect();
1005 crate::murmur_home::write_private_file(path, &bytes).unwrap();
1006 });
1007 }
1008
1009 #[test]
1010 fn a_truncated_entry_is_derived_again() {
1011 an_entry_damaged_by(|path| {
1012 let bytes = std::fs::read(path).unwrap();
1013 crate::murmur_home::write_private_file(path, &bytes[..bytes.len() / 2]).unwrap();
1014 });
1015 }
1016
1017 #[test]
1018 fn an_entry_of_another_format_is_derived_again() {
1019 an_entry_damaged_by(|path| {
1020 edit_entry(path, |entry| {
1021 entry["format"] = serde_json::json!(HOST_FACTS_FORMAT + 1);
1022 });
1023 });
1024 }
1025
1026 #[test]
1027 fn an_entry_from_another_runtime_version_is_derived_again() {
1028 an_entry_damaged_by(|path| {
1029 edit_entry(path, |entry| entry["runtime_version"] = "0.0.0".into());
1030 });
1031 }
1032
1033 #[test]
1034 fn an_entry_from_another_interpreter_table_is_derived_again() {
1035 an_entry_damaged_by(|path| {
1036 edit_entry(path, |entry| {
1037 entry["kinds"][0]["args"] = serde_json::json!(["-c", "exit 0"]);
1038 });
1039 });
1040 }
1041
1042 #[test]
1043 fn a_group_readable_entry_is_derived_again() {
1044 an_entry_damaged_by(|path| {
1045 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o644)).unwrap();
1046 });
1047 }
1048
1049 #[test]
1050 fn an_entry_that_is_a_symlink_to_a_valid_entry_is_derived_again() {
1051 an_entry_damaged_by(|path| {
1052 let target = path.with_file_name("elsewhere.json");
1053 std::fs::rename(path, &target).unwrap();
1054 std::os::unix::fs::symlink(&target, path).unwrap();
1055 assert!(holds_a_valid_entry(&target));
1056 });
1057 }
1058
1059 #[test]
1060 fn an_entry_for_another_binary_is_derived_again() {
1061 an_entry_damaged_by(|path| {
1062 edit_entry(path, |entry| {
1063 entry["binary"]["path"] = "/usr/bin/another".into();
1064 });
1065 });
1066 }
1067
1068 #[test]
1071 fn the_cache_prints_nothing_of_its_own() {
1072 let source = include_str!("host_facts.rs");
1073 let production = &source[..source
1074 .find("#[cfg(all(test, target_os = \"linux\"))]")
1075 .unwrap()];
1076 for writer in ["runtime_err!", "eprintln!", "eprint!", "println!", "stderr"] {
1077 assert!(!production.contains(writer), "host_facts.rs uses {writer}");
1078 }
1079 }
1080
1081 #[test]
1086 fn a_kept_entry_is_filtered_against_each_stagings_roots() {
1087 let (_guard, root) = canonical_tempdir();
1088 let a = root.join("a");
1089 let b = root.join("b");
1090 let deps = root.join("deps");
1091 for dir in [&a, &b, &deps] {
1092 std::fs::create_dir(dir).unwrap();
1093 }
1094 shared_object(
1095 &a.join("ext.so"),
1096 &["libextra.so.1"],
1097 Some("$ORIGIN/../deps"),
1098 );
1099 std::fs::write(deps.join("libextra.so.1"), b"not elf").unwrap();
1100 let Some(stand_in) = StandIn::new(&[&a, &b]) else {
1101 return;
1102 };
1103
1104 let first = stand_in.derive(&[&deps]);
1105 assert_eq!(stand_in.source(&first), absent());
1106 assert_eq!(first.runtimes[0].dirs, vec![a.clone(), b.clone()]);
1107 assert_eq!(first.runtimes[0].libraries, Vec::<PathBuf>::new());
1108
1109 stand_in.forget_run();
1110 let without_b = stand_in.derive(&[&deps, &b]);
1111 assert_eq!(stand_in.source(&without_b), HostFactsSource::Kept);
1112 assert_eq!(without_b.runtimes[0].dirs, vec![a.clone()]);
1113 assert_eq!(
1114 without_b.runtimes,
1115 stand_in.derive_uncached(&[&deps, &b]).runtimes
1116 );
1117 stand_in.forget_run();
1118 let both = stand_in.derive(&[&deps]);
1119 assert_eq!(stand_in.source(&both), HostFactsSource::Kept);
1120 assert_eq!(both.runtimes, first.runtimes);
1121 assert!(!stand_in.ran());
1122
1123 let reached = stand_in.derive(&[]);
1124 assert_eq!(
1125 stand_in.source(&reached),
1126 derived(HostFactsReason::Changed, Some(deps.join("libextra.so.1")))
1127 );
1128 assert!(stand_in.ran());
1129 assert_eq!(
1130 reached.runtimes[0].libraries,
1131 vec![deps.join("libextra.so.1")]
1132 );
1133 assert_eq!(reached.runtimes, stand_in.derive_uncached(&[]).runtimes);
1134 }
1135
1136 #[test]
1138 fn a_cache_under_a_writable_root_is_never_read_or_written() {
1139 let (_guard, root) = canonical_tempdir();
1140 let lib = lib_with_one_module(&root);
1141 let Some(stand_in) = StandIn::new(&[&lib]) else {
1142 return;
1143 };
1144 let first = stand_in.derive(&[]);
1145 let work = root.join("work");
1146 std::fs::create_dir(&work).unwrap();
1147 crate::murmur_home::hold_private_dir(&work.join("host_facts")).unwrap();
1148 let planted = work.join("host_facts").join(entry_name(&stand_in.binary));
1149 std::fs::copy(stand_in.entry(), &planted).unwrap();
1150 std::fs::set_permissions(&planted, std::fs::Permissions::from_mode(0o600)).unwrap();
1151 let planted_stamp = stamp_of(&planted);
1152
1153 stand_in.forget_run();
1154 let under = stand_in.derive_with(&[&work], &HostFactsCache::at(work.join("host_facts")));
1155 assert_eq!(stand_in.source(&under), not_kept());
1156 assert!(stand_in.ran(), "the entry under the writable root was read");
1157 assert_eq!(under.runtimes, first.runtimes);
1158 assert_eq!(stamp_of(&planted), planted_stamp, "the entry was rewritten");
1159 assert_eq!(
1160 std::fs::read_dir(work.join("host_facts")).unwrap().count(),
1161 1
1162 );
1163
1164 let fresh = work.join("fresh");
1166 let uncreated =
1167 stand_in.derive_with(&[&work], &HostFactsCache::at(fresh.join("host_facts")));
1168 assert_eq!(stand_in.source(&uncreated), not_kept());
1169 assert!(!fresh.join("host_facts").exists());
1170 }
1171
1172 #[test]
1175 fn a_symlinked_or_wide_cache_directory_keeps_nothing() {
1176 let (_guard, root) = canonical_tempdir();
1177 let lib = lib_with_one_module(&root);
1178 let Some(stand_in) = StandIn::new(&[&lib]) else {
1179 return;
1180 };
1181 let first = stand_in.derive(&[]);
1182 let entry_file = entry_name(&stand_in.binary);
1183
1184 let real = root.join("real");
1185 crate::murmur_home::hold_private_dir(&real).unwrap();
1186 std::fs::copy(stand_in.entry(), real.join(&entry_file)).unwrap();
1187 std::fs::set_permissions(
1188 real.join(&entry_file),
1189 std::fs::Permissions::from_mode(0o600),
1190 )
1191 .unwrap();
1192 let real_stamp = stamp_of(&real.join(&entry_file));
1193 let link_home = root.join("linked");
1194 std::fs::create_dir(&link_home).unwrap();
1195 std::os::unix::fs::symlink(&real, link_home.join("host_facts")).unwrap();
1196 stand_in.forget_run();
1197 let linked = stand_in.derive_with(&[], &HostFactsCache::at(link_home.join("host_facts")));
1198 assert_eq!(stand_in.source(&linked), not_kept());
1199 assert!(stand_in.ran());
1200 assert_eq!(linked.runtimes, first.runtimes);
1201 assert_eq!(stamp_of(&real.join(&entry_file)), real_stamp);
1202
1203 let wide_home = root.join("wide");
1204 let wide = wide_home.join("host_facts");
1205 std::fs::create_dir_all(&wide).unwrap();
1206 std::fs::set_permissions(&wide, std::fs::Permissions::from_mode(0o755)).unwrap();
1207 std::fs::copy(stand_in.entry(), wide.join(&entry_file)).unwrap();
1208 std::fs::set_permissions(
1209 wide.join(&entry_file),
1210 std::fs::Permissions::from_mode(0o600),
1211 )
1212 .unwrap();
1213 let planted_link = root.join("planted-entry");
1216 std::fs::hard_link(wide.join(&entry_file), &planted_link).unwrap();
1217 let planted_ino = std::fs::metadata(&planted_link).unwrap().ino();
1218 std::fs::write(wide.join("planted.json"), b"{}").unwrap();
1219 std::fs::create_dir(wide.join("subdir")).unwrap();
1220 stand_in.forget_run();
1221 let narrowed = stand_in.derive_with(&[], &HostFactsCache::at(&wide));
1222 assert_eq!(stand_in.source(&narrowed), not_kept());
1223 assert!(stand_in.ran(), "an entry in a wide directory was trusted");
1224 assert_eq!(narrowed.runtimes, first.runtimes);
1225 assert_eq!(
1226 std::fs::symlink_metadata(&wide).unwrap().mode() & 0o777,
1227 0o700
1228 );
1229 assert!(!wide.join("planted.json").exists());
1230 assert!(wide.join("subdir").is_dir());
1231 assert_ne!(
1232 std::fs::metadata(wide.join(&entry_file)).unwrap().ino(),
1233 planted_ino,
1234 "the entry found in the wide directory was kept"
1235 );
1236 assert_eq!(std::fs::metadata(&planted_link).unwrap().nlink(), 1);
1237 assert!(holds_a_valid_entry(&wide.join(&entry_file)));
1238 let kept = stand_in.derive_with(&[], &HostFactsCache::at(&wide));
1239 assert_eq!(stand_in.source(&kept), HostFactsSource::Kept);
1240 }
1241}