Same two lines, two machines. On Docker Desktop on my Mac I step into an Alpine container, run sleep 30 & and then strace -p $!: "Process 10 attached". On an Ubuntu 26.04 server, a container from the same image, the same two commands: strace: attach: ptrace(PTRACE_SEIZE, 11): Operation not permitted. Same user, same capability set (CapEff: 00000000a80425fb on both sides), same seccomp profile. The difference is not in the container but in the kernel underneath: linuxkit 6.10.14's LSM list reads capability,bpf, Yama is not on it, and there is no file called /proc/sys/kernel/yama/ptrace_scope; Ubuntu's 7.0 kernel has one, and it says 1.
Everyone knows this file; the first answer on the internet is "set it to zero". I was curious about something else: what exactly 1 forbids. Ubuntu's own configuration file says "only direct child processes". The kernel source says something broader: the rule is built on lineage, grandchildren are fine, but siblings are not. And that is precisely why strace -p never works from a shell. The rest of this post tests that rule in a lab, along with its gates beyond ptrace (/proc/pid/mem, process_vm_readv, pidfd_getfd) and its three leaks (user namespaces, file capabilities, PR_SET_PTRACER). The lab is Ubuntu 26.04 / kernel 7.0.0-31, with no compiler: every experiment is a ten-line call through Python ctypes. As I did for kptr_restrict and perf_event_paranoid, I start with where the number comes from.
Where the 1 comes from
Three places, and all three say the same thing. First, the kernel itself: security/yama/yama_lsm.c contains static int ptrace_scope = YAMA_SCOPE_RELATIONAL;, so a kernel built with Yama boots at 1 even with no distribution file at all. Second, Ubuntu's procps package: on my 24.04 server it is /etc/sysctl.d/10-ptrace.conf, on the 26.04 lab machine it is /usr/lib/sysctl.d/55-ptrace.conf. The file moved and its number grew; the procps 2:4.0.4-9ubuntu1 changelog gives the reason as "ship configs in /usr, with higher priority — this ensures that Ubuntu's defaults take precedence over 50-default.conf from linux-sysctl-defaults". Third, me: on 24.04 I had written kernel.yama.ptrace_scope = 1 into /etc/sysctl.d/99-security.conf with my own hands back in May. Kernel already 1, package already 1, me a third time 1. This is what hardening checklists do to a person; it does no harm, but one day you will be checking three files to answer "who put this here".
The history is short: Ubuntu introduced the restriction with its own patch in 10.10, Kees Cook's "security: Yama LSM" commit (2d514487) landed upstream in 3.4, and levels 2 and 3 followed one release later in 3.5. The line the kernel prints at boot is still the same: "Yama: becoming mindful."
And the effect? I searched the kernel logs of two servers. Yama writes every denial with pr_notice_ratelimited as "ptrace attach of ... was attempted by ..."; on VPS3 since the 13 September boot, on the lab machine since 19 August across three boots: zero lines. In thirty-two days, not one process tried to attach to another. A lock that bothers nobody; until somebody types strace -p.
The rule: lineage, not children
Yama's ptrace_access_check hook separates the four levels with a switch. The core of level 1 is three conditions (preceded by a pid_alive check): the target must be a descendant of current (task_is_descendant), or there must be an exception registered with PR_SET_PTRACER (ptracer_exception_found), or the caller must hold CAP_SYS_PTRACE in the target's user namespace (ns_capable). None of the three, -EPERM. task_is_descendant starts from the target and walks the real_parent chain upward while pid > 0, returning 1 if it meets the caller on the way. There is no depth limit. Because the chain runs over real_parent, it breaks when the parent dies: if a supervisor goes down, or a daemon double-forks and hands itself to init (or a subreaper), that process is no longer anyone's descendant.
To test it, a Python process forked a sleep as its child and another as its grandchild, then called PTRACE_ATTACH on both:
parent 4049006 -> child 4049008 -> grandchild [4049009]
attach child : OK
attach grandchild: OK
sibling attach : EPERM
The grandchild is allowed. The sentence in Ubuntu's conf file, "only to direct child processes (e.g. gdb name-of-program and strace -f name-of-program work)", is incomplete; what counts is lineage rather than proximity. The third line is the real lesson: a sibling forked by the same parent cannot attach to its sibling.
That is why strace -p never works from a shell, even when the target is a process you started yourself. When you type sleep 120 &, sleep is a child of the shell; the strace -p $! you type next is also a child of the shell. They are siblings. In Yama's eyes strace is not an ancestor of sleep; the shell is, but the shell is not the one attaching. This is the mechanism under the "I can't even trace my own processes" complaint. The scenario where the rule does help is the parent attaching itself: a test runner, a supervisor or a crash handler can PTRACE_ATTACH to its children and grandchildren freely.
Not just ptrace, but only "attach"
Yama has two hooks: ptrace_traceme (which only acts at levels 2 and 3) and the one doing the real work, ptrace_access_check. The latter hangs off the kernel's "ptrace access mode" check rather than the ptrace() system call alone. The ptrace(2) man page splits that check into two classes: PTRACE_MODE_READ (/proc/pid/environ, /proc/pid/auxv, /proc/pid/stat, kcmp, get_robust_list) and PTRACE_MODE_ATTACH (PTRACE_ATTACH, process_vm_writev and friends). Yama only looks at the second; the first line in the source is if (mode & PTRACE_MODE_ATTACH). I tried this split on the same sibling sleep through six gates:
maps(READ) : OK
environ(READ) : OK
mem(ATTACH) : 13 Permission denied
process_vm_readv: EPERM
pidfd_getfd : EPERM
PTRACE_ATTACH : EPERM
Four gates closed, two open. A small oddity: /proc/pid/mem returns EACCES, the other three EPERM. The reason is in the call path; mem_open in fs/proc/base.c goes to mm_access with PTRACE_MODE_ATTACH, and that function in kernel/fork.c returns the access denial as ERR_PTR(-EACCES). process_vm_readv calls the same mm_access, but mm/process_vm_access.c explicitly maps EACCES to EPERM as "a more appropriate error code". Same Yama decision, two different errnos; it is easy to see "Permission denied" and not think of Yama. The kernel log, meanwhile, wrote the same sentence for all four, with no mention of process_vm_readv or pidfd_getfd in any of them:
ptrace attach of "sleep 120"[4048743] was attempted by "python3 yamalab.py probe 4048743"[4048747]
process_vm_readv (reading memory) and pidfd_getfd (duplicating another process's file descriptor) are explicitly tied to a PTRACE_MODE_ATTACH_REALCREDS check in their man pages. perf_event_open, on the other hand, when given another process's pid, asks for PTRACE_MODE_READ_REALCREDS via perf_check_permission in kernel/events/core.c (the mode is upgraded to ATTACH only if sigtrap is requested); Yama has no say at that gate, perf_event_paranoid does. The result is an odd asymmetry: reading a sibling process's counters does not trip Yama, tracing its system calls does.
The exception gate: PR_SET_PTRACER
This is the gate the documentation describes for crash handlers: the target process calls prctl(PR_SET_PTRACER, pid) to say "this pid may attach to me". There is a subtle detail in the source: ptracer_exception_found accepts, besides the registered tracer itself, everyone descended from the registered pid via task_is_descendant(parent, tracer). So if the target registers the pid of your interactive shell, every strace and every gdb you launch from that shell can attach. In the lab the victim registered $$:
PR_SET_PTRACER 4049119 -> OK pid 4049120
strace: Process 4049120 attached
An strace -p aimed at the same victim from a second SSH session got Operation not permitted; that session is not descended from the registered shell. Registering PR_SET_PTRACER_ANY (value -1) switches Yama off entirely for the process at level 1 (level 2 does not consult the exception), and the registration survives execve: I could attach to a process that registered ANY and then exec'd into sleep. The entry is removed either when the process itself writes 0 or when it dies (yama_task_free); a new registration replaces the old one. I learned this the accidental way too: when I pushed the victim into the background with nohup and closed the shell, the exception died with the registered tracer and strace went back to EPERM. The registration is tied to a living process; the pid number on its own means nothing.
The documentation lists who uses it: the crash handlers of KDE, Chromium and Firefox, and Wine's ptrace permission among its own processes. A supervisor you write yourself can use the same gate; it is a far narrower permission than pulling the sysctl down to zero.
Containers: why docker exec is a sibling
The container experiment on the server matches the intro: docker exec ctr strace -p 1 → EPERM. The ps output from inside the container already does not show strace as a child of pid 1; both have PPID 0. Looking from the host makes the reason clear:
4051978 1 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 6f3f5777...
4052001 4051978 sleep infinity <- the container's pid 1
4052092 4051978 sleep 40 <- started with docker exec
The process that enters via docker exec is not a child of pid 1 but another child of containerd-shim. A sibling. Yama's rule knows nothing about namespaces; a sidecar sharing the pid namespace with --pid=container:ctr got the same EPERM. The fix is the known one, --cap-add SYS_PTRACE: CapEff goes from a80425fb to a80c25fb (bit 19, the number of CAP_SYS_PTRACE) and strace -p 1 says "Process 1 attached". It worked for both docker exec and the sidecar.
The three layers in a container need separating, because all three can produce the same EPERM. Docker's default seccomp profile (moby/profiles, default.json) allows ptrace, process_vm_readv and process_vm_writev unconditionally on kernel 4.8 and newer; kcmp, pidfd_getfd and process_madvise are only on the list when CAP_SYS_PTRACE is present. So if you try pidfd_getfd in a container, seccomp cuts it off before Yama gets a turn, whereas ptrace does reach Yama. The way to tell them apart is the kernel log: seccomp's EPERM writes no line to dmesg, Yama's does. The third layer is AppArmor; the docker-default template allows ptrace between processes in the same profile with ptrace (trace,tracedby,read,readby) peer="docker-default", so once the capability is added it no longer stands in the way.
The pids in the log are from the host namespace: I had typed strace -p 1 inside the container, and the line says "sleep infinity"[4052001] was attempted by "strace -p 1"[4052067]. The command line is the container's, the pids are the host's. When working an incident you need to map that pid with docker inspect -f '{{.State.Pid}}'.
The same holds in Kubernetes: kernel.yama.ptrace_scope is not a namespaced sysctl, so it cannot be set through a pod's securityContext.sysctls; the Kubernetes documentation calls such sysctls "node-level" and says they must be set on the node by hand or through a privileged DaemonSet.
And the explanation for the difference on the Mac: Docker Desktop's linuxkit kernel does not enable Yama; mounting securityfs in a --privileged container and reading /sys/kernel/security/lsm gives capability,bpf. An strace -p that works on the developer's machine gets EPERM on the Ubuntu node in CI or on the server. The kernel edition of "works on my machine". (strace's main branch has added a hint for exactly this: when -p is refused it prints sysctl kernel.yama.ptrace_scope = 1 may be restricting this attach; the 6.19 in 26.04 does not have that line yet.)
Two more leaks and one steel door
User namespaces. The ptrace(2) man page warns explicitly: creating a new user namespace effectively removes Yama's protection, because processes in the parent namespace whose uid created the namespace hold all capabilities inside it, CAP_SYS_PTRACE included. Yama's third condition is ns_capable(__task_cred(child)->user_ns, CAP_SYS_PTRACE); if the target is in its own namespace, that condition is true for you. Since Ubuntu 24.04, AppArmor restricts unprivileged user namespaces (kernel.apparmor_restrict_unprivileged_userns = 1), and in the lab unshare -Ur genuinely could not write uid_map. But unshare -U sleep 30 ran: the process inside had CapEff: 0, in a capability-less namespace. And an unrelated strace -p under the same uid attached to it; /proc/pid/mem, process_vm_readv and pidfd_getfd opened as well. A process that locks itself into a namespace is weaker against those outside the door. For applications that confine themselves, this is an item to add to the ledger in the Landlock post.
File capabilities. If you see the suggestion "instead of zeroing the sysctl, setcap cap_sys_ptrace=ep /usr/bin/strace", stop. I tried it: I gave a copy of strace that capability, it attached to the sibling sleep; then it also attached to a process owned by root that I had started with sudo sleep 31. In ptrace(2)'s access algorithm, CAP_SYS_PTRACE in the target's namespace bypasses the uid match as well. Anyone who can run that binary can read root's memory; it is the same thing as setuid root in a less conspicuous disguise. I deleted the copy.
Dumpable. A door in the opposite direction: ssh-agent makes itself untraceable with the prctl(PR_SET_DUMPABLE, 0) in OpenSSH's platform-tracing.c. In the lab the parent process could not attach to its own child ssh-agent -D; it did attach to the sleep child next to it. This denial comes before Yama, at step four of the access algorithm, and writes no line to dmesg. If you have seen an EPERM that does not appear in Yama's log, this is the candidate.
Two notes on the levels. I switched level 2 on for a few seconds on the lab machine. strace -o /dev/null /bin/true died with two lines: first do_test_ptrace_get_syscall_info: PTRACE_TRACEME: Operation not permitted from strace's own self-test, then attach: ptrace(PTRACE_SEIZE, 4050505): Operation not permitted; and this time a ptrace traceme of ... line landed in the kernel log. The parent's PTRACE_ATTACH to its own child got EPERM as well; at 2 the lineage rule is switched off entirely, only CAP_SYS_PTRACE gets through. I did not switch level 3 on, because it cannot be switched back off: when the value equals the maximum, yama_dointvec_minmax locks the lower bound to the maximum too, and there is no way back until a reboot. Then there is who may write the sysctl: the same function demands capable(CAP_SYS_PTRACE), not CAP_SYS_ADMIN. I tried it as root with sudo capsh --drop=cap_sys_ptrace, and sysctl -w said "Operation not permitted".
Decision framework
-
Single-admin server, occasional
strace -porgdb -p: change nothing, run it undersudo.CAP_SYS_PTRACEpasses at 0, 1 and 2; at 3 nobody passes. -
Multi-user development box: before dropping to 0, write down who will be able to read whose memory; every process under the same uid opens up to every other, except those with dumpable=0 like
ssh-agent. -
Your own supervisor / test runner: let the parent itself do the attaching, lineage is free. If an external tool is needed, add
PR_SET_PTRACERto the target; registering the shell's pid covers every tool started from that shell. -
Container debugging:
--cap-add SYS_PTRACEis enough; leave the sysctl alone, it is not namespaced anyway. Ifpidfd_getfd/kcmpare needed, the capability is required for seccomp as well. -
Diagnosing
EPERM:dmesg | grep "ptrace attach"; a line means Yama, no line means seccomp or dumpable.EACCESon/proc/pid/memcan be Yama too. -
setcap cap_sys_ptrace: no. It is an alias for setuid root. -
Software that confines itself in a userns: Yama does not protect that process; if it holds sensitive memory,
PR_SET_DUMPABLE 0is the software's own job.
Conclusion
The promise of ptrace_scope=1 is "if one of your applications is compromised, it should not spread to the others". Zero denials in thirty-two days of logs say that on most servers this promise costs nobody anything. But the rule is both wider and narrower than assumed: wider, because it is open to grandchildren and to the entire lineage of a shell registered with PR_SET_PTRACER; narrower, because it is closed even to a sibling process you started yourself, and because it guards not just ptrace but all four gates leading to memory and file descriptors. The leaks have to be looked for around Yama: user namespaces remove the protection, a file capability turns it into a weapon against root, dumpable steps in before it. Understanding a hardening knob means knowing which question the 1 answers. This knob's question is not "who may attach to whom", but "who is whose ancestor".
Official Sources
- Yama — Linux kernel admin guide
- security/yama/yama_lsm.c — kernel source
- kernel/fork.c — mm_access and EACCES · mm/process_vm_access.c — EACCES→EPERM · kernel/events/core.c — perf_check_permission
- ptrace(2) — Ptrace access mode checking and the ptrace_scope section
- PR_SET_PTRACER(2const)
- process_vm_readv(2) · pidfd_getfd(2) · perf_event_open(2)
- Ubuntu Security Features — ptrace scope
- Ubuntu Kernel Hardening Roadmap — ptrace Protection
- moby/profiles — default seccomp profile · docker-default AppArmor template
- Kubernetes — Using sysctls in a Kubernetes Cluster
- OpenSSH platform-tracing.c — PR_SET_DUMPABLE
Top comments (0)