• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

xd009642 / tarpaulin / #680

19 Oct 2025 02:09PM UTC coverage: 75.356% (-0.2%) from 75.524%
#680

push

xd009642
Release 0.34.0

68 of 83 new or added lines in 7 files covered. (81.93%)

1 existing line in 1 file now uncovered.

3920 of 5202 relevant lines covered (75.36%)

131250.96 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

50.11
/src/statemachine/linux.rs
1
use crate::breakpoint::*;
2
use crate::cargo::rust_flags;
3
use crate::config::Config;
4
use crate::errors::RunError;
5
use crate::generate_tracemap;
6
use crate::ptrace_control::*;
7
use crate::source_analysis::LineAnalysis;
8
use crate::statemachine::*;
9
use crate::TestHandle;
10
use nix::errno::Errno;
11
use nix::sys::signal::Signal;
12
use nix::sys::wait::*;
13
use nix::unistd::Pid;
14
use nix::Error as NixErr;
15
use procfs::process::{MMapPath, Process};
16
use std::collections::{HashMap, HashSet};
17
use std::ops::RangeBounds;
18
use std::path::PathBuf;
19
use tracing::{debug, info, trace, trace_span, warn};
20

21
/// Handle to linux process state
22
pub struct LinuxData<'a> {
23
    /// Recent results from waitpid to be handled by statemachine
24
    wait_queue: Vec<WaitStatus>,
25
    /// Pending action queue, this is for actions where we need to wait one cycle before we can
26
    /// apply them :sobs:
27
    pending_actions: Vec<TracerAction<ProcessInfo>>,
28
    /// Parent pid of the test
29
    parent: Pid,
30
    /// Current Pid to process
31
    current: Pid,
32
    /// Program config
33
    config: &'a Config,
34
    /// Optional event log to update as the test progresses
35
    event_log: &'a Option<EventLog>,
36
    /// Instrumentation points in code with associated coverage data
37
    traces: &'a mut TraceMap,
38
    /// Source analysis, needed in case we need to follow any executables
39
    analysis: &'a HashMap<PathBuf, LineAnalysis>,
40
    /// Processes we're tracing
41
    processes: HashMap<Pid, TracedProcess>,
42
    /// Map from pids to their parent
43
    pid_map: HashMap<Pid, Pid>,
44
    /// So if we have the exit code but we're also waiting for all the spawned processes to end
45
    exit_code: Option<i32>,
46
}
47

48
#[derive(Debug)]
49
pub struct TracedProcess {
50
    /// Map of addresses to breakpoints
51
    breakpoints: HashMap<u64, Breakpoint>,
52
    /// Thread count. Hopefully getting rid of in future
53
    thread_count: isize,
54
    /// Breakpoint offset
55
    offset: u64,
56
    /// Instrumentation points in code with associated coverage data
57
    /// If this is the root tracemap we don't use it...
58
    traces: Option<TraceMap>,
59
    /// Parent pid of the process
60
    parent: Pid,
61
    /// Whether the process is part of the test binary, or the result of an exec or fork
62
    is_test_proc: bool,
63
}
64

65
pub fn create_state_machine<'a>(
184✔
66
    test: impl Into<TestHandle>,
67
    traces: &'a mut TraceMap,
68
    source_analysis: &'a HashMap<PathBuf, LineAnalysis>,
69
    config: &'a Config,
70
    event_log: &'a Option<EventLog>,
71
) -> (TestState, LinuxData<'a>) {
72
    let mut data = LinuxData::new(traces, source_analysis, config, event_log);
1,104✔
73
    let handle = test.into();
552✔
74
    match handle {
184✔
75
        TestHandle::Id(test) => {
184✔
76
            data.parent = test;
×
77
        }
78
        _ => unreachable!("Test handle must be a PID for ptrace engine"),
79
    }
80
    (TestState::start_state(), data)
×
81
}
82

83
pub type UpdateContext = (TestState, TracerAction<ProcessInfo>);
84

85
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
86
pub struct ProcessInfo {
87
    pub(crate) pid: Pid,
88
    pub(crate) signal: Option<Signal>,
89
}
90

91
impl ProcessInfo {
92
    fn new(pid: Pid, signal: Option<Signal>) -> Self {
3,732✔
93
        Self { pid, signal }
94
    }
95
}
96

97
impl From<Pid> for ProcessInfo {
98
    fn from(pid: Pid) -> Self {
3,502✔
99
        ProcessInfo::new(pid, None)
10,506✔
100
    }
101
}
102

103
impl From<&Pid> for ProcessInfo {
104
    fn from(pid: &Pid) -> Self {
230✔
105
        ProcessInfo::new(*pid, None)
690✔
106
    }
107
}
108

109
fn get_offset(pid: Pid, config: &Config) -> u64 {
192✔
110
    // TODO I don't think this would be an issue... but I'll test it
111
    if rust_flags(config, &Default::default()).contains("dynamic-no-pic") {
384✔
UNCOV
112
        0
×
113
    } else if let Ok(proc) = Process::new(pid.as_raw()) {
384✔
114
        let exe = proc.exe().ok();
×
115
        if let Ok(maps) = proc.maps() {
192✔
116
            let offset_info = maps.iter().find(|x| match (&x.pathname, exe.as_ref()) {
576✔
117
                (MMapPath::Path(p), Some(e)) => p == e,
576✔
118
                (MMapPath::Path(_), None) => true,
×
119
                _ => false,
×
120
            });
121
            if let Some(first) = offset_info {
192✔
122
                first.address.0
×
123
            } else {
124
                0
×
125
            }
126
        } else {
127
            0
×
128
        }
129
    } else {
130
        0
×
131
    }
132
}
133

134
impl<'a> StateData for LinuxData<'a> {
135
    fn start(&mut self) -> Result<Option<TestState>, RunError> {
184✔
136
        match waitpid(self.current, Some(WaitPidFlag::WNOHANG)) {
552✔
137
            Ok(WaitStatus::StillAlive) => Ok(None),
×
138
            Ok(sig @ WaitStatus::Stopped(_, Signal::SIGTRAP)) => {
184✔
139
                if let WaitStatus::Stopped(child, _) = sig {
184✔
140
                    self.current = child;
×
141
                }
142
                trace!("Caught inferior transitioning to Initialise state");
×
143
                Ok(Some(TestState::Initialise))
×
144
            }
145
            Ok(_) => Err(RunError::TestRuntime(
×
146
                "Unexpected signal when starting test".to_string(),
×
147
            )),
148
            Err(e) => Err(RunError::TestRuntime(format!(
×
149
                "Error when starting test: {e}"
×
150
            ))),
151
        }
152
    }
153

154
    fn init(&mut self) -> Result<TestState, RunError> {
184✔
155
        let mut traced_process = self.init_process(self.current, None)?;
920✔
156
        traced_process.is_test_proc = true;
×
157

158
        if continue_exec(traced_process.parent, None).is_ok() {
×
159
            trace!("Initialised inferior, transitioning to wait state");
184✔
160
            self.processes.insert(self.current, traced_process);
736✔
161
            Ok(TestState::wait_state())
184✔
162
        } else {
163
            Err(RunError::TestRuntime(
×
164
                "Test didn't launch correctly".to_string(),
×
165
            ))
166
        }
167
    }
168

169
    fn last_wait_attempt(&mut self) -> Result<Option<TestState>, RunError> {
×
170
        if let Some(ec) = self.exit_code {
×
171
            let parent = self.parent;
×
172
            for (_, process) in self.processes.iter().filter(|(k, _)| **k != parent) {
×
173
                if let Some(tm) = process.traces.as_ref() {
×
174
                    self.traces.merge(tm);
×
175
                }
176
            }
177
            Ok(Some(TestState::End(ec)))
×
178
        } else {
179
            Ok(None)
×
180
        }
181
    }
182

183
    fn wait(&mut self) -> Result<Option<TestState>, RunError> {
1,445,350✔
184
        let mut result = Ok(None);
2,890,700✔
185
        let mut running = true;
2,890,700✔
186
        while running {
2,893,558✔
187
            let wait = waitpid(
188
                Pid::from_raw(-1),
×
189
                Some(WaitPidFlag::WNOHANG | WaitPidFlag::__WALL),
×
190
            );
191
            match wait {
1,448,208✔
192
                Ok(WaitStatus::StillAlive) => {
1,444,308✔
193
                    running = false;
1,444,308✔
194
                }
195
                Ok(WaitStatus::Exited(_, _)) => {
408✔
196
                    self.wait_queue.push(wait.unwrap());
408✔
197
                    result = Ok(Some(TestState::Stopped));
408✔
198
                    running = false;
408✔
199
                }
200
                Ok(WaitStatus::PtraceEvent(_, _, _)) => {
634✔
201
                    self.wait_queue.push(wait.unwrap());
3,170✔
202
                    result = Ok(Some(TestState::Stopped));
1,268✔
203
                    running = false;
634✔
204
                }
205
                Ok(s) => {
5,716✔
206
                    self.wait_queue.push(s);
11,432✔
207
                    result = Ok(Some(TestState::Stopped));
2,858✔
208
                }
209
                Err(_) if self.exit_code.is_some() => {
×
210
                    running = false;
×
211
                    result = self.last_wait_attempt();
×
212
                }
213
                Err(e) => {
×
214
                    running = false;
×
215
                    result = Err(RunError::TestRuntime(format!(
×
216
                        "An error occurred while waiting for response from test: {e}"
×
217
                    )));
218
                }
219
            }
220
        }
221
        if !self.wait_queue.is_empty() {
1,445,350✔
222
            trace!("Result queue is {:?}", self.wait_queue);
3,578✔
223
        } else {
224
            self.apply_pending_actions(..);
2,883,544✔
225
        }
226
        result
1,445,350✔
227
    }
228

229
    fn stop(&mut self) -> Result<TestState, RunError> {
3,578✔
230
        let mut actions = Vec::new();
7,156✔
231
        let mut pcs = HashMap::new();
7,156✔
232
        let mut result = Ok(TestState::wait_state());
7,156✔
233
        let pending = self.wait_queue.clone();
10,734✔
234
        let pending_action_len = self.pending_actions.len();
10,734✔
235
        self.wait_queue.clear();
7,156✔
236
        for status in &pending {
11,376✔
237
            let span = trace_span!("pending", event=?status.pid());
7,800✔
238
            let _enter = span.enter();
11,700✔
239
            if let Some(log) = self.event_log.as_ref() {
3,900✔
240
                let offset = if let Some(process) = self.get_traced_process_mut(self.current) {
×
241
                    process.offset
×
242
                } else {
243
                    0
×
244
                };
245
                let traces = match self.get_active_trace_map(self.current) {
×
246
                    Some(tm) => tm,
×
247
                    None => self.traces,
×
248
                };
249
                let event = TraceEvent::new_from_wait(status, offset, traces);
×
250
                log.push_trace(event);
×
251
            }
252
            let state = match status {
6,756✔
253
                WaitStatus::PtraceEvent(c, s, e) => match self.handle_ptrace_event(*c, *s, *e) {
4,438✔
254
                    Ok(s) => Ok(s),
634✔
255
                    Err(e) => Err(RunError::TestRuntime(format!(
×
256
                        "Error occurred when handling ptrace event: {e}"
×
257
                    ))),
258
                },
259
                WaitStatus::Stopped(c, Signal::SIGTRAP) => {
2,628✔
260
                    self.current = *c;
2,628✔
261
                    match self.collect_coverage_data(&mut pcs) {
5,256✔
262
                        Ok(s) => Ok(s),
2,628✔
263
                        Err(e) => Err(RunError::TestRuntime(format!(
×
264
                            "Error when collecting coverage: {e}"
×
265
                        ))),
266
                    }
267
                }
268
                WaitStatus::Stopped(child, Signal::SIGSTOP) => Ok((
436✔
269
                    TestState::wait_state(),
436✔
270
                    TracerAction::Continue(child.into()),
218✔
271
                )),
272
                WaitStatus::Stopped(_, Signal::SIGSEGV) => Err(RunError::TestRuntime(
×
273
                    "A segfault occurred while executing tests".to_string(),
×
274
                )),
275
                WaitStatus::Stopped(child, Signal::SIGILL) => {
×
276
                    let pc = current_instruction_pointer(*child).unwrap_or(1) - 1;
×
277
                    trace!("SIGILL raised. Child program counter is: 0x{:x}", pc);
×
278
                    Err(RunError::TestRuntime(format!(
×
279
                        "Error running test - SIGILL raised in {child}"
×
280
                    )))
281
                }
282
                WaitStatus::Stopped(c, Signal::SIGCHLD) => {
12✔
283
                    Ok((TestState::wait_state(), TracerAction::Continue(c.into())))
24✔
284
                }
285
                WaitStatus::Stopped(c, s) => {
×
286
                    let sig = if self.config.forward_signals {
×
287
                        Some(*s)
×
288
                    } else {
289
                        None
×
290
                    };
291
                    let info = ProcessInfo::new(*c, sig);
×
292
                    Ok((TestState::wait_state(), TracerAction::TryContinue(info)))
×
293
                }
294
                WaitStatus::Signaled(c, s, f) => {
×
295
                    if let Ok(s) = self.handle_signaled(c, s, *f) {
×
296
                        Ok(s)
×
297
                    } else {
298
                        Err(RunError::TestRuntime(
×
299
                            "Attempting to handle tarpaulin being signaled".to_string(),
×
300
                        ))
301
                    }
302
                }
303
                WaitStatus::Exited(child, ec) => {
408✔
304
                    let mut parent = Pid::from_raw(0);
×
305
                    if let Some(proc) = self.get_traced_process_mut(*child) {
190✔
306
                        for ref mut value in proc.breakpoints.values_mut() {
1,828✔
307
                            value.thread_killed(*child);
×
308
                        }
309
                        parent = proc.parent;
×
310
                    }
311
                    if &parent == child {
×
312
                        if let Some(removed) = self.processes.remove(&parent) {
570✔
313
                            if parent != self.parent {
6✔
314
                                let traces = removed.traces.unwrap();
24✔
315
                                self.traces.merge(&traces);
12✔
316
                            }
317
                        }
318
                    }
319
                    trace!("Exited {:?} parent {:?}", child, self.parent);
×
320
                    if child == &self.parent {
×
321
                        if self.processes.is_empty() || !self.config.follow_exec {
370✔
322
                            Ok((TestState::End(*ec), TracerAction::Nothing))
182✔
323
                        } else {
324
                            self.exit_code = Some(*ec);
2✔
325
                            info!("Test process exited, but spawned processes still running. Continuing tracing");
4✔
326
                            Ok((TestState::wait_state(), TracerAction::Nothing))
2✔
327
                        }
328
                    } else {
329
                        match self.exit_code {
2✔
330
                            Some(ec) if self.processes.is_empty() => return Ok(TestState::End(ec)),
6✔
331
                            _ => {
×
332
                                // Process may have already been destroyed. This is just in case
333
                                Ok((
222✔
334
                                    TestState::wait_state(),
222✔
335
                                    TracerAction::TryContinue(self.parent.into()),
222✔
336
                                ))
337
                            }
338
                        }
339
                    }
340
                }
341
                _ => Err(RunError::TestRuntime(
×
342
                    "An unexpected signal has been caught by tarpaulin!".to_string(),
×
343
                )),
344
            };
345
            match state {
3,898✔
346
                Ok((TestState::Waiting { .. }, action)) => {
3,716✔
347
                    actions.push(action);
×
348
                }
349
                Ok((state, action)) => {
546✔
350
                    result = Ok(state);
546✔
351
                    actions.push(action);
364✔
352
                }
353
                Err(e) => result = Err(e),
×
354
            }
355
        }
356
        let mut continued = false;
3,576✔
357
        let mut actioned_pids = HashSet::new();
×
358

359
        for a in &actions {
11,372✔
360
            if let Some(d) = a.get_data() {
7,612✔
361
                if actioned_pids.contains(&d.pid) {
×
362
                    trace!("Skipping action '{:?}', pid already sent command", a);
×
363
                    continue;
×
364
                } else {
365
                    trace!("No action for {} yet", d.pid);
3,714✔
366
                }
367
            } else {
368
                trace!("No process info for action");
184✔
369
            }
370
            trace!("Action: {:?}", a);
3,898✔
371
            if let Some(log) = self.event_log.as_ref() {
×
372
                let event = TraceEvent::new_from_action(a);
×
373
                log.push_trace(event);
×
374
            }
375
            match a {
×
376
                TracerAction::TryContinue(t) => {
624✔
377
                    continued = true;
×
378
                    actioned_pids.insert(t.pid);
×
379
                    let _ = continue_exec(t.pid, t.signal);
×
380
                }
381
                TracerAction::Continue(t) => {
1,774✔
382
                    continued = true;
1,774✔
383
                    actioned_pids.insert(t.pid);
5,322✔
384
                    continue_exec(t.pid, t.signal)?;
5,322✔
385
                }
386
                TracerAction::Step(t) => {
1,314✔
387
                    continued = true;
1,314✔
388
                    actioned_pids.insert(t.pid);
3,942✔
389
                    single_step(t.pid)?;
2,628✔
390
                }
391
                TracerAction::Detach(t) => {
4✔
392
                    continued = true;
4✔
393
                    actioned_pids.insert(t.pid);
8✔
394
                    let _ = detach_child(t.pid);
2✔
395
                }
396
                TracerAction::Nothing => {}
184✔
397
            }
398
        }
399
        // Here we assume that no pending actions will exist for things that have currently
400
        // signaled as stopped in this iteration. Currently, the pending actions are just fork
401
        // parents that ptrace will stall until child returns so this will hold true. But if that
402
        // behaviour changes in future the pending action list may need to be pruned more
403
        // thoroughly
404
        self.apply_pending_actions(..pending_action_len);
3,576✔
405

406
        if !continued && self.exit_code.is_none() {
368✔
407
            trace!("No action suggested to continue tracee. Attempting a continue");
182✔
408
            let _ = continue_exec(self.parent, None);
364✔
409
        }
410
        result
×
411
    }
412
}
413

414
impl<'a> LinuxData<'a> {
415
    pub fn new(
184✔
416
        traces: &'a mut TraceMap,
417
        analysis: &'a HashMap<PathBuf, LineAnalysis>,
418
        config: &'a Config,
419
        event_log: &'a Option<EventLog>,
420
    ) -> LinuxData<'a> {
421
        LinuxData {
422
            wait_queue: Vec::new(),
368✔
423
            pending_actions: Vec::new(),
368✔
424
            processes: HashMap::new(),
368✔
425
            current: Pid::from_raw(0),
368✔
426
            parent: Pid::from_raw(0),
368✔
427
            traces,
428
            analysis,
429
            config,
430
            event_log,
431
            pid_map: HashMap::new(),
184✔
432
            exit_code: None,
433
        }
434
    }
435

436
    fn get_parent(&self, pid: Pid) -> Option<Pid> {
6,282✔
437
        self.pid_map.get(&pid).copied().or_else(|| {
31,630✔
438
            let mut parent_pid = None;
440✔
439
            'outer: for k in self.processes.keys() {
660✔
440
                let proc = Process::new(k.as_raw()).ok()?;
1,100✔
441
                if let Ok(tasks) = proc.tasks() {
220✔
442
                    for task in tasks.filter_map(Result::ok) {
520✔
443
                        if task.tid == pid.as_raw() {
×
444
                            parent_pid = Some(*k);
2✔
445
                            break 'outer;
×
446
                        }
447
                    }
448
                }
449
            }
450
            parent_pid
220✔
451
        })
452
    }
453

454
    fn get_traced_process_mut(&mut self, pid: Pid) -> Option<&mut TracedProcess> {
3,654✔
455
        let parent = self.get_parent(pid)?;
14,616✔
456
        self.processes.get_mut(&parent)
×
457
    }
458

459
    fn get_active_trace_map_mut(&mut self, pid: Pid) -> Option<&mut TraceMap> {
2,628✔
460
        let parent = self.get_parent(pid)?;
10,512✔
461
        let process = self.processes.get_mut(&parent)?;
2,628✔
462
        if process.traces.is_some() {
×
463
            process.traces.as_mut()
104✔
464
        } else {
465
            Some(self.traces)
2,576✔
466
        }
467
    }
468

469
    fn get_active_trace_map(&mut self, pid: Pid) -> Option<&TraceMap> {
×
470
        let parent = self.get_parent(pid)?;
×
471
        let process = self.processes.get(&parent)?;
×
472
        Some(process.traces.as_ref().unwrap_or(self.traces))
×
473
    }
474

475
    fn init_process(
192✔
476
        &mut self,
477
        pid: Pid,
478
        trace_map: Option<TraceMap>,
479
    ) -> Result<TracedProcess, RunError> {
480
        let traces = match trace_map.as_ref() {
384✔
481
            Some(s) => s,
16✔
482
            None => self.traces,
184✔
483
        };
484
        let mut breakpoints = HashMap::new();
384✔
485
        trace_children(pid)?;
384✔
486
        let offset = get_offset(pid, self.config);
192✔
487
        trace!(
×
488
            "Initialising process: {}, address offset: 0x{:x}",
×
489
            pid,
×
490
            offset
×
491
        );
492
        let mut clashes = HashSet::new();
×
493
        for trace in traces.all_traces() {
1,182✔
494
            for addr in &trace.address {
4,902✔
495
                if clashes.contains(&align_address(*addr)) {
×
496
                    trace!(
2✔
497
                        "Skipping {} as it clashes with previously disabled breakpoints",
×
498
                        addr
×
499
                    );
500
                    continue;
2✔
501
                }
502
                match Breakpoint::new(pid, *addr + offset) {
×
503
                    Ok(bp) => {
1,842✔
504
                        let _ = breakpoints.insert(*addr + offset, bp);
×
505
                    }
506
                    Err(Errno::EIO) => {
×
507
                        return Err(RunError::TestRuntime(
×
508
                            "Tarpaulin cannot find code addresses check your linker settings."
×
509
                                .to_string(),
×
510
                        ));
511
                    }
512
                    Err(NixErr::UnknownErrno) => {
×
513
                        debug!("Instrumentation address clash, ignoring 0x{:x}", addr);
16✔
514
                        // Now to avoid weird false positives lets get rid of the other breakpoint
515
                        // at this address.
516
                        let aligned = align_address(*addr);
×
517
                        clashes.insert(aligned);
×
518
                        breakpoints.retain(|address, breakpoint| {
82✔
519
                            if align_address(*address - offset) == aligned {
164✔
520
                                trace!("Disabling clashing breakpoint");
×
521
                                if let Err(e) = breakpoint.disable(pid) {
×
522
                                    error!("Unable to disable breakpoint: {}", e);
×
523
                                }
524
                                false
×
525
                            } else {
526
                                true
82✔
527
                            }
528
                        });
529
                    }
530
                    Err(_) => {
×
531
                        return Err(RunError::TestRuntime(
×
532
                            "Failed to instrument test executable".to_string(),
×
533
                        ));
534
                    }
535
                }
536
            }
537
        }
538
        // a processes pid is it's own parent
539
        match self.pid_map.insert(pid, pid) {
192✔
540
            Some(old) if old != pid => {
2✔
541
                debug!("{} being promoted to parent. Old parent {}", pid, old)
×
542
            }
543
            _ => {}
192✔
544
        }
545
        Ok(TracedProcess {
×
546
            parent: pid,
×
547
            breakpoints,
×
548
            thread_count: 0,
×
549
            offset,
×
550
            is_test_proc: false,
×
551
            traces: trace_map,
×
552
        })
553
    }
554

555
    fn handle_exec(
16✔
556
        &mut self,
557
        pid: Pid,
558
    ) -> Result<(TestState, TracerAction<ProcessInfo>), RunError> {
559
        trace!("Handling process exec");
16✔
560
        let res = Ok((TestState::wait_state(), TracerAction::Continue(pid.into())));
48✔
561

562
        if let Ok(proc) = Process::new(pid.into()) {
48✔
563
            let exe = match proc.exe() {
14✔
564
                Ok(e) if !e.starts_with(self.config.target_dir()) => {
18✔
565
                    return Ok((TestState::wait_state(), TracerAction::Detach(pid.into())));
×
566
                }
567
                Ok(e) => e,
28✔
568
                _ => return Ok((TestState::wait_state(), TracerAction::Detach(pid.into()))),
×
569
            };
570
            match generate_tracemap(&exe, self.analysis, self.config) {
56✔
571
                Ok(tm) if !tm.is_empty() => match self.init_process(pid, Some(tm)) {
36✔
572
                    Ok(tp) => {
8✔
573
                        self.processes.insert(pid, tp);
×
574
                        Ok((TestState::wait_state(), TracerAction::Continue(pid.into())))
×
575
                    }
576
                    Err(e) => {
×
577
                        error!("Failed to init process (attempting continue): {}", e);
×
578
                        res
×
579
                    }
580
                },
581
                _ => {
×
582
                    trace!("Failed to create trace map for executable, continuing");
6✔
583
                    res
×
584
                }
585
            }
586
        } else {
587
            trace!("Failed to get process info from PID");
×
588
            res
×
589
        }
590
    }
591

592
    fn handle_ptrace_event(
634✔
593
        &mut self,
594
        child: Pid,
595
        sig: Signal,
596
        event: i32,
597
    ) -> Result<(TestState, TracerAction<ProcessInfo>), RunError> {
598
        use nix::libc::*;
599

600
        if sig == Signal::SIGTRAP {
634✔
601
            match event {
634✔
602
                PTRACE_EVENT_CLONE => match get_event_data(child) {
210✔
603
                    Ok(t) => {
210✔
604
                        trace!("New thread spawned {}", t);
×
605
                        let mut parent = None;
×
606
                        if let Some(proc) = self.get_traced_process_mut(child) {
210✔
607
                            proc.thread_count += 1;
×
608
                            parent = Some(proc.parent);
×
609
                        } else {
610
                            warn!("Couldn't find parent for {}", child);
×
611
                        }
612
                        if let Some(p) = parent {
210✔
613
                            self.pid_map.insert(Pid::from_raw(t as _), p);
×
614
                        }
615
                        Ok((
×
616
                            TestState::wait_state(),
×
617
                            TracerAction::Continue(child.into()),
×
618
                        ))
619
                    }
620
                    Err(e) => {
×
621
                        trace!("Error in clone event {:?}", e);
×
622
                        Err(RunError::TestRuntime(
×
623
                            "Error occurred upon test executable thread creation".to_string(),
×
624
                        ))
625
                    }
626
                },
627
                PTRACE_EVENT_FORK => {
×
628
                    if let Ok(fork_child) = get_event_data(child) {
12✔
629
                        trace!("Caught fork event. Child {}", fork_child);
×
630
                        let parent = if let Some(process) = self.get_traced_process_mut(child) {
6✔
631
                            // Counting a fork as a new thread ?
632
                            process.thread_count += 1;
×
633
                            Some(process.parent)
×
634
                        } else {
635
                            None
×
636
                        };
637
                        if let Some(parent) = parent {
6✔
638
                            self.pid_map.insert(Pid::from_raw(fork_child as _), parent);
×
639
                        }
640
                    } else {
641
                        trace!("No event data for child");
×
642
                    }
643
                    Ok((
6✔
644
                        TestState::wait_state(),
12✔
645
                        TracerAction::Continue(child.into()),
6✔
646
                    ))
647
                }
648
                PTRACE_EVENT_VFORK => {
×
649
                    // So VFORK used to be handled the same place as FORK however, from the man
650
                    // page for posix_spawn:
651
                    //
652
                    // > [The] posix_spawn() function commences by calling clone with CLONE_VM and CLONE_VFORK flags.
653
                    //
654
                    // This suggests that Command::new().spawn() will result in a
655
                    // `PTRACE_EVENT_VFORK` not `PTRACE_EVENT_EXEC`
656
                    if let Ok(fork_child) = get_event_data(child) {
16✔
657
                        let fork_child = Pid::from_raw(fork_child as _);
×
658
                        if self.config.follow_exec {
×
659
                            // So I've seen some recursive bin calls with vforks... Maybe just assume
660
                            // every vfork is an exec :thinking:
661
                            let (state, action) = self.handle_exec(fork_child)?;
32✔
662
                            if self.config.forward_signals {
8✔
663
                                self.pending_actions
16✔
664
                                    .push(TracerAction::Continue(child.into()));
16✔
665
                            }
666
                            Ok((state, action))
×
667
                        } else {
668
                            Ok((
×
669
                                TestState::wait_state(),
×
670
                                TracerAction::Continue(child.into()),
×
671
                            ))
672
                        }
673
                    } else {
674
                        Ok((
×
675
                            TestState::wait_state(),
×
676
                            TracerAction::Continue(child.into()),
×
677
                        ))
678
                    }
679
                }
680
                PTRACE_EVENT_EXEC => {
×
681
                    if self.config.follow_exec {
8✔
682
                        self.handle_exec(child)
24✔
683
                    } else {
684
                        Ok((TestState::wait_state(), TracerAction::Detach(child.into())))
×
685
                    }
686
                }
687
                PTRACE_EVENT_EXIT => {
×
688
                    trace!("Child exiting");
402✔
689
                    let mut is_parent = false;
804✔
690
                    if let Some(proc) = self.get_traced_process_mut(child) {
1,206✔
691
                        proc.thread_count -= 1;
×
692
                        is_parent |= proc.parent == child;
×
693
                    }
694
                    if !is_parent {
620✔
695
                        self.pid_map.remove(&child);
218✔
696
                    }
697
                    Ok((
402✔
698
                        TestState::wait_state(),
804✔
699
                        TracerAction::TryContinue(child.into()),
402✔
700
                    ))
701
                }
702
                _ => Err(RunError::TestRuntime(format!(
×
703
                    "Unrecognised ptrace event {event}"
×
704
                ))),
705
            }
706
        } else {
707
            trace!("Unexpected signal with ptrace event {event}");
×
708
            trace!("Signal: {:?}", sig);
×
709
            Err(RunError::TestRuntime("Unexpected signal".to_string()))
×
710
        }
711
    }
712

713
    fn collect_coverage_data(
2,628✔
714
        &mut self,
715
        visited_pcs: &mut HashMap<Pid, HashSet<u64>>,
716
    ) -> Result<UpdateContext, RunError> {
717
        let mut action = None;
5,256✔
718
        let current = self.current;
5,256✔
719
        let enable = self.config.count;
5,256✔
720
        let mut hits_to_increment = HashSet::new();
5,256✔
721
        if let Some(process) = self.get_traced_process_mut(current) {
7,884✔
722
            let visited = visited_pcs.entry(process.parent).or_default();
×
723
            if let Ok(pc) = current_instruction_pointer(current) {
2,628✔
724
                let pc = (pc - 1) as u64;
×
725
                trace!("Hit address {:#x}", pc);
×
726
                if process.breakpoints.contains_key(&pc) {
×
727
                    let bp = process.breakpoints.get_mut(&pc).unwrap();
6,830✔
728
                    let updated = if visited.contains(&pc) {
5,464✔
729
                        let _ = bp.jump_to(current);
×
730
                        (true, TracerAction::Continue(current.into()))
×
731
                    } else {
732
                        // Don't re-enable if multithreaded as can't yet sort out segfault issue
733
                        if let Ok(x) = bp.process(current, enable) {
2,732✔
734
                            x
×
735
                        } else {
736
                            // So failed to process a breakpoint.. Still continue to avoid
737
                            // stalling
738
                            (false, TracerAction::Continue(current.into()))
×
739
                        }
740
                    };
741
                    if updated.0 {
2,680✔
742
                        hits_to_increment.insert(pc - process.offset);
3,942✔
743
                    }
744
                    action = Some(updated.1);
1,366✔
745
                }
746
            }
747
        } else {
748
            warn!("Failed to find process for pid: {}", current);
×
749
        }
750
        if let Some(traces) = self.get_active_trace_map_mut(current) {
7,884✔
751
            for addr in &hits_to_increment {
5,256✔
752
                traces.increment_hit(*addr);
×
753
            }
754
        } else {
755
            warn!("Failed to find traces for pid: {}", current);
×
756
        }
757
        let action = action.unwrap_or_else(|| TracerAction::Continue(current.into()));
10,408✔
758
        Ok((TestState::wait_state(), action))
2,628✔
759
    }
760

761
    fn handle_signaled(
×
762
        &mut self,
763
        pid: &Pid,
764
        sig: &Signal,
765
        flag: bool,
766
    ) -> Result<UpdateContext, RunError> {
767
        let parent = self.get_parent(*pid);
×
768
        if let Some(p) = parent {
×
769
            if let Some(proc) = self.processes.get(&p) {
×
770
                if !proc.is_test_proc {
×
771
                    let info = ProcessInfo::new(*pid, Some(*sig));
×
772
                    return Ok((TestState::wait_state(), TracerAction::TryContinue(info)));
×
773
                }
774
            }
775
        }
776
        match (sig, flag) {
×
777
            (Signal::SIGKILL, _) => Ok((TestState::wait_state(), TracerAction::Detach(pid.into()))),
×
778
            (Signal::SIGTRAP, true) => {
×
779
                Ok((TestState::wait_state(), TracerAction::Continue(pid.into())))
×
780
            }
781
            (Signal::SIGCHLD, _) => {
×
782
                Ok((TestState::wait_state(), TracerAction::Continue(pid.into())))
×
783
            }
784
            (Signal::SIGTERM, _) => {
×
785
                let info = ProcessInfo {
786
                    pid: *pid,
×
787
                    signal: Some(Signal::SIGTERM),
×
788
                };
789
                Ok((TestState::wait_state(), TracerAction::TryContinue(info)))
×
790
            }
791
            _ => Err(RunError::StateMachine("Unexpected stop".to_string())),
×
792
        }
793
    }
794

795
    fn apply_pending_actions(&mut self, range: impl RangeBounds<usize>) {
1,445,348✔
796
        for a in self.pending_actions.drain(range) {
4,336,052✔
797
            if let Some(log) = self.event_log.as_ref() {
×
798
                let event = TraceEvent::new_from_action(&a);
×
799
                log.push_trace(event);
×
800
            }
801
            match a {
×
802
                TracerAction::Continue(t) | TracerAction::TryContinue(t) => {
16✔
803
                    let _ = continue_exec(t.pid, t.signal);
8✔
804
                }
805
                e => {
×
806
                    error!("Pending actions should only be continues: {:?}", e);
×
807
                }
808
            }
809
        }
810
    }
811
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc