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 struct Simulation {
40 pub distro: Option<String>,
41 pub kernel: Option<String>,
42 pub runner: Option<Runner>,
43 pub image: Option<String>,
44}
45
46#[derive(Debug, PartialEq, Eq)]
48pub enum Plan {
49 Direct,
51 Local(String),
52 Container(String, String),
53}
54
55impl Simulation {
56 pub 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 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 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 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 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 enum HostGlibc {
168 Satisfied,
169 Missing(Vec<String>),
171 Unknown(String),
173}
174
175pub 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> {
314 let program = args
315 .next()
316 .ok_or("usage: libc-runner PROGRAM [ARGS...] (Cargo's target runner)")?;
317 let program = program
318 .into_string()
319 .map_err(|_| "program path must be UTF-8".to_string())?;
320 let args = args
321 .map(|a| {
322 a.into_string()
323 .map_err(|_| "arguments must be UTF-8".to_string())
324 })
325 .collect::<Result<Vec<_>, _>>()?;
326 let (sim, ceiling) = Simulation::from_environment()?;
327
328 let info = report::read_elf(Path::new(&program))?;
329 let assessment = report::assess(&info, Some(ceiling));
330 if !assessment.errors.is_empty() {
331 for error in &assessment.errors {
332 eprintln!("libc-runner: {program}: {error}");
333 }
334 eprintln!("libc-runner: not running {program}: it needs a newer glibc than {ceiling}");
335 return Ok(2);
336 }
337
338 let (plan, target) = sim.plan()?;
339 if !matches!(plan, Plan::Container(..)) {
341 if let Some(code) = check_host(&program, &info) {
342 return Ok(code);
343 }
344 }
345 if let Some(t) = &target {
346 if t.kernel < table_floor() {
347 eprintln!(
348 "libc-runner: warning: kernel data starts at {}; {} is treated as {}",
349 table_floor(),
350 t.kernel,
351 table_floor()
352 );
353 }
354 }
355 match (&plan, &target) {
356 (Plan::Local(_), Some(t)) => local::run(t, &program, &args),
357 (Plan::Container(_, image), Some(t)) => {
358 container::run(&Engine::from_env(), t, image, &program, &args)
359 }
360 _ => {
361 let status = Command::new(&program)
362 .args(&args)
363 .status()
364 .map_err(|e| format!("cannot start {program}: {e}"))?;
365 Ok(status.code().unwrap_or(128))
366 }
367 }
368}
369
370pub fn runtime_from_engine_args(args: &[String]) -> Option<String> {
373 let mut it = args.iter();
374 while let Some(arg) = it.next() {
375 if arg == "--runtime" {
376 return it.next().cloned();
377 }
378 if let Some(value) = arg.strip_prefix("--runtime=") {
379 return Some(value.to_string());
380 }
381 }
382 None
383}
384
385fn first_line(program: &str, args: &[&str]) -> Result<String, String> {
386 let output = Command::new(program)
387 .args(args)
388 .output()
389 .map_err(|e| format!("cannot run {program}: {e}"))?;
390 if !output.status.success() {
391 return Err(format!("{program} {} failed", args.join(" ")));
392 }
393 Ok(String::from_utf8_lossy(&output.stdout)
394 .lines()
395 .next()
396 .unwrap_or("")
397 .trim()
398 .to_string())
399}
400
401fn check_local(target: &Target) -> Result<String, String> {
404 if !cfg!(all(target_os = "linux", target_arch = "x86_64")) {
405 return Err("needs an x86_64 Linux host".into());
406 }
407 match local::run(target, "/bin/sh", &["-c".into(), "exit 0".into()])? {
408 0 => Ok(format!(
409 "seccomp filter for {} installed without root; /bin/sh ran under it",
410 target.label
411 )),
412 code => Err(format!("/bin/sh exited with {code} under the filter")),
413 }
414}
415
416const UNTESTED: &str =
417 "engine found; no container started (select an image with --image or --distro)";
418
419fn check_container(target: &Target, image: Option<&str>) -> Result<String, String> {
422 let engine = Engine::from_env();
423 let version = first_line(&engine.program, &["--version"])?;
424 let runtime = match runtime_from_engine_args(&engine.engine_args) {
425 Some(r) => format!("runtime {r} ({})", first_line(&r, &["--version"])?),
426 None => "the engine's default runtime".into(),
427 };
428 println!(" container: {version}; {runtime}");
429 let Some(image) = image else {
430 return Ok(UNTESTED.into());
431 };
432 println!(
433 " container: starting `true` in {image} under the {} profile (may pull the image)",
434 target.label
435 );
436 match container::run(&engine, target, image, "true", &[])? {
437 0 => Ok(format!(
438 "`true` ran in {image} under the {} profile",
439 target.label
440 )),
441 code => Err(format!(
442 "`true` exited with {code} in {image}. If the runtime failed to start, its \
443 libseccomp may not know every syscall in the profile, or the runtime itself \
444 (e.g. runc's statx) is caught by the older kernel's profile; crun 1.28 works: \
445 KERNEL_ABI_ENGINE_ARGS=\"--runtime /path/to/crun\""
446 )),
447 }
448}
449
450pub fn doctor(simulation: &Simulation) -> Result<bool, String> {
453 let (plan, target) = simulation.plan()?;
454 println!("Kernel simulation for test/run: {}", describe(&plan));
455 let Some(target) = target else {
456 println!("Runner checks skipped: no kernel floor (--distro or --kernel).");
457 return Ok(true);
458 };
459 let image = match &plan {
460 Plan::Container(_, image) => Some(image.clone()),
461 _ => simulation.image.clone().or_else(|| {
462 simulation
463 .distro
464 .as_deref()
465 .and_then(kernel_abi_tools::distro)
466 .map(|d| d.image.to_string())
467 }),
468 };
469 let report = |name: &str, result: &Result<String, String>| match result {
470 Ok(text) if text == UNTESTED => println!(" {name}: not tested: {text}"),
471 Ok(text) => println!(" {name}: ok: {text}"),
472 Err(text) => println!(" {name}: unavailable: {text}"),
473 };
474 println!("Runner checks:");
475 let local = check_local(&target);
476 report("local", &local);
477 let container = check_container(&target, image.as_deref());
478 report("container", &container);
479 let selected = match plan {
480 Plan::Local(_) => local.is_ok(),
481 Plan::Container(..) => container.is_ok(),
482 Plan::Direct => true,
483 };
484 println!(
485 "Selected runner: {}",
486 if selected { "usable" } else { "NOT usable" }
487 );
488 Ok(selected)
489}
490
491#[cfg(test)]
492mod tests {
493 use super::*;
494
495 fn sim(
496 distro: Option<&str>,
497 kernel: Option<&str>,
498 runner: Option<Runner>,
499 image: Option<&str>,
500 ) -> Simulation {
501 Simulation {
502 distro: distro.map(Into::into),
503 kernel: kernel.map(Into::into),
504 runner,
505 image: image.map(Into::into),
506 }
507 }
508
509 #[test]
510 fn plans_follow_the_runner_rules() {
511 let (plan, _) = sim(Some("sles-12-sp5"), None, None, None).plan().unwrap();
512 assert_eq!(
513 plan,
514 Plan::Container(
515 "SUSE Linux Enterprise Server 12 SP5 (Linux 4.12)".into(),
516 "registry.suse.com/suse/sles12sp5:latest".into()
517 )
518 );
519 let (plan, target) = sim(Some("sles-12-sp5"), None, Some(Runner::Local), None)
520 .plan()
521 .unwrap();
522 assert!(
523 matches!(plan, Plan::Local(_)),
524 "local ignores the preset image"
525 );
526 assert_eq!(target.unwrap().kernel.to_string(), "4.12");
527 let (plan, _) = sim(None, Some("3.10"), None, None).plan().unwrap();
528 assert_eq!(plan, Plan::Local("Linux 3.10".into()));
529 let (plan, _) = sim(None, Some("3.10"), None, Some("centos:7"))
530 .plan()
531 .unwrap();
532 assert_eq!(
533 plan,
534 Plan::Container("Linux 3.10".into(), "centos:7".into())
535 );
536 assert_eq!(sim(None, None, None, None).plan().unwrap().0, Plan::Direct);
537 for bad in [
538 sim(None, None, Some(Runner::Local), None),
539 sim(None, None, None, Some("centos:7")),
540 sim(None, Some("3.10"), Some(Runner::Container), None),
541 sim(None, Some("3.10"), Some(Runner::Local), Some("centos:7")),
542 sim(Some("rhel-7"), Some("3.10"), None, None),
543 sim(Some("no-such-distro"), None, None, None),
544 ] {
545 assert!(bad.plan().is_err(), "{bad:?}");
546 }
547 }
548
549 fn needs(entries: &[(&str, &str, bool)]) -> ElfInfo {
550 ElfInfo {
551 machine: 62,
552 interpreter: None,
553 needed: Vec::new(),
554 rpath: Vec::new(),
555 runpath: Vec::new(),
556 versions: entries
557 .iter()
558 .map(|(library, name, weak)| crate::elf::VersionNeed {
559 library: (*library).into(),
560 name: (*name).into(),
561 weak: *weak,
562 })
563 .collect(),
564 definitions: Vec::new(),
565 }
566 }
567
568 #[test]
569 fn host_glibc_compares_required_with_defined_versions() {
570 let host = |library: &str| -> Result<Vec<String>, String> {
571 match library {
572 "libc.so.6" => Ok(vec!["GLIBC_2.2.5".into(), "GLIBC_2.17".into()]),
573 "libm.so.6" => Ok(vec!["GLIBC_2.2.5".into()]),
574 _ => Err(format!("cannot read {library}")),
575 }
576 };
577 let fits = needs(&[
578 ("libc.so.6", "GLIBC_2.17", false),
579 ("libm.so.6", "GLIBC_2.2.5", false),
580 ]);
581 assert_eq!(host_glibc(&fits, host), HostGlibc::Satisfied);
582 let too_new = needs(&[
583 ("libc.so.6", "GLIBC_2.34", false),
584 ("libm.so.6", "GLIBC_2.29", false),
585 ("libc.so.6", "GLIBC_2.38", true),
586 ("libgcc_s.so.1", "GCC_3.0", false),
587 ]);
588 let HostGlibc::Missing(missing) = host_glibc(&too_new, host) else {
589 panic!("expected missing versions")
590 };
591 assert_eq!(missing, ["libc.so.6 GLIBC_2.34", "libm.so.6 GLIBC_2.29"]);
592 assert_eq!(highest(&missing), Some("2.34".parse().unwrap()));
593 let unreadable = needs(&[("libdl.so.2", "GLIBC_2.2.5", false)]);
594 assert!(matches!(
595 host_glibc(&unreadable, host),
596 HostGlibc::Unknown(_)
597 ));
598 }
599
600 #[test]
601 fn search_dirs_follow_the_loader_order_and_expand_origin() {
602 let mut info = needs(&[]);
603 info.rpath.push("/r1:$ORIGIN/r2".into());
604 let dirs = search_dirs(Path::new("/nonexistent/bin/app"), &info).unwrap();
605 assert!(dirs.starts_with(&[PathBuf::from("/r1")]));
606 assert!(dirs[1].ends_with("r2"), "{dirs:?}");
607 info.runpath.push("/run".into());
609 let dirs = search_dirs(Path::new("/nonexistent/bin/app"), &info).unwrap();
610 assert!(!dirs.contains(&PathBuf::from("/r1")));
611 assert_eq!(dirs.last(), Some(&PathBuf::from("/run")));
612 info.runpath.push("/x/$LIB".into());
613 assert!(search_dirs(Path::new("/nonexistent/bin/app"), &info).is_err());
614 }
615
616 #[test]
617 fn finds_the_runtime_in_engine_options() {
618 let words = |s: &str| s.split_whitespace().map(String::from).collect::<Vec<_>>();
619 assert_eq!(
620 runtime_from_engine_args(&words("--log-level warn --runtime /opt/crun")),
621 Some("/opt/crun".into())
622 );
623 assert_eq!(
624 runtime_from_engine_args(&words("--runtime=crun")),
625 Some("crun".into())
626 );
627 assert_eq!(runtime_from_engine_args(&words("--log-level warn")), None);
628 }
629
630 #[test]
631 fn selection_round_trips_through_the_environment() {
632 let mut cmd = Command::new("x");
633 let s = sim(Some("rhel-7"), None, Some(Runner::Local), None);
634 s.set_environment(&mut cmd, "2.17".parse().unwrap());
635 let envs: Vec<_> = cmd
636 .get_envs()
637 .map(|(k, v)| {
638 (
639 k.to_string_lossy().into_owned(),
640 v.map(|v| v.to_string_lossy().into_owned()),
641 )
642 })
643 .collect();
644 assert!(envs.contains(&(GLIBC_ENV.into(), Some("2.17".into()))));
645 assert!(envs.contains(&(DISTRO_ENV.into(), Some("rhel-7".into()))));
646 assert!(envs.contains(&(RUNNER_ENV.into(), Some("local".into()))));
647 assert!(
648 envs.contains(&(KERNEL_ENV.into(), None)),
649 "stale values are removed"
650 );
651 }
652}