libclues
Linux C++ Tracing Library
Loading...
Searching...
No Matches
TermTracer.cxx
1// C
2#include <fnmatch.h>
3
4// C++
5#include <cassert>
6#include <cstdlib>
7#include <iostream>
8#include <map>
9#include <sstream>
10#include <string_view>
11#include <type_traits>
12#include <vector>
13
14// cosmos
15#include <cosmos/cosmos.hxx>
16#include <cosmos/error/ApiError.hxx>
17#include <cosmos/error/CosmosError.hxx>
18#include <cosmos/formatting.hxx>
19#include <cosmos/proc/process.hxx>
20#include <cosmos/proc/signal.hxx>
21#include <cosmos/string.hxx>
22#include <cosmos/utils.hxx>
23
24// clues
25#include <clues/ChildTracee.hxx>
26#include <clues/ForeignTracee.hxx>
27#include <clues/format.hxx>
28#include <clues/logger.hxx>
29#include <clues/syscalls/process.hxx>
30#include <clues/sysnrs/generic.hxx>
31#include <clues/SystemCallItem.hxx>
32#include <clues/utils.hxx>
33
34// termtracer
35#include "TermTracer.hxx"
36
37namespace clues {
38
39namespace {
40
41std::string_view to_label(const cosmos::ptrace::Event event) {
42 using Event = cosmos::ptrace::Event;
43 switch (event) {
44 case Event::VFORK: return "vfork()";
45 case Event::FORK: return "fork()";
46 case Event::CLONE: return "clone()";
47 default: return "???";
48 }
49}
50
51bool ask_yes_no() {
52 std::string yes_no;
53 while (true) {
54 std::cout << "(y/n) > ";
55 std::cin >> yes_no;
56 if (!std::cin.good() || yes_no == "n")
57 return false;
58 else if (yes_no == "y")
59 return true;
60 }
61}
62
63} // anon ns
64
65TermTracer::TermTracer() :
66 m_engine{*this} {
67}
68
69bool TermTracer::processPars() {
70
71 auto handle_bad_arg = [](auto &arg) {
72 std::cerr << "bad argument to --" << arg.getName() << ": '" << arg.getValue() << "'\n";
73 return false;
74 };
75
76 if (m_args.list_syscalls.isSet()) {
77 printSyscalls();
78 throw cosmos::ExitStatus::SUCCESS;
79 } else if (m_args.list_abis.isSet()) {
80 printABIs();
81 throw cosmos::ExitStatus::SUCCESS;
82 } else if (m_args.list_abi_syscalls.isSet()) {
83 printABISyscalls(m_args.list_abi_syscalls.getValue());
84 throw cosmos::ExitStatus::SUCCESS;
85 }
86
87 if (const auto max_len = m_args.max_value_len.getValue(); max_len == 0)
88 m_print_pars = false;
89 else if (max_len < 0)
90 m_par_truncation_len = SIZE_MAX;
91 else
92 m_par_truncation_len = static_cast<size_t>(max_len);
93
94 if (m_args.follow_execve.isSet()) {
95 const auto &follow = m_args.follow_execve.getValue();
96 constexpr std::string_view PATH_PREFIX{"path:"};
97 constexpr std::string_view GLOB_PREFIX{"glob:"};
98
99 if (follow == "yes") {
100 m_follow_exec = FollowExecContext::YES;
101 } else if (follow == "no") {
102 m_follow_exec = FollowExecContext::NO;
103 } else if (follow == "ask") {
104 m_follow_exec = FollowExecContext::ASK;
105 } else if (cosmos::is_prefix(follow, PATH_PREFIX)) {
106 m_follow_exec = FollowExecContext::CHECK_PATH;
107 m_exec_context_arg = follow.substr(PATH_PREFIX.size());
108 } else if (cosmos::is_prefix(follow, GLOB_PREFIX)) {
109 m_follow_exec = FollowExecContext::CHECK_GLOB;
110 m_exec_context_arg = follow.substr(GLOB_PREFIX.size());
111 } else {
112 return handle_bad_arg(m_args.follow_execve);
113 }
114 }
115
116 if (m_args.follow_children.isSet() && m_args.follow_children_switch.isSet()) {
117 std::cerr << "cannot set both '-f' and '--follow-children'\n";
118 return false;
119 } else if (m_args.follow_children_switch.isSet()) {
120 m_follow_children = FollowChildMode::YES;
121 } else if (m_args.follow_children.isSet()) {
122 const auto &follow = m_args.follow_children.getValue();
123
124 if (follow == "yes") {
125 m_follow_children = FollowChildMode::YES;
126 } else if (follow == "no") {
127 m_follow_children = FollowChildMode::NO;
128 } else if (follow == "ask") {
129 m_follow_children = FollowChildMode::ASK;
130 } else if (follow == "threads") {
131 m_follow_children = FollowChildMode::THREADS;
132 } else {
133 return handle_bad_arg(m_args.follow_children);
134 }
135 }
136
137 if (m_args.follow_threads.isSet()) {
138 if (m_follow_children != FollowChildMode::NO) {
139 std::cerr << "cannot combine '--threads' with '-f' or '--follow-children'\n";
140 return false;
141 }
142
143 m_follow_children = FollowChildMode::THREADS;
144 }
145
146 if (m_args.syscall_filter.isSet()) {
147 auto parts = cosmos::split(m_args.syscall_filter.getValue(), ",");
148
149 auto translate_name = [](const std::string_view name) {
150 if (auto nr = lookup_system_call(name); nr) {
151 return *nr;
152 }
153
154 std::cerr << "invalid system call name: "
155 << name << "\n";
156 throw cosmos::ExitStatus::FAILURE;
157 };
158
159 std::vector<SystemCallNr> to_add;
160 std::vector<SystemCallNr> to_remove;
161
162 for (auto &part: parts) {
163 if (part.starts_with("!")) {
164 part = part.substr(1);
165 to_remove.push_back(translate_name(part));
166 } else {
167 to_add.push_back(translate_name(part));
168 }
169 }
170
171 for (const auto nr: to_add) {
172 m_syscall_filter.insert(nr);
173 }
174
175 // NOTE: this removal logic can make sense in the future when
176 // we support system call groups (→ add a group, remove a few
177 // again)
178 for (const auto nr: to_remove) {
179 m_syscall_filter.erase(nr);
180 }
181 }
182
183 if (m_args.print_fd_info.isSet()) {
184 m_engine.setFormatFlags(Engine::FormatFlag::FD_INFO);
185 }
186
187 return true;
188}
189
190void TermTracer::printSyscalls() {
191 // start at index one to skip the "UKNOWN" system call
192 for (size_t nr = 1; nr < SYSTEM_CALL_NAMES.size(); nr++) {
193 auto NAME = SYSTEM_CALL_NAMES[nr];
194 if (!NAME[0])
195 continue;
196 std::cout << NAME << "\n";
197 }
198}
199
200void TermTracer::printABISyscalls(const std::string &abi_str) {
201 auto print_syscalls = [](auto sysnr) {
202 using ABISystemCallNr = decltype(sysnr);
203 for (sysnr = ABISystemCallNr::_FIRST;
204 sysnr != ABISystemCallNr::_LAST;
205 sysnr = ABISystemCallNr{cosmos::to_integral(sysnr)+1}) {
206 /*
207 * convert to the generic system call number, this way
208 * we can get the system call name
209 */
210 auto gen_sysnr = clues::to_generic(sysnr);
211 if (gen_sysnr == clues::SystemCallNr::UNKNOWN)
212 // unassigned
213 continue;
214
215 auto name = clues::SYSTEM_CALL_NAMES[cosmos::to_integral(gen_sysnr)];
216
217 std::cout << name << " (";
218 if constexpr (std::is_same<ABISystemCallNr, SystemCallNrX32>::value) {
219 std::cout << "X32_SYSCALL_BIT + " << (cosmos::to_integral(sysnr) & (~clues::X32_SYSCALL_BIT));
220 } else {
221 std::cout << cosmos::to_integral(sysnr);
222 }
223 std::cout << ")\n";
224 }
225 };
226
227 clues::AnySystemCallNr sys_nr;
228
229 if (abi_str == "i386") {
230 sys_nr = SystemCallNrI386{};
231 } else if (abi_str == "x86-64") {
232 sys_nr = SystemCallNrX64{};
233 } else if (abi_str == "x32") {
234 sys_nr = SystemCallNrX32{};
235 } else if (abi_str == "aarch64") {
236 sys_nr = SystemCallNrAARCH64{};
237 } else {
238 throw cosmos::RuntimeError{"unexpected abi string encountered"};
239 }
240
241 std::visit(print_syscalls, sys_nr);
242}
243
244void TermTracer::printABIs() {
245 for (const auto abi: clues::get_supported_abis()) {
246 std::cout << clues::get_abi_label(abi) << "\n";
247 }
248}
249
250void TermTracer::configureLogger() {
251 // enable errors and warnings by default
252 m_logger.setChannels(true, true, false, false);
253 m_logger.configFromEnvVar("CLUES_LOGGING");
254 clues::set_logger(m_logger);
255}
256
257void TermTracer::printParsOnEntry(std::ostream &trace, const SystemCall::ParameterVector &pars) const {
258 /*
259 * This logic covers printing of parameters during system-call entry.
260 *
261 * During entry we can print all input-only parameters and stop once
262 * we see a non-input parameter or once we are finished with all
263 * parameters.
264 *
265 * During exit we continue printing from the first non-input parameter
266 * onwards. This way we can present at least some of the system call
267 * data already for blocking system calls.
268 *
269 * There exist system calls where the order of parameters is like
270 * this:
271 *
272 * <input>, <output>, <input>
273 *
274 * In this case, while the final parameter would still be printable,
275 * we will stop at the third parameter which is an output parameter.
276 * `strace` does this similarly. In theory we could print a '?' in
277 * this case for the not yet available output data, then do a
278 * carriage-return during syscall-exit to print the end result.
279 */
280
281 if (pars.empty())
282 return;
283
284 const auto &last = pars.back();
285
286 for (const auto &par: pars) {
287 if (clues::item::is_unused_par(*par)) {
288 continue;
289 } else if (!par->isIn()) {
290 // non-input parameter encountered, stop output
291 break;
292 }
293
294 printPar(trace, *par);
295
296 if (const auto is_last = &par == &last; !is_last) {
297 trace << ", ";
298 }
299 }
300}
301
302void TermTracer::printParsOnExit(std::ostream &trace, const SystemCall::ParameterVector &pars) const {
303 if (pars.empty())
304 return;
305
306 const auto &last = pars.back();
307 bool seen_non_input = false;
308
309 for (const auto &par: pars) {
310 if (clues::item::is_unused_par(*par)) {
311 continue;
312 } else if (!seen_non_input) {
313 if (par->isIn()) {
314 // this was already printed during entry
315 continue;
316 } else {
317 seen_non_input = true;
318 }
319 }
320
321 printPar(trace, *par);
322
323 if (const auto is_last = &par == &last; !is_last) {
324 trace << ", ";
325 }
326 }
327}
328
329std::string TermTracer::formatTraceeInvocation(const Tracee &tracee) {
330 return formatTraceeInvocation(tracee.executable(), tracee.cmdLine());
331}
332
333std::string TermTracer::formatTraceeInvocation(const std::string &exe,
334 const cosmos::StringVector &cmdline) const {
335 std::stringstream ss;
336 ss << exe << " [";
337 bool first = true;
338 for (const auto &arg: cmdline) {
339 if (first) {
340 first = false;
341 } else {
342 ss << ", ";
343 }
344 ss << "\"" << arg << "\"";
345 }
346
347 ss << "]";
348
349 return ss.str();
350}
351
352void TermTracer::printPar(std::ostream &trace, const SystemCallItem &par) const {
353 trace << (m_args.verbose.isSet() ? par.longName() : par.shortName());
354
355 if (m_print_pars) {
356 auto value = par.str();
357
358 if (value.size() > m_par_truncation_len) {
359 value.resize(m_par_truncation_len);
360 value += "...";
361 }
362
363 trace << "=" << value;
364 }
365}
366
367void TermTracer::syscallEntry(Tracee &tracee,
368 const SystemCall &sc,
369 const StatusFlags flags) {
370
371 if (!isEnabled(&sc)) {
372 return;
373 }
374
375 // this needs to be assigned before returning from this function but
376 // after traceStream() is called, if at all.
377 auto defer_assign_syscall = cosmos::defer([this, &tracee, &sc]() {
378 m_active_syscall = std::make_optional(
379 std::make_tuple(tracee.pid(), &sc));
380 });
381
382 const auto syscall_info = *tracee.currentSystemCallInfo();
383 checkABI(tracee, syscall_info);
384 /* libclues also offers an ABI_CHANGED flag in `cflags`, but this
385 * doesn't suffice for us, since we might filter system calls, which
386 * means we don't necessarily need to report every ABI change.
387 */
388 m_last_abi = syscall_info.abi();
389
390 auto &trace = traceStream(tracee);
391
392 if (flags[StatusFlag::RESUMED]) {
393 trace << "<resuming previously interrupted " << sc.name() << "...>\n";
394 }
395 trace << sc.name() << "(";
396
397 printParsOnEntry(trace, sc.parameters());
398
399 trace << std::flush;
400}
401
402void TermTracer::syscallExit(Tracee &tracee, const SystemCall &sc,
403 const StatusFlags flags) {
404
405 if (isExecSyscall(sc) && sc.result()) {
406 // this was already dealt with in newExecutionContext()
407 return;
408 } else if (!isEnabled(&sc)) {
409 return;
410 }
411
414 auto defer_reset_syscall = cosmos::defer([this]() {
415 m_active_syscall.reset();
416 });
417
418
419 checkResumedSyscall(tracee);
420
421 // we are preempting another Tracee's system call (one Tracee already entered, this Tracee exits
422 const bool need_newline = m_active_syscall && !hasActiveSyscall(tracee);
423
424 auto &trace = traceStream(tracee, need_newline);
425 printParsOnExit(trace, sc.parameters());
426
427 trace << ") = ";
428
429 if (auto res = sc.result(); res) {
430 trace << res->str() << " (" << (m_args.verbose.isSet() ? res->longName() : res->shortName()) << ")";
431 } else {
432 const auto err = *sc.error();
433 trace << err.str() << " (errno)";
434 }
435
436 if (flags[StatusFlag::INTERRUPTED]) {
437 trace << " (interrupted)";
438 }
439
440 trace << std::endl;
441}
442
443void TermTracer::signaled(Tracee &tracee, const cosmos::SigInfo &info) {
444 traceStream(tracee) << "-- " << clues::format::sig_info(info) << " --\n";
445}
446
447void TermTracer::attached(Tracee &tracee) {
448 if (m_num_tracees++ == 0) {
449 // the initial process, don't print anything special in this case
450 return;
451 } else if (tracee.isInitiallyAttachedThread()) {
452 // part of the initial attach procedure when attaching to a
453 // possibly multi-threaded existing process.
454 traceStream(tracee) << "→ additionally attached thread\n";
455 } else if (auto it = m_new_tracees.find(tracee.pid()); it != m_new_tracees.end()) {
456 auto [parent, event] = it->second;
457
458 traceStream(tracee) << "→ automatically attached (created by PID " << parent << " via " << to_label(event) << ")\n";
459 m_new_tracees.erase(it);
460 } else {
461 traceStream(tracee) << "unknown Tracee " << cosmos::to_integral(tracee.pid()) << " attached?!";
462 }
463}
464
465void TermTracer::exited(Tracee &tracee, const cosmos::WaitStatus status, const StatusFlags flags) {
466 if (tracee.prevState() == Tracee::State::SYSCALL_ENTER_STOP) {
467 abortSyscall(tracee);
468 }
469
470 if (flags[StatusFlag::LOST_TO_MT_EXIT]) {
471 auto &trace = traceStream(tracee);
472 trace << "--- <lost to exit or execve in another thread> ---\n";
473 }
474
475 if (status.exited()) {
476 traceStream(tracee) << "+++ exited with " << cosmos::to_integral(*status.status()) << " +++\n";
477 if (!seenInitialExec()) {
478 traceStream(tracee) << "!!! failed to execute the specified program\n";
479 }
480 } else {
481 traceStream(tracee) << "+++ killed by signal " << cosmos::to_integral(status.termSig()->raw()) << " +++\n";
482 }
483
484 if (tracee.pid() == m_main_tracee_pid) {
485 m_main_status = status;
486 }
487
488 cleanupTracee(tracee);
489}
490
491void TermTracer::stopped(Tracee &tracee) {
492 if (tracee.syscallCtr() == 0) {
493 traceStream(tracee) << "--- currently in stopped state due to " << *tracee.stopSignal() << " ---\n";
494 }
495}
496
497void TermTracer::disappeared(Tracee &tracee, const cosmos::ChildState &data) {
498 abortSyscall(tracee);
499
500 traceStream(tracee) << "!!! disappeared (e.g. exit() or execve() in another thread)\n";
501
502 if (data.exited()) {
503 traceStream(tracee) << "+++ exited with " << cosmos::to_integral(*data.status) << " +++\n";
504 } else {
505 traceStream(tracee) << "+++ killed by signal " << cosmos::to_integral(data.signal->raw()) << " +++\n";
506 }
507
508 if (tracee.pid() == m_main_tracee_pid) {
509 /*
510 * Here we get a more complex cosmos::ChildState instead of
511 * WaitStatus as in the other callbacks. Convert it into a
512 * WaitStatus so that we can treat all exit paths
513 * homogeneously.
514 */
515 m_main_status = cosmos::WaitStatus{data};
516 }
517
518 cleanupTracee(tracee);
519}
520
522 const std::string &old_exe,
523 const cosmos::StringVector &old_cmdline,
524 const std::optional<cosmos::ProcessID> old_pid) {
525 if (old_pid) {
526 // this needs to be done first to avoid any inconsistent state down below.
527 updateTracee(tracee, *old_pid);
528 }
529
530 // cache this state here, because checkResumedSyscall() might alter the info
531 const auto is_enabled = isEnabled(currentSyscall(tracee));
532
533 if (is_enabled) {
534 checkResumedSyscall(tracee);
535 /* anticipate the success system call status to avoid
536 * complexities while outputting status messages and
537 * interactive dialogs. */
538 traceStream(tracee, false) << ") = 0 (success)\n";
539 }
540
541 m_active_syscall = {};
542
543 if (old_pid) {
544 traceStream(tracee) << "--- PID " << *old_pid << " is now known as PID " << tracee.pid() << " ---\n";
545 }
546
547 // skip the output for the initial exec of a new child process,
548 // because there's no interesting information in that
549 if (seenInitialExec()) {
550 traceStream(tracee) << "--- no longer running " << formatTraceeInvocation(old_exe, old_cmdline) << " ---\n";
551 }
552 traceStream(tracee) << "--- now running " << formatTraceeInvocation(tracee) << " ---\n";
553
554 if (!seenInitialExec()) {
556 } else {
557 if (!followExecutionContext(tracee)) {
558 traceStream(tracee) << "--- detaching after execve ---\n";
559 tracee.detach();
560 }
561 }
562}
563
564void TermTracer::newChildProcess(Tracee &parent, Tracee &child,
565 const cosmos::ptrace::Event event, const StatusFlags flags) {
566 auto follow = m_follow_children;
567
568 if (follow == FollowChildMode::ASK) {
569 traceStream(parent) << "Follow into new child process created by PID " << parent.pid() << " via " << to_label(event) << "?\n";
570 std::cout << "PID " << parent.pid() << " is " << formatTraceeInvocation(parent) << "\n";
571 follow = ask_yes_no() ? FollowChildMode::YES : FollowChildMode::NO;
572 } else if (follow == FollowChildMode::THREADS) {
573 follow = flags[StatusFlag::CLONED_THREAD] ? FollowChildMode::YES : FollowChildMode::NO;
574 }
575
576 if (follow == FollowChildMode::YES) {
577 // keep this information for later when something actually happens in the new child
578 m_new_tracees.insert({child.pid(), {parent.pid(), event}});
579 } else {
580 child.detach();
581 }
582}
583
584bool TermTracer::followExecutionContext(Tracee &tracee) {
585 switch (m_follow_exec) {
586 case FollowExecContext::YES: return true;
587 case FollowExecContext::NO: return false;
588 case FollowExecContext::ASK: {
589 std::cout << "Follow into new execution context?\n";
590 return ask_yes_no();
591 } case FollowExecContext::CHECK_PATH: {
592 return m_exec_context_arg == tracee.executable();
593 } case FollowExecContext::CHECK_GLOB: {
594 return ::fnmatch(m_exec_context_arg.c_str(), tracee.executable().c_str(), 0) == 0;
595 }
596 default:
597 return false;
598 }
599}
600
601std::ostream& TermTracer::traceStream(const Tracee &tracee, const bool new_line) {
602 // TODO: this is currently fixed to cerr, but we should implement
603 // trace to file, maybe also separate files for each PID, or even
604 // separate PTYs in some form.
605 auto &stream = std::cerr;
606
607 if (new_line) {
608 startNewLine(stream, tracee);
609 }
610 return std::cerr;
611}
612
613void TermTracer::startNewLine(std::ostream &trace, const Tracee &tracee) {
614 // make sure to terminate a possibly pending system call line
616 // a system call in another thread remains unfinished
617 trace << " <...unfinished>\n";
618 }
619
620 if (m_num_tracees > 1) {
621 trace << "[" << tracee.pid() << "] ";
622 } else if (m_flags.steal(Flag::DROPPED_TO_LAST_TRACEE)) {
623 trace << "--- only PID " << tracee.pid() << " is remaining ---\n";
624 }
625}
626
628 if (!m_active_syscall) {
629 return false;
630 }
631
632 auto [pid, syscall] = *m_active_syscall;
633 const bool ret = isEnabled(syscall);
634
635 m_unfinished_syscalls[pid] = syscall;
636 m_active_syscall.reset();
637 return ret;
638}
639
640const SystemCall* TermTracer::findSyscall(const Tracee &tracee) const {
641 if (const auto syscall = activeSyscall(tracee); syscall)
642 return syscall;
643
644 if (auto it = m_unfinished_syscalls.find(tracee.pid()); it != m_unfinished_syscalls.end()) {
645 return it->second;
646 }
647
648 return nullptr;
649}
650
651bool TermTracer::isExecSyscall(const SystemCall &sc) const {
652 switch (sc.callNr()) {
653 default: return false;
654 case SystemCallNr::EXECVE:
655 case SystemCallNr::EXECVEAT:
656 return true;
657 }
658}
659
660const SystemCall* TermTracer::currentSyscall(const Tracee &tracee) const {
661 if (m_active_syscall) {
662 auto &[pid, syscall] = *m_active_syscall;
663
664 if (pid == tracee.pid()) {
665 return syscall;
666 }
667 }
668
669 auto it = m_unfinished_syscalls.find(tracee.pid());
670 if (it != m_unfinished_syscalls.end()) {
671 return it->second;
672 }
673
674 return nullptr;
675}
676
677bool TermTracer::isEnabled(const SystemCall *sc) const {
678 if (!sc)
679 return false;
680 else if (m_syscall_filter.empty())
681 return true;
682
683 return m_syscall_filter.count(sc->callNr()) != 0;
684}
685
686void TermTracer::checkResumedSyscall(const Tracee &tracee) {
687 if (hasActiveSyscall(tracee))
688 return;
689
690 /*
691 * when there's no active system call then we expect one to be stored
692 * as unfinished, otherwise something is off.
693 */
694
695 auto node = m_unfinished_syscalls.extract(tracee.pid());
696 assert(!node.empty());
697 auto sc = node.mapped();
698
699 auto &trace = traceStream(tracee);
700 trace << "<resuming ...> " << sc->name();
701 if (sc->hasOutParameter()) {
702 trace << "(..., ";
703 } else {
704 trace << "(...";
705 }
706}
707
708void TermTracer::cleanupTracee(const Tracee &tracee) {
709 /* remove the given tracee from all bookkeeping */
710 if (hasActiveSyscall(tracee)) {
711 m_active_syscall.reset();
712 } else {
713 m_unfinished_syscalls.erase(tracee.pid());
714 }
715
716 m_new_tracees.erase(tracee.pid());
717
718 if (--m_num_tracees == 1) {
719 m_flags.set(Flag::DROPPED_TO_LAST_TRACEE);
720 }
721}
722
723void TermTracer::updateTracee(const Tracee &tracee, const cosmos::ProcessID old_pid) {
724 if (hasActiveSyscall(old_pid)) {
725 m_active_syscall = std::make_tuple(tracee.pid(), activeSyscall());
726 } else if (auto it = m_unfinished_syscalls.find(old_pid); it != m_unfinished_syscalls.end()) {
727 m_unfinished_syscalls[tracee.pid()] = it->second;
728 m_unfinished_syscalls.erase(it);
729 }
730
731 // if there should be an entry for the new PID already, then it's from
732 // an already dead sibbling tracee. get rid of that.
733 m_new_tracees.erase(tracee.pid());
734
735 if (auto it = m_new_tracees.find(old_pid); it != m_new_tracees.end()) {
736 m_new_tracees[tracee.pid()] = it->second;
737 m_new_tracees.erase(it);
738 }
739}
740
741void TermTracer::abortSyscall(const Tracee &tracee) {
742 // obtain this information before the active syscall is reset below
743 const auto is_enabled = isEnabled(currentSyscall(tracee));
744
745 if (hasActiveSyscall(tracee)) {
746 m_active_syscall.reset();
747 } else if (is_enabled) {
748 checkResumedSyscall(tracee);
749 }
750
751 if (is_enabled) {
752 /*
753 * finish any possibly pending system call to make clear it
754 * never visibly returned.
755 */
756 traceStream(tracee, false) << ") = ?\n";
757 }
758}
759
760void TermTracer::checkABI(const Tracee &tracee, const SystemCallInfo &info) {
761 bool report_abi = false;
762
763 if (m_last_abi == clues::ABI::UNKNOWN) {
764 // check if the initial system call already has some
765 // non-default ABI.
766 if (!clues::is_default_abi(info.abi()))
767 report_abi = true;
768 } else if (info.abi() != m_last_abi) {
769 report_abi = true;
770 }
771
772 if (report_abi) {
773 traceStream(tracee) << "[system call ABI changed to " << get_abi_label(info.abi()) << "]\n";
774 }
775}
776
777bool TermTracer::configureTracee(const cosmos::ProcessID pid) {
778 // this is only for newly created child processes, so set it right away
779 m_flags.set(Flag::SEEN_INITIAL_EXEC);
780 TraceePtr tracee;
781 try {
782 /* strace ties the attach threads behaviour to `-f`. There
783 * can be situations when this is not helpful. Thus we do the
784 * same but offer a `-1` switch to opt out of this and only
785 * attach to the single thread specified on the command line.
786 */
787 tracee = m_engine.addTracee(pid,
788 FollowChildren{m_follow_children == FollowChildMode::NO ? false : true},
789 AttachThreads{m_follow_children == FollowChildMode::NO || m_args.no_initial_threads_attach.isSet() ? false : true});
790 } catch (const cosmos::ApiError &e) {
791 std::cerr << "Failed to attach to PID " << pid << ": " << e.msg() << "\n";
792 if (e.errnum() == cosmos::Errno::PERMISSION) {
793 std::cerr << "You need to be root to attach to processes not owned by you\n";
794 std::cerr << "The YAMA kernel security extension can also prevent attaching your own processes.\n";
795 std::cerr << "This also happens when another process is already tracing this process.\n";
796 }
797 return false;
798 }
799
800 traceStream(*tracee, false) << "--- tracing " << formatTraceeInvocation(*tracee) << " ---\n";
801
802 m_main_tracee_pid = tracee->pid();
803
804 return true;
805}
806
807cosmos::ExitStatus TermTracer::main(const int argc, const char **argv) {
808 constexpr auto FAILURE = cosmos::ExitStatus::FAILURE;
809 m_args.cmdline.parse(argc, argv);
810
811 configureLogger();
812
813 if (!processPars()) {
814 return FAILURE;
815 }
816
817 cosmos::signal::block(cosmos::SigSet{cosmos::signal::CHILD});
818
819 if (m_args.attach_proc.isSet()) {
820 if (!configureTracee(cosmos::ProcessID{m_args.attach_proc.getValue()})) {
821 return FAILURE;
822 }
823 } else {
824 // extract any additional arguments into a StringVector
825 cosmos::StringVector sv;
826 bool found_sep = false;
827
828 for (auto arg = 1; arg < argc; arg++) {
829 if (found_sep) {
830 sv.push_back(argv[arg]);
831 } else if (std::string{argv[arg]} == "--") {
832 found_sep = true;
833 }
834 }
835
836 if (sv.empty()) {
837 std::cerr << "Neither -p nor command to execute after '--' was given. Nothing to do.\n";
838 return FAILURE;
839 }
840
841 try {
842 auto tracee = m_engine.addTracee(sv,
843 FollowChildren{m_follow_children == FollowChildMode::NO ? false : true});
844 m_main_tracee_pid = tracee->pid();
845 } catch (const cosmos::CosmosError &ex) {
846 std::cerr << ex.shortWhat() << "\n";
847 return FAILURE;
848 }
849 }
850
851 try {
852 m_engine.trace();
853 } catch (const std::exception &ex) {
854 std::cerr << "internal tracing error: " << ex.what() << std::endl;
855 // try to do a hard exit to avoid blocking in the Engine's
856 // destructor, should the state be messed up
857 cosmos::proc::exit(FAILURE);
858 }
859
860 auto status = cosmos::ExitStatus::SUCCESS;
861
862 /*
863 * Evaluate exit code of the tracee, valid only after detach().
864 *
865 * We mimic the behaviour of the tracee by returning the same exit
866 * status.
867 */
868 if (m_main_status) {
869 if (m_main_status->exited()) {
870 status = *m_main_status->status();
871 } else if (m_main_status->signaled()) {
872 // this is what the shell returns for children that have been killed.
873 // TODO: strace actually sends the same kill signal to iself
874 // I don't know whether this is very useful. It can
875 // cause core dumps of the tracer, thus we'd need to
876 // change ulimit to prevent that.
877 status = cosmos::ExitStatus{128 + cosmos::to_integral(m_main_status->termSig()->raw())};
878 }
879 }
880
881 return status;
882}
883
884} // end ns
885
886int main(const int argc, const char **argv) {
887 return cosmos::main<clues::TermTracer>(argc, argv);
888}
@ INTERRUPTED
A system call was interrupted (only appears during syscallExit()).
@ LOST_TO_MT_EXIT
An exit occurs because another thread called execve() or exit() (only appears in exited()).
@ RESUMED
A previously interrupted system call is resumed (only appears during syscallEntry()).
@ CLONED_THREAD
used in newChildProcess() to indicate that a new thread has been created.
Extended ptrace system call state information.
Access to System Call Data.
std::string_view name() const
Returns the system call's human readable name.
const ParameterVector & parameters() const
Access to the parameters associated with this system call.
SystemCallItemPtr result() const
Access to the return value parameter associated with this system call.
std::optional< ErrnoResult > error() const
Access to the errno result seen for this system call.
SystemCallNr callNr() const
Returns the system call table number for this system call.
const SystemCall * findSyscall(const Tracee &tracee) const
Find any active or unfinished system call for pid.
void newChildProcess(Tracee &parent, Tracee &child, const cosmos::ptrace::Event event, const StatusFlags flags) override
A new child process has been created.
std::optional< std::tuple< cosmos::ProcessID, const SystemCall * > > m_active_syscall
The currently active system call, if any.
void syscallExit(Tracee &tracee, const SystemCall &sc, const StatusFlags flags) override
A system call has been finished.
const SystemCall * currentSyscall(const Tracee &tracee) const
Returns the system call last seen for tracee, or nullptr if there's none.
Flags m_flags
State flags with global context or carried between different callbacks.
void checkABI(const Tracee &tracee, const SystemCallInfo &info)
Checks the current system call's ABI and reports ABI changes.
bool storeUnfinishedSyscallCtx()
Store an active system call in m_unfinished_syscalls.
Args m_args
Command line arguments and parser.
void syscallEntry(Tracee &tracee, const SystemCall &sc, const StatusFlags flags) override
A system call is about to be executed in the Tracee.
void abortSyscall(const Tracee &tracee)
Abort syscall if one was active for tracee.
std::ostream & traceStream(const Tracee &tracee, const bool new_line=true)
Returns the currently active trace output stream, starting a new output line.
std::map< cosmos::ProcessID, std::pair< cosmos::ProcessID, cosmos::ptrace::Event > > m_new_tracees
Newly created tracees that haven't seen any ptrace stop yet.
void signaled(Tracee &tracee, const cosmos::SigInfo &info) override
The tracee has received a signal.
void updateTracee(const Tracee &tracee, const cosmos::ProcessID old_pid)
Update internal data structures in case a tracee changed PID.
clues::ABI m_last_abi
The ABI of the last system call we've seen.
FollowChildMode m_follow_children
Behaviour upon newChildProcess()
void exited(Tracee &tracee, const cosmos::WaitStatus status, const StatusFlags flags) override
The tracee is about to end execution.
@ DROPPED_TO_LAST_TRACEE
whether we've returned to tracing only a single PID anymore.
@ SEEN_INITIAL_EXEC
whether we've seen a ChildTracee's initial newExecutionContext().
std::map< cosmos::ProcessID, const SystemCall * > m_unfinished_syscalls
Unfinished / preempted system calls.
void checkResumedSyscall(const Tracee &tracee)
Check whether tracee has an unfinished system call pending.
void stopped(Tracee &tracee) override
The tracee entered group-stop due to a stopping signal.
size_t m_num_tracees
The number of tracees we're currently dealing with.
void disappeared(Tracee &tracee, const cosmos::ChildState &data) override
The tracee disappeared for unclear reasons.
std::set< SystemCallNr > m_syscall_filter
Whitelist of system calls to trace, if any.
void attached(Tracee &tracee) override
The tracee is now properly attached to.
void startNewLine(std::ostream &trace, const Tracee &tracee)
Start a new output line concerning `tracee.
bool isEnabled(const SystemCall *sc) const
Returns true if sc is set and supposed to the printed.
void newExecutionContext(Tracee &tracee, const std::string &old_exe, const cosmos::StringVector &old_cmdline, const std::optional< cosmos::ProcessID > old_pid) override
A new program is executed in the tracee.
cosmos::ProcessID m_main_tracee_pid
The PID of the main process we're tracing (the one we created or attached to).
std::optional< cosmos::WaitStatus > m_main_status
The WaitStatus of the main process we've seen upon it exiting.
Base class for traced processes.
Definition Tracee.hxx:39
bool isInitiallyAttachedThread() const
Indicates whether this Tracee is an automatically attached thread.
Definition Tracee.hxx:269
bool detach()
Attempt to detach the Tracee.
Definition Tracee.cxx:177
size_t syscallCtr() const
Returns the number of system calls observed so far.
Definition Tracee.hxx:132
@ SYSCALL_ENTER_STOP
system call started.
Definition Tracee.hxx:51
std::optional< cosmos::Signal > stopSignal() const
For state() == State::GROUP_STOP this returns the stopping signal that caused it.
Definition Tracee.hxx:137
SystemCallNrAARCH64
Native system call numbers as used by Linux on the aarch64 ABI.
Definition aarch64.hxx:31
std::array< ABI, SUPPORTED_ABIS > get_supported_abis()
Returns a list of ABIs supported on the current platform.
Definition utils.cxx:299
std::optional< SystemCallNr > lookup_system_call(const std::string_view name)
Returns the SystemCallNr for the given system call name, if it exists.
Definition utils.cxx:271
cosmos::NamedBool< struct follow_children_t, true > FollowChildren
A strong boolean type denoting whether to automatically attach to newly created child processes.
Definition types.hxx:24
SystemCallNrX64
Native system call numbers as used by Linux on the x64 ABI.
Definition x64.hxx:31
void set_logger(cosmos::ILogger &_logger)
Configure a cosmos ILogger instance to use in the Clues library.
Definition logger.cxx:7
clues::SystemCallNr to_generic(const SystemCallNrAARCH64 nr)
Convert the native system call nr. into its generic representation.
Definition aarch64.cxx:21
bool is_default_abi(const ABI abi)
Returns whether the given ABI is default ABI for this system.
Definition utils.cxx:280
SystemCallNrI386
Native system call numbers as used by Linux on the i386 ABI.
Definition i386.hxx:32