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