From: "Günther Noack" <gnoack3000@gmail.com>
To: "Mickaël Salaün" <mic@digikod.net>
Cc: "Christian Brauner" <brauner@kernel.org>,
"Günther Noack" <gnoack@google.com>,
"Paul Moore" <paul@paul-moore.com>,
"Serge E . Hallyn" <serge@hallyn.com>,
"Justin Suess" <utilityemal77@gmail.com>,
"Lennart Poettering" <lennart@poettering.net>,
"Mikhail Ivanov" <ivanov.mikhail1@huawei-partners.com>,
"Nicolas Bouchinet" <nicolas.bouchinet@oss.cyber.gouv.fr>,
"Shervin Oloumi" <enlightened@google.com>,
"Tingmao Wang" <m@maowtm.org>,
kernel-team@cloudflare.com, linux-fsdevel@vger.kernel.org,
linux-kernel@vger.kernel.org,
linux-security-module@vger.kernel.org
Subject: Re: [RFC PATCH v1 10/11] samples/landlock: Add capability and namespace restriction support
Date: Wed, 22 Apr 2026 23:20:45 +0200 [thread overview]
Message-ID: <20260422.cd00ad04e709@gnoack.org> (raw)
In-Reply-To: <20260312100444.2609563-11-mic@digikod.net>
On Thu, Mar 12, 2026 at 11:04:43AM +0100, Mickaël Salaün wrote:
> Extend the sandboxer sample to demonstrate the new Landlock capability
> and namespace restriction features. The LL_CAPS environment variable
> takes a colon-delimited list of allowed capability numbers (e.g. "18"
> for CAP_SYS_CHROOT). The LL_NS variable takes a colon-delimited list of
> allowed namespace types by short name (e.g. "user:uts:net"). Update
> LANDLOCK_ABI_LAST to 9 and add best-effort degradation for older
> kernels.
>
> Allow creating user and UTS namespaces but deny network namespaces
> (works as an unprivileged user). All capabilities are available
> (LL_CAPS is not set), but namespace creation is still restricted to the
> types listed in LL_NS. The first command succeeds because user and UTS
> types are in the allowed set, and sets the hostname inside the new UTS
> namespace. The second command fails because the network namespace type
> is not allowed by the LANDLOCK_PERM_NAMESPACE_ENTER rule:
>
> LL_FS_RO=/ LL_FS_RW=/proc LL_NS="user:uts" \
> ./sandboxer /bin/sh -c \
> "unshare --user --uts --map-root-user hostname sandbox \
> && ! unshare --user --net true"
>
> Allow only user namespace creation and CAP_SYS_CHROOT (18), denying all
> other capabilities and namespace types (works as an unprivileged user).
> An unprivileged process creates a user namespace (no capability
> required) and calls chroot inside it using the CAP_SYS_CHROOT granted
> within the new namespace:
>
> LL_FS_RO=/ LL_FS_RW="" LL_NS="user" LL_CAPS="18" \
> ./sandboxer /bin/sh -c \
> "unshare --user --keep-caps chroot / true"
>
> Cc: Christian Brauner <brauner@kernel.org>
> Cc: Günther Noack <gnoack@google.com>
> Cc: Paul Moore <paul@paul-moore.com>
> Cc: Serge E. Hallyn <serge@hallyn.com>
> Signed-off-by: Mickaël Salaün <mic@digikod.net>
> ---
> samples/landlock/sandboxer.c | 164 +++++++++++++++++++++++++++++++++--
> 1 file changed, 155 insertions(+), 9 deletions(-)
>
> diff --git a/samples/landlock/sandboxer.c b/samples/landlock/sandboxer.c
> index 9f21088c0855..09c499703835 100644
> --- a/samples/landlock/sandboxer.c
> +++ b/samples/landlock/sandboxer.c
> @@ -14,6 +14,8 @@
> #include <fcntl.h>
> #include <linux/landlock.h>
> #include <linux/socket.h>
> +#include <sched.h>
> +#include <stdbool.h>
> #include <stddef.h>
> #include <stdio.h>
> #include <stdlib.h>
> @@ -22,12 +24,16 @@
> #include <sys/stat.h>
> #include <sys/syscall.h>
> #include <unistd.h>
> -#include <stdbool.h>
>
> #if defined(__GLIBC__)
> #include <linux/prctl.h>
> #endif
>
> +/* From include/linux/bits.h, not available in userspace. */
> +#ifndef BITS_PER_TYPE
> +#define BITS_PER_TYPE(type) (sizeof(type) * 8)
> +#endif
> +
> #ifndef landlock_create_ruleset
> static inline int
> landlock_create_ruleset(const struct landlock_ruleset_attr *const attr,
> @@ -60,6 +66,8 @@ static inline int landlock_restrict_self(const int ruleset_fd,
> #define ENV_FS_RW_NAME "LL_FS_RW"
> #define ENV_TCP_BIND_NAME "LL_TCP_BIND"
> #define ENV_TCP_CONNECT_NAME "LL_TCP_CONNECT"
> +#define ENV_CAPS_NAME "LL_CAPS"
> +#define ENV_NS_NAME "LL_NS"
> #define ENV_SCOPED_NAME "LL_SCOPED"
> #define ENV_FORCE_LOG_NAME "LL_FORCE_LOG"
> #define ENV_DELIMITER ":"
> @@ -226,11 +234,125 @@ static int populate_ruleset_net(const char *const env_var, const int ruleset_fd,
> return ret;
> }
>
> +static __u64 str2ns(const char *const name)
> +{
> + static const struct {
> + const char *name;
> + __u64 value;
> + } ns_map[] = {
> + /* clang-format off */
> + { "cgroup", CLONE_NEWCGROUP },
> + { "ipc", CLONE_NEWIPC },
> + { "mnt", CLONE_NEWNS },
> + { "net", CLONE_NEWNET },
> + { "pid", CLONE_NEWPID },
> + { "time", CLONE_NEWTIME },
> + { "user", CLONE_NEWUSER },
> + { "uts", CLONE_NEWUTS },
> + /* clang-format on */
> + };
> + size_t i;
> +
> + for (i = 0; i < sizeof(ns_map) / sizeof(ns_map[0]); i++) {
> + if (strcmp(name, ns_map[i].name) == 0)
> + return ns_map[i].value;
> + }
> + return 0;
> +}
> +
> +static int populate_ruleset_caps(const char *const env_var,
> + const int ruleset_fd)
> +{
> + int ret = 1;
> + char *env_cap_name, *env_cap_name_next, *strcap;
> + struct landlock_capability_attr cap_attr = {
> + .allowed_perm = LANDLOCK_PERM_CAPABILITY_USE,
> + };
> +
> + env_cap_name = getenv(env_var);
> + if (!env_cap_name)
> + return 0;
> + env_cap_name = strdup(env_cap_name);
> + unsetenv(env_var);
> +
> + env_cap_name_next = env_cap_name;
> + while ((strcap = strsep(&env_cap_name_next, ENV_DELIMITER))) {
> + __u64 cap;
> +
> + if (strcmp(strcap, "") == 0)
> + continue;
> +
> + if (str2num(strcap, &cap) ||
libcap has cap_from_name(3). I believe we are linking with libcap
already to drop them before tests. (I have not used this function
myself yet, but it sounds like it would address this case.)
> + cap >= BITS_PER_TYPE(cap_attr.capabilities)) {
> + fprintf(stderr,
> + "Failed to parse capability at \"%s\"\n",
> + strcap);
> + goto out_free_name;
> + }
> + cap_attr.capabilities = 1ULL << cap;
> + if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_CAPABILITY,
> + &cap_attr, 0)) {
> + fprintf(stderr,
> + "Failed to update the ruleset with capability \"%llu\": %s\n",
> + (unsigned long long)cap, strerror(errno));
> + goto out_free_name;
> + }
> + }
> + ret = 0;
> +
> +out_free_name:
> + free(env_cap_name);
> + return ret;
> +}
> +
> +static int populate_ruleset_ns(const char *const env_var, const int ruleset_fd)
> +{
> + int ret = 1;
> + char *env_ns_name, *env_ns_name_next, *strns;
> + struct landlock_namespace_attr ns_attr = {
> + .allowed_perm = LANDLOCK_PERM_NAMESPACE_ENTER,
> + };
> +
> + env_ns_name = getenv(env_var);
> + if (!env_ns_name)
> + return 0;
> + env_ns_name = strdup(env_ns_name);
> + unsetenv(env_var);
> +
> + env_ns_name_next = env_ns_name;
> + while ((strns = strsep(&env_ns_name_next, ENV_DELIMITER))) {
> + __u64 ns_type;
> +
> + if (strcmp(strns, "") == 0)
> + continue;
> +
> + ns_type = str2ns(strns);
> + if (!ns_type) {
> + fprintf(stderr, "Unknown namespace type \"%s\"\n",
> + strns);
> + goto out_free_name;
> + }
> + ns_attr.namespace_types = ns_type;
> + if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NAMESPACE,
> + &ns_attr, 0)) {
> + fprintf(stderr,
> + "Failed to update the ruleset with namespace \"%s\": %s\n",
> + strns, strerror(errno));
> + goto out_free_name;
> + }
> + }
> + ret = 0;
> +
> +out_free_name:
> + free(env_ns_name);
> + return ret;
> +}
> +
> /* Returns true on error, false otherwise. */
> static bool check_ruleset_scope(const char *const env_var,
> struct landlock_ruleset_attr *ruleset_attr)
> {
> - char *env_type_scope, *env_type_scope_next, *ipc_scoping_name;
> + char *env_type_scope, *env_type_scope_next, *scope_name;
> bool error = false;
> bool abstract_scoping = false;
> bool signal_scoping = false;
> @@ -247,16 +369,14 @@ static bool check_ruleset_scope(const char *const env_var,
>
> env_type_scope = strdup(env_type_scope);
> env_type_scope_next = env_type_scope;
> - while ((ipc_scoping_name =
> - strsep(&env_type_scope_next, ENV_DELIMITER))) {
> - if (strcmp("a", ipc_scoping_name) == 0 && !abstract_scoping) {
> + while ((scope_name = strsep(&env_type_scope_next, ENV_DELIMITER))) {
> + if (strcmp("a", scope_name) == 0 && !abstract_scoping) {
> abstract_scoping = true;
> - } else if (strcmp("s", ipc_scoping_name) == 0 &&
> - !signal_scoping) {
> + } else if (strcmp("s", scope_name) == 0 && !signal_scoping) {
> signal_scoping = true;
> } else {
> fprintf(stderr, "Unknown or duplicate scope \"%s\"\n",
> - ipc_scoping_name);
> + scope_name);
> error = true;
> goto out_free_name;
> }
> @@ -299,7 +419,7 @@ static bool check_ruleset_scope(const char *const env_var,
>
> /* clang-format on */
>
> -#define LANDLOCK_ABI_LAST 8
> +#define LANDLOCK_ABI_LAST 9
>
> #define XSTR(s) #s
> #define STR(s) XSTR(s)
> @@ -322,6 +442,10 @@ static const char help[] =
> "means an empty list):\n"
> "* " ENV_TCP_BIND_NAME ": ports allowed to bind (server)\n"
> "* " ENV_TCP_CONNECT_NAME ": ports allowed to connect (client)\n"
> + "* " ENV_CAPS_NAME ": capability numbers allowed to use "
> + "(e.g. 10 for CAP_NET_BIND_SERVICE, 21 for CAP_SYS_ADMIN)\n"
> + "* " ENV_NS_NAME ": namespace types allowed to enter "
> + "(cgroup, ipc, mnt, net, pid, time, user, uts)\n"
> "* " ENV_SCOPED_NAME ": actions denied on the outside of the landlock domain\n"
> " - \"a\" to restrict opening abstract unix sockets\n"
> " - \"s\" to restrict sending signals\n"
> @@ -334,6 +458,8 @@ static const char help[] =
> ENV_FS_RW_NAME "=\"/dev/null:/dev/full:/dev/zero:/dev/pts:/tmp\" "
> ENV_TCP_BIND_NAME "=\"9418\" "
> ENV_TCP_CONNECT_NAME "=\"80:443\" "
> + ENV_CAPS_NAME "=\"21\" "
> + ENV_NS_NAME "=\"user:uts:net\" "
> ENV_SCOPED_NAME "=\"a:s\" "
> "%1$s bash -i\n"
> "\n"
> @@ -357,6 +483,8 @@ int main(const int argc, char *const argv[], char *const *const envp)
> LANDLOCK_ACCESS_NET_CONNECT_TCP,
> .scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
> LANDLOCK_SCOPE_SIGNAL,
> + .handled_perm = LANDLOCK_PERM_CAPABILITY_USE |
> + LANDLOCK_PERM_NAMESPACE_ENTER,
> };
> int supported_restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON;
> int set_restrict_flags = 0;
> @@ -438,6 +566,10 @@ int main(const int argc, char *const argv[], char *const *const envp)
> ~LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON;
> __attribute__((fallthrough));
> case 7:
> + __attribute__((fallthrough));
> + case 8:
> + /* Removes permission support for ABI < 9 */
> + ruleset_attr.handled_perm = 0;
> /* Must be printed for any ABI < LANDLOCK_ABI_LAST. */
> fprintf(stderr,
> "Hint: You should update the running kernel "
> @@ -470,6 +602,14 @@ int main(const int argc, char *const argv[], char *const *const envp)
> ~LANDLOCK_ACCESS_NET_CONNECT_TCP;
> }
>
> + /* Removes capability handling if not set by a user. */
> + if (!getenv(ENV_CAPS_NAME))
> + ruleset_attr.handled_perm &= ~LANDLOCK_PERM_CAPABILITY_USE;
> +
> + /* Removes namespace handling if not set by a user. */
> + if (!getenv(ENV_NS_NAME))
> + ruleset_attr.handled_perm &= ~LANDLOCK_PERM_NAMESPACE_ENTER;
> +
> if (check_ruleset_scope(ENV_SCOPED_NAME, &ruleset_attr))
> return 1;
>
> @@ -514,6 +654,12 @@ int main(const int argc, char *const argv[], char *const *const envp)
> goto err_close_ruleset;
> }
>
> + if (populate_ruleset_caps(ENV_CAPS_NAME, ruleset_fd))
> + goto err_close_ruleset;
> +
> + if (populate_ruleset_ns(ENV_NS_NAME, ruleset_fd))
> + goto err_close_ruleset;
> +
> if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
> perror("Failed to restrict privileges");
> goto err_close_ruleset;
> --
> 2.53.0
>
next prev parent reply other threads:[~2026-04-22 21:20 UTC|newest]
Thread overview: 51+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-03-12 10:04 [RFC PATCH v1 00/11] Landlock: Namespace and capability control Mickaël Salaün
2026-03-12 10:04 ` [RFC PATCH v1 01/11] security: add LSM blob and hooks for namespaces Mickaël Salaün
2026-03-25 12:31 ` Christian Brauner
2026-04-09 16:40 ` Mickaël Salaün
2026-04-10 9:35 ` Christian Brauner
2026-04-22 21:21 ` Günther Noack
2026-04-23 0:19 ` Paul Moore
2026-04-24 18:56 ` Mickaël Salaün
2026-04-24 19:28 ` Paul Moore
2026-04-27 14:57 ` Christian Brauner
2026-04-27 21:46 ` Paul Moore
2026-03-12 10:04 ` [RFC PATCH v1 02/11] security: Add LSM_AUDIT_DATA_NS for namespace audit records Mickaël Salaün
2026-03-25 12:32 ` Christian Brauner
2026-04-01 16:38 ` Mickaël Salaün
2026-04-01 18:48 ` Mickaël Salaün
2026-04-09 13:29 ` Christian Brauner
2026-04-22 21:21 ` Günther Noack
2026-03-12 10:04 ` [RFC PATCH v1 03/11] nsproxy: Add FOR_EACH_NS_TYPE() X-macro and CLONE_NS_ALL Mickaël Salaün
2026-03-25 12:33 ` Christian Brauner
2026-03-25 15:26 ` Mickaël Salaün
2026-03-26 14:22 ` (subset) " Christian Brauner
2026-03-12 10:04 ` [RFC PATCH v1 04/11] landlock: Wrap per-layer access masks in struct layer_rights Mickaël Salaün
2026-04-10 1:45 ` Tingmao Wang
2026-04-22 21:29 ` Günther Noack
2026-03-12 10:04 ` [RFC PATCH v1 05/11] landlock: Enforce namespace entry restrictions Mickaël Salaün
2026-03-29 13:15 ` kernel test robot
2026-04-10 1:45 ` Tingmao Wang
2026-05-08 15:46 ` Günther Noack
2026-03-12 10:04 ` [RFC PATCH v1 06/11] landlock: Enforce capability restrictions Mickaël Salaün
2026-04-22 21:36 ` Günther Noack
2026-05-08 15:54 ` Günther Noack
2026-03-12 10:04 ` [RFC PATCH v1 07/11] selftests/landlock: Drain stale audit records on init Mickaël Salaün
2026-03-24 13:27 ` Günther Noack
2026-03-12 10:04 ` [RFC PATCH v1 08/11] selftests/landlock: Add namespace restriction tests Mickaël Salaün
2026-03-12 10:04 ` [RFC PATCH v1 09/11] selftests/landlock: Add capability " Mickaël Salaün
2026-03-12 10:04 ` [RFC PATCH v1 10/11] samples/landlock: Add capability and namespace restriction support Mickaël Salaün
2026-04-22 21:20 ` Günther Noack [this message]
2026-04-23 13:51 ` Mickaël Salaün
2026-03-12 10:04 ` [RFC PATCH v1 11/11] landlock: Add documentation for capability and namespace restrictions Mickaël Salaün
2026-03-12 14:48 ` Justin Suess
2026-04-23 13:51 ` Mickaël Salaün
2026-04-23 16:01 ` Justin Suess
2026-04-23 16:08 ` Justin Suess
2026-04-22 20:38 ` Günther Noack
2026-04-23 13:52 ` Mickaël Salaün
2026-05-08 15:13 ` Günther Noack
2026-03-25 12:34 ` [RFC PATCH v1 00/11] Landlock: Namespace and capability control Christian Brauner
2026-04-20 15:06 ` Günther Noack
2026-04-21 8:24 ` Mickaël Salaün
2026-04-22 21:16 ` Günther Noack
2026-04-23 13:50 ` Mickaël Salaün
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260422.cd00ad04e709@gnoack.org \
--to=gnoack3000@gmail.com \
--cc=brauner@kernel.org \
--cc=enlightened@google.com \
--cc=gnoack@google.com \
--cc=ivanov.mikhail1@huawei-partners.com \
--cc=kernel-team@cloudflare.com \
--cc=lennart@poettering.net \
--cc=linux-fsdevel@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-security-module@vger.kernel.org \
--cc=m@maowtm.org \
--cc=mic@digikod.net \
--cc=nicolas.bouchinet@oss.cyber.gouv.fr \
--cc=paul@paul-moore.com \
--cc=serge@hallyn.com \
--cc=utilityemal77@gmail.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.