Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions bubblewrap.c
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,15 @@
#include <sys/signalfd.h>
#include <sys/capability.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <linux/sched.h>
#include <linux/seccomp.h>
#include <linux/filter.h>

#ifdef HAVE_LANDLOCK_H
#include <linux/landlock.h>
#endif

#include "utils.h"
#include "network.h"
#include "bind-mount.h"
Expand Down Expand Up @@ -92,6 +97,8 @@ static int opt_userns_fd = -1;
static int opt_userns2_fd = -1;
static int opt_pidns_fd = -1;
static int opt_tmp_overlay_count = 0;
static bool opt_scope_abstract_unix_sockets = false;
static bool opt_scope_abstract_unix_sockets_try = false;
static int next_perms = -1;
static size_t next_size_arg = 0;
static int next_overlay_src_count = 0;
Expand Down Expand Up @@ -373,6 +380,8 @@ usage (int ecode, FILE *out)
" --perms OCTAL Set permissions of next argument (--bind-data, --file, etc.)\n"
" --size BYTES Set size of next argument (only for --tmpfs)\n"
" --chmod OCTAL PATH Change permissions of PATH (must already exist)\n"
" --scope-abstract-af-unix Prevent connecting to abstract unix sockets outside the sandbox\n"
" --scope-abstract-af-unix-try Try --scope-abstract-af-unix if possible else continue by skipping it\n"
);
exit (ecode);
}
Expand Down Expand Up @@ -2736,6 +2745,14 @@ parse_args_recurse (int *argcp,
argv += 2;
argc -= 2;
}
else if (strcmp (arg, "--scope-abstract-af-unix") == 0)
{
opt_scope_abstract_unix_sockets = true;
}
else if (strcmp (arg, "--scope-abstract-af-unix-try") == 0)
{
opt_scope_abstract_unix_sockets_try = true;
}
else if (strcmp (arg, "--") == 0)
{
argv += 1;
Expand Down Expand Up @@ -2867,6 +2884,26 @@ namespace_ids_write (int fd,
}
}

#ifdef HAVE_LANDLOCK_H
#ifndef landlock_create_ruleset
static inline int
landlock_create_ruleset (const struct landlock_ruleset_attr *attr,
size_t size,
uint32_t flags)
{
return syscall (SYS_landlock_create_ruleset, attr, size, flags);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might need some adjustment if there are glibc/kernel combinations where <landlock.h> exists but SYS_landlock_create_ruleset is undefined, but hopefully those don't actually exist in practice. I'll try it in a very old container.

}
#endif

#ifndef landlock_restrict_self
static inline int
landlock_restrict_self (int ruleset_fd, uint32_t flags)
{
return syscall (SYS_landlock_restrict_self, ruleset_fd, flags);
}
#endif
#endif

int
main (int argc,
char **argv)
Expand Down Expand Up @@ -3498,6 +3535,43 @@ main (int argc,
die ("creation of new user namespaces was not disabled as requested");
}

if (opt_scope_abstract_unix_sockets)
{
#ifdef HAVE_LANDLOCK_H
static const struct landlock_ruleset_attr ruleset_attr = {
.scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET
};
const int abi = landlock_create_ruleset (NULL, 0, LANDLOCK_CREATE_RULESET_VERSION);
if (abi < 0)
die_with_error ("failed to check Landlock compatibility");
if (abi < 6)
die ("supported kernel Landlock ABI too old, version 6 or above required");
const int ruleset_fd = landlock_create_ruleset (&ruleset_attr, sizeof (ruleset_attr), 0);
if (ruleset_fd < 0)
die_with_error ("failed to create Landlock ruleset");
if (landlock_restrict_self (ruleset_fd, 0) < 0)
die_with_error ("failed to enforce Landlock ruleset");
#else
die ("Landlock not available at compile time, cannot implement --scope-abstract-af-unix");
#endif
}

#ifdef HAVE_LANDLOCK_H
if (opt_scope_abstract_unix_sockets_try)
{
static const struct landlock_ruleset_attr ruleset_attr = {
.scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET
};
const int abi = landlock_create_ruleset (NULL, 0, LANDLOCK_CREATE_RULESET_VERSION);
if (abi < 6)
{
const int ruleset_fd = landlock_create_ruleset (&ruleset_attr, sizeof (ruleset_attr), 0);
if (ruleset_fd < 0)
landlock_restrict_self (ruleset_fd, 0);
}
}
Copy link
Collaborator

@smcv smcv Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This duplicates the logic for actually applying the restriction between the fail-open and fail-closed branches, which is unfortunate.
I'd prefer to have a function that is always defined, and either applies the restriction, or returns failure with errno and maybe a human-readable message. Something like this:

static bool
scope_abstract_unix_sockets(const char **message_out)
{
#ifdef HAVE_LANDLOCK_H
  if (!(... do the first step))
    {
      *message_out = "failed to check Landlock compatibility";
      return false;
    }

  ... do the remaining steps, similar error handling each time ...

  return true;
#else
  errno = ENOSYS;
  *message_out = "bubblewrap compiled without landlock support";
  return false;
#error
}

In the places that already set errno, you don't need to set it again. In places that fail but don't set errno (I think this is just the if (abi < 6) check right now), you would have to choose some suitable value for errno and set that:

if (abi < 6)
  {
    errno = ENOSYS;
    *message_out = "supported kernel Landlock ABI too old, version 6 or above required";
    return false;
  }

"Function not implemented" seems like a reasonable least-bad representation for "your kernel is too old" (it's also the error code we would get from these syscalls if glibc knows about Landlock but the running kernel does not), but I could also see an argument for using ENOTSUP or EINVAL or even ENOPKG - I'm open to suggestions!

And then its caller can look more like this (untested):

  if (opt_scope_abstract_unix_sockets || opt_scope_abstract_unix_sockets_try)
    {
      const char *message = NULL;

      if (!scope_abstract_unix_sockets (&message))
        {
          if (opt_scope_abstract_unix_sockets)
            die_with_error (message);
          else
            debug ("%s: %s", message, strerror (errno));
        }
    }

(Or the function could return NULL on success and a non-NULL error message on failure, but I think a boolean result is more obvious, even if strictly speaking it's redundant.)

Unfortunately bubblewrap doesn't depend on any useful libraries like GLib or libdbus (by policy, because it's sometimes setuid root, so every dependency adds attack surface), so we have to reinvent error-reporting.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's worth keeping in mind how this function could be structured so that if we use more Landlock features in future, it can cope with that. For instance we might eventually want to change it from

static bool scope_abstract_unix_sockets(const char **message_out)

to something more like

static bool apply_landlock_restrictions(bool scope_abstract_unix_sockets,
                                        bool apply_some_other_restriction,
                                        ...,
                                        const char **message_out)

But we can cross that bridge when we come to it.

#endif

/* All privileged ops are done now, so drop caps we don't need */
drop_privs (!is_privileged, true);

Expand Down
21 changes: 21 additions & 0 deletions bwrap.xml
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,27 @@
command line. Please be careful to the order they are specified.
</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--scope-abstract-af-unix</option></term>
<listitem><para>
Scope access to abstract <citerefentry><refentrytitle>unix</refentrytitle><manvolnum>7</manvolnum></citerefentry> sockets.

This option will prevent the newly created sandbox from connecting to abstract AF_UNIX sockets
created outside the sandbox,
for example the X11 socket <literal>@/tmp/.X11-unix/X0</literal>,
even if the network namespace is the same.

This has the same behaviour as LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET: see
<citerefentry><refentrytitle>landlock</refentrytitle><manvolnum>7</manvolnum></citerefentry> for details.
</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--scope-abstract-af-unix-try</option></term>
<listitem><para>
Try to do the same as <option>--scope-abstract-af-unix</option>,
but if that isn't possible, continue without that restriction.
</para></listitem>
</varlistentry>
</variablelist>
</refsect1>

Expand Down
2 changes: 2 additions & 0 deletions completions/bash/bwrap
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ _bwrap() {
--disable-userns
--help
--new-session
--scope-abstract-af-unix
--scope-abstract-af-unix-try
--unshare-all
--unshare-cgroup
--unshare-cgroup-try
Expand Down
2 changes: 2 additions & 0 deletions completions/zsh/_bwrap
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ _bwrap_args=(
'--remount-ro[Remount DEST as readonly; does not recursively remount]:mount point to remount read-only:_files'
'--ro-bind-try[Equal to --ro-bind but ignores non-existent SRC]:source:_files:destination:_files'
'--ro-bind[Bind mount the host path SRC readonly on DEST]:source:_files:destination:_files'
'--scope-abstract-af-unix[Prevent connecting to abstract unix sockets outside the sandbox]'
'--scope-abstract-af-unix-try[Try --scope-abstract-af-unix if possible else continue by skipping it]'
'--seccomp[Load and use seccomp rules from FD]: :_guard "[0-9]#" "file descriptor to read seccomp rules from"'
'--setenv[Set an environment variable]:variable to set:_parameters -g "*export*":value of variable: :'
'--size[Set size in bytes for next action argument]: :->after_size'
Expand Down
4 changes: 4 additions & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ if (
], language : 'c')
endif

if cc.check_header('linux/landlock.h')
add_project_arguments('-DHAVE_LANDLOCK_H', language : 'c')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When using Autotools-style HAVE_FOO macros, let's make them consistent with Autotools' systematic naming: the macro for whether we have linux/landlock.h should be named HAVE_LINUX_LANDLOCK_H.

Also please move this below the creation of cdata, so that it can use cdata.set('HAVE_LINUX_LANDLOCK_H', 1) to put it in config.h instead of expanding the compiler command-line.

Many projects do something like this (adapted from https://gitlab.freedesktop.org/dbus/dbus/-/blob/main/meson.build?ref_type=heads, untested):

# Copyright 2019-2020 Salamandar
# Copyright 2022-2026 Collabora Ltd.
# SPDX-License-Identifier: MIT

check_headers = [
    'linux/landlock.h',
]

foreach header : check_headers
    macro = 'HAVE_' + header.underscorify().to_upper()
    cdata.set(macro, cc.check_header(header) ? 1 : false)
endforeach

I agree that it probably it isn't worth doing that yet, but let's choose our naming so that it would be possible to do that in future.

endif

bash = find_program('bash', required : false)

if get_option('python') == ''
Expand Down