rstack/
lib.rs

1//! Thread stack traces of remote processes.
2//!
3//! `rstack` (named after Java's `jstack`) uses ptrace to capture stack traces of the threads of a remote process. It
4//! currently only supports Linux, and requires that the `/proc` pseudo-filesystem be mounted and accessible. Multiple
5//! unwinding implementations are supported via Cargo features:
6//!
7//! * `unwind`: Uses [libunwind].
8//! * `dw`: Uses libdw, part of the [elfutils] project.
9//!
10//! By default, the libunwind backend is used. You can switch to libdw via Cargo:
11//!
12//! ```toml
13//! [dependencies]
14//! rstack = { version = "0.1", features = ["dw"], default-features = false }
15//! ```
16//!
17//! [libunwind]: http://www.nongnu.org/libunwind/
18//! [elfutils]: https://sourceware.org/elfutils/
19#![doc(html_root_url = "https://sfackler.github.io/rstack/doc")]
20#![warn(missing_docs)]
21
22use cfg_if::cfg_if;
23use libc::{
24    c_void, pid_t, ptrace, waitpid, ESRCH, PTRACE_ATTACH, PTRACE_CONT, PTRACE_DETACH,
25    PTRACE_INTERRUPT, PTRACE_SEIZE, SIGSTOP, WIFSTOPPED, WSTOPSIG, __WALL,
26};
27use log::debug;
28use std::borrow::Borrow;
29use std::cmp::Ordering;
30use std::collections::BTreeSet;
31use std::error;
32use std::fmt;
33use std::fs::{self, File};
34use std::io::{self, Read};
35use std::ptr;
36use std::result;
37
38cfg_if! {
39    if #[cfg(feature = "dw")] {
40        #[path = "imp/dw.rs"]
41        mod imp;
42    } else if #[cfg(feature = "unwind")] {
43        #[path = "imp/unwind.rs"]
44        mod imp;
45    } else {
46        compile_error!("You must select an unwinding implementation");
47    }
48}
49
50/// The result type returned by methods in this crate.
51pub type Result<T> = result::Result<T, Error>;
52
53#[derive(Debug)]
54enum ErrorInner {
55    Io(io::Error),
56    Unwind(imp::Error),
57}
58
59/// The error type returned by methods in this crate.
60#[derive(Debug)]
61pub struct Error(ErrorInner);
62
63impl fmt::Display for Error {
64    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
65        match self.0 {
66            ErrorInner::Io(ref e) => fmt::Display::fmt(e, fmt),
67            ErrorInner::Unwind(ref e) => fmt::Display::fmt(e, fmt),
68        }
69    }
70}
71
72impl error::Error for Error {
73    fn cause(&self) -> Option<&dyn error::Error> {
74        match self.0 {
75            ErrorInner::Io(ref e) => Some(e),
76            ErrorInner::Unwind(ref e) => Some(e),
77        }
78    }
79}
80
81/// Information about a remote process.
82#[derive(Debug, Clone)]
83pub struct Process {
84    id: u32,
85    threads: Vec<Thread>,
86}
87
88impl Process {
89    /// Returns the process's ID.
90    pub fn id(&self) -> u32 {
91        self.id
92    }
93
94    /// Returns information about the threads of the process.
95    pub fn threads(&self) -> &[Thread] {
96        &self.threads
97    }
98}
99
100/// Information about a thread of a remote process.
101#[derive(Debug, Clone)]
102pub struct Thread {
103    id: u32,
104    name: Option<String>,
105    frames: Vec<Frame>,
106}
107
108impl Thread {
109    /// Returns the thread's ID.
110    #[inline]
111    pub fn id(&self) -> u32 {
112        self.id
113    }
114
115    /// Returns the thread's name, if known.
116    #[inline]
117    pub fn name(&self) -> Option<&str> {
118        self.name.as_ref().map(|s| &**s)
119    }
120
121    /// Returns the frames of the stack trace representing the state of the thread.
122    #[inline]
123    pub fn frames(&self) -> &[Frame] {
124        &self.frames
125    }
126}
127
128/// Information about a stack frame of a remote process.
129#[derive(Debug, Clone)]
130pub struct Frame {
131    ip: u64,
132    is_signal: bool,
133    symbol: Option<Symbol>,
134}
135
136impl Frame {
137    /// Returns the instruction pointer of the frame.
138    #[inline]
139    pub fn ip(&self) -> u64 {
140        self.ip
141    }
142
143    /// Determines if the frame is from a signal handler.
144    #[inline]
145    pub fn is_signal(&self) -> bool {
146        self.is_signal
147    }
148
149    /// Returns information about the symbol corresponding to this frame's instruction pointer, if known.
150    #[inline]
151    pub fn symbol(&self) -> Option<&Symbol> {
152        self.symbol.as_ref()
153    }
154}
155
156/// Information about the symbol corresponding to a stack frame.
157#[derive(Debug, Clone)]
158pub struct Symbol {
159    name: String,
160    offset: u64,
161    address: u64,
162    size: u64,
163}
164
165impl Symbol {
166    /// Returns the name of the procedure.
167    #[inline]
168    pub fn name(&self) -> &str {
169        &self.name
170    }
171
172    /// Returns the offset of the instruction pointer from the symbol's starting address.
173    #[inline]
174    pub fn offset(&self) -> u64 {
175        self.offset
176    }
177
178    /// Returns the starting address of the symbol.
179    #[inline]
180    pub fn address(&self) -> u64 {
181        self.address
182    }
183
184    /// Returns the size of the symbol.
185    #[inline]
186    pub fn size(&self) -> u64 {
187        self.size
188    }
189}
190
191/// A convenience wrapper over `TraceOptions` which returns a maximally verbose trace.
192pub fn trace(pid: u32) -> Result<Process> {
193    TraceOptions::new()
194        .thread_names(true)
195        .symbols(true)
196        .trace(pid)
197}
198
199/// Options controlling the behavior of tracing.
200#[derive(Debug, Clone)]
201pub struct TraceOptions {
202    snapshot: bool,
203    thread_names: bool,
204    symbols: bool,
205    ptrace_attach: bool,
206}
207
208impl Default for TraceOptions {
209    fn default() -> TraceOptions {
210        TraceOptions {
211            snapshot: false,
212            thread_names: false,
213            symbols: false,
214            ptrace_attach: true,
215        }
216    }
217}
218
219impl TraceOptions {
220    /// Returns a new `TraceOptions` with default settings.
221    pub fn new() -> TraceOptions {
222        TraceOptions::default()
223    }
224
225    /// If set, the threads of the process will be traced in a consistent snapshot.
226    ///
227    /// A snapshot-mode trace ensures a consistent view of all threads, but requires that all
228    /// threads be paused for the entire duration of the trace.
229    ///
230    /// Defaults to `false`.
231    pub fn snapshot(&mut self, snapshot: bool) -> &mut TraceOptions {
232        self.snapshot = snapshot;
233        self
234    }
235
236    /// If set, the names of the process's threads will be recorded.
237    ///
238    /// Defaults to `false`.
239    pub fn thread_names(&mut self, thread_names: bool) -> &mut TraceOptions {
240        self.thread_names = thread_names;
241        self
242    }
243
244    /// If set, information about the symbol at each frame will be recorded.
245    ///
246    /// Defaults to `false`.
247    pub fn symbols(&mut self, symbols: bool) -> &mut TraceOptions {
248        self.symbols = symbols;
249        self
250    }
251
252    /// If set, `rstack` will automatically attach to threads via ptrace.
253    ///
254    /// If disabled, the calling process must already be attached to all traced threads, and the
255    /// threads must be in the stopped state.
256    ///
257    /// Defaults to `true`.
258    pub fn ptrace_attach(&mut self, ptrace_attach: bool) -> &mut TraceOptions {
259        self.ptrace_attach = ptrace_attach;
260        self
261    }
262
263    /// Traces the threads of the specified process.
264    pub fn trace(&self, pid: u32) -> Result<Process> {
265        let mut state = imp::State::new(pid).map_err(|e| Error(ErrorInner::Unwind(e)))?;
266
267        let threads = if self.snapshot {
268            self.trace_snapshot(pid, &mut state)?
269        } else {
270            self.trace_rolling(pid, &mut state)?
271        };
272
273        Ok(Process { id: pid, threads })
274    }
275
276    fn trace_snapshot(&self, pid: u32, state: &mut imp::State) -> Result<Vec<Thread>> {
277        let threads = snapshot_threads(pid, self.ptrace_attach)?
278            .iter()
279            .map(|t| t.info(pid, state, self))
280            .collect();
281
282        Ok(threads)
283    }
284
285    fn trace_rolling(&self, pid: u32, state: &mut imp::State) -> Result<Vec<Thread>> {
286        let mut threads = vec![];
287
288        each_thread(pid, |tid| {
289            let thread = if self.ptrace_attach {
290                TracedThread::attach(tid)
291            } else {
292                TracedThread::traced(tid)
293            };
294            let thread = match thread {
295                Ok(thread) => thread,
296                Err(ref e) if e.raw_os_error() == Some(ESRCH) => {
297                    debug!("error attaching to thread {}: {}", tid, e);
298                    return Ok(());
299                }
300                Err(e) => return Err(Error(ErrorInner::Io(e))),
301            };
302
303            let trace = thread.info(pid, state, self);
304            threads.push(trace);
305            Ok(())
306        })?;
307
308        Ok(threads)
309    }
310}
311
312fn snapshot_threads(pid: u32, ptrace_attach: bool) -> Result<BTreeSet<TracedThread>> {
313    let mut threads = BTreeSet::new();
314
315    // new threads may be created while we're in the process of stopping them all, so loop a couple
316    // of times to hopefully converge
317    for _ in 0..5 {
318        let prev = threads.len();
319        add_threads(&mut threads, pid, ptrace_attach)?;
320        if prev == threads.len() {
321            break;
322        }
323    }
324
325    Ok(threads)
326}
327
328fn add_threads(threads: &mut BTreeSet<TracedThread>, pid: u32, ptrace_attach: bool) -> Result<()> {
329    each_thread(pid, |tid| {
330        if !threads.contains(&tid) {
331            let thread = if ptrace_attach {
332                TracedThread::attach(pid)
333            } else {
334                TracedThread::traced(pid)
335            };
336            let thread = match thread {
337                Ok(thread) => thread,
338                // ESRCH just means the thread died in the middle of things, which is fine
339                Err(e) => {
340                    if e.raw_os_error() == Some(ESRCH) {
341                        debug!("error attaching to thread {}: {}", pid, e);
342                        return Ok(());
343                    } else {
344                        return Err(Error(ErrorInner::Io(e)));
345                    }
346                }
347            };
348            threads.insert(thread);
349        }
350
351        Ok(())
352    })
353}
354
355fn each_thread<F>(pid: u32, mut f: F) -> Result<()>
356where
357    F: FnMut(u32) -> Result<()>,
358{
359    let dir = format!("/proc/{}/task", pid);
360    for entry in fs::read_dir(dir).map_err(|e| Error(ErrorInner::Io(e)))? {
361        let entry = entry.map_err(|e| Error(ErrorInner::Io(e)))?;
362
363        if let Some(tid) = entry
364            .file_name()
365            .to_str()
366            .and_then(|s| s.parse::<u32>().ok())
367        {
368            f(tid)?;
369        }
370    }
371    Ok(())
372}
373
374struct TracedThread {
375    id: u32,
376    // True if TraceOptions::ptrace_attach was true (default value)
377    // It means that Drop should perform detach
378    should_detach: bool,
379}
380
381impl Drop for TracedThread {
382    fn drop(&mut self) {
383        if self.should_detach {
384            unsafe {
385                ptrace(
386                    PTRACE_DETACH,
387                    self.id as pid_t,
388                    ptr::null_mut::<c_void>(),
389                    ptr::null_mut::<c_void>(),
390                );
391            }
392        }
393    }
394}
395
396// these need to be manually implemented to only work off of id so the borrow impl works
397impl PartialOrd for TracedThread {
398    fn partial_cmp(&self, other: &TracedThread) -> Option<Ordering> {
399        Some(self.cmp(other))
400    }
401}
402
403impl Ord for TracedThread {
404    fn cmp(&self, other: &TracedThread) -> Ordering {
405        self.id.cmp(&other.id)
406    }
407}
408
409impl PartialEq for TracedThread {
410    fn eq(&self, other: &TracedThread) -> bool {
411        self.id == other.id
412    }
413}
414
415impl Eq for TracedThread {}
416
417impl Borrow<u32> for TracedThread {
418    fn borrow(&self) -> &u32 {
419        &self.id
420    }
421}
422
423impl TracedThread {
424    fn attach(pid: u32) -> io::Result<TracedThread> {
425        unsafe {
426            let ret = ptrace(
427                PTRACE_SEIZE,
428                pid as pid_t,
429                ptr::null_mut::<c_void>(),
430                ptr::null_mut::<c_void>(),
431            );
432            if ret != 0 {
433                let e = io::Error::last_os_error();
434                // ptrace returns ESRCH if PTRACE_SEIZE isn't supported for some reason
435                if e.raw_os_error() == Some(ESRCH as i32) {
436                    return TracedThread::new_fallback(pid);
437                }
438
439                return Err(e);
440            }
441
442            let thread = TracedThread {
443                id: pid,
444                should_detach: true,
445            };
446
447            let ret = ptrace(
448                PTRACE_INTERRUPT,
449                pid as pid_t,
450                ptr::null_mut::<c_void>(),
451                ptr::null_mut::<c_void>(),
452            );
453            if ret != 0 {
454                return Err(io::Error::last_os_error());
455            }
456
457            let mut status = 0;
458            while waitpid(pid as pid_t, &mut status, __WALL) < 0 {
459                let e = io::Error::last_os_error();
460                if e.kind() != io::ErrorKind::Interrupted {
461                    return Err(e);
462                }
463            }
464
465            if !WIFSTOPPED(status) {
466                return Err(io::Error::new(
467                    io::ErrorKind::Other,
468                    format!("unexpected wait status {}", status),
469                ));
470            }
471
472            Ok(thread)
473        }
474    }
475
476    /// Creates `TracedThread` without attaching to process. Should not be used, if pid is not
477    /// traced by current process
478    fn traced(pid: u32) -> io::Result<TracedThread> {
479        Ok(TracedThread {
480            id: pid,
481            should_detach: false,
482        })
483    }
484
485    fn new_fallback(pid: u32) -> io::Result<TracedThread> {
486        unsafe {
487            let ret = ptrace(
488                PTRACE_ATTACH,
489                pid as pid_t,
490                ptr::null_mut::<c_void>(),
491                ptr::null_mut::<c_void>(),
492            );
493            if ret != 0 {
494                return Err(io::Error::last_os_error());
495            }
496
497            let thread = TracedThread {
498                id: pid,
499                should_detach: true,
500            };
501
502            let mut status = 0;
503            loop {
504                let ret = waitpid(pid as pid_t, &mut status, __WALL);
505                if ret < 0 {
506                    let e = io::Error::last_os_error();
507                    if e.kind() != io::ErrorKind::Interrupted {
508                        return Err(e);
509                    }
510
511                    continue;
512                }
513
514                if !WIFSTOPPED(status) {
515                    return Err(io::Error::new(
516                        io::ErrorKind::Other,
517                        format!("unexpected wait status {}", status),
518                    ));
519                }
520
521                let sig = WSTOPSIG(status);
522                if sig == SIGSTOP {
523                    return Ok(thread);
524                }
525
526                let ret = ptrace(
527                    PTRACE_CONT,
528                    pid as pid_t,
529                    ptr::null_mut::<c_void>(),
530                    sig as *const c_void,
531                );
532                if ret != 0 {
533                    return Err(io::Error::last_os_error());
534                }
535            }
536        }
537    }
538
539    fn info(&self, pid: u32, state: &mut imp::State, options: &TraceOptions) -> Thread {
540        let name = if options.thread_names {
541            self.name(pid)
542        } else {
543            None
544        };
545
546        let frames = self.dump(state, options);
547
548        Thread {
549            id: self.id,
550            name,
551            frames,
552        }
553    }
554
555    fn dump(&self, state: &mut imp::State, options: &TraceOptions) -> Vec<Frame> {
556        let mut frames = vec![];
557
558        if let Err(e) = self.dump_inner(state, options, &mut frames) {
559            debug!("error tracing thread {}: {}", self.id, e);
560        }
561
562        frames
563    }
564
565    fn name(&self, pid: u32) -> Option<String> {
566        let path = format!("/proc/{}/task/{}/comm", pid, self.id);
567        let mut name = vec![];
568        match File::open(path).and_then(|mut f| f.read_to_end(&mut name)) {
569            Ok(_) => Some(String::from_utf8_lossy(&name).trim().to_string()),
570            Err(e) => {
571                debug!("error getting name for thread {}: {}", self.id, e);
572                None
573            }
574        }
575    }
576}