ptrace and Debugging Interfaces

On this page

ptrace allows a tracer to observe and control a tracee thread subject to permission rules, such as reading registers, inspecting memory, continuing execution, or single-stepping at stop points. It is commonly used by native Linux debuggers and system call tracers, but the entire process, thread group, and a single traced thread are not the same entity.

/proc/PID/mem, process_vm_readv, and seccomp user notification each solve parts of the problem and should not be directly viewed as interchangeable "next-generation ptrace" replacements.

After Establishing the Trace Relationship, Wait for an Operable State

PTRACE_TRACEME is called by the tracee to request tracing by its parent process; it does not imply that any other process has permission to trace it thereafter. PTRACE_ATTACH attaches and triggers the corresponding stop sequence, but the tracer must still wait to confirm; PTRACE_SEIZE establishes the relationship without immediately stopping, after which PTRACE_INTERRUPT can be used to request a stop.

ptrace requests typically specify threads by TID. A thread can have only one tracer; attaching to a specific TID does not automatically bring all existing and future threads under full control. You need to enumerate existing threads and set event options for clone, fork, exec, exit, etc., as needed.

Some read/write requests require the tracee to already be in an appropriate ptrace-stop state. Merely seeing the process "seem not to be running" or having just called ATTACH is not enough to guarantee that reading registers will succeed immediately. ptrace(2)

Stop States are the Core of Debugging

First step of the debugging loop: identify the reason for the stop, then decide how to continue waitpid returns tracee status Exited, terminated, or stopped? Syscall stop Read entry/exit info Signal delivery stop Decide whether to forward the original signal ptrace event or other stops Handle thread and execution state according to the event SIGTRAP is not the only reason for a stop; do not assume that every two waits correspond exactly to a syscall entry and exit.

Between syscall entry and exit, signals, exec, exit, or other events may occur. A real loop must first distinguish between WIFEXITED, WIFSIGNALED, and WIFSTOPPED, then interpret the specific stop type, rather than unconditionally treating two waits as a pair.

PTRACE_O_TRACESYSGOOD can add 0x80 to the signal identifier for syscall stops, making it easier to distinguish from regular SIGTRAP. When supported, PTRACE_GET_SYSCALL_INFO provides more explicit entry/exit information, reducing direct dependency on architecture-specific register layouts; however, you still need to handle syscall restarts and event conversions.

After a signal delivery stop, continuing with 0 may suppress the signal; passing the original signal allows it to continue delivery. You cannot pass 0 for all stops and assume the trace is completely transparent to program semantics. Handling of group-stops, ptrace event stops, and regular signal stops also differs.

Syscall pairing belongs to the thread; other stops cannot flip entry/exit states TID 11: entry read Record the call for the current thread TID 12: entry write Create a separate state TID 11: signal stop Identify the signal, do not treat as exit TID 12: exit write Only end the pairing for thread 12 TID 11: exit read Only end the pairing for thread 11 The diagram shows an offline event sequence; real-time collection must also handle syscall restarts, exec, thread exits, and mismatches.

Classification and Pairing Must Be Done Separately

First, distinguish between exit and stop based on the wait status, then identify events from the stop information; do not flip an "enter/exit" boolean first and then try to ignore signals. When using PTRACE_GET_SYSCALL_INFO, also check the returned op; only explicit ENTRY/EXIT should enter the corresponding handling; SECCOMP and NONE cannot be forced into ordinary pairing. ptrace stops and syscall info

Below is an already-classified offline event model that does not call ptrace. It demonstrates why a global in_syscall boolean is insufficient when two threads interleave; when a thread terminates without a regular syscall exit, incomplete observations are recorded separately.

pending = {}
completed = []
incomplete = []
signal_stops = []
events = [
    (11, "entry", "read"),
    (12, "entry", "write"),
    (11, "signal", "SIGUSR1"),
    (12, "exit", 3),
    (11, "exit", 8),
    (12, "entry", "exit_group"),
    (12, "terminated", 0),
]
for tid, kind, value in events:
    if kind == "entry":
        assert tid not in pending
        pending[tid] = value
    elif kind == "exit":
        assert tid in pending
        completed.append((tid, pending.pop(tid), value))
    elif kind == "signal":
        signal_stops.append((tid, value))
    elif kind == "terminated":
        if tid in pending:
            incomplete.append((tid, pending.pop(tid)))
assert completed == [(12, "write", 3), (11, "read", 8)]
assert signal_stops == [(11, "SIGUSR1")]
assert incomplete == [(12, "exit_group")]
assert not pending

The model's asserts are there to expose broken pairings in the input. A real tracer cannot simply exit with a program error when it encounters a thread already inside a syscall, exec, or restart path upon attachment; it needs to reconstruct state based on the observation starting point and event semantics, and mark records that cannot be reliably associated. Timing statistics can only cover the interval of actual pairings; calls with missing exits cannot be recorded as zero-duration.

Stop ClassificationApplicable ActionsActions Not to Blindly Apply
Explicit syscall entry/exitRead corresponding call info, associate by TIDFlipping pairing status on every SIGTRAP
signal-delivery-stopRead signal info, decide whether to forwardDiscarding all signals uniformly
ptrace eventUpdate thread, exec, or exit stateTreating event codes as regular syscall results
group-stopHandle job control according to the attachment methodUnconditional CONT, changing original stop semantics
Exited/TerminatedReap thread state and incomplete recordsContinuing to read registers that no longer exist

Will a single signal stop disrupt syscall pairing?

Let A enter a syscall, then let B enter, then add a signal stop to A, and finally finish B's call. The two calls can still be paired correctly; if you use a global enter/exit switch, the second entry would be mistakenly recognized as an exit. Unpaired records at thread termination should be marked as incomplete and cannot be recorded as zero-duration.

Preparing the visual
Will a single signal stop disrupt syscall pairing?

Syscall entry/exit must be classified first, then paired by TID. Signal stops do not complete calls; thread exits leave incomplete observations.

System Call Observation and Perturbation

The PTRACE_SYSCALL request stops at relevant syscall boundaries. At entry, you can read the syscall number and arguments; at exit, you can read the return value; the raw return of a negative errno differs from the libc wrapper's -1 + errno. On x86-64, the 4th syscall argument is in r10, not rcx as in regular C calls; arm64 has its own ABI.

Each stop involves kernel processing, scheduling the tracer, and reading information; frequent syscalls will be noticeably perturbed. You cannot fix the overhead to exactly four context switches, nor can you directly replace normal performance metrics with the traced execution time.

strace can filter displayed content, but display filtering is not at the same level as whether ptrace stops still occur; some optimization modes use seccomp to reduce irrelevant stops. Performance hotspots can first be located using sampling or BPF aggregation, then verified with fine-grained tracing for specific behaviors.

Reading Registers and Memory

PTRACE_GETREGS does not provide the same interface on all architectures. PTRACE_GETREGSET with the appropriate note type and iovec is better suited for taking state by register set, but still requires interpreting byte layouts based on the target architecture.

PEEK operations may successfully read data that equals -1, so you must combine this with errno:

#include <errno.h>
#include <stddef.h>
#include <sys/ptrace.h>
#include <sys/types.h>

int peek_word(pid_t tid, const void *address, long *out) {
    errno = 0;
    long value = ptrace(PTRACE_PEEKDATA, tid, address, NULL);
    if (value == -1 && errno != 0) return -1;
    *out = value;
    return 0;
}

The caller must first obtain permission and ensure the target is in an appropriate stop state. Reading memory by machine word also involves length, alignment, page boundaries, and target word size; a successful read of one word does not imply that the adjacent page is also readable.

Software and Hardware Breakpoints

On x86, software breakpoints commonly replace the beginning of the target instruction with a single-byte INT3. Upon hit, the debugger identifies the breakpoint, restores the original instruction, and adjusts the PC according to architecture rules (the INT3 stop position is usually past that byte), executes the original instruction, and then re-inserts the breakpoint.

Simply saying "continue after restoration" misses re-insertion and multi-thread contention: another thread might pass that location while the breakpoint is temporarily removed. Debuggers need to pause coordination, single-step, or use other strategies. arm64's BRK encoding and PC handling differ; you cannot generalize 0xCC to all targets.

Hardware execution breakpoints and watchpoints are implemented according to architecture debug resources, with limits on quantity, length, and alignment. Optimizations may also cause one source line to correspond to multiple addresses, or a variable to have no readable memory location; this is not simply a wrong register read.

/proc/PID/mem and process_vm_readv

/proc/PID/mem allows accessing memory via file offsets based on the target's virtual address. Opening is subject to ptrace access modes and other security rules; it is not a bypass of permissions. Whether you need to stop the target first, and which addresses are accessible, also depends on consistency requirements and specific interface behaviors. proc_pid_mem(5)

process_vm_readv/writev can transfer memory in bulk between processes, provided since Linux 3.2, not just 5.x. They may transfer partially, do not guarantee atomicity for the entire vector operation, and do not freeze the target for the application; reading a list being modified may stitch together field combinations that never existed simultaneously. process_vm_readv(2)

Bulk reads are suitable for reducing access overhead, but do not provide single-step, breakpoint, or register stop semantics. Access permissions and page protection details should not be generalized as "newer kernels allow everything."

A partially positive read result only proves a copied prefix

Assume the local target buffer capacity is 8192, and process_vm_readv returns 4096. Only the first 4096 bytes are data successfully copied in this instance; the second half cannot be parsed as a new snapshot; "returning non-negative" is not enough to prove the entire structure is complete. Before submitting parsing results, verify that the actual length covers the required fields.

Read StrategyWhat It Can ImproveWhat It Still Cannot Solve
Splitting remote iovec by pageLimiting fault boundaries for unknown addresses to smaller regionsConsistency when the entire object spans pages
Checking actual transfer lengthAvoiding parsing of unread tail dataTarget modifying content between two reads
Reading header first, then body by lengthLimiting allocation size and read scopeHeader and body belonging to different versions
Stopping all threads that modify the objectReducing concurrent modifications within covered rangesExternal modifiers like shared mappings from other processes

process_vm_readv handles partial transfers at iovec element boundaries; when reading strings of unknown length, do not let a single remote iovec cross unknown pages without boundaries. More importantly, the copy process does not provide atomic snapshot semantics for application objects. process_vm_readv transfer conventions

For example, reading an old length=100, then reading a new pointer that has already been replaced; individually, both fields are readable, but put together they may not have existed simultaneously. If the target program can cooperate, using an immutable snapshot generated and published by the target itself is easier to define consistency for; if it cannot cooperate, you must retain the collection scope and consistency limitations in the results.

seccomp user notification: Delegating Handling of Some Syscalls

seccomp filters can cause matching calls to send notifications to listeners, allowing the supervising process to return results, errors, or, in supported cases, continue the original call or inject fds. It is suitable for proxying and virtualizing specific syscalls, not as a general-purpose user instruction debugger.

The memory pointed to by user pointers appearing in notifications may be modified by other threads. Reading the path for inspection first, then allowing the original syscall to continue using the same pointer, presents a TOCTOU race: the content may have changed by the time the call executes. Notification ID validity checks only solve part of the lifecycle problem and do not prove that user memory content is frozen.

Therefore, the CONTINUE method cannot be treated as a general solution for secure authorization of pointer content, nor can the supervising side's "check/modify/approve" be written as an atomic transaction. Copy, proxy operations, and return controlled resources as needed, adhering to the specific race handling rules of the interface. seccomp_unotify(2)

Yama is an Attachment Restriction, Not a Replacement for Basic Permissions

ptrace_scopeCore Meaning
0Classic permission check; not any user can trace any process
1Restricted attachment, usually requiring appropriate ancestry or explicit authorization; administrator capabilities still have conditions
2Only managers with the corresponding CAP_SYS_PTRACE capability can perform restricted tracing operations
3Prohibits corresponding attachments and TRACEME tracing; once set, this value cannot be lowered

Specifics also depend on credentials, dumpable status, user namespaces, other LSMs, and capabilities. You cannot write "2" as "only descendants can trace." Modern Linux also does not have a simple rule that universally prohibits PID 1 in all permission environments. Yama documentation

When diagnosing permission failures, first confirm parent-child relationships, TID, credentials, and system policies, rather than directly modifying global restrictions. Tracing programs with privileged execution may also change credential elevation behavior during exec, so debugging state and normal running state may differ.

Choosing Observation Interfaces

For breakpoints, single-stepping, and controlled register access, choose debuggers/ptrace; for bulk memory checks, combine with process_vm_readv; for statistics on high-frequency behaviors, consider perf/BPF; for syscall proxies, design according to the seccomp notification protocol. All solutions require clear observation scopes and consistency; faster tools do not automatically mean more complete evidence.

Keywords: ptrace, tracee, TID, syscall stop, TRACESYSGOOD, GETREGSET, breakpoint, process_vm_readv, seccomp notification, Yama, TOCTOU