1use crate::{elf::ElfInfo, report, version::Version};
17use kernel_abi_tools::{
18 container::{self, Engine},
19 local, table_floor, KernelVersion, Runner, Target,
20};
21use std::{
22 collections::BTreeMap,
23 env,
24 ffi::OsString,
25 fs,
26 path::{Path, PathBuf},
27 process::Command,
28};
29
30const GLIBC_ENV: &str = "CARGO_LIBC_RUN_GLIBC";
31const DISTRO_ENV: &str = "CARGO_LIBC_RUN_DISTRO";
32const KERNEL_ENV: &str = "CARGO_LIBC_RUN_KERNEL";
33const RUNNER_ENV: &str = "CARGO_LIBC_RUN_RUNNER";
34const IMAGE_ENV: &str = "CARGO_LIBC_RUN_IMAGE";
35
36#[derive(Clone, Debug, Default, PartialEq, Eq)]
39pub(crate) struct Simulation {
40 pub(crate) distro: Option<String>,
41 pub(crate) kernel: Option<String>,
42 pub(crate) runner: Option<Runner>,
43 pub(crate) image: Option<String>,
44}
45
46#[derive(Debug, PartialEq, Eq)]
48pub(crate) enum Plan {
49 Direct,
51 Local(String),
52 Container(String, String),
53}
54
55impl Simulation {
56 pub(crate) fn target(&self) -> Result<Option<Target>, String> {
58 match (&self.distro, &self.kernel) {
59 (Some(_), Some(_)) => {
60 Err("--distro sets the kernel floor; do not combine it with --kernel".into())
61 }
62 (Some(id), None) => {
63 let d =
64 kernel_abi_tools::distro(id).ok_or_else(|| format!("unknown distro {id:?}"))?;
65 Ok(Some(Target::distro(&d)))
66 }
67 (None, Some(k)) => Ok(Some(Target::kernel(KernelVersion::parse(k)?))),
68 (None, None) => Ok(None),
69 }
70 }
71
72 pub(crate) fn plan(&self) -> Result<(Plan, Option<Target>), String> {
77 let target = self.target()?;
78 let Some(t) = &target else {
79 if self.runner.is_some() || self.image.is_some() {
80 return Err(
81 "--runner and --image need a kernel floor (--distro or --kernel)".into(),
82 );
83 }
84 return Ok((Plan::Direct, None));
85 };
86 let preset = self
87 .distro
88 .as_deref()
89 .and_then(kernel_abi_tools::distro)
90 .map(|d| d.image.to_string());
91 let image = match self.runner {
92 Some(Runner::Local) => self.image.clone(),
93 _ => self.image.clone().or(preset),
94 };
95 let plan = match Runner::choose(self.runner, image.as_deref())? {
96 Runner::Local => Plan::Local(t.label.clone()),
97 Runner::Container => Plan::Container(t.label.clone(), image.unwrap_or_default()),
98 };
99 Ok((plan, target))
100 }
101
102 pub(crate) fn variables(&self, ceiling: Version) -> [(&'static str, Option<String>); 5] {
105 let runner = self.runner.map(|r| match r {
106 Runner::Local => "local".to_string(),
107 Runner::Container => "container".to_string(),
108 });
109 [
110 (GLIBC_ENV, Some(ceiling.to_string())),
111 (DISTRO_ENV, self.distro.clone()),
112 (KERNEL_ENV, self.kernel.clone()),
113 (RUNNER_ENV, runner),
114 (IMAGE_ENV, self.image.clone()),
115 ]
116 }
117
118 pub(crate) fn set_environment(&self, cmd: &mut Command, ceiling: Version) {
119 for (key, value) in self.variables(ceiling) {
120 match value {
121 Some(v) => cmd.env(key, v),
122 None => cmd.env_remove(key),
123 };
124 }
125 }
126
127 fn from_environment() -> Result<(Self, Version), String> {
128 let text = |key: &str| -> Result<Option<String>, String> {
129 match env::var_os(key) {
130 None => Ok(None),
131 Some(v) if v.is_empty() => Ok(None),
132 Some(v) => v
133 .into_string()
134 .map(Some)
135 .map_err(|_| format!("{key} must be UTF-8")),
136 }
137 };
138 let ceiling = text(GLIBC_ENV)?
139 .ok_or_else(|| {
140 format!("{GLIBC_ENV} is missing; this helper is launched by cargo libc test/run")
141 })?
142 .parse::<Version>()?
143 .validate_glibc_target()?;
144 let sim = Simulation {
145 distro: text(DISTRO_ENV)?,
146 kernel: text(KERNEL_ENV)?,
147 runner: text(RUNNER_ENV)?.map(|r| Runner::parse(&r)).transpose()?,
148 image: text(IMAGE_ENV)?,
149 };
150 Ok((sim, ceiling))
151 }
152}
153
154pub(crate) fn describe(plan: &Plan) -> String {
156 match plan {
157 Plan::Direct => "no kernel simulation (host kernel); glibc check only".into(),
158 Plan::Local(label) => {
159 format!("{label} with the local seccomp filter; userland (libc) is the host's")
160 }
161 Plan::Container(label, image) => format!("{label} in container image {image}"),
162 }
163}
164
165#[derive(Debug, PartialEq, Eq)]
167pub(crate) enum HostGlibc {
168 Satisfied,
169 Missing(Vec<String>),
171 Unknown(String),
173}
174
175pub(crate) fn host_glibc(
178 info: &ElfInfo,
179 mut defined: impl FnMut(&str) -> Result<Vec<String>, String>,
180) -> HostGlibc {
181 let mut required: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
182 for need in info
183 .versions
184 .iter()
185 .filter(|n| !n.weak && n.name.starts_with("GLIBC_"))
186 {
187 required.entry(&need.library).or_default().push(&need.name);
188 }
189 let mut missing = Vec::new();
190 for (library, names) in required {
191 let available = match defined(library) {
192 Ok(available) => available,
193 Err(e) => return HostGlibc::Unknown(e),
194 };
195 for name in names {
196 if !available.iter().any(|a| a == name) {
197 missing.push(format!("{library} {name}"));
198 }
199 }
200 }
201 if missing.is_empty() {
202 HostGlibc::Satisfied
203 } else {
204 HostGlibc::Missing(missing)
205 }
206}
207
208fn host_libc_dir() -> Result<PathBuf, String> {
210 let maps = fs::read_to_string("/proc/self/maps")
211 .map_err(|e| format!("cannot read /proc/self/maps: {e}"))?;
212 maps.lines()
213 .filter_map(|line| line.split_whitespace().nth(5))
214 .map(Path::new)
215 .find(|path| path.file_name().is_some_and(|name| name == "libc.so.6"))
216 .and_then(Path::parent)
217 .map(Path::to_path_buf)
218 .ok_or_else(|| "this helper did not load a glibc libc.so.6".into())
219}
220
221fn search_dirs(program: &Path, info: &ElfInfo) -> Result<Vec<PathBuf>, String> {
225 let origin = program
226 .canonicalize()
227 .ok()
228 .and_then(|p| p.parent().map(Path::to_path_buf))
229 .unwrap_or_else(|| PathBuf::from("."));
230 let expand = |list: &str| -> Result<Vec<PathBuf>, String> {
231 list.split(':')
232 .map(|entry| {
233 let entry = entry
234 .replace("${ORIGIN}", &origin.to_string_lossy())
235 .replace("$ORIGIN", &origin.to_string_lossy());
236 if entry.contains('$') {
237 return Err(format!("cannot expand library search path entry {entry:?}"));
238 }
239 Ok(PathBuf::from(if entry.is_empty() { "." } else { &entry }))
240 })
241 .collect()
242 };
243 let mut dirs = Vec::new();
244 if info.runpath.is_empty() {
245 for list in &info.rpath {
246 dirs.extend(expand(list)?);
247 }
248 }
249 if let Some(list) = env::var_os("LD_LIBRARY_PATH").filter(|v| !v.is_empty()) {
250 dirs.extend(expand(&list.to_string_lossy())?);
251 }
252 for list in &info.runpath {
253 dirs.extend(expand(list)?);
254 }
255 Ok(dirs)
256}
257
258fn host_definitions(dirs: &[PathBuf], host: &Path, library: &str) -> Result<Vec<String>, String> {
261 if library.contains('/') {
262 return Err(format!("unexpected library name {library:?}"));
263 }
264 let path = dirs
265 .iter()
266 .map(|dir| dir.join(library))
267 .find(|path| path.is_file())
268 .unwrap_or_else(|| host.join(library));
269 let bytes = fs::read(&path).map_err(|e| format!("cannot read {}: {e}", path.display()))?;
270 Ok(crate::elf::inspect(&bytes)
271 .map_err(|e| format!("{}: {e}", path.display()))?
272 .definitions)
273}
274
275fn highest(entries: &[String]) -> Option<Version> {
277 entries
278 .iter()
279 .filter_map(|e| e.rsplit_once(" GLIBC_")?.1.parse().ok())
280 .max()
281}
282
283fn check_host(program: &str, info: &ElfInfo) -> Option<i32> {
287 let located =
288 host_libc_dir().and_then(|host| Ok((search_dirs(Path::new(program), info)?, host)));
289 let result = match &located {
290 Ok((dirs, host)) => host_glibc(info, |library| host_definitions(dirs, host, library)),
291 Err(e) => HostGlibc::Unknown(e.clone()),
292 };
293 match result {
294 HostGlibc::Satisfied => None,
295 HostGlibc::Unknown(e) => {
296 eprintln!("libc-runner: warning: host glibc not checked ({e}); running {program}");
297 None
298 }
299 HostGlibc::Missing(missing) => {
300 let need = highest(&missing).map_or_else(String::new, |v| format!(" {v} or newer"));
301 eprintln!(
302 "libc-runner: not running {program}: the host's libraries lack {}, so the \
303 loader would refuse it. Run it in a container with glibc{need} (--runner \
304 container --image IMAGE, or --distro), or build it with an older provider.",
305 missing.join(", ")
306 );
307 Some(2)
308 }
309 }
310}
311
312pub fn run_program(mut args: impl Iterator<Item = OsString>) -> Result<i32, String> {
324 let program = args
325 .next()
326 .ok_or("usage: libc-runner PROGRAM [ARGS...] (Cargo's target runner)")?;
327 let program = program
328 .into_string()
329 .map_err(|_| "program path must be UTF-8".to_string())?;
330 let args = args
331 .map(|a| {
332 a.into_string()
333 .map_err(|_| "arguments must be UTF-8".to_string())
334 })
335 .collect::<Result<Vec<_>, _>>()?;
336 let (sim, ceiling) = Simulation::from_environment()?;
337
338 let info = report::read_elf(Path::new(&program))?;
339 let assessment = report::assess(&info, Some(ceiling));
340 if !assessment.errors.is_empty() {
341 for error in &assessment.errors {
342 eprintln!("libc-runner: {program}: {error}");
343 }
344 eprintln!("libc-runner: not running {program}: it needs a newer glibc than {ceiling}");
345 return Ok(2);
346 }
347
348 let (plan, target) = sim.plan()?;
349 if !matches!(plan, Plan::Container(..)) {
351 if let Some(code) = check_host(&program, &info) {
352 return Ok(code);
353 }
354 }
355 if let Some(t) = &target {
356 if t.kernel < table_floor() {
357 eprintln!(
358 "libc-runner: warning: kernel data starts at {}; {} is treated as {}",
359 table_floor(),
360 t.kernel,
361 table_floor()
362 );
363 }
364 }
365 match (&plan, &target) {
366 (Plan::Local(_), Some(t)) => local::run(t, &program, &args),
367 (Plan::Container(_, image), Some(t)) => {
368 container::run(&Engine::from_env(), t, image, &program, &args)
369 }
370 _ => {
371 let status = Command::new(&program)
372 .args(&args)
373 .status()
374 .map_err(|e| format!("cannot start {program}: {e}"))?;
375 Ok(status.code().unwrap_or(128))
376 }
377 }
378}
379
380pub(crate) fn runtime_from_engine_args(args: &[String]) -> Option<String> {
383 let mut it = args.iter();
384 while let Some(arg) = it.next() {
385 if arg == "--runtime" {
386 return it.next().cloned();
387 }
388 if let Some(value) = arg.strip_prefix("--runtime=") {
389 return Some(value.to_string());
390 }
391 }
392 None
393}
394
395fn first_line(program: &str, args: &[&str]) -> Result<String, String> {
396 let output = Command::new(program)
397 .args(args)
398 .output()
399 .map_err(|e| format!("cannot run {program}: {e}"))?;
400 if !output.status.success() {
401 return Err(format!("{program} {} failed", args.join(" ")));
402 }
403 Ok(String::from_utf8_lossy(&output.stdout)
404 .lines()
405 .next()
406 .unwrap_or("")
407 .trim()
408 .to_string())
409}
410
411fn check_local(target: &Target) -> Result<String, String> {
414 if !cfg!(all(target_os = "linux", target_arch = "x86_64")) {
415 return Err("needs an x86_64 Linux host".into());
416 }
417 match local::run(target, "/bin/sh", &["-c".into(), "exit 0".into()])? {
418 0 => Ok(format!(
419 "seccomp filter for {} installed without root; /bin/sh ran under it",
420 target.label
421 )),
422 code => Err(format!("/bin/sh exited with {code} under the filter")),
423 }
424}
425
426const UNTESTED: &str =
427 "engine found; no container started (select an image with --image or --distro)";
428
429fn check_container(target: &Target, image: Option<&str>) -> Result<String, String> {
432 let engine = Engine::from_env();
433 let version = first_line(&engine.program, &["--version"])?;
434 let runtime = match runtime_from_engine_args(&engine.engine_args) {
435 Some(r) => format!("runtime {r} ({})", first_line(&r, &["--version"])?),
436 None => "the engine's default runtime".into(),
437 };
438 println!(" container: {version}; {runtime}");
439 let Some(image) = image else {
440 return Ok(UNTESTED.into());
441 };
442 println!(
443 " container: starting `true` in {image} under the {} profile (may pull the image)",
444 target.label
445 );
446 match container::run(&engine, target, image, "true", &[])? {
447 0 => Ok(format!(
448 "`true` ran in {image} under the {} profile",
449 target.label
450 )),
451 code => Err(format!(
452 "`true` exited with {code} in {image}. If the runtime failed to start, its \
453 libseccomp may not know every syscall in the profile, or the runtime itself \
454 (e.g. runc's statx) is caught by the older kernel's profile; crun 1.28 works: \
455 KERNEL_ABI_ENGINE_ARGS=\"--runtime /path/to/crun\""
456 )),
457 }
458}
459
460pub(crate) fn doctor(simulation: &Simulation) -> Result<bool, String> {
463 let (plan, target) = simulation.plan()?;
464 println!("Kernel simulation for test/run: {}", describe(&plan));
465 let Some(target) = target else {
466 println!("Runner checks skipped: no kernel floor (--distro or --kernel).");
467 return Ok(true);
468 };
469 let image = match &plan {
470 Plan::Container(_, image) => Some(image.clone()),
471 _ => simulation.image.clone().or_else(|| {
472 simulation
473 .distro
474 .as_deref()
475 .and_then(kernel_abi_tools::distro)
476 .map(|d| d.image.to_string())
477 }),
478 };
479 let report = |name: &str, result: &Result<String, String>| match result {
480 Ok(text) if text == UNTESTED => println!(" {name}: not tested: {text}"),
481 Ok(text) => println!(" {name}: ok: {text}"),
482 Err(text) => println!(" {name}: unavailable: {text}"),
483 };
484 println!("Runner checks:");
485 let local = check_local(&target);
486 report("local", &local);
487 let container = check_container(&target, image.as_deref());
488 report("container", &container);
489 let selected = match plan {
490 Plan::Local(_) => local.is_ok(),
491 Plan::Container(..) => container.is_ok(),
492 Plan::Direct => true,
493 };
494 println!(
495 "Selected runner: {}",
496 if selected { "usable" } else { "NOT usable" }
497 );
498 Ok(selected)
499}
500
501#[cfg(test)]
502mod tests {
503 use super::*;
504
505 fn sim(
506 distro: Option<&str>,
507 kernel: Option<&str>,
508 runner: Option<Runner>,
509 image: Option<&str>,
510 ) -> Simulation {
511 Simulation {
512 distro: distro.map(Into::into),
513 kernel: kernel.map(Into::into),
514 runner,
515 image: image.map(Into::into),
516 }
517 }
518
519 #[test]
520 fn plans_follow_the_runner_rules() {
521 let (plan, _) = sim(Some("sles-12-sp5"), None, None, None).plan().unwrap();
522 assert_eq!(
523 plan,
524 Plan::Container(
525 "SUSE Linux Enterprise Server 12 SP5 (Linux 4.12)".into(),
526 "registry.suse.com/suse/sles12sp5:latest".into()
527 )
528 );
529 let (plan, target) = sim(Some("sles-12-sp5"), None, Some(Runner::Local), None)
530 .plan()
531 .unwrap();
532 assert!(
533 matches!(plan, Plan::Local(_)),
534 "local ignores the preset image"
535 );
536 assert_eq!(target.unwrap().kernel.to_string(), "4.12");
537 let (plan, _) = sim(None, Some("3.10"), None, None).plan().unwrap();
538 assert_eq!(plan, Plan::Local("Linux 3.10".into()));
539 let (plan, _) = sim(None, Some("3.10"), None, Some("centos:7"))
540 .plan()
541 .unwrap();
542 assert_eq!(
543 plan,
544 Plan::Container("Linux 3.10".into(), "centos:7".into())
545 );
546 assert_eq!(sim(None, None, None, None).plan().unwrap().0, Plan::Direct);
547 for bad in [
548 sim(None, None, Some(Runner::Local), None),
549 sim(None, None, None, Some("centos:7")),
550 sim(None, Some("3.10"), Some(Runner::Container), None),
551 sim(None, Some("3.10"), Some(Runner::Local), Some("centos:7")),
552 sim(Some("rhel-7"), Some("3.10"), None, None),
553 sim(Some("no-such-distro"), None, None, None),
554 ] {
555 assert!(bad.plan().is_err(), "{bad:?}");
556 }
557 }
558
559 fn needs(entries: &[(&str, &str, bool)]) -> ElfInfo {
560 ElfInfo {
561 machine: 62,
562 interpreter: None,
563 needed: Vec::new(),
564 rpath: Vec::new(),
565 runpath: Vec::new(),
566 versions: entries
567 .iter()
568 .map(|(library, name, weak)| crate::elf::VersionNeed {
569 library: (*library).into(),
570 name: (*name).into(),
571 weak: *weak,
572 })
573 .collect(),
574 definitions: Vec::new(),
575 }
576 }
577
578 #[test]
579 fn host_glibc_compares_required_with_defined_versions() {
580 let host = |library: &str| -> Result<Vec<String>, String> {
581 match library {
582 "libc.so.6" => Ok(vec!["GLIBC_2.2.5".into(), "GLIBC_2.17".into()]),
583 "libm.so.6" => Ok(vec!["GLIBC_2.2.5".into()]),
584 _ => Err(format!("cannot read {library}")),
585 }
586 };
587 let fits = needs(&[
588 ("libc.so.6", "GLIBC_2.17", false),
589 ("libm.so.6", "GLIBC_2.2.5", false),
590 ]);
591 assert_eq!(host_glibc(&fits, host), HostGlibc::Satisfied);
592 let too_new = needs(&[
593 ("libc.so.6", "GLIBC_2.34", false),
594 ("libm.so.6", "GLIBC_2.29", false),
595 ("libc.so.6", "GLIBC_2.38", true),
596 ("libgcc_s.so.1", "GCC_3.0", false),
597 ]);
598 let HostGlibc::Missing(missing) = host_glibc(&too_new, host) else {
599 panic!("expected missing versions")
600 };
601 assert_eq!(missing, ["libc.so.6 GLIBC_2.34", "libm.so.6 GLIBC_2.29"]);
602 assert_eq!(highest(&missing), Some("2.34".parse().unwrap()));
603 let unreadable = needs(&[("libdl.so.2", "GLIBC_2.2.5", false)]);
604 assert!(matches!(
605 host_glibc(&unreadable, host),
606 HostGlibc::Unknown(_)
607 ));
608 }
609
610 #[test]
611 fn search_dirs_follow_the_loader_order_and_expand_origin() {
612 let mut info = needs(&[]);
613 info.rpath.push("/r1:$ORIGIN/r2".into());
614 let dirs = search_dirs(Path::new("/nonexistent/bin/app"), &info).unwrap();
615 assert!(dirs.starts_with(&[PathBuf::from("/r1")]));
616 assert!(dirs[1].ends_with("r2"), "{dirs:?}");
617 info.runpath.push("/run".into());
619 let dirs = search_dirs(Path::new("/nonexistent/bin/app"), &info).unwrap();
620 assert!(!dirs.contains(&PathBuf::from("/r1")));
621 assert_eq!(dirs.last(), Some(&PathBuf::from("/run")));
622 info.runpath.push("/x/$LIB".into());
623 assert!(search_dirs(Path::new("/nonexistent/bin/app"), &info).is_err());
624 }
625
626 #[test]
627 fn finds_the_runtime_in_engine_options() {
628 let words = |s: &str| s.split_whitespace().map(String::from).collect::<Vec<_>>();
629 assert_eq!(
630 runtime_from_engine_args(&words("--log-level warn --runtime /opt/crun")),
631 Some("/opt/crun".into())
632 );
633 assert_eq!(
634 runtime_from_engine_args(&words("--runtime=crun")),
635 Some("crun".into())
636 );
637 assert_eq!(runtime_from_engine_args(&words("--log-level warn")), None);
638 }
639
640 #[test]
641 fn selection_round_trips_through_the_environment() {
642 let mut cmd = Command::new("x");
643 let s = sim(Some("rhel-7"), None, Some(Runner::Local), None);
644 s.set_environment(&mut cmd, "2.17".parse().unwrap());
645 let envs: Vec<_> = cmd
646 .get_envs()
647 .map(|(k, v)| {
648 (
649 k.to_string_lossy().into_owned(),
650 v.map(|v| v.to_string_lossy().into_owned()),
651 )
652 })
653 .collect();
654 assert!(envs.contains(&(GLIBC_ENV.into(), Some("2.17".into()))));
655 assert!(envs.contains(&(DISTRO_ENV.into(), Some("rhel-7".into()))));
656 assert!(envs.contains(&(RUNNER_ENV.into(), Some("local".into()))));
657 assert!(
658 envs.contains(&(KERNEL_ENV.into(), None)),
659 "stale values are removed"
660 );
661 }
662}