1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/
// SPDX-License-Identifier: Apache-2.0
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
mod libunwind_x86_64;
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
mod libunwind_aarch64;
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
pub use libunwind_aarch64::*;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub use libunwind_x86_64::*;
#[cfg(all(test, target_os = "linux"))]
mod remote_tests {
use super::*;
/// Fork a child that stops itself with `PTRACE_TRACEME` + `SIGSTOP`.
/// The parent waits, unwinds the child's stack, then kills it.
#[test]
#[cfg_attr(miri, ignore)]
fn test_remote_unwind_child() {
unsafe {
let child_pid = libc::fork();
assert!(child_pid >= 0, "fork failed");
if child_pid == 0 {
libc::ptrace(
libc::PTRACE_TRACEME,
0,
std::ptr::null_mut::<libc::c_void>(),
std::ptr::null_mut::<libc::c_void>(),
);
libc::raise(libc::SIGSTOP);
libc::_exit(libc::EXIT_SUCCESS);
}
let mut status: libc::c_int = 0;
libc::waitpid(child_pid, &mut status, libc::WUNTRACED);
assert!(libc::WIFSTOPPED(status), "child did not stop");
let addr_space =
unw_create_addr_space(std::ptr::addr_of!(_UPT_accessors) as *mut UnwAccessors, 0);
let upt_info = _UPT_create(child_pid);
let mut cursor: UnwCursor = std::mem::zeroed();
let ret = unw_init_remote(&mut cursor, addr_space, upt_info);
assert_eq!(ret, 0, "unw_init_remote failed");
let mut frames = 0usize;
while frames <= 256 {
if unw_step_remote(&mut cursor) <= 0 {
break;
}
frames += 1;
let mut ip: UnwWord = 0;
unw_get_reg_remote(&mut cursor, UNW_REG_IP, &mut ip);
let mut name: [libc::c_char; 256] = [0; 256];
let mut offset: UnwWord = 0;
let sym =
if unw_get_proc_name_remote(&mut cursor, name.as_mut_ptr(), 256, &mut offset)
== 0
{
std::ffi::CStr::from_ptr(name.as_ptr())
.to_string_lossy()
.into_owned()
} else {
"<unknown>".to_owned()
};
println!(" frame {frames:3}: ip=0x{ip:016x} {sym}+0x{offset:x}");
}
assert!(frames > 0, "expected at least one remote frame");
_UPT_destroy(upt_info);
unw_destroy_addr_space(addr_space);
libc::kill(child_pid, libc::SIGKILL);
libc::waitpid(child_pid, std::ptr::null_mut(), 0);
}
}
/// We specifically test that the child process can unwind the parent's stack
/// because that is the use case in crashtracker
///
/// 1. Parent records its own pid and opens a pipe
/// 2. Parent forks; now it knows the child pid
/// 3. Parent calls `prctl(PR_SET_PTRACER, child_pid)` so the kernel
/// allows the child to attach (required when ptrace_scope >= 1)
/// 4. Parent writes one byte to the pipe then blocks in `waitpid`
/// 5. Child reads the byte, calls `PTRACE_ATTACH` on the parent, waits
/// for the parent to stop, unwinds its stack, asserts frames captured,
/// detaches, and exits
/// 6. Parent's `waitpid` returns; asserts child exited cleanly
#[test]
#[cfg_attr(miri, ignore)]
fn test_remote_child_ptrace_unwind() {
unsafe {
// Use the current thread's TID, not the process TGID. In the
// parallel test harness the test runs in a worker thread whose
// TID != getpid(); attaching to the TGID would be on the
// harness coordinator thread instead.
let parent_tid = libc::syscall(libc::SYS_gettid) as libc::pid_t;
let mut pipe_fds: [libc::c_int; 2] = [0; 2];
assert_eq!(libc::pipe(pipe_fds.as_mut_ptr()), 0);
let [pipe_r, pipe_w] = pipe_fds;
let child_pid = libc::fork();
assert!(child_pid >= 0, "fork failed");
if child_pid == 0 {
libc::close(pipe_w);
// Wait until the parent has called prctl.
let mut byte = 0u8;
libc::read(pipe_r, &mut byte as *mut u8 as *mut libc::c_void, 1);
libc::close(pipe_r);
// Attach to the parent thread and wait for it to stop.
// __WALL is required when the tracee is a thread (TID != TGID).
let ret = libc::ptrace(
libc::PTRACE_ATTACH,
parent_tid,
std::ptr::null_mut::<libc::c_void>(),
std::ptr::null_mut::<libc::c_void>(),
);
if ret != 0 {
libc::_exit(1);
}
let mut status: libc::c_int = 0;
libc::waitpid(parent_tid, &mut status, libc::__WALL);
if !libc::WIFSTOPPED(status) {
libc::ptrace(
libc::PTRACE_DETACH,
parent_tid,
std::ptr::null_mut::<libc::c_void>(),
std::ptr::null_mut::<libc::c_void>(),
);
libc::_exit(1);
}
// Walk the parent thread's stack.
let addr_space = unw_create_addr_space(
std::ptr::addr_of!(_UPT_accessors) as *mut UnwAccessors,
0,
);
let upt_info = _UPT_create(parent_tid);
let mut cursor: UnwCursor = std::mem::zeroed();
let ret = unw_init_remote(&mut cursor, addr_space, upt_info);
let mut frames = 0usize;
if ret == 0 {
while frames <= 256 {
if unw_step_remote(&mut cursor) <= 0 {
break;
}
frames += 1;
let mut ip: UnwWord = 0;
unw_get_reg_remote(&mut cursor, UNW_REG_IP, &mut ip);
let mut name: [libc::c_char; 256] = [0; 256];
let mut offset: UnwWord = 0;
let sym = if unw_get_proc_name_remote(
&mut cursor,
name.as_mut_ptr(),
256,
&mut offset,
) == 0
{
std::ffi::CStr::from_ptr(name.as_ptr())
.to_string_lossy()
.into_owned()
} else {
"<unknown>".to_owned()
};
// eprintln so it's visible from the forked child
eprintln!(" frame {frames:3}: ip=0x{ip:016x} {sym}+0x{offset:x}");
}
}
assert!(frames > 0, "Expected at least one remote frame");
_UPT_destroy(upt_info);
unw_destroy_addr_space(addr_space);
libc::ptrace(
libc::PTRACE_DETACH,
parent_tid,
std::ptr::null_mut::<libc::c_void>(),
std::ptr::null_mut::<libc::c_void>(),
);
libc::_exit(if frames > 0 { 0 } else { 1 });
}
// Parent grants ptrace permission to child, then signals it
libc::close(pipe_r);
libc::prctl(libc::PR_SET_PTRACER, child_pid as libc::c_ulong, 0, 0, 0);
libc::write(pipe_w, b"g".as_ptr() as *const libc::c_void, 1);
libc::close(pipe_w);
// the child will stop us, unwind our stack, then detach
let mut status: libc::c_int = 0;
libc::waitpid(child_pid, &mut status, 0);
assert!(
libc::WIFEXITED(status) && libc::WEXITSTATUS(status) == 0,
"child failed: status={status}"
);
}
}
}
#[cfg(all(test, target_os = "linux"))]
mod tests {
use super::*;
#[test]
#[cfg_attr(miri, ignore)] // Miri cannot execute FFI calls to libunwind
fn test_basic_unwind() {
unsafe {
let mut context: UnwContext = std::mem::zeroed();
let ret = unw_getcontext(&mut context);
assert_eq!(ret, 0, "unw_getcontext failed");
// Initialize cursor
let mut cursor: UnwCursor = std::mem::zeroed();
let ret = unw_init_local2(&mut cursor, &mut context, 0);
assert_eq!(ret, 0, "unw_init_local2 failed");
// Walk the stack
let mut frames = 0;
loop {
let ret = unw_step(&mut cursor);
if ret <= 0 {
break;
}
frames += 1;
// Limit iterations to prevent infinite loops
if frames > 100 {
break;
}
}
// Should have at least a few frames
assert!(frames > 0, "Expected at least one stack frame");
}
}
#[test]
#[cfg_attr(miri, ignore)] // Miri cannot execute FFI calls to libunwind
fn test_get_register() {
unsafe {
let mut context: UnwContext = std::mem::zeroed();
assert_eq!(unw_getcontext(&mut context), 0);
let mut cursor: UnwCursor = std::mem::zeroed();
assert_eq!(unw_init_local2(&mut cursor, &mut context, 0), 0);
// Get instruction pointer
let mut ip: UnwWord = 0;
let ret = unw_get_reg(&mut cursor, UNW_REG_IP, &mut ip);
assert_eq!(ret, 0, "Failed to get IP register");
assert_ne!(ip, 0, "IP should not be zero");
// Get stack pointer
let mut sp: UnwWord = 0;
let ret = unw_get_reg(&mut cursor, UNW_REG_SP, &mut sp);
assert_eq!(ret, 0, "Failed to get SP register");
assert_ne!(sp, 0, "SP should not be zero");
}
}
#[test]
#[cfg_attr(miri, ignore)] // Miri cannot execute FFI calls to libunwind
fn test_backtrace2() {
unsafe {
let mut context: UnwContext = std::mem::zeroed();
assert_eq!(unw_getcontext(&mut context), 0);
let mut cursor: UnwCursor = std::mem::zeroed();
assert_eq!(unw_init_local2(&mut cursor, &mut context, 0), 0);
// unw_backtrace2 expects an array of void pointers
let mut frames: [*mut ::std::os::raw::c_void; 100] = [std::ptr::null_mut(); 100];
let ret = unw_backtrace2(frames.as_mut_ptr(), 100, &mut context, 0);
// Return value should be >= 0 (number of frames captured)
assert!(ret >= 0, "unw_backtrace2 failed with error: {}", ret);
let frame_count = ret as usize;
assert!(frame_count > 0, "Expected at least one frame");
// Print captured frames
for (i, &frame) in frames.iter().enumerate().take(frame_count) {
let frame_ptr = frame as usize;
println!("Frame {}: 0x{:016x}", i, frame_ptr);
}
}
}
#[test]
#[cfg_attr(miri, ignore)] // Miri cannot execute FFI calls to libunwind
fn test_get_proc_name() {
unsafe {
let mut context: UnwContext = std::mem::zeroed();
assert_eq!(unw_getcontext(&mut context), 0);
let mut cursor: UnwCursor = std::mem::zeroed();
assert_eq!(
unw_init_local2(&mut cursor, &mut context, UNW_INIT_SIGNAL_FRAME),
0
);
let mut name: [libc::c_char; 100] = [0; 100];
let ret = unw_get_proc_name(&mut cursor, name.as_mut_ptr(), 100, std::ptr::null_mut());
assert_eq!(ret, 0, "unw_get_proc_name failed");
let fn_name = std::ffi::CStr::from_ptr(name.as_ptr()).to_string_lossy();
assert!(!fn_name.is_empty(), "Name should not be empty");
// name is managed: _ZN15libdd_libunwind5tests18test_get_proc_name17hec15ec5ad6978a00E
// we should just chekc that test_get_proc_name is part of it
assert!(
fn_name.contains("test_get_proc_name"),
"Name should contain 'test_get_proc_name'"
);
}
}
}