#![cfg(test)]
use super::*;
#[test]
fn round_up_pow2_zero_align_clamps_to_one() {
assert_eq!(round_up_pow2(7, 0), Some(7));
}
#[test]
fn round_up_pow2_handles_overflow() {
assert_eq!(round_up_pow2(u64::MAX, 8), None);
}
#[test]
fn round_up_pow2_basic() {
assert_eq!(round_up_pow2(7, 8), Some(8));
assert_eq!(round_up_pow2(8, 8), Some(8));
assert_eq!(round_up_pow2(9, 8), Some(16));
}
#[test]
#[cfg(target_arch = "x86_64")]
fn variant_ii_basic() {
let addr = compute_tls_address_variant_ii(0x10000, 0x100, 0x10, 8).unwrap();
assert_eq!(addr, 0xff18);
}
#[test]
#[cfg(target_arch = "x86_64")]
fn variant_ii_underflow() {
assert!(compute_tls_address_variant_ii(0x100, 0x200, 0, 0).is_err());
}
#[test]
fn counter_offsets_rejects_reversed_pair() {
assert!(CounterOffsets::new(64, 16).is_err());
assert!(CounterOffsets::new(16, 16).is_err());
assert!(CounterOffsets::new(16, 32).is_ok());
}
#[test]
fn counter_offsets_combined_span() {
let off = CounterOffsets::new(16, 32).unwrap();
assert_eq!(off.combined_read_span(), 24);
}
#[test]
fn parse_maps_elf_path_keeps_executable_only() {
let exe_line = "5583e6f7a000-5583e6f7b000 r-xp 00000000 fe:00 12345 /usr/bin/example";
assert_eq!(
parse_maps_elf_path(exe_line),
Some(PathBuf::from("/usr/bin/example"))
);
}
#[test]
fn parse_maps_elf_path_drops_non_executable() {
let data_line = "5583e6f7a000-5583e6f7b000 r--p 00000000 fe:00 12345 /usr/bin/example";
assert_eq!(parse_maps_elf_path(data_line), None);
}
#[test]
fn parse_maps_elf_path_drops_anonymous() {
let anon = "7f0000000000-7f0000001000 r-xp 00000000 00:00 0";
assert_eq!(parse_maps_elf_path(anon), None);
}
#[test]
fn parse_maps_elf_path_drops_special_brackets() {
let stack = "7fff00000000-7fff00001000 r-xp 00000000 00:00 0 [stack]";
assert_eq!(parse_maps_elf_path(stack), None);
}
#[test]
fn attach_error_tags_are_unique() {
let pairs: Vec<(&'static str, AttachError)> = vec![
("pid-missing", AttachError::PidMissing(anyhow!("x"))),
(
"readlink-failure",
AttachError::ReadlinkFailure(anyhow!("x")),
),
(
"maps-read-failure",
AttachError::MapsReadFailure(anyhow!("x")),
),
(
"jemalloc-not-found",
AttachError::JemallocNotFound(anyhow!("x")),
),
("jemalloc-in-dso", AttachError::JemallocInDso(anyhow!("x"))),
("arch-mismatch", AttachError::ArchMismatch(anyhow!("x"))),
(
"dwarf-parse-failure",
AttachError::DwarfParseFailure(anyhow!("x")),
),
];
let mut seen: std::collections::BTreeSet<&'static str> = std::collections::BTreeSet::new();
for (expected, err) in &pairs {
assert_eq!(*expected, err.tag());
assert!(seen.insert(err.tag()), "duplicate tag {}", err.tag());
}
}
#[test]
fn probe_error_tags_are_unique() {
let pairs: Vec<(&'static str, ProbeError)> = vec![
("ptrace-seize", ProbeError::PtraceSeize(anyhow!("x"))),
(
"ptrace-interrupt",
ProbeError::PtraceInterrupt(anyhow!("x")),
),
("waitpid", ProbeError::Waitpid(anyhow!("x"))),
("get-regset", ProbeError::GetRegset(anyhow!("x"))),
("process-vm-readv", ProbeError::ProcessVmReadv(anyhow!("x"))),
("tls-arithmetic", ProbeError::TlsArithmetic(anyhow!("x"))),
];
let mut seen: std::collections::BTreeSet<&'static str> = std::collections::BTreeSet::new();
for (expected, err) in &pairs {
assert_eq!(*expected, err.tag());
assert!(seen.insert(err.tag()), "duplicate tag {}", err.tag());
}
}
#[test]
fn attach_pid_missing_returns_pid_missing_error() {
match attach_jemalloc(0) {
Err(AttachError::PidMissing(_)) => {}
other => panic!("expected PidMissing for pid=0, got {other:?}"),
}
}
#[test]
fn attach_returns_pid_missing_for_regular_dead_pid() {
match attach_jemalloc(i32::MAX) {
Err(AttachError::PidMissing(_)) => {}
other => panic!("expected PidMissing for pid=i32::MAX, got {other:?}"),
}
}
#[test]
fn attach_at_returns_jemalloc_not_found_on_maps_without_jemalloc() {
let sleep = PathBuf::from("/bin/sleep");
if !sleep.exists() {
eprintln!("skipping — /bin/sleep unavailable");
return;
}
let tmp = tempfile::TempDir::new().expect("tempdir");
let pid: i32 = 4242;
let pid_dir = tmp.path().join(pid.to_string());
std::fs::create_dir_all(&pid_dir).expect("mkdir pid_dir");
std::os::unix::fs::symlink(&sleep, pid_dir.join("exe")).expect("symlink exe");
let maps = format!(
"5583e6f7a000-5583e6f7b000 r-xp 00000000 fe:00 12345 {}\n",
sleep.display(),
);
std::fs::write(pid_dir.join("maps"), maps).expect("write maps");
match attach_jemalloc_at(tmp.path(), pid) {
Err(AttachError::JemallocNotFound(_)) => {}
other => panic!("expected JemallocNotFound for non-jemalloc maps, got {other:?}",),
}
}
#[test]
fn attach_at_returns_readlink_failure_when_exe_symlink_missing() {
let tmp = tempfile::TempDir::new().expect("tempdir");
let pid: i32 = 4243;
let pid_dir = tmp.path().join(pid.to_string());
std::fs::create_dir_all(&pid_dir).expect("mkdir pid_dir");
let maps = "5583e6f7a000-5583e6f7b000 r-xp 00000000 fe:00 12345 /usr/bin/anything\n";
std::fs::write(pid_dir.join("maps"), maps).expect("write maps");
match attach_jemalloc_at(tmp.path(), pid) {
Err(AttachError::ReadlinkFailure(_)) => {}
other => panic!("expected ReadlinkFailure when exe symlink is absent, got {other:?}",),
}
}
#[test]
#[cfg(target_arch = "x86_64")]
fn variant_ii_worked_example() {
let fs_base = 0x7f12_3456_7000;
let aligned = 512;
let st_value = 0x100;
let field = 264;
let addr = compute_tls_address_variant_ii(fs_base, aligned, st_value, field).unwrap();
assert_eq!(addr, 0x7f12_3456_7008);
}
#[test]
#[cfg(target_arch = "x86_64")]
fn variant_ii_boundary_tp_equals_image_size() {
let addr = compute_tls_address_variant_ii(4096, 4096, 0, 0).unwrap();
assert_eq!(addr, 0);
}
#[cfg(target_arch = "aarch64")]
#[test]
fn variant_i_worked_example() {
let tpidr = 0x7f12_3456_7000;
let p_align = 16;
let st_value = 0x100;
let field = 264;
let addr = compute_tls_address_variant_i(tpidr, p_align, st_value, field).unwrap();
assert_eq!(addr, 0x7f12_3456_7218);
}
#[cfg(target_arch = "aarch64")]
#[test]
fn variant_i_high_alignment() {
let addr = compute_tls_address_variant_i(0x1000, 64, 0, 0).unwrap();
assert_eq!(addr, 0x1040);
}
#[cfg(target_arch = "aarch64")]
#[test]
fn variant_i_tcb_sized_alignment() {
let addr = compute_tls_address_variant_i(0x1000, TCB_SIZE_AARCH64, 0, 0).unwrap();
assert_eq!(addr, 0x1010);
}
#[cfg(target_arch = "aarch64")]
#[test]
fn variant_i_sub_tcb_alignment() {
let addr = compute_tls_address_variant_i(0x1000, 8, 0, 0).unwrap();
assert_eq!(addr, 0x1010);
}
#[cfg(target_arch = "aarch64")]
#[test]
fn variant_i_zero_align_clamped() {
let addr = compute_tls_address_variant_i(0x1000, 0, 0, 0).unwrap();
assert_eq!(addr, 0x1010);
}
#[cfg(target_arch = "aarch64")]
#[test]
fn variant_i_overflow_errors() {
let err = compute_tls_address_variant_i(u64::MAX - 10, 16, 0x100, 0).unwrap_err();
assert!(
format!("{err}").contains("TLS address arithmetic overflow"),
"got: {err}",
);
}
#[cfg(target_arch = "aarch64")]
#[test]
fn variant_i_image_offset_overflow_errors() {
let err = compute_tls_address_variant_i(0x1000, u64::MAX, 0, 0).unwrap_err();
assert!(
format!("{err}").contains("TLS image offset overflow"),
"expected image-offset overflow, got: {err}",
);
}
#[test]
fn compute_tls_address_dispatches_by_target_arch() {
let got = compute_tls_address(4096, 4096, 16, 0, 0).unwrap();
#[cfg(target_arch = "x86_64")]
assert_eq!(got, 0, "x86_64 must dispatch to Variant II");
#[cfg(target_arch = "aarch64")]
assert_eq!(got, 4112, "aarch64 must dispatch to Variant I");
}
#[test]
fn compute_tls_address_dispatches_positionally_distinct() {
let got = compute_tls_address(13_000_009, 1009, 64, 307, 83).unwrap();
#[cfg(target_arch = "x86_64")]
assert_eq!(got, 12_999_390, "x86_64 Variant II formula");
#[cfg(target_arch = "aarch64")]
assert_eq!(got, 13_000_463, "aarch64 Variant I formula");
}
#[test]
fn extract_pt_tls_layout_on_real_elf() {
let exe = std::env::current_exe().expect("current_exe");
let data = std::fs::read(&exe).expect("read current_exe");
let elf = goblin::elf::Elf::parse(&data).expect("parse current_exe");
let (rounded, align) = extract_pt_tls_layout(&elf).expect("test binary must carry PT_TLS");
assert!(
align.is_power_of_two(),
"p_align {align} must be a power of two",
);
assert!(
rounded >= align,
"aligned_size {rounded} must be >= align {align}"
);
assert!(
rounded % align == 0,
"aligned_size {rounded} must be a multiple of align {align}",
);
}
#[test]
fn counter_offsets_combined_span_adjacent() {
let o = CounterOffsets::new(100, 108).unwrap();
let span = o.combined_read_span();
assert_eq!(span, 16);
}
#[test]
fn read_build_id_on_real_elf_is_lowercase_hex() {
let exe = std::env::current_exe().expect("current_exe");
let data = std::fs::read(&exe).expect("read current_exe");
let elf = goblin::elf::Elf::parse(&data).expect("parse current_exe");
let Some(hex) = read_build_id(&elf, &data) else {
eprintln!("skip: current_exe carries no NT_GNU_BUILD_ID; toolchain elided it",);
return;
};
assert!(!hex.is_empty(), "build-id hex must be non-empty");
assert_eq!(
hex,
hex.to_ascii_lowercase(),
"build-id must be rendered in lowercase hex",
);
assert!(
hex.chars()
.all(|c| c.is_ascii_hexdigit() && (c.is_ascii_digit() || c.is_ascii_lowercase())),
"build-id must contain only ASCII hex digits [0-9a-f]; got {hex:?}",
);
assert!(
build_id_hex_is_safe(&hex),
"read_build_id output must pass build_id_hex_is_safe",
);
}
#[test]
fn read_gnu_debuglink_on_inline_debug_elf_returns_none() {
let exe = std::env::current_exe().expect("current_exe");
let data = std::fs::read(&exe).expect("read current_exe");
let elf = goblin::elf::Elf::parse(&data).expect("parse current_exe");
assert!(
read_gnu_debuglink(&elf, &data).is_none(),
"test binary has inline .debug_info; .gnu_debuglink must be absent",
);
}
#[test]
fn candidate_debuginfo_paths_full_layout() {
let target = Path::new("/usr/bin/example");
let paths = candidate_debuginfo_paths(target, Some("example.debug"), Some("abcdef0123456789"));
assert_eq!(paths.len(), 4);
assert_eq!(
paths[0],
PathBuf::from("/usr/lib/debug/.build-id/ab/cdef0123456789.debug"),
);
assert_eq!(paths[1], PathBuf::from("/usr/bin/example.debug"));
assert_eq!(paths[2], PathBuf::from("/usr/bin/.debug/example.debug"));
assert_eq!(
paths[3],
PathBuf::from("/usr/lib/debug/usr/bin/example.debug"),
);
}
#[test]
fn candidate_debuginfo_paths_returns_empty_when_no_hints() {
let target = Path::new("/usr/bin/example");
let paths = candidate_debuginfo_paths(target, None, None);
assert!(paths.is_empty());
}
#[test]
fn candidate_debuginfo_paths_skips_short_build_id() {
let target = Path::new("/usr/bin/example");
let paths = candidate_debuginfo_paths(target, Some("example.debug"), Some("a"));
assert_eq!(paths.len(), 3);
assert!(
!paths[0].to_string_lossy().contains("/.build-id/"),
"first candidate must be a debuglink path; got {:?}",
paths[0],
);
}
#[test]
fn candidate_debuginfo_paths_empty_build_id_skipped() {
let target = Path::new("/usr/bin/example");
let paths = candidate_debuginfo_paths(target, Some("example.debug"), Some(""));
assert_eq!(paths.len(), 3);
assert!(
!paths
.iter()
.any(|p| p.to_string_lossy().contains(".build-id")),
);
}
#[test]
fn candidate_debuginfo_paths_relative_target_skips_lib_debug_root() {
let target = Path::new("./example");
let paths = candidate_debuginfo_paths(target, Some("example.debug"), Some("deadbeef12345678"));
assert_eq!(paths.len(), 3);
assert!(
!paths
.iter()
.any(|p| p.starts_with("/usr/lib/debug") && !p.to_string_lossy().contains(".build-id")),
"no /usr/lib/debug-rooted debuglink candidate may emit \
for a relative target; got {:?}",
paths,
);
}
#[test]
fn candidate_debuginfo_paths_build_id_exactly_two_chars() {
let target = Path::new("/usr/bin/example");
let paths = candidate_debuginfo_paths(target, None, Some("ab"));
assert_eq!(paths.len(), 1);
assert_eq!(
paths[0],
PathBuf::from("/usr/lib/debug/.build-id/ab/.debug"),
);
}
#[test]
fn candidate_debuginfo_paths_build_id_only() {
let target = Path::new("/usr/bin/example");
let paths = candidate_debuginfo_paths(target, None, Some("abcdef0123456789"));
assert_eq!(paths.len(), 1);
assert_eq!(
paths[0],
PathBuf::from("/usr/lib/debug/.build-id/ab/cdef0123456789.debug"),
);
}
#[test]
fn candidate_debuginfo_paths_debuglink_only() {
let target = Path::new("/usr/bin/example");
let paths = candidate_debuginfo_paths(target, Some("example.debug"), None);
assert_eq!(paths.len(), 3);
assert_eq!(paths[0], PathBuf::from("/usr/bin/example.debug"));
assert_eq!(paths[1], PathBuf::from("/usr/bin/.debug/example.debug"));
assert_eq!(
paths[2],
PathBuf::from("/usr/lib/debug/usr/bin/example.debug"),
);
}
#[test]
fn candidate_debuginfo_paths_no_parent_skips_debuglink() {
let target = Path::new("/");
let paths = candidate_debuginfo_paths(target, Some("orphan.debug"), Some("abcdef0123456789"));
assert_eq!(paths.len(), 1);
assert_eq!(
paths[0],
PathBuf::from("/usr/lib/debug/.build-id/ab/cdef0123456789.debug"),
);
let paths = candidate_debuginfo_paths(target, Some("orphan.debug"), None);
assert!(paths.is_empty());
}
#[test]
fn candidate_debuginfo_paths_root_relative_target() {
let target = Path::new("/example");
let paths = candidate_debuginfo_paths(target, Some("example.debug"), None);
assert_eq!(paths.len(), 3);
assert_eq!(paths[0], PathBuf::from("/example.debug"));
assert_eq!(paths[1], PathBuf::from("/.debug/example.debug"));
assert_eq!(paths[2], PathBuf::from("/usr/lib/debug/example.debug"));
}
#[test]
fn candidate_debuginfo_paths_bare_basename_target() {
let target = Path::new("example");
let paths = candidate_debuginfo_paths(target, Some("example.debug"), None);
assert_eq!(paths.len(), 2);
assert_eq!(paths[0], PathBuf::from("example.debug"));
assert_eq!(paths[1], PathBuf::from(".debug/example.debug"));
assert!(
!paths.iter().any(|p| p.starts_with("/usr/lib/debug")),
"bare-basename target must not produce /usr/lib/debug-rooted \
debuglink candidate; got {:?}",
paths,
);
}
#[test]
fn debuglink_name_rejects_path_traversal_and_absolute_paths() {
assert!(!debuglink_name_is_safe(""));
assert!(!debuglink_name_is_safe("/"));
assert!(!debuglink_name_is_safe("/etc/passwd"));
assert!(!debuglink_name_is_safe("/etc/shadow"));
assert!(!debuglink_name_is_safe("../etc/passwd"));
assert!(!debuglink_name_is_safe("../../etc/passwd"));
assert!(!debuglink_name_is_safe("subdir/foo.debug"));
assert!(!debuglink_name_is_safe("./foo.debug"));
assert!(!debuglink_name_is_safe("."));
assert!(!debuglink_name_is_safe(".."));
assert!(!debuglink_name_is_safe("\0"));
assert!(!debuglink_name_is_safe("foo\0bar.debug"));
assert!(debuglink_name_is_safe("example.debug"));
assert!(debuglink_name_is_safe("ktstr.debug"));
assert!(debuglink_name_is_safe("libfoo-1.2.3.so.debug"));
assert!(debuglink_name_is_safe(".hidden.debug"));
assert!(debuglink_name_is_safe("a"));
}
#[test]
fn candidate_debuginfo_paths_drops_unsafe_debuglink_name() {
let target = Path::new("/usr/bin/example");
let paths = candidate_debuginfo_paths(target, Some("/etc/passwd"), None);
assert!(
paths.is_empty(),
"unsafe debuglink name (absolute path) must produce zero \
candidates; got {:?}",
paths,
);
let paths = candidate_debuginfo_paths(target, Some("../../etc/passwd"), None);
assert!(paths.is_empty());
let paths = candidate_debuginfo_paths(target, Some("/etc/passwd"), Some("abcdef0123456789"));
assert_eq!(paths.len(), 1);
assert_eq!(
paths[0],
PathBuf::from("/usr/lib/debug/.build-id/ab/cdef0123456789.debug"),
);
}
#[test]
fn build_id_hex_rejects_non_hex_inputs() {
assert!(!build_id_hex_is_safe(""));
assert!(!build_id_hex_is_safe("/"));
assert!(!build_id_hex_is_safe("/."));
assert!(!build_id_hex_is_safe("/.passwd"));
assert!(!build_id_hex_is_safe("../etc"));
assert!(!build_id_hex_is_safe("a/b"));
assert!(!build_id_hex_is_safe(".."));
assert!(!build_id_hex_is_safe("\0\0"));
assert!(!build_id_hex_is_safe("AB"));
assert!(!build_id_hex_is_safe("ABCD"));
assert!(!build_id_hex_is_safe("ABCDEF0123456789"));
assert!(!build_id_hex_is_safe("xx"));
assert!(!build_id_hex_is_safe("xy"));
assert!(!build_id_hex_is_safe("zz"));
assert!(!build_id_hex_is_safe("abc")); assert!(!build_id_hex_is_safe("ab cd")); assert!(!build_id_hex_is_safe("ab-cd")); }
#[test]
fn build_id_hex_accepts_lowercase_hex() {
assert!(build_id_hex_is_safe("ab"));
assert!(build_id_hex_is_safe("abcd"));
assert!(build_id_hex_is_safe("0123456789abcdef"));
assert!(build_id_hex_is_safe(
"abcdef0123456789abcdef0123456789abcdef01"
));
assert!(build_id_hex_is_safe(
"0011223344556677889900112233445566778899001122334455667788990011"
));
}
#[test]
fn candidate_debuginfo_paths_drops_unsafe_build_id_hex() {
let target = Path::new("/usr/bin/example");
let paths = candidate_debuginfo_paths(target, None, Some("/.passwd"));
assert!(
paths.is_empty(),
"unsafe build-id hex (path separator) must produce zero \
candidates; got {:?}",
paths,
);
let paths = candidate_debuginfo_paths(target, None, Some(".."));
assert!(paths.is_empty());
let paths = candidate_debuginfo_paths(target, None, Some("ABCDEF0123456789"));
assert!(paths.is_empty());
let paths = candidate_debuginfo_paths(target, None, Some("xyzzy012345"));
assert!(paths.is_empty());
let paths = candidate_debuginfo_paths(target, None, Some("abc"));
assert!(paths.is_empty());
let paths = candidate_debuginfo_paths(target, Some("example.debug"), Some("/.passwd"));
assert_eq!(paths.len(), 3);
assert_eq!(paths[0], PathBuf::from("/usr/bin/example.debug"));
assert_eq!(paths[1], PathBuf::from("/usr/bin/.debug/example.debug"));
assert_eq!(
paths[2],
PathBuf::from("/usr/lib/debug/usr/bin/example.debug"),
);
}
#[test]
fn test_elf_has_populated_debug_info_section_and_stt_func_symbols() {
use goblin::elf::sym;
let exe = std::env::current_exe().expect("current_exe");
let data = std::fs::read(&exe).expect("read current_exe");
let elf = goblin::elf::Elf::parse(&data).expect("parse current_exe");
assert!(
section_is_populated(&elf, &data, ".debug_info"),
"test binary must carry a populated .debug_info section",
);
let func_count = elf
.syms
.iter()
.filter(|s| s.st_type() == sym::STT_FUNC)
.count();
assert!(
func_count > 0,
"test binary must carry at least one STT_FUNC symbol in .symtab",
);
}
#[test]
fn round_up_pow2_boundary_matrix() {
assert_eq!(round_up_pow2(0, 0), Some(0));
assert_eq!(round_up_pow2(0, 1), Some(0));
assert_eq!(round_up_pow2(u64::MAX, 1), Some(u64::MAX));
assert_eq!(round_up_pow2(u64::MAX, 2), None);
assert_eq!(round_up_pow2(7, 8), Some(8));
assert_eq!(round_up_pow2(8, 8), Some(8));
assert_eq!(round_up_pow2(9, 8), Some(16));
}
#[test]
fn find_jemalloc_tsd_tls_in_table_empty_returns_none() {
let tab: goblin::elf::Symtab<'_> = Default::default();
let strs = goblin::strtab::Strtab::default();
assert!(find_jemalloc_tsd_tls_in_table(&tab, &strs).is_none());
}
#[test]
fn is_jemalloc_tsd_tls_symbol_accepts_bare_form() {
assert!(is_jemalloc_tsd_tls_symbol("tsd_tls"));
}
#[test]
fn is_jemalloc_tsd_tls_symbol_accepts_known_prefixes() {
assert!(is_jemalloc_tsd_tls_symbol("je_tsd_tls"));
assert!(is_jemalloc_tsd_tls_symbol("_rjem_je_tsd_tls"));
assert!(is_jemalloc_tsd_tls_symbol("jemalloc_je_tsd_tls"));
assert!(is_jemalloc_tsd_tls_symbol("custom_prefix_tsd_tls"));
}
#[test]
fn is_jemalloc_tsd_tls_symbol_rejects_lookalikes() {
assert!(!is_jemalloc_tsd_tls_symbol("mytsd_tls"));
assert!(!is_jemalloc_tsd_tls_symbol("tsd_tls_v2"));
assert!(!is_jemalloc_tsd_tls_symbol("je_tsd_tls_extra"));
assert!(!is_jemalloc_tsd_tls_symbol("tsd"));
assert!(!is_jemalloc_tsd_tls_symbol("je_tsd"));
assert!(!is_jemalloc_tsd_tls_symbol("_tsd_tls")); assert!(!is_jemalloc_tsd_tls_symbol(""));
assert!(!is_jemalloc_tsd_tls_symbol("tls"));
}
mod elf_fixture {
use goblin::elf::header as h;
use goblin::elf::section_header as sh;
use std::io::Write;
pub(super) struct SecSpec {
name: &'static str,
sh_type: u32,
sh_flags: u64,
sh_addr: u64,
data: Vec<u8>,
sh_link: u32,
sh_info: u32,
sh_entsize: u64,
}
impl SecSpec {
pub(super) fn new(name: &'static str, sh_type: u32) -> Self {
Self {
name,
sh_type,
sh_flags: 0,
sh_addr: 0,
data: Vec::new(),
sh_link: 0,
sh_info: 0,
sh_entsize: 0,
}
}
pub(super) fn flags(mut self, f: u64) -> Self {
self.sh_flags = f;
self
}
pub(super) fn data(mut self, d: Vec<u8>) -> Self {
self.data = d;
self
}
}
pub(super) fn build_elf64(sections: Vec<SecSpec>, e_machine: u16, e_type: u16) -> Vec<u8> {
let mut shstrtab: Vec<u8> = vec![0u8]; let null_name_off = 0u32;
let mut sec_name_offs: Vec<u32> = Vec::new();
for s in §ions {
sec_name_offs.push(shstrtab.len() as u32);
shstrtab.extend_from_slice(s.name.as_bytes());
shstrtab.push(0);
}
let shstrtab_self_name_off = shstrtab.len() as u32;
shstrtab.extend_from_slice(b".shstrtab");
shstrtab.push(0);
let ehdr_size: usize = 64;
let shdr_size: usize = 64;
let mut data_blob: Vec<u8> = Vec::new();
let mut sec_file_off: Vec<u64> = Vec::new();
sec_file_off.push(0);
let mut cursor: u64 = ehdr_size as u64;
for s in §ions {
sec_file_off.push(cursor);
data_blob.extend_from_slice(&s.data);
cursor += s.data.len() as u64;
}
let shstrtab_file_off = cursor;
data_blob.extend_from_slice(&shstrtab);
cursor += shstrtab.len() as u64;
let shoff = cursor;
let shnum = (1 + sections.len() + 1) as u16;
let shstrndx = (1 + sections.len()) as u16;
let mut blob: Vec<u8> = Vec::with_capacity(ehdr_size);
blob.extend_from_slice(h::ELFMAG); blob.push(h::ELFCLASS64); blob.push(h::ELFDATA2LSB); blob.push(h::EV_CURRENT); blob.push(0); blob.push(0); blob.extend_from_slice(&[0u8; 7]); blob.extend_from_slice(&e_type.to_le_bytes());
blob.extend_from_slice(&e_machine.to_le_bytes());
blob.extend_from_slice(&1u32.to_le_bytes()); blob.extend_from_slice(&0u64.to_le_bytes()); blob.extend_from_slice(&0u64.to_le_bytes()); blob.extend_from_slice(&shoff.to_le_bytes()); blob.extend_from_slice(&0u32.to_le_bytes()); blob.extend_from_slice(&(ehdr_size as u16).to_le_bytes()); blob.extend_from_slice(&0u16.to_le_bytes()); blob.extend_from_slice(&0u16.to_le_bytes()); blob.extend_from_slice(&(shdr_size as u16).to_le_bytes()); blob.extend_from_slice(&shnum.to_le_bytes()); blob.extend_from_slice(&shstrndx.to_le_bytes());
blob.extend_from_slice(&data_blob);
let mut write_shdr = |sh_name: u32,
sh_type: u32,
sh_flags: u64,
sh_addr: u64,
sh_offset: u64,
sh_size: u64,
sh_link: u32,
sh_info: u32,
sh_addralign: u64,
sh_entsize: u64| {
blob.write_all(&sh_name.to_le_bytes()).unwrap();
blob.write_all(&sh_type.to_le_bytes()).unwrap();
blob.write_all(&sh_flags.to_le_bytes()).unwrap();
blob.write_all(&sh_addr.to_le_bytes()).unwrap();
blob.write_all(&sh_offset.to_le_bytes()).unwrap();
blob.write_all(&sh_size.to_le_bytes()).unwrap();
blob.write_all(&sh_link.to_le_bytes()).unwrap();
blob.write_all(&sh_info.to_le_bytes()).unwrap();
blob.write_all(&sh_addralign.to_le_bytes()).unwrap();
blob.write_all(&sh_entsize.to_le_bytes()).unwrap();
};
write_shdr(null_name_off, sh::SHT_NULL, 0, 0, 0, 0, 0, 0, 0, 0);
for (i, s) in sections.iter().enumerate() {
write_shdr(
sec_name_offs[i],
s.sh_type,
s.sh_flags,
s.sh_addr,
sec_file_off[i + 1],
s.data.len() as u64,
s.sh_link,
s.sh_info,
1,
s.sh_entsize,
);
}
write_shdr(
shstrtab_self_name_off,
sh::SHT_STRTAB,
0,
0,
shstrtab_file_off,
shstrtab.len() as u64,
0,
0,
1,
0,
);
blob
}
}
#[test]
fn parse_maps_elf_path_truncates_path_with_spaces() {
assert_eq!(
parse_maps_elf_path("5583e6f7a000-5583e6f7b000 r-xp 00000000 fe:00 12345 /opt/my app/bin"),
Some(PathBuf::from("/opt/my")),
);
}
#[test]
fn parse_maps_elf_path_returns_none_on_too_few_fields() {
assert_eq!(
parse_maps_elf_path("5583e6f7a000-5583e6f7b000 r-xp 00000000 fe:00"),
None,
);
assert_eq!(parse_maps_elf_path("5583e6f7a000-5583e6f7b000 r-xp"), None);
}
#[test]
fn member_offset_accepts_every_constant_dwarf_form() {
type R = gimli::EndianSlice<'static, gimli::LittleEndian>;
assert_eq!(member_offset::<R>(None).unwrap(), None);
assert_eq!(
member_offset::<R>(Some(gimli::AttributeValue::Udata(264))).unwrap(),
Some(264),
);
assert_eq!(
member_offset::<R>(Some(gimli::AttributeValue::Data1(7))).unwrap(),
Some(7),
);
assert_eq!(
member_offset::<R>(Some(gimli::AttributeValue::Data2(513))).unwrap(),
Some(513),
);
assert_eq!(
member_offset::<R>(Some(gimli::AttributeValue::Data4(70000))).unwrap(),
Some(70000),
);
assert_eq!(
member_offset::<R>(Some(gimli::AttributeValue::Data8(u32::MAX as u64 + 1))).unwrap(),
Some(4_294_967_296),
);
assert_eq!(
member_offset::<R>(Some(gimli::AttributeValue::Sdata(40))).unwrap(),
Some(40),
);
}
#[test]
fn member_offset_rejects_negative_sdata_and_expr_forms() {
type R = gimli::EndianSlice<'static, gimli::LittleEndian>;
let e = member_offset::<R>(Some(gimli::AttributeValue::Sdata(-1))).unwrap_err();
let msg = format!("{e}");
assert!(
msg.contains("unexpected DW_AT_data_member_location form"),
"got: {msg}",
);
assert!(
msg.contains("DWARF expression forms are not supported"),
"got: {msg}",
);
let expr = member_offset::<R>(Some(gimli::AttributeValue::Block(gimli::EndianSlice::new(
&[0x91, 0x00],
gimli::LittleEndian,
))));
assert!(
expr.is_err(),
"DWARF expression (Block) form must be rejected, got: {expr:?}",
);
}
#[test]
fn read_gnu_debuglink_parses_name_and_crc_from_synthetic_section() {
use elf_fixture::{SecSpec, build_elf64};
use goblin::elf::header::{EM_X86_64, ET_REL};
use goblin::elf::section_header::SHT_PROGBITS;
let section = vec![
b'f', b'o', b'o', b'.', b'd', b'e', b'b', b'u', b'g', 0, 0, 0, 0x78, 0x56, 0x34, 0x12,
];
let data = build_elf64(
vec![SecSpec::new(".gnu_debuglink", SHT_PROGBITS).data(section)],
EM_X86_64,
ET_REL,
);
let elf = goblin::elf::Elf::parse(&data).expect("parse synthetic ELF");
assert_eq!(
read_gnu_debuglink(&elf, &data),
Some(("foo.debug".to_string(), 0x1234_5678)),
);
}
#[test]
fn read_gnu_debuglink_returns_none_on_missing_nul_and_truncated_crc() {
use elf_fixture::{SecSpec, build_elf64};
use goblin::elf::header::{EM_X86_64, ET_REL};
use goblin::elf::section_header::SHT_PROGBITS;
let data_no_nul = build_elf64(
vec![SecSpec::new(".gnu_debuglink", SHT_PROGBITS).data(b"abcd".to_vec())],
EM_X86_64,
ET_REL,
);
let elf_no_nul = goblin::elf::Elf::parse(&data_no_nul).expect("parse no-nul ELF");
assert_eq!(read_gnu_debuglink(&elf_no_nul, &data_no_nul), None);
let data_short = build_elf64(
vec![SecSpec::new(".gnu_debuglink", SHT_PROGBITS).data(b"a\0".to_vec())],
EM_X86_64,
ET_REL,
);
let elf_short = goblin::elf::Elf::parse(&data_short).expect("parse short-crc ELF");
assert_eq!(read_gnu_debuglink(&elf_short, &data_short), None);
}
#[test]
fn read_build_id_renders_descriptor_as_lowercase_hex() {
use elf_fixture::{SecSpec, build_elf64};
use goblin::elf::header::{EM_X86_64, ET_REL};
use goblin::elf::section_header::SHT_NOTE;
let mut note: Vec<u8> = Vec::new();
note.extend_from_slice(&4u32.to_le_bytes()); note.extend_from_slice(&4u32.to_le_bytes()); note.extend_from_slice(&3u32.to_le_bytes()); note.extend_from_slice(b"GNU\0"); note.extend_from_slice(&[0xde, 0xad, 0xbe, 0xef]);
let data = build_elf64(
vec![SecSpec::new(".note.gnu.build-id", SHT_NOTE).data(note)],
EM_X86_64,
ET_REL,
);
let elf = goblin::elf::Elf::parse(&data).expect("parse build-id ELF");
assert_eq!(read_build_id(&elf, &data), Some("deadbeef".to_string()));
}
#[test]
fn read_build_id_returns_none_on_short_section_and_overlong_descsz() {
use elf_fixture::{SecSpec, build_elf64};
use goblin::elf::header::{EM_X86_64, ET_REL};
use goblin::elf::section_header::SHT_NOTE;
let data_short = build_elf64(
vec![SecSpec::new(".note.gnu.build-id", SHT_NOTE).data(vec![0u8; 8])],
EM_X86_64,
ET_REL,
);
let elf_short = goblin::elf::Elf::parse(&data_short).expect("parse short note ELF");
assert_eq!(read_build_id(&elf_short, &data_short), None);
let mut over: Vec<u8> = Vec::new();
over.extend_from_slice(&4u32.to_le_bytes()); over.extend_from_slice(&99u32.to_le_bytes()); over.extend_from_slice(&3u32.to_le_bytes()); over.extend_from_slice(b"GNU\0"); over.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]); let data_over = build_elf64(
vec![SecSpec::new(".note.gnu.build-id", SHT_NOTE).data(over)],
EM_X86_64,
ET_REL,
);
let elf_over = goblin::elf::Elf::parse(&data_over).expect("parse overlong note ELF");
assert_eq!(read_build_id(&elf_over, &data_over), None);
}
#[test]
fn find_section_slice_returns_none_for_absent_section_and_slice_for_present() {
use elf_fixture::{SecSpec, build_elf64};
use goblin::elf::header::{EM_X86_64, ET_REL};
use goblin::elf::section_header::SHT_PROGBITS;
let data = build_elf64(
vec![SecSpec::new(".mydata", SHT_PROGBITS).data(b"PAYLOAD!".to_vec())],
EM_X86_64,
ET_REL,
);
let elf = goblin::elf::Elf::parse(&data).expect("parse section-slice ELF");
assert_eq!(
find_section_slice(&elf, &data, ".mydata"),
Some(&b"PAYLOAD!"[..]),
);
assert_eq!(find_section_slice(&elf, &data, ".absent"), None);
}
#[test]
fn section_is_populated_distinguishes_empty_present_and_absent() {
use elf_fixture::{SecSpec, build_elf64};
use goblin::elf::header::{EM_X86_64, ET_REL};
use goblin::elf::section_header::SHT_PROGBITS;
let data = build_elf64(
vec![
SecSpec::new(".full", SHT_PROGBITS).data(vec![1, 2, 3]),
SecSpec::new(".empty", SHT_PROGBITS),
],
EM_X86_64,
ET_REL,
);
let elf = goblin::elf::Elf::parse(&data).expect("parse populated/empty ELF");
assert!(section_is_populated(&elf, &data, ".full"));
assert!(!section_is_populated(&elf, &data, ".empty"));
assert!(!section_is_populated(&elf, &data, ".absent"));
}
#[test]
fn extract_pt_tls_layout_errors_when_no_pt_tls_segment() {
use elf_fixture::{SecSpec, build_elf64};
use goblin::elf::header::{EM_X86_64, ET_REL};
use goblin::elf::section_header::{SHF_EXECINSTR, SHT_PROGBITS};
let data = build_elf64(
vec![SecSpec::new(".text", SHT_PROGBITS).flags(SHF_EXECINSTR.into())],
EM_X86_64,
ET_REL,
);
let elf = goblin::elf::Elf::parse(&data).expect("parse no-PT_TLS ELF");
let err = extract_pt_tls_layout(&elf).unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("ELF has no PT_TLS segment"), "got: {msg}");
assert!(msg.contains("does not use static TLS"), "got: {msg}");
}
#[test]
fn attach_at_returns_maps_read_failure_when_maps_absent() {
let exe_target = PathBuf::from("/bin/sh");
if !exe_target.exists() {
eprintln!("skipping — /bin/sh unavailable for exe symlink target");
return;
}
let tmp = tempfile::TempDir::new().expect("tempdir");
let pid: i32 = 4244;
let pid_dir = tmp.path().join(pid.to_string());
std::fs::create_dir_all(&pid_dir).expect("mkdir pid_dir");
std::os::unix::fs::symlink(&exe_target, pid_dir.join("exe")).expect("symlink exe");
match attach_jemalloc_at(tmp.path(), pid) {
Err(AttachError::MapsReadFailure(_)) => {}
other => panic!("expected MapsReadFailure when maps file is absent, got {other:?}"),
}
}