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


Reply via email to