#[cfg(all(unix, not(target_os = "macos")))]
mod elf;
#[cfg(target_os = "macos")]
mod macho;
use anyhow::Context;
use clap::Args;
use regex::Regex;
use std::collections::HashMap;
use std::ffi::OsString;
use std::fs;
use std::io::{self, BufRead};
use std::os::unix::ffi::OsStringExt;
use std::path::{Path, PathBuf};
use std::process::Command;
#[derive(Args, Debug)]
pub struct CheckShlibs {}
pub struct CheckState {
#[cfg_attr(target_os = "macos", allow(dead_code))]
cross_destdir: Option<PathBuf>,
destdir: PathBuf,
#[cfg_attr(target_os = "macos", allow(dead_code))]
system_paths: Vec<PathBuf>,
wrkdir: PathBuf,
wrkref: Vec<PathBuf>,
pkgdb: HashMap<PathBuf, Option<String>>,
pkg_admin_cmd: PathBuf,
pkg_admin_args: Vec<String>,
depends: Vec<(String, String, String)>,
toxic: Vec<Regex>,
statlibs: HashMap<PathBuf, bool>,
}
fn check_pkg<P1, P2>(
obj: P1,
lib: P2,
state: &mut CheckState,
) -> anyhow::Result<bool>
where
P1: AsRef<Path>,
P2: AsRef<Path>,
{
if state.pkgdb.is_empty() {
let cmd = Command::new(&state.pkg_admin_cmd)
.args(&state.pkg_admin_args)
.arg("dump")
.output()
.with_context(|| {
format!("unable to execute {}", state.pkg_admin_cmd.display())
})?;
if let Some(0) = cmd.status.code() {
for line in cmd.stdout.split(|nl| *nl == b'\n') {
if !line.starts_with(b"file: ") {
continue;
}
let Some(pos) = line.windows(6).position(|s| s == b" pkg: ")
else {
continue;
};
let file =
PathBuf::from(OsString::from_vec((line[6..pos]).to_vec()));
let Some(pkg) =
line.split(|sp| (*sp as char).is_whitespace()).next_back()
else {
continue;
};
let pkg = match String::from_utf8(pkg.to_vec()) {
Ok(p) => p,
Err(_) => continue,
};
state.pkgdb.insert(file, Some(pkg));
}
}
}
let pkgname = if let Some(entry) = state.pkgdb.get(lib.as_ref()) {
match entry {
Some(p) => p.to_string(),
None => return Ok(true),
}
} else {
state.pkgdb.insert(lib.as_ref().to_path_buf(), None);
return Ok(true);
};
let mut found = false;
for dep in &state.depends {
if dep.2 == pkgname {
found = true;
if dep.0 == "full" || dep.0 == "indirect-full" {
return Ok(true);
}
}
}
if found {
println!(
"{}: {}: {} is not a runtime dependency",
obj.as_ref().display(),
lib.as_ref().display(),
pkgname
);
}
Ok(false)
}
fn check_shlib<P1, P2>(obj: P1, lib: P2, state: &CheckState) -> bool
where
P1: AsRef<Path>,
P2: AsRef<Path>,
{
let obj = obj.as_ref();
let lib = lib.as_ref();
let mut rv = true;
if lib.starts_with(&state.wrkdir) {
println!(
"{}: path relative to WRKDIR: {}",
obj.display(),
lib.display()
);
rv = false;
}
for dir in &state.wrkref {
if lib.starts_with(dir) {
println!(
"{}: rpath {} relative to CHECK_WRKREF_EXTRA_DIRS directory {}",
obj.display(),
lib.display(),
dir.display()
);
rv = false;
}
}
let lib_str = lib.to_string_lossy();
for regex in &state.toxic {
if regex.is_match(&lib_str) {
println!(
"{}: resolved path {} matches toxic {}",
obj.display(),
lib.display(),
regex
);
rv = false;
}
}
if !lib.starts_with("/") {
println!("{}: relative library path: {}", obj.display(), lib.display());
rv = false;
}
rv
}
impl CheckShlibs {
pub fn run(&self) -> anyhow::Result<i32> {
let destdir = match std::env::var("DESTDIR") {
Ok(s) => PathBuf::from(s),
Err(_) => {
eprintln!("DESTDIR is mandatory");
std::process::exit(1);
}
};
let cross_destdir = match std::env::var("CROSS_DESTDIR") {
Ok(s) => {
if s.is_empty() {
None
} else {
Some(PathBuf::from(s))
}
}
Err(_) => {
eprintln!("CROSS_DESTDIR is mandatory (can be empty)");
std::process::exit(1);
}
};
let wrkdir = match std::env::var("WRKDIR") {
Ok(s) => PathBuf::from(s),
Err(_) => {
eprintln!("WRKDIR is mandatory");
std::process::exit(1);
}
};
let pkg_admin_cmd: PathBuf;
let mut pkg_admin_args = vec![];
match std::env::var("PKG_ADMIN_CMD") {
Ok(s) => {
let v: Vec<_> = s.split_whitespace().collect();
if let Some((first, args)) = v.split_first() {
pkg_admin_cmd = PathBuf::from(first);
for arg in args {
pkg_admin_args.push(arg.to_string());
}
} else {
eprintln!("Malformed PKG_ADMIN_CMD {s}");
std::process::exit(1);
}
}
Err(_) => {
eprintln!("PKG_ADMIN_CMD is mandatory");
std::process::exit(1);
}
};
let depends = match std::env::var("DEPENDS_FILE") {
Ok(s) => {
let mut deps = vec![];
let f = fs::read_to_string(s)?;
for line in f.lines() {
let fields: Vec<_> = line.split_whitespace().collect();
if fields.len() != 3 {
eprintln!("Bad DEPENDS_FILE input?");
std::process::exit(1);
}
let deptype = fields[0].to_string();
let pkgmatch = fields[1].to_string();
let pkg = fields[2].to_string();
deps.push((deptype, pkgmatch, pkg))
}
deps
}
Err(_) => {
eprintln!("DEPENDS_FILE is mandatory");
std::process::exit(1);
}
};
let mut system_paths: Vec<PathBuf> = vec![];
if let Ok(paths) = std::env::var("PLATFORM_RPATH") {
let cross_prefix = match &cross_destdir {
Some(p) => p.clone(),
None => PathBuf::new(),
};
for p in paths.split(':').collect::<Vec<&str>>() {
let mut path = cross_prefix.clone();
path.push(p);
system_paths.push(path);
}
}
let toxic = match std::env::var("CHECK_SHLIBS_TOXIC") {
Ok(s) => {
let mut v = vec![];
let rgxs: Vec<_> = s.split_whitespace().collect();
for r in rgxs {
let rgx = Regex::new(r).with_context(|| {
format!("invalid CHECK_SHLIBS_TOXIC regex: {r}")
})?;
v.push(rgx);
}
v
}
Err(_) => {
vec![]
}
};
let wrkref = match std::env::var("CHECK_WRKREF_EXTRA_DIRS") {
Ok(s) => {
let mut v = vec![];
let dirs: Vec<_> = s.split_whitespace().collect();
for d in dirs {
v.push(PathBuf::from(d));
}
v
}
Err(_) => {
vec![]
}
};
let mut state = CheckState {
destdir,
cross_destdir,
system_paths,
wrkdir,
wrkref,
pkg_admin_cmd,
pkg_admin_args,
depends,
toxic,
statlibs: HashMap::new(),
pkgdb: HashMap::new(),
};
for line in io::stdin().lock().lines() {
let line = line?;
let path = Path::new(&line);
match fs::read(path) {
Ok(dso) => self.check_dso(path, &dso, &mut state)?,
Err(e) => eprintln!("{}: {e}", path.display()),
}
}
Ok(0)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn re(s: &str) -> Regex {
match Regex::new(s) {
Ok(r) => r,
Err(e) => panic!("invalid test regex {s}: {e}"),
}
}
#[test]
fn test_shlib() {
let state = CheckState {
cross_destdir: None,
destdir: PathBuf::from("/destdir"),
system_paths: vec![],
wrkdir: PathBuf::from("/wrkdir"),
wrkref: vec![PathBuf::from("/wrkref")],
pkg_admin_cmd: PathBuf::from("/notyet"),
pkg_admin_args: vec![],
depends: vec![],
toxic: vec![re("libtoxic.so"), re("^/toxic")],
statlibs: HashMap::new(),
pkgdb: HashMap::new(),
};
let obj = "/opt/pkg/bin/mutt";
assert!(!check_shlib(obj, "libfoo.so", &state));
assert!(!check_shlib(obj, "/libtoxic.so", &state));
assert!(!check_shlib(obj, "/toxic/lib.so", &state));
assert!(!check_shlib(obj, "/wrkdir/libfoo.so", &state));
assert!(!check_shlib(obj, "/wrkref/libfoo.so", &state));
assert!(check_shlib(obj, "/libfoo.so", &state));
assert!(check_shlib(obj, "/libnottoxic.so", &state));
}
}