#![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"));
}