libclues
Linux C++ Tracing Library
Loading...
Searching...
No Matches
Tracee.cxx
1// C++
2#include <cstring>
3#include <fstream>
4
5// cosmos
6#include <cosmos/compiler.hxx>
7#include <cosmos/error/ApiError.hxx>
8#include <cosmos/error/RuntimeError.hxx>
9#include <cosmos/formatting.hxx>
10#include <cosmos/fs/DirStream.hxx>
11#include <cosmos/fs/filesystem.hxx>
12#include <cosmos/io/ILogger.hxx>
13
14// clues
15#include <clues/Engine.hxx>
16#include <clues/EventConsumer.hxx>
17#include <clues/items/clone.hxx>
18#include <clues/logger.hxx>
19#include <clues/RegisterSet.hxx>
20#include <clues/syscalls/process.hxx>
21#include <clues/sysnrs/generic.hxx>
22#include <clues/SystemCall.hxx>
23#include <clues/Tracee.hxx>
24#include <clues/utils.hxx>
25
26#define LOG_DEBUG_PID(X) LOG_DEBUG("[" << cosmos::to_integral(m_ptrace.pid()) << "] " << X)
27#define LOG_INFO_PID(X) LOG_INFO("[" << cosmos::to_integral(m_ptrace.pid()) << "] " << X)
28#define LOG_WARN_PID(X) LOG_WARN("[" << cosmos::to_integral(m_ptrace.pid()) << "] " << X)
29#define LOG_ERROR_PID(X) LOG_ERROR("[" << cosmos::to_integral(m_ptrace.pid()) << "] " << X)
30
31namespace {
32
34
38template <typename CONTAINER>
39class ContainerFiller {
40 using ptr_type = typename CONTAINER::pointer;
41public: // functions
42
43 explicit ContainerFiller(CONTAINER &container) :
44 m_container{container} {
45 }
46
47 bool operator()(long word) {
48 static_assert(std::is_trivial_v<typename CONTAINER::value_type> == true);
49 static_assert(sizeof(ITEM_SIZE) <= sizeof(long), "Unexpected ITEM_SIZE (must be <= sizeof(long))");
50 ptr_type word_ptr = reinterpret_cast<ptr_type>(&word);
51 typename CONTAINER::value_type item;
52
53 for (size_t numitem = 0; numitem < sizeof(word) / ITEM_SIZE; numitem++) {
54 std::memcpy(&item, word_ptr + numitem, sizeof(item));
55 if (item == 0)
56 // termination found
57 return false;
58
59 m_container.push_back(item);
60 }
61
62 return true;
63 }
64
65protected:
66 CONTAINER &m_container;
67 static constexpr size_t ITEM_SIZE = sizeof(typename CONTAINER::value_type);
68};
69
71class BlobFiller {
72public: // functions
73
74 BlobFiller(const size_t bytes, char *buffer) :
75 m_left{bytes},
76 m_buffer{buffer} {
77 }
78
79 bool operator()(long word) {
80 const size_t to_copy = std::min(sizeof(word), m_left);
81
82 std::memcpy(m_buffer, &word, to_copy);
83
84 m_buffer += to_copy;
85 m_left -= to_copy;
86
87 return m_left != 0;
88 }
89
90protected: // data
91
92 size_t m_left;
93 char *m_buffer;
94};
95
96} // end anon ns
97
98namespace clues {
99
100Tracee::Tracee(Engine &engine, EventConsumer &consumer, TraceePtr sibling) :
101 m_engine{engine},
102 m_consumer{consumer},
103 m_process_data{sibling ?
104 sibling->m_process_data :
105 std::make_shared<ProcessData>()} {
106}
107
108Tracee::~Tracee() {
110 LOG_WARN_PID("destroying Tracee in live state");
111 }
112}
113
114const char* Tracee::getStateLabel(const State state) {
115 switch (state) {
116 case State::UNKNOWN: return "UNKNOWN";
117 case State::RUNNING: return "RUNNING";
118 case State::SYSCALL_ENTER_STOP: return "SYSCALL_ENTER_STOP";
119 case State::SYSCALL_EXIT_STOP: return "SYSCALL_EXIT_STOP";
120 case State::SIGNAL_DELIVERY_STOP: return "SIGNAL_DELIVERY_STOP";
121 case State::GROUP_STOP: return "GROUP_STOP";
122 case State::EVENT_STOP: return "EVENT_STOP";
123 case State::DEAD: return "DEAD";
124 case State::DETACHED: return "DETACHED";
125 default: return "???";
126 }
127}
128
129void Tracee::setPID(const cosmos::ProcessID tracee) {
130 m_ptrace = cosmos::Tracee{tracee};
131}
132
133void Tracee::attach(const FollowChildren follow_children, const AttachThreads attach_threads) {
134 using Opt = cosmos::ptrace::Opt;
135 m_ptrace_opts = cosmos::ptrace::Opts{
136 Opt::TRACESYSGOOD,
137 Opt::TRACEEXIT,
138 Opt::TRACEEXEC,
139 };
140
141 if (follow_children) {
142 m_ptrace_opts.set({
143 Opt::TRACECLONE,
144 Opt::TRACEFORK,
145 Opt::TRACEVFORK,
146 Opt::TRACEVFORKDONE
147 });
148 }
149
150 try {
152 } catch (...) {
153 if (!isChildProcess()) {
154 changeState(State::DEAD);
155 }
156 throw;
157 }
158 updateExecutable();
159 updateCmdLine();
160 interrupt();
163 // send SIGCONT via kill() instead of via a ptrace injected
164 // signal. For some reason, when injecting the signal via
165 // ptrace, the stopped state will be applied again after
166 // performing a PTRACE_DETACH operation. This could even be a
167 // kernel bug.
168 cosmos::signal::send(m_ptrace.pid(), cosmos::signal::CONT);
170 }
171
172 if (attach_threads) {
174 }
175}
176
178 if (!m_ptrace.valid()) {
179 return true;
180 } else if (m_state == State::DEAD || m_state == State::DETACHED) {
181 return true;
182 }
183
185 // we can detach immediately
186 doDetach();
187 return true;
188 }
189
190 // otherwise we need to indicate that we want to detach at the
191 // next ptrace stop we're seeing
193
195 // interrupt, if not still pending, for being able to detach
196 try {
197 interrupt();
198 } catch (const cosmos::ApiError &error) {
199 handleError(error);
200 }
201 }
202
203 return false;
204}
205
207 try {
208 m_ptrace.detach();
209 changeState(State::DETACHED);
210 } catch (const cosmos::ApiError &error) {
211 handleError(error);
212 }
213
214 m_ptrace = cosmos::Tracee{};
215 m_flags = {};
216 m_initial_attacher.reset();
217}
218
219void Tracee::handleError(const cosmos::ApiError &error) {
220 if (error.errnum() == cosmos::Errno::SEARCH) {
221 // some execve() scenarios end up without a proper
222 // exit notification. ignore them.
224 currentSystemCallNr() != SystemCallNr::EXECVE) {
225 LOG_WARN_PID("tracee found to be already dead upon detach()/interrupt()");
226 }
227 // already gone for some reason
228 changeState(State::DEAD);
229 } else {
230 // shouldn't really happen, unless we're not a
231 // tracee for the process at all anyway
232 m_ptrace = cosmos::Tracee{};
233 throw;
234 }
235}
236
239 return false;
240
241 const auto &syscall = *m_current_syscall;
242
243 switch (syscall.callNr()) {
244 case SystemCallNr::CLONE: {
245 auto &clone_sc = dynamic_cast<const CloneSystemCall&>(syscall);
246 return clone_sc.flags.flags()[cosmos::CloneFlag::THREAD];
247 } case SystemCallNr::CLONE3: {
248 auto &clone3_sc = dynamic_cast<const Clone3SystemCall&>(syscall);
249 const auto &args = *clone3_sc.cl_args.args();
250 return args.flags()[cosmos::CloneFlag::THREAD];
251 } default: {
252 return false;
253 }
254 }
255}
256
257void Tracee::updateExecutable() {
258 // obtain the executable name from proc.
259 // as long as we're tracing the PID there is no race involved.
260 // this information is more reliable than what we see in execve(),
261 // since symlinks or other magic might be involved.
262 const auto path = cosmos::sprintf("/proc/%d/exe", cosmos::to_integral(m_ptrace.pid()));
263 try {
264 m_process_data->executable = cosmos::fs::read_symlink(path);
265 } catch (const cosmos::ApiError &e) {
266 // likely ENOENT, tracee disappeared
267 // but can this even happen while we're tracing it? and even
268 // if it happens, then no more tracing will happen, so the
269 // information is no longer relevant.
270 //
271 // As a fallback we could rely on the less precise information
272 // from evecve-entry. The issue is that the execve-event-stop
273 // happens before system-call-exit-stop and also that in
274 // multi-threaded contexts, the actual execve system call data
275 // might be in a wholly different tracee, or one that we're
276 // not even tracing.
277 //
278 // so let's ignore this for the moment, assuming that this is
279 // not a relevant situation anyway.
280 m_process_data->executable.clear();
281 }
282}
283
284void Tracee::updateCmdLine() {
285 // the cmdline file contains null terminator separated strings
286 // the tracee controls this data and could place crafted data here.
287 //
288 // obtain the data from here instead of from execve() has the
289 // following advantages:
290 // - it works the same for newly created tracee's as for tracee's
291 // we're attaching to during runtime.
292 // - we can obtain the information without having to find the correct
293 // syscall-exit stop context that caused a ptrace-exec event stop.
294 const auto path = cosmos::sprintf("/proc/%d/cmdline", cosmos::to_integral(m_ptrace.pid()));
295
296 m_process_data->cmdline.clear();
297
298 std::ifstream is{path};
299 if (!is) {
300 // see updateExecutable() for the rationale here
301 return;
302 }
303 std::string arg;
304 while (std::getline(is, arg, '\0').good()) {
305 m_process_data->cmdline.push_back(arg);
306 }
307}
308
309void Tracee::syncFDsAfterExec() {
310 /*
311 * We could mimic what the kernel does by inspecting the file
312 * descriptor flags. To prevent any inconsistencies and because it
313 * shouldn't bee too expensive to do this for each execve(),
314 * synchronize the state with what is found in /proc/<pid>/fd.
315 */
316 try {
317 auto left_fds = clues::get_currently_open_fds(m_ptrace.pid());
318
319 auto &fd_info_map = m_process_data->fd_info_map;
320
321 for (auto it = fd_info_map.begin(); it != fd_info_map.end(); it++) {
322 if (left_fds.count(it->first) == 0) {
323 it = fd_info_map.erase(it);
324 }
325 }
326 } catch (const cosmos::ApiError &ex) {
327 /* It seems the process died.
328 * The Tracee will be cleaned up soon anyway, so let's keep
329 * the file descriptors we've seen last time to avoid further
330 * confusion.
331 */
332 LOG_WARN_PID("unable to get currently open fds: " << ex.what());
333 }
334}
335
336void Tracee::changeState(const State new_state) {
337 LOG_DEBUG_PID("state " << m_state << " → " << new_state);
338
339 if (new_state == State::SYSCALL_ENTER_STOP) {
341 } else if (new_state == State::SYSCALL_EXIT_STOP) {
343 } else if (new_state == State::GROUP_STOP) {
345 // our own injected group stop
346 // let the tracee continue and we're tracing syscalls now
348 m_restart_mode = cosmos::Tracee::RestartMode::SYSCALL;
349 } else {
350 // enter listen state to avoid restarting a stopped
351 // process as a side-effect.
352 m_restart_mode = cosmos::Tracee::RestartMode::LISTEN;
353 }
354 } else if (new_state == State::DETACHED) {
356 } else if (new_state == State::DEAD) {
357 if (isChildProcess()) {
358 cleanupChild();
359 }
360 }
361
362 if (m_state != State::RUNNING)
364
366 m_stop_signal = std::nullopt;
367
368 m_state = new_state;
369}
370
371void Tracee::handleStateMismatch() {
372 LOG_ERROR_PID("encountered system call state mismatch: we believe "
373 << (m_flags[Flag::SYSCALL_ENTERED] ? "we already entered " : "we did not enter ")
374 << "a system call, but we got a "
375 << (m_syscall_info->isEntry() ? "SyscallInfo::ENTRY" : "SyscallInfo::EXIT")
376 << " event.");
377
378 throw cosmos::RuntimeError{"system call state mismatch"};
379}
380
381void Tracee::handleSystemCall() {
382
383 m_syscall_info = SystemCallInfo{};
384 auto &info = *m_syscall_info;
385
386 // NOTE: this call can fail if the tracee was killed meanwhile
387 m_ptrace.getSyscallInfo(info);
388
390 if (!info.isExit()) {
391 handleStateMismatch();
392 }
393
394 changeState(State::SYSCALL_EXIT_STOP);
395 handleSystemCallExit();
396 } else {
397 if (!info.isEntry()) {
398 handleStateMismatch();
399 }
400
401 m_syscall_info->updateSysNr();
402
403 changeState(State::SYSCALL_ENTER_STOP);
404 handleSystemCallEntry();
405 }
406
407 m_syscall_info.reset();
408}
409
410void Tracee::handleSystemCallEntry() {
411 EventConsumer::StatusFlags flags;
412
413 const SystemCallNr nr = m_syscall_info->sysNr();
415
416 if (const auto last_abi = m_current_syscall->abi();
417 last_abi != ABI::UNKNOWN &&
418 last_abi != m_syscall_info->abi()) {
420 }
421
422 verifyArch();
423
424 if (nr == SystemCallNr::RESTART_SYSCALL) {
427 m_interrupted_syscall = nullptr;
429 } else if (m_syscall_ctr != 0) {
430 // explicit restart_syscall done by user space?
431 LOG_WARN_PID("unknown system call is resumed");
432 } else if (auto orig_syscall = getInitialSyscallNr(m_syscall_info->abi()); orig_syscall) {
433 // this happens when attaching to a non-child process
434 // and a system call like clock_nanosleep is
435 // interrupted as a result.
436 // we cannot know which system call is being resumed,
437 // since we have no history. restart_syscall() carries
438 // no additional context information pointing us to
439 // the kind of system call that is being resumed.
440 //
441 // it would be quite a feature, though, to know which
442 // system call is being resumed, as it happens quite
443 // often that a process is attached that behaves
444 // unusually e.g. because it blocks.
445
446 // m_initial_regset is obtained during the initial
447 // ptrace-event-stop. It seems it contains the
448 // currently running system call. This only works if
449 // the system call was not interrupted before already,
450 // though.
451
452 if (*orig_syscall != SystemCallNr::RESTART_SYSCALL && SystemCall::validNr(*orig_syscall)) {
453 m_current_syscall = &m_syscall_db.get(*orig_syscall);
455 }
456 }
458 if (m_interrupted_syscall->callNr() == m_current_syscall->callNr()) {
459 /*
460 * no restart_syscall is used in this case, but we can
461 * still set the flag and cleanup state.
462 */
464 } else {
465 LOG_WARN_PID("unknown system call is resumed after sigreturn");
466 }
467
469 m_interrupted_syscall = nullptr;
470 }
471
472 m_current_syscall->setEntryInfo(*this, *m_syscall_info);
474 m_consumer.syscallEntry(*this, *m_current_syscall, flags);
475}
476
477void Tracee::handleSystemCallExit() {
478 EventConsumer::StatusFlags flags;
479 auto &syscall = *m_current_syscall;
480
481 syscall.setExitInfo(*this, *m_syscall_info);
482
483 if (auto error = syscall.error(); error && error->hasKernelErrorCode()) {
484 // system call was interrupted, remember it for later
487 }
488
489 m_consumer.syscallExit(*this, syscall, flags);
490
491 auto is_sigreturn = [](const SystemCall &call) -> bool {
492 switch (call.callNr()) {
493 case SystemCallNr::RT_SIGRETURN: [[fallthrough]];
494 // although this looks legacy it is still used on the I386 ABI
495 case SystemCallNr::SIGRETURN: return true;
496 default: return false;
497 }
498 };
499
500 /*
501 * check if this is a sigreturn() system calling marking the end of
502 * asynchronous signal handler execution and thus the end of the
503 * interruption.
504 */
505 if (m_interrupted_syscall && is_sigreturn(syscall)) {
506 /*
507 * to differentiate transparent restart and visible EINTR
508 * returns we need to check this syscall's return value. If it
509 * succeeds then it is a transparent restart, otherwise a
510 * visible EINTR.
511 */
512 if (const auto error = syscall.error(); error &&
513 error->hasErrorCode() &&
514 error->errorCode() == cosmos::Errno::INTERRUPTED) {
515 m_interrupted_syscall = nullptr;
516 } else {
518 }
519 }
520
521 m_current_syscall = nullptr;
522}
523
524void Tracee::handleSignal(const cosmos::SigInfo &info) {
525 LOG_INFO_PID("Signal: " << info.sigNr());
526
527 if (m_flags[Flag::INJECTED_SIGCONT] && info.sigNr() == cosmos::signal::CONT) {
528 // ignore injected SIGCONT
530 return;
531 }
532
533 m_consumer.signaled(*this, info);
534}
535
536void Tracee::handleEvent(const cosmos::ChildState &data,
537 const cosmos::ptrace::Event event,
538 const cosmos::Signal signal) {
539 LOG_INFO_PID("PTRACE_EVENT_" << get_ptrace_event_str(event) << " (" << signal << ")");
540
541 using Event = cosmos::ptrace::Event;
542
543 switch(event) {
544 case Event::STOP: return handleStopEvent(signal);
545 case Event::EXIT: return handleExitEvent();
546 case Event::EXEC: return handleExecEvent(data.child.pid);
547 case Event::CLONE:
548 case Event::VFORK:
549 case Event::VFORK_DONE:
550 case Event::FORK:
551 return handleNewChildEvent(event);
552 default: LOG_WARN_PID("PTRACE_EVENT unhandled");
553 }
554}
555
556void Tracee::handleStopEvent(const cosmos::Signal signal) {
558 // this is the initial ptrace-stop. now we can start tracing
559 LOG_INFO_PID("initial ptrace-stop");
560 handleAttached();
561 } else if (cosmos::in_container(signal, STOPPING_SIGNALS)) {
562 // must be a group stop, unless we're tracing
563 // automatically-attached children, which is not yet
564 // implemented
565 changeState(State::GROUP_STOP);
566 m_stop_signal = signal;
567 m_consumer.stopped(*this);
568 } else if (signal == cosmos::signal::TRAP) {
569 // SIGTRAP has quite a blurry meaning in the ptrace()
570 // API. From practical experiments and the original
571 // strace code I could deduce that for some reason
572 // PTRACE_EVENT_STOP combined with SIGTRAP is observed
573 // when a SIGCONT is received by a tracee that is
574 // currently in group-stop.
575 // We then need to restart using system call tracing
576 // to see the actual SIGCONT being delivered.
577 m_restart_mode = cosmos::Tracee::RestartMode::SYSCALL;
578 }
579}
580
581void Tracee::handleExitEvent() {
582
583 EventConsumer::StatusFlags flags;
584
585 /*
586 * Detecting "lost to execve" is a bit of a heuristic:
587 *
588 * - regular exit happens due to exit_group(2). No SYSCALL_EXIT_STOP
589 * will be reported for this.
590 * - exit due to signal. SIGNAL_DELIVERY_STOP should have been the
591 * last state we saw.
592 * - anything else should be execve in multi-threaded process.
593 */
594
595 const auto wait_status = m_ptrace.getExitEventMsg();
596
597 if (wait_status.exited() &&
598 (prevState() != State::SYSCALL_ENTER_STOP ||
599 !cosmos::in_list(*currentSystemCallNr(),
600 {SystemCallNr::EXIT_GROUP, SystemCallNr::EXIT}))) {
601 // the exit status in case of an execve() by another thread is simply 0;
602 // otherwise the exit status specified by the other thread
603 //
604 // it seems we cannot really differentiate the exact reason for
605 // the exit here, see also "death under ptrace" in ptrace(2).
606 //
607 // in theory, provided we're tracing all threads of a process,
608 // we could inspect all other threads if any has an exit or
609 // execve() system call running, but this would still be racy
610 // in a lot of ways.
611 LOG_INFO_PID("multi-threading related EVENT_EXIT detected");
613 }
614
615 m_consumer.exited(*this, wait_status, flags);
616
618}
619
620void Tracee::handleExecEvent(const cosmos::ProcessID main_pid) {
621 const auto old_exe = m_process_data->executable;
622 const auto old_cmdline = m_process_data->cmdline;
623
624 // in a multi-threaded conext we can receive this event under a
625 // different PID than we currently have. Thus use a dedicated ptrace
626 // wrapper here until we maybe call setPID() below..
627 cosmos::Tracee ptrace{main_pid};
628
629 std::optional<cosmos::ProcessID> old_pid;
630
631 // for a regular execve() this returns the same pid as we have, ignore that
632 if (const auto former_pid = ptrace.getPIDEventMsg(); former_pid != main_pid) {
633 old_pid = former_pid;
634 }
635
636 if (old_pid) {
637 auto old_tracee = m_engine.handleSubstitution(*old_pid);
638 /*
639 * we will receive this event already under the new PID i.e.
640 * if we trace the main thread (which doesn't exec) and the
641 * exec'ing thread as well then we will receive this in main
642 * thread context.
643 *
644 * The "old" main thread no longer exists and we need to sync
645 * state with the actually exec()'ing thread.
646 *
647 * This is good for us currently, because if we have a
648 * ChildTracee running then it will be the main thread and
649 * this thread will stay the main thread in the newly
650 * executing process no matter what. Otherwise we'd have to
651 * abandon the ChildTracee, which holds a SubProc that needs
652 * to be wait()'ed on.
653 */
654 if (old_tracee) {
655 syncState(*old_tracee);
656 } else {
657 /*
658 * If the old main thread was not traced at all then
659 * `old_tracee` is not available here and we are
660 * already the correct Tracee (Engine takes care of
661 * that).
662 * We have to actively change our own PID, though.
663 */
664 setPID(ptrace.pid());
665 }
666 }
667
668 // only update executable info here, since our PID might have changed
669 // above
670 updateExecInfo();
671
672 /*
673 * Check which file descriptors have been closed due to execve.
674 */
675 syncFDsAfterExec();
676
677 m_consumer.newExecutionContext(*this, old_exe, old_cmdline, old_pid);
678}
679
680void Tracee::handleNewChildEvent(const cosmos::ptrace::Event event) {
681 // the engine will perform callbacks at m_consumer, since it is
682 // responsible for creating the new Tracee instance.
683 //
684 // m_current_syscall should always be valid at this point, since the
685 // new child must have been created some way.
686 m_engine.handleAutoAttach(*this, m_ptrace.getPIDEventMsg(), event, *m_current_syscall);
687}
688
689void Tracee::handleAttached() {
691
693 m_restart_mode = cosmos::Tracee::RestartMode::SYSCALL;
694 }
695
696 getInitialRegisters();
697
698 if (m_process_data->fd_info_map.empty()) {
699 auto &map = m_process_data->fd_info_map;
700 /*
701 * this is a root tracee, either a direct child process or an
702 * initially attached foreign process
703 */
704 try {
705 for (auto &info: get_fd_infos(pid())) {
706 map.insert({info.fd, std::move(info)});
707 }
708 } catch (const cosmos::CosmosError &error) {
709 LOG_WARN_PID("failed to get initial FD infos: " << error.what());
710 }
711 }
712
713 m_consumer.attached(*this);
714
716 try {
718 } catch (const std::exception &e) {
719 LOG_ERROR_PID("failed to attach to other threads: " << e.what());
720 }
722 }
723}
724
726 const auto path = cosmos::sprintf("/proc/%d/task",
727 cosmos::to_integral(m_ptrace.pid()));
728
729 /*
730 * TODO: there is a race condition involved here:
731 *
732 * We are stopping one of the threads but all the others are still
733 * running while we're looking into /proc. Two things can happen here:
734 *
735 * - threads we're trying to attach might already be lost by the time
736 * we're trying to call ptrace() on them. This is not much of an
737 * issue.
738 * - new threads might come into existence that we don't catch in
739 * /proc, and we'll never trace them, giving an incomplete picture.
740 *
741 * It would be safer to send a SIGSTOP to the whole thread group
742 * before attaching, but this would make the attach procedure even
743 * more complicated than it already is.
744 *
745 * It seems `strace` does not take any precautions regarding this
746 * matter neither.
747 *
748 * We could look at /proc/<pid>/status, which has a thread count
749 * field. This is again racy, however. We could do this after the
750 * fact for verification, or iterate over /proc/<pid>/task until we're
751 * sure we've attached all threads.
752 */
753 cosmos::DirStream task_dir{path};
754 const FollowChildren follow_children{
755 m_ptrace_opts[cosmos::ptrace::Opt::TRACEFORK]};
756
757 for (const auto &entry: task_dir) {
758 if (entry.isDotEntry() ||
759 entry.type() != cosmos::DirEntry::Type::DIRECTORY)
760 continue;
761
762 auto pid = cosmos::ProcessID{static_cast<int>(std::strtol(entry.name(), nullptr, 10))};
763 if (pid == m_ptrace.pid())
764 // ourselves
765 continue;
766
767 try {
768 auto tracee = m_engine.addTracee(
769 pid, follow_children, AttachThreads{false}, this->pid());
770 tracee->m_initial_attacher = m_ptrace.pid();
771 } catch (const std::exception &ex) {
772 LOG_WARN_PID("failed to attach to thread " << cosmos::to_integral(pid) << ": " << ex.what());
773 }
774 }
775}
776
778 /*
779 * A Tracee can change "personality" during its lifetime, thus every
780 * system call has to be observed for changes in ABI. In theory the
781 * same binary could even employ multiple calling conventions at the
782 * same time.
783 *
784 * The usual situation is that 32-bit programs can also be executed on
785 * 64-bit operating systems. This is not only the case on x86_64 but
786 * also on powerpc64, arm64 etc. On x86 we even have three different
787 * ABIs: i386, x86_64 and x32. The latter is using the amd64
788 * instruction set but only 32-bit data types.
789 *
790 * To support these different ABIs we use an abstract SystemCallNr
791 * which is the same for all ABIs, allowing us to identify the correct
792 * system calls. Differences between ABIs might still need to be taken
793 * into account by concrete SystemCall instances (e.g. differently
794 * sized structures when running 32-bit emulation binaries).
795 */
796 using Arch = cosmos::ptrace::Arch;
797 switch (m_syscall_info->arch()) {
798 case Arch::I386:
799 if (cosmos::arch::X86_64) {
800 // 32-bit emulation, should work out of the box
801 }
802 return;
803 case Arch::X86_64:
804 if (!cosmos::arch::X86_64) {
805 throw cosmos::RuntimeError{"tracing 64-bit binaries with a 32-bit tracer is not supported"};
806 }
807 return;
808 case Arch::AARCH64:
809 if (!cosmos::arch::AARCH64) {
810 throw cosmos::RuntimeError{"tracing 64-bit binaries with a 32-bit tracer is not supported"};
811 }
812 return;
813 default:
814 throw cosmos::RuntimeError{"tracing this architecture/ABI is not currently supported"};
815 }
816}
817
818std::optional<SystemCallNr> Tracee::getInitialSyscallNr(const ABI abi) const {
819 auto visitor = [abi](const auto &rs) -> std::optional<SystemCallNr> {
820 if (abi == rs.ABI)
821 return rs.syscallNr();
822 return {};
823 };
824
825 return std::visit(visitor, m_initial_regset);
826}
827
828void Tracee::syncState(Tracee &other) {
830 m_syscall_db = std::move(other.m_syscall_db);
834 other.changeState(State::DEAD);
835}
836
837void Tracee::processEvent(const cosmos::ChildState &data) {
838 m_inject_sig = {};
839
840 if (data.exited() || data.killed() || data.dumped()) {
841 changeState(State::DEAD);
843 m_consumer.disappeared(*this, data);
844 }
845 m_exit_data = data;
846 return;
847 } else if (data.trapped()) {
849 doDetach();
850 return;
851 }
852
853 if (data.signal == cosmos::signal::SYS_TRAP) {
854 try {
855 handleSystemCall();
856 } catch (const std::exception &ex) {
857 /*
858 * TODO: we need some way to robustly deal
859 * with unexpected errors during tracing.
860 *
861 * we could offer an API to lib clients to let
862 * them decide what to do.
863 */
864 throw;
865 }
866 } else if (data.signal->isPtraceEventStop()) {
867 changeState(State::EVENT_STOP);
868 const auto [signr, event] = cosmos::ptrace::decode_event(*data.signal);
869 handleEvent(data, event, cosmos::Signal{signr});
870 } else {
871 changeState(State::SIGNAL_DELIVERY_STOP);
872 cosmos::SigInfo info;
873 m_ptrace.getSigInfo(info);
874 handleSignal(info);
875 m_inject_sig = *data.signal;
876 }
877 } else if (data.signaled()) {
878 // it seems this branch is never hit, signals are
879 // always handled as a TRAP, never as regular events
880 // when tracing.
881 // TODO: but for direct child processes that we want to stop
882 // tracing this could happen after detaching.
883 LOG_WARN_PID("seeing non-trap signal delivery stop");
884 changeState(State::SIGNAL_DELIVERY_STOP);
885 cosmos::SigInfo info;
886 m_ptrace.getSigInfo(info);
887 handleSignal(info);
888 m_inject_sig = *data.signal;
889 } else {
890 LOG_WARN_PID("Other Tracee event?");
891 }
892
894 return;
895
896 // NOTE: this can fail with ESRCH if the tracee got killed meanwhile
898
899 if (m_restart_mode != cosmos::Tracee::RestartMode::LISTEN) {
900 const auto resumed = m_state == State::GROUP_STOP;
901 changeState(State::RUNNING);
902 if (resumed) {
903 m_consumer.resumed(*this);
904 }
905 }
906}
907
908template <ABI abi>
910 cosmos::InputMemoryRegion iovec;
911 rs.fillIov(iovec);
912
913 m_ptrace.getRegisterSet(rs.registerType(), iovec);
914 rs.iovFilled(iovec);
915}
916
917long Tracee::getData(const ForeignPtr addr) const {
918 return m_ptrace.peekData(reinterpret_cast<const long*>(
919 cosmos::to_integral(addr)));
920}
921
923template <typename FILLER>
924void Tracee::fillData(ForeignPtr addr, FILLER &filler) const {
925 long word;
926
927 do {
928 word = getData(addr);
929
930 // get the next word
931 addr++;
932 } while (filler(word));
933}
934
935void Tracee::readString(const ForeignPtr addr, std::string &out) const {
936 return readVector(addr, out);
937}
938
939template <typename VECTOR>
940void Tracee::readVector(const ForeignPtr addr, VECTOR &out) const {
941 out.clear();
942
943 ContainerFiller<VECTOR> filler{out};
944 fillData(addr, filler);
945}
946
948 const auto path = cosmos::sprintf("/proc/%d/status",
949 cosmos::to_integral(m_ptrace.pid()));
950 std::ifstream is{path};
951
952 if (!is) {
953 // Tracee disappeared?!
954 return false;
955 }
956
957 std::string line;
958
959 while (std::getline(is, line).good()) {
960 auto parts = cosmos::split(line, ":",
961 cosmos::SplitFlags{cosmos::SplitFlag::STRIP_PARTS});
962 if (parts[0] == "Tgid") {
963 auto tgid = std::strtol(parts[1].c_str(), nullptr, 10);
964 return cosmos::ProcessID{static_cast<int>(tgid)} == m_ptrace.pid();
965 }
966 }
967
968 throw cosmos::RuntimeError{"failed to parse Tgid"};
969
970 // nothing found?!
971 return false;
972}
973
974std::optional<SystemCallNr> Tracee::currentSystemCallNr() const {
976 return {};
977
978 return m_current_syscall->callNr();
979}
980
981void Tracee::readBlob(const ForeignPtr addr, char *buffer, const size_t bytes) const {
982 BlobFiller filler{bytes, buffer};
983 fillData(addr, filler);
984}
985
986void Tracee::getInitialRegisters() {
987 /*
988 * TODO: we don't know on which ABI we're sitting on currently, we
989 * could inspect the ELF binary? (like strace does?), but it's also
990 * only kind of a heuristic.
991 */
992
993 try {
995 getRegisters(rs);
996 m_initial_regset = rs;
997 if constexpr (rs.ABI == ABI::X86_64) {
998 /*
999 * detecting the X32 ABI is hard when fetching the
1000 * initial registers. We need to check for the syscall
1001 * bit and redo using the proper ABI in this case.
1002 */
1003 if ((cosmos::to_integral(rs.abiSyscallNr()) & X32_SYSCALL_BIT) != 0) {
1004 RegisterSet<ABI::X32> rs_x32;
1005 getRegisters(rs_x32);
1006 m_initial_regset = rs_x32;
1007 }
1008 }
1009 } catch (const cosmos::RuntimeError &) {
1010 if (get_default_abi() == ABI::X86_64) {
1011 RegisterSet<ABI::I386> rs2;
1012 getRegisters(rs2);
1013 m_initial_regset = rs2;
1014 } else {
1015 throw;
1016 }
1017 }
1018}
1019
1020void Tracee::unshareProcessData() {
1021 m_process_data = std::make_shared<ProcessData>(*m_process_data);
1022}
1023
1024void Tracee::trackFD(FDInfo &&info) const {
1025 const auto fd = info.fd;
1026 auto res = m_process_data->fd_info_map.insert(
1027 std::make_pair(fd, std::move(info))
1028 );
1029
1030 if (!res.second) {
1031 LOG_WARN_PID("FD " << cosmos::to_integral(fd) << " was already open?!");
1032 // bail out
1033 return;
1034 }
1035
1036 LOG_DEBUG_PID("new file descriptor tracking for fd " << cosmos::to_integral(fd));
1037}
1038
1039void Tracee::dropFD(const cosmos::FileNum fd) const {
1040 if (m_process_data->fd_info_map.erase(fd) != 0) {
1041 LOG_DEBUG_PID("removed fd " << cosmos::to_integral(fd) << " from registered mappings");
1042 } else {
1043 LOG_WARN_PID("closed fd " << cosmos::to_integral(fd) << " wasn't open before?!");
1044 }
1045}
1046
1047// explicit template instantiations
1048#if !defined(COSMOS_I386) && !defined(COSMOS_X32)
1049template void Tracee::readVector<std::vector<uintptr_t>>(const ForeignPtr, std::vector<uintptr_t>&) const;
1050#endif
1051/* for 32-bit emulation */
1052template void Tracee::readVector<std::vector<uint32_t>>(const ForeignPtr, std::vector<uint32_t>&) const;
1053
1054} // end ns
1055
1056std::ostream& operator<<(std::ostream &o, const clues::Tracee::State &state) {
1057 o << clues::Tracee::getStateLabel(state);
1058 return o;
1059}
Callback interface for consumers of tracing events.
@ ABI_CHANGED
The system call ABI changed since the last observed system call.
@ 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()).
This type contains data that is shared between tracees of the same thread group.
Holds a set of registers for the given ABI.
void fillIov(cosmos::InputMemoryRegion &iov)
Prepares iov for doing a ptrace system call of type ptrace::Request::GETREGSET.
void iovFilled(const cosmos::InputMemoryRegion &iov)
Verify data received from a ptrace system call of type ptrace::Request::GETREGSET.
auto abiSyscallNr() const
Returns the ABI-specific system call number on entry to a syscall.
static constexpr cosmos::ptrace::RegisterType registerType()
The type to pass to ptrace::Request::GETREGSET for obtaining the general purpose registers.
static bool validNr(const SystemCallNr nr)
Returns whether the given system call number is in a valid range.
Base class for traced processes.
Definition Tracee.hxx:39
bool hasClonedThread() const
Returns whether the current/last seen system call was a clone() for a thread.
Definition Tracee.cxx:237
void setPID(const cosmos::ProcessID tracee)
Sets the tracee PID.
Definition Tracee.cxx:129
void attachThreads()
Definition Tracee.cxx:725
void trackFD(FDInfo &&info) const
Track a new file descriptor for this Tracee's thread group.
Definition Tracee.cxx:1024
void verifyArch()
Verifies the tracee's architecture according to m_syscall_info, throws on mismatch.
Definition Tracee.cxx:777
cosmos::ptrace::Opts m_ptrace_opts
The options we've set for ptrace().
Definition Tracee.hxx:446
ProcessDataPtr m_process_data
Shared process data.
Definition Tracee.hxx:470
bool isThreadGroupLeader() const
Checks in the proc file system whether the Tracee is a thread group leader.
Definition Tracee.cxx:947
@ INJECTED_SIGSTOP
whether we've injected a SIGSTOP that needs to be undone.
Definition Tracee.hxx:64
@ SYSCALL_ENTERED
we've seen a syscall-enter-stop and are waiting for the corresponding exit-stop.
Definition Tracee.hxx:66
@ SEEN_SIGRETURN
a sigreturn with a pending interrupted system call has been observed.
Definition Tracee.hxx:69
@ INJECTED_SIGCONT
whether we've injected a SIGCONT that needs to be ignored.
Definition Tracee.hxx:65
@ DETACH_AT_NEXT_STOP
as soon as the tracee reaches a stop stace, detach from it.
Definition Tracee.hxx:63
@ WAIT_FOR_ATTACH_STOP
we're still waiting for the PTRACE_INTERRUPT event stop.
Definition Tracee.hxx:62
@ WAIT_FOR_EXITED
we've already seen PTHREAD_EVENT_EXIT but are still waiting for CLD_EXITED.
Definition Tracee.hxx:67
@ ATTACH_THREADS_PENDING
attach all threads of the process as soon as the initial event stop happens.
Definition Tracee.hxx:68
std::optional< cosmos::Signal > m_stop_signal
For State::GROUP_STOP this contains the signal that caused it.
Definition Tracee.hxx:468
void handleError(const cosmos::ApiError &error)
Handle ApiErrors raised from ptrace() requests.
Definition Tracee.cxx:219
bool detach()
Attempt to detach the Tracee.
Definition Tracee.cxx:177
Tracee(Engine &engine, EventConsumer &consumer, TraceePtr sibling=nullptr)
Definition Tracee.cxx:100
void seize(const cosmos::ptrace::Opts opts)
Makes the tracee a tracee.
Definition Tracee.hxx:344
size_t m_syscall_ctr
Number of system calls observed.
Definition Tracee.hxx:464
void doDetach()
Actually perform a detach without checking tracee state.
Definition Tracee.cxx:206
SystemCall * m_current_syscall
Holds state for the currently executing system call.
Definition Tracee.hxx:452
void dropFD(const cosmos::FileNum fd) const
Drop a file descriptor from the tracking of the Tracee's thread group.
Definition Tracee.cxx:1039
Engine & m_engine
The engine that manages this tracee.
Definition Tracee.hxx:434
std::optional< cosmos::ChildState > m_exit_data
If tracee exit was observed then this contains the final exit data.
Definition Tracee.hxx:460
Flags m_flags
These keep track of various state on the tracer side.
Definition Tracee.hxx:442
void readVector(const ForeignPtr pointer, VECTOR &out) const
Reads in a zero terminated array of data items into the STL-vector like parameter out.
Definition Tracee.cxx:940
cosmos::Tracee m_ptrace
libcosmos API for the Tracee.
Definition Tracee.hxx:444
void processEvent(const cosmos::ChildState &data)
Process the given ptrace event.
Definition Tracee.cxx:837
long getData(const ForeignPtr addr) const
Reads a word of data from the tracee's memory.
Definition Tracee.cxx:917
std::optional< SystemCallNr > currentSystemCallNr() const
Returns the number of the currently running system call, if any.
Definition Tracee.cxx:974
std::optional< SystemCallInfo > m_syscall_info
The current system call information, if any.
Definition Tracee.hxx:448
static constexpr std::array< cosmos::Signal, 4 > STOPPING_SIGNALS
Array of signals that cause tracee stop.
Definition Tracee.hxx:292
State
Current tracing state for a single tracee.
Definition Tracee.hxx:48
@ DETACHED
we already detached from the tracee
Definition Tracee.hxx:57
@ SYSCALL_ENTER_STOP
system call started.
Definition Tracee.hxx:51
@ SYSCALL_EXIT_STOP
system call finished.
Definition Tracee.hxx:52
@ RUNNING
tracee is running normally / not in a special trace state.
Definition Tracee.hxx:50
@ SIGNAL_DELIVERY_STOP
signal was delivered.
Definition Tracee.hxx:53
@ UNKNOWN
initial PTRACE_SIZE / PTRACE_INTERRUPT.
Definition Tracee.hxx:49
@ EVENT_STOP
special ptrace event occurred.
Definition Tracee.hxx:55
@ DEAD
the tracee no longer exists.
Definition Tracee.hxx:56
@ GROUP_STOP
SIGSTOP executed, the tracee is stopped.
Definition Tracee.hxx:54
void readBlob(const ForeignPtr addr, char *buffer, const size_t bytes) const
Reads an arbitrary binary blob of fixed length from the tracee.
Definition Tracee.cxx:981
SystemCall * m_interrupted_syscall
Previous system call, if it has been interrupted.
Definition Tracee.hxx:454
void readString(const ForeignPtr addr, std::string &out) const
Reads a zero-terminated C-string from the tracee.
Definition Tracee.cxx:935
State m_state
The current state the tracee is in.
Definition Tracee.hxx:438
std::optional< cosmos::Signal > m_inject_sig
signal to inject upon next restart of the tracee.
Definition Tracee.hxx:458
std::optional< cosmos::ProcessID > m_initial_attacher
If this Tracee was automatically attached due to AttachThreads{true} then this contains the ProcessID...
Definition Tracee.hxx:462
virtual bool isChildProcess() const
Returns whether the tracee is a child process created by us.
Definition Tracee.hxx:245
cosmos::Tracee::RestartMode m_restart_mode
current RestartMode to use.
Definition Tracee.hxx:456
virtual void attach(const FollowChildren follow_children, const AttachThreads attach_threads=AttachThreads{false})
Logic to handle attaching to the tracee.
Definition Tracee.cxx:133
EventConsumer & m_consumer
Callback interface receiving our information.
Definition Tracee.hxx:436
std::optional< SystemCallNr > getInitialSyscallNr(const ABI abi) const
Returns the initial system call nr. stored in m_initial_regset, if available for abi.
Definition Tracee.cxx:818
void getRegisters(RegisterSet< abi > &rs)
Reads the current register set from the process.
Definition Tracee.cxx:909
void interrupt()
Forces the traced process to stop.
Definition Tracee.hxx:328
void restart(const cosmos::Tracee::RestartMode mode=cosmos::Tracee::RestartMode::CONT, const std::optional< cosmos::Signal > signal={})
Restarts the traced process, optionally delivering signal.
Definition Tracee.hxx:333
void fillData(ForeignPtr addr, FILLER &filler) const
Reads data from the Tracee starting at addr and feeds it to filler until it's saturated.
Definition Tracee.cxx:924
AnyRegisterSet m_initial_regset
Register set observed during initial attach event stop.
Definition Tracee.hxx:466
State m_prev_state
The previous Tracee state we've seen (except RUNNING).
Definition Tracee.hxx:440
SystemCallDB m_syscall_db
Reusable database object for tracing system calls.
Definition Tracee.hxx:450
const std::optional< cosmos::CloneArgs > & args() const
Returns an optional containing the cosmos::CloneArgs structure, if available.
Definition clone.hxx:60
cosmos::NamedBool< struct attach_threads_t, true > AttachThreads
A strong boolean type denoting whether to automatically all other threads of a process.
Definition types.hxx:27
const char * get_ptrace_event_str(const cosmos::ptrace::Event event)
Returns a string label for the given event.
Definition utils.cxx:47
constexpr ABI get_default_abi()
Returns the default ABI for this system.
Definition utils.hxx:59
constexpr uint64_t X32_SYSCALL_BIT
Native system call numbers as used by Linux on the x32 ABI.
Definition x32.hxx:38
ABI
System Call ABI.
Definition types.hxx:62
std::vector< FDInfo > get_fd_infos(const cosmos::ProcessID pid)
Obtain detailed information about currently open file descriptors according to /proc/<pid>/fd.
Definition utils.cxx:210
SystemCallNr
Abstract system call number usable across architectures and ABIs.
Definition generic.hxx:29
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
ForeignPtr
Strongly typed opaque pointer to tracee memory.
Definition types.hxx:140
std::set< cosmos::FileNum > get_currently_open_fds(const cosmos::ProcessID pid)
Returns the currently open file descriptors according to /proc/<pid>/fd.
Definition utils.cxx:62
item::CloneArgs cl_args
Combined clone arguments.
Definition process.hxx:132
Wrapper for the clone() and clone2() system calls.
Definition process.hxx:62
Contextual information about a file descriptor in a Tracee.
Definition types.hxx:75