-
Notifications
You must be signed in to change notification settings - Fork 25
add: expose public Unwinder::capture API #396
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
d0b0380
8447563
fde162f
e82a886
e352175
250735d
945f1b9
11be733
75d82d6
592656c
353e5ed
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) { | ||
| 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; | ||
|
|
@@ -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) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. complete NIT, but a bit hard to remember what a
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| let (pc, fp, sp) = unsafe { read_pc_fp_sp(ucontext) }; | ||
| unsafe { unwind(pc, fp, sp, out) } | ||
| } | ||
|
|
@@ -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); | ||
|
|
@@ -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); | ||
| } | ||
|
|
||
|
|
@@ -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] | ||
|
|
@@ -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] | ||
|
|
@@ -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] | ||
|
|
@@ -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"); | ||
|
|
@@ -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); | ||
| } | ||
|
|
||
|
|
@@ -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); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
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