Skip to content
3 changes: 2 additions & 1 deletion perf-self-profile/benches/unwind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ fn read_registers() -> (usize, usize, usize) {
fn do_unwind() -> usize {
let (pc, fp, sp) = read_registers();
let mut out = [0u64; 128];
unsafe { unwind(pc, fp, sp, &mut out) }
let (n, _truncated) = unsafe { unwind(pc, fp, sp, &mut out) };
n
}

/// Build a chain of exactly N inline(never) frames via recursion.
Expand Down
1 change: 1 addition & 0 deletions perf-self-profile/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ mod sampler;
mod symbolize;
mod sys;
pub mod tracepoint;
pub mod unwinder;

pub use offline_symbolize::SymbolTableEntry;
pub use sampler::{EventSource, Sample, SamplerConfig, SamplingMode};
Expand Down
2 changes: 1 addition & 1 deletion perf-self-profile/src/sys/linux/ctimer_sampler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ extern "C" fn sigprof_handler(
slot.write(pid, tid, time, cpu, period);

// Unwind into the slot's frame buffer
let count = unwind::unwind_from_ucontext(ucontext, slot.frames_mut());
let (count, _truncated) = unwind::unwind_from_ucontext(ucontext, slot.frames_mut());
slot.set_num_frames(count as u32);

slot.commit();
Expand Down
31 changes: 30 additions & 1 deletion perf-self-profile/src/sys/linux/fp_profiler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,35 @@ mod supported {
Ok(())
}

/// Check whether our SIGSEGV handler is still the active handler for
/// SIGSEGV. Returns `true` if the currently-installed handler matches
/// the one we registered in [`install_handler`].
///
/// Some other code may install its own SIGSEGV handler after ours,
/// either chaining to us or not. This function lets callers detect
/// that case so they can reinstall or skip capture.
///
/// Performs one `sigaction` syscall; not suitable for hot paths.
pub fn handler_is_installed() -> bool {
// If we never installed, we cannot be installed.
if !HANDLER_INSTALLED.load(Ordering::SeqCst) {
return false;
}

// Query current SIGSEGV handler without modifying it.
let mut current: libc::sigaction = unsafe { std::mem::zeroed() };
// SAFETY: passing a null `act` pointer is valid per POSIX and only
// retrieves the current action.
let rc = unsafe { libc::sigaction(libc::SIGSEGV, ptr::null(), &mut current) };
if rc != 0 {
return false;
}

// Compare sa_sigaction function pointer against our handler.
let expected = sigsegv_handler as *const () as usize;
current.sa_sigaction == expected
}

/// SIGSEGV handler for `safe_load`: if the faulting PC is within the
/// `safe_load_start..safe_load_end` instruction range, it skips the faulting
/// load, and resumes execution.
Expand Down Expand Up @@ -192,4 +221,4 @@ mod supported {
}
}

pub use supported::{install_handler, load};
pub use supported::{handler_is_installed, install_handler, load};
106 changes: 82 additions & 24 deletions perf-self-profile/src/sys/linux/fp_profiler/unwind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,27 +36,34 @@ const MAX_FRAME_SIZE: usize = 0x40000;
/// Safe to apply unconditionally, on non-PAC systems the upper bits are zero.
#[cfg(target_arch = "aarch64")]
#[inline(always)]
fn strip_pac(addr: usize) -> usize {
pub(crate) fn strip_pac(addr: usize) -> usize {
addr & 0x0000_FFFF_FFFF_FFFF
}

#[cfg(target_arch = "x86_64")]
#[inline(always)]
fn strip_pac(addr: usize) -> usize {
pub(crate) fn strip_pac(addr: usize) -> usize {
addr
}

/// Walk the frame-pointer chain starting from the given (pc, fp, sp) triple,
/// usually obtained from a signal handler's ucontext.
///
/// Returns `(frames_written, truncated)`. `truncated` is `true` if the walk
/// stopped because the output buffer (or [`MAX_FRAMES`]) was full *and* at
/// least one additional frame would have been valid. A natural stop (end of
/// chain, faulty load, implausible pointer) produces `truncated = false`.
///
/// # Safety
/// - `install_handler` must have been called.
/// - Should generally be called from a signal handler where the target thread
/// is stopped; walking a running thread's stack races with mutations.
pub unsafe fn unwind(pc: usize, mut fp: usize, sp: usize, out: &mut [u64]) -> usize {
pub unsafe fn unwind(pc: usize, mut fp: usize, sp: usize, out: &mut [u64]) -> (usize, bool) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest a named struct return here as well

let limit = out.len().min(MAX_FRAMES);
if limit == 0 {
return 0;
// No room even for the interrupted PC. The walk would have produced
// at least one frame, so this is truncation.
return (0, true);
}

out[0] = pc as u64;
Expand All @@ -65,47 +72,54 @@ pub unsafe fn unwind(pc: usize, mut fp: usize, sp: usize, out: &mut [u64]) -> us
let stack_lo = sp;
let stack_hi = sp.saturating_add(8 * 1024 * 1024);

while n < limit {
loop {
// Validate current fp before reading from it.
if fp < stack_lo || fp >= stack_hi {
break;
return (n, false);
}
if fp & (core::mem::size_of::<usize>() - 1) != 0 {
break; // misaligned
return (n, false); // misaligned
}

let saved_fp = unsafe { load(fp as *const usize) };
if saved_fp == SAFE_LOAD_FAULT {
break;
return (n, false);
}
let ret_addr_slot = (fp + core::mem::size_of::<usize>()) as *const usize;
let ret_addr = strip_pac(unsafe { load(ret_addr_slot) });
if ret_addr == SAFE_LOAD_FAULT {
break;
return (n, false);
}

if !(DEAD_ZONE..=usize::MAX - DEAD_ZONE).contains(&ret_addr) {
break;
return (n, false);
}

// Frame pointer must advance (stacks grow down -> saved_fp > fp)
// but not by more than MAX_FRAME_SIZE.
if saved_fp <= fp || saved_fp - fp > MAX_FRAME_SIZE {
break;
return (n, false);
}

// We have a valid next frame. If we ran out of room to record it,
// the walk is truncated.
if n >= limit {
return (n, true);
}

out[n] = ret_addr as u64;
n += 1;
fp = saved_fp;
}

n
}

/// Unwind from inside a signal handler given the raw ucontext.
///
/// Returns `(frames_written, truncated)`; see [`unwind`] for details.
///
/// # Safety
/// `ucontext` must be the pointer the kernel passed to a SA_SIGINFO handler.
pub unsafe fn unwind_from_ucontext(ucontext: *mut libc::c_void, out: &mut [u64]) -> usize {
pub unsafe fn unwind_from_ucontext(ucontext: *mut libc::c_void, out: &mut [u64]) -> (usize, bool) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

complete NIT, but a bit hard to remember what a (usize, bool) means without reading docs, might be worth creating a type?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

related nit, can we call this pub(crate) if it is not re-exported? It's a bit hard to tell currently which is exported, I see that capture() is currently.

let (pc, fp, sp) = unsafe { read_pc_fp_sp(ucontext) };
unsafe { unwind(pc, fp, sp, out) }
}
Expand Down Expand Up @@ -161,9 +175,10 @@ mod tests {
stack[4] = base + 4 * sz;

let mut out = [0u64; MAX_FRAMES];
let n = unsafe { unwind(0x40_0000, base, base, &mut out) };
let (n, truncated) = unsafe { unwind(0x40_0000, base, base, &mut out) };

assert_eq!(n, 3);
assert!(!truncated);
assert_eq!(out[0], 0x40_0000); // interrupted PC
assert_eq!(out[1], 0x40_1000);
assert_eq!(out[2], 0x40_2000);
Expand All @@ -173,8 +188,9 @@ mod tests {
fn frame_zero_is_always_the_interrupted_pc() {
install();
let mut out = [0u64; MAX_FRAMES];
let n = unsafe { unwind(0xDEAD, 0, 0x1000, &mut out) };
let (n, truncated) = unsafe { unwind(0xDEAD, 0, 0x1000, &mut out) };
assert_eq!(n, 1);
assert!(!truncated);
assert_eq!(out[0], 0xDEAD);
}

Expand All @@ -183,17 +199,19 @@ mod tests {
install();
let mut out = [0u64; MAX_FRAMES];
let sp = 0x7fff_0000_0000usize;
let n = unsafe { unwind(0x40_0000, sp + 1, sp, &mut out) };
let (n, truncated) = unsafe { unwind(0x40_0000, sp + 1, sp, &mut out) };
assert_eq!(n, 1);
assert!(!truncated);
}

#[test]
fn stops_when_fp_below_stack() {
install();
let mut out = [0u64; MAX_FRAMES];
let sp = 0x7fff_0000_0000usize;
let n = unsafe { unwind(0x40_0000, sp.wrapping_sub(8), sp, &mut out) };
let (n, truncated) = unsafe { unwind(0x40_0000, sp.wrapping_sub(8), sp, &mut out) };
assert_eq!(n, 1);
assert!(!truncated);
}

#[test]
Expand All @@ -207,8 +225,9 @@ mod tests {
stack[1] = 0x100; // return addr in dead zone (< 0x1000)

let mut out = [0u64; MAX_FRAMES];
let n = unsafe { unwind(0x40_0000, base, base, &mut out) };
let (n, truncated) = unsafe { unwind(0x40_0000, base, base, &mut out) };
assert_eq!(n, 1);
assert!(!truncated);
}

#[test]
Expand All @@ -220,8 +239,9 @@ mod tests {
stack[0] = base + MAX_FRAME_SIZE + 8; // jump exceeds MAX_FRAME_SIZE

let mut out = [0u64; MAX_FRAMES];
let n = unsafe { unwind(0x40_0000, base, base, &mut out) };
let (n, truncated) = unsafe { unwind(0x40_0000, base, base, &mut out) };
assert_eq!(n, 1);
assert!(!truncated);
}

#[test]
Expand All @@ -233,19 +253,55 @@ mod tests {
stack[0] = base; // saved_fp == fp, doesn't advance

let mut out = [0u64; MAX_FRAMES];
let n = unsafe { unwind(0x40_0000, base, base, &mut out) };
let (n, truncated) = unsafe { unwind(0x40_0000, base, base, &mut out) };
assert_eq!(n, 1);
assert!(!truncated);
}

#[test]
fn respects_output_buffer_limit() {
install();
let mut out = [0u64; 1];
let n = unsafe { unwind(0x40_0000, 0, 0x1000, &mut out) };
let (n, _truncated) = unsafe { unwind(0x40_0000, 0, 0x1000, &mut out) };
assert_eq!(n, 1);
assert_eq!(out[0], 0x40_0000);
}

#[test]
fn reports_truncation_when_buffer_fills_before_chain_ends() {
install();
let sz = std::mem::size_of::<usize>();
let mut stack = [0usize; 8];
let base = stack.as_mut_ptr() as usize;

// Chain of 3 valid frames (same structure as `walks_valid_frame_chain`).
stack[0] = base + 2 * sz;
stack[1] = 0x40_1000;
stack[2] = base + 4 * sz;
stack[3] = 0x40_2000;
stack[4] = base + 4 * sz; // terminates naturally here

// Buffer fits only pc + 1 frame, so the 3rd frame is dropped.
let mut out = [0u64; 2];
let (n, truncated) = unsafe { unwind(0x40_0000, base, base, &mut out) };
assert_eq!(n, 2);
assert!(
truncated,
"should report truncation when output buffer fills"
);
assert_eq!(out[0], 0x40_0000);
assert_eq!(out[1], 0x40_1000);
}

#[test]
fn empty_buffer_reports_truncation() {
install();
let mut out: [u64; 0] = [];
let (n, truncated) = unsafe { unwind(0x40_0000, 0, 0x1000, &mut out) };
assert_eq!(n, 0);
assert!(truncated, "empty buffer can always hold more");
}

fn page_size() -> usize {
let ps = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
assert!(ps > 0, "sysconf(_SC_PAGESIZE) must return a positive value");
Expand All @@ -272,12 +328,13 @@ mod tests {

// sp = fp keeps fp in range, page-aligned.
let mut out = [0u64; MAX_FRAMES];
let n = unsafe { unwind(0x40_0000, fp, fp, &mut out) };
let (n, truncated) = unsafe { unwind(0x40_0000, fp, fp, &mut out) };

// Unmap before the asserts so a panic doesn't leak the mapping.
assert_eq!(unsafe { libc::munmap(guard, ps) }, 0);

assert_eq!(n, 1, "unwind must stop when saved_fp load faults");
assert!(!truncated);
assert_eq!(out[0], 0x40_0000);
}

Expand Down Expand Up @@ -310,12 +367,13 @@ mod tests {
unsafe { (fp as *mut usize).write(fp + 16) };

let mut out = [0u64; MAX_FRAMES];
let n = unsafe { unwind(0x40_0000, fp, fp, &mut out) };
let (n, truncated) = unsafe { unwind(0x40_0000, fp, fp, &mut out) };

// Unmap before the asserts so a panic doesn't leak the mapping.
assert_eq!(unsafe { libc::munmap(region, 2 * ps) }, 0);

assert_eq!(n, 1, "unwind must stop when ret_addr load faults");
assert!(!truncated);
assert_eq!(out[0], 0x40_0000);
}
}
Loading
Loading