ptrace(2)
๐ ptrace(2) โ from the System Calls Manual, bestie ๐
okay so. bestie. sit DOWN. we're doing ptrace(2) today and she is a whole personality. she's the thing that makes gdb work, she's the thing that makes strace work, she is the entire backbone of "watching another process do stuff". let's get into it. ๐ฆ
โจ NAME โจ
ptrace โ process trace. that's it. that's the name. short, iconic, does exactly what it says on the tin. ๐
๐ LIBRARY ๐
Standard C library โ girl, she's in libc, so link it with -lc. standard behavior, no notes. ๐
#include <sys/ptrace.h>
long ptrace(enum __ptrace_request op, pid_t pid,
void *addr, void *data);
like. look at that signature. four args. one of them is literally called op. she knows what she's doing.
๐ธ DESCRIPTION ๐ธ
okay so the vibe here is: the ptrace() system call is the whole entire mechanism that lets one process โ we call her the "tracer" โ observe and control the execution of another process โ and she's the "tracee" โ and also like, poke around in the tracee's memory and registers. read 'em, change 'em, all of it. ngl her main character job is implementing breakpoint debugging and system call tracing. that's the whole thing. that's her career. ๐ซถ
first things first: a tracee has to get attached to the tracer before any of this works. and here's the thing that trips everyone up โ attachment and all the commands after it are per thread. like, in a multithreaded process, every single thread can individually be attached to a tracer (and yeah, potentially a different tracer per thread), or just left unattached and therefore not debugged at all. so when this page says "tracee", she always means "(one) thread". never "a (possibly multithreaded) process". periodt. ๐
Ptrace commands are always sent to one specific tracee, using a call that looks like this:
ptrace(PTRACE_foo, pid, ...)
where pid is the thread ID of the corresponding Linux thread. iykyk. ๐ง
(quick aside because it matters: in this page, a "multithreaded process" means a thread group consisting of threads created using the clone(2) CLONE_THREAD flag. okay cool. moving on.)
so how does a trace even start? two ways, bestie:
- A process calls
fork(2)and the resulting child does aPTRACE_TRACEME, followed (typically) by anexecve(2). classic family business. ๐ - One process just. walks up to another process and starts tracing it using
PTRACE_ATTACHorPTRACE_SEIZE. bold. iconic. we love a direct approach. โจ
while being traced, the tracee will stop every single time a signal is delivered โ and yes, even if the signal is being ignored. the one exception is SIGKILL, which does its usual thing (i.e., it kills, she doesn't stop for that one, no cap). ๐
the tracer gets notified at its next call to waitpid(2) (or one of the related "wait" system calls). that call returns a status value that tells you why the tracee stopped. and while the tracee is stopped, the tracer can use various ptrace operations to inspect and modify her. then the tracer makes the tracee continue โ optionally ignoring the delivered signal, or even delivering a different signal instead. the audacity. the power. ๐
if the PTRACE_O_TRACEEXEC option is not in effect, then every successful call to execve(2) by the traced process will cause it to be sent a SIGTRAP signal โ which gives the parent a chance to grab the wheel before the new program starts executing. ๐
and when the tracer is done tracing, she can let the tracee go back to living her normal, untraced life via PTRACE_DETACH. freeing, honestly. ๐ฆ
๐ฅ the op values ๐ฅ
the value of op determines which operation gets performed. let's go one by one, no skipping, we're doing all of them. ๐
PTRACE_TRACEMEโ signals that this process wants to be traced by its parent. lowkey a process probably shouldn't do this if its parent isn't expecting to trace it (awkward!!). (pid,addr, anddataare all ignored here.) and btw โPTRACE_TRACEMEis the only operation used by the tracee; every other operation is used by the tracer. in all the following operations,pidspecifies the thread ID of the tracee to be acted on. for every operation other thanPTRACE_ATTACH,PTRACE_SEIZE,PTRACE_INTERRUPT, andPTRACE_KILL, the tracee must already be stopped. no exceptions bestie. ๐PTRACE_PEEKTEXT,PTRACE_PEEKDATAโ read one word at addressaddrin the tracee's memory, and return that word as the result of theptrace()call. Linux doesn't have separate text and data address spaces, so these two are currently equivalent. (datais ignored โ but see NOTES, okay, don't @ me.) ๐PTRACE_PEEKUSERโ read a word at offsetaddrin the tracee's USER area, which holds the registers and other info about the process (peep<sys/user.h>). the word comes back as the result of theptrace()call. typically the offset has to be word-aligned, though this might vary by architecture. see NOTES. (datais ignored; but again, see NOTES.) ๐PTRACE_POKETEXT,PTRACE_POKEDATAโ copy the worddatato addressaddrin the tracee's memory. same deal asPTRACE_PEEKTEXT/PTRACE_PEEKDATAโ these two are currently equivalent. ๐ซถPTRACE_POKEUSERโ copy the worddatato offsetaddrin the tracee's USER area. like withPTRACE_PEEKUSER, the offset must typically be word-aligned. and because the kernel would like to stay intact, some modifications to the USER area are disallowed. respect the boundaries. ๐งPTRACE_GETREGS,PTRACE_GETFPREGSโ copy the tracee's general-purpose registers or floating-point registers (respectively!!) into the addressdatain the tracer. see<sys/user.h>for the format of this data. (addris ignored.) โ ๏ธ heads up: SPARC systems have the meaning ofdataandaddrreversed โ that is,datais ignored and the registers get copied to the addressaddr. also,PTRACE_GETREGSandPTRACE_GETFPREGSare not present on all architectures. so like, check before you assume. ๐PTRACE_GETREGSET(since Linux 2.6.34) โ read the tracee's registers.addrspecifies, in an architecture-dependent way, the type of registers to read.NT_PRSTATUS(numerical value1) usually results in reading general-purpose registers. if the CPU has, say, floating-point and/or vector registers, you can get those by settingaddrto the correspondingNT_fooconstant.datapoints to astruct iovec, which describes the destination buffer's location and size. and on return, the kernel modifiesiov.lento indicate the actual number of bytes returned. queen behavior, honestly. ๐PTRACE_SETREGS,PTRACE_SETFPREGSโ modify the tracee's general-purpose registers or floating-point registers (respectively) from the addressdatain the tracer. just like withPTRACE_POKEUSER, some general-purpose register modifications may be disallowed. (addris ignored.) โ ๏ธ and again โ SPARC systems have the meaning ofdataandaddrreversed; that is,datais ignored and the registers are copied from the addressaddr. also,PTRACE_SETREGSandPTRACE_SETFPREGSare not present on all architectures. ๐ซPTRACE_SETREGSET(since Linux 2.6.34) โ modify the tracee's registers. the meaning ofaddranddatais analogous toPTRACE_GETREGSET. matching set, we love cohesion. โจPTRACE_GETSIGINFO(since Linux 2.3.99-pre6) โ retrieve info about the signal that caused the stop. copies asiginfo_tstructure (seesigaction(2)) from the tracee to the addressdatain the tracer. (addris ignored.) ๐PTRACE_SETSIGINFO(since Linux 2.3.99-pre6) โ set signal information: copy asiginfo_tstructure from the addressdatain the tracer over to the tracee. this affects only signals that would normally be delivered to the tracee and were caught by the tracer. fair warning: it can be genuinely difficult to tell these normal signals apart from the synthetic signals thatptrace()itself generates. (addris ignored.) ๐ตโ๐ซPTRACE_PEEKSIGINFO(since Linux 3.10) โ retrievesiginfo_tstructures without removing signals from the queue.addrpoints to aptrace_peeksiginfo_argsstructure that specifies the ordinal position to start copying signals from, and how many signals to copy. thesiginfo_tstructures get copied into the buffer pointed to bydata. the return value is the number of copied signals (zero means there's no signal corresponding to that ordinal position). and ooh โ within the returnedsiginfostructures, thesi_codefield includes info (__SI_CHLD,__SI_FAULT, etc.) that isn't otherwise exposed to user space. ๐โจ
struct ptrace_peeksiginfo_args {
u64 off; /* Ordinal position in queue at which
to start copying signals */
u32 flags; /* PTRACE_PEEKSIGINFO_SHARED or 0 */
s32 nr; /* Number of signals to copy */
};
right now there's only one flag, PTRACE_PEEKSIGINFO_SHARED, and it's for dumping signals from the process-wide signal queue. if that flag isn't set, signals are read from the per-thread queue of the specified thread. pick your queue bestie. ๐
PTRACE_GETSIGMASK(since Linux 3.11) โ put a copy of the mask of blocked signals (seesigprocmask(2)) into the buffer pointed to bydata, which should be a pointer to a buffer of typesigset_t. theaddrargument holds the size of the buffer pointed to bydata(i.e.,sizeof(sigset_t)). ๐PTRACE_SETSIGMASK(since Linux 3.11) โ change the mask of blocked signals (seesigprocmask(2)) to the value specified in the buffer pointed to bydata, which should be a pointer to a buffer of typesigset_t. theaddrargument holds the size of the buffer pointed to bydata(i.e.,sizeof(sigset_t)). same energy, opposite direction. ๐PTRACE_SETOPTIONS(since Linux 2.4.6; see BUGS for caveats) โ set ptrace options fromdata. (addris ignored.)datais interpreted as a bit mask of options, and those options are the following flags:PTRACE_O_EXITKILL(since Linux 3.8) โ send aSIGKILLsignal to the tracee if the tracer exits. this one is lowkey made for ptrace jailers who want to make sure their tracees can never escape the tracer's control. no escape room. ๐ช๐PTRACE_O_TRACECLONE(since Linux 2.5.46) โ stop the tracee at the nextclone(2)and automatically start tracing the newly cloned process, which will start with aSIGSTOP, orPTRACE_EVENT_STOPifPTRACE_SEIZEwas used. awaitpid(2)by the tracer will return a status value such that
status>>8 == (SIGTRAP | (PTRACE_EVENT_CLONE<<8))
the PID of the new process can be retrieved with PTRACE_GETEVENTMSG. ๐ผ
โ ๏ธ but bestie, this option may not catch clone(2) calls in all cases. if the tracee calls clone(2) with the CLONE_VFORK flag, then PTRACE_EVENT_VFORK will be delivered instead โ but only if PTRACE_O_TRACEVFORK is set. otherwise, if the tracee calls clone(2) with the exit signal set to SIGCHLD, then PTRACE_EVENT_FORK will be delivered โ if PTRACE_O_TRACEFORK is set. so many ifs. so many. ๐ญ
PTRACE_O_TRACEEXEC(since Linux 2.5.46) โ stop the tracee at the nextexecve(2). awaitpid(2)by the tracer will return a status value such that
status>>8 == (SIGTRAP | (PTRACE_EVENT_EXEC<<8))
and here's a fun one: if the execing thread is not a thread group leader, the thread ID is reset to the thread group leader's ID before this stop. since Linux 3.0, the former thread ID can be retrieved with PTRACE_GETEVENTMSG. she said "identity crisis? handled." ๐
โจ
(...to be continued, bestie โ she's still going, and we are NOT done. more parts incoming ๐ซถ)
๐ PTRACE_O_TRACEEXIT (since Linux 2.5.60)
okay so bestie, this one makes the tracee stop right at exit. like, at the door, keys in hand. โจ a waitpid(2) from the tracer is gonna hand you back a status value where:
status>>8 == (SIGTRAP | (PTRACE_EVENT_EXIT<<8))
and the tracee's actual exit status? you grab that with PTRACE_GETEVENTMSG. ๐
why is this iconic? because the tracee gets stopped early during process exit โ while the registers are still available โ so your tracer can literally see where the exit happened. the normal exit notification is giving "too late bestie, they're already gone" energy, happening only after the process is fully done exiting. and yes, even though the context is right there, the tracer cannot prevent the exit from happening at this point. it's a spectator sport, no cap. ๐ซถ
PTRACE_O_TRACEFORK (since Linux 2.5.46)
Stop the tracee at the next fork(2) and automatically start tracing the brand new forked process ๐ฃ โ which is gonna start with a SIGSTOP, or PTRACE_EVENT_STOP if PTRACE_SEIZE was used. Your waitpid(2) returns a status like:
status>>8 == (SIGTRAP | (PTRACE_EVENT_FORK<<8))
and the PID of the new lil process? PTRACE_GETEVENTMSG. it's giving found family, honestly. ๐
PTRACE_O_TRACESYSGOOD (since Linux 2.4.6)
When delivering system call traps, set bit 7 in the signal number โ meaning you deliver SIGTRAP|0x80. โจ this makes it SO easy for the tracer to tell normal traps apart from the ones caused by a system call. it's the little distinguishing detail that separates the girlies from the chaos. ๐
PTRACE_O_TRACEVFORK (since Linux 2.5.46)
Stop the tracee at the next vfork(2) and automatically start tracing the newly vforked process, which starts with a SIGSTOP, or PTRACE_EVENT_STOP if PTRACE_SEIZE was used. waitpid(2) returns:
status>>8 == (SIGTRAP | (PTRACE_EVENT_VFORK<<8))
PID of the new process โ PTRACE_GETEVENTMSG. same vibe as TRACEFORK, different syscall, you know the drill ๐
PTRACE_O_TRACEVFORKDONE (since Linux 2.5.60)
Stop the tracee at the completion of the next vfork(2). (not the start bb, the finish โ we love a completion arc ๐ฆ) waitpid(2) gives you:
status>>8 == (SIGTRAP | (PTRACE_EVENT_VFORK_DONE<<8))
The PID of the new process can โ since Linux 2.6.18 โ be retrieved with PTRACE_GETEVENTMSG.
PTRACE_O_TRACESECCOMP (since Linux 3.5)
Stop the tracee when a seccomp(2) SECCOMP_RET_TRACE rule gets triggered. ๐จ waitpid(2) from the tracer returns:
status>>8 == (SIGTRAP | (PTRACE_EVENT_SECCOMP<<8))
okay so while this triggers a PTRACE_EVENT stop, it's actually similar to a syscall-enter-stop. for the details, peek at the note on PTRACE_EVENT_SECCOMP below ๐. And the seccomp event message data (that's from the SECCOMP_RET_DATA portion of the seccomp filter rule) can be retrieved with PTRACE_GETEVENTMSG.
PTRACE_O_SUSPEND_SECCOMP (since Linux 4.3)
Suspend the tracee's seccomp protections. ๐ this applies regardless of mode, and you can use it even when the tracee hasn't installed seccomp filters yet. like, a fully valid use case is: suspend a tracee's seccomp protections before they're installed by the tracee, let the tracee install the filters, and then clear this flag when the filters should be resumed. main character planning energy fr fr โจ
โ ๏ธ hold up, setting this option requires that the tracer have theCAP_SYS_ADMINcapability, not have any seccomp protections installed, and not havePTRACE_O_SUSPEND_SECCOMPset on itself. all three, no exceptions, bestie.
๐ซถ PTRACE_GETEVENTMSG (since Linux 2.5.46)
Retrieve a message (as an unsigned long) about the ptrace event that just happened, and place it at the address data in the tracer. ๐ฌ
PTRACE_EVENT_EXITโ this is the tracee's exit statusPTRACE_EVENT_FORK,PTRACE_EVENT_VFORK,PTRACE_EVENT_VFORK_DONE, andPTRACE_EVENT_CLONEโ this is the PID of the new processPTRACE_EVENT_SECCOMPโ this is theseccomp(2)filter'sSECCOMP_RET_DATAassociated with the triggered rule
(addr is ignored, she said unbothered ๐
)
โจ PTRACE_CONT
Restart the stopped tracee process. if data is nonzero, it's interpreted as the number of a signal to be delivered to the tracee; otherwise, no signal is delivered. so for example, the tracer can control whether a signal sent to the tracee is delivered or not. you literally hold the aux cord on signals. ๐ง (addr is ignored.)
PTRACE_SYSCALL
PTRACE_SINGLESTEP
Restart the stopped tracee just like PTRACE_CONT โ but arrange for the tracee to be stopped at the next entry to or exit from a system call, or after execution of a single instruction, respectively. ๐พ (the tracee will also, as usual, be stopped upon receipt of a signal.)
from the tracer's perspective, the tracee will appear to have been stopped by receipt of a SIGTRAP. so for PTRACE_SYSCALL, for example, the idea is to inspect the arguments to the system call at the first stop, then do another PTRACE_SYSCALL and inspect the return value of the system call at the second stop. ๐ฌ it's giving two-act structure and honestly? ate.
the data argument is treated as for PTRACE_CONT. (addr is ignored.)
PTRACE_SET_SYSCALL (since Linux 2.6.16)
When you're in syscall-enter-stop, change the number of the system call that's about to be executed to the number specified in the data argument. ๐ข the addr argument is ignored.
heads up: this operation is currently supported only on arm (and arm64, though only for backwards compatibility), but most other architectures have other means of accomplishing this โ usually by changing the register that the userland code passed the system call number in. different routes, same slay. ๐
PTRACE_SYSEMU
PTRACE_SYSEMU_SINGLESTEP (since Linux 2.6.14)
For PTRACE_SYSEMU: continue and stop on entry to the next system call, which will not be executed. ๐ณ (see the documentation on syscall-stops below.) For PTRACE_SYSEMU_SINGLESTEP: do the same, but also singlestep if it's not a system call.
This call is used by programs like User Mode Linux that want to emulate all the tracee's system calls. ๐ the data argument is treated as for PTRACE_CONT. The addr argument is ignored.
scope check: these operations are currently supported only on x86.
๐ PTRACE_LISTEN (since Linux 3.4)
Restart the stopped tracee โ but prevent it from executing. ๐ the resulting state of the tracee is similar to a process that has been stopped by a SIGSTOP (or another stopping signal). hit up the "group-stop" subsection for the extra deets. PTRACE_LISTEN works only on tracees attached by PTRACE_SEIZE.
๐ PTRACE_KILL
Send the tracee a SIGKILL to terminate it. (addr and data are ignored.)
๐จ This operation is deprecated; do not use it! Instead, send aSIGKILLdirectly usingkill(2)ortgkill(2). The problem withPTRACE_KILLis that it requires the tracee to be in signal-delivery-stop, otherwise it may not work โ meaning it may complete successfully but won't actually kill the tracee. ๐ญ By contrast, sending aSIGKILLdirectly has no such limitation. bestie, just usekill(2). it's not a vibe to be fooled by a fake success.
PTRACE_INTERRUPT (since Linux 3.4)
Stop a tracee. ๐
- If the tracee is running or sleeping in kernel space and
PTRACE_SYSCALLis in effect, the system call is interrupted and syscall-exit-stop is reported. (the interrupted system call is restarted when the tracee is restarted.) ๐ - If the tracee was already stopped by a signal and
PTRACE_LISTENwas sent to it, the tracee stops withPTRACE_EVENT_STOPandWSTOPSIG(status)returns the stop signal. - If any other ptrace-stop is generated at the same time (for example, if a signal is sent to the tracee), this ptrace-stop happens.
- If none of the above applies (for example, if the tracee is running in user space), it stops with
PTRACE_EVENT_STOPwithWSTOPSIG(status) == SIGTRAP.
PTRACE_INTERRUPT works only on tracees attached by PTRACE_SEIZE. seized girlies only, sorry to the rest.
PTRACE_ATTACH
Attach to the process specified in pid, making it a tracee of the calling process. ๐ค the tracee is sent a SIGSTOP, but it will not necessarily have stopped by the completion of this call; use waitpid(2) to wait for the tracee to stop. see the "Attaching and detaching" subsection for extra info. (addr and data are ignored.)
Permission to perform aPTRACE_ATTACHis governed by a ptrace access modePTRACE_MODE_ATTACH_REALCREDScheck; see below. ๐
PTRACE_SEIZE (since Linux 3.4)
Attach to the process specified in pid, making it a tracee of the calling process. unlike PTRACE_ATTACH, PTRACE_SEIZE does not stop the process. ๐ iconic, frankly.
- Group-stops are reported as
PTRACE_EVENT_STOPandWSTOPSIG(status)returns the stop signal. - Automatically attached children stop with
PTRACE_EVENT_STOPandWSTOPSIG(status)returnsSIGTRAP, instead of havingSIGSTOPsignal delivered to them. execve(2)does not deliver an extraSIGTRAP. โจ- Only a
PTRACE_SEIZEdprocess can acceptPTRACE_INTERRUPTandPTRACE_LISTENcommands. - The "seized" behavior just described is inherited by children that are automatically attached using
PTRACE_O_TRACEFORK,PTRACE_O_TRACEVFORK, andPTRACE_O_TRACECLONE. ๐งฌ generational!
addr must be zero. data contains a bit mask of ptrace options to activate immediately. ๐๏ธ
Permission to perform aPTRACE_SEIZEis governed by a ptrace access modePTRACE_MODE_ATTACH_REALCREDScheck; see below. ๐
๐ PTRACE_SECCOMP_GET_FILTER (since Linux 4.4)
This operation allows the tracer to dump the tracee's classic BPF filters. ๐ซ dump 'em, queen.
addris an integer specifying the index of the filter to be dumped. the most recently installed filter has the index0. ifaddris greater than the number of installed filters, the operation fails with the errorENOENT.datais either a pointer to astruct sock_filterarray that is large enough to store the BPF program, orNULLif the program is not to be stored.
Upon success, the return value is the number of instructions in the BPF program. ๐ and if data was NULL, then this return value can be used to correctly size the struct sock_filter array passed in a subsequent call. measure twice, allocate once, bestie. ๐
Okay so real quick about that operation we were just vibing with โ it ###throws EACCES if you don't have the CAP_SYS_ADMIN capability ๐ or if you're chilling in strict or filter seccomp mode. And if the filter that addr is pointing at isn't a classic BPF filter? Boom, EMEDIUMTYPE. Rude, but okay.
It's only even available if your kernel was built with both the CONFIG_SECCOMP_FILTER and the CONFIG_CHECKPOINT_RESTORE options turned on, so likeโฆ check before you come for it. ๐ซถ
PTRACE_DETACH ๐
Restarts the stopped tracee exactly like PTRACE_CONT does, but first it detaches from it. Under Linux you can detach a tracee this way no matter which method was used to start the tracing in the first place โ iconic, versatile, no notes. (addr is ignored, bestie, don't stress about it.)
PTRACE_GET_THREAD_AREA (since Linux 2.6.0) ๐งต
Does a very similar job to get_thread_area(2). It reads the TLS entry in the GDT whose index you passed in addr, and drops a copy of that entry into the struct user_desc pointed to by data. โจ
Lowkey important: unlikeget_thread_area(2), theentry_numberof thestruct user_descis ignored here.
PTRACE_SET_THREAD_AREA (since Linux 2.6.0) ๐งต
Does a very similar job to set_thread_area(2). It sets the TLS entry in the GDT whose index you gave in addr, assigning it the data you supplied in the struct user_desc pointed to by data.
And again, unlikeset_thread_area(2), theentry_numberof thestruct user_descis ignored โ in other words, this ptrace operation cannot be used to allocate a free TLS entry. So don't try it, it's giving "no". ๐
PTRACE_GET_SYSCALL_INFO (since Linux 5.3) ๐
Retrieves info about the system call that caused the stop, and it goes into the buffer pointed to by the data argument โ which should be a pointer to a buffer of type struct ptrace_syscall_info. The addr argument holds the size of the buffer that data points to (i.e., sizeof(struct ptrace_syscall_info)). The return value tells you how many bytes were available to be written by the kernel. If the data the kernel wants to write is bigger than the size you gave in addr, the output data gets truncated. โ๏ธ
The ptrace_syscall_info structure contains these fields:
struct ptrace_syscall_info {
__u8 op; /* Type of system call stop */
__u8 reserved; /* Reserved for future use, must be zero */
__u16 flags; /* Reserved for future use, must be zero */
__u32 arch; /* AUDIT_ARCH_* value; see seccomp(2) */
__u64 instruction_pointer; /* CPU instruction pointer */
__u64 stack_pointer; /* CPU stack pointer */
union {
struct { /* op == PTRACE_SYSCALL_INFO_ENTRY */
__u64 nr; /* System call number */
__u64 args[6]; /* System call arguments */
} entry;
struct { /* op == PTRACE_SYSCALL_INFO_EXIT */
__s64 rval; /* System call return value */
__u8 is_error; /* System call error flag;
Boolean: does rval contain
an error value (-ERRCODE) or
a nonerror return value? */
} exit;
struct { /* op == PTRACE_SYSCALL_INFO_SECCOMP */
__u64 nr; /* System call number */
__u64 args[6]; /* System call arguments */
__u32 ret_data; /* SECCOMP_RET_DATA portion
of SECCOMP_RET_TRACE
return value */
} seccomp;
};
};
So the op, arch, instruction_pointer, and stack_pointer fields are defined for all kinds of ptrace system call stops. The rest of the structure is a union, which means: read only the fields that are actually meaningful for whichever kind of stop op says it is. Don't go reading random union members, that's not the vibe. ๐
The op field takes one of these values (defined in <linux/ptrace.h>) to tell you what type of stop happened and which part of the union got filled in:
PTRACE_SYSCALL_INFO_ENTRYโ theentrycomponent of the union has the info about a system call entry stop. ๐ชPTRACE_SYSCALL_INFO_EXITโ theexitcomponent of the union has the info about a system call exit stop. ๐ชPTRACE_SYSCALL_INFO_SECCOMPโ theseccompcomponent of the union has the info about aPTRACE_EVENT_SECCOMPstop. ๐PTRACE_SYSCALL_INFO_NONEโ no component of the union has anything relevant for you. F in the chat. ๐
And here's a lil catch: for system call entry or exit stops, the data returned by PTRACE_GET_SYSCALL_INFO is limited to type PTRACE_SYSCALL_INFO_NONE unless the PTRACE_O_TRACESYSGOOD option was set before the corresponding system call stop already happened. So timing matters, bestie. โฐ
PTRACE_SET_SYSCALL_INFO (since Linux 6.16) โ๏ธ
Modifies the info about the system call that caused the stop. The data argument is a pointer to a struct ptrace_syscall_info that specifies the system call info you want to set. The addr argument should be sizeof(struct ptrace_syscall_info).
Heads up: only thenr,args, andrvalfields can be modified. Everything else, hands off. ๐
Death under ptrace โฐ๏ธ๐
When a (possibly multithreaded) process catches a killing signal โ and by killing signal we mean one whose disposition is set to SIG_DFL and whose default action is to kill the process โ then all the threads exit. Tracees report their death to their tracer(s), and notification of this whole event gets delivered via waitpid(2).
Now, plot twist: the killing signal will actually first cause a signal-delivery-stop (on one tracee only), and only after it's injected by the tracer โ or after it was dispatched to a thread that isn't traced โ does the death from the signal happen on all the tracees inside a multithreaded process. (The term "signal-delivery-stop" gets explained further down, chill.)
SIGKILL does not generate signal-delivery-stop, which means the tracer can't suppress it. Not even a little. SIGKILL kills even from within system calls โ syscall-exit-stop is not generated before death by SIGKILL. Net effect? SIGKILL always kills the process (all its threads), even if some threads of the process are being ptraced. It's giving "unstoppable force", no notes. ๐
When the tracee calls _exit(3), it reports its death to its tracer. Other threads are not affected.
When any thread executes exit_group(2), every tracee in its thread group reports its death to its tracer. ๐ซ
If the PTRACE_O_TRACEEXIT option is on, then PTRACE_EVENT_EXIT will happen before the actual death. This applies to:
- exits via
_exit(3) - exits via
exit_group(2) - signal deaths (except
SIGKILL, depending on the kernel version โ see BUGS below ๐) - and when threads get torn down on
execve(2)in a multithreaded process
Now for the scary part: the tracer cannot assume that the ptrace-stopped tracee still exists. ๐ญ There are many scenarios where the tracee may die while stopped (such as by SIGKILL). So the tracer has to be prepared to handle an ESRCH error on any ptrace operation.
And unfortunately? The exact same error gets returned if the tracee exists but isn't ptrace-stopped (for commands that require a stopped tracee), or if it isn't traced by the process that issued the ptrace call. So ESRCH is a whole mood that could mean three different things. ๐ The tracer needs to keep track of the stopped/running state of the tracee itself, and only interpret ESRCH as "tracee died unexpectedly" if it knows the tracee was observed entering ptrace-stop.
Also, note that there's no guarantee that waitpid(WNOHANG) will reliably report the tracee's death status if a ptrace operation returned ESRCH. waitpid(WNOHANG) may return 0 instead. In other words, the tracee may be "not yet fully dead" but already refusing ptrace operations. ๐
And the tracer can't assume the tracee always ends its life by reporting WIFEXITED(status) or WIFSIGNALED(status) โ there are cases where that just doesn't occur. For example, if a thread other than the thread group leader does an execve(2), it disappears; its PID will never be seen again, and any subsequent ptrace stops will be reported under the thread group leader's PID. Poof. Gone. ๐ฆ
Stopped states ๐
A tracee can be in two states: running or stopped. For ptrace purposes, a tracee that's blocked in a system call (like read(2), pause(2), etc.) still counts as running, even if it's been blocked for a long time. โณ
The state of the tracee after PTRACE_LISTEN is kinda a gray area: it's not in any ptrace-stop (ptrace commands won't work on it, and it will deliver waitpid(2) notifications), but it also may be considered "stopped" because it isn't executing instructions (it isn't scheduled), and if it was in group-stop before PTRACE_LISTEN, it won't respond to signals until SIGCONT is received. It's giving "situationship". ๐
There are so many kinds of stopped states and in ptrace discussions people constantly conflate them, so precise terms are everything.
In this manual page, any stopped state in which the tracee is ready to accept ptrace commands from the tracer is called a ptrace-stop. Ptrace-stops can be broken down even further into signal-delivery-stop, group-stop, syscall-stop, PTRACE_EVENT stops, and so on. All of these stopped states are described in detail below. ๐
When the running tracee enters ptrace-stop, it notifies its tracer using waitpid(2) (or one of the other "wait" system calls). Most of this manual page assumes the tracer waits with:
pid = waitpid(pid_or_minus_1, &status, __WALL);
Ptrace-stopped tracees get reported as returns with pid greater than 0 and WIFSTOPPED(status) true. โ
The __WALL flag does not include the WSTOPPED and WEXITED flags, but it implies their functionality.
Setting theWCONTINUEDflag when callingwaitpid(2)is not recommended: the "continued" state is per-process and consuming it can confuse the real parent of the tracee. Don't do that to them. ๐ฅบ
Use of the WNOHANG flag may cause waitpid(2) to return 0 ("no wait results available yet") even if the tracer knows there should be a notification. Example:
errno = 0;
ptrace(PTRACE_CONT, pid, 0L, 0L);
if (errno == ESRCH) {
/* tracee is dead */
r = waitpid(tracee, &status, __WALL | WNOHANG);
/* r can still be 0 here! */
}
Likeโฆ r can literally still be 0 right there. Traumatizing. ๐ญ
okay bestie so here's the tea on the kinds of stops you're gonna meet in the wild ๐
There are four flavors of ptrace-stop, and you will get to know all of them:
- signal-delivery-stops ๐ธ
- group-stops โจ
- PTRACE_EVENT stops ๐
- syscall-stops ๐ฆ
Every single one of them gets reported to you by waitpid(2) with WIFSTOPPED(status) coming back true. So far so unbothered. To tell them apart, you peek at the value status>>8, and if that's giving ambiguous, you go ask PTRACE_GETSIGINFO directly. ๐
โ ๏ธ Note bestie: theWSTOPSIG(status)macro can't do this job for you, because it hands back the value(status>>8) & 0xff. It's like asking a question with half the answer cut off. Not the move. ๐ญ
๐ Signal-delivery-stop ๐
When a process (could be multithreaded, doesn't matter) receives any signal except SIGKILL, the kernel just picks an arbitrary thread to handle it. Rude? kinda. But if the signal is generated with tgkill(2), then the caller can explicitly pick which thread it targets. ๐ฏ
If that chosen thread happens to be traced, it enters signal-delivery-stop. And listen โ at this exact moment, the signal has not been delivered to the process yet, so the tracer can straight up suppress it if she wants. If the tracer doesn't suppress it, she passes the signal to the tracee in the next ptrace restart operation. That second step of delivery is called signal injection in this manual page. ๐
One thing though: if the signal is blocked, signal-delivery-stop won't happen until it gets unblocked. Same usual exception as always โ SIGSTOP cannot be blocked. Periodt.
So how does the tracer see it? waitpid(2) returns with WIFSTOPPED(status) true, and the signal comes back from WSTOPSIG(status). BUT โ if that signal is SIGTRAP, it might lowkey be a different kind of ptrace-stop, so go read the "Syscall-stops" and "execve" sections below for the deets. And if WSTOPSIG(status) gives you a stopping signal, that could be a group-stop โ scroll down. ๐
๐ซถ Signal injection and suppression ๐ซถ
Once the tracer has observed the signal-delivery-stop, she should restart the tracee with this call:
ptrace(PTRACE_restart, pid, 0, sig)
where PTRACE_restart is one of the restarting ptrace operations. Here's the vibe check on sig:
- if
sigis0, then no signal gets delivered. bye ๐ - otherwise, the signal
siggets delivered. hi bestie ๐
This operation is called signal injection in this manual page, and yes the name exists specifically so we can tell it apart from signal-delivery-stop. Naming things properly is self care.
And plot twist: the sig value may be different from the WSTOPSIG(status) value. The tracer can just decide to inject a whole different signal. Main character move, honestly.
๐ญ Careful though: a suppressed signal still causes system calls to return prematurely. In that case, system calls get restarted โ the tracer will see the tracee reexecute the interrupted system call (or therestart_syscall(2)system call, for the few system calls that use a different restarting mechanism) if the tracer is usingPTRACE_SYSCALL.
Even system calls like poll(2) that aren't restartable after a signal get restarted after the signal is suppressed. So cute. But heads up โ there are kernel bugs out there that cause some system calls to fail with EINTR even when no observable signal was injected into the tracee. Bugs, bestie. We don't claim them, we just note them. ๐
Now, restarting ptrace commands issued in ptrace-stops other than signal-delivery-stop? Not guaranteed to inject a signal at all, even if sig is nonzero. And no error gets reported โ a nonzero sig might just be silently ignored. ๐ So please, do not try to "create a new signal" this way. Use tgkill(2) instead. That's the way.
This whole thing is a major source of confusion among ptrace users, and honestly valid. The typical scenario: the tracer observes a group-stop, mistakes it for a signal-delivery-stop, restarts the tracee with
ptrace(PTRACE_restart, pid, 0, stopsig)
fully intending to inject stopsigโฆ and stopsig gets ignored while the tracee just keeps running. The audacity. ๐ญ
โจ SIGCONT and stopping signals, the side-effect lore โจ
SIGCONT has a side effect: it wakes up (all threads of) a group-stopped process. And this side effect happens before signal-delivery-stop. The tracer can't suppress this side effect โ she can only suppress signal injection, which just means the SIGCONT handler doesn't get executed in the tracee (if such a handler is even installed). ๐ง
In fact, waking up from group-stop may be followed by signal-delivery-stop for signal(s) other than SIGCONT, if those were pending when SIGCONT was delivered. In other words โ SIGCONT may not be the first signal the tracee observes after it was sent. Surprise cameo. ๐
Stopping signals, meanwhile, cause (all threads of) a process to enter group-stop. That side effect happens after signal injection, so the tracer can suppress it. ๐
Oh and โ in Linux 2.4 and earlier, SIGSTOP can't be injected. Ancient lore. ๐๏ธ
๐ getting and setting the siginfo
PTRACE_GETSIGINFO can retrieve a siginfo_t structure corresponding to the delivered signal, and PTRACE_SETSIGINFO can modify it.
โ ๏ธ IfPTRACE_SETSIGINFOwas used to alter thesiginfo_t, then thesi_signofield and thesigparameter in the restarting command must match โ otherwise the result is undefined. Like, genuinely undefined. Don't test the universe on this one.
๐ธ Group-stop ๐ธ
When a (possibly multithreaded) process receives a stopping signal, all threads stop. If some of those threads are traced, they enter group-stop. โ
Important sequencing detail, bestie: the stopping signal will first cause signal-delivery-stop (on one tracee only), and only after it gets injected by the tracer (or after it was dispatched to a thread that isn't traced) will group-stop be initiated on all tracees within the multithreaded process. And as always, every tracee reports its group-stop separately to its corresponding tracer. One at a time, no bundling. ๐
The tracer observes group-stop as waitpid(2) returning with WIFSTOPPED(status) true, with the stopping signal available via WSTOPSIG(status). BUT โ some other classes of ptrace-stops return the exact same result, so the recommended practice is to make this call:
ptrace(PTRACE_GETSIGINFO, pid, 0, &siginfo)
Great news though, you can skip that call entirely if the signal isn't one of the four stopping signals: SIGSTOP, SIGTSTP, SIGTTIN, or SIGTTOU. Only those four are stopping signals. If the tracer sees anything else, it simply cannot be a group-stop. ๐ซ
Otherwise the tracer does need to call PTRACE_GETSIGINFO. And if PTRACE_GETSIGINFO fails with EINVAL, then it is definitely a group-stop. Confirmed. โจ (Other failure codes are possible too, like ESRCH โ "no such process" โ if a SIGKILL killed the tracee. Dramatic but valid.)
Now: if the tracee was attached using PTRACE_SEIZE, group-stop is indicated by PTRACE_EVENT_STOP: specifically status>>16 == PTRACE_EVENT_STOP. This lets you detect group-stops without needing the extra PTRACE_GETSIGINFO call. Efficiency icon. ๐
the 2.6.38 lore
As of Linux 2.6.38, after the tracer sees the tracee ptrace-stop and until it restarts or kills it, the tracee will not run, and will not send notifications (except SIGKILL death) to the tracer โ even if the tracer enters into another waitpid(2) call. It's just... sitting there. Holding still. ๐ง
And this kernel behavior described in the previous paragraph causes a real problem with transparent handling of stopping signals. Watch how this goes down:
- If the tracer restarts the tracee after group-stop, the stopping signal is effectively ignored โ the tracee doesn't remain stopped, it runs. Sneaky.
- If the tracer doesn't restart the tracee before entering the next
waitpid(2), futureSIGCONTsignals will not be reported to the tracer โ which would cause theSIGCONTsignals to have no effect on the tracee. ๐ญ
the fix: PTRACE_LISTEN
Since Linux 3.4, there's a method to overcome this whole problem: instead of PTRACE_CONT, use a PTRACE_LISTEN command to restart a tracee in a way where it does not execute, but waits for a new event which it can report via waitpid(2) โ such as when it gets restarted by a SIGCONT. chef's kiss. ๐ฉโ๐ณ๐
๐ PTRACE_EVENT stops ๐
If the tracer sets PTRACE_O_TRACE_* options, the tracee will enter ptrace-stops called PTRACE_EVENT stops.
How you spot one: waitpid(2) returns with WIFSTOPPED(status), and WSTOPSIG(status) returns SIGTRAP (or, for PTRACE_EVENT_STOP, returns the stopping signal if the tracee is in a group-stop). Plus there's an extra bit set in the higher byte of the status word โ the value status>>8 will be:
((PTRACE_EVENT_foo<<8) | SIGTRAP).
Here's the full event roster, bestie. All of them. No skipping. ๐
PTRACE_EVENT_VFORKโ Stop before return fromvfork(2)orclone(2)with theCLONE_VFORKflag. When the tracee is continued after this stop, it will wait for the child to exit/exec before continuing its execution โ in other words, the usual behavior onvfork(2). ๐ผPTRACE_EVENT_FORKโ Stop before return fromfork(2)orclone(2)with the exit signal set toSIGCHLD.PTRACE_EVENT_CLONEโ Stop before return fromclone(2).PTRACE_EVENT_VFORK_DONEโ Stop before return fromvfork(2)orclone(2)with theCLONE_VFORKflag, but after the child unblocked this tracee by exiting or execing. ๐ฌ
For all four stops described above, the stop occurs in the parent (i.e., the tracee), not in the newly created thread. And PTRACE_GETEVENTMSG can be used to retrieve the new thread's ID. ๐ฌ
PTRACE_EVENT_EXECโ Stop before return fromexecve(2). Since Linux 3.0,PTRACE_GETEVENTMSGreturns the former thread ID.PTRACE_EVENT_EXITโ Stop before exit (including death fromexit_group(2)), signal death, or exit caused byexecve(2)in a multithreaded process.PTRACE_GETEVENTMSGreturns the exit status. Registers can be examined here โ unlike when a "real" exit happens. And the tracee is still alive; it needs to bePTRACE_CONTed orPTRACE_DETACHed to finish exiting. ๐ (but like, politely)PTRACE_EVENT_STOPโ Stop induced by thePTRACE_INTERRUPTcommand, or a group-stop, or the initial ptrace-stop when a new child is attached (only if attached usingPTRACE_SEIZE).PTRACE_EVENT_SECCOMPโ Stop triggered by aseccomp(2)rule on tracee syscall entry, whenPTRACE_O_TRACESECCOMPhas been set by the tracer. The seccomp event message data โ from theSECCOMP_RET_DATAportion of the seccomp filter rule โ can be retrieved withPTRACE_GETEVENTMSG. The semantics of this stop get described in detail in a separate section below. ๐ต๏ธโโ๏ธ
๐ก And one more:PTRACE_GETSIGINFOon PTRACE_EVENT stops returnsSIGTRAPinsi_signo, withsi_codeset to(event<<8) | SIGTRAP.
๐ฆ Syscall-stops ๐ฆ
If the tracee was restarted by PTRACE_SYSCALL or PTRACE_SYSEMU, the tracee enters syscall-enter-stop just prior to entering any system call. ๐ช
And bestie, hear this: the system call will not be executed if the restart was using PTRACE_SYSEMU, regardless of any change made to registers at this point, or how the tracee is restarted after this stop. No cap.
No matter which method caused the syscall-entry-stop, if the tracer restarts the tracee with PTRACE_SYSCALL, the tracee enters syscall-exit-stop when the system call is finished โ or if it gets interrupted by a signal. ๐
(That is: signal-delivery-stop never happens between syscall-enter-stop and syscall-exit-stop. It happens after syscall-exit-stop. Get the order right, it matters.)
And if the tracee is continued using any other method (including PTRACE_SYSEMU), no syscall-exit-stop occurs at all. Poof, gone. โจ
๐ซถ Note that all mentions ofPTRACE_SYSEMUapply equally toPTRACE_SYSEMU_SINGLESTEP. They're a package deal, iykyk.
okay so even if you continued the tracee with PTRACE_SYSCALL, bestie, that does not guarantee the next stop is a syscall-exit-stop. ๐
The plot could go several ways: it might stop in a PTRACE_EVENT stop (seccomp stops included, yes queen), it could exit (if it wandered into _exit(3) or exit_group(2)), get absolutely unalived by SIGKILL, or justโฆ die silently ๐ (and that specifically happens if it's a thread group leader, the execve(2) happened in another thread, and that other thread isn't traced by the same tracer โ we get into that situation later, don't worry).
So the tracer sees syscall-enter-stop and syscall-exit-stop as waitpid(2) coming back with WIFSTOPPED(status) true, and WSTOPSIG(status) handing over SIGTRAP. And if the tracer had set the PTRACE_O_TRACESYSGOOD option, then WSTOPSIG(status) gives you the value (SIGTRAP | 0x80) instead. โจ
Now, how do you tell syscall-stops apart from a signal-delivery-stop with SIGTRAP? You ask PTRACE_GETSIGINFO and check these cases:
si_code <= 0โ theSIGTRAPgot delivered because of a user-space action. Like a system call (tgkill(2),kill(2),sigqueue(3), etc.), a POSIX timer expiring, a state change on a POSIX message queue, or an async I/O operation finishing up.si_code == SI_KERNEL (0x80)โ the kernel itself sent thatSIGTRAP.si_code == SIGTRAPorsi_code == (SIGTRAP|0x80)โ boom, that's a syscall-stop.
But here's the catch, bestie: syscall-stops happen constantly โ twice per system call โ and doing PTRACE_GETSIGINFO for literally every single one can get kinda expensive. ๐ญ
Some architectures let you tell the cases apart by peeking at registers instead. On x86 for example, rax == -ENOSYS in syscall-enter-stop. And since SIGTRAP (like every other signal, no exceptions) always lands after syscall-exit-stop, and at that point rax basically never contains -ENOSYS, that SIGTRAP reads like "syscall-stop which is not syscall-enter-stop" โ aka a "stray syscall-exit-stop" โ so you can catch it that way. But ngl that detection is fragile and it's best avoided. ๐ซค
Using the PTRACE_O_TRACESYSGOOD option is the recommended method to tell syscall-stops from other kinds of ptrace-stops, because it's reliable and costs you zero performance penalty. Iconic behavior honestly. ๐
Okay and here's a spicy one: syscall-enter-stop and syscall-exit-stop are indistinguishable from each other by the tracer. Like, they're twins. ๐ฏ The tracer has to keep track of the sequence of ptrace-stops itself so it doesn't misread syscall-enter-stop as syscall-exit-stop or vice versa. Generally a syscall-enter-stop is always followed by syscall-exit-stop, a PTRACE_EVENT stop, or the tracee's death โ no other kinds of ptrace-stop can sneak in between. However โ and this is important โ seccomp stops (see below) can cause syscall-exit-stops without any preceding syscall-entry-stops. So if seccomp is in play, be careful not to misread those stops as syscall-entry-stops. ๐
And if, after syscall-enter-stop, the tracer uses a restarting command that is not PTRACE_SYSCALL, then syscall-exit-stop just doesn't get generated. Periodt.
PTRACE_GETSIGINFO on syscall-stops returns SIGTRAP in si_signo, with si_code set to SIGTRAP or (SIGTRAP|0x80).
๐ PTRACE_EVENT_SECCOMP stops (Linux 3.5 to Linux 4.7) ๐
The behavior of PTRACE_EVENT_SECCOMP stops and how they interact with other kinds of ptrace stops changed between kernel versions, so pay attention bestie. This part documents the behavior from their introduction all the way up to Linux 4.7 (inclusive). The behavior in later kernel versions is documented in the next section. ๐
A PTRACE_EVENT_SECCOMP stop happens whenever a SECCOMP_RET_TRACE rule gets triggered. This is independent of whichever method was used to restart the system call. Notably, seccomp still runs even if the tracee was restarted using PTRACE_SYSEMU and this system call is unconditionally skipped. ๐ฎ
Restarts from this stop behave as if the stop had occurred right before the system call in question. Specifically, both PTRACE_SYSCALL and PTRACE_SYSEMU will normally cause a subsequent syscall-entry-stop. However, if after the PTRACE_EVENT_SECCOMP the system call number is negative, then both the syscall-entry-stop and the system call itself get skipped. Which means: if the system call number is negative after a PTRACE_EVENT_SECCOMP and the tracee is restarted using PTRACE_SYSCALL, the next stop you observe will be a syscall-exit-stop โ not the syscall-entry-stop you were probably expecting. ๐
โจ PTRACE_EVENT_SECCOMP stops (since Linux 4.8) โจ
Starting with Linux 4.8, the PTRACE_EVENT_SECCOMP stop got reordered to happen between syscall-entry-stop and syscall-exit-stop. Also note that seccomp no longer runs (and no PTRACE_EVENT_SECCOMP will be reported) if the system call is skipped because of PTRACE_SYSEMU.
Functionally, a PTRACE_EVENT_SECCOMP stop works pretty much comparably to a syscall-entry-stop (like, continuations using PTRACE_SYSCALL will cause syscall-exit-stops, the system call number may be changed, and any other modified registers are visible to the to-be-executed system call as well). Just keep in mind there may be โ but need not have been โ a preceding syscall-entry-stop.
After a PTRACE_EVENT_SECCOMP stop, seccomp gets rerun, with a SECCOMP_RET_TRACE rule now functioning the same as a SECCOMP_RET_ALLOW. Specifically: if registers aren't modified during the PTRACE_EVENT_SECCOMP stop, then the system call will then be allowed. ๐
๐ฆ PTRACE_SINGLESTEP stops ๐ฆ
[Details of these kinds of stops are yet to be documented.] โ yeah bestie, the manpage really just leaves us hanging here. ๐
๐ Informational and restarting ptrace commands ๐
Most ptrace commands โ literally all of them except PTRACE_ATTACH, PTRACE_SEIZE, PTRACE_TRACEME, PTRACE_INTERRUPT, and PTRACE_KILL โ require the tracee to be in a ptrace-stop. Otherwise they fail with ESRCH. No stop, no service. ๐ซ
When the tracee is in ptrace-stop, the tracer can read and write data to the tracee using informational commands. These commands leave the tracee in ptrace-stopped state:
ptrace(PTRACE_PEEKTEXT/PEEKDATA/PEEKUSER, pid, addr, 0);
ptrace(PTRACE_POKETEXT/POKEDATA/POKEUSER, pid, addr, long_val);
ptrace(PTRACE_GETREGS/GETFPREGS, pid, 0, &struct);
ptrace(PTRACE_SETREGS/SETFPREGS, pid, 0, &struct);
ptrace(PTRACE_GETREGSET, pid, NT_foo, &iov);
ptrace(PTRACE_SETREGSET, pid, NT_foo, &iov);
ptrace(PTRACE_GETSIGINFO, pid, 0, &siginfo);
ptrace(PTRACE_SETSIGINFO, pid, 0, &siginfo);
ptrace(PTRACE_GETEVENTMSG, pid, 0, &long_var);
ptrace(PTRACE_SETOPTIONS, pid, 0, PTRACE_O_flags);
Now note that some errors just don't get reported. ๐ For example, setting signal information (siginfo) may have no effect in some ptrace-stops, yet the call can still succeed (return 0 and not set errno). And querying PTRACE_GETEVENTMSG may succeed and hand you back some random value if the current ptrace-stop isn't documented as returning a meaningful event message. It's giving "trust me bro". ๐ญ
The call
ptrace(PTRACE_SETOPTIONS, pid, 0, PTRACE_O_flags);
affects one tracee. The tracee's current flags get replaced. And those flags are inherited by new tracees created and "auto-attached" via the active PTRACE_O_TRACEFORK, PTRACE_O_TRACEVFORK, or PTRACE_O_TRACECLONE options. ๐ซถ
Another group of commands makes the ptrace-stopped tracee run. They look like this:
ptrace(cmd, pid, 0, sig);
where cmd is PTRACE_CONT, PTRACE_LISTEN, PTRACE_DETACH, PTRACE_SYSCALL, PTRACE_SINGLESTEP, PTRACE_SYSEMU, or PTRACE_SYSEMU_SINGLESTEP. If the tracee is in signal-delivery-stop, sig is the signal to be injected (if it's nonzero). Otherwise, sig may be ignored. (And when you're restarting a tracee from a ptrace-stop that isn't signal-delivery-stop, the recommended practice is to always pass 0 in sig.) ๐
๐ธ Attaching and detaching ๐ธ
A thread can be attached to the tracer using the call
ptrace(PTRACE_ATTACH, pid, 0, 0);
or
ptrace(PTRACE_SEIZE, pid, 0, PTRACE_O_flags);
PTRACE_ATTACH sends SIGSTOP to this thread. If the tracer wants that SIGSTOP to have no effect, it needs to suppress it. And heads up: if other signals are concurrently sent to this thread during attach, the tracer may see the tracee enter signal-delivery-stop with those other signal(s) first! ๐ณ The usual practice is to reinject those signals until SIGSTOP shows up, then suppress the SIGSTOP injection. The design bug here (yes, actual bug, not a vibe) is that a ptrace attach and a concurrently delivered SIGSTOP may race, and the concurrent SIGSTOP may get lost. ๐
Since attaching sends SIGSTOP and the tracer usually suppresses it, this may cause a stray EINTR return from the currently executing system call in the tracee โ exactly like it says in the "Signal injection and suppression" section.
Since Linux 3.4, PTRACE_SEIZE can be used instead of PTRACE_ATTACH. PTRACE_SEIZE does not stop the attached process. So if you need to stop it after attach (or at any other time) without sending it any signals, use the PTRACE_INTERRUPT command. โจ
The operation
ptrace(PTRACE_TRACEME, 0, 0, 0);
turns the calling thread into a tracee. The thread keeps running (it doesn't enter ptrace-stop). A common practice is to follow PTRACE_TRACEME with
raise(SIGSTOP);
and let the parent (which is our tracer now, hi bestie ๐) observe our signal-delivery-stop.
If the PTRACE_O_TRACEFORK, PTRACE_O_TRACEVFORK, or PTRACE_O_TRACECLONE options are in effect, then children get automatically attached to the same tracer which traced their parent. Specifically: children created by vfork(2) or clone(2) with the CLONE_VFORK flag, children created by fork(2) or clone(2) with the exit signal set to SIGCHLD, and other kinds of clone(2). SIGSTOP is delivered to those children, causing them to enter signal-delivery-stop after they exit the system call which created them. ๐ผ
Detaching of the tracee is performed by:
ptrace(PTRACE_DETACH, pid, 0, sig);
PTRACE_DETACH is a restarting operation, so it requires the tracee to be in ptrace-stop. If the tracee is in signal-delivery-stop, a signal can be injected. Otherwise, the sig parameter may be silently ignored. ๐คท
If the tracee is running when the tracer wants to detach it, the usual solution is to send SIGSTOP (using tgkill(2), so you make sure it goes to the correct thread), wait for the tracee to stop in signal-delivery-stop for SIGSTOP, and then detach it (suppressing the SIGSTOP injection). A design bug is that this can race with concurrent SIGSTOPs. Another complication: the tracee may enter other ptrace-stops and needs to be restarted and waited for again, until SIGSTOP is finally seen. Yet another complication: you have to be sure the tracee isn't already ptrace-stopped, because no signal delivery happens while it is โ not even SIGSTOP. ๐ตโ๐ซ
If the tracer dies, all tracees are automatically detached and restarted โ unless they were in group-stop. Handling of restart from group-stop is currently buggy, but the "as planned" behavior is to leave the tracee stopped and waiting for SIGCONT. And if the tracee is restarted from signal-delivery-stop, the pending signal gets injected. ๐
execve(2) under ptrace ๐
okay bestie, sit down for this one, because this is the part that makes tracers go "wait, WHAT". ๐ซ
When one thread in a multithreaded process calls execve(2), the kernel literally destroys all the other threads in that process, and resets the thread ID of the execing thread to be the thread group ID (process ID). (Or, putting it another way: when a multithreaded process does an execve(2), at completion of the call, it appears as though the execve(2) occurred in the thread group leader, regardless of which thread did the execve(2).) This resetting of the thread ID looks so confusing to tracers:
- All other threads stop in
PTRACE_EVENT_EXITstop, if thePTRACE_O_TRACEEXIToption was turned on. Then all other threads except the thread group leader report death as if they exited via_exit(3)with exit code0. - The execing tracee changes its thread ID while it is in the
execve(2). (Remember, under ptrace, the "pid" returned fromwaitpid(2), or fed into ptrace calls, is the tracee's thread ID.) That is, the tracee's thread ID is reset to be the same as its process ID, which is the same as the thread group leader's thread ID. - Then a
PTRACE_EVENT_EXECstop happens, if thePTRACE_O_TRACEEXECoption was turned on. - If the thread group leader has reported its
PTRACE_EVENT_EXITstop by this time, it appears to the tracer that the dead thread leader "reappears from nowhere". (Note: the thread group leader does not report death viaWIFEXITED(status)until there is at least one other live thread. This eliminates the possibility that the tracer will see it dying and then reappearing.) If the thread group leader was still alive, for the tracer this may look as if the thread group leader returns from a different system call than it entered, or even "returned from a system call even though it was not in any system call". If the thread group leader was not traced (or was traced by a different tracer), then duringexecve(2)it will appear as if it has become a tracee of the tracer of the execing tracee.
All of the above effects are the artifacts of the thread ID change in the tracee. no cap, it's just the artifact of that ID glow-up. ๐ญ
The PTRACE_O_TRACEEXEC option is the recommended tool for dealing with this situation. First, it enables PTRACE_EVENT_EXEC stop, which occurs before execve(2) returns. In this stop, the tracer can use PTRACE_GETEVENTMSG to retrieve the tracee's former thread ID. (This feature was introduced in Linux 3.0.) Second, the PTRACE_O_TRACEEXEC option disables legacy SIGTRAP generation on execve(2).
When the tracer receives the PTRACE_EVENT_EXEC stop notification, it is guaranteed that except this tracee and the thread group leader, no other threads from the process are alive.
On receiving the PTRACE_EVENT_EXEC stop notification, the tracer should clean up all its internal data structures describing the threads of this process, and retain only one data structure โ one which describes the single still running tracee, with
thread ID == thread group ID == process ID.
Example: two threads call execve(2) at the same time:
*** we get syscall-enter-stop in thread 1: **
PID1 execve("/bin/foo", "foo" <unfinished ...>
*** we issue PTRACE_SYSCALL for thread 1 **
*** we get syscall-enter-stop in thread 2: **
PID2 execve("/bin/bar", "bar" <unfinished ...>
*** we issue PTRACE_SYSCALL for thread 2 **
*** we get PTRACE_EVENT_EXEC for PID0, we issue PTRACE_SYSCALL **
*** we get syscall-exit-stop for PID0: **
PID0 <... execve resumed> ) = 0
If the PTRACE_O_TRACEEXEC option is not in effect for the execing tracee, and if the tracee was PTRACE_ATTACHed rather that PTRACE_SEIZEd, the kernel delivers an extra SIGTRAP to the tracee after execve(2) returns. This is an ordinary signal (similar to one which can be generated by kill -TRAP), not a special kind of ptrace-stop. Employing PTRACE_GETSIGINFO for this signal returns si_code set to 0 (SI_USER). This signal may be blocked by signal mask, and thus may be delivered (much) later. โณ
Usually, the tracer (for example, strace(1)) would not want to show this extra post-execve SIGTRAP signal to the user, and would suppress its delivery to the tracee (if SIGTRAP is set to SIG_DFL, it is a killing signal). However, determining which SIGTRAP to suppress is not easy. Setting the PTRACE_O_TRACEEXEC option or using PTRACE_SEIZE and thus suppressing this extra SIGTRAP is the recommended approach. bestie said "just use the recommended approach", and honestly? she's right. โจ
Real parent
The ptrace API (ab)uses the standard UNIX parent/child signaling over waitpid(2). This used to cause the real parent of the process to stop receiving several kinds of waitpid(2) notifications when the child process is traced by some other process. messy messy messy. ๐
Many of these bugs have been fixed, but as of Linux 2.6.38 several still exist; see BUGS below.
As of Linux 2.6.38, the following is believed to work correctly:
- exit/death by signal is reported first to the tracer, then, when the tracer consumes the
waitpid(2)result, to the real parent (to the real parent only when the whole multithreaded process exits). If the tracer and the real parent are the same process, the report is sent only once.
๐ RETURN VALUE ๐
On success, the PTRACE_PEEK* operations return the requested data (but see NOTES), the PTRACE_SECCOMP_GET_FILTER operation returns the number of instructions in the BPF program, the PTRACE_GET_SYSCALL_INFO operation returns the number of bytes available to be written by the kernel, and other operations return zero.
On error, all operations return -1, and errno is set to indicate the error. Since the value returned by a successful PTRACE_PEEK* operation may be -1, the caller must clear errno before the call, and then check it afterward to determine whether or not an error occurred. Yes bestie, clear it FIRST. The classic "forgot to zero errno" heartbreak, we do not do that here. ๐
๐ ERRORS ๐
EBUSYโ (i386 only) There was an error with allocating or freeing a debug register.EFAULTโ There was an attempt to read from or write to an invalid area in the tracer's or the tracee's memory, probably because the area wasn't mapped or accessible. Unfortunately, under Linux, different variations of this fault will returnEIOorEFAULTmore or less arbitrarily. choose-your-own-adventure error, iconic behaviour tbh.EINVALโ An attempt was made to set an invalid option.EIOโopis invalid, or an attempt was made to read from or write to an invalid area in the tracer's or the tracee's memory, or there was a word-alignment violation, or an invalid signal was specified during a restart operation.EPERMโ The specified process cannot be traced. This could be because the tracer has insufficient privileges (the required capability isCAP_SYS_PTRACE); unprivileged processes cannot trace processes that they cannot send signals to or those running set-user-ID/set-group-ID programs, for obvious reasons. Alternatively, the process may already be being traced, or (before Linux 2.6.26) beinit(1)(PID 1).ESRCHโ The specified process does not exist, or is not currently being traced by the caller, or is not stopped (for operations that require a stopped tracee).
๐ STANDARDS ๐
None. Nothing. Nada. This is a Linux girlie. ๐
๐ฆ HISTORY ๐ฆ
SVr4, 4.3BSD.
Before Linux 2.6.26, init(1), the process with PID 1, may not be traced.
โจ NOTES โจ
Although arguments to ptrace() are interpreted according to the prototype given, glibc currently declares ptrace() as a variadic function with only the op argument fixed. It is recommended to always supply four arguments, even if the requested operation does not use them, setting unused/ignored arguments to 0L or (void *) 0. yes, all four, even the ones doing nothing. character development.
A tracees parent continues to be the tracer even if that tracer calls execve(2).
The layout of the contents of memory and the USER area are quite operating-system- and architecture-specific. The offset supplied, and the data returned, might not entirely match with the definition of struct user.
The size of a "word" is determined by the operating-system variant (e.g., for 32-bit Linux it is 32 bits).
This page documents the way the ptrace() call works currently in Linux. Its behavior differs significantly on other flavors of UNIX. In any case, use of ptrace() is highly specific to the operating system and architecture. tl;dr: this is not portable, do not treat it like it is.
Ptrace access mode checking ๐
Various parts of the kernel-user-space API (not just ptrace() operations), require so-called "ptrace access mode" checks, whose outcome determines whether an operation is permitted (or, in a few cases, causes a "read" operation to return sanitized data). These checks are performed in cases where one process can inspect sensitive information about, or in some cases modify the state of, another process. The checks are based on factors such as the credentials and capabilities of the two processes, whether or not the "target" process is dumpable, and the results of checks performed by any enabled Linux Security Module (LSM) โ for example, SELinux, Yama, or Smack โ and by the commoncap LSM (which is always invoked).
Prior to Linux 2.6.27, all access checks were of a single type. Since Linux 2.6.27, two access mode levels are distinguished:
PTRACE_MODE_READโ For "read" operations or other operations that are less dangerous, such as:get_robust_list(2);kcmp(2); reading/proc/pid/auxv,/proc/pid/environ, or/proc/pid/stat; orreadlink(2)of a/proc/pid/ns/*file.PTRACE_MODE_ATTACHโ For "write" operations, or other operations that are more dangerous, such as: ptrace attaching (PTRACE_ATTACH) to another process or callingprocess_vm_writev(2). (PTRACE_MODE_ATTACHwas effectively the default before Linux 2.6.27.)
Since Linux 4.5, the above access mode checks are combined (ORed) with one of the following modifiers:
PTRACE_MODE_FSCREDSโ Use the caller's filesystem UID and GID (seecredentials(7)) or effective capabilities for LSM checks.PTRACE_MODE_REALCREDSโ Use the caller's real UID and GID or permitted capabilities for LSM checks. This was effectively the default before Linux 4.5.
Because combining one of the credential modifiers with one of the aforementioned access modes is typical, some macros are defined in the kernel sources for the combinations:
PTRACE_MODE_READ_FSCREDSโ Defined asPTRACE_MODE_READ | PTRACE_MODE_FSCREDS.PTRACE_MODE_READ_REALCREDSโ Defined asPTRACE_MODE_READ | PTRACE_MODE_REALCREDS.PTRACE_MODE_ATTACH_FSCREDSโ Defined asPTRACE_MODE_ATTACH | PTRACE_MODE_FSCREDS.PTRACE_MODE_ATTACH_REALCREDSโ Defined asPTRACE_MODE_ATTACH | PTRACE_MODE_REALCREDS.
One further modifier can be ORed with the access mode:
PTRACE_MODE_NOAUDIT(since Linux 3.3) โ okay so this one says don't audit this access mode check. It's used for the ptrace access mode checks that literally only filter or sanitize the output (think: reading/proc/pid/stat), instead of actually throwing an error back at whoever asked. In those cases opening the file is not a security violation, so there's zero reason to generate a security audit record. This modifier just suppresses that audit record for that one specific access check. ๐
Quick lil heads-up bestie: every single PTRACE_MODE_* constant in this subsection is kernel-internal โ us user-space girlies can't see them. They're named here purely so we can label the different kinds of ptrace access mode checks that happen for various system calls and for accesses to various pseudofiles (like the ones under /proc). Other manual pages use these names as a lil shorthand for labeling the different kernel checks. fr fr, that's the whole point of them. ๐
The algorithm ๐ง โจ
So the algorithm used for ptrace access mode checking decides whether the calling process is allowed to do the corresponding thing to the target process. (And psst โ when you're opening /proc/pid files, the "calling process" is whoever's opening the file, and the process with that PID is the "target process".) Here's how it goes:
- If the calling thread and the target thread are in the same thread group, access is always allowed. slay, no further questions.
- If the access mode says
PTRACE_MODE_FSCREDS, then for the check in the next step, use the caller's filesystem UID and GID. (Ascredentials(7)notes, the filesystem UID and GID almost always have the same values as the corresponding effective IDs. almost always, bestie.)
Otherwise, the access mode says PTRACE_MODE_REALCREDS, so use the caller's real UID and GID for the checks in the next step. (Most APIs that check the caller's UID and GID use the effective IDs. But for historical reasons, the PTRACE_MODE_REALCREDS check uses the real IDs instead. history stays messy, we move.)
- Deny access if neither of these is true:
- The real, effective, and saved-set user IDs of the target match the caller's user ID, and the real, effective, and saved-set group IDs of the target match the caller's group ID.
- The caller has the
CAP_SYS_PTRACEcapability in the user namespace of the target.
- Deny access if the target process "dumpable" attribute has a value other than
1(SUID_DUMP_USER; see the discussion ofPR_SET_DUMPABLEinprctl(2)), and the caller does not have theCAP_SYS_PTRACEcapability in the user namespace of the target process. - The kernel LSM
security_ptrace_access_check()interface gets invoked to see if ptrace access is permitted. The results depend on the LSM(s). The implementation of this interface in thecommoncapLSM does the following:- If the access mode includes
PTRACE_MODE_FSCREDS, then use the caller's effective capability set in the following check; otherwise (the access mode saysPTRACE_MODE_REALCREDS, so) use the caller's permitted capability set. - Deny access if neither of these is true:
- The caller and the target process are in the same user namespace, and the caller's capabilities are a superset of the target process's permitted capabilities.
- The caller has the
CAP_SYS_PTRACEcapability in the target process's user namespace.
- If the access mode includes
Note that the commoncap LSM does not distinguish between PTRACE_MODE_READ and PTRACE_MODE_ATTACH. it said "they're the same picture" ๐
- If access hasn't been denied by any of the steps above... congrats babe, access is allowed. ๐
๐ /proc/sys/kernel/yama/ptrace_scope ๐
On systems with the Yama Linux Security Module (LSM) installed (i.e. the kernel was built with CONFIG_SECURITY_YAMA), the /proc/sys/kernel/yama/ptrace_scope file (available since Linux 3.4) can restrict your ability to trace a process with ptrace() โ and yeah, that also restricts your ability to use tools like strace(1) and gdb(1). ๐ซถ The whole goal here is preventing attack escalation, where a compromised process ptrace-attaches to other sensitive processes owned by the same user (think: a GPG agent or an SSH session) to scoop up extra credentials that might be chilling in memory, and thereby expand the scope of the attack. safety first, always.
More precisely, the Yama LSM limits two types of operations:
- Any operation that performs a ptrace access mode
PTRACE_MODE_ATTACHcheck โ for example,ptrace()PTRACE_ATTACH. (See the "Ptrace access mode checking" discussion above. ๐) ptrace()PTRACE_TRACEME.
A process that has the CAP_SYS_PTRACE capability can update the /proc/sys/kernel/yama/ptrace_scope file with one of these values:
0("classic ptrace permissions") โ no additional restrictions on operations that performPTRACE_MODE_ATTACHchecks (beyond those imposed bycommoncapand other LSMs). The use ofPTRACE_TRACEMEis unchanged.1("restricted ptrace") โ this is the default value, periodt. When doing an operation that requires aPTRACE_MODE_ATTACHcheck, the calling process must either have theCAP_SYS_PTRACEcapability in the user namespace of the target process, or it must have a predefined relationship with the target process. By default, that predefined relationship is that the target process must be a descendant of the caller.
A target process can use the prctl(2) PR_SET_PTRACER operation to declare an additional PID that's allowed to perform PTRACE_MODE_ATTACH operations on the target. See the kernel source file Documentation/admin-guide/LSM/Yama.rst for further details.
The use of PTRACE_TRACEME is unchanged.
2("admin-only attach") โ only processes with theCAP_SYS_PTRACEcapability in the user namespace of the target process may performPTRACE_MODE_ATTACHoperations or trace children that employPTRACE_TRACEME.3("no attach") โ no process may performPTRACE_MODE_ATTACHoperations or trace children that employPTRACE_TRACEME.
Once this value has been written to the file, it cannot be changed. like, ever. choose wisely bestie, there's no undo button. ๐ญ
With respect to values 1 and 2, note that creating a new user namespace effectively removes the protection offered by Yama. Wanna know why? Because a process in the parent user namespace whose effective UID matches the UID of the creator of a child namespace has all capabilities (including CAP_SYS_PTRACE) when performing operations within the child user namespace (and further-removed descendants of that namespace). So when a process tries to use user namespaces to sandbox itself, it inadvertently weakens the protections offered by the Yama LSM. the audacity, honestly ๐
๐ฆ C library/kernel differences ๐ฆ
At the system call level, PTRACE_PEEKTEXT, PTRACE_PEEKDATA, and PTRACE_PEEKUSER have a different API: they store the result at the address specified by the data parameter, and the return value is the error flag. The glibc wrapper function gives you the API described in DESCRIPTION above, with the result coming back through the function return value. yes, they're different, no, it's not a you problem, it's just the syscall being like that. โจ
๐ BUGS ๐
Okay bestie, grab a snack, this part is giving "known issues energy" and we're keeping every single one:
- On hosts with Linux 2.6 kernel headers,
PTRACE_SETOPTIONSis declared with a different value than the one for Linux 2.4. This leads to applications compiled with Linux 2.6 kernel headers failing when run on Linux 2.4. You can work around it by redefiningPTRACE_SETOPTIONStoPTRACE_OLDSETOPTIONS, if that is defined. - Group-stop notifications are sent to the tracer, but not to the real parent. Last confirmed on 2.6.38.6.
- If a thread group leader is traced and exits by calling
_exit(3), aPTRACE_EVENT_EXITstop will happen for it (if requested), but the subsequentWIFEXITEDnotification will not be delivered until all other threads exit. And as explained above, if one of the other threads callsexecve(2), the death of the thread group leader will never be reported. If the execed thread isn't traced by this tracer, the tracer will never know thatexecve(2)happened. One possible workaround is toPTRACE_DETACHthe thread group leader instead of restarting it in this case. Last confirmed on 2.6.38.6. - A
SIGKILLsignal may still cause aPTRACE_EVENT_EXITstop before actual signal death. This may change in the future;SIGKILLis meant to always immediately kill tasks, even under ptrace. Last confirmed on Linux 3.13. - Some system calls return with
EINTRif a signal was sent to a tracee, but delivery was suppressed by the tracer. (This is very typical operation โ debuggers usually do it on every attach, so they don't introduce a bogusSIGSTOP.) As of Linux 3.2.9, the following system calls are affected (and yeah, this list is likely incomplete):epoll_wait(2), andread(2)from aninotify(7)file descriptor. The usual symptom of this bug is that when you attach to a quiescent process with the command
strace -p <process-ID>
then, instead of the usual and expected one-line output such as
restart_syscall(<... resuming interrupted call ...>_
or
select(6, [5], NULL, [5], NULL_
(_ denotes the cursor position), you observe more than one line. For example:
clock_gettime(CLOCK_MONOTONIC, {15370, 690928118}) = 0
epoll_wait(4,_
What's not visible here is that the process was blocked in epoll_wait(2) before strace(1) attached to it. Attaching caused epoll_wait(2) to return to user space with the error EINTR. In this particular case, the program reacted to EINTR by checking the current time, and then executing epoll_wait(2) again. (Programs that don't expect such "stray" EINTR errors may behave in an unintended way upon an strace(1) attach. ๐ that's the tea.)
- And contrary to the normal rules, the glibc wrapper for
ptrace()can seterrnoto zero. iconic behavior, honestly. ๐
๐ SEE ALSO ๐
the whole friend group, assembled:
gdb(1)ltrace(1)strace(1)clone(2)execve(2)fork(2)gettid(2)prctl(2)seccomp(2)sigaction(2)tgkill(2)vfork(2)waitpid(2)exec(3)capabilities(7)signal(7)
okay that's the whole entire page bestie, you officially know ptrace now ๐ This one's from Linux man-pages 6.20, dated 2026-06-05, and it's the ptrace(2) page โ go forth and trace responsibly, mwah ๐๐ฆโจ
๐ฆ where this page comes from
- arch rolling: man-pages 6.20-1 ยท BSD-2-Clause AND BSD-3-Clause AND BSD-4.3TAHOE AND BSD-4-Clause-UC AND GPL-1.0-or-later AND GPL-2.0-only AND GPL-2.0-or-later AND GPL-3.0-or-later AND LGPL-3.0-linking-exception AND LGPL-3.0-or-later AND LicenseRef-Public-Domain AND LicenseRef-UltraPermissive AND Linux-man-pages-1-para AND Linux-man-pages-copyleft AND Linux-man-pages-copyleft-2-para AND Linux-man-pages-copyleft-var AND MIT AND Spencer-94
- nixpkgs unstable: man-pages 6.20
- ๐ the page's license: GPL-2.0-or-later
- Copyright 1999, ike Coleman <mkc@acm.org>
- Copyright 2011, Denys Vlasenko <dvlasenk@redhat.com>
- Copyright 2015-2016, Michael Kerrisk <mtk.manpages@gmail.com>
- ๐ girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms