Skip to content

Force disable usage of ptrace() until reboot / Increase kernel.yama.ptrace_scope: 2 → 3 #321

Description

@raja-grewal

Given the large amounts of new security hardening settings that are being incorporated into the upcoming Debian 13 port, there is another sysctl setting 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 to kernel.yama.ptrace_scope=2 inside the configs.

As per the kernel docs I previously proposed increasing this to the KSPP recommenced kernel.yama.ptrace_scope=3 in #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=-1 forcing 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

Activity

  1. ArrayBolt3 commented on Sep 28, 2025

    @ArrayBolt3
    Contributor

    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 ways ptrace() could behave:

    • Maybe Linux's namespacing features are smart enough to keep anything not in the new namespace from being ptrace'd. man 2 unshare doesn'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.)

  2. raja-grewal commented on Sep 28, 2025

    @raja-grewal
    ContributorAuthor

    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.

  3. ArrayBolt3 commented on Sep 28, 2025

    @ArrayBolt3
    Contributor

    Sounds good. One possible breakage this could cause, I learned the hard way that accessing files under /proc oftentimes 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 /proc in order to figure out which Wayland compositor to connect to. So that's something else we should probably test.

  4. raja-grewal commented on Sep 28, 2025

    @raja-grewal
    ContributorAuthor

    Now worries, let me know when you think it is appropriate to mark the PR ready for review.

  5. ArrayBolt3 commented on Sep 30, 2025

    @ArrayBolt3
    Contributor

    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 set kernel.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 seems unshare() not only doesn't let me work around ptrace restrictions, it actually locks them down further. Nice.

    I guess setting ptrace_scope=3 might 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=3 and then snooping around in /proc. It looks like disabling ptrace does not break reading from /proc, and it does prohibit using the actual ptrace() system call even as root, so I don't think /proc is an obstacle.

    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.

    @adrelanos Thoughts?

  6. ArrayBolt3 commented on Sep 30, 2025

    @ArrayBolt3
    Contributor

    For reference, this is the program I wrote to test unshare and ptrace. UID 0 is mapped to UID 1001 because I was running this as the sysmaint user, and on this particular VM sysmaint had a UID of 1001.

    #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);
    }
  7. raja-grewal commented on Oct 1, 2025

    @raja-grewal
    ContributorAuthor

    I 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.

  8. 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
  9. adrelanos commented on Oct 10, 2025

    @adrelanos
    Contributor

    Pull request welcome.

    Note: Needs to be implemented in a separate file so this can be disabled in debug-misc.

  10. ArrayBolt3 commented on Oct 10, 2025

    @ArrayBolt3
    Contributor

    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.

  11. raja-grewal commented on Oct 12, 2025

    @raja-grewal
    ContributorAuthor

    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-misc so as not to break something, so if either of you could do that corresponding change it would be great.

  12. adrelanos commented on Oct 19, 2025

    @adrelanos
    Contributor

    /usr/lib/sysctl.d/30_security-misc_ptrace-disable.conf

    Should be all done.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions