Skip to main content

libdd_libunwind_sys/
lib.rs

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