use super::policy::{Policy, POLICIES};
use crate::auditwheel::PlatformTag;
use crate::Target;
use anyhow::Result;
use fs_err::File;
use goblin::elf::{sym::STT_FUNC, Elf};
use goblin::strtab::Strtab;
use regex::Regex;
use scroll::Pread;
use std::collections::{HashMap, HashSet};
use std::io;
use std::io::Read;
use std::path::Path;
use thiserror::Error;
#[derive(Error, Debug)]
#[error("Ensuring manylinux/musllinux compliance failed")]
pub enum AuditWheelError {
#[error("Failed to read the wheel")]
IoError(#[source] io::Error),
#[error("Goblin failed to parse the elf file")]
GoblinError(#[source] goblin::error::Error),
#[error(
"Your library links libpython ({0}), which libraries must not do. Have you forgotten to activate the extension-module feature?",
)]
LinksLibPythonError(String),
#[error(
"Your library is not {0} compliant because it links the following forbidden libraries: {1:?}",
)]
PlatformTagValidationError(Policy, Vec<String>),
#[error("Your library is not {0} compliant because it has unsupported architecture: {1}")]
UnsupportedArchitecture(Policy, String),
}
#[derive(Clone, Copy, Debug, Pread)]
#[repr(C)]
struct GnuVersionNeed {
version: u16,
cnt: u16,
file: u32,
aux: u32,
next: u32,
}
#[derive(Clone, Copy, Debug, Pread)]
#[repr(C)]
struct GnuVersionNeedAux {
hash: u32,
flags: u16,
other: u16,
name: u32,
next: u32,
}
#[derive(Clone, Debug)]
struct VersionedLibrary {
name: String,
versions: HashSet<String>,
}
fn find_versioned_libraries(
elf: &Elf,
buffer: &[u8],
) -> Result<Vec<VersionedLibrary>, AuditWheelError> {
let mut symbols = Vec::new();
let section = elf
.section_headers
.iter()
.find(|h| &elf.shdr_strtab[h.sh_name] == ".gnu.version_r");
if let Some(section) = section {
let linked_section = &elf.section_headers[section.sh_link as usize];
linked_section
.check_size(buffer.len())
.map_err(AuditWheelError::GoblinError)?;
let strtab = Strtab::parse(
buffer,
linked_section.sh_offset as usize,
linked_section.sh_size as usize,
0x0,
)
.map_err(AuditWheelError::GoblinError)?;
let num_versions = section.sh_info as usize;
let mut offset = section.sh_offset as usize;
for _ in 0..num_versions {
let ver = buffer
.gread::<GnuVersionNeed>(&mut offset)
.map_err(goblin::error::Error::Scroll)
.map_err(AuditWheelError::GoblinError)?;
let mut versions = HashSet::new();
for _ in 0..ver.cnt {
let ver_aux = buffer
.gread::<GnuVersionNeedAux>(&mut offset)
.map_err(goblin::error::Error::Scroll)
.map_err(AuditWheelError::GoblinError)?;
if let Some(aux_name) = strtab.get(ver_aux.name as usize) {
let aux_name = aux_name.map_err(AuditWheelError::GoblinError)?;
versions.insert(aux_name.to_string());
}
}
if let Some(name) = strtab.get(ver.file as usize) {
let name = name.map_err(AuditWheelError::GoblinError)?;
if name.starts_with("ld-linux") || name == "ld64.so.2" || name == "ld64.so.1" {
continue;
}
symbols.push(VersionedLibrary {
name: name.to_string(),
versions,
});
}
}
}
Ok(symbols)
}
fn find_incompliant_symbols(
elf: &Elf,
symbol_versions: &[String],
) -> Result<Vec<String>, AuditWheelError> {
let mut symbols = Vec::new();
let strtab = &elf.strtab;
for sym in &elf.syms {
if sym.st_type() == STT_FUNC {
let name = strtab
.get(sym.st_name)
.unwrap_or(Ok("BAD NAME"))
.map_err(AuditWheelError::GoblinError)?;
for symbol_version in symbol_versions {
if name.ends_with(&format!("@{}", symbol_version)) {
symbols.push(name.to_string());
}
}
}
}
Ok(symbols)
}
fn policy_is_satisfied(
policy: &Policy,
elf: &Elf,
arch: &str,
deps: &[String],
versioned_libraries: &[VersionedLibrary],
) -> Result<(), AuditWheelError> {
let arch_versions = &policy.symbol_versions.get(arch).ok_or_else(|| {
AuditWheelError::UnsupportedArchitecture(policy.clone(), arch.to_string())
})?;
let mut offenders = HashSet::new();
for dep in deps {
if dep.starts_with("ld-linux") || dep == "ld64.so.2" || dep == "ld64.so.1" {
continue;
}
if !policy.lib_whitelist.contains(dep) {
offenders.insert(dep.clone());
}
}
for library in versioned_libraries {
if !policy.lib_whitelist.contains(&library.name) {
offenders.insert(library.name.clone());
continue;
}
let mut versions: HashMap<String, HashSet<String>> = HashMap::new();
for v in &library.versions {
let mut parts = v.splitn(2, '_');
let name = parts.next().unwrap();
let version = parts.next().unwrap();
versions
.entry(name.to_string())
.or_default()
.insert(version.to_string());
}
for (name, versions_needed) in versions.iter() {
let versions_allowed = &arch_versions[name];
if !versions_needed.is_subset(versions_allowed) {
let offending_versions: Vec<&str> = versions_needed
.difference(versions_allowed)
.map(|v| v.as_ref())
.collect();
let offending_symbol_versions: Vec<String> = offending_versions
.iter()
.map(|v| format!("{}_{}", name, v))
.collect();
let offending_symbols = find_incompliant_symbols(&elf, &offending_symbol_versions)?;
let offender = if offending_symbols.is_empty() {
format!(
"{} offending versions: {}",
library.name,
offending_symbol_versions.join(", ")
)
} else {
format!(
"{} offending symbols: {}",
library.name,
offending_symbols.join(", ")
)
};
offenders.insert(offender);
}
}
}
let is_libpython = Regex::new(r"^libpython3\.\d+\.so\.\d+\.\d+$").unwrap();
let offenders: Vec<String> = offenders.into_iter().collect();
match offenders.as_slice() {
[] => Ok(()),
[lib] if is_libpython.is_match(lib) => {
Err(AuditWheelError::LinksLibPythonError(lib.clone()))
}
offenders => Err(AuditWheelError::PlatformTagValidationError(
policy.clone(),
offenders.to_vec(),
)),
}
}
pub fn auditwheel_rs(
path: &Path,
target: &Target,
platform_tag: Option<PlatformTag>,
) -> Result<Policy, AuditWheelError> {
if !target.is_linux() || platform_tag == Some(PlatformTag::Linux) {
return Ok(Policy::default());
}
if let Some(musl_tag @ PlatformTag::Musllinux { .. }) = platform_tag {
eprintln!("⚠ Warning: no auditwheel support for musllinux yet");
return Ok(Policy {
name: musl_tag.to_string(),
aliases: Vec::new(),
priority: 0,
symbol_versions: Default::default(),
lib_whitelist: Default::default(),
});
}
let arch = target.target_arch().to_string();
let mut file = File::open(path).map_err(AuditWheelError::IoError)?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer)
.map_err(AuditWheelError::IoError)?;
let elf = Elf::parse(&buffer).map_err(AuditWheelError::GoblinError)?;
let deps: Vec<String> = elf.libraries.iter().map(ToString::to_string).collect();
let versioned_libraries = find_versioned_libraries(&elf, &buffer)?;
let mut highest_policy = None;
for policy in POLICIES.iter() {
let result = policy_is_satisfied(&policy, &elf, &arch, &deps, &versioned_libraries);
match result {
Ok(_) => {
highest_policy = Some(policy.clone());
break;
}
Err(AuditWheelError::PlatformTagValidationError(_, _))
| Err(AuditWheelError::UnsupportedArchitecture(..)) => continue,
Err(err) => return Err(err),
}
}
if let Some(platform_tag) = platform_tag {
let policy = Policy::from_name(&platform_tag.to_string()).unwrap();
if let Some(highest_policy) = highest_policy {
if policy.priority < highest_policy.priority {
println!(
"📦 Wheel is eligible for a higher priority tag. \
You requested {} but this wheel is eligible for {}",
policy, highest_policy,
);
}
}
match policy_is_satisfied(&policy, &elf, &arch, &deps, &versioned_libraries) {
Ok(_) => Ok(policy),
Err(err) => Err(err),
}
} else if let Some(policy) = highest_policy {
Ok(policy)
} else {
println!(
"⚠ Warning: No compatible platform tag found, using the linux tag instead. \
You won't be able to upload those wheels to pypi."
);
Ok(Policy::default())
}
}