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
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.
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.
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 Classification
Applicable Actions
Actions Not to Blindly Apply
Explicit syscall entry/exit
Read corresponding call info, associate by TID
Flipping pairing status on every SIGTRAP
signal-delivery-stop
Read signal info, decide whether to forward
Discarding all signals uniformly
ptrace event
Update thread, exec, or exit state
Treating event codes as regular syscall results
group-stop
Handle job control according to the attachment method
Unconditional CONT, changing original stop semantics
Exited/Terminated
Reap thread state and incomplete records
Continuing 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.
{"id":"ptrace-per-thread-pairing","title":"Will a single signal stop disrupt syscall pairing?","summary":"Syscall entry/exit must be classified first, then paired by TID. Signal stops do not complete calls; thread exits leave incomplete observations.","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"two\"><section class=\"card\"><header>TID A</header><button data-tid=\"A\" data-op=\"entry\" data-i18n=\"entry\"></button><button data-tid=\"A\" data-op=\"signal\" data-i18n=\"signal\"></button><button data-tid=\"A\" data-op=\"exit\" data-i18n=\"exit\"></button><button data-tid=\"A\" data-op=\"death\" data-i18n=\"death\"></button></section><section class=\"card\"><header>TID B</header><button data-tid=\"B\" data-op=\"entry\" data-i18n=\"entry\"></button><button data-tid=\"B\" data-op=\"signal\" data-i18n=\"signal\"></button><button data-tid=\"B\" data-op=\"exit\" data-i18n=\"exit\"></button><button data-tid=\"B\" data-op=\"death\" data-i18n=\"death\"></button></section></div><section class=\"panel\"><h3 data-i18n=\"records\"></h3><div id=\"records\"></div></section><div class=\"metrics\"><div><span data-i18n=\"open\"></span><strong id=\"open\"></strong></div><div><span data-i18n=\"done\"></span><strong id=\"done\"></strong></div><div><span data-i18n=\"lost\"></span><strong id=\"lost\"></strong></div></div><div class=\"actions\"><button id=\"reset\" data-i18n=\"reset\"></button></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px var(--viz-font-ui);padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 var(--viz-font-ui);overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n.card button{margin:4px 0}#records .tr{grid-template-columns:.6fr 2fr}#records .tr>*{min-height:4em}.metrics strong{font-size:17px}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}let threads={A:{},B:{}},done=0,lost=0,serial=0;function draw(){table('records',Object.entries(threads).map(([n,s])=>[n,s.dead?t('ended'):s.call?'call #'+s.call:t('idle')]));set('open',Object.values(threads).filter(s=>s.call).length);set('done',done);set('lost',lost);document.querySelectorAll('[data-op]').forEach(e=>{const s=threads[e.dataset.tid];e.disabled=!!s.dead||(e.dataset.op==='entry'&&!!s.call)||(e.dataset.op==='exit'&&!s.call)});}document.querySelectorAll('[data-op]').forEach(e=>e.onclick=()=>{const s=threads[e.dataset.tid],op=e.dataset.op;if(op==='entry')s.call=++serial;if(op==='exit'){delete s.call;done++}if(op==='death'){if(s.call)lost++;delete s.call;s.dead=true}draw()});$('#reset').onclick=()=>{threads={A:{},B:{}};done=lost=serial=0;draw()};draw();","audio":false,"strings":{"death":"thread end","done":"paired","ended":"thread has ended","entry":"syscall entry","exit":"syscall exit","heading":"Will a single signal stop disrupt syscall pairing?","idle":"no incomplete calls","intro":"Independently add entry, signal stop, and exit to two threads, observing incomplete records per thread.","lost":"incomplete at end","open":"incomplete call","records":"per-thread observations","reset":"Reset","scope":"Offline classified events; no ptrace calls, no simulating exec, restarts, or mid-attachment. Entries and exits are optional only in states allowed by the model.","scopeLabel":"Model Scope and Conventions","signal":"signal stop"}}
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:
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 Strategy
What It Can Improve
What It Still Cannot Solve
Splitting remote iovec by page
Limiting fault boundaries for unknown addresses to smaller regions
Consistency when the entire object spans pages
Checking actual transfer length
Avoiding parsing of unread tail data
Target modifying content between two reads
Reading header first, then body by length
Limiting allocation size and read scope
Header and body belonging to different versions
Stopping all threads that modify the object
Reducing concurrent modifications within covered ranges
External 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_scope
Core Meaning
0
Classic permission check; not any user can trace any process
1
Restricted attachment, usually requiring appropriate ancestry or explicit authorization; administrator capabilities still have conditions
2
Only managers with the corresponding CAP_SYS_PTRACE capability can perform restricted tracing operations
3
Prohibits 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.