one_collect 0.1.34811

Cross-platform library for capturing machine-level traces.
Documentation
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
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

// This module was previously the standalone `ruwind` crate and is now an
// internal implementation detail of `one_collect`. Its items retain their
// original library-level visibility so the small public facade in
// `crate::unwind` can re-export the types that appear in this crate's public
// API. As an internal module, many of those `pub` items are no longer
// reachable outside the crate, and a few are only used on specific targets,
// so the corresponding lints are allowed here rather than churning the
// vendored code.
#![allow(unreachable_pub, dead_code)]

use std::collections::HashMap;
use std::collections::hash_map::Entry::{Vacant, Occupied};
use std::fs::File;
use std::hash::{Hash, Hasher};

pub mod elf;
pub mod dwarf;

mod module;
mod process;
mod machine;

pub trait Unwindable {
    fn find<'a>(
        &'a self,
        ip: u64) -> Option<&'a dyn CodeSection>;
}

pub trait CodeSection {
    fn anon(&self) -> bool;

    fn unwind_type(&self) -> UnwindType;

    fn rva(
        &self,
        ip: u64) -> u64;

    fn key(&self) -> ModuleKey;
}

#[derive(Eq, Copy)]
pub struct ModuleKey {
    pub dev: u64,
    pub ino: u64,
}

impl Hash for ModuleKey {
    fn hash<H: Hasher>(
        &self,
        state: &mut H) {
        self.dev.hash(state);
        self.ino.hash(state);
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum UnwindError {
    AnonPrologNotFound,
    RegisterOutOfRange,
    NoReturnAddressRegister,
    CfaWouldGoBackwards,
    BadStackRbpRead,
    BadStackIpRead,
    NoModuleFound,
    ProcessNotMapped,
}

impl UnwindError {
    pub fn as_str(self) -> &'static str {
        match self {
            UnwindError::AnonPrologNotFound => "Anon prolog not found",
            UnwindError::RegisterOutOfRange => "Register out of range",
            UnwindError::NoReturnAddressRegister => "No return address register",
            UnwindError::CfaWouldGoBackwards => "CFA would go backwards",
            UnwindError::BadStackRbpRead => "Bad stack RBP read",
            UnwindError::BadStackIpRead => "Bad stack IP read",
            UnwindError::NoModuleFound => "No module found",
            UnwindError::ProcessNotMapped => "Process not mapped",
        }
    }
}

impl std::fmt::Display for UnwindError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.as_str())
    }
}

pub struct UnwindResult {
    pub frames_pushed: usize,
    pub error: Option<UnwindError>,
}

impl UnwindResult {
    pub fn new() -> Self {
        Self {
            frames_pushed: 0,
            error: None,
        }
    }
}

impl Default for UnwindResult {
    fn default() -> Self {
        Self::new()
    }
}

pub trait MachineUnwinder {
    fn reset(
        &mut self,
        rip: u64,
        rbp: u64,
        rsp: u64);

    fn unwind(
        &mut self,
        process: &dyn Unwindable,
        accessor: &dyn ModuleAccessor,
        stack_data: &[u8],
        stack_frames: &mut Vec<u64>,
        result: &mut UnwindResult);
}

pub trait ModuleAccessor {
    fn open(
        &self,
        key: &ModuleKey) -> Option<File>;
}

#[derive(Debug, Eq, Clone, Copy, PartialEq)]
pub enum UnwindType {
    DWARF,
    Prolog,
}

#[derive(Eq, Clone, Copy)]
pub struct Module {
    start: u64,
    end: u64,
    offset: u64,
    va_offset: u64,
    key: ModuleKey,
    anon: bool,
    unwind_type: UnwindType,
}

#[derive(Default)]
pub struct Process {
    mods: Vec<Module>,
    sorted: bool,
}

#[derive(Default)]
pub struct Machine {
    processes: HashMap<u32, Process>,
}

#[cfg(target_arch = "x86_64")]
pub fn default_unwinder() -> impl MachineUnwinder {
    #[path = "x64unwinder.rs"]
    mod unwinder;
    unwinder::Unwinder::new()
}

#[cfg(test)]
#[cfg(target_arch = "x86_64")]
mod tests {
    use super::*;
    use std::fs::{self, File};

    struct SingleAccessor {
    }

    impl ModuleAccessor for SingleAccessor {
        fn open(
            &self,
            _key: &ModuleKey) -> Option<File> {
            match File::open("test_assets/test") {
                Ok(file) => { Some(file) },
                Err(_) => { None },
            }
        }
    }

    #[test]
    fn it_works() {
        let mut unwinder = default_unwinder();
        let mut machine = Machine::new();

        /* Pull these from stack_gen program */
        let rip: u64 = 0x5601ed65766d;
        let rsp: u64 = 0x7ffeee363070;
        let rbp: u64 = 0x7ffeee363090;
        let start: u64 = 0x5601ed657000;
        let end: u64 = 0x5601ed658000;
        let off: u64 = 0x1000;

        let accessor = SingleAccessor {};
        let mut proc = Process::new();
        let module = Module::new(start, end, off, 0, 0, 0, UnwindType::DWARF);
        let stack_data = fs::read("test_assets/test.data").unwrap();
        let mut stack_frames: Vec<u64> = Vec::new();

        proc.add_module(module);
        assert!(machine.add_process(0, proc));

        let result = machine.unwind_process(
            0,
            &mut unwinder,
            &accessor,
            rip,
            rbp,
            rsp,
            &stack_data[..],
            &mut stack_frames);

        println!("Got {} frames:", result.frames_pushed);

        for ip in stack_frames {
            println!("0x{:X}", ip);
        }

        if let Some(error) = result.error {
            println!("Error: {}", error);
        }

        assert!(machine.remove_process(0));
    }

    /*
     * The following tests cover the prolog/scan/fallback paths added
     * to the x86_64 unwinder for issue #255 without requiring real
     * .eh_frame data:
     *
     *   - prolog_rbp_chain_finds_return_address: the RBP-chain walk
     *     in `unwind_prolog` recognises a well-formed `[rbp]/[rbp+8]`
     *     pair and returns the saved return address.
     *
     *   - prolog_skips_one_chain_link_to_find_caller: the chain walk
     *     can skip a frame whose `[rbp+8]` is not a valid code address
     *     and follow `[rbp]` to the next link.
     *
     *   - prolog_scan_finds_return_address_when_chain_invalid: when
     *     the RBP chain is corrupt (bad alignment) the linear scan
     *     finds the (saved_rsp, return_addr) pair.
     *
     *   - prolog_scan_exhausted_returns_no_frame: when no plausible
     *     pair exists in the captured stack the unwinder gives up
     *     cleanly.
     *
     *   - dwarf_lookup_miss_falls_back_to_prolog_walk: a DWARF module
     *     whose accessor cannot supply an ELF file (so FDE lookup
     *     fails with "No module found") triggers the prolog walk
     *     fallback so unwinding still progresses.
     *
     *   - scan_recovery_when_unwound_ip_is_outside_any_module: when
     *     the prolog walk produces a return address that lies outside
     *     any registered module the loop terminates without trying to
     *     dereference further frames (no infinite walk, no panic).
     */

    struct NoFileAccessor;

    impl ModuleAccessor for NoFileAccessor {
        fn open(
            &self,
            _key: &ModuleKey) -> Option<File> {
            None
        }
    }

    /// Builds a stack buffer that begins at `rsp` and is `len` bytes long.
    /// `writes` is a list of `(stack_addr, value)` pairs to place at
    /// the chosen stack addresses (each value is written as an 8-byte
    /// little-endian u64).
    fn build_stack(
        rsp: u64,
        len: usize,
        writes: &[(u64, u64)]) -> Vec<u8> {
        let mut data = vec![0u8; len];

        for (addr, value) in writes {
            assert!(*addr >= rsp,
                "stack write addr {:#x} below rsp {:#x}", addr, rsp);
            let offset = (*addr - rsp) as usize;
            assert!(offset + 8 <= len,
                "stack write at {:#x} exceeds stack buffer", addr);
            data[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
        }

        data
    }

    /// Helper to drive a single unwind through the public API.
    fn run_unwind(
        proc: Process,
        rip: u64,
        rbp: u64,
        rsp: u64,
        stack_data: &[u8]) -> (UnwindResult, Vec<u64>) {
        let mut unwinder = default_unwinder();
        let mut machine = Machine::new();
        let accessor = NoFileAccessor;
        let mut stack_frames: Vec<u64> = Vec::new();

        assert!(machine.add_process(1, proc));
        let result = machine.unwind_process(
            1,
            &mut unwinder,
            &accessor,
            rip,
            rbp,
            rsp,
            stack_data,
            &mut stack_frames);
        assert!(machine.remove_process(1));

        (result, stack_frames)
    }

    #[test]
    fn prolog_rbp_chain_finds_return_address() {
        /* Single anonymous (Prolog-style) module covers both the
         * starting IP and the return address we expect to recover. */
        let module_start: u64 = 0x4000_0000;
        let module_end:   u64 = 0x4000_1000;
        let rip:          u64 = 0x4000_0500;
        let return_addr:  u64 = 0x4000_0700;

        let rsp: u64 = 0x7000_0000;
        let rbp: u64 = 0x7000_0040;
        let saved_rbp: u64 = 0x7000_0080;

        /* [rbp] = saved_rbp ; [rbp+8] = return_addr */
        let stack = build_stack(
            rsp,
            512,
            &[(rbp, saved_rbp), (rbp + 8, return_addr)]);

        let mut proc = Process::new();
        proc.add_module(Module::new_anon(module_start, module_end));

        let (result, frames) = run_unwind(proc, rip, rbp, rsp, &stack);

        /* Initial IP plus at least one unwound frame containing the
         * return address; the trailing frame is popped by the unwind
         * loop's "stopped" cleanup which is why we don't check the
         * very last entry. */
        assert!(
            frames.contains(&return_addr),
            "expected return_addr {:#x} in frames {:?}", return_addr, frames);
        assert!(result.frames_pushed >= 2);
    }

    #[test]
    fn prolog_skips_one_chain_link_to_find_caller() {
        /* The chain walker should follow [rbp] when [rbp+8] is junk
         * and stop at the first link whose [rbp+8] is a valid IP. */
        let module_start: u64 = 0x4000_0000;
        let module_end:   u64 = 0x4000_1000;
        let rip:          u64 = 0x4000_0500;
        let return_addr:  u64 = 0x4000_0900;
        let bogus_ra:     u64 = 0xdead_beef_dead_beef;

        let rsp:        u64 = 0x7000_0000;
        let rbp:        u64 = 0x7000_0040;
        let next_rbp:   u64 = 0x7000_0080;
        let final_rbp:  u64 = 0x7000_00c0;

        let stack = build_stack(
            rsp,
            512,
            &[
                (rbp,         next_rbp),    /* link 1: saved rbp */
                (rbp + 8,     bogus_ra),    /* link 1: junk return addr */
                (next_rbp,    final_rbp),   /* link 2: saved rbp */
                (next_rbp + 8, return_addr),/* link 2: real return addr */
            ]);

        let mut proc = Process::new();
        proc.add_module(Module::new_anon(module_start, module_end));

        let (_result, frames) = run_unwind(proc, rip, rbp, rsp, &stack);

        assert!(
            frames.contains(&return_addr),
            "expected return_addr {:#x} in frames {:?}", return_addr, frames);
        assert!(!frames.contains(&bogus_ra),
            "bogus_ra {:#x} should not have been pushed: frames {:?}",
            bogus_ra, frames);
    }

    #[test]
    fn prolog_scan_finds_return_address_when_chain_invalid() {
        /* Misalign rbp so the chain walker rejects it (alignment guard);
         * place a (cfa, ip) scan pair further up the stack. */
        let module_start: u64 = 0x4000_0000;
        let module_end:   u64 = 0x4000_1000;
        let rip:          u64 = 0x4000_0500;
        let return_addr:  u64 = 0x4000_0a00;

        let rsp:    u64 = 0x7000_0000;
        let rbp:    u64 = 0x7000_0041; /* misaligned, breaks chain walk */
        let new_rsp: u64 = 0x7000_0100;

        /* The scan looks for first > cfa && first <= cfa + len, then
         * checks that the following slot is a valid IP. cfa equals rsp
         * here because reset() seeds REG_RSP from rsp. */
        let stack = build_stack(
            rsp,
            512,
            &[(rsp + 0x40, new_rsp), (rsp + 0x48, return_addr)]);

        let mut proc = Process::new();
        proc.add_module(Module::new_anon(module_start, module_end));

        let (_result, frames) = run_unwind(proc, rip, rbp, rsp, &stack);

        assert!(
            frames.contains(&return_addr),
            "expected return_addr {:#x} in frames {:?}", return_addr, frames);
    }

    #[test]
    fn prolog_scan_exhausted_returns_no_frame() {
        /* Stack contains nothing that looks like a (cfa, ip) pair.
         * The unwinder must terminate without panicking and without
         * pushing any spurious frames beyond the initial IP. */
        let module_start: u64 = 0x4000_0000;
        let module_end:   u64 = 0x4000_1000;
        let rip:          u64 = 0x4000_0500;

        let rsp: u64 = 0x7000_0000;
        let rbp: u64 = 0x7000_0041; /* misaligned to defeat chain walk */

        let stack = vec![0u8; 512];

        let mut proc = Process::new();
        proc.add_module(Module::new_anon(module_start, module_end));

        let (result, frames) = run_unwind(proc, rip, rbp, rsp, &stack);

        /* Only the initial IP is recorded; nothing else is recoverable. */
        assert_eq!(frames, vec![rip]);
        assert_eq!(result.frames_pushed, 1);
    }

    #[test]
    fn dwarf_lookup_miss_falls_back_to_prolog_walk() {
        /* A DWARF-typed module whose backing file cannot be opened
         * (NoFileAccessor) triggers the "No module found" path in
         * unwind_module; the loop then falls back to the prolog walk,
         * which can recover the return address from the RBP chain. */
        let module_start: u64 = 0x4000_0000;
        let module_end:   u64 = 0x4000_1000;
        let rip:          u64 = 0x4000_0500;
        let return_addr:  u64 = 0x4000_0700;

        let rsp:        u64 = 0x7000_0000;
        let rbp:        u64 = 0x7000_0040;
        let saved_rbp:  u64 = 0x7000_0080;

        let stack = build_stack(
            rsp,
            512,
            &[(rbp, saved_rbp), (rbp + 8, return_addr)]);

        let mut proc = Process::new();
        proc.add_module(Module::new(
            module_start,
            module_end,
            0,
            0,
            42,
            42,
            UnwindType::DWARF));

        let (_result, frames) = run_unwind(proc, rip, rbp, rsp, &stack);

        assert!(
            frames.contains(&return_addr),
            "expected return_addr {:#x} in frames {:?}", return_addr, frames);
    }

    #[test]
    fn scan_recovery_when_unwound_ip_is_outside_any_module() {
        /* The prolog walker is given an [rbp+8] value that is NOT
         * inside any registered module, so the walk fails. The unwind
         * loop must terminate cleanly with just the initial IP. */
        let module_start: u64 = 0x4000_0000;
        let module_end:   u64 = 0x4000_1000;
        let rip:          u64 = 0x4000_0500;
        let bogus_ip:     u64 = 0xdead_beef_dead_beef;

        let rsp:       u64 = 0x7000_0000;
        let rbp:       u64 = 0x7000_0040;
        let saved_rbp: u64 = 0x7000_0080;

        let stack = build_stack(
            rsp,
            512,
            &[(rbp, saved_rbp), (rbp + 8, bogus_ip)]);

        let mut proc = Process::new();
        proc.add_module(Module::new_anon(module_start, module_end));

        let (result, frames) = run_unwind(proc, rip, rbp, rsp, &stack);

        assert_eq!(frames, vec![rip]);
        assert_eq!(result.frames_pushed, 1);
        assert!(!frames.contains(&bogus_ip));
    }
}