def test_ptrace_syscalls(self): def process_func(): ptrace.traceme() os.kill(os.getpid(), signal.SIGSTOP) with open('/dev/null', 'w') as f: f.write('foo') rd, wr = os.pipe() os.close(rd) os.close(wr) try: process = multiprocessing.Process(target=process_func) process.start() pid, status = os.waitpid(process.pid, 0) self.assertTrue(os.WIFSTOPPED(status)) stopsig = os.WSTOPSIG(status) self.assertEqual(stopsig, signal.SIGSTOP) ptrace.setoptions(process.pid, ptrace.PTRACE_O_TRACESYSGOOD) syscalls = [] in_syscall = None while True: ptrace.syscall(process.pid) pid, status = os.waitpid(process.pid, 0) if os.WIFEXITED(status): break self.assertTrue(os.WIFSTOPPED(status)) stopsig = os.WSTOPSIG(status) self.assertTrue(stopsig & 0x80) self.assertEqual(stopsig & 0x7F, signal.SIGTRAP) regs = ptrace.getregs(process.pid) if not in_syscall: syscall = ptrace.syscall_enter(process.pid, regs) syscalls.append(syscall) in_syscall = syscall else: ptrace.syscall_exit(in_syscall, regs) in_syscall = None finally: try: os.kill(process.pid, signal.SIGKILL) except OSError as e: if e.errno == errno.ESRCH: pass else: raise syscalls = [ s for s in syscalls if s.name in {'open', 'write', 'close'} ] self.assertEqual(len(syscalls), 5) open_call, write_call, close_call = syscalls[:3] self.assertEqual(open_call.args[0].value, b'/dev/null') fno = open_call.result.value self.assertGreater(fno, 0) self.assertIsNotNone(open_call.result.type) self.assertEqual(write_call.args[0].value, fno) self.assertEqual(write_call.args[2].value, 3) self.assertEqual(write_call.result.value, 3) self.assertEqual(close_call.args[0].value, fno)
def _debugger_thread_inner(main_pid, dbgproc_started, dbgthread_stop, stack_request_pipe, stack_queue, syscall_queue, syscall_filter): ptrace_options = ptrace.PTRACE_O_TRACECLONE # Attach to the tracee and wait for it to stop. ptrace.attach_and_wait(main_pid, ptrace_options) if syscall_filter is not None: filter_ = lambda sc: any(m.match(sc) for m in syscall_filter) else: filter_ = None syscall_trap = signal.SIGTRAP | 0x80 enabled = False signum = 0 syscall_state = {} sigstop_received = set() processes = {main_pid} mem_fds = {} mem_fds[main_pid] = _open_procmem(main_pid) # Notify the parent that we are ready to start tracing. dbgproc_started.set() try: # Restart the tracee and enter the tracing loop. ptrace.syscall(main_pid) while True: if dbgthread_stop.is_set(): break pid, status = ptrace.wait(-1) if os.WIFEXITED(status) or os.WIFSIGNALED(status): # Traced thread has died. processes.discard(pid) mem_fd = mem_fds.get(pid) if mem_fd is not None: try: os.close(mem_fd) except IOError: pass if not processes: break else: continue elif os.WIFSTOPPED(status): ptrace_event = ptrace.WPTRACEEVENT(status) if ptrace_event == ptrace.PTRACE_EVENT_CLONE: # A new thread has been created. new_pid = ptrace.geteventmsg(pid) # See the comment below for the explanation of this check. if new_pid not in sigstop_received: ptrace.wait_for_trace_stop(new_pid) try: ptrace.syscall(new_pid) except OSError as e: if e.errno != errno.ESRCH: # The new thread might have already died. raise else: sigstop_received.discard(new_pid) mem_fds[new_pid] = _open_procmem(new_pid) processes.add(new_pid) ptrace.syscall(pid) continue stopsig = os.WSTOPSIG(status) if stopsig != syscall_trap: # Signal-delivery-stop. # The special condition below is for cases when we # receive a SIGSTOP for a newly created thread _before_ # receiving the PTRACE_EVENT_CLONE event for its parent. # In this case we must not forward the signal, but # must record its receipt so that once we _do_ receive # PTRACE_EVENT_CLONE for the parent, we don't wait for # SIGSTOP in the child again. if (stopsig != signal.SIGSTOP or pid in processes or all(syscall.name != 'clone' for syscall in syscall_state.values() if syscall is not None)): # forward the signal signum = stopsig else: sigstop_received.add(pid) else: # Syscall-stop. syscall = syscall_state.get(pid) regs = ptrace.getregs(pid) mem_fd = mem_fds.get(pid) if syscall is None: # Syscall-enter-stop. syscall_state[pid] = ptrace.syscall_enter( pid, regs, mem_fd) else: # Syscall-exit-stop. ptrace.syscall_exit(syscall, regs, mem_fd) if enabled: # Stop tracing once the tracee executes # the magic open() in ptracer.disable(). stop_tracing = ( syscall.name == 'open' and syscall.args[0].value == b'\x03\x02\x01' ) or (syscall.name == 'openat' and syscall.args[1].value == b'\x03\x02\x01') if stop_tracing: break elif filter_ is None or filter_(syscall): # Wait for the traceback to arrive. os.write(stack_request_pipe, struct.pack('!Q', pid)) stack = stack_queue.get() if stack is None: ptrace.cont(pid) break syscall.traceback = stack syscall_queue.put_nowait(syscall) elif not enabled: # Start tracing once the tracee executes # the magic open() in ptracer.enable(). start_tracing = ( syscall.name == 'open' and syscall.args[0].value == b'\x01\x02\x03' ) or (syscall.name == 'openat' and syscall.args[1].value == b'\x01\x02\x03') if start_tracing: enabled = True syscall_state[pid] = None else: logger.error('unexpected status of traced process %s: %s', pid, status) # Continue until next syscall. ptrace.syscall(pid, signum) signum = 0 finally: for process in processes: try: ptrace.detach(process) except OSError as e: if e.errno == errno.ESRCH: pass else: raise for fd in mem_fds.values(): try: os.close(fd) except (OSError, IOError): pass
def test_ptrace_procmem(self): def process_func(): ptrace.traceme() os.kill(os.getpid(), signal.SIGSTOP) with open('/dev/null', 'w') as f: f.write('foo') rd, wr = os.pipe() os.close(rd) os.close(wr) try: process = multiprocessing.Process(target=process_func) process.start() pid, status = os.waitpid(process.pid, 0) ptrace.setoptions(process.pid, ptrace.PTRACE_O_TRACESYSGOOD) syscalls = [] in_syscall = None mem_fd = os.open('/proc/{}/mem'.format(pid), os.O_RDONLY) while True: ptrace.syscall(process.pid) pid, status = ptrace.wait(process.pid) if os.WIFEXITED(status): break regs = ptrace.getregs(process.pid) if not in_syscall: syscall = ptrace.syscall_enter(process.pid, regs, mem_fd) syscalls.append(syscall) in_syscall = syscall else: ptrace.syscall_exit(in_syscall, regs, mem_fd) in_syscall = None finally: os.close(mem_fd) try: os.kill(process.pid, signal.SIGKILL) except OSError as e: if e.errno == errno.ESRCH: pass else: raise syscalls = [ s for s in syscalls if s.name in {'open', 'openat', 'write', 'close'} ] self.assertEqual(len(syscalls), 5) open_call, write_call, close_call = syscalls[:3] if open_call.name == 'openat': self.assertEqual(open_call.args[1].value, b'/dev/null') else: self.assertEqual(open_call.args[0].value, b'/dev/null') fno = open_call.result.value self.assertGreater(fno, 0) self.assertEqual(write_call.args[0].value, fno) self.assertEqual(write_call.args[2].value, 3) self.assertEqual(write_call.result.value, 3) self.assertEqual(close_call.args[0].value, fno)