Repository navigation
Force disable usage of ptrace() until reboot / Increase kernel.yama.ptrace_scope: 2 → 3 #321
Description
Activity
Just took a look at the previous discussion. It might be worth increasing the strength of this setting, since obtaining CAP_SYS_PTRACE does not require root access, it can also be done by using
unshare()to enter a new user namespace. At that point, there are a couple of different waysptrace()could behave:- Maybe Linux's namespacing features are smart enough to keep anything not in the new namespace from being ptrace'd.
man 2 unsharedoesn't mention anything about this. - Maybe Linux allows any process running as the user that called the process that called
unshare()to be ptrace'd by the process. I have a suspicion (unconfirmed) that this is what happens... and if so that would essentially allow bypassing the ptrace restriction entirely.
I think we should probably do some more research on this. If it turns out we can dodge the ptrace restrictions through the user of
unshare(), then I would say we definitely should set this to the strictest setting. Otherwise, I think the previous logic is still sound, there's no need to keep root from ptrace'ing things. (Unless there are processes on the filesystem that ship with CAP_SYS_PTRACE enabled automatically, in which case the right solution would be stripping capabilities from those processes, something permission-hardener is already capable of.)- Maybe Linux's namespacing features are smart enough to keep anything not in the new namespace from being ptrace'd.
Thanks for the quick review and response. Unfortunately, my knowledge on
unshare()is not enough for me to comment on it's undocumented functionality. I personally do not see any reason not to set to the strictest setting if we can do so without breakages, which will not occur as it should already be inaccessible unless a user purposefully bypasses. For this case I am sure the user would be capable of manually reducing the setting back to the current level.Therefore, I suppose I will submit a draft PR while we continue this discussion.
Sounds good. One possible breakage this could cause, I learned the hard way that accessing files under
/procoftentimes requires ptrace-like privileges. I'm not sure if disabling this form of ptrace will break that use case, but if so, this would cause breakage since the kloak rewrite planned for Whonix 18 has to inspect other processes via/procin order to figure out which Wayland compositor to connect to. So that's something else we should probably test.Reacted by raja-grewalNow worries, let me know when you think it is appropriate to mark the PR ready for review.
At least in my initial experiments, I am not (so far) able to leverage unshare to allow me to ptrace processes I shouldn't be allowed to ptrace when using
kernel.yama.ptrace_scope=2. Indeed, even if I setkernel.yama.ptrace_scope=0, I can't ptrace things outside of a user namespace from within a user namespace, even if my UID is the same both inside and outside the namespace (and even if my UID inside the namespace is 0). So it seemsunshare()not only doesn't let me work around ptrace restrictions, it actually locks them down further. Nice.I guess setting
ptrace_scope=3might be useful if we're trying to limit the blast radius of a compromised root account. It won't really prevent anything since the root account can still load a kernel module that will let them inspect any memory in the system they'd like (they might be able to just read it from /proc/kcore too). This would probably make such attacks trickier, but it wouldn't really prevent them.I also tried setting
ptrace_scope=3and then snooping around in/proc. It looks like disabling ptrace does not break reading from/proc, and it does prohibit using the actualptrace()system call even as root, so I don't think/procis an obstacle.One serious issue though is that because this setting can't be undone, it can't be circumvented easily by
debug-miscunless the setting is placed in its own file anddebug-miscdiverts that file somewhere else. Other than that, I can't see any strong technical reasons to avoid this, and even if it adds no theoretical security, extra frustration for attackers seems like a good thing.@adrelanos Thoughts?
Reacted by raja-grewalFor reference, this is the program I wrote to test
unshareandptrace. UID 0 is mapped to UID 1001 because I was running this as thesysmaintuser, and on this particular VMsysmainthad a UID of1001.#define _GNU_SOURCE #include <sys/ptrace.h> #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <limits.h> #include <stdint.h> #include <unistd.h> #include <sched.h> #include <fcntl.h> int main(int argc, char **argv) { if (argc != 2) { fprintf(stderr, "FATAL ERROR: PID of process to attach to must be given as an argument!\n"); exit(1); } pid_t trace_pid = 0; long trace_pid_parse = 0; char *check_ptr = NULL; errno = 0; trace_pid_parse = strtol(argv[1], &check_ptr, 10); if (errno != 0) { fprintf(stderr, "FATAL ERROR: Could not parse PID argument: %s\n", strerror(errno)); exit(1); } if (*check_ptr != '\0') { fprintf(stderr, "FATAL ERROR: Invalid PID argument provided!\n"); exit(1); } if (trace_pid_parse > INT32_MAX || trace_pid_parse < 0) { fprintf(stderr, "FATAL ERROR: PID argument out of range!\n"); exit(1); } trace_pid = (pid_t)(trace_pid_parse); if (unshare(CLONE_NEWUSER) == -1) { fprintf(stderr, "FATAL ERROR: Could not drop to new user namespace: %s\n", strerror(errno)); exit(1); } int setgroups_fd = open("/proc/self/setgroups", O_WRONLY); if (setgroups_fd == -1) { fprintf(stderr, "FATAL ERROR: Could not open /proc/self/setgroups: %s\n", strerror(errno)); exit(1); } if (dprintf(setgroups_fd, "deny\n") == -1) { fprintf(stderr, "FATAL ERROR: Could not write to /proc/self/setgroups: %s\n", strerror(errno)); exit(1); } if (close(setgroups_fd) == -1) { fprintf(stderr, "FATAL ERROR: Could not close /proc/self/setgroups: %s\n", strerror(errno)); exit(1); } int gid_map_fd = open("/proc/self/gid_map", O_WRONLY); if (gid_map_fd == -1) { fprintf(stderr, "FATAL ERROR: Could not open /proc/self/gid_map: %s\n", strerror(errno)); exit(1); } if (dprintf(gid_map_fd, "0 1001 1\n") == -1) { fprintf(stderr, "FATAL ERROR: Failed to write to /proc/self/gid_map: %s\n", strerror(errno)); exit(1); } if (close(gid_map_fd) == -1) { fprintf(stderr, "FATAL ERROR: Could not close /proc/self/gid_map: %s\n", strerror(errno)); exit(1); } int uid_map_fd = open("/proc/self/uid_map", O_WRONLY); if (uid_map_fd == -1) { fprintf(stderr, "FATAL ERROR: Could not open /proc/self/uid_map: %s\n", strerror(errno)); exit(1); } if (dprintf(uid_map_fd, "0 1001 1\n") == -1) { fprintf(stderr, "FATAL ERROR: Failed to write to /proc/self/uid_map: %s\n", strerror(errno)); exit(1); } if (close(uid_map_fd) == -1) { fprintf(stderr, "FATAL ERROR: Could not close /proc/self/uid_map: %s\n", strerror(errno)); exit(1); } if (setresuid(0, 0, 0) == -1) { fprintf(stderr, "FATAL ERROR: Could not setresuid: %s\n", strerror(errno)); exit(1); } if (setresgid(0, 0, 0) == -1) { fprintf(stderr, "FATAL ERROR: Could not setresgid: %s\n", strerror(errno)); exit(1); } printf("%d\n", getuid()); long ret = ptrace(PTRACE_ATTACH, trace_pid, NULL, NULL); if (ret != 0) { fprintf(stderr, "PTRACE_ATTACH on process %d failed: %s\n", trace_pid, strerror(errno)); exit(1); } sleep(1); ret = ptrace(PTRACE_DETACH, trace_pid, NULL, NULL); if (ret != 0) { fprintf(stderr, "PTRACE_DETACH on process %d failed: %s\n", trace_pid, strerror(errno)); exit(1); } exit(0); }
Reacted by raja-grewalI also agree with soundness your argument when it comes to at the very least raising frustration even though it does mean in practice that it will make debugging require a reboot.
For this reason I am going to mark the PR as ready for review given after also thinking about it I can not also think of any other reasons not to enable this moving forward.
- changed the title
[-]Disable usage of `ptrace()`[/-][+]Force disable usage of `ptrace()` until reboot / Increase `kernel.yama.ptrace_scope`: `2` → `3`[/+]on Oct 2, 2025 Pull request welcome.
Note: Needs to be implemented in a separate file so this can be disabled in
debug-misc.One serious issue though is that because this setting can't be undone, it can't be circumvented easily by debug-misc unless the setting is placed in its own file and debug-misc diverts that file somewhere else. Other than that, I can't see any strong technical reasons to avoid this, and even if it adds no theoretical security, extra frustration for attackers seems like a good thing.
^ Note this, the way in which debug-misc works here will likely need to be done differently than normal.
Ok thanks for the feedback. The file has been split separately the PR. I am not exactly certain how to go about disabling it in
debug-miscso as not to break something, so if either of you could do that corresponding change it would be great.- added a commit that references this issue
on Oct 19, 2025
Given the large amounts of new security hardening settings that are being incorporated into the upcoming Debian 13 port, there is another
sysctlsetting we should consider revisiting for two reasons, one to reopen the discussion, and two in order to adhere to KSPP requirements.This is again regarding the use use of process trace
ptrace()which is currently set tokernel.yama.ptrace_scope=2inside the configs.As per the kernel docs I previously proposed increasing this to the KSPP recommenced
kernel.yama.ptrace_scope=3in #242 but we decided to keep it as is since it was at the time considered outside the scope of security-misc.The question is whether the same logic still hold now over 15 months later?
Note that several other projects focused on security hardening such as Brace, nix-mineral, and secureblue all utilise the strictest setting by default.
Just like we decided in #313 to enable
kernel.panic=-1forcing instant reboots when previously we allowed the system to hang forever, maybe we should also consider tightening up this loose bolt?I am happy to submit a draft PR if there are no objections.
@adrelanos @ArrayBolt3