On 18/10/2026 20:58, Collin Funk wrote:
Hi,Various Operating Systems provide interfaces for unprivileged processes to declare what capabilities they require and prevent any other capabilities from subsequently being used. If they try to use these capabilities, they will either fail or cause the program to be killed. This means that if even the process is hijacked, e.g., by a crafted input file that is used to exploit a buffer overflow, the resulting process is limited in what it can do, depending, of course, on how limited the hijacked commands' required capabilities are. There are three APIs that I think GNU coreutils could use, and I am interested in trying them. Landlock on GNU/Linux, pledge(2) and unveil(2) on OpenBSD, and Capsicum on FreeBSD. Before further comments, I would like to say that all of my comments are based on my very minimal experience using them. I certainly welcome corrections from anyone with more experience. Also, when discussing pros and cons, I do not mean to imply that any implementation is necessarily "better" or "worse" than the other. This area of API design is a bit tricky, and there are rationales to explain why they are implemented in the way that they are. For further explinations, it may be helpful to also view a series of patches that I wrote to use Landlock or unveil(2) and pledge(2): https://github.com/coreutils/coreutils/compare/master...collinfunk:coreutils:sandbox Note that this is missing Capsicum support. This is not due to any fault of that interface. I simply implemented support for Landlock and pledge(2) first, and began thinking about how an improved API could be designed, given some of the trickiness I experienced. This also means my experience with Capsicum is next to zero, and should be taken with a large grain of salt. The first interface is Landlock, supported by GNU/Linux [1] [2]. To use this API, you first create a ruleset using attributes declaring which capabilities are implemented by the ruleset. These capabilities depend on which ABI version the running kernel supports. This is performed using the landlock_create_ruleset function. Then, you add rules to the ruleset using the landlock_add_rule function, which allows the use of defined capabilities in certain contexts. In my patch, I use a "struct landlock_path_beneath_attr" as an argument to this function to allow reading from "/usr" and 'LOCALEDIR' if it does not already start with /usr. This is because nothing in /usr seems particularly sensitive, and guessing where libc implementations load locale files and translations from feels like a losing battle. This API feels very versitile; e.g., you could heavily restrict 'mkfifo' to be unable to do anything but create FIFOs by using "struct landlock_path_beneath_attr" with an allowed_access of LANDLOCK_ACCESS_FS_MAKE_FIFO and parent_fd set to a file descriptor to "/". This can also make it a bit tricky to use. The second interface is pledge(2) [3] and unveil(2) [4]. This, compared to Landlock, feels much simpler to use. E.g., many programs could likely be fine just using the following: if (pledge ("stdio rpath", NULL) < 0) abort (); This would allow them to read files and use standard file descriptors (read or write). If a program does not read files and simply uses standard file descriptors, an unveil(2) call(s) must be used first to allow reading locale and translation files. After the first pledge call without an "unveil" string in the argument, a subsequent call to unveil(2) is not permitted; hence, it is important to do it first. I have done this to allow reading from "/usr" and 'LOCALEDIR', similarly to Landlock. While easier to use, this API is less customizable than Landlock. E.g., trying to place 'uname' in a sandbox will cause the program to die when using --all or --all-labeled, since no permissions give you access to the HW_MODEL sysctl. Lastly, there is Capsicum [5], which, as I said, I do not have any experience with. At first glance, this one seems a bit tricky to use in GNU coreutils. It operates on file descriptors using the cap_rights_limit function [6]. Assuming that you have more file arguments than your file descriptor limit, I am not sure how you could use this one. Perhaps that issue is solvable with a bit more research. Some issues that came up, or that I considered: The Landlock code, in particular, uses a lot of system calls. I was concerned about startup time, but that doesn't seem to be an issue, at least as it stands currently. See the below example: $ taskset 1 perf stat --repeat 100000 ./src/echo-sandbox >/dev/null [...] 0.000924494 +- 0.000000325 seconds time elapsed ( +- 0.04% ) $ taskset 1 perf stat --repeat 100000 ./src/echo >/dev/null [...] 0.000924517 +- 0.000000318 seconds time elapsed ( +- 0.03% ) You could try to load locale and translation files by moving the sandbox setup after setlocale and making a bogus gettext call. This felt a bit hacky to me, and in many cases the translations are not needed, so there is no point in loading them. If we were to use dlopen, using the method that Luca suggested, the sandbox would obviously complicate things. Handling two, let alone three, different sandboxing mechanisms while using the most restrictive sandbox possible on each platform is a pain in the ass, to be quite honest. This is the reason I began reconsidering the API before implementing Capsicum support. To resolve this issue, I am thinking of providing the Landlock flags on OpenBSD and FreeBSD. At least on OpenBSD, it seems quite simple to translate Landlock flags and the "struct landlock_path_beneath_attr" information into the relevant permissions for pledge and/or unveil. I was thinking programs could provide an array of structures like this: struct landlock_path_beneath_attr_request { /* Path to operate on. */ char const *path;/* If true, an open error will cause the program to exit. */bool required;/* Allowed acess flags. */unsigned long long int allowed_access; } Then, translate them into the platform-specific interfaces. But that is mostly just an initial thought. I figured it was worth sending what I have now to get others' opinions on whether this is even worth the effort before I spend too much time on it. Also, many thanks to Lasse Collin, who I discussed this while considering and then implementing this, because of his work adding sandboxing to xz. He did not review this email or my code, so he is not to blame for any mistruths I said or any of the sloppy code I wrote. :) Collin [1] https://docs.kernel.org/userspace-api/landlock.html [2] https://man7.org/linux/man-pages/man7/landlock.7.html [3] https://man.openbsd.org/pledge.2 [4] https://man.openbsd.org/unveil.2 [5] https://man.freebsd.org/cgi/man.cgi?capsicum [6] https://man.freebsd.org/cgi/man.cgi?cap_rights_limit [7] https://github.com/coreutils/coreutils/pull/317
First of all thanks for looking into this. It does look useful. Defense in depth and all that. From a high level it seems useful to mark each utility with the privileges it requires, but we need to be quite conservative I think. Users can externally compose extra restrictions on top after all. E.g. restricting exec() and network access seems useful to many utilities. (Though I see landlock doesn't currently restrict UDP access). Also since reduced privileges inherit, you couldn't apply any to `split --filter` for example, since that should be allowed to run anything. Even ls (and others) need access to the net for NSS dependencies. Then cksum openssl dependencies have extensive system interactions, which would limit what restrictions they might impose. Then there are setups like fakeroot, which intercepts calls redirecting them (over net/ipc) to fakerootd, so a landlock setup that restricted network would break that. Also user space auditing solutions come to mind as something that might be negatively restricted. In summary the generality of coreutils usage works against being able to impose restrictions. It needs to be very carefully considered at least. thanks! Padraig
