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>
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>
35#include "TermTracer.hxx"
41std::string_view to_label(
const cosmos::ptrace::Event event) {
42 using Event = cosmos::ptrace::Event;
44 case Event::VFORK:
return "vfork()";
45 case Event::FORK:
return "fork()";
46 case Event::CLONE:
return "clone()";
47 default:
return "???";
54 std::cout <<
"(y/n) > ";
56 if (!std::cin.good() || yes_no ==
"n")
58 else if (yes_no ==
"y")
65TermTracer::TermTracer() :
69bool TermTracer::processPars() {
71 auto handle_bad_arg = [](
auto &arg) {
72 std::cerr <<
"bad argument to --" << arg.getName() <<
": '" << arg.getValue() <<
"'\n";
76 if (m_args.list_syscalls.isSet()) {
78 throw cosmos::ExitStatus::SUCCESS;
79 }
else if (m_args.list_abis.isSet()) {
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;
87 if (
const auto max_len = m_args.max_value_len.getValue(); max_len == 0)
90 m_par_truncation_len = SIZE_MAX;
92 m_par_truncation_len =
static_cast<size_t>(max_len);
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:"};
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());
112 return handle_bad_arg(m_args.follow_execve);
116 if (m_args.follow_children.isSet() && m_args.follow_children_switch.isSet()) {
117 std::cerr <<
"cannot set both '-f' and '--follow-children'\n";
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();
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;
133 return handle_bad_arg(m_args.follow_children);
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";
143 m_follow_children = FollowChildMode::THREADS;
146 if (m_args.syscall_filter.isSet()) {
147 auto parts = cosmos::split(m_args.syscall_filter.getValue(),
",");
149 auto translate_name = [](
const std::string_view name) {
154 std::cerr <<
"invalid system call name: "
156 throw cosmos::ExitStatus::FAILURE;
159 std::vector<SystemCallNr> to_add;
160 std::vector<SystemCallNr> to_remove;
162 for (
auto &part: parts) {
163 if (part.starts_with(
"!")) {
164 part = part.substr(1);
165 to_remove.push_back(translate_name(part));
167 to_add.push_back(translate_name(part));
171 for (
const auto nr: to_add) {
172 m_syscall_filter.insert(nr);
178 for (
const auto nr: to_remove) {
179 m_syscall_filter.erase(nr);
183 if (m_args.print_fd_info.isSet()) {
184 m_engine.setFormatFlags(Engine::FormatFlag::FD_INFO);
190void TermTracer::printSyscalls() {
192 for (
size_t nr = 1; nr < SYSTEM_CALL_NAMES.size(); nr++) {
193 auto NAME = SYSTEM_CALL_NAMES[nr];
196 std::cout << NAME <<
"\n";
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}) {
211 if (gen_sysnr == clues::SystemCallNr::UNKNOWN)
215 auto name = clues::SYSTEM_CALL_NAMES[cosmos::to_integral(gen_sysnr)];
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));
221 std::cout << cosmos::to_integral(sysnr);
227 clues::AnySystemCallNr sys_nr;
229 if (abi_str ==
"i386") {
231 }
else if (abi_str ==
"x86-64") {
233 }
else if (abi_str ==
"x32") {
234 sys_nr = SystemCallNrX32{};
235 }
else if (abi_str ==
"aarch64") {
238 throw cosmos::RuntimeError{
"unexpected abi string encountered"};
241 std::visit(print_syscalls, sys_nr);
244void TermTracer::printABIs() {
246 std::cout << clues::get_abi_label(abi) <<
"\n";
250void TermTracer::configureLogger() {
252 m_logger.setChannels(
true,
true,
false,
false);
253 m_logger.configFromEnvVar(
"CLUES_LOGGING");
257void TermTracer::printParsOnEntry(std::ostream &trace,
const SystemCall::ParameterVector &pars)
const {
284 const auto &last = pars.back();
286 for (
const auto &par: pars) {
287 if (clues::item::is_unused_par(*par)) {
289 }
else if (!par->isIn()) {
294 printPar(trace, *par);
296 if (
const auto is_last = &par == &last; !is_last) {
302void TermTracer::printParsOnExit(std::ostream &trace,
const SystemCall::ParameterVector &pars)
const {
306 const auto &last = pars.back();
307 bool seen_non_input =
false;
309 for (
const auto &par: pars) {
310 if (clues::item::is_unused_par(*par)) {
312 }
else if (!seen_non_input) {
317 seen_non_input =
true;
321 printPar(trace, *par);
323 if (
const auto is_last = &par == &last; !is_last) {
329std::string TermTracer::formatTraceeInvocation(
const Tracee &tracee) {
330 return formatTraceeInvocation(tracee.executable(), tracee.cmdLine());
333std::string TermTracer::formatTraceeInvocation(
const std::string &exe,
334 const cosmos::StringVector &cmdline)
const {
335 std::stringstream ss;
338 for (
const auto &arg: cmdline) {
344 ss <<
"\"" << arg <<
"\"";
352void TermTracer::printPar(std::ostream &trace,
const SystemCallItem &par)
const {
353 trace << (m_args.verbose.isSet() ? par.longName() : par.shortName());
356 auto value = par.str();
358 if (value.size() > m_par_truncation_len) {
359 value.resize(m_par_truncation_len);
363 trace <<
"=" << value;
369 const StatusFlags flags) {
377 auto defer_assign_syscall = cosmos::defer([
this, &tracee, &sc]() {
379 std::make_tuple(tracee.pid(), &sc));
382 const auto syscall_info = *tracee.currentSystemCallInfo();
393 trace <<
"<resuming previously interrupted " << sc.
name() <<
"...>\n";
395 trace << sc.
name() <<
"(";
403 const StatusFlags flags) {
405 if (isExecSyscall(sc) && sc.
result()) {
414 auto defer_reset_syscall = cosmos::defer([
this]() {
429 if (
auto res = sc.
result(); res) {
430 trace << res->str() <<
" (" << (
m_args.verbose.isSet() ? res->longName() : res->shortName()) <<
")";
432 const auto err = *sc.
error();
433 trace << err.str() <<
" (errno)";
437 trace <<
" (interrupted)";
444 traceStream(tracee) <<
"-- " << clues::format::sig_info(info) <<
" --\n";
454 traceStream(tracee) <<
"→ additionally attached thread\n";
456 auto [parent, event] = it->second;
458 traceStream(tracee) <<
"→ automatically attached (created by PID " << parent <<
" via " << to_label(event) <<
")\n";
461 traceStream(tracee) <<
"unknown Tracee " << cosmos::to_integral(tracee.pid()) <<
" attached?!";
472 trace <<
"--- <lost to exit or execve in another thread> ---\n";
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";
481 traceStream(tracee) <<
"+++ killed by signal " << cosmos::to_integral(status.termSig()->raw()) <<
" +++\n";
488 cleanupTracee(tracee);
500 traceStream(tracee) <<
"!!! disappeared (e.g. exit() or execve() in another thread)\n";
503 traceStream(tracee) <<
"+++ exited with " << cosmos::to_integral(*data.status) <<
" +++\n";
505 traceStream(tracee) <<
"+++ killed by signal " << cosmos::to_integral(data.signal->raw()) <<
" +++\n";
518 cleanupTracee(tracee);
522 const std::string &old_exe,
523 const cosmos::StringVector &old_cmdline,
524 const std::optional<cosmos::ProcessID> old_pid) {
544 traceStream(tracee) <<
"--- PID " << *old_pid <<
" is now known as PID " << tracee.pid() <<
" ---\n";
549 if (seenInitialExec()) {
550 traceStream(tracee) <<
"--- no longer running " << formatTraceeInvocation(old_exe, old_cmdline) <<
" ---\n";
552 traceStream(tracee) <<
"--- now running " << formatTraceeInvocation(tracee) <<
" ---\n";
554 if (!seenInitialExec()) {
557 if (!followExecutionContext(tracee)) {
558 traceStream(tracee) <<
"--- detaching after execve ---\n";
565 const cosmos::ptrace::Event event,
const StatusFlags flags) {
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) {
576 if (follow == FollowChildMode::YES) {
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";
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;
605 auto &stream = std::cerr;
617 trace <<
" <...unfinished>\n";
621 trace <<
"[" << tracee.pid() <<
"] ";
623 trace <<
"--- only PID " << tracee.pid() <<
" is remaining ---\n";
641 if (
const auto syscall = activeSyscall(tracee); syscall)
651bool TermTracer::isExecSyscall(
const SystemCall &sc)
const {
653 default:
return false;
654 case SystemCallNr::EXECVE:
655 case SystemCallNr::EXECVEAT:
664 if (pid == tracee.pid()) {
687 if (hasActiveSyscall(tracee))
696 assert(!node.empty());
697 auto sc = node.mapped();
700 trace <<
"<resuming ...> " << sc->
name();
701 if (sc->hasOutParameter()) {
708void TermTracer::cleanupTracee(
const Tracee &tracee) {
710 if (hasActiveSyscall(tracee)) {
711 m_active_syscall.reset();
713 m_unfinished_syscalls.erase(tracee.pid());
716 m_new_tracees.erase(tracee.pid());
718 if (--m_num_tracees == 1) {
719 m_flags.set(Flag::DROPPED_TO_LAST_TRACEE);
724 if (hasActiveSyscall(old_pid)) {
745 if (hasActiveSyscall(tracee)) {
747 }
else if (is_enabled) {
761 bool report_abi =
false;
773 traceStream(tracee) <<
"[system call ABI changed to " << get_abi_label(info.abi()) <<
"]\n";
777bool TermTracer::configureTracee(
const cosmos::ProcessID pid) {
779 m_flags.set(Flag::SEEN_INITIAL_EXEC);
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";
800 traceStream(*tracee,
false) <<
"--- tracing " << formatTraceeInvocation(*tracee) <<
" ---\n";
802 m_main_tracee_pid = tracee->pid();
807cosmos::ExitStatus TermTracer::main(
const int argc,
const char **argv) {
808 constexpr auto FAILURE = cosmos::ExitStatus::FAILURE;
809 m_args.cmdline.parse(argc, argv);
813 if (!processPars()) {
817 cosmos::signal::block(cosmos::SigSet{cosmos::signal::CHILD});
819 if (m_args.attach_proc.isSet()) {
820 if (!configureTracee(cosmos::ProcessID{m_args.attach_proc.getValue()})) {
825 cosmos::StringVector sv;
826 bool found_sep =
false;
828 for (
auto arg = 1; arg < argc; arg++) {
830 sv.push_back(argv[arg]);
831 }
else if (std::string{argv[arg]} ==
"--") {
837 std::cerr <<
"Neither -p nor command to execute after '--' was given. Nothing to do.\n";
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";
853 }
catch (
const std::exception &ex) {
854 std::cerr <<
"internal tracing error: " << ex.what() << std::endl;
857 cosmos::proc::exit(FAILURE);
860 auto status = cosmos::ExitStatus::SUCCESS;
869 if (m_main_status->exited()) {
870 status = *m_main_status->status();
871 }
else if (m_main_status->signaled()) {
877 status = cosmos::ExitStatus{128 + cosmos::to_integral(m_main_status->termSig()->raw())};
886int main(
const int argc,
const char **argv) {
887 return cosmos::main<clues::TermTracer>(argc, argv);
@ 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.
bool isInitiallyAttachedThread() const
Indicates whether this Tracee is an automatically attached thread.
bool detach()
Attempt to detach the Tracee.
size_t syscallCtr() const
Returns the number of system calls observed so far.
@ SYSCALL_ENTER_STOP
system call started.
std::optional< cosmos::Signal > stopSignal() const
For state() == State::GROUP_STOP this returns the stopping signal that caused it.
SystemCallNrAARCH64
Native system call numbers as used by Linux on the aarch64 ABI.
std::array< ABI, SUPPORTED_ABIS > get_supported_abis()
Returns a list of ABIs supported on the current platform.
std::optional< SystemCallNr > lookup_system_call(const std::string_view name)
Returns the SystemCallNr for the given system call name, if it exists.
cosmos::NamedBool< struct follow_children_t, true > FollowChildren
A strong boolean type denoting whether to automatically attach to newly created child processes.
SystemCallNrX64
Native system call numbers as used by Linux on the x64 ABI.
void set_logger(cosmos::ILogger &_logger)
Configure a cosmos ILogger instance to use in the Clues library.
clues::SystemCallNr to_generic(const SystemCallNrAARCH64 nr)
Convert the native system call nr. into its generic representation.
bool is_default_abi(const ABI abi)
Returns whether the given ABI is default ABI for this system.
SystemCallNrI386
Native system call numbers as used by Linux on the i386 ABI.