Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 23 additions & 12 deletions e2e-tests/tests/backtrace_execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1970,6 +1970,14 @@ trace dlopen_main_after_limit_heartbeat {
refresh_count >= metadata_load_count,
"each metadata load should have a corresponding identity resolution, got {metadata_load_count} loads and {refresh_count} resolutions\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}"
);
let stderr_after_limit = stderr
.split_once(&warning)
.map(|(_, remainder)| remainder)
.expect("the limit warning count was checked above");
assert!(
!stderr_after_limit.contains("Resolving one backtrace runtime module in the background"),
"runtime module discovery should stop after the terminal limit warning\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}"
);
assert_eq!(
metadata_load_count,
MODULE_LIMIT as usize,
Expand Down Expand Up @@ -2042,19 +2050,14 @@ trace dlopen_main_callback {
}
anyhow::ensure!(exit_code == 0, "stderr={stderr} stdout={stdout}");

let blocks = backtrace_blocks_after(&stdout, "T_MODE_DLOPEN_CALLBACK_STACK", 6)?;
let refreshed_block = blocks
.iter()
.find(|block| block.contains("dlopen_lib_middle") && block.contains("dlopen_lib_driver"))
.ok_or_else(|| {
anyhow::anyhow!(
"expected a target-mode dlopen backtrace block to unwind inside the library\n\
STDOUT:\n{stdout}\nSTDERR:\n{stderr}"
)
})?;
assert_ordered_patterns(
refreshed_block,
let refreshed_block = matching_backtrace_block_with_ordered_patterns_after(
&stdout,
&stderr,
"T_MODE_DLOPEN_CALLBACK_STACK",
6,
"a fully symbolized target-mode dlopen backtrace",
&[
"backtrace: truncated, 6 frames (max 6)",
"#0 dlopen_main_callback",
"#1 dlopen_lib_leaf",
"#2 dlopen_lib_middle",
Expand Down Expand Up @@ -2118,6 +2121,7 @@ trace dlopen_lib_leaf {
anyhow::ensure!(exit_code == 0, "stderr={stderr} stdout={stdout}");

let expected_frames = [
"backtrace: truncated, 6 frames (max 6)",
"#0 dlopen_lib_leaf",
"#1 dlopen_lib_middle",
"#2 dlopen_lib_driver",
Expand All @@ -2137,6 +2141,13 @@ trace dlopen_lib_leaf {
"shared-library target dlopen backtrace should publish the first target mapping refresh\n\
BLOCK:\n{refreshed_block}\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}"
);
// The target-only periodic refresh must not undo a completed process range
// snapshot. Check steady-state output, not just the first successful event.
let blocks = backtrace_blocks_after(&stdout, "T_MODE_DLOPEN_SHARED_LIBRARY_STACK", 6)?;
assert!(blocks.len() >= 10, "expected steady-state backtrace events");
for block in blocks.iter().rev().take(10) {
assert_ordered_patterns(block, &expected_frames)?;
}

Ok(())
}
Expand Down
113 changes: 113 additions & 0 deletions e2e-tests/tests/backtrace_publication_execution.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
//! Shared CFI allocation across initial loaders and background runtime appends.

mod common;

use aya::maps::{Array, HashMap, Map, MapData};
use ghostscope_loader::GhostScopeLoader;
use ghostscope_process::pinned_bpf_maps::*;
use ghostscope_protocol::{BacktraceModuleRowRange, BacktraceUnwindRow};
use std::sync::{Arc, Barrier};

#[tokio::test]
async fn test_shared_cfi_initialization_and_runtime_append_do_not_overlap() -> anyhow::Result<()> {
common::init();
if std::env::var_os("E2E_RUN_CONTAINER_TOPOLOGY").is_some() {
return Ok(());
}
cleanup_current_pinned_maps()?;
let _cleanup = scopeguard::guard((), |_| {
let _ = cleanup_current_pinned_maps();
});
ensure_pinned_proc_offsets_exists(4096)?;
ensure_pinned_pid_aliases_exists(4096)?;
ensure_pinned_proc_module_ranges_exist(4096)?;
ensure_pinned_backtrace_cfi_maps_exist(4096, 4096)?;

let binary = common::FIXTURES.get_test_binary("backtrace_hot_program")?;
let analyzer = ghostscope_dwarf::DwarfAnalyzer::from_exec_path(&binary).await?;
let options = ghostscope_compiler::CompileOptions {
binary_path_hint: Some(binary.to_string_lossy().into_owned()),
backtrace_unwind_rows_max_entries: 4096,
..Default::default()
};
let compiled = ghostscope_compiler::compile_script(
"trace hot_bt_probe { bt; }",
&analyzer,
None,
Some(1),
&options,
)?;
anyhow::ensure!(
compiled.uprobe_configs.len() == 1,
"expected one backtrace config"
);
let bytecode = &compiled.uprobe_configs[0].ebpf_bytecode;
let initial = GhostScopeLoader::new_with_shared_backtrace_maps(bytecode, true)?;
let runtime = GhostScopeLoader::new_with_shared_backtrace_maps(bytecode, true)?;
let start = Arc::new(Barrier::new(2));
let row = |cookie, index| BacktraceUnwindRow {
pc_start: cookie * 0x1000 + index,
pc_end: cookie * 0x1000 + index + 1,
..Default::default()
};
std::thread::scope(|scope| -> anyhow::Result<()> {
let mut publishers = Vec::new();
for (publisher, mut loader) in [(0, initial), (1, runtime)] {
let start = Arc::clone(&start);
publishers.push(scope.spawn(move || -> anyhow::Result<()> {
start.wait();
for round in 0..16 {
let cookie = 1 + round * 2 + publisher;
let rows: Vec<_> = (0..32).map(|index| row(cookie, index)).collect();
if publisher == 0 {
loader.populate_backtrace_unwind_rows_and_module_row_ranges(
&rows,
&[(
cookie,
BacktraceModuleRowRange {
row_start: 0,
row_end: 32,
},
)],
)?;
} else {
loader.append_backtrace_unwind_rows_for_modules(&[(cookie, rows)])?;
}
}
Ok(())
}));
}
for publisher in publishers {
publisher.join().unwrap()?;
}
Ok(())
})?;

let ranges = HashMap::<_, u64, BacktraceModuleRowRange>::try_from(Map::from_map_data(
MapData::from_pin(bt_module_row_ranges_pin_path()?)?,
)?)?;
let rows = Array::<_, BacktraceUnwindRow>::try_from(Map::from_map_data(MapData::from_pin(
bt_unwind_rows_pin_path()?,
)?)?)?;
let mut allocated = std::collections::BTreeSet::new();
for cookie in 1..=32 {
let range = ranges.get(&cookie, 0)?;
assert_eq!(range.row_end - range.row_start, 32);
for index in range.row_start..range.row_end {
assert!(
allocated.insert(index),
"overlapping CFI row allocation at {index}"
);
assert_eq!(
rows.get(&index, 0)?,
row(cookie, u64::from(index - range.row_start))
);
}
}
// A trace attached after all original loaders are gone sees completed modules.
let mut replacement = GhostScopeLoader::new_with_shared_backtrace_maps(bytecode, true)?;
let stats = replacement.append_backtrace_unwind_rows_for_modules(&[(1, vec![row(1, 0)])])?;
assert_eq!(stats.modules, 0);
assert_eq!(stats.rows, 0);
Ok(())
}
77 changes: 69 additions & 8 deletions e2e-tests/tests/globals_target_execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ use serial_test::serial;
use std::env;
use std::os::unix::fs as unix_fs;
use std::path::{Path, PathBuf};
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use std::thread::JoinHandle;
use std::time::Duration;
use tempfile::Builder;

Expand Down Expand Up @@ -52,11 +57,64 @@ fn prepare_late_start_launcher(
common::targets::ensure_target_binary_ready_for_default_sandbox(binary_path)
}

fn ghostscope_log_path() -> anyhow::Result<std::path::PathBuf> {
Ok(Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.ok_or_else(|| anyhow::anyhow!("failed to resolve workspace root for ghostscope.log"))?
.join("ghostscope.log"))
fn ghostscope_log_path() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("ghostscope.log")
}

struct MapChangeChurn {
stop: Arc<AtomicBool>,
handle: Option<JoinHandle<()>>,
}

impl MapChangeChurn {
fn start() -> Self {
const MAPS_PER_BATCH: usize = 64;

let stop = Arc::new(AtomicBool::new(false));
let worker_stop = Arc::clone(&stop);
let handle = std::thread::spawn(move || {
let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
if page_size <= 0 {
return;
}
while !worker_stop.load(Ordering::Relaxed) {
for _ in 0..MAPS_PER_BATCH {
// SAFETY: The anonymous mapping is private to this worker and is unmapped
// immediately without exposing the pointer to Rust references.
let mapped = unsafe {
libc::mmap(
std::ptr::null_mut(),
page_size as usize,
libc::PROT_NONE,
libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
-1,
0,
)
};
if mapped != libc::MAP_FAILED {
// SAFETY: `mapped` and the length exactly match the successful mmap above.
unsafe {
libc::munmap(mapped, page_size as usize);
}
}
}
std::thread::sleep(Duration::from_millis(1));
}
});
Self {
stop,
handle: Some(handle),
}
}
}

impl Drop for MapChangeChurn {
fn drop(&mut self) {
self.stop.store(true, Ordering::Relaxed);
if let Some(handle) = self.handle.take() {
let _ = handle.join();
}
}
}

fn workspace_root() -> anyhow::Result<PathBuf> {
Expand Down Expand Up @@ -559,6 +617,9 @@ async fn test_t_mode_library_late_start_globals_prints() -> anyhow::Result<()> {
// only after GhostScope reports its late-start hooks are ready.
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let _target_sandbox_guard = prepare_late_start_launcher(&binary_path)?;
// Keep unrelated mmap traffic flowing while GhostScope discovers the late-start target. This
// guards against sysmon maintenance becoming dependent on a globally quiet event stream.
let _map_change_churn = MapChangeChurn::start();
let bin_dir = binary_path.parent().unwrap().to_path_buf();
let lib_path = bin_dir.join("libgvars.so");
let script = r#"
Expand Down Expand Up @@ -603,14 +664,14 @@ trace lib_tick {
"Late-start: LIB_STATE.counter should +2 per tick. STDOUT: {stdout}"
);
} else {
let log_dump = tokio::fs::read_to_string(ghostscope_log_path()?)
let log_dump = tokio::fs::read_to_string(ghostscope_log_path())
.await
.unwrap_or_else(|_| "<ghostscope.log unavailable>".to_string());
let msg =
format!("Late-start: No events for our PID {pid}. STDOUT: {stdout}. LOG: {log_dump}");
assert!(!uniq.is_empty(), "{}", msg);
}
let _ = tokio::fs::remove_file(ghostscope_log_path()?).await;
let _ = tokio::fs::remove_file(ghostscope_log_path()).await;
Ok(())
}

Expand Down Expand Up @@ -779,7 +840,7 @@ trace lib_tick {
"Should not surface raw read_user errors for PID {pid}. STDOUT: {stdout}"
);

let _ = tokio::fs::remove_file(ghostscope_log_path()?).await;
let _ = tokio::fs::remove_file(ghostscope_log_path()).await;

Ok(())
}
19 changes: 16 additions & 3 deletions ghostscope-dwarf/src/analyzer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1049,9 +1049,22 @@ impl DwarfAnalyzer {
) -> Option<String> {
let symbols = self.runtime_text_symbols.get(&module_cookie)?;
let upper = symbols.partition_point(|symbol| symbol.address <= address);
let symbol = symbols[..upper].iter().rev().find(|symbol| {
symbol.size == 0 || address < symbol.address.saturating_add(symbol.size)
})?;
let symbol = symbols[..upper]
.iter()
.enumerate()
.rev()
.find(|(index, symbol)| {
let end = if symbol.size == 0 {
symbols[index + 1..]
.iter()
.find(|next| next.address > symbol.address)
.map(|next| next.address)
} else {
Some(symbol.address.saturating_add(symbol.size))
};
end.is_none_or(|end| address < end)
})
.map(|(_, symbol)| symbol)?;
let rust_hashes = if full {
RustSymbolHashDisplay::Shown
} else {
Expand Down
36 changes: 36 additions & 0 deletions ghostscope-dwarf/src/analyzer/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,39 @@ fn runtime_text_symbols_resolve_without_loading_module_dwarf() {
.find_runtime_function_name_for_display(0x5678, 0x13f, false)
.is_none());
}

#[test]
fn zero_sized_runtime_text_symbol_stops_at_next_symbol() {
let mut analyzer = DwarfAnalyzer::from_modules(0, Vec::new());
analyzer.add_runtime_text_symbols(
0x1234,
vec![
RuntimeTextSymbol {
name: "zero_sized".to_string(),
address: 0x100,
size: 0,
},
RuntimeTextSymbol {
name: "next_function".to_string(),
address: 0x120,
size: 0x10,
},
],
);

assert_eq!(
analyzer
.find_runtime_function_name_for_display(0x1234, 0x110, false)
.as_deref(),
Some("zero_sized")
);
assert_eq!(
analyzer
.find_runtime_function_name_for_display(0x1234, 0x125, false)
.as_deref(),
Some("next_function")
);
assert!(analyzer
.find_runtime_function_name_for_display(0x1234, 0x130, false)
.is_none());
}
Loading
Loading