use std::path::{Path, PathBuf};
#[cfg(any(target_os = "linux", target_os = "macos"))]
use std::process::Command;
use object::{Object as _, ObjectSymbol as _};
use super::wire::{
CaptureFormat, DiscoveryConfig, FrameStatus, ModuleRef, ModuleSymbolStatus, RawCapture,
RawFrame, RawThread,
};
use super::{SymbolizerWorker, WorkerError};
const MAX_IMAGE_BYTES: u64 = 1024 * 1024 * 1024;
#[cfg(any(target_os = "linux", target_os = "macos"))]
const MAX_TOOL_STDERR_BYTES: usize = 4096;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum SplitMechanism {
GnuDebugLink,
DsymBundle,
LinkerPdb,
}
impl SplitMechanism {
pub const fn covers_linked_image(self) -> bool {
match self {
Self::GnuDebugLink | Self::DsymBundle | Self::LinkerPdb => true,
}
}
const fn label(self) -> &'static str {
match self {
Self::GnuDebugLink => "gnu-debuglink",
Self::DsymBundle => "dsym-bundle",
Self::LinkerPdb => "linker-pdb",
}
}
}
impl std::fmt::Display for SplitMechanism {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(self.label())
}
}
#[derive(Clone, Debug)]
pub struct DebugSplitRequest {
binary: PathBuf,
symbol_file: Option<PathBuf>,
}
impl DebugSplitRequest {
pub fn new(binary: impl Into<PathBuf>) -> Self {
Self {
binary: binary.into(),
symbol_file: None,
}
}
pub fn symbol_file(mut self, path: impl Into<PathBuf>) -> Self {
self.symbol_file = Some(path.into());
self
}
pub fn binary(&self) -> &Path {
&self.binary
}
}
#[derive(Clone, Debug)]
pub struct DebugSplit {
binary: PathBuf,
symbol_file: PathBuf,
mechanism: SplitMechanism,
identity: String,
}
impl DebugSplit {
pub fn binary(&self) -> &Path {
&self.binary
}
pub fn symbol_file(&self) -> &Path {
&self.symbol_file
}
pub fn mechanism(&self) -> SplitMechanism {
self.mechanism
}
pub fn build_identity(&self) -> &str {
&self.identity
}
pub async fn verify_resolves(
&self,
worker: &SymbolizerWorker,
function: &str,
) -> Result<VerifiedResolution, DebugSplitError> {
let module_offset = self.function_offset(function)?;
let image = self.resolution_image()?;
let capture = RawCapture {
format: CaptureFormat::CooperativeFrames,
discovery: DiscoveryConfig::default(),
modules: vec![ModuleRef {
name: file_name(&self.binary),
debug_id: Some(self.identity.clone()),
path_hint: Some(image.to_string_lossy().into_owned()),
..ModuleRef::default()
}],
threads: vec![RawThread {
frames: vec![RawFrame {
module_index: 0,
relative_address: module_offset,
}],
..RawThread::default()
}],
};
let report = worker.symbolize(&capture).await?;
let module_status = report
.modules
.first()
.map_or(ModuleSymbolStatus::NotFound, |module| module.status);
let symbol_file = report
.modules
.first()
.and_then(|module| module.symbol_file.clone());
let frame = report
.threads
.first()
.and_then(|thread| thread.frames.first());
let resolved = frame.and_then(|frame| frame.function.clone());
let frame_status = frame.map_or(FrameStatus::RawOnly, |frame| frame.status);
if module_status != ModuleSymbolStatus::Resolved || resolved.as_deref() != Some(function) {
return Err(DebugSplitError::Unresolved {
function: function.to_owned(),
module_offset,
module_status,
frame_status,
resolved,
});
}
let answered = symbol_file.as_deref();
if self.mechanism != SplitMechanism::LinkerPdb
&& answered != Some(image.to_string_lossy().as_ref())
{
return Err(incomplete(
self.mechanism,
&format!(
"the symbolizer answered from {} rather than the produced symbol file",
answered.unwrap_or("<nothing>")
),
));
}
Ok(VerifiedResolution {
function: function.to_owned(),
module_offset,
symbol_file: symbol_file.map_or_else(|| image.clone(), PathBuf::from),
source_line: frame.and_then(|frame| frame.file.clone().zip(frame.line)),
})
}
fn function_offset(&self, function: &str) -> Result<u64, DebugSplitError> {
let bytes = read_bounded(&self.binary)?;
let file = parse_object(&self.binary, &bytes)?;
let base = file.relative_address_base();
let address = file
.symbols()
.chain(file.dynamic_symbols())
.find(|symbol| !symbol.is_undefined() && symbol.name() == Ok(function))
.map(|symbol| symbol.address())
.or_else(|| {
file.exports().ok().and_then(|exports| {
exports
.iter()
.find(|export| export.name() == function.as_bytes())
.map(object::read::Export::address)
})
});
match address {
Some(address) if address >= base => Ok(address - base),
_ => Err(DebugSplitError::FunctionAddressUnknown {
function: function.to_owned(),
binary: self.binary.clone(),
}),
}
}
fn resolution_image(&self) -> Result<PathBuf, DebugSplitError> {
match self.mechanism {
SplitMechanism::GnuDebugLink => Ok(self.symbol_file.clone()),
SplitMechanism::DsymBundle => Ok(dsym_dwarf_binary(&self.symbol_file, &self.binary)),
SplitMechanism::LinkerPdb => Ok(self.binary.clone()),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VerifiedResolution {
function: String,
module_offset: u64,
symbol_file: PathBuf,
source_line: Option<(String, u32)>,
}
impl VerifiedResolution {
pub fn function(&self) -> &str {
&self.function
}
pub fn module_offset(&self) -> u64 {
self.module_offset
}
pub fn symbol_file(&self) -> &Path {
&self.symbol_file
}
pub fn source_line(&self) -> Option<(&str, u32)> {
self.source_line
.as_ref()
.map(|(file, line)| (file.as_str(), *line))
}
}
#[derive(Debug, thiserror::Error)]
pub enum DebugSplitError {
#[error("{mechanism} needs {missing} on PATH", missing = missing.join(" or "))]
MechanismUnavailable {
mechanism: SplitMechanism,
missing: Vec<String>,
},
#[error("no debug-symbol split mechanism for this target")]
UnsupportedTarget,
#[error(
"{binary} has no build identity; a Linux release lane needs \
-C link-arg=-Wl,--build-id, which Rust does not pass by default",
binary = binary.display()
)]
BuildIdentityMissing {
binary: PathBuf,
},
#[error("{binary} carries no debug info to split out", binary = binary.display())]
DebugInfoMissing {
binary: PathBuf,
},
#[error("{path} does not exist", path = path.display())]
SymbolFileMissing {
path: PathBuf,
},
#[error("{program} failed with status {status:?}: {stderr}")]
Tool {
program: String,
status: Option<i32>,
stderr: String,
},
#[error("{path}: {source}", path = path.display())]
Io {
path: PathBuf,
source: std::io::Error,
},
#[error("{path} is {bytes} bytes, over the {limit}-byte inspection limit", path = path.display())]
ImageTooLarge {
path: PathBuf,
bytes: u64,
limit: u64,
},
#[error("{path} is not a readable object file: {reason}", path = path.display())]
UnreadableImage {
path: PathBuf,
reason: String,
},
#[error("{mechanism} produced an unusable pair: {reason}")]
IncompleteSplit {
mechanism: SplitMechanism,
reason: String,
},
#[error("{binary} has no symbol named {function}", binary = binary.display())]
FunctionAddressUnknown {
function: String,
binary: PathBuf,
},
#[error(
"symbolizing {function} at +{module_offset:#x} returned {resolved:?} \
(module {module_status:?}, frame {frame_status:?})"
)]
Unresolved {
function: String,
module_offset: u64,
module_status: ModuleSymbolStatus,
frame_status: FrameStatus,
resolved: Option<String>,
},
#[error("symbolizer worker: {0}")]
Worker(#[from] WorkerError),
}
pub fn split_debug_symbols(request: &DebugSplitRequest) -> Result<DebugSplit, DebugSplitError> {
#[cfg(target_os = "linux")]
{
split_gnu_debuglink(request)
}
#[cfg(target_os = "macos")]
{
split_dsym_bundle(request)
}
#[cfg(windows)]
{
locate_linker_pdb(request)
}
#[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
{
let _ = request;
Err(DebugSplitError::UnsupportedTarget)
}
}
#[cfg(target_os = "linux")]
fn split_gnu_debuglink(request: &DebugSplitRequest) -> Result<DebugSplit, DebugSplitError> {
let mechanism = SplitMechanism::GnuDebugLink;
let binary = absolute(&request.binary)?;
let identity = {
let bytes = read_bounded(&binary)?;
let file = parse_object(&binary, &bytes)?;
if !has_debug_info(&file) {
return Err(DebugSplitError::DebugInfoMissing {
binary: binary.clone(),
});
}
identity_of(&file).ok_or_else(|| DebugSplitError::BuildIdentityMissing {
binary: binary.clone(),
})?
};
let symbol_file = match &request.symbol_file {
Some(path) => absolute(path)?,
None => appended_extension(&binary, ".debug"),
};
let objcopy = probe_tool(mechanism, &["objcopy", "llvm-objcopy"])?;
let strip = probe_tool(mechanism, &["strip", "llvm-strip"])?;
let directory = parent_directory(&binary);
run_tool(
&objcopy,
&directory,
&[
"--only-keep-debug".as_ref(),
binary.as_os_str(),
symbol_file.as_os_str(),
],
)?;
let stripped = appended_extension(&binary, ".kernal-split-stripped");
let linked = appended_extension(&binary, ".kernal-split-linked");
let result = (|| {
run_tool(
&strip,
&directory,
&[
"--strip-debug".as_ref(),
"-o".as_ref(),
stripped.as_os_str(),
binary.as_os_str(),
],
)?;
run_tool(
&objcopy,
&directory,
&[
debuglink_argument(&symbol_file, &directory).as_os_str(),
stripped.as_os_str(),
linked.as_os_str(),
],
)?;
copy_permissions(&binary, &linked)?;
rename(&linked, &binary)
})();
let _ = std::fs::remove_file(&stripped);
if result.is_err() {
let _ = std::fs::remove_file(&linked);
}
result?;
let split = DebugSplit {
binary,
symbol_file,
mechanism,
identity,
};
inspect_gnu_debuglink(&split)?;
Ok(split)
}
#[cfg(target_os = "linux")]
fn inspect_gnu_debuglink(split: &DebugSplit) -> Result<(), DebugSplitError> {
let binary_bytes = read_bounded(&split.binary)?;
let binary = parse_object(&split.binary, &binary_bytes)?;
let symbol_bytes = read_bounded(&split.symbol_file)?;
let symbols = parse_object(&split.symbol_file, &symbol_bytes)?;
if has_debug_info(&binary) {
return Err(incomplete(
split.mechanism,
"the stripped binary still carries DWARF; the debug info was duplicated, not moved",
));
}
if !has_debug_info(&symbols) {
return Err(incomplete(
split.mechanism,
"the symbol file carries no DWARF, so it can resolve nothing",
));
}
if identity_of(&symbols).as_deref() != Some(split.identity.as_str()) {
return Err(incomplete(
split.mechanism,
"the symbol file's build identity does not match the binary",
));
}
let Ok(Some((name, crc))) = binary.gnu_debuglink() else {
return Err(incomplete(
split.mechanism,
"the stripped binary has no .gnu_debuglink section",
));
};
let expected_name = file_name(&split.symbol_file);
if String::from_utf8_lossy(name) != expected_name {
return Err(incomplete(
split.mechanism,
"the .gnu_debuglink names a different file than the symbol file produced",
));
}
if crc != crc32(&symbol_bytes) {
return Err(incomplete(
split.mechanism,
"the .gnu_debuglink CRC-32 does not match the symbol file's bytes",
));
}
Ok(())
}
#[cfg(target_os = "macos")]
fn split_dsym_bundle(request: &DebugSplitRequest) -> Result<DebugSplit, DebugSplitError> {
let mechanism = SplitMechanism::DsymBundle;
let binary = absolute(&request.binary)?;
let identity = {
let bytes = read_bounded(&binary)?;
let file = parse_object(&binary, &bytes)?;
identity_of(&file).ok_or_else(|| DebugSplitError::BuildIdentityMissing {
binary: binary.clone(),
})?
};
let bundle = match &request.symbol_file {
Some(path) => absolute(path)?,
None => appended_extension(&binary, ".dSYM"),
};
let dsymutil = probe_tool(mechanism, &["dsymutil"])?;
let strip = probe_tool(mechanism, &["strip"])?;
let directory = parent_directory(&binary);
run_tool(
&dsymutil,
&directory,
&[binary.as_os_str(), "-o".as_ref(), bundle.as_os_str()],
)?;
run_tool(&strip, &directory, &["-x".as_ref(), binary.as_os_str()])?;
let split = DebugSplit {
binary,
symbol_file: bundle,
mechanism,
identity,
};
inspect_dsym_bundle(&split)?;
Ok(split)
}
#[cfg(target_os = "macos")]
fn inspect_dsym_bundle(split: &DebugSplit) -> Result<(), DebugSplitError> {
let dwarf = dsym_dwarf_binary(&split.symbol_file, &split.binary);
if !dwarf.is_file() {
return Err(incomplete(
split.mechanism,
"the .dSYM bundle has no DWARF binary inside it",
));
}
let bytes = read_bounded(&dwarf)?;
let symbols = parse_object(&dwarf, &bytes)?;
if !has_debug_info(&symbols) {
return Err(incomplete(
split.mechanism,
"the .dSYM bundle carries no DWARF, so it can resolve nothing",
));
}
if identity_of(&symbols).as_deref() != Some(split.identity.as_str()) {
return Err(incomplete(
split.mechanism,
"the .dSYM bundle's UUID does not match the binary",
));
}
Ok(())
}
#[cfg(windows)]
fn locate_linker_pdb(request: &DebugSplitRequest) -> Result<DebugSplit, DebugSplitError> {
let mechanism = SplitMechanism::LinkerPdb;
let binary = absolute(&request.binary)?;
let bytes = read_bounded(&binary)?;
let file = parse_object(&binary, &bytes)?;
let identity = identity_of(&file).ok_or_else(|| DebugSplitError::BuildIdentityMissing {
binary: binary.clone(),
})?;
let recorded = file
.pdb_info()
.ok()
.flatten()
.map(|info| PathBuf::from(String::from_utf8_lossy(info.path()).into_owned()));
let symbol_file = match &request.symbol_file {
Some(path) => absolute(path)?,
None => recorded
.filter(|path| path.is_file())
.unwrap_or_else(|| binary.with_extension("pdb")),
};
if !symbol_file.is_file() {
return Err(DebugSplitError::SymbolFileMissing { path: symbol_file });
}
Ok(DebugSplit {
binary,
symbol_file,
mechanism,
identity,
})
}
fn dsym_dwarf_binary(bundle: &Path, binary: &Path) -> PathBuf {
bundle
.join("Contents")
.join("Resources")
.join("DWARF")
.join(file_name(binary))
}
fn identity_of(file: &object::File<'_>) -> Option<String> {
if let Ok(Some(build_id)) = file.build_id() {
return Some(format!("elf:{}", hex(build_id)));
}
if let Ok(Some(uuid)) = file.mach_uuid() {
return Some(format!("macho:{}", hex(&uuid)));
}
let info = file.pdb_info().ok()??;
let mut guid = info.guid();
guid[0..4].reverse();
guid[4..6].reverse();
guid[6..8].reverse();
Some(format!("pdb:{}-{}", hex(&guid), info.age()))
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn has_debug_info(file: &object::File<'_>) -> bool {
file.section_by_name(".debug_info").is_some() || file.section_by_name("__debug_info").is_some()
}
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|byte| format!("{byte:02x}")).collect()
}
fn file_name(path: &Path) -> String {
path.file_name()
.unwrap_or(path.as_os_str())
.to_string_lossy()
.into_owned()
}
fn incomplete(mechanism: SplitMechanism, reason: &str) -> DebugSplitError {
DebugSplitError::IncompleteSplit {
mechanism,
reason: reason.to_owned(),
}
}
fn absolute(path: &Path) -> Result<PathBuf, DebugSplitError> {
std::path::absolute(path).map_err(|source| DebugSplitError::Io {
path: path.to_path_buf(),
source,
})
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn parent_directory(binary: &Path) -> PathBuf {
binary
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
.map_or_else(|| PathBuf::from("."), Path::to_path_buf)
}
#[cfg(any(target_os = "linux", target_os = "macos", test))]
fn appended_extension(path: &Path, suffix: &str) -> PathBuf {
let mut appended = path.to_path_buf();
appended.as_mut_os_string().push(suffix);
appended
}
#[cfg(target_os = "linux")]
fn debuglink_argument(symbol_file: &Path, directory: &Path) -> std::ffi::OsString {
let mut argument = std::ffi::OsString::from("--add-gnu-debuglink=");
if symbol_file.parent() == Some(directory) {
argument.push(symbol_file.file_name().unwrap_or(symbol_file.as_os_str()));
} else {
argument.push(symbol_file.as_os_str());
}
argument
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn probe_tool(mechanism: SplitMechanism, names: &[&str]) -> Result<PathBuf, DebugSplitError> {
for name in names {
let ran = Command::new(name)
.arg("--version")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok();
if ran {
return Ok(PathBuf::from(name));
}
}
Err(DebugSplitError::MechanismUnavailable {
mechanism,
missing: names.iter().map(|name| (*name).to_owned()).collect(),
})
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn run_tool(
program: &Path,
directory: &Path,
arguments: &[&std::ffi::OsStr],
) -> Result<(), DebugSplitError> {
let output = Command::new(program)
.args(arguments)
.current_dir(directory)
.stdin(std::process::Stdio::null())
.output()
.map_err(|source| DebugSplitError::Io {
path: program.to_path_buf(),
source,
})?;
if output.status.success() {
return Ok(());
}
let mut stderr = String::from_utf8_lossy(&output.stderr).trim().to_owned();
stderr.truncate(
(0..=MAX_TOOL_STDERR_BYTES.min(stderr.len()))
.rev()
.find(|index| stderr.is_char_boundary(*index))
.unwrap_or(0),
);
Err(DebugSplitError::Tool {
program: program.to_string_lossy().into_owned(),
status: output.status.code(),
stderr,
})
}
#[cfg(target_os = "linux")]
fn copy_permissions(from: &Path, to: &Path) -> Result<(), DebugSplitError> {
let permissions = std::fs::metadata(from)
.map_err(|source| DebugSplitError::Io {
path: from.to_path_buf(),
source,
})?
.permissions();
std::fs::set_permissions(to, permissions).map_err(|source| DebugSplitError::Io {
path: to.to_path_buf(),
source,
})
}
#[cfg(target_os = "linux")]
fn rename(from: &Path, to: &Path) -> Result<(), DebugSplitError> {
std::fs::rename(from, to).map_err(|source| DebugSplitError::Io {
path: to.to_path_buf(),
source,
})
}
fn read_bounded(path: &Path) -> Result<Vec<u8>, DebugSplitError> {
use std::io::Read as _;
let file = std::fs::File::open(path).map_err(|source| DebugSplitError::Io {
path: path.to_path_buf(),
source,
})?;
let length = file
.metadata()
.map_err(|source| DebugSplitError::Io {
path: path.to_path_buf(),
source,
})?
.len();
if length > MAX_IMAGE_BYTES {
return Err(DebugSplitError::ImageTooLarge {
path: path.to_path_buf(),
bytes: length,
limit: MAX_IMAGE_BYTES,
});
}
let mut bytes = Vec::with_capacity(length as usize);
file.take(MAX_IMAGE_BYTES)
.read_to_end(&mut bytes)
.map_err(|source| DebugSplitError::Io {
path: path.to_path_buf(),
source,
})?;
Ok(bytes)
}
fn parse_object<'data>(
path: &Path,
bytes: &'data [u8],
) -> Result<object::File<'data>, DebugSplitError> {
object::File::parse(bytes).map_err(|error| DebugSplitError::UnreadableImage {
path: path.to_path_buf(),
reason: error.to_string(),
})
}
#[cfg(any(target_os = "linux", test))]
fn crc32(bytes: &[u8]) -> u32 {
const TABLE: [u32; 256] = {
let mut table = [0_u32; 256];
let mut index = 0;
while index < 256 {
let mut value = index as u32;
let mut bit = 0;
while bit < 8 {
value = if value & 1 == 1 {
0xedb8_8320 ^ (value >> 1)
} else {
value >> 1
};
bit += 1;
}
table[index] = value;
index += 1;
}
table
};
let mut crc = 0xffff_ffff_u32;
for byte in bytes {
crc = TABLE[((crc ^ u32::from(*byte)) & 0xff) as usize] ^ (crc >> 8);
}
crc ^ 0xffff_ffff
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_checksum_matches_the_published_crc32_vector() {
assert_eq!(crc32(b"123456789"), 0xcbf4_3926);
assert_eq!(crc32(b""), 0);
}
#[test]
fn the_symbol_file_name_keeps_the_binary_extension() {
assert_eq!(
appended_extension(Path::new("/opt/lib/libfoo.so.1"), ".debug"),
PathBuf::from("/opt/lib/libfoo.so.1.debug")
);
}
#[test]
fn every_reported_mechanism_covers_the_whole_linked_image() {
for mechanism in [
SplitMechanism::GnuDebugLink,
SplitMechanism::DsymBundle,
SplitMechanism::LinkerPdb,
] {
assert!(mechanism.covers_linked_image(), "{mechanism}");
}
}
#[test]
fn the_dsym_symbol_file_is_the_binary_inside_the_bundle() {
assert_eq!(
dsym_dwarf_binary(Path::new("/tmp/app.dSYM"), Path::new("/tmp/app")),
PathBuf::from("/tmp/app.dSYM/Contents/Resources/DWARF/app")
);
}
#[cfg(target_os = "linux")]
#[test]
fn a_sibling_symbol_file_is_linked_by_bare_name() {
assert_eq!(
debuglink_argument(Path::new("/opt/app.debug"), Path::new("/opt")),
std::ffi::OsString::from("--add-gnu-debuglink=app.debug")
);
assert_eq!(
debuglink_argument(Path::new("/elsewhere/app.debug"), Path::new("/opt")),
std::ffi::OsString::from("--add-gnu-debuglink=/elsewhere/app.debug")
);
}
}