From: "Mickaël Salaün" <mic@digikod.net>
To: "Christian Brauner" <brauner@kernel.org>,
"Günther Noack" <gnoack@google.com>,
"Paul Moore" <paul@paul-moore.com>,
"Serge E . Hallyn" <serge@hallyn.com>
Cc: "Mickaël Salaün" <mic@digikod.net>,
"Daniel Durning" <danieldurning.work@gmail.com>,
"Jonathan Corbet" <corbet@lwn.net>,
"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: [PATCH v4 7/8] samples/landlock: Add capability and namespace restriction support
Date: Fri, 2 Oct 2026 14:44:00 +0200 [thread overview]
Message-ID: <20261002124409.1277970-8-mic@digikod.net> (raw)
In-Reply-To: <20261002124409.1277970-1-mic@digikod.net>
Extend the sandboxer sample to demonstrate the new Landlock capability
and namespace restriction features. LL_CAP takes a colon-delimited list
of allowed capabilities, parsed with cap_from_name(3) from libcap so
names and numeric strings are both accepted. LL_NS takes a
colon-delimited list of allowed namespace types by short name. Add
best-effort degradation for older kernels that predate the
LANDLOCK_PERMISSION_* features.
Allow creating user and UTS namespaces but deny network namespaces, as
an unprivileged user. The first command succeeds and sets the hostname
inside the new UTS namespace; the second is denied because the network
namespace type is not allowed:
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, denying all other
capabilities and namespace types. An unprivileged process creates a
user namespace, which requires no capability, and calls chroot inside it
using the CAP_SYS_CHROOT granted within that namespace:
LL_FS_RO=/ LL_FS_RW="" LL_NS="user" LL_CAP="cap_sys_chroot" \
./sandboxer /bin/sh -c \
"unshare --user --keep-caps chroot / true"
Allow user namespace creation but deny network namespaces, and quiet the
network-namespace denials with LL_NS_QUIET so the denied creation is not
audit logged. The negated second command makes the whole line succeed:
LL_FS_RO=/ LL_FS_RW=/proc LL_NS="user" LL_NS_QUIET="net" \
./sandboxer /bin/sh -c \
"unshare --user --map-root-user true && ! unshare --user --net 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>
Cc: Tingmao Wang <m@maowtm.org>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---
Changes since v3:
https://patch.msgid.link/20260726161400.3010511-12-mic@digikod.net
- Bump the sample's latest supported ABI to 12.
- Gate namespace and capability permissions and rule calls on ABI 12.
Consume unsupported settings before executing the sandboxed command.
- Reject capability numbers that cannot fit in the UAPI's 64-bit mask.
- Expand the Kconfig help from filesystem-only wording to the complete
sandboxer policy and state the libcap development-file dependency.
- Clarify that quiet capability and namespace members suppress audit
logging, and that quieting alone still restricts the permission.
Changes since v2:
https://patch.msgid.link/20260527181127.879771-9-mic@digikod.net
- Rebased for ABI 11: bump LANDLOCK_ABI_LAST to 11 and extend the ABI
back-compat fall-through with a new case stripping the LANDLOCK_PERM_*
handled bits for ABI < 11.
- Adopt the renamed capability and namespace rule attributes: the rule
bodies use perm plus allowed_capabilities / allowed_namespace_types
(matching the per-member quiet restructuring in the enforcement
patches).
- Add LL_CAP_QUIET and LL_NS_QUIET to quiet capability and
namespace-type denials (per-member, merged into the allowed rule).
Changes since v1:
https://patch.msgid.link/20260312100444.2609563-11-mic@digikod.net
- Rename LANDLOCK_PERM_NAMESPACE_ENTER references to
LANDLOCK_PERM_NAMESPACE_USE (companion change to the introducing
commit).
- Replace handled_perm = 0 with a per-bit mask in the ABI compat
fall-through, mirroring the doc example so future ABI extensions
adding new LANDLOCK_PERM_* bits do not get stripped.
- Parse LL_CAP values with cap_from_name(3) from libcap so users
can pass capability names (e.g. "cap_sys_chroot") in addition to
numbers. cap_from_name accepts both: the canonical name lookup
is case-insensitive, and a numeric-string fallback maps "18" to
CAP_SYS_CHROOT identically to the previous numeric-only path.
Drop the BITS_PER_TYPE workaround and the manual numeric bound
check (cap_from_name does the right thing in both cases). Link
the sandboxer against libcap by adding userldlibs += -lcap in
samples/landlock/Makefile. Update help text and example command
to show capability names (suggested by Günther Noack).
- Rename the LL_CAPS env var to LL_CAP for consistency with the
singular form of all other sandboxer env vars (LL_NS, LL_FS_RO,
LL_FS_RW, LL_TCP_BIND, LL_TCP_CONNECT, LL_SCOPED, LL_FORCE_LOG).
Internal symbols renamed accordingly: ENV_CAPS_NAME -> ENV_CAP_NAME,
populate_ruleset_caps() -> populate_ruleset_cap().
- Tingmao Wang's v1 Reviewed-by is not carried forward to v2: the
cap_from_name() / libcap migration is a material implementation
change requested by Günther Noack that was not part of his
review. Cc'd instead.
---
samples/Kconfig | 6 +-
samples/landlock/Makefile | 1 +
samples/landlock/sandboxer.c | 230 ++++++++++++++++++++++++++++++++++-
3 files changed, 232 insertions(+), 5 deletions(-)
diff --git a/samples/Kconfig b/samples/Kconfig
index a75e8e78330d..b18efc19b85d 100644
--- a/samples/Kconfig
+++ b/samples/Kconfig
@@ -166,8 +166,10 @@ config SAMPLE_LANDLOCK
bool "Landlock example"
depends on CC_CAN_LINK && HEADERS_INSTALL
help
- Build a simple Landlock sandbox manager able to start a process
- restricted by a user-defined filesystem access control policy.
+ Build a Landlock sandbox manager able to start a process restricted
+ by user-defined filesystem, network, scope, namespace, and capability
+ policies. This sample requires the libcap development headers and
+ library.
config SAMPLE_PIDFD
bool "pidfd sample"
diff --git a/samples/landlock/Makefile b/samples/landlock/Makefile
index 5d601e51c2eb..b30239c8a281 100644
--- a/samples/landlock/Makefile
+++ b/samples/landlock/Makefile
@@ -3,6 +3,7 @@
userprogs-always-y := sandboxer
userccflags += -I usr/include
+userldlibs += -lcap
.PHONY: all clean
diff --git a/samples/landlock/sandboxer.c b/samples/landlock/sandboxer.c
index 030583273f3f..4a86ae6d4552 100644
--- a/samples/landlock/sandboxer.c
+++ b/samples/landlock/sandboxer.c
@@ -12,17 +12,20 @@
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
+#include <limits.h>
#include <linux/landlock.h>
#include <linux/socket.h>
+#include <sched.h>
+#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
+#include <sys/capability.h>
#include <sys/prctl.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <unistd.h>
-#include <stdbool.h>
#if defined(__GLIBC__)
#include <linux/prctl.h>
@@ -62,6 +65,10 @@ static inline int landlock_restrict_self(const int ruleset_fd,
#define ENV_TCP_BIND_NAME "LL_TCP_BIND"
#define ENV_TCP_CONNECT_NAME "LL_TCP_CONNECT"
#define ENV_NET_QUIET_NAME "LL_NET_QUIET"
+#define ENV_NS_NAME "LL_NS"
+#define ENV_NS_QUIET_NAME "LL_NS_QUIET"
+#define ENV_CAP_NAME "LL_CAP"
+#define ENV_CAP_QUIET_NAME "LL_CAP_QUIET"
#define ENV_SCOPED_NAME "LL_SCOPED"
#define ENV_QUIET_ACCESS_NAME "LL_QUIET_ACCESS"
#define ENV_FORCE_LOG_NAME "LL_FORCE_LOG"
@@ -69,6 +76,8 @@ static inline int landlock_restrict_self(const int ruleset_fd,
#define ENV_UDP_CONNECT_SEND_NAME "LL_UDP_CONNECT_SEND"
#define ENV_DELIMITER ":"
+#define ARRAY_SIZE(array) (sizeof(array) / sizeof((array)[0]))
+
static int str2num(const char *numstr, __u64 *num_dst)
{
char *endptr = NULL;
@@ -232,6 +241,166 @@ 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 < ARRAY_SIZE(ns_map); i++) {
+ if (strcmp(name, ns_map[i].name) == 0)
+ return ns_map[i].value;
+ }
+ return 0;
+}
+
+/*
+ * Parses a colon-delimited list of namespace type names into a bitmask.
+ * Returns 0 on success (mask 0 when the variable is unset or empty), or 1 on a
+ * parse error.
+ */
+static int parse_ns_list(const char *const env_var, __u64 *const mask)
+{
+ int ret = 1;
+ char *env_ns_name, *env_ns_name_next, *strns;
+
+ *mask = 0;
+ 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;
+ }
+ *mask |= ns_type;
+ }
+ ret = 0;
+
+out_free_name:
+ free(env_ns_name);
+ return ret;
+}
+
+static int populate_ruleset_ns(const char *const allowed_env,
+ const char *const quiet_env,
+ const int ruleset_fd)
+{
+ struct landlock_namespace_attr ns_attr = {
+ .permissions = LANDLOCK_PERMISSION_NAMESPACE_USE,
+ };
+
+ if (parse_ns_list(allowed_env, &ns_attr.allowed_namespace_types))
+ return 1;
+ if (parse_ns_list(quiet_env, &ns_attr.quiet_namespace_types))
+ return 1;
+
+ if (!ns_attr.allowed_namespace_types && !ns_attr.quiet_namespace_types)
+ return 0;
+
+ if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NAMESPACE, &ns_attr,
+ 0)) {
+ fprintf(stderr,
+ "Failed to update the ruleset with namespace types: %s\n",
+ strerror(errno));
+ return 1;
+ }
+ return 0;
+}
+
+/*
+ * Parses a colon-delimited list of capability names into a bitmask. Returns 0
+ * on success (mask 0 when the variable is unset or empty), or 1 on a parse
+ * error.
+ */
+static int parse_cap_list(const char *const env_var, __u64 *const mask)
+{
+ int ret = 1;
+ char *env_cap_name, *env_cap_name_next, *strcap;
+
+ *mask = 0;
+ 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))) {
+ cap_value_t cap;
+
+ if (strcmp(strcap, "") == 0)
+ continue;
+
+ if (cap_from_name(strcap, &cap)) {
+ fprintf(stderr, "Failed to parse capability \"%s\"\n",
+ strcap);
+ goto out_free_name;
+ }
+ if ((unsigned int)cap >= sizeof(*mask) * CHAR_BIT) {
+ fprintf(stderr, "Capability \"%s\" is out of range\n",
+ strcap);
+ goto out_free_name;
+ }
+ *mask |= 1ULL << cap;
+ }
+ ret = 0;
+
+out_free_name:
+ free(env_cap_name);
+ return ret;
+}
+
+static int populate_ruleset_cap(const char *const allowed_env,
+ const char *const quiet_env,
+ const int ruleset_fd)
+{
+ struct landlock_capability_attr cap_attr = {
+ .permissions = LANDLOCK_PERMISSION_CAPABILITY_USE,
+ };
+
+ if (parse_cap_list(allowed_env, &cap_attr.allowed_capabilities))
+ return 1;
+ if (parse_cap_list(quiet_env, &cap_attr.quiet_capabilities))
+ return 1;
+
+ if (!cap_attr.allowed_capabilities && !cap_attr.quiet_capabilities)
+ return 0;
+
+ if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_CAPABILITY, &cap_attr,
+ 0)) {
+ fprintf(stderr,
+ "Failed to update the ruleset with capabilities: %s\n",
+ strerror(errno));
+ return 1;
+ }
+ return 0;
+}
+
/* Returns true on error, false otherwise. */
static bool check_ruleset_scope(const char *const env_var,
struct landlock_ruleset_attr *ruleset_attr)
@@ -369,7 +538,7 @@ static int add_quiet_access(const char *const env_var,
return 0;
}
-#define LANDLOCK_ABI_LAST 11
+#define LANDLOCK_ABI_LAST 12
#define XSTR(s) #s
#define STR(s) XSTR(s)
@@ -397,6 +566,22 @@ static const char help[] =
"* " ENV_UDP_CONNECT_SEND_NAME ": remote UDP ports allowed to connect "
"or send to (client: use as destination port / server: receive only from it)\n"
"(caution: sending requires being able to bind to a local source port)\n"
+ "* " ENV_NS_NAME ": namespace types allowed to use\n"
+ " (cgroup, ipc, mnt, net, pid, time, user, uts)\n"
+ "* " ENV_NS_QUIET_NAME
+ ": namespace types whose denial should not be audit logged\n"
+ " (same value format as " ENV_NS_NAME
+ "; quieting an allowed member is inert,\n"
+ " as an allowed member is never denied; setting it alone still\n"
+ " restricts namespace use, denying every type)\n"
+ "* " ENV_CAP_NAME ": capabilities allowed to use, as names or numbers\n"
+ " (e.g. cap_net_bind_service, cap_sys_admin, 18)\n"
+ "* " ENV_CAP_QUIET_NAME
+ ": capabilities whose denial should not be audit logged\n"
+ " (same value format as " ENV_CAP_NAME
+ "; quieting an allowed member is inert,\n"
+ " as an allowed member is never denied; setting it alone still\n"
+ " restricts capability use, denying every capability)\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"
@@ -423,6 +608,8 @@ static const char help[] =
ENV_TCP_BIND_NAME "=\"9418\" "
ENV_TCP_CONNECT_NAME "=\"80:443\" "
ENV_UDP_CONNECT_SEND_NAME "=\"53\" "
+ ENV_NS_NAME "=\"user:uts:net\" "
+ ENV_CAP_NAME "=\"cap_sys_admin\" "
ENV_SCOPED_NAME "=\"a:s\" "
"%1$s bash -i\n"
"\n"
@@ -451,6 +638,8 @@ int main(const int argc, char *const argv[], char *const *const envp)
.quiet_access_fs = 0,
.quiet_access_net = 0,
.quiet_scoped = 0,
+ .handled_permissions = LANDLOCK_PERMISSION_NAMESPACE_USE |
+ LANDLOCK_PERMISSION_CAPABILITY_USE,
};
bool quiet_supported = true;
int supported_restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON |
@@ -552,7 +741,12 @@ int main(const int argc, char *const argv[], char *const *const envp)
supported_restrict_flags &=
~LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS;
set_restrict_flags &= ~LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS;
-
+ __attribute__((fallthrough));
+ case 11:
+ /* Removes LANDLOCK_PERMISSION_* for ABI < 12 */
+ ruleset_attr.handled_permissions &=
+ ~(LANDLOCK_PERMISSION_NAMESPACE_USE |
+ LANDLOCK_PERMISSION_CAPABILITY_USE);
/* Must be printed for any ABI < LANDLOCK_ABI_LAST. */
fprintf(stderr,
"Hint: You should update the running kernel "
@@ -597,6 +791,26 @@ int main(const int argc, char *const argv[], char *const *const envp)
~LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP;
}
+ /* Removes namespace handling if not set by a user. */
+ if (!getenv(ENV_NS_NAME) && !getenv(ENV_NS_QUIET_NAME))
+ ruleset_attr.handled_permissions &=
+ ~LANDLOCK_PERMISSION_NAMESPACE_USE;
+ if (!(ruleset_attr.handled_permissions &
+ LANDLOCK_PERMISSION_NAMESPACE_USE)) {
+ unsetenv(ENV_NS_NAME);
+ unsetenv(ENV_NS_QUIET_NAME);
+ }
+
+ /* Removes capability handling if not set by a user. */
+ if (!getenv(ENV_CAP_NAME) && !getenv(ENV_CAP_QUIET_NAME))
+ ruleset_attr.handled_permissions &=
+ ~LANDLOCK_PERMISSION_CAPABILITY_USE;
+ if (!(ruleset_attr.handled_permissions &
+ LANDLOCK_PERMISSION_CAPABILITY_USE)) {
+ unsetenv(ENV_CAP_NAME);
+ unsetenv(ENV_CAP_QUIET_NAME);
+ }
+
if (check_ruleset_scope(ENV_SCOPED_NAME, &ruleset_attr))
return 1;
@@ -680,6 +894,16 @@ int main(const int argc, char *const argv[], char *const *const envp)
}
}
+ if ((ruleset_attr.handled_permissions &
+ LANDLOCK_PERMISSION_NAMESPACE_USE) &&
+ populate_ruleset_ns(ENV_NS_NAME, ENV_NS_QUIET_NAME, ruleset_fd))
+ goto err_close_ruleset;
+
+ if ((ruleset_attr.handled_permissions &
+ LANDLOCK_PERMISSION_CAPABILITY_USE) &&
+ populate_ruleset_cap(ENV_CAP_NAME, ENV_CAP_QUIET_NAME, ruleset_fd))
+ goto err_close_ruleset;
+
if (!(set_restrict_flags & LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS) &&
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
perror("Failed to restrict privileges");
--
2.55.0
next prev parent reply other threads:[~2026-10-02 12:44 UTC|newest]
Thread overview: 9+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-02 12:43 [PATCH v4 0/8] Landlock: Namespace and capability control Mickaël Salaün
2026-10-02 12:43 ` [PATCH v4 1/8] landlock: Rename quiet_masks to quiet_access Mickaël Salaün
2026-10-02 12:43 ` [PATCH v4 2/8] landlock: Wrap per-layer access masks in struct layer_config Mickaël Salaün
2026-10-02 12:43 ` [PATCH v4 3/8] landlock: Enforce namespace use restrictions Mickaël Salaün
2026-10-02 12:43 ` [PATCH v4 4/8] landlock: Enforce capability restrictions Mickaël Salaün
2026-10-02 12:43 ` [PATCH v4 5/8] selftests/landlock: Add namespace restriction tests Mickaël Salaün
2026-10-02 12:43 ` [PATCH v4 6/8] selftests/landlock: Add capability " Mickaël Salaün
2026-10-02 12:44 ` Mickaël Salaün [this message]
2026-10-02 12:44 ` [PATCH v4 8/8] landlock: Add documentation for capability and namespace restrictions 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=20261002124409.1277970-8-mic@digikod.net \
--to=mic@digikod.net \
--cc=brauner@kernel.org \
--cc=corbet@lwn.net \
--cc=danieldurning.work@gmail.com \
--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=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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®