* [PATCH bpf-next v3 00/15] BPF interface for applying Landlock rulesets
@ 2026-09-09 19:37 Justin Suess
2026-09-09 19:37 ` [PATCH bpf-next v3 01/15] lsm: Add the LSM policy object lifetime hooks Justin Suess
` (14 more replies)
0 siblings, 15 replies; 29+ messages in thread
From: Justin Suess @ 2026-09-09 19:37 UTC (permalink / raw)
To: ast, daniel, andrii, kpsingh, matt, paul, mic, viro, brauner,
kees
Cc: casey, gnoack, jack, song, yonghong.song, martin.lau, eddyz87,
memxor, jolsa, m, bpf, linux-security-module, linux-kernel,
Justin Suess
Howdy,
This series lets BPF programs apply an existing, userspace-created
Landlock ruleset to a program during exec. The goal is unchanged
from the RFC [1], v1 [2], and v2 [3]: BPF does not create, inspect,
or mutate Landlock policy, it only decides whether a ruleset that
was already created and validated through Landlock's existing
userspace API should be applied, based on runtime exec context.
The policy is in place before the first instruction of the new
program runs, closing the race a userspace supervisor cannot.
v3 is v2 rebased onto bpf-next, plus small fixes; the design is
unchanged. The Landlock prerequisites (the ruleset/domain split,
the tracepoint series, and LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS)
went upstream in the 7.3 merge window, so the series now applies
directly to bpf-next.
The interface, for reference:
bpf_lsm_policy_from_fd(fd, flags) KF_ACQUIRE | KF_RET_NULL | KF_SLEEPABLE
bpf_lsm_policy_acquire(object) KF_ACQUIRE | KF_RCU | KF_RET_NULL
bpf_lsm_policy_release(object) KF_RELEASE
bpf_lsm_policy_apply_bprm(object, bprm, flags) KF_SLEEPABLE
The kfuncs are LSM-generic: they operate on struct
lsm_policy_object, which the owning LSM embeds in its own policy
structure, and dispatch to that LSM through four ordinary LSM hooks
(policy_object_from_fd, policy_object_get, policy_object_put,
bprm_apply_policy_object). No kfunc argument names an LSM anywhere
in the interface, yet it is not an ioctl-like multiplexer.
Landlock is the first provider.
Rather than repeating the whole design here, see the v2
cover letter [3] for the details, as the core design and API are
identical to the previous iteration.
Changes since v2
===
- Rebased onto bpf-next; prerequisites are now met.
- The kfunc filter's BPF_LSM_CGROUP case is dropped: since
commit 5b038319be44 ("bpf: Reject sleepable BPF_LSM_CGROUP
programs at load time") such programs cannot be sleepable, so
KF_SLEEPABLE already excludes them from the apply kfunc, making
that case redundant.
- The apply_bprm patch now documents why the attach-point filter,
not the verifier's argument typing, is the authorization boundary:
trusted linux_binprm pointers are also available at the other bprm
hooks and to tp_btf programs via the exec tracepoints, which share
the LSM programs' kfunc registration bucket.
- Fixed a pipe fd leak on the fork() error path of
test_restrict_binprm_discard() (Sashiko AI review).
Changes since v1 are summarized in the v2 cover letter [3].
The series is structured with LSM framework patches first: patches
1-2 add the hooks, 3 is trivial macro motion, 4-7 the kfuncs, 8 the
interface documentation, and 9 its LSM-independent selftests. The
Landlock provider follows: patches 10-13 add it, 14 its selftests,
and 15 its documentation.
[1] https://lore.kernel.org/linux-security-module/20260407200157.3874806-1-utilityemal77@gmail.com/
[2] https://lore.kernel.org/bpf/20260731022047.189137-1-utilityemal77@gmail.com/
[3] https://lore.kernel.org/bpf/20260831145858.3869191-1-utilityemal77@gmail.com/
Justin Suess (15):
lsm: Add the LSM policy object lifetime hooks
lsm: Add the bprm_apply_policy_object LSM hook
lsm: Move the lsm_for_each_hook() macro to security/lsm.h
lsm: Add the bpf_lsm_policy_release kfunc and policy object destructor
lsm: Add the bpf_lsm_policy_from_fd kfunc
lsm: Add the bpf_lsm_policy_acquire kfunc
lsm: Add the bpf_lsm_policy_apply_bprm kfunc
lsm: Document the LSM policy object interface
selftests/bpf: Add tests for the LSM policy object kfuncs
landlock: Expose the ruleset fd lookup to the rest of Landlock
landlock: Factor the credential restriction out of
landlock_restrict_self()
landlock: Free rulesets after an RCU grace period
landlock: Implement the LSM policy object hooks
selftests/bpf: Test the LSM policy object kfuncs with Landlock
landlock: Document the BPF policy interface
Documentation/security/landlock.rst | 38 ++
Documentation/security/lsm-development.rst | 49 ++
Documentation/trace/events-landlock.rst | 5 +-
MAINTAINERS | 1 +
include/linux/lsm_hook_defs.h | 6 +
include/linux/security.h | 11 +
include/trace/events/landlock.h | 15 +-
kernel/bpf/bpf_lsm.c | 4 +
kernel/bpf/verifier.c | 3 +
security/Makefile | 2 +-
security/bpf_lsm_kfuncs.c | 247 ++++++++
security/landlock/Makefile | 2 +
security/landlock/bpf.c | 152 +++++
security/landlock/bpf.h | 21 +
security/landlock/cred.c | 148 ++++-
security/landlock/cred.h | 47 ++
security/landlock/limits.h | 4 +
security/landlock/ruleset.c | 30 +-
security/landlock/ruleset.h | 75 ++-
security/landlock/setup.c | 2 +
security/landlock/syscalls.c | 105 +---
security/lsm.h | 6 +
security/security.c | 5 -
tools/testing/selftests/bpf/config | 1 +
tools/testing/selftests/bpf/config.x86_64 | 2 +-
.../bpf/prog_tests/lsm_policy_kfuncs.c | 54 ++
.../bpf/prog_tests/lsm_policy_landlock.c | 525 ++++++++++++++++++
.../selftests/bpf/progs/lsm_policy_kfuncs.c | 52 ++
.../bpf/progs/lsm_policy_kfuncs_failure.c | 154 +++++
.../selftests/bpf/progs/lsm_policy_landlock.c | 142 +++++
30 files changed, 1785 insertions(+), 123 deletions(-)
create mode 100644 security/bpf_lsm_kfuncs.c
create mode 100644 security/landlock/bpf.c
create mode 100644 security/landlock/bpf.h
create mode 100644 tools/testing/selftests/bpf/prog_tests/lsm_policy_kfuncs.c
create mode 100644 tools/testing/selftests/bpf/prog_tests/lsm_policy_landlock.c
create mode 100644 tools/testing/selftests/bpf/progs/lsm_policy_kfuncs.c
create mode 100644 tools/testing/selftests/bpf/progs/lsm_policy_kfuncs_failure.c
create mode 100644 tools/testing/selftests/bpf/progs/lsm_policy_landlock.c
base-commit: af0b84a9215d951d16f26b7ee34353b970cf5d4e
--
2.55.0
^ permalink raw reply [flat|nested] 29+ messages in thread
* [PATCH bpf-next v3 01/15] lsm: Add the LSM policy object lifetime hooks
2026-09-09 19:37 [PATCH bpf-next v3 00/15] BPF interface for applying Landlock rulesets Justin Suess
@ 2026-09-09 19:37 ` Justin Suess
2026-09-09 19:37 ` [PATCH bpf-next v3 02/15] lsm: Add the bprm_apply_policy_object LSM hook Justin Suess
` (13 subsequent siblings)
14 siblings, 0 replies; 29+ messages in thread
From: Justin Suess @ 2026-09-09 19:37 UTC (permalink / raw)
To: ast, daniel, andrii, kpsingh, matt, paul, mic, viro, brauner,
kees
Cc: casey, gnoack, jack, song, yonghong.song, martin.lau, eddyz87,
memxor, jolsa, m, bpf, linux-security-module, linux-kernel,
Justin Suess
Add struct lsm_policy_object, the identity an LSM embeds in a policy
object it shares with BPF programs, and the three hooks managing such
an object's lifetime:
policy_object_from_fd(fd, &object)
policy_object_get(object)
policy_object_put(object)
The object records the owning LSM's LSM_ID_* value. The BPF kfuncs
built on these hooks dispatch each call on an object to the one LSM
matching its lsmid, which resolves the containing object with
container_of(); the framework never interprets an object beyond its
lsmid. The type field discriminates between the owning LSM's own
policy object kinds and is private to it, with 0 reserved as "unset"
so a zeroed, untagged object fails every type check.
from_fd has no object to route by: the fd refers to a file set up
through the owning LSM's own userspace interface, so the fd itself
identifies its LSM. The framework offers the fd to every
implementation in turn; an LSM declines a fd that is not one of its
policy objects with -EOPNOTSUPP, and any other error is a definitive
translation failure.
The hooks back referenced BPF kptrs, which imposes the same lifetime
contract on every implementation: from_fd returns a reference on a
live object, get acquires with inc-not-zero semantics and fails with
-ENOENT once the count dropped to zero, put may be called from
contexts that cannot sleep (BPF drives it from map destructors), and
the containing object is freed only after an RCU grace period, as
programs load policy object kptrs from maps under RCU and may examine
an object concurrently with its last put.
The hooks are excluded from the "bpf" LSM's attachment points. The
object-routed hooks are unreachable there, as LSM_ID_BPF policy
objects cannot exist; for from_fd, whose walk visits every
implementation, a BPF program cannot fill the object out parameter,
so an attachment returning 0 would hand the caller an uninitialized
pointer.
Cc: Paul Moore <paul@paul-moore.com>
Cc: Casey Schaufler <casey@schaufler-ca.com>
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
Notes:
v2->v3:
- No change.
include/linux/lsm_hook_defs.h | 4 ++++
include/linux/security.h | 11 +++++++++++
kernel/bpf/bpf_lsm.c | 3 +++
3 files changed, 18 insertions(+)
diff --git a/include/linux/lsm_hook_defs.h b/include/linux/lsm_hook_defs.h
index 65c9609ec207..d7684407737a 100644
--- a/include/linux/lsm_hook_defs.h
+++ b/include/linux/lsm_hook_defs.h
@@ -452,6 +452,10 @@ LSM_HOOK(int, 0, bpf_token_create, struct bpf_token *token, union bpf_attr *attr
LSM_HOOK(void, LSM_RET_VOID, bpf_token_free, struct bpf_token *token)
LSM_HOOK(int, 0, bpf_token_cmd, const struct bpf_token *token, enum bpf_cmd cmd)
LSM_HOOK(int, 0, bpf_token_capable, const struct bpf_token *token, int cap)
+LSM_HOOK(int, -EOPNOTSUPP, policy_object_from_fd, int fd,
+ struct lsm_policy_object **object)
+LSM_HOOK(int, -EOPNOTSUPP, policy_object_get, struct lsm_policy_object *object)
+LSM_HOOK(void, LSM_RET_VOID, policy_object_put, struct lsm_policy_object *object)
#endif /* CONFIG_BPF_SYSCALL */
LSM_HOOK(int, 0, locked_down, enum lockdown_reason what)
diff --git a/include/linux/security.h b/include/linux/security.h
index 153e9043058f..5e423bea080e 100644
--- a/include/linux/security.h
+++ b/include/linux/security.h
@@ -168,6 +168,17 @@ struct lsm_prop {
struct lsm_prop_bpf bpf;
};
+/*
+ * Identity of a policy object an LSM shares with BPF programs,
+ * embedded in the LSM's own object. @lsmid identifies the owning
+ * LSM; @type discriminates that LSM's policy object types, with 0
+ * reserved as "unset".
+ */
+struct lsm_policy_object {
+ u64 lsmid;
+ u32 type;
+};
+
extern const char *const lockdown_reasons[LOCKDOWN_CONFIDENTIALITY_MAX+1];
/* These functions are in security/commoncap.c */
diff --git a/kernel/bpf/bpf_lsm.c b/kernel/bpf/bpf_lsm.c
index 82c5988417a0..f762cac6838b 100644
--- a/kernel/bpf/bpf_lsm.c
+++ b/kernel/bpf/bpf_lsm.c
@@ -56,6 +56,9 @@ BTF_ID(func, bpf_lsm_xfrm_decode_session)
#endif
BTF_ID(func, bpf_lsm_ismaclabel)
BTF_ID(func, bpf_lsm_file_alloc_security)
+BTF_ID(func, bpf_lsm_policy_object_from_fd)
+BTF_ID(func, bpf_lsm_policy_object_get)
+BTF_ID(func, bpf_lsm_policy_object_put)
BTF_SET_END(bpf_lsm_disabled_hooks)
/* List of LSM hooks that should operate on 'current' cgroup regardless
--
2.55.0
^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH bpf-next v3 02/15] lsm: Add the bprm_apply_policy_object LSM hook
2026-09-09 19:37 [PATCH bpf-next v3 00/15] BPF interface for applying Landlock rulesets Justin Suess
2026-09-09 19:37 ` [PATCH bpf-next v3 01/15] lsm: Add the LSM policy object lifetime hooks Justin Suess
@ 2026-09-09 19:37 ` Justin Suess
2026-09-09 19:37 ` [PATCH bpf-next v3 03/15] lsm: Move the lsm_for_each_hook() macro to security/lsm.h Justin Suess
` (12 subsequent siblings)
14 siblings, 0 replies; 29+ messages in thread
From: Justin Suess @ 2026-09-09 19:37 UTC (permalink / raw)
To: ast, daniel, andrii, kpsingh, matt, paul, mic, viro, brauner,
kees
Cc: casey, gnoack, jack, song, yonghong.song, martin.lau, eddyz87,
memxor, jolsa, m, bpf, linux-security-module, linux-kernel,
Justin Suess
Add the first policy object operation, applying a policy to the
credentials prepared for an execution:
bprm_apply_policy_object(bprm, object, flags)
The hook is only called between the preparation and the commitment of
the bprm's credentials, i.e. from a bprm_creds_for_exec() or
bprm_creds_from_file() context, where the executed task can still be
arranged to start confined by the policy.
How the policy composes with restrictions the credentials already
carry, and the meaning of @flags, are defined by the implementing
LSM, which must reject unsupported flags with -EINVAL. An LSM with
no notion of applying a policy object to an execution does not
implement the hook, and the calling kfunc fails with -EOPNOTSUPP.
Like the lifetime hooks, this hook is excluded from the "bpf" LSM's
attachment points, as the targeted dispatch makes an attachment there
unreachable.
Cc: Paul Moore <paul@paul-moore.com>
Cc: Casey Schaufler <casey@schaufler-ca.com>
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
Notes:
v2->v3:
- No change.
include/linux/lsm_hook_defs.h | 2 ++
kernel/bpf/bpf_lsm.c | 1 +
2 files changed, 3 insertions(+)
diff --git a/include/linux/lsm_hook_defs.h b/include/linux/lsm_hook_defs.h
index d7684407737a..ddb15bea383e 100644
--- a/include/linux/lsm_hook_defs.h
+++ b/include/linux/lsm_hook_defs.h
@@ -452,6 +452,8 @@ LSM_HOOK(int, 0, bpf_token_create, struct bpf_token *token, union bpf_attr *attr
LSM_HOOK(void, LSM_RET_VOID, bpf_token_free, struct bpf_token *token)
LSM_HOOK(int, 0, bpf_token_cmd, const struct bpf_token *token, enum bpf_cmd cmd)
LSM_HOOK(int, 0, bpf_token_capable, const struct bpf_token *token, int cap)
+LSM_HOOK(int, -EOPNOTSUPP, bprm_apply_policy_object, struct linux_binprm *bprm,
+ struct lsm_policy_object *object, u32 flags)
LSM_HOOK(int, -EOPNOTSUPP, policy_object_from_fd, int fd,
struct lsm_policy_object **object)
LSM_HOOK(int, -EOPNOTSUPP, policy_object_get, struct lsm_policy_object *object)
diff --git a/kernel/bpf/bpf_lsm.c b/kernel/bpf/bpf_lsm.c
index f762cac6838b..f3de70511c0f 100644
--- a/kernel/bpf/bpf_lsm.c
+++ b/kernel/bpf/bpf_lsm.c
@@ -56,6 +56,7 @@ BTF_ID(func, bpf_lsm_xfrm_decode_session)
#endif
BTF_ID(func, bpf_lsm_ismaclabel)
BTF_ID(func, bpf_lsm_file_alloc_security)
+BTF_ID(func, bpf_lsm_bprm_apply_policy_object)
BTF_ID(func, bpf_lsm_policy_object_from_fd)
BTF_ID(func, bpf_lsm_policy_object_get)
BTF_ID(func, bpf_lsm_policy_object_put)
--
2.55.0
^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH bpf-next v3 03/15] lsm: Move the lsm_for_each_hook() macro to security/lsm.h
2026-09-09 19:37 [PATCH bpf-next v3 00/15] BPF interface for applying Landlock rulesets Justin Suess
2026-09-09 19:37 ` [PATCH bpf-next v3 01/15] lsm: Add the LSM policy object lifetime hooks Justin Suess
2026-09-09 19:37 ` [PATCH bpf-next v3 02/15] lsm: Add the bprm_apply_policy_object LSM hook Justin Suess
@ 2026-09-09 19:37 ` Justin Suess
2026-09-09 19:37 ` [PATCH bpf-next v3 04/15] lsm: Add the bpf_lsm_policy_release kfunc and policy object destructor Justin Suess
` (11 subsequent siblings)
14 siblings, 0 replies; 29+ messages in thread
From: Justin Suess @ 2026-09-09 19:37 UTC (permalink / raw)
To: ast, daniel, andrii, kpsingh, matt, paul, mic, viro, brauner,
kees
Cc: casey, gnoack, jack, song, yonghong.song, martin.lau, eddyz87,
memxor, jolsa, m, bpf, linux-security-module, linux-kernel,
Justin Suess
Move the lsm_for_each_hook() iterator from security/security.c to
security/lsm.h, verbatim: a following commit adds a user outside
security.c, the file implementing the LSM policy object kfuncs.
No functional change.
Cc: Paul Moore <paul@paul-moore.com>
Cc: Casey Schaufler <casey@schaufler-ca.com>
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
Notes:
v2->v3:
- No change.
security/lsm.h | 6 ++++++
security/security.c | 5 -----
2 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/security/lsm.h b/security/lsm.h
index 32f808ad4335..264ae63290a8 100644
--- a/security/lsm.h
+++ b/security/lsm.h
@@ -24,6 +24,12 @@ extern bool lsm_debug;
extern unsigned int lsm_active_cnt;
extern const struct lsm_id *lsm_idlist[];
+/* Iterate over the active implementations of a given hook */
+#define lsm_for_each_hook(scall, NAME) \
+ for (scall = static_calls_table.NAME; \
+ scall - static_calls_table.NAME < MAX_LSM_COUNT; scall++) \
+ if (static_key_enabled(&scall->active->key))
+
/* LSM blob configuration */
extern struct lsm_blob_sizes blob_sizes;
diff --git a/security/security.c b/security/security.c
index 2ee276ab15c5..74f98ef8025e 100644
--- a/security/security.c
+++ b/security/security.c
@@ -495,11 +495,6 @@ OUT: \
RC; \
})
-#define lsm_for_each_hook(scall, NAME) \
- for (scall = static_calls_table.NAME; \
- scall - static_calls_table.NAME < MAX_LSM_COUNT; scall++) \
- if (static_key_enabled(&scall->active->key))
-
/* Security operations */
/**
--
2.55.0
^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH bpf-next v3 04/15] lsm: Add the bpf_lsm_policy_release kfunc and policy object destructor
2026-09-09 19:37 [PATCH bpf-next v3 00/15] BPF interface for applying Landlock rulesets Justin Suess
` (2 preceding siblings ...)
2026-09-09 19:37 ` [PATCH bpf-next v3 03/15] lsm: Move the lsm_for_each_hook() macro to security/lsm.h Justin Suess
@ 2026-09-09 19:37 ` Justin Suess
2026-09-09 20:29 ` bot+bpf-ci
2026-09-09 21:34 ` Paul Moore
2026-09-09 19:37 ` [PATCH bpf-next v3 05/15] lsm: Add the bpf_lsm_policy_from_fd kfunc Justin Suess
` (10 subsequent siblings)
14 siblings, 2 replies; 29+ messages in thread
From: Justin Suess @ 2026-09-09 19:37 UTC (permalink / raw)
To: ast, daniel, andrii, kpsingh, matt, paul, mic, viro, brauner,
kees
Cc: casey, gnoack, jack, song, yonghong.song, martin.lau, eddyz87,
memxor, jolsa, m, bpf, linux-security-module, linux-kernel,
Justin Suess
Add security/bpf_lsm_kfuncs.c, the home of the kfuncs exposing LSM
policy objects to BPF programs, with the first of them:
bpf_lsm_policy_release(object) KF_RELEASE
The kfuncs are the LSM framework's own BPF interface: there is no
per-LSM kfunc and no intermediate security_*() layer. Each kfunc
walks the matching hook's implementation list and calls the one
registered by the LSM whose lsmid the policy object carries. Calling
a kfunc for an LSM that is not active or has no policy object support
fails at runtime rather than hiding the kfunc at verification time,
so BPF program loading is independent of the boot-time LSM
configuration.
A policy object reference is meant to be handed over through a map
kptr field, so also register a destructor for struct
lsm_policy_object: map-held references are dropped on map teardown,
possibly from a context that cannot sleep, which the
policy_object_put() hook contract accounts for. For the same reason
the kfunc is not KF_SLEEPABLE, and the filter adds no per-kfunc rule:
releasing a reference must be allowed wherever one can be held. The
filter itself is needed because BPF_PROG_TYPE_LSM and
BPF_PROG_TYPE_SYSCALL, the two registered program types, share their
kfunc lookup buckets with other program types.
Cc: Paul Moore <paul@paul-moore.com>
Cc: KP Singh <kpsingh@kernel.org>
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
Notes:
v2->v3:
- No change.
MAINTAINERS | 1 +
security/Makefile | 2 +-
security/bpf_lsm_kfuncs.c | 98 +++++++++++++++++++++++++++++++++++++++
3 files changed, 100 insertions(+), 1 deletion(-)
create mode 100644 security/bpf_lsm_kfuncs.c
diff --git a/MAINTAINERS b/MAINTAINERS
index 6215fcb07770..ba816d7ee127 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -5081,6 +5081,7 @@ F: kernel/bpf/bpf_lsm.c
F: kernel/bpf/bpf_lsm_proto.c
F: kernel/trace/bpf_trace.c
F: security/bpf/
+F: security/bpf_lsm_kfuncs.c
BPF [SELFTESTS] (Test Runners & Infrastructure)
M: Andrii Nakryiko <andrii@kernel.org>
diff --git a/security/Makefile b/security/Makefile
index 4601230ba442..a9364ea9828b 100644
--- a/security/Makefile
+++ b/security/Makefile
@@ -23,7 +23,7 @@ obj-$(CONFIG_SECURITY_LOADPIN) += loadpin/
obj-$(CONFIG_SECURITY_SAFESETID) += safesetid/
obj-$(CONFIG_SECURITY_LOCKDOWN_LSM) += lockdown/
obj-$(CONFIG_CGROUPS) += device_cgroup.o
-obj-$(CONFIG_BPF_LSM) += bpf/
+obj-$(CONFIG_BPF_LSM) += bpf/ bpf_lsm_kfuncs.o
obj-$(CONFIG_SECURITY_LANDLOCK) += landlock/
obj-$(CONFIG_SECURITY_IPE) += ipe/
diff --git a/security/bpf_lsm_kfuncs.c b/security/bpf_lsm_kfuncs.c
new file mode 100644
index 000000000000..e1190215d477
--- /dev/null
+++ b/security/bpf_lsm_kfuncs.c
@@ -0,0 +1,98 @@
+// SPDX-License-Identifier: GPL-2.0
+
+/* BPF kfuncs exposing LSM policy objects. */
+
+#include <linux/bpf.h>
+#include <linux/btf.h>
+#include <linux/btf_ids.h>
+#include <linux/cfi.h>
+#include <linux/init.h>
+#include <linux/lsm_hooks.h>
+#include <linux/security.h>
+
+#include "lsm.h"
+
+__bpf_kfunc_start_defs();
+
+/**
+ * bpf_lsm_policy_release - Release a policy object reference
+ * @object: policy object to release
+ *
+ * Release an acquired reference on a policy object.
+ */
+__bpf_kfunc void bpf_lsm_policy_release(struct lsm_policy_object *object)
+{
+ struct lsm_static_call *scall;
+
+ lsm_for_each_hook(scall, policy_object_put) {
+ if (scall->hl->lsmid->id != object->lsmid)
+ continue;
+ scall->hl->hook.policy_object_put(object);
+ return;
+ }
+ /* A held reference implies the owning LSM implements the hook. */
+ WARN_ON_ONCE(1);
+}
+
+/* Destructor for referenced lsm_policy_object kptrs. */
+__bpf_kfunc void bpf_lsm_policy_release_dtor(void *object)
+{
+ bpf_lsm_policy_release(object);
+}
+CFI_NOSEAL(bpf_lsm_policy_release_dtor);
+
+__bpf_kfunc_end_defs();
+
+BTF_KFUNCS_START(bpf_lsm_policy_kfunc_ids)
+BTF_ID_FLAGS(func, bpf_lsm_policy_release, KF_RELEASE)
+BTF_KFUNCS_END(bpf_lsm_policy_kfunc_ids)
+
+BTF_ID_LIST(bpf_lsm_policy_dtor_ids)
+BTF_ID(struct, lsm_policy_object)
+BTF_ID(func, bpf_lsm_policy_release_dtor)
+
+/*
+ * BPF_PROG_TYPE_LSM and BPF_PROG_TYPE_SYSCALL share their kfunc
+ * lookup buckets with other program types, so restricting the policy
+ * kfuncs requires a filter.
+ */
+static int bpf_lsm_policy_kfunc_filter(const struct bpf_prog *prog,
+ u32 kfunc_id)
+{
+ if (!btf_id_set8_contains(&bpf_lsm_policy_kfunc_ids, kfunc_id))
+ return 0;
+
+ switch (prog->type) {
+ case BPF_PROG_TYPE_SYSCALL:
+ case BPF_PROG_TYPE_LSM:
+ return 0;
+ default:
+ return -EACCES;
+ }
+}
+
+static const struct btf_kfunc_id_set bpf_lsm_policy_kfunc_set = {
+ .owner = THIS_MODULE,
+ .set = &bpf_lsm_policy_kfunc_ids,
+ .filter = bpf_lsm_policy_kfunc_filter,
+};
+
+static int __init bpf_lsm_policy_kfunc_init(void)
+{
+ const struct btf_id_dtor_kfunc bpf_lsm_policy_dtors[] = {
+ {
+ .btf_id = bpf_lsm_policy_dtor_ids[0],
+ .kfunc_btf_id = bpf_lsm_policy_dtor_ids[1],
+ },
+ };
+ int ret;
+
+ ret = register_btf_kfunc_id_set(BPF_PROG_TYPE_LSM,
+ &bpf_lsm_policy_kfunc_set);
+ ret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_SYSCALL,
+ &bpf_lsm_policy_kfunc_set);
+ return ret ?: register_btf_id_dtor_kfuncs(bpf_lsm_policy_dtors,
+ ARRAY_SIZE(bpf_lsm_policy_dtors),
+ THIS_MODULE);
+}
+late_initcall(bpf_lsm_policy_kfunc_init);
--
2.55.0
^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH bpf-next v3 05/15] lsm: Add the bpf_lsm_policy_from_fd kfunc
2026-09-09 19:37 [PATCH bpf-next v3 00/15] BPF interface for applying Landlock rulesets Justin Suess
` (3 preceding siblings ...)
2026-09-09 19:37 ` [PATCH bpf-next v3 04/15] lsm: Add the bpf_lsm_policy_release kfunc and policy object destructor Justin Suess
@ 2026-09-09 19:37 ` Justin Suess
2026-09-09 20:46 ` bot+bpf-ci
2026-09-09 19:37 ` [PATCH bpf-next v3 06/15] lsm: Add the bpf_lsm_policy_acquire kfunc Justin Suess
` (9 subsequent siblings)
14 siblings, 1 reply; 29+ messages in thread
From: Justin Suess @ 2026-09-09 19:37 UTC (permalink / raw)
To: ast, daniel, andrii, kpsingh, matt, paul, mic, viro, brauner,
kees
Cc: casey, gnoack, jack, song, yonghong.song, martin.lau, eddyz87,
memxor, jolsa, m, bpf, linux-security-module, linux-kernel,
Justin Suess
Add the kfunc translating a file descriptor into a referenced policy
object:
bpf_lsm_policy_from_fd(fd, flags)
KF_ACQUIRE|KF_RET_NULL|KF_SLEEPABLE
No argument names an LSM: a policy object fd refers to a file set up
through the owning LSM's own userspace interface so the fd itself
identifies the LSM asked to translate it. The kfunc offers the fd to
every policy_object_from_fd implementation in turn until one claims it.
Following the convention of the lsm_*(2) syscalls, @flags belongs to
the framework and is reserved: the kfunc returns NULL for @flags != 0.
A policy object fd is only meaningful in the fd table of the process
that set the object up, while an LSM program runs in the context of
the task it mediates, so the filter makes this kfunc exclusive to
syscall programs (BPF_PROG_TYPE_SYSCALL), which run in the context of
the task invoking them. The acquired object may be released with
bpf_lsm_policy_release().
Cc: Paul Moore <paul@paul-moore.com>
Cc: KP Singh <kpsingh@kernel.org>
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
Notes:
v2->v3:
- No change.
security/bpf_lsm_kfuncs.c | 53 +++++++++++++++++++++++++++++++++++++--
1 file changed, 51 insertions(+), 2 deletions(-)
diff --git a/security/bpf_lsm_kfuncs.c b/security/bpf_lsm_kfuncs.c
index e1190215d477..988dcd6f4dd9 100644
--- a/security/bpf_lsm_kfuncs.c
+++ b/security/bpf_lsm_kfuncs.c
@@ -14,11 +14,50 @@
__bpf_kfunc_start_defs();
+/**
+ * bpf_lsm_policy_from_fd - Get an LSM policy object from a fd
+ * @fd: file descriptor referring to a policy object, resolved in the
+ * file descriptor table of the task running the program
+ * @flags: reserved for future use, must be 0
+ *
+ * Translate @fd, as set up through the owning LSM's own userspace
+ * interface, into a referenced policy object. The fd identifies the
+ * LSM asked to translate it: each LSM recognizes its own fds and
+ * declines every other. Only syscall programs may call this kfunc:
+ * they run in the context of the task invoking them, where the fd is
+ * meaningful. The reference must be released with
+ * bpf_lsm_policy_release().
+ *
+ * Return: A referenced policy object, or NULL if @flags is not 0, if
+ * no enabled LSM recognizes @fd as one of its policy objects, or if
+ * the recognizing LSM fails to translate it.
+ */
+__bpf_kfunc struct lsm_policy_object *bpf_lsm_policy_from_fd(int fd, u32 flags)
+{
+ struct lsm_static_call *scall;
+ struct lsm_policy_object *object;
+ int err;
+
+ if (flags)
+ return NULL;
+
+ lsm_for_each_hook(scall, policy_object_from_fd) {
+ err = scall->hl->hook.policy_object_from_fd(fd, &object);
+ if (err == -EOPNOTSUPP)
+ /* Not this LSM's fd: let another claim it. */
+ continue;
+ if (err)
+ return NULL;
+ return object;
+ }
+ return NULL;
+}
+
/**
* bpf_lsm_policy_release - Release a policy object reference
* @object: policy object to release
*
- * Release an acquired reference on a policy object.
+ * Release a reference acquired with bpf_lsm_policy_from_fd().
*/
__bpf_kfunc void bpf_lsm_policy_release(struct lsm_policy_object *object)
{
@@ -44,6 +83,8 @@ CFI_NOSEAL(bpf_lsm_policy_release_dtor);
__bpf_kfunc_end_defs();
BTF_KFUNCS_START(bpf_lsm_policy_kfunc_ids)
+BTF_ID_FLAGS(func, bpf_lsm_policy_from_fd,
+ KF_ACQUIRE | KF_RET_NULL | KF_SLEEPABLE)
BTF_ID_FLAGS(func, bpf_lsm_policy_release, KF_RELEASE)
BTF_KFUNCS_END(bpf_lsm_policy_kfunc_ids)
@@ -51,10 +92,14 @@ BTF_ID_LIST(bpf_lsm_policy_dtor_ids)
BTF_ID(struct, lsm_policy_object)
BTF_ID(func, bpf_lsm_policy_release_dtor)
+BTF_ID_LIST_SINGLE(bpf_lsm_policy_from_fd_ids, func, bpf_lsm_policy_from_fd)
+
/*
* BPF_PROG_TYPE_LSM and BPF_PROG_TYPE_SYSCALL share their kfunc
* lookup buckets with other program types, so restricting the policy
- * kfuncs requires a filter.
+ * kfuncs requires a filter. A policy object fd is only meaningful in
+ * the fd table of the task that set the object up: the fd kfunc is
+ * exclusive to syscall programs, which run in that task's context.
*/
static int bpf_lsm_policy_kfunc_filter(const struct bpf_prog *prog,
u32 kfunc_id)
@@ -64,7 +109,11 @@ static int bpf_lsm_policy_kfunc_filter(const struct bpf_prog *prog,
switch (prog->type) {
case BPF_PROG_TYPE_SYSCALL:
+ return 0;
case BPF_PROG_TYPE_LSM:
+ if (kfunc_id == bpf_lsm_policy_from_fd_ids[0])
+ return -EACCES;
+
return 0;
default:
return -EACCES;
--
2.55.0
^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH bpf-next v3 06/15] lsm: Add the bpf_lsm_policy_acquire kfunc
2026-09-09 19:37 [PATCH bpf-next v3 00/15] BPF interface for applying Landlock rulesets Justin Suess
` (4 preceding siblings ...)
2026-09-09 19:37 ` [PATCH bpf-next v3 05/15] lsm: Add the bpf_lsm_policy_from_fd kfunc Justin Suess
@ 2026-09-09 19:37 ` Justin Suess
2026-09-09 20:30 ` bot+bpf-ci
2026-09-09 19:37 ` [PATCH bpf-next v3 07/15] lsm: Add the bpf_lsm_policy_apply_bprm kfunc Justin Suess
` (8 subsequent siblings)
14 siblings, 1 reply; 29+ messages in thread
From: Justin Suess @ 2026-09-09 19:37 UTC (permalink / raw)
To: ast, daniel, andrii, kpsingh, matt, paul, mic, viro, brauner,
kees
Cc: casey, gnoack, jack, song, yonghong.song, martin.lau, eddyz87,
memxor, jolsa, m, bpf, linux-security-module, linux-kernel,
Justin Suess
Add the kfunc acquiring a reference on a policy object the program
does not own:
bpf_lsm_policy_acquire(object) KF_ACQUIRE|KF_RCU|KF_RET_NULL
bpf_kptr_xchg() is the only way to take an owned pointer out of a map
kptr field, and it empties the slot: concurrent executions of an
enforcement program would race for the one stored reference. Modeled
after bpf_task_acquire(), this kfunc removes the exclusivity: a
program loads the kptr field with a plain read under
bpf_rcu_read_lock(), acquires its own reference through the
policy_object_get hook, and leaves the map slot untouched. The
acquired reference survives bpf_rcu_read_unlock(), carrying over to a
sleepable bpf_lsm_policy_apply_bprm() call, and is released with
bpf_lsm_policy_release().
Adding struct lsm_policy_object to the verifier's rcu_protected_types
set makes the plain load yield an RCU-protected pointer instead of an
untrusted one. This is where the policy object contract's RCU
requirements become load-bearing: the kfunc and the get hook examine
the object concurrently with a possible last put, which is safe
because implementations free only after an RCU grace period and
acquire with inc-not-zero semantics. A failed get makes the kfunc
return NULL, per KF_RET_NULL.
The kfunc does not sleep and is meaningful wherever a policy object
pointer can be loaded, so the filter adds no per-kfunc rule.
Cc: Paul Moore <paul@paul-moore.com>
Cc: KP Singh <kpsingh@kernel.org>
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
Notes:
v2->v3:
- No change.
kernel/bpf/verifier.c | 3 +++
security/bpf_lsm_kfuncs.c | 34 +++++++++++++++++++++++++++++++++-
2 files changed, 36 insertions(+), 1 deletion(-)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 9e79750e2480..d1af0090d38f 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -4660,6 +4660,9 @@ BTF_ID(struct, bpf_crypto_ctx)
#ifdef CONFIG_INET
BTF_ID(struct, bpf_ksock)
#endif
+#ifdef CONFIG_BPF_LSM
+BTF_ID(struct, lsm_policy_object)
+#endif
BTF_SET_END(rcu_protected_types)
static bool rcu_protected_object(const struct btf *btf, u32 btf_id)
diff --git a/security/bpf_lsm_kfuncs.c b/security/bpf_lsm_kfuncs.c
index 988dcd6f4dd9..43a4bf57fd31 100644
--- a/security/bpf_lsm_kfuncs.c
+++ b/security/bpf_lsm_kfuncs.c
@@ -14,6 +14,36 @@
__bpf_kfunc_start_defs();
+/**
+ * bpf_lsm_policy_acquire - Acquire a reference on a shared policy object
+ * @object: RCU-protected pointer to a policy object, e.g. loaded from
+ * a map kptr field under bpf_rcu_read_lock()
+ *
+ * Acquire a reference of its own on a policy object the program does
+ * not own, so that any number of concurrent program executions can
+ * use the object shared through one map kptr field, without emptying
+ * it as bpf_kptr_xchg() would. The returned reference stays valid
+ * after bpf_rcu_read_unlock() and must be released with
+ * bpf_lsm_policy_release().
+ *
+ * Return: A referenced policy object, or NULL if the object's
+ * reference count concurrently dropped to zero.
+ */
+__bpf_kfunc struct lsm_policy_object *
+bpf_lsm_policy_acquire(struct lsm_policy_object *object)
+{
+ struct lsm_static_call *scall;
+
+ lsm_for_each_hook(scall, policy_object_get) {
+ if (scall->hl->lsmid->id != object->lsmid)
+ continue;
+ if (scall->hl->hook.policy_object_get(object))
+ return NULL;
+ return object;
+ }
+ return NULL;
+}
+
/**
* bpf_lsm_policy_from_fd - Get an LSM policy object from a fd
* @fd: file descriptor referring to a policy object, resolved in the
@@ -57,7 +87,8 @@ __bpf_kfunc struct lsm_policy_object *bpf_lsm_policy_from_fd(int fd, u32 flags)
* bpf_lsm_policy_release - Release a policy object reference
* @object: policy object to release
*
- * Release a reference acquired with bpf_lsm_policy_from_fd().
+ * Release a reference acquired with bpf_lsm_policy_from_fd() or
+ * bpf_lsm_policy_acquire().
*/
__bpf_kfunc void bpf_lsm_policy_release(struct lsm_policy_object *object)
{
@@ -83,6 +114,7 @@ CFI_NOSEAL(bpf_lsm_policy_release_dtor);
__bpf_kfunc_end_defs();
BTF_KFUNCS_START(bpf_lsm_policy_kfunc_ids)
+BTF_ID_FLAGS(func, bpf_lsm_policy_acquire, KF_ACQUIRE | KF_RCU | KF_RET_NULL)
BTF_ID_FLAGS(func, bpf_lsm_policy_from_fd,
KF_ACQUIRE | KF_RET_NULL | KF_SLEEPABLE)
BTF_ID_FLAGS(func, bpf_lsm_policy_release, KF_RELEASE)
--
2.55.0
^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH bpf-next v3 07/15] lsm: Add the bpf_lsm_policy_apply_bprm kfunc
2026-09-09 19:37 [PATCH bpf-next v3 00/15] BPF interface for applying Landlock rulesets Justin Suess
` (5 preceding siblings ...)
2026-09-09 19:37 ` [PATCH bpf-next v3 06/15] lsm: Add the bpf_lsm_policy_acquire kfunc Justin Suess
@ 2026-09-09 19:37 ` Justin Suess
2026-09-09 19:55 ` sashiko-bot
2026-09-09 19:37 ` [PATCH bpf-next v3 08/15] lsm: Document the LSM policy object interface Justin Suess
` (7 subsequent siblings)
14 siblings, 1 reply; 29+ messages in thread
From: Justin Suess @ 2026-09-09 19:37 UTC (permalink / raw)
To: ast, daniel, andrii, kpsingh, matt, paul, mic, viro, brauner,
kees
Cc: casey, gnoack, jack, song, yonghong.song, martin.lau, eddyz87,
memxor, jolsa, m, bpf, linux-security-module, linux-kernel,
Justin Suess
Add the kfunc applying a policy object to an execution:
bpf_lsm_policy_apply_bprm(object, bprm, flags) KF_SLEEPABLE
It asks the LSM owning @object, through the bprm_apply_policy_object
hook, to restrict the credentials prepared in @bprm, so that the
executed task starts confined by the policy. The meaning of @flags
and the composition with restrictions the credentials already carry
are the owning LSM's; an LSM without execution policy support makes
the call fail with -EOPNOTSUPP.
The kfunc runs the hook in a root memcg charging scope: the policy
restricts the execution on behalf of the BPF program, not of the
mediated task, so what the owning LSM allocates to compute it, e.g.
Landlock's merged domain, is not charged to the task the program
supervises.
The filter makes the kfunc exclusive to the sleepable LSM programs
attached to the bprm_creds_for_exec() or bprm_creds_from_file()
hooks, the only contexts where the bprm's credentials are prepared
but not yet committed. The verifier's argument typing does not draw
this boundary on its own: a trusted struct linux_binprm pointer is
also available at the other bprm hooks -- bprm_check_security(),
which runs per binfmt while an interpreter may still rewrite the
execution, and bprm_committing_creds()/bprm_committed_creds(), which
run at or past the point of no return, where the prepared
credentials are frozen or installed -- and to tp_btf programs via
the sched_prepare_exec and sched_process_exec tracepoints, which
resolve kfuncs from the same registration bucket as LSM programs.
The attach-point filter, not the argument type, is the authorization
boundary.
No filter case is needed for BPF_LSM_CGROUP programs: since
commit 5b038319be44 ("bpf: Reject sleepable BPF_LSM_CGROUP programs
at load time") they cannot be sleepable, so KF_SLEEPABLE already
excludes them.
Cc: Paul Moore <paul@paul-moore.com>
Cc: KP Singh <kpsingh@kernel.org>
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
Notes:
v2->v3:
- Drop the BPF_LSM_CGROUP case from the kfunc filter: since
commit 5b038319be44 ("bpf: Reject sleepable BPF_LSM_CGROUP
programs at load time") such programs cannot be sleepable, so
KF_SLEEPABLE already excludes them from the apply kfunc.
- Document, in the commit message and the filter comment, why the
attach-point filter rather than the verifier's argument typing
is the authorization boundary.
security/bpf_lsm_kfuncs.c | 68 +++++++++++++++++++++++++++++++++++++++
1 file changed, 68 insertions(+)
diff --git a/security/bpf_lsm_kfuncs.c b/security/bpf_lsm_kfuncs.c
index 43a4bf57fd31..857e9a316d1c 100644
--- a/security/bpf_lsm_kfuncs.c
+++ b/security/bpf_lsm_kfuncs.c
@@ -2,16 +2,25 @@
/* BPF kfuncs exposing LSM policy objects. */
+#include <linux/binfmts.h>
#include <linux/bpf.h>
#include <linux/btf.h>
#include <linux/btf_ids.h>
#include <linux/cfi.h>
#include <linux/init.h>
#include <linux/lsm_hooks.h>
+#include <linux/memcontrol.h>
+#include <linux/sched/mm.h>
#include <linux/security.h>
#include "lsm.h"
+/* The sleepable LSM hooks bpf_lsm_policy_apply_bprm() may be called from. */
+BTF_SET_START(bpf_lsm_policy_bprm_hooks)
+BTF_ID(func, bpf_lsm_bprm_creds_for_exec)
+BTF_ID(func, bpf_lsm_bprm_creds_from_file)
+BTF_SET_END(bpf_lsm_policy_bprm_hooks)
+
__bpf_kfunc_start_defs();
/**
@@ -44,6 +53,49 @@ bpf_lsm_policy_acquire(struct lsm_policy_object *object)
return NULL;
}
+/**
+ * bpf_lsm_policy_apply_bprm - Apply a policy object to exec credentials
+ * @object: policy object to apply
+ * @bprm: execution context providing the prepared credentials to
+ * restrict
+ * @flags: flags defined by the LSM owning @object
+ *
+ * Ask the LSM owning @object to restrict the credentials prepared in
+ * @bprm with it, so that the executed task starts confined by the
+ * policy. How the policy composes with restrictions the credentials
+ * already carry, and the meaning of @flags, are defined by the owning
+ * LSM. @object is only borrowed: the caller keeps its reference.
+ * The hook runs in a root memcg charging scope: policy the LSM
+ * computes on behalf of the program is not charged to the mediated
+ * task.
+ *
+ * Return: 0 on success, -EOPNOTSUPP if the LSM owning @object does
+ * not support applying policy to an execution, -EINVAL on unsupported
+ * @flags, other negative values on LSM-specific failures.
+ */
+__bpf_kfunc int bpf_lsm_policy_apply_bprm(struct lsm_policy_object *object,
+ struct linux_binprm *bprm, u32 flags)
+{
+ struct lsm_static_call *scall;
+ struct mem_cgroup *old_memcg;
+ int err;
+
+ lsm_for_each_hook(scall, bprm_apply_policy_object) {
+ if (scall->hl->lsmid->id != object->lsmid)
+ continue;
+ /*
+ * The hook runs on behalf of the BPF program, not of the
+ * mediated task: charge its allocations to the root memcg.
+ */
+ old_memcg = set_active_memcg(root_mem_cgroup);
+ err = scall->hl->hook.bprm_apply_policy_object(bprm, object,
+ flags);
+ set_active_memcg(old_memcg);
+ return err;
+ }
+ return -EOPNOTSUPP;
+}
+
/**
* bpf_lsm_policy_from_fd - Get an LSM policy object from a fd
* @fd: file descriptor referring to a policy object, resolved in the
@@ -115,6 +167,7 @@ __bpf_kfunc_end_defs();
BTF_KFUNCS_START(bpf_lsm_policy_kfunc_ids)
BTF_ID_FLAGS(func, bpf_lsm_policy_acquire, KF_ACQUIRE | KF_RCU | KF_RET_NULL)
+BTF_ID_FLAGS(func, bpf_lsm_policy_apply_bprm, KF_SLEEPABLE)
BTF_ID_FLAGS(func, bpf_lsm_policy_from_fd,
KF_ACQUIRE | KF_RET_NULL | KF_SLEEPABLE)
BTF_ID_FLAGS(func, bpf_lsm_policy_release, KF_RELEASE)
@@ -124,6 +177,8 @@ BTF_ID_LIST(bpf_lsm_policy_dtor_ids)
BTF_ID(struct, lsm_policy_object)
BTF_ID(func, bpf_lsm_policy_release_dtor)
+BTF_ID_LIST_SINGLE(bpf_lsm_policy_apply_bprm_ids, func,
+ bpf_lsm_policy_apply_bprm)
BTF_ID_LIST_SINGLE(bpf_lsm_policy_from_fd_ids, func, bpf_lsm_policy_from_fd)
/*
@@ -132,6 +187,12 @@ BTF_ID_LIST_SINGLE(bpf_lsm_policy_from_fd_ids, func, bpf_lsm_policy_from_fd)
* kfuncs requires a filter. A policy object fd is only meaningful in
* the fd table of the task that set the object up: the fd kfunc is
* exclusive to syscall programs, which run in that task's context.
+ * Applying policy to an execution is exclusive to the sleepable bprm
+ * LSM hooks the operation is specified for. The bprm argument's type
+ * alone does not draw that boundary: other bprm hooks and the tp_btf
+ * exec tracepoints, which share the LSM programs' kfunc bucket, also
+ * provide a trusted bprm pointer outside the window where the
+ * prepared credentials may still be updated.
*/
static int bpf_lsm_policy_kfunc_filter(const struct bpf_prog *prog,
u32 kfunc_id)
@@ -141,11 +202,18 @@ static int bpf_lsm_policy_kfunc_filter(const struct bpf_prog *prog,
switch (prog->type) {
case BPF_PROG_TYPE_SYSCALL:
+ if (kfunc_id == bpf_lsm_policy_apply_bprm_ids[0])
+ return -EACCES;
return 0;
case BPF_PROG_TYPE_LSM:
if (kfunc_id == bpf_lsm_policy_from_fd_ids[0])
return -EACCES;
+ if (kfunc_id == bpf_lsm_policy_apply_bprm_ids[0] &&
+ !btf_id_set_contains(&bpf_lsm_policy_bprm_hooks,
+ prog->aux->attach_btf_id))
+ return -EACCES;
+
return 0;
default:
return -EACCES;
--
2.55.0
^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH bpf-next v3 08/15] lsm: Document the LSM policy object interface
2026-09-09 19:37 [PATCH bpf-next v3 00/15] BPF interface for applying Landlock rulesets Justin Suess
` (6 preceding siblings ...)
2026-09-09 19:37 ` [PATCH bpf-next v3 07/15] lsm: Add the bpf_lsm_policy_apply_bprm kfunc Justin Suess
@ 2026-09-09 19:37 ` Justin Suess
2026-09-09 19:37 ` [PATCH bpf-next v3 09/15] selftests/bpf: Add tests for the LSM policy object kfuncs Justin Suess
` (6 subsequent siblings)
14 siblings, 0 replies; 29+ messages in thread
From: Justin Suess @ 2026-09-09 19:37 UTC (permalink / raw)
To: ast, daniel, andrii, kpsingh, matt, paul, mic, viro, brauner,
kees
Cc: casey, gnoack, jack, song, yonghong.song, martin.lau, eddyz87,
memxor, jolsa, m, bpf, linux-security-module, linux-kernel,
Justin Suess
Describe the split of responsibilities in lsm-development.rst: the
LSM framework owns the BPF-facing kfuncs, an LSM opts in by embedding
struct lsm_policy_object, tagged with its lsmid and an LSM-private
type, and implementing ordinary LSM hooks, and the lifetime contract
those hooks must satisfy comes from BPF's execution model.
Cc: Paul Moore <paul@paul-moore.com>
Cc: Casey Schaufler <casey@schaufler-ca.com>
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
Notes:
v2->v3:
- No change.
Documentation/security/lsm-development.rst | 49 ++++++++++++++++++++++
1 file changed, 49 insertions(+)
diff --git a/Documentation/security/lsm-development.rst b/Documentation/security/lsm-development.rst
index 5895e529da7f..190d9b8f2346 100644
--- a/Documentation/security/lsm-development.rst
+++ b/Documentation/security/lsm-development.rst
@@ -15,3 +15,52 @@ see ``security/security.c`` and associated structures:
.. kernel-doc:: security/security.c
:export:
+
+LSM policy objects and BPF kfuncs
+=================================
+
+The LSM framework implements an interface for individual LSMs to
+expose their policy through BPF kfuncs and kptrs. An LSM may not
+export any kfunc or other BPF interface directly.
+
+An LSM opts in by embedding ``struct lsm_policy_object`` in one of
+its own objects, setting its ``lsmid`` to the LSM's ``LSM_ID_*``
+value and its ``type`` to a nonzero value of the LSM's choosing, and
+implementing the policy object hooks (``policy_object_from_fd``,
+``policy_object_get``, ``policy_object_put``, and per-operation hooks
+such as ``bprm_apply_policy_object``) like any other hook, resolving
+the containing object with ``container_of()``. The ``type`` namespace
+is private to the owning LSM, which uses it to tell its own policy
+object kinds apart; the framework never interprets it, and 0 is
+reserved as "unset".
+
+The kfuncs, defined once in ``security/bpf_lsm_kfuncs.c``, dispatch
+each call on an object to the single LSM matching its ``lsmid``. The
+fd translation has no object yet: the framework offers the fd to
+every ``policy_object_from_fd`` implementation in turn, and an LSM
+declines a fd that is not one of its own with ``-EOPNOTSUPP``; any
+other error fails the translation. A program that expects a policy of
+a specific LSM can read the returned object's ``lsmid``. Either way,
+a BPF program reaches an LSM the same way every other kernel caller
+does, through an LSM hook, while the BPF verifier tracks the object
+as a referenced kptr.
+
+An LSM opting in must satisfy the lifetime contract that BPF's
+execution model imposes: the containing object is reference counted,
+``policy_object_get`` acquires with inc-not-zero semantics and fails
+once the count dropped to zero, ``policy_object_put`` may be called
+from contexts that cannot sleep (map destructors), and the object's
+memory is freed only after an RCU grace period, as programs load
+policy object kptrs from BPF maps under RCU. Hooks for operations an
+LSM does not provide are simply not implemented: the corresponding
+kfunc then fails with ``-EOPNOTSUPP`` at runtime. Whether the LSM
+providing an operation is built in and active is likewise a runtime
+property: the kfuncs are always registered when ``CONFIG_BPF_LSM`` is
+enabled, so BPF program loading is independent of the boot-time LSM
+configuration.
+
+.. kernel-doc:: security/bpf_lsm_kfuncs.c
+ :identifiers: bpf_lsm_policy_acquire
+ bpf_lsm_policy_apply_bprm
+ bpf_lsm_policy_from_fd
+ bpf_lsm_policy_release
--
2.55.0
^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH bpf-next v3 09/15] selftests/bpf: Add tests for the LSM policy object kfuncs
2026-09-09 19:37 [PATCH bpf-next v3 00/15] BPF interface for applying Landlock rulesets Justin Suess
` (7 preceding siblings ...)
2026-09-09 19:37 ` [PATCH bpf-next v3 08/15] lsm: Document the LSM policy object interface Justin Suess
@ 2026-09-09 19:37 ` Justin Suess
2026-09-09 20:30 ` bot+bpf-ci
2026-09-09 19:37 ` [PATCH bpf-next v3 10/15] landlock: Expose the ruleset fd lookup to the rest of Landlock Justin Suess
` (5 subsequent siblings)
14 siblings, 1 reply; 29+ messages in thread
From: Justin Suess @ 2026-09-09 19:37 UTC (permalink / raw)
To: ast, daniel, andrii, kpsingh, matt, paul, mic, viro, brauner,
kees
Cc: casey, gnoack, jack, song, yonghong.song, martin.lau, eddyz87,
memxor, jolsa, m, bpf, linux-security-module, linux-kernel,
Justin Suess
Test the properties of the policy object interface that hold
independently of any LSM implementing the hooks.
The failure programs pin down the verifier-side contract: the kfuncs
are rejected in tracing programs, the fd kfunc in LSM programs, the
apply kfunc in syscall programs, on non-bprm LSM hooks and in
non-sleepable programs, leaked references fail verification, and a
kptr loaded outside an RCU read-side section cannot be acquired.
The syscall program checks the runtime contract of
bpf_lsm_policy_from_fd(): a bad fd, a fd that is no LSM's policy
object, and a nonzero value of the reserved flags all resolve to
NULL.
Exercising the kfuncs against an LSM actually providing policy
objects is left to that LSM's own tests.
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
Notes:
v2->v3:
- No change.
.../bpf/prog_tests/lsm_policy_kfuncs.c | 54 ++++++
.../selftests/bpf/progs/lsm_policy_kfuncs.c | 52 ++++++
.../bpf/progs/lsm_policy_kfuncs_failure.c | 154 ++++++++++++++++++
3 files changed, 260 insertions(+)
create mode 100644 tools/testing/selftests/bpf/prog_tests/lsm_policy_kfuncs.c
create mode 100644 tools/testing/selftests/bpf/progs/lsm_policy_kfuncs.c
create mode 100644 tools/testing/selftests/bpf/progs/lsm_policy_kfuncs_failure.c
diff --git a/tools/testing/selftests/bpf/prog_tests/lsm_policy_kfuncs.c b/tools/testing/selftests/bpf/prog_tests/lsm_policy_kfuncs.c
new file mode 100644
index 000000000000..9f4ffb5f47be
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/lsm_policy_kfuncs.c
@@ -0,0 +1,54 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright © 2026 Justin Suess <utilityemal77@gmail.com> */
+
+#include <test_progs.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "lsm_policy_kfuncs.skel.h"
+#include "lsm_policy_kfuncs_failure.skel.h"
+
+/*
+ * Runtime contract of bpf_lsm_policy_from_fd(), independent of any
+ * LSM implementing the policy object hooks: a bad fd, a fd that is no
+ * LSM's policy object, and a nonzero value of the reserved flags all
+ * resolve to NULL.
+ */
+static void test_from_fd_null(void)
+{
+ LIBBPF_OPTS(bpf_test_run_opts, opts);
+ struct lsm_policy_kfuncs *skel;
+ char tmp_path[] = "/tmp/lsm_policy_kfuncs_XXXXXX";
+ int tmp_fd, err;
+
+ tmp_fd = mkstemp(tmp_path);
+ if (!ASSERT_GE(tmp_fd, 0, "mkstemp"))
+ return;
+
+ skel = lsm_policy_kfuncs__open_and_load();
+ if (!ASSERT_OK_PTR(skel, "skel_open_and_load"))
+ goto out_close;
+ skel->bss->plain_fd = tmp_fd;
+
+ err = bpf_prog_test_run_opts(bpf_program__fd(skel->progs.check_from_fd),
+ &opts);
+ if (!ASSERT_OK(err, "check_from_fd_run") ||
+ !ASSERT_OK(opts.retval, "check_from_fd_retval"))
+ goto out_destroy;
+
+ ASSERT_TRUE(skel->bss->got_null_for_bad_fd, "bad_fd_null");
+ ASSERT_TRUE(skel->bss->got_null_for_plain_fd, "plain_fd_null");
+ ASSERT_TRUE(skel->bss->got_null_for_bad_flags, "bad_flags_null");
+out_destroy:
+ lsm_policy_kfuncs__destroy(skel);
+out_close:
+ close(tmp_fd);
+ unlink(tmp_path);
+}
+
+void test_lsm_policy_kfuncs(void)
+{
+ if (test__start_subtest("from_fd_null"))
+ test_from_fd_null();
+ RUN_TESTS(lsm_policy_kfuncs_failure);
+}
diff --git a/tools/testing/selftests/bpf/progs/lsm_policy_kfuncs.c b/tools/testing/selftests/bpf/progs/lsm_policy_kfuncs.c
new file mode 100644
index 000000000000..f084ccfcde91
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/lsm_policy_kfuncs.c
@@ -0,0 +1,52 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright © 2026 Justin Suess <utilityemal77@gmail.com> */
+
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+
+char _license[] SEC("license") = "GPL";
+
+extern struct lsm_policy_object *
+bpf_lsm_policy_from_fd(int fd, u32 flags) __ksym;
+extern void bpf_lsm_policy_release(struct lsm_policy_object *object) __ksym;
+
+int plain_fd;
+bool got_null_for_bad_fd;
+bool got_null_for_plain_fd;
+bool got_null_for_bad_flags;
+
+/*
+ * Runs in the test runner's context through BPF_PROG_RUN, where
+ * @plain_fd is meaningful.
+ */
+SEC("syscall")
+int check_from_fd(void *ctx)
+{
+ struct lsm_policy_object *object;
+
+ /* A fd not open in this task's fd table must resolve to NULL. */
+ object = bpf_lsm_policy_from_fd(-1, 0);
+ if (!object)
+ got_null_for_bad_fd = true;
+ else
+ bpf_lsm_policy_release(object);
+
+ /*
+ * A valid fd that is not any LSM's policy object must be
+ * declined by every LSM and resolve to NULL.
+ */
+ object = bpf_lsm_policy_from_fd(plain_fd, 0);
+ if (!object)
+ got_null_for_plain_fd = true;
+ else
+ bpf_lsm_policy_release(object);
+
+ /* The flags are reserved: any nonzero value must resolve to NULL. */
+ object = bpf_lsm_policy_from_fd(plain_fd, 1);
+ if (!object)
+ got_null_for_bad_flags = true;
+ else
+ bpf_lsm_policy_release(object);
+
+ return 0;
+}
diff --git a/tools/testing/selftests/bpf/progs/lsm_policy_kfuncs_failure.c b/tools/testing/selftests/bpf/progs/lsm_policy_kfuncs_failure.c
new file mode 100644
index 000000000000..04080838aefd
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/lsm_policy_kfuncs_failure.c
@@ -0,0 +1,154 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright © 2026 Justin Suess <utilityemal77@gmail.com> */
+
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+#include "bpf_misc.h"
+
+char _license[] SEC("license") = "GPL";
+
+extern struct lsm_policy_object *
+bpf_lsm_policy_acquire(struct lsm_policy_object *object) __ksym;
+extern int bpf_lsm_policy_apply_bprm(struct lsm_policy_object *object,
+ struct linux_binprm *bprm,
+ u32 flags) __ksym;
+extern struct lsm_policy_object *
+bpf_lsm_policy_from_fd(int fd, u32 flags) __ksym;
+extern void bpf_lsm_policy_release(struct lsm_policy_object *object) __ksym;
+void bpf_rcu_read_lock(void) __ksym;
+void bpf_rcu_read_unlock(void) __ksym;
+
+struct policy_slot {
+ struct lsm_policy_object __kptr *object;
+};
+
+struct {
+ __uint(type, BPF_MAP_TYPE_ARRAY);
+ __uint(max_entries, 1);
+ __type(key, int);
+ __type(value, struct policy_slot);
+} policy_map SEC(".maps");
+
+/*
+ * The LSM policy kfuncs are limited to LSM and syscall programs by
+ * the BPF-side kfunc filter: a tracing program calling one must fail
+ * verification.
+ */
+SEC("tp_btf/task_newtask")
+__failure __msg("calling kernel function bpf_lsm_policy_from_fd is not allowed")
+int BPF_PROG(tracing_prog, struct task_struct *task, u64 clone_flags)
+{
+ struct lsm_policy_object *object;
+
+ object = bpf_lsm_policy_from_fd(-1, 0);
+ if (object)
+ bpf_lsm_policy_release(object);
+ return 0;
+}
+
+/*
+ * The fd kfunc is exclusive to syscall programs: it must be rejected
+ * in an LSM program, even on an allowed hook.
+ */
+SEC("lsm.s/bprm_creds_for_exec")
+__failure __msg("calling kernel function bpf_lsm_policy_from_fd is not allowed")
+int BPF_PROG(lsm_get, struct linux_binprm *bprm)
+{
+ struct lsm_policy_object *object;
+
+ object = bpf_lsm_policy_from_fd(-1, 0);
+ if (object)
+ bpf_lsm_policy_release(object);
+ return 0;
+}
+
+/*
+ * The enforcement kfunc is exclusive to the sleepable bprm LSM
+ * hooks: it must be rejected in a syscall program.
+ */
+SEC("syscall")
+__failure __msg("calling kernel function bpf_lsm_policy_apply_bprm is not allowed")
+int syscall_restrict(void *ctx)
+{
+ return bpf_lsm_policy_apply_bprm(NULL, NULL, 0);
+}
+
+/*
+ * Any LSM attach point other than the sleepable bprm hooks must be
+ * rejected for the enforcement kfunc.
+ */
+SEC("lsm.s/file_open")
+__failure __msg("calling kernel function bpf_lsm_policy_apply_bprm is not allowed")
+int BPF_PROG(wrong_hook, struct file *file)
+{
+ return bpf_lsm_policy_apply_bprm(NULL, NULL, 0);
+}
+
+/*
+ * The enforcement kfunc may sleep: a non-sleepable program on an
+ * allowed hook must be rejected.
+ */
+SEC("lsm/bprm_creds_for_exec")
+__failure
+__msg("program must be sleepable to call sleepable kfunc bpf_lsm_policy_apply_bprm")
+int BPF_PROG(nonsleepable_prog, struct linux_binprm *bprm)
+{
+ return bpf_lsm_policy_apply_bprm(NULL, bprm, 0);
+}
+
+/* An acquired policy object reference must be released before returning. */
+SEC("syscall")
+__failure __msg("Unreleased reference")
+int leak_policy(void *ctx)
+{
+ bpf_lsm_policy_from_fd(-1, 0);
+ return 0;
+}
+
+/*
+ * A kptr loaded outside an RCU read-side critical section is
+ * untrusted: the acquire kfunc must reject it.
+ */
+SEC("lsm.s/file_open")
+__failure __msg("must be a rcu pointer")
+int BPF_PROG(acquire_untrusted, struct file *file)
+{
+ struct lsm_policy_object *object;
+ struct policy_slot *slot;
+ int key = 0;
+
+ slot = bpf_map_lookup_elem(&policy_map, &key);
+ if (!slot)
+ return 0;
+
+ object = slot->object;
+ if (!object)
+ return 0;
+
+ object = bpf_lsm_policy_acquire(object);
+ if (object)
+ bpf_lsm_policy_release(object);
+ return 0;
+}
+
+/* A reference acquired from a shared policy object must be released too. */
+SEC("lsm.s/file_open")
+__failure __msg("Unreleased reference")
+int BPF_PROG(leak_shared_policy, struct file *file)
+{
+ struct lsm_policy_object *object;
+ struct policy_slot *slot;
+ int key = 0;
+
+ slot = bpf_map_lookup_elem(&policy_map, &key);
+ if (!slot)
+ return 0;
+
+ bpf_rcu_read_lock();
+ object = slot->object;
+ if (object)
+ object = bpf_lsm_policy_acquire(object);
+ bpf_rcu_read_unlock();
+ return 0;
+}
--
2.55.0
^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH bpf-next v3 10/15] landlock: Expose the ruleset fd lookup to the rest of Landlock
2026-09-09 19:37 [PATCH bpf-next v3 00/15] BPF interface for applying Landlock rulesets Justin Suess
` (8 preceding siblings ...)
2026-09-09 19:37 ` [PATCH bpf-next v3 09/15] selftests/bpf: Add tests for the LSM policy object kfuncs Justin Suess
@ 2026-09-09 19:37 ` Justin Suess
2026-09-09 19:37 ` [PATCH bpf-next v3 11/15] landlock: Factor the credential restriction out of landlock_restrict_self() Justin Suess
` (4 subsequent siblings)
14 siblings, 0 replies; 29+ messages in thread
From: Justin Suess @ 2026-09-09 19:37 UTC (permalink / raw)
To: ast, daniel, andrii, kpsingh, matt, paul, mic, viro, brauner,
kees
Cc: casey, gnoack, jack, song, yonghong.song, martin.lau, eddyz87,
memxor, jolsa, m, bpf, linux-security-module, linux-kernel,
Justin Suess
Rename get_ruleset_from_fd() to landlock_get_ruleset_from_fd() and
give it external linkage within Landlock, declared in ruleset.h next
to the other ruleset lifetime helpers.
A following commit implements the LSM kfunc policy hooks, which need
to translate a ruleset fd into a landlock_ruleset reference from
outside syscalls.c. No behavioral change.
Cc: Mickaël Salaün <mic@digikod.net>
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
Notes:
v2->v3:
- No change.
security/landlock/ruleset.h | 3 +++
security/landlock/syscalls.c | 9 +++++----
2 files changed, 8 insertions(+), 4 deletions(-)
diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h
index b536fa0425b7..b58e3d9846af 100644
--- a/security/landlock/ruleset.h
+++ b/security/landlock/ruleset.h
@@ -214,6 +214,9 @@ int landlock_store_rule(struct landlock_rules *const rules,
void landlock_free_rules(struct landlock_rules *const rules);
+struct landlock_ruleset *landlock_get_ruleset_from_fd(const int fd,
+ const fmode_t mode);
+
/**
* landlock_get_rule_root - Get the root of a rule tree by key type
*
diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c
index 1d02d57f4c48..cb294a3582ae 100644
--- a/security/landlock/syscalls.c
+++ b/security/landlock/syscalls.c
@@ -305,8 +305,8 @@ SYSCALL_DEFINE3(landlock_create_ruleset,
* Returns an owned ruleset from a FD. It is thus needed to call
* landlock_put_ruleset() on the return value.
*/
-static struct landlock_ruleset *get_ruleset_from_fd(const int fd,
- const fmode_t mode)
+struct landlock_ruleset *landlock_get_ruleset_from_fd(const int fd,
+ const fmode_t mode)
{
CLASS(fd, ruleset_f)(fd);
struct landlock_ruleset *ruleset;
@@ -486,7 +486,7 @@ SYSCALL_DEFINE4(landlock_add_rule, const int, ruleset_fd,
return -EINVAL;
/* Gets and checks the ruleset. */
- ruleset = get_ruleset_from_fd(ruleset_fd, FMODE_CAN_WRITE);
+ ruleset = landlock_get_ruleset_from_fd(ruleset_fd, FMODE_CAN_WRITE);
if (IS_ERR(ruleset))
return PTR_ERR(ruleset);
@@ -585,7 +585,8 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
(flags & ~LANDLOCK_RESTRICT_SELF_TSYNC) ==
LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)) {
/* Gets and checks the ruleset. */
- ruleset = get_ruleset_from_fd(ruleset_fd, FMODE_CAN_READ);
+ ruleset = landlock_get_ruleset_from_fd(ruleset_fd,
+ FMODE_CAN_READ);
if (IS_ERR(ruleset))
return PTR_ERR(ruleset);
}
--
2.55.0
^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH bpf-next v3 11/15] landlock: Factor the credential restriction out of landlock_restrict_self()
2026-09-09 19:37 [PATCH bpf-next v3 00/15] BPF interface for applying Landlock rulesets Justin Suess
` (9 preceding siblings ...)
2026-09-09 19:37 ` [PATCH bpf-next v3 10/15] landlock: Expose the ruleset fd lookup to the rest of Landlock Justin Suess
@ 2026-09-09 19:37 ` Justin Suess
2026-09-09 20:29 ` bot+bpf-ci
2026-09-09 19:37 ` [PATCH bpf-next v3 12/15] landlock: Free rulesets after an RCU grace period Justin Suess
` (3 subsequent siblings)
14 siblings, 1 reply; 29+ messages in thread
From: Justin Suess @ 2026-09-09 19:37 UTC (permalink / raw)
To: ast, daniel, andrii, kpsingh, matt, paul, mic, viro, brauner,
kees
Cc: casey, gnoack, jack, song, yonghong.song, martin.lau, eddyz87,
memxor, jolsa, m, bpf, linux-security-module, linux-kernel,
Justin Suess
Split the core of landlock_restrict_self() into two credential
helpers:
landlock_prepare_restriction() - translate the
landlock_restrict_self(2) flags, merge the ruleset with the
credentials' domain, and configure the new domain's log state,
producing a struct landlock_restriction: the complete new state
that the enforcement gives to a credential.
landlock_apply_restriction() - enforce a computed restriction on
credentials exclusively owned by the caller. This step cannot
fail, so a caller may run it past its last point of failure.
The merge runs under @ruleset->lock and the landlock_create_domain
trace event is emitted before the lock is released, exactly as in the
syscall before this change: the event still observes the ruleset
snapshot that was merged, and it still fires before any thread-sync
wait. Committing the hierarchy out of LANDLOCK_LOG_UNCOMMITTED moves
along with it, so every domain computed by
landlock_prepare_restriction() has its create/free trace events
balanced, whether or not it ends up enforced.
The syscall behaves exactly as before: prepare and apply run back to
back on the prepared credentials. The no_new_privs/CAP_SYS_ADMIN
precheck, the flag mask check, the TSYNC handling, and the
landlock_enforce_domain trace event are syscall policy and stay in
place.
The point of the split is that application is decoupled from
computation: a following commit restricts an execution from a BPF
kfunc by staging a prepared restriction in the binprm credentials and
applying it at the exec point of no return, with the flag
translation, domain merge, and log configuration in one shared place.
The restriction records the flags it was computed with instead of
translating them into per-flag fields: consumers read the staged
flags at application time, so a future flag that must be honored at
enforcement travels with the restriction automatically, with no
per-flag plumbing in the callers.
Cc: Mickaël Salaün <mic@digikod.net>
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
Notes:
v2->v3:
- No change.
security/landlock/cred.c | 132 +++++++++++++++++++++++++++++++++++
security/landlock/cred.h | 31 ++++++++
security/landlock/syscalls.c | 96 ++++---------------------
3 files changed, 178 insertions(+), 81 deletions(-)
diff --git a/security/landlock/cred.c b/security/landlock/cred.c
index 03449c26247e..f02706f12c7d 100644
--- a/security/landlock/cred.c
+++ b/security/landlock/cred.c
@@ -8,14 +8,146 @@
*/
#include <linux/binfmts.h>
+#include <linux/bits.h>
#include <linux/cred.h>
+#include <linux/err.h>
+#include <linux/errno.h>
#include <linux/lsm_hooks.h>
+#include <linux/mutex.h>
+#include <uapi/linux/landlock.h>
#include "common.h"
#include "cred.h"
+#include "domain.h"
#include "ruleset.h"
#include "setup.h"
+#include <trace/events/landlock.h>
+
+/**
+ * landlock_prepare_restriction - Compute a credential restriction
+ *
+ * @llcred: Landlock credentials to restrict: provides the parent domain and
+ * the previous log configuration. Not modified.
+ * @ruleset: Ruleset to enforce, or NULL for a log-configuration-only change.
+ * @flags: landlock_restrict_self(2) flags. The caller is responsible for
+ * validating them against the set of flags it supports.
+ * @restriction: Computed restriction. On success, holds a reference on
+ * @restriction->domain (if any), which
+ * landlock_apply_restriction() transfers to the restricted
+ * credentials.
+ *
+ * The restriction builds on @llcred's current state: the caller must apply
+ * it to (or stage it for) these same credentials.
+ *
+ * Return: 0 on success, -errno on failure.
+ */
+int landlock_prepare_restriction(
+ const struct landlock_cred_security *const llcred,
+ struct landlock_ruleset *const ruleset, const u32 flags,
+ struct landlock_restriction *const restriction)
+{
+ struct landlock_domain *new_dom;
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
+ /* Translates "off" and "on" flags to booleans. */
+ const bool log_same_exec =
+ !(flags & LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF);
+ const bool log_new_exec =
+ !!(flags & LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON);
+ const bool prev_log_subdomains = !llcred->log_subdomains_off;
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
+
+ *restriction = (struct landlock_restriction){
+ .flags = flags,
+ };
+
+ if (!ruleset)
+ return 0;
+
+ mutex_lock(&ruleset->lock);
+ new_dom = landlock_merge_ruleset(llcred->domain, ruleset);
+ if (IS_ERR(new_dom)) {
+ mutex_unlock(&ruleset->lock);
+ return PTR_ERR(new_dom);
+ }
+ /*
+ * Emits the domain-creation event while @ruleset->lock is still
+ * held, right after the merge, so an eBPF program attached to
+ * the tracepoint reads the exact ruleset that was merged into
+ * the domain: a consistent snapshot that a concurrent
+ * landlock_add_rule() (which holds the same lock) cannot
+ * modify.
+ *
+ * This must not be delayed past the return of this function.
+ * Holding @ruleset->lock across
+ * landlock_restrict_sibling_threads() would hang: a sibling
+ * thread blocked in landlock_add_rule() on the same
+ * @ruleset->lock cannot run the task_work that thread-sync
+ * waits for (the lock wait is uninterruptible). Emitting here
+ * keeps the lock off the thread-sync path.
+ *
+ * The trade-off is that the event fires for a domain that may
+ * never be enforced: a later (rare) thread-sync failure or an
+ * aborted execution drops it. Those paths free the domain,
+ * which emits the matching free_domain event so the create/free
+ * pair stays balanced.
+ */
+ trace_landlock_create_domain(new_dom, ruleset);
+ mutex_unlock(&ruleset->lock);
+
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
+ new_dom->hierarchy->log_same_exec = log_same_exec;
+ new_dom->hierarchy->log_new_exec = log_new_exec;
+ /*
+ * The creation event fired above, so move the domain out of
+ * LANDLOCK_LOG_UNCOMMITTED: its free_domain event must fire
+ * too, even if the domain is dropped before being enforced.
+ * Audit logging may still be disabled (DISABLED); tracing
+ * observes it anyway.
+ */
+ if ((!log_same_exec && !log_new_exec) || !prev_log_subdomains)
+ new_dom->hierarchy->log_status = LANDLOCK_LOG_DISABLED;
+ else
+ new_dom->hierarchy->log_status = LANDLOCK_LOG_PENDING;
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
+
+ restriction->domain = new_dom;
+ return 0;
+}
+
+/**
+ * landlock_apply_restriction - Enforce a computed restriction on credentials
+ *
+ * @llcred: Landlock credentials to restrict, exclusively owned by the caller
+ * (prepared and not yet committed).
+ * @restriction: Restriction computed by landlock_prepare_restriction()
+ * against the same credential state; its domain reference is
+ * transferred to @llcred.
+ *
+ * Cannot fail, so that a caller may apply a restriction past its last point
+ * of failure, e.g. an exec point of no return.
+ */
+void landlock_apply_restriction(struct landlock_cred_security *const llcred,
+ struct landlock_restriction *const restriction)
+{
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
+ if (restriction->flags & LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)
+ llcred->log_subdomains_off = true;
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
+
+ if (!restriction->domain)
+ return;
+
+ /* Replaces the old domain. */
+ landlock_put_domain(llcred->domain);
+ llcred->domain = restriction->domain;
+ restriction->domain = NULL;
+
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
+ llcred->domain_exec |= BIT(llcred->domain->num_layers - 1);
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
+}
+
static void hook_cred_transfer(struct cred *const new,
const struct cred *const old)
{
diff --git a/security/landlock/cred.h b/security/landlock/cred.h
index a5ff9957949a..88fa97fc3bd2 100644
--- a/security/landlock/cred.h
+++ b/security/landlock/cred.h
@@ -21,6 +21,29 @@
#include "ruleset.h"
#include "setup.h"
+/**
+ * struct landlock_restriction - Computed credential restriction
+ *
+ * The result of landlock_prepare_restriction(): the new state that
+ * enforcing a ruleset with a set of landlock_restrict_self(2) flags
+ * gives to a credential, decoupled from its application. It is
+ * enforced with landlock_apply_restriction(), either right away
+ * (landlock_restrict_self(2)) or after a staging period (restriction
+ * of an execution).
+ */
+struct landlock_restriction {
+ /**
+ * @domain: New domain to enforce, owning a reference. NULL if the
+ * restriction only carries a log configuration change.
+ */
+ struct landlock_domain *domain;
+ /**
+ * @flags: landlock_restrict_self(2) flags the restriction was
+ * computed with, validated by the caller.
+ */
+ u32 flags;
+};
+
/**
* struct landlock_cred_security - Credential security blob
*
@@ -152,6 +175,14 @@ landlock_get_applicable_subject(const struct cred *const cred,
return NULL;
}
+int landlock_prepare_restriction(
+ const struct landlock_cred_security *const llcred,
+ struct landlock_ruleset *const ruleset, const u32 flags,
+ struct landlock_restriction *const restriction);
+
+void landlock_apply_restriction(struct landlock_cred_security *const llcred,
+ struct landlock_restriction *const restriction);
+
__init void landlock_add_cred_hooks(void);
#endif /* _SECURITY_LANDLOCK_CRED_H */
diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c
index cb294a3582ae..9451376ccf50 100644
--- a/security/landlock/syscalls.c
+++ b/security/landlock/syscalls.c
@@ -9,7 +9,6 @@
#include <asm/current.h>
#include <linux/anon_inodes.h>
-#include <linux/bitops.h>
#include <linux/build_bug.h>
#include <linux/capability.h>
#include <linux/cleanup.h>
@@ -31,7 +30,6 @@
#include <uapi/linux/landlock.h>
#include "cred.h"
-#include "domain.h"
#include "fs.h"
#include "limits.h"
#include "net.h"
@@ -546,10 +544,9 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
struct landlock_ruleset *ruleset __free(landlock_put_ruleset) = NULL;
struct landlock_domain *new_dom = NULL;
struct cred *new_cred;
- struct landlock_cred_security *new_llcred;
+ struct landlock_restriction restriction;
bool process_wide;
- bool __maybe_unused log_same_exec, log_new_exec, log_subdomains,
- prev_log_subdomains;
+ int err;
if (!is_initialized())
return -EOPNOTSUPP;
@@ -568,13 +565,6 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
!ns_capable_noaudit(current_user_ns(), CAP_SYS_ADMIN))
return -EPERM;
- /* Translates "off" flag to boolean. */
- log_same_exec = !(flags & LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF);
- /* Translates "on" flag to boolean. */
- log_new_exec = !!(flags & LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON);
- /* Translates "off" flag to boolean. */
- log_subdomains = !(flags & LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF);
-
/*
* It is allowed to set LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF with
* -1 as ruleset_fd, optionally combined with
@@ -596,85 +586,29 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
if (!new_cred)
return -ENOMEM;
- new_llcred = landlock_cred(new_cred);
-
-#ifdef CONFIG_SECURITY_LANDLOCK_LOG
- prev_log_subdomains = !new_llcred->log_subdomains_off;
- new_llcred->log_subdomains_off = !prev_log_subdomains ||
- !log_subdomains;
-#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
-
/*
* The only case when a ruleset may not be set is if
* LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF is set (optionally with
* LANDLOCK_RESTRICT_SELF_TSYNC) and ruleset_fd is -1. We could
* optimize this case by not calling commit_creds() if this flag was
* already set, but it is not worth the complexity.
+ *
+ * There is no possible race condition while copying and manipulating
+ * the current credentials because they are dedicated per thread.
*/
- if (ruleset) {
- /*
- * There is no possible race condition while copying and
- * manipulating the current credentials because they are
- * dedicated per thread.
- */
- mutex_lock(&ruleset->lock);
- new_dom = landlock_merge_ruleset(new_llcred->domain, ruleset);
- if (IS_ERR(new_dom)) {
- mutex_unlock(&ruleset->lock);
- abort_creds(new_cred);
- return PTR_ERR(new_dom);
- }
- /*
- * Emits the domain-creation event while @ruleset->lock is still
- * held, right after the merge, so an eBPF program attached to
- * the tracepoint reads the exact ruleset that was merged into
- * the domain: a consistent snapshot that a concurrent
- * landlock_add_rule() (which holds the same lock) cannot
- * modify.
- *
- * This must come before the thread-sync wait below. Holding
- * @ruleset->lock across landlock_restrict_sibling_threads()
- * would hang: a sibling thread blocked in landlock_add_rule()
- * on the same @ruleset->lock cannot run the task_work that
- * thread-sync waits for (the lock wait is uninterruptible).
- * Emitting here keeps the lock off the thread-sync path.
- *
- * The trade-off is that the event fires for a domain that a
- * later (rare) thread-sync failure aborts. That path emits the
- * matching free_domain event so the create/free pair stays
- * balanced (see the thread-sync error path below).
- */
- trace_landlock_create_domain(new_dom, ruleset);
- mutex_unlock(&ruleset->lock);
-
-#ifdef CONFIG_SECURITY_LANDLOCK_LOG
- new_dom->hierarchy->log_same_exec = log_same_exec;
- new_dom->hierarchy->log_new_exec = log_new_exec;
- /*
- * The creation event fired above, so move the domain out of
- * LANDLOCK_LOG_UNCOMMITTED: its free_domain event must fire
- * too, even if a thread-sync failure aborts it below. Audit
- * logging may still be disabled (DISABLED); tracing observes it
- * anyway.
- */
- if ((!log_same_exec && !log_new_exec) || !prev_log_subdomains)
- new_dom->hierarchy->log_status = LANDLOCK_LOG_DISABLED;
- else
- new_dom->hierarchy->log_status = LANDLOCK_LOG_PENDING;
-#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
-
- /* Replaces the old (prepared) domain. */
- landlock_put_domain(new_llcred->domain);
- new_llcred->domain = new_dom;
-
-#ifdef CONFIG_SECURITY_LANDLOCK_LOG
- new_llcred->domain_exec |= BIT(new_dom->num_layers - 1);
-#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
+ err = landlock_prepare_restriction(landlock_cred(new_cred), ruleset,
+ flags, &restriction);
+ if (err) {
+ abort_creds(new_cred);
+ return err;
}
+ new_dom = restriction.domain;
+ landlock_apply_restriction(landlock_cred(new_cred), &restriction);
+
if (flags & LANDLOCK_RESTRICT_SELF_TSYNC) {
- const int err = landlock_restrict_sibling_threads(
- current_cred(), new_cred, flags);
+ err = landlock_restrict_sibling_threads(current_cred(), new_cred,
+ flags);
if (err) {
/*
* Thread-sync failed (rare), so the new domain is
--
2.55.0
^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH bpf-next v3 12/15] landlock: Free rulesets after an RCU grace period
2026-09-09 19:37 [PATCH bpf-next v3 00/15] BPF interface for applying Landlock rulesets Justin Suess
` (10 preceding siblings ...)
2026-09-09 19:37 ` [PATCH bpf-next v3 11/15] landlock: Factor the credential restriction out of landlock_restrict_self() Justin Suess
@ 2026-09-09 19:37 ` Justin Suess
2026-09-09 20:46 ` bot+bpf-ci
2026-09-09 19:37 ` [PATCH bpf-next v3 13/15] landlock: Implement the LSM policy object hooks Justin Suess
` (2 subsequent siblings)
14 siblings, 1 reply; 29+ messages in thread
From: Justin Suess @ 2026-09-09 19:37 UTC (permalink / raw)
To: ast, daniel, andrii, kpsingh, matt, paul, mic, viro, brauner,
kees
Cc: casey, gnoack, jack, song, yonghong.song, martin.lau, eddyz87,
memxor, jolsa, m, bpf, linux-security-module, linux-kernel,
Justin Suess
Defer every ruleset free behind an RCU grace period, and keep the
fields that stay readable while a free is pending out of the union
that overlays the deferred-free work item.
The policy_object_get LSM hook lets a caller holding only an
RCU-protected pointer to a ruleset (e.g. loaded from a BPF map kptr
field under rcu_read_lock()) race a refcount_inc_not_zero() against
the drop of the last reference. For that to be sound, the ruleset's
memory, and its reference count in particular, must remain valid
until every RCU reader that could still observe the pointer is done:
free the ruleset through queue_rcu_work(), which waits for a grace
period before running the free work.
The work item is overlaid with the fields that no one may touch once
@usage reaches zero: @lock, @quiet_masks and @handled_masks. @usage
itself stays outside the union so a racing reader observes zero
instead of the work item's bytes, and the tracing fields @version and
@id stay outside too because the landlock_free_ruleset trace event
reads them when the queued work finally runs.
Since queueing the work never sleeps, the might_sleep() annotation
is dropped: a following commit releases ruleset references from
BPF object destructors that cannot sleep.
Cc: Mickaël Salaün <mic@digikod.net>
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
Notes:
v2->v3:
- No change.
security/landlock/ruleset.c | 24 ++++++++++++++---
security/landlock/ruleset.h | 54 ++++++++++++++++++++++++-------------
2 files changed, 57 insertions(+), 21 deletions(-)
diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
index 0d07707523cd..00a6b9938fd1 100644
--- a/security/landlock/ruleset.c
+++ b/security/landlock/ruleset.c
@@ -21,6 +21,7 @@
#include <linux/refcount.h>
#include <linux/slab.h>
#include <linux/spinlock.h>
+#include <linux/workqueue.h>
#include <uapi/linux/landlock.h>
#include "access.h"
@@ -346,9 +347,26 @@ static void free_ruleset(struct landlock_ruleset *const ruleset)
kfree(ruleset);
}
+static void free_ruleset_work(struct work_struct *const work)
+{
+ struct landlock_ruleset *ruleset;
+
+ ruleset = container_of(to_rcu_work(work), struct landlock_ruleset,
+ work_free);
+ free_ruleset(ruleset);
+}
+
+/*
+ * RCU readers (cf. the policy_object_get LSM hook) may call
+ * refcount_inc_not_zero() on a ruleset they hold no reference to: the memory
+ * must survive a grace period after the last put. Queueing the free also
+ * makes this callable from contexts that cannot sleep (cf. the
+ * policy_object_put LSM hook).
+ */
void landlock_put_ruleset(struct landlock_ruleset *const ruleset)
{
- might_sleep();
- if (ruleset && refcount_dec_and_test(&ruleset->usage))
- free_ruleset(ruleset);
+ if (ruleset && refcount_dec_and_test(&ruleset->usage)) {
+ INIT_RCU_WORK(&ruleset->work_free, free_ruleset_work);
+ queue_rcu_work(system_dfl_wq, &ruleset->work_free);
+ }
}
diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h
index b58e3d9846af..1465f8a5c464 100644
--- a/security/landlock/ruleset.h
+++ b/security/landlock/ruleset.h
@@ -15,6 +15,7 @@
#include <linux/mutex.h>
#include <linux/rbtree.h>
#include <linux/refcount.h>
+#include <linux/workqueue.h>
#include "access.h"
#include "limits.h"
@@ -157,12 +158,10 @@ struct landlock_ruleset {
*/
struct landlock_rules rules;
/**
- * @lock: Protects against concurrent modifications of @rules, if @usage
- * is greater than zero.
- */
- struct mutex lock;
- /**
- * @usage: Number of file descriptors referencing this ruleset.
+ * @usage: Number of file descriptors referencing this ruleset. Kept
+ * outside the union with @work_free: RCU readers may still call
+ * refcount_inc_not_zero() while a queued free waits out the grace
+ * period.
*/
refcount_t usage;
@@ -175,22 +174,41 @@ struct landlock_ruleset {
*/
u32 version;
/**
- * @id: Unique identifier for this ruleset, used for tracing.
+ * @id: Unique identifier for this ruleset, used for tracing. Kept
+ * outside the union with @work_free: the free_ruleset trace event
+ * reads it after the free has been queued.
*/
u64 id;
#endif /* CONFIG_TRACEPOINTS */
- /**
- * @quiet_masks: Stores the quiet flags for an unmerged ruleset. For a
- * merged domain, this is stored in each layer's struct
- * landlock_hierarchy instead.
- */
- struct access_masks quiet_masks;
- /**
- * @handled_masks: Contains the subset of filesystem and network actions
- * that are handled by this ruleset.
- */
- struct access_masks handled_masks;
+ union {
+ /**
+ * @work_free: Enables to free a ruleset after an RCU grace
+ * period, within a lockless section. This is queued by
+ * landlock_put_ruleset() when @usage reaches zero. The
+ * fields @lock, @quiet_masks and @handled_masks are then
+ * unused.
+ */
+ struct rcu_work work_free;
+ struct {
+ /**
+ * @lock: Protects against concurrent modifications of
+ * @rules, if @usage is greater than zero.
+ */
+ struct mutex lock;
+ /**
+ * @quiet_masks: Stores the quiet flags for an unmerged
+ * ruleset. For a merged domain, this is stored in each
+ * layer's struct landlock_hierarchy instead.
+ */
+ struct access_masks quiet_masks;
+ /**
+ * @handled_masks: Contains the subset of filesystem and
+ * network actions that are handled by this ruleset.
+ */
+ struct access_masks handled_masks;
+ };
+ };
};
struct landlock_ruleset *
--
2.55.0
^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH bpf-next v3 13/15] landlock: Implement the LSM policy object hooks
2026-09-09 19:37 [PATCH bpf-next v3 00/15] BPF interface for applying Landlock rulesets Justin Suess
` (11 preceding siblings ...)
2026-09-09 19:37 ` [PATCH bpf-next v3 12/15] landlock: Free rulesets after an RCU grace period Justin Suess
@ 2026-09-09 19:37 ` Justin Suess
2026-09-09 20:46 ` bot+bpf-ci
2026-09-09 19:37 ` [PATCH bpf-next v3 14/15] selftests/bpf: Test the LSM policy object kfuncs with Landlock Justin Suess
2026-09-09 19:37 ` [PATCH bpf-next v3 15/15] landlock: Document the BPF policy interface Justin Suess
14 siblings, 1 reply; 29+ messages in thread
From: Justin Suess @ 2026-09-09 19:37 UTC (permalink / raw)
To: ast, daniel, andrii, kpsingh, matt, paul, mic, viro, brauner,
kees
Cc: casey, gnoack, jack, song, yonghong.song, martin.lau, eddyz87,
memxor, jolsa, m, bpf, linux-security-module, linux-kernel,
Justin Suess
Implement the generic LSM hooks exposing Landlock rulesets as policy
objects to BPF. The new code is gated on CONFIG_BPF_LSM, the only
configuration where the kfuncs calling the hooks exist.
struct lsm_policy_object is embedded in struct landlock_ruleset and
tagged with LSM_ID_LANDLOCK and LANDLOCK_POLICY_TYPE_RULESET at
ruleset creation; the hooks resolve the ruleset with container_of()
and no Landlock type crosses the LSM boundary. The type namespace is
private to Landlock and routes the container_of() resolution once
several policy object types exist: the consuming hooks reject an
object of an unexpected type with -EINVAL (the put hook, which cannot
fail, warns instead), while from_fd only produces objects and needs
no check. The embedded object sits outside the union overlaying the
deferred-free work item: an RCU reader may still read its identity
while a queued free waits out the grace period.
- policy_object_from_fd() translates a ruleset fd, created with
landlock_create_ruleset(2) and populated with landlock_add_rule(2),
into an owned ruleset reference, validated the same way as for the
Landlock syscalls (ruleset file type, FMODE_CAN_READ). A fd
referring to a file that is not a Landlock ruleset is declined with
-EOPNOTSUPP instead of the syscall's -EBADFD: it may be another
LSM's policy object, and the decline lets the framework offer it to
the LSM it belongs to.
- policy_object_put() releases such a reference. The hook may be
reached from BPF object destructors that cannot sleep, which is
fine: ruleset puts queue the free as RCU work.
- policy_object_get() acquires an additional reference for a caller
that only holds an RCU-protected pointer, e.g. loaded from a BPF
map kptr field under rcu_read_lock(). The reference is taken with
refcount_inc_not_zero(); the racing reader's access to the ruleset
memory is safe because rulesets are freed after an RCU grace
period.
- bprm_apply_policy_object() shares the landlock_restrict_self(2)
path: it calls landlock_prepare_restriction() on the credentials
prepared in the binprm and stages the computed restriction (the
merged struct landlock_domain) in their Landlock blob. The flags
are the landlock_restrict_self(2) flags; only
LANDLOCK_RESTRICT_SELF_TSYNC is rejected with -EINVAL because the
restriction targets the execution, not the calling threads.
LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS is honored, not ignored: the
syscall implements it directly in landlock_restrict_self(), so the
staged path reads it back from the staged restriction's flags and
sets no_new_privs on the executing task at application time. The
executed program then starts with no_new_privs set, binding it and
all its descendants; the current execution's
privilege computation is unaffected, as the bprm credentials
(including any setuid elevation) were computed before
bprm_committing_creds().
The staged restriction is enforced by a bprm_committing_creds() hook
with the same landlock_apply_restriction() call as the syscall, past
the exec point of no return: an execution either starts confined by
the domain or, if it fails earlier, leaves the calling task
untouched. The applied layer is accounted in domain_exec so the
LOG_SAME_EXEC and LOG_NEW_EXEC audit flags follow the executed
program. There is no no_new_privs/CAP_SYS_ADMIN precondition here:
gating who may load a policy-applying BPF program is the BPF
attachment's privilege model.
The application emits the landlock_enforce_domain trace event,
keeping the event's invariant that every enforcement falls between
the domain's create_domain and free_domain events. The process is
single-threaded past de_thread(), so the single event concludes the
operation with complete == 1 and process_wide == 1. no_new_privs
reports the post-flag state; on this path a value of 0 carries no
authorization meaning and only tells the observer that the confined
program can still elevate through future setuid execs, which the
event's documentation now spells out.
The staged restriction's lifetime is fully covered: a second
bprm_apply_policy_object() call on the same execution releases and
replaces the previously staged restriction, an execution failing
before the point of no return releases it through hook_cred_free(),
and the application clears the staged domain so committed task
credentials never carry one. landlock_cred_copy(), now also used by
hook_cred_transfer(), upholds that invariant by never copying a
staged restriction.
Cc: Mickaël Salaün <mic@digikod.net>
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
Notes:
v2->v3:
- No change.
include/trace/events/landlock.h | 15 +++-
security/landlock/Makefile | 2 +
security/landlock/bpf.c | 152 ++++++++++++++++++++++++++++++++
security/landlock/bpf.h | 21 +++++
security/landlock/cred.c | 16 ++--
security/landlock/cred.h | 16 ++++
security/landlock/limits.h | 4 +
security/landlock/ruleset.c | 6 ++
security/landlock/ruleset.h | 22 +++++
security/landlock/setup.c | 2 +
10 files changed, 245 insertions(+), 11 deletions(-)
create mode 100644 security/landlock/bpf.c
create mode 100644 security/landlock/bpf.h
diff --git a/include/trace/events/landlock.h b/include/trace/events/landlock.h
index f82588f6f90e..012ab9dcccb2 100644
--- a/include/trace/events/landlock.h
+++ b/include/trace/events/landlock.h
@@ -500,13 +500,22 @@ TRACE_EVENT(landlock_create_domain,
* enforcement time: 1 if set (by a prior
* :manpage:`prctl(2)` %PR_SET_NO_NEW_PRIVS or by
* %LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS), 0 if the domain
- * was enforced with %CAP_SYS_ADMIN instead.
+ * was enforced with %CAP_SYS_ADMIN instead, or without
+ * either precondition on the BPF exec path (see below).
*
* Emitted for each thread sys_landlock_restrict_self() enforces the
* domain on, in that thread's own context, right after its
* commit_creds(), so it fires only once the thread is irreversibly
- * enforcing the domain (aborted operations emit none). Not
- * balanced; every enforcement falls between the domain's
+ * enforcing the domain (aborted operations emit none). Also emitted
+ * at execve(2)'s point of no return when a BPF-staged policy object
+ * is applied to the execution (see bpf_lsm_policy_apply_bprm()): the
+ * process is single-threaded after de_thread(), so the single event
+ * has @complete == 1 and @process_wide == 1. On that path,
+ * @no_new_privs == 0 carries no authorization meaning (the authority
+ * is the privilege to attach the BPF program, not
+ * no_new_privs/%CAP_SYS_ADMIN); it only tells the observer that the
+ * confined program can still elevate through future setuid execs.
+ * Not balanced; every enforcement falls between the domain's
* landlock_create_domain and landlock_free_domain events.
*
* @complete == 1 && @process_wide == 1 means the whole process is
diff --git a/security/landlock/Makefile b/security/landlock/Makefile
index 2711f4876939..606bc91522e2 100644
--- a/security/landlock/Makefile
+++ b/security/landlock/Makefile
@@ -20,3 +20,5 @@ landlock-$(CONFIG_SECURITY_LANDLOCK_LOG) += \
landlock-$(CONFIG_AUDIT) += audit.o
landlock-$(CONFIG_TRACEPOINTS) += trace.o
+
+landlock-$(CONFIG_BPF_LSM) += bpf.o
diff --git a/security/landlock/bpf.c b/security/landlock/bpf.c
new file mode 100644
index 000000000000..28dc68013251
--- /dev/null
+++ b/security/landlock/bpf.c
@@ -0,0 +1,152 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Landlock - LSM policy object hooks
+ *
+ * Copyright © 2026 Justin Suess <utilityemal77@gmail.com>
+ */
+
+#include <linux/binfmts.h>
+#include <linux/cred.h>
+#include <linux/err.h>
+#include <linux/errno.h>
+#include <linux/fs.h>
+#include <linux/lsm_hooks.h>
+#include <linux/sched.h>
+#include <uapi/linux/landlock.h>
+
+#include "bpf.h"
+#include "cred.h"
+#include "domain.h"
+#include "limits.h"
+#include "ruleset.h"
+#include "setup.h"
+
+#include <trace/events/landlock.h>
+
+static int hook_bprm_apply_policy_object(struct linux_binprm *bprm,
+ struct lsm_policy_object *object,
+ u32 flags)
+{
+ struct landlock_cred_security *bprm_llcred = landlock_cred(bprm->cred);
+ struct landlock_ruleset *ruleset;
+ struct landlock_restriction restriction;
+ int err;
+
+ if (object->type != LANDLOCK_POLICY_TYPE_RULESET)
+ return -EINVAL;
+
+ ruleset = container_of(object, struct landlock_ruleset, policy_object);
+
+ /*
+ * landlock_restrict_self(2) flags minus TSYNC, which targets
+ * the calling threads, not the execution.
+ */
+ if ((flags | LANDLOCK_MASK_RESTRICT_BINPRM) !=
+ LANDLOCK_MASK_RESTRICT_BINPRM)
+ return -EINVAL;
+
+ err = landlock_prepare_restriction(bprm_llcred, ruleset, flags,
+ &restriction);
+ if (err)
+ return err;
+
+ /*
+ * Replaces (and releases) a previously staged restriction.
+ * Nothing is enforced until bprm_committing_creds(); a failed
+ * execution drops the staged restriction in hook_cred_free().
+ */
+ landlock_put_domain(bprm_llcred->staged.domain);
+ bprm_llcred->staged = restriction;
+ return 0;
+}
+
+static void hook_bprm_committing_creds(const struct linux_binprm *bprm)
+{
+ struct landlock_cred_security *bprm_llcred = landlock_cred(bprm->cred);
+ struct landlock_domain *domain = bprm_llcred->staged.domain;
+
+ if (!domain)
+ return;
+
+ /* Set first so the enforcement event reports the post-flag state. */
+ if (bprm_llcred->staged.flags & LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS)
+ task_set_no_new_privs(current);
+
+ /* The application clears @staged's domain pointer. */
+ landlock_apply_restriction(bprm_llcred, &bprm_llcred->staged);
+
+ /*
+ * Past de_thread(), the process is single-threaded: this single
+ * event both concludes the operation and covers the whole
+ * process.
+ */
+ trace_landlock_enforce_domain(domain, true, true,
+ task_no_new_privs(current));
+}
+
+static int hook_policy_object_from_fd(int fd, struct lsm_policy_object **object)
+{
+ struct landlock_ruleset *ruleset;
+
+ ruleset = landlock_get_ruleset_from_fd(fd, FMODE_CAN_READ);
+ if (IS_ERR(ruleset)) {
+ if (ruleset == ERR_PTR(-EBADFD))
+ return -EOPNOTSUPP;
+ return PTR_ERR(ruleset);
+ }
+
+ *object = &ruleset->policy_object;
+ return 0;
+}
+
+/*
+ * The caller holds no reference, only an RCU-protected pointer: the
+ * RCU-deferred ruleset free keeps the memory valid for the
+ * inc_not_zero() race against a concurrent last put.
+ */
+static int hook_policy_object_get(struct lsm_policy_object *object)
+{
+ struct landlock_ruleset *ruleset;
+
+ if (object->type != LANDLOCK_POLICY_TYPE_RULESET)
+ return -EINVAL;
+
+ ruleset = container_of(object, struct landlock_ruleset, policy_object);
+ if (!refcount_inc_not_zero(&ruleset->usage))
+ return -ENOENT;
+ return 0;
+}
+
+static void hook_policy_object_put(struct lsm_policy_object *object)
+{
+ struct landlock_ruleset *ruleset;
+
+ /*
+ * The type routes the container_of() resolution once several
+ * policy object types exist; only rulesets are referenced today.
+ */
+ if (WARN_ON_ONCE(object->type != LANDLOCK_POLICY_TYPE_RULESET))
+ return;
+
+ ruleset = container_of(object, struct landlock_ruleset, policy_object);
+
+ /*
+ * May be reached from BPF object destructors that cannot sleep,
+ * which is fine: the put queues the free as RCU work.
+ */
+ landlock_put_ruleset(ruleset);
+}
+
+static struct security_hook_list landlock_hooks[] __ro_after_init = {
+ LSM_HOOK_INIT(bprm_apply_policy_object, hook_bprm_apply_policy_object),
+ LSM_HOOK_INIT(bprm_committing_creds, hook_bprm_committing_creds),
+ LSM_HOOK_INIT(policy_object_from_fd, hook_policy_object_from_fd),
+ LSM_HOOK_INIT(policy_object_get, hook_policy_object_get),
+ LSM_HOOK_INIT(policy_object_put, hook_policy_object_put),
+};
+
+__init void landlock_add_bpf_hooks(void)
+{
+ security_add_hooks(landlock_hooks, ARRAY_SIZE(landlock_hooks),
+ &landlock_lsmid);
+}
diff --git a/security/landlock/bpf.h b/security/landlock/bpf.h
new file mode 100644
index 000000000000..7c0f199c630a
--- /dev/null
+++ b/security/landlock/bpf.h
@@ -0,0 +1,21 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Landlock - LSM policy object hooks
+ *
+ * Copyright © 2026 Justin Suess <utilityemal77@gmail.com>
+ */
+
+#ifndef _SECURITY_LANDLOCK_BPF_H
+#define _SECURITY_LANDLOCK_BPF_H
+
+#include <linux/init.h>
+
+#ifdef CONFIG_BPF_LSM
+__init void landlock_add_bpf_hooks(void);
+#else /* CONFIG_BPF_LSM */
+static inline void landlock_add_bpf_hooks(void)
+{
+}
+#endif /* CONFIG_BPF_LSM */
+
+#endif /* _SECURITY_LANDLOCK_BPF_H */
diff --git a/security/landlock/cred.c b/security/landlock/cred.c
index f02706f12c7d..8e5b1b6c165e 100644
--- a/security/landlock/cred.c
+++ b/security/landlock/cred.c
@@ -151,11 +151,7 @@ void landlock_apply_restriction(struct landlock_cred_security *const llcred,
static void hook_cred_transfer(struct cred *const new,
const struct cred *const old)
{
- const struct landlock_cred_security *const old_llcred =
- landlock_cred(old);
-
- landlock_get_domain(old_llcred->domain);
- *landlock_cred(new) = *old_llcred;
+ landlock_cred_copy(landlock_cred(new), landlock_cred(old));
}
static int hook_cred_prepare(struct cred *const new,
@@ -167,10 +163,14 @@ static int hook_cred_prepare(struct cred *const new,
static void hook_cred_free(struct cred *const cred)
{
- struct landlock_domain *const dom = landlock_cred(cred)->domain;
+ struct landlock_cred_security *const llcred = landlock_cred(cred);
+
+ landlock_put_domain_deferred(llcred->domain);
- if (dom)
- landlock_put_domain_deferred(dom);
+#ifdef CONFIG_BPF_LSM
+ /* Releases a restriction staged for an aborted execution. */
+ landlock_put_domain_deferred(llcred->staged.domain);
+#endif /* CONFIG_BPF_LSM */
}
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
diff --git a/security/landlock/cred.h b/security/landlock/cred.h
index 88fa97fc3bd2..9a2971197892 100644
--- a/security/landlock/cred.h
+++ b/security/landlock/cred.h
@@ -59,6 +59,16 @@ struct landlock_cred_security {
*/
struct landlock_domain *domain;
+#ifdef CONFIG_BPF_LSM
+ /**
+ * @staged: Restriction staged by the bprm_apply_policy_object() hook,
+ * owning its domain reference, applied at bprm_committing_creds().
+ * Only ever set on credentials prepared for an execution; committed
+ * task credentials never carry a staged restriction.
+ */
+ struct landlock_restriction staged;
+#endif /* CONFIG_BPF_LSM */
+
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
/**
* @domain_exec: Bitmask identifying the domain layers that were enforced by
@@ -99,6 +109,12 @@ static inline void landlock_cred_copy(struct landlock_cred_security *dst,
*dst = *src;
landlock_get_domain(src->domain);
+
+#ifdef CONFIG_BPF_LSM
+ /* Only bprm credentials own a staged restriction: never copied. */
+ WARN_ON_ONCE(src->staged.domain);
+ dst->staged = (struct landlock_restriction){};
+#endif /* CONFIG_BPF_LSM */
}
static inline struct landlock_domain *landlock_get_current_domain(void)
diff --git a/security/landlock/limits.h b/security/landlock/limits.h
index 1a7c5fb8f6fd..9aeb99b2f173 100644
--- a/security/landlock/limits.h
+++ b/security/landlock/limits.h
@@ -37,6 +37,10 @@
#define LANDLOCK_LAST_RESTRICT_SELF LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS
#define LANDLOCK_MASK_RESTRICT_SELF ((LANDLOCK_LAST_RESTRICT_SELF << 1) - 1)
+/* Subset of the restrict-self flags applicable to an execution. */
+#define LANDLOCK_MASK_RESTRICT_BINPRM \
+ (LANDLOCK_MASK_RESTRICT_SELF & ~LANDLOCK_RESTRICT_SELF_TSYNC)
+
/* clang-format on */
#endif /* _SECURITY_LANDLOCK_LIMITS_H */
diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
index 00a6b9938fd1..6de326b71a5a 100644
--- a/security/landlock/ruleset.c
+++ b/security/landlock/ruleset.c
@@ -23,6 +23,7 @@
#include <linux/spinlock.h>
#include <linux/workqueue.h>
#include <uapi/linux/landlock.h>
+#include <uapi/linux/lsm.h>
#include "access.h"
#include "id.h"
@@ -51,6 +52,11 @@ landlock_create_ruleset(const access_mask_t fs_access_mask,
mutex_init(&new_ruleset->lock);
new_ruleset->rules.root_inode = RB_ROOT;
+#ifdef CONFIG_BPF_LSM
+ new_ruleset->policy_object.lsmid = LSM_ID_LANDLOCK;
+ new_ruleset->policy_object.type = LANDLOCK_POLICY_TYPE_RULESET;
+#endif /* CONFIG_BPF_LSM */
+
#if IS_ENABLED(CONFIG_INET)
new_ruleset->rules.root_net_port = RB_ROOT;
#endif /* IS_ENABLED(CONFIG_INET) */
diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h
index 1465f8a5c464..22edb140bb7f 100644
--- a/security/landlock/ruleset.h
+++ b/security/landlock/ruleset.h
@@ -15,6 +15,7 @@
#include <linux/mutex.h>
#include <linux/rbtree.h>
#include <linux/refcount.h>
+#include <linux/security.h>
#include <linux/workqueue.h>
#include "access.h"
@@ -146,6 +147,16 @@ struct landlock_rules {
u32 num_rules;
};
+#ifdef CONFIG_BPF_LSM
+/*
+ * Landlock's lsm_policy_object types. The namespace is private to
+ * Landlock; 0 stays reserved as "unset".
+ */
+enum landlock_policy_type {
+ LANDLOCK_POLICY_TYPE_RULESET = 1,
+};
+#endif /* CONFIG_BPF_LSM */
+
/**
* struct landlock_ruleset - Landlock ruleset
*
@@ -157,6 +168,17 @@ struct landlock_ruleset {
* @rules: Red-black tree storage for rules.
*/
struct landlock_rules rules;
+
+#ifdef CONFIG_BPF_LSM
+ /**
+ * @policy_object: Identity under which the ruleset is handed out
+ * to BPF programs as a referenced kptr: the LSM policy kfuncs
+ * dispatch back to Landlock through its lsmid. Kept outside the
+ * union with @work_free: RCU readers may read its lsmid while a
+ * queued free waits out the grace period.
+ */
+ struct lsm_policy_object policy_object;
+#endif /* CONFIG_BPF_LSM */
/**
* @usage: Number of file descriptors referencing this ruleset. Kept
* outside the union with @work_free: RCU readers may still call
diff --git a/security/landlock/setup.c b/security/landlock/setup.c
index 47dac1736f10..3b7e18edadfb 100644
--- a/security/landlock/setup.c
+++ b/security/landlock/setup.c
@@ -11,6 +11,7 @@
#include <linux/lsm_hooks.h>
#include <uapi/linux/lsm.h>
+#include "bpf.h"
#include "common.h"
#include "cred.h"
#include "errata.h"
@@ -68,6 +69,7 @@ static int __init landlock_init(void)
landlock_add_task_hooks();
landlock_add_fs_hooks();
landlock_add_net_hooks();
+ landlock_add_bpf_hooks();
landlock_init_id();
landlock_initialized = true;
pr_info("Up and running.\n");
--
2.55.0
^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH bpf-next v3 14/15] selftests/bpf: Test the LSM policy object kfuncs with Landlock
2026-09-09 19:37 [PATCH bpf-next v3 00/15] BPF interface for applying Landlock rulesets Justin Suess
` (12 preceding siblings ...)
2026-09-09 19:37 ` [PATCH bpf-next v3 13/15] landlock: Implement the LSM policy object hooks Justin Suess
@ 2026-09-09 19:37 ` Justin Suess
2026-09-09 20:46 ` bot+bpf-ci
2026-09-09 19:37 ` [PATCH bpf-next v3 15/15] landlock: Document the BPF policy interface Justin Suess
14 siblings, 1 reply; 29+ messages in thread
From: Justin Suess @ 2026-09-09 19:37 UTC (permalink / raw)
To: ast, daniel, andrii, kpsingh, matt, paul, mic, viro, brauner,
kees
Cc: casey, gnoack, jack, song, yonghong.song, martin.lau, eddyz87,
memxor, jolsa, m, bpf, linux-security-module, linux-kernel,
Justin Suess
Exercise the policy object kfuncs against an LSM actually providing
policy objects, complementing the LSM-independent tests of the
verifier-side and from_fd contracts.
The programs mirror the intended usage: a syscall program acquires a
Landlock ruleset with bpf_lsm_policy_from_fd() and parks it in a map
kptr field; an LSM program on bprm_creds_for_exec() loads the field
under bpf_rcu_read_lock(), takes its own reference with
bpf_lsm_policy_acquire(), and applies the ruleset with
bpf_lsm_policy_apply_bprm(). The prog_tests runner checks that:
- a monitored execution starts confined (a handled-but-not-allowed
write fails) while an unmonitored one is untouched,
- two concurrent monitored executions are both restricted from the
one shared map slot,
- the landlock_restrict_self(2) log flags are accepted while
LANDLOCK_RESTRICT_SELF_TSYNC is rejected with -EINVAL and leaves
the execution unrestricted,
- LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS makes the executed program
start with no_new_privs set, while an execution without it stays
non-nnp,
- a second apply call on the same execution replaces the staged
restriction rather than failing,
- an execution failing past the bprm hook (ENOEXEC) discards the
staged restriction and leaves the caller unconfined,
- the object's identity is BTF-readable off the trusted kptr: the
LSM-private type tag is nonzero, and the lsmid is LSM_ID_LANDLOCK
with the fd alone having routed the translation,
- the bprm application emits a single landlock_enforce_domain event,
observed by a tp_btf program: complete == 1, process_wide == 1, and
no_new_privs reporting the post-flag state.
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
Notes:
v2->v3:
- Fix a pipe fd leak on the fork() error path of
test_restrict_binprm_discard().
tools/testing/selftests/bpf/config | 1 +
tools/testing/selftests/bpf/config.x86_64 | 2 +-
.../bpf/prog_tests/lsm_policy_landlock.c | 525 ++++++++++++++++++
.../selftests/bpf/progs/lsm_policy_landlock.c | 142 +++++
4 files changed, 669 insertions(+), 1 deletion(-)
create mode 100644 tools/testing/selftests/bpf/prog_tests/lsm_policy_landlock.c
create mode 100644 tools/testing/selftests/bpf/progs/lsm_policy_landlock.c
diff --git a/tools/testing/selftests/bpf/config b/tools/testing/selftests/bpf/config
index 2f79688dcf7c..42e20d245f90 100644
--- a/tools/testing/selftests/bpf/config
+++ b/tools/testing/selftests/bpf/config
@@ -122,6 +122,7 @@ CONFIG_SAMPLES=y
CONFIG_SAMPLE_LIVEPATCH=m
CONFIG_SECURITY=y
CONFIG_SECURITYFS=y
+CONFIG_SECURITY_LANDLOCK=y
CONFIG_SYN_COOKIES=y
CONFIG_TEST_BPF=m
CONFIG_UDMABUF=y
diff --git a/tools/testing/selftests/bpf/config.x86_64 b/tools/testing/selftests/bpf/config.x86_64
index 523e0d29bbd4..2c4d857f69f5 100644
--- a/tools/testing/selftests/bpf/config.x86_64
+++ b/tools/testing/selftests/bpf/config.x86_64
@@ -125,7 +125,7 @@ CONFIG_LEGACY_VSYSCALL_NONE=y
CONFIG_LOG_BUF_SHIFT=21
CONFIG_LOG_CPU_MAX_BUF_SHIFT=0
CONFIG_LOGO=y
-CONFIG_LSM="selinux,bpf,integrity"
+CONFIG_LSM="landlock,selinux,bpf,integrity"
CONFIG_MAC_PARTITION=y
CONFIG_MAGIC_SYSRQ=y
CONFIG_MCORE2=y
diff --git a/tools/testing/selftests/bpf/prog_tests/lsm_policy_landlock.c b/tools/testing/selftests/bpf/prog_tests/lsm_policy_landlock.c
new file mode 100644
index 000000000000..f01ae5be8aef
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/lsm_policy_landlock.c
@@ -0,0 +1,525 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright © 2026 Justin Suess <utilityemal77@gmail.com> */
+
+#include <test_progs.h>
+#include <errno.h>
+#include <linux/landlock.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/prctl.h>
+#include <sys/stat.h>
+#include <sys/syscall.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "lsm_policy_landlock.skel.h"
+
+/* Fallbacks for old system headers. */
+#ifndef LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON
+#define LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON (1U << 1)
+#endif
+#ifndef LANDLOCK_RESTRICT_SELF_TSYNC
+#define LANDLOCK_RESTRICT_SELF_TSYNC (1U << 3)
+#endif
+#ifndef LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS
+#define LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS (1U << 4)
+#endif
+#ifndef LSM_ID_LANDLOCK
+#define LSM_ID_LANDLOCK 110 /* uapi/linux/lsm.h */
+#endif
+
+struct policy_test_env {
+ struct lsm_policy_landlock *skel;
+ char tmp_path[64];
+ int tmp_fd;
+ int ruleset_fd;
+};
+
+static int create_ruleset(void)
+{
+ const struct landlock_ruleset_attr attr = {
+ .handled_access_fs = LANDLOCK_ACCESS_FS_WRITE_FILE,
+ };
+
+ return syscall(__NR_landlock_create_ruleset, &attr, sizeof(attr), 0);
+}
+
+static void reset_prog_state(struct lsm_policy_landlock *skel)
+{
+ skel->bss->called = false;
+ skel->bss->no_policy = false;
+ skel->bss->restrict_err = -1;
+ skel->bss->restrict2_err = -1;
+ skel->bss->restrict_ok_count = 0;
+ skel->bss->kfunc_flags = 0;
+ skel->bss->double_call = false;
+ skel->bss->monitored_pid = 0;
+ skel->bss->monitored_pid2 = 0;
+ skel->bss->enforce_domain_id = 0;
+ skel->bss->enforce_count = 0;
+ skel->bss->enforce_complete = false;
+ skel->bss->enforce_process_wide = false;
+ skel->bss->enforce_no_new_privs = false;
+}
+
+/*
+ * Runs the syscall program that acquires the ruleset from
+ * @ruleset_fd, in the runner's fd table, and parks it in the map kptr
+ * slot for the LSM program.
+ */
+static int load_ruleset_into_map(struct lsm_policy_landlock *skel)
+{
+ LIBBPF_OPTS(bpf_test_run_opts, opts);
+ int err;
+
+ err = bpf_prog_test_run_opts(bpf_program__fd(skel->progs.load_policy),
+ &opts);
+ if (!ASSERT_OK(err, "load_policy_run"))
+ return -1;
+ if (!ASSERT_OK(opts.retval, "load_policy_retval"))
+ return -1;
+ /* Landlock's type tag, read off the trusted kptr, is never 0. */
+ ASSERT_NEQ(skel->bss->policy_type, 0, "policy_type_nonzero");
+ /* The fd, not a kfunc argument, routed the call to Landlock. */
+ ASSERT_EQ(skel->bss->policy_lsmid, LSM_ID_LANDLOCK, "policy_lsmid");
+ return 0;
+}
+
+/*
+ * Creates the target tmp file and the ruleset, loads and attaches the
+ * skeleton, and parks the ruleset in the map. Returns 0 on success;
+ * on failure (or skip), the caller must still run teardown_env().
+ */
+static int setup_env(struct policy_test_env *env)
+{
+ env->skel = NULL;
+ env->ruleset_fd = -1;
+ strcpy(env->tmp_path, "/tmp/lsm_policy_landlock_XXXXXX");
+ env->tmp_fd = mkstemp(env->tmp_path);
+ if (!ASSERT_GE(env->tmp_fd, 0, "mkstemp"))
+ return -1;
+
+ env->ruleset_fd = create_ruleset();
+ if (env->ruleset_fd < 0) {
+ if (errno == EOPNOTSUPP || errno == ENOSYS)
+ test__skip();
+ else
+ ASSERT_GE(env->ruleset_fd, 0,
+ "landlock_create_ruleset");
+ return -1;
+ }
+
+ env->skel = lsm_policy_landlock__open_and_load();
+ if (!ASSERT_OK_PTR(env->skel, "skel_open_and_load"))
+ return -1;
+ env->skel->bss->ruleset_fd = env->ruleset_fd;
+ reset_prog_state(env->skel);
+
+ if (!ASSERT_OK(lsm_policy_landlock__attach(env->skel),
+ "skel_attach"))
+ return -1;
+
+ return load_ruleset_into_map(env->skel);
+}
+
+static void teardown_env(struct policy_test_env *env)
+{
+ lsm_policy_landlock__destroy(env->skel);
+ if (env->ruleset_fd >= 0)
+ close(env->ruleset_fd);
+ if (env->tmp_fd >= 0)
+ close(env->tmp_fd);
+ unlink(env->tmp_path);
+}
+
+/*
+ * Forks a child that blocks on a pipe, then execs @path with @argv.
+ * Returns the child's pid, or -1 on error. @release_fd receives the
+ * pipe's write end: release_exec_child() lets the child exec, after
+ * its pid has been published to the BPF program.
+ */
+static pid_t spawn_exec_child(const char *path, char *const argv[],
+ int *release_fd)
+{
+ int pipe_fds[2];
+ char buf = 0;
+ pid_t pid;
+
+ if (!ASSERT_OK(pipe(pipe_fds), "pipe"))
+ return -1;
+
+ pid = fork();
+ if (!ASSERT_GE(pid, 0, "fork")) {
+ close(pipe_fds[0]);
+ close(pipe_fds[1]);
+ return -1;
+ }
+ if (pid == 0) {
+ close(pipe_fds[1]);
+ read(pipe_fds[0], &buf, 1);
+ close(pipe_fds[0]);
+ execv(path, argv);
+ exit(127);
+ }
+ close(pipe_fds[0]);
+ *release_fd = pipe_fds[1];
+ return pid;
+}
+
+static void release_exec_child(int release_fd)
+{
+ char buf = 0;
+
+ write(release_fd, &buf, 1);
+ close(release_fd);
+}
+
+/* Returns the child's exit status, or -1 on error. */
+static int wait_exec_child(pid_t pid)
+{
+ int status;
+
+ if (!ASSERT_EQ(waitpid(pid, &status, 0), pid, "waitpid"))
+ return -1;
+ if (!ASSERT_TRUE(WIFEXITED(status), "child_exited"))
+ return -1;
+ return WEXITSTATUS(status);
+}
+
+static int run_exec_child(struct lsm_policy_landlock *skel,
+ bool monitored, const char *shell_cmd)
+{
+ char *argv[] = { "sh", "-c", (char *)shell_cmd, NULL };
+ int release_fd;
+ pid_t pid;
+
+ pid = spawn_exec_child("/bin/sh", argv, &release_fd);
+ if (pid < 0)
+ return -1;
+ skel->bss->monitored_pid = monitored ? pid : 0;
+ release_exec_child(release_fd);
+ return wait_exec_child(pid);
+}
+
+/*
+ * Exit codes: 4 = unexpected write outcome, 0 = everything as
+ * expected.
+ */
+static void format_child_cmd(char *cmd, size_t len, bool expect_write_ok,
+ const char *tmp_path)
+{
+ if (expect_write_ok)
+ snprintf(cmd, len, "echo x > %s || exit 4; exit 0", tmp_path);
+ else
+ snprintf(cmd, len,
+ "if echo x > %s 2>/dev/null; then exit 4; fi; exit 0",
+ tmp_path);
+}
+
+static void test_restrict_binprm(void)
+{
+ struct policy_test_env env;
+ struct lsm_policy_landlock *skel;
+ char cmd[256];
+ int ret;
+
+ if (setup_env(&env))
+ goto out;
+ skel = env.skel;
+
+ /* Control: an unmonitored execution may write to the tmp file. */
+ reset_prog_state(skel);
+ format_child_cmd(cmd, sizeof(cmd), true, env.tmp_path);
+ ret = run_exec_child(skel, false, cmd);
+ if (!ASSERT_EQ(ret, 0, "control_child_exit"))
+ goto out;
+ ASSERT_FALSE(skel->bss->called, "control_not_monitored");
+
+ /*
+ * A monitored execution starts landlocked: the ruleset handles
+ * LANDLOCK_ACCESS_FS_WRITE_FILE without any rule, so the write
+ * must fail.
+ */
+ reset_prog_state(skel);
+ format_child_cmd(cmd, sizeof(cmd), false, env.tmp_path);
+ ret = run_exec_child(skel, true, cmd);
+ if (!ASSERT_EQ(ret, 0, "restricted_child_exit"))
+ goto out;
+ ASSERT_TRUE(skel->bss->called, "lsm_prog_called");
+ ASSERT_FALSE(skel->bss->no_policy, "ruleset_in_map");
+ ASSERT_EQ(skel->bss->restrict_err, 0, "restrict_binprm");
+
+ /* The audit log flags of landlock_restrict_self(2) apply too. */
+ reset_prog_state(skel);
+ skel->bss->kfunc_flags = LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON;
+ format_child_cmd(cmd, sizeof(cmd), false, env.tmp_path);
+ ret = run_exec_child(skel, true, cmd);
+ if (!ASSERT_EQ(ret, 0, "log_flags_child_exit"))
+ goto out;
+ ASSERT_EQ(skel->bss->restrict_err, 0, "log_flags_restrict_binprm");
+
+ /*
+ * LANDLOCK_RESTRICT_SELF_TSYNC targets the calling threads, not
+ * an execution: the kfunc must reject it and the execution must
+ * stay unrestricted.
+ */
+ reset_prog_state(skel);
+ skel->bss->kfunc_flags = LANDLOCK_RESTRICT_SELF_TSYNC;
+ format_child_cmd(cmd, sizeof(cmd), true, env.tmp_path);
+ ret = run_exec_child(skel, true, cmd);
+ if (!ASSERT_EQ(ret, 0, "tsync_child_exit"))
+ goto out;
+ ASSERT_TRUE(skel->bss->called, "tsync_prog_called");
+ ASSERT_EQ(skel->bss->restrict_err, -EINVAL, "tsync_rejected");
+
+ /*
+ * A second call on the same execution replaces the staged
+ * domain (and releases the first one): the result is a single
+ * restriction, not an error.
+ */
+ reset_prog_state(skel);
+ skel->bss->double_call = true;
+ format_child_cmd(cmd, sizeof(cmd), false, env.tmp_path);
+ ret = run_exec_child(skel, true, cmd);
+ if (!ASSERT_EQ(ret, 0, "double_child_exit"))
+ goto out;
+ ASSERT_EQ(skel->bss->restrict_err, 0, "double_restrict_first");
+ ASSERT_EQ(skel->bss->restrict2_err, 0, "double_restrict_second");
+out:
+ teardown_env(&env);
+}
+
+/*
+ * Two monitored executions, released together, must both be
+ * restricted from the one shared map kptr slot.
+ */
+static void test_restrict_binprm_concurrent(void)
+{
+ struct policy_test_env env;
+ char *argv[4];
+ int release_fds[2] = { -1, -1 };
+ pid_t pids[2] = { -1, -1 };
+ char cmd[256];
+ int i;
+
+ if (setup_env(&env))
+ goto out;
+
+ format_child_cmd(cmd, sizeof(cmd), false, env.tmp_path);
+ argv[0] = "sh";
+ argv[1] = "-c";
+ argv[2] = cmd;
+ argv[3] = NULL;
+
+ for (i = 0; i < 2; i++) {
+ pids[i] = spawn_exec_child("/bin/sh", argv, &release_fds[i]);
+ if (pids[i] < 0)
+ goto out_kill;
+ }
+
+ env.skel->bss->monitored_pid = pids[0];
+ env.skel->bss->monitored_pid2 = pids[1];
+
+ /* Releases both children only once both pids are published. */
+ for (i = 0; i < 2; i++) {
+ release_exec_child(release_fds[i]);
+ release_fds[i] = -1;
+ }
+
+ for (i = 0; i < 2; i++) {
+ ASSERT_EQ(wait_exec_child(pids[i]), 0,
+ "concurrent_child_exit");
+ pids[i] = -1;
+ }
+
+ ASSERT_FALSE(env.skel->bss->no_policy, "ruleset_in_map");
+ ASSERT_EQ(env.skel->bss->restrict_ok_count, 2,
+ "both_execs_restricted");
+
+out_kill:
+ for (i = 0; i < 2; i++) {
+ if (pids[i] > 0) {
+ kill(pids[i], SIGKILL);
+ waitpid(pids[i], NULL, 0);
+ }
+ if (release_fds[i] >= 0)
+ close(release_fds[i]);
+ }
+out:
+ teardown_env(&env);
+}
+
+/*
+ * Checks that a staged restriction is discarded, and the staged
+ * domain released, when the execution fails after the bprm hook: the
+ * calling task must not end up landlocked.
+ */
+static void test_restrict_binprm_discard(void)
+{
+ struct policy_test_env env;
+ char garbage_path[] = "/tmp/lsm_policy_garbage_XXXXXX";
+ int garbage_fd, pipe_fds[2];
+ char buf = 0;
+ pid_t pid;
+
+ if (setup_env(&env))
+ goto out;
+
+ /*
+ * An executable file that no binfmt handler accepts: the exec
+ * fails with ENOEXEC after bprm_creds_for_exec() has run.
+ */
+ garbage_fd = mkstemp(garbage_path);
+ if (!ASSERT_GE(garbage_fd, 0, "mkstemp_garbage"))
+ goto out;
+ if (!ASSERT_EQ(write(garbage_fd, "junk\n", 5), 5, "write_garbage") ||
+ !ASSERT_OK(fchmod(garbage_fd, 0700), "chmod_garbage")) {
+ close(garbage_fd);
+ goto out_unlink;
+ }
+ close(garbage_fd);
+
+ if (!ASSERT_OK(pipe(pipe_fds), "pipe"))
+ goto out_unlink;
+
+ /*
+ * Cannot use spawn_exec_child(): the same process must test its
+ * write access after the failed exec.
+ */
+ pid = fork();
+ if (!ASSERT_GE(pid, 0, "fork")) {
+ close(pipe_fds[0]);
+ close(pipe_fds[1]);
+ goto out_unlink;
+ }
+ if (pid == 0) {
+ char *argv[] = { "garbage", NULL };
+ int fd;
+
+ close(pipe_fds[1]);
+ read(pipe_fds[0], &buf, 1);
+ close(pipe_fds[0]);
+ execv(garbage_path, argv);
+ /*
+ * The failed execution must leave no trace: no
+ * Landlock domain, i.e. writing must still work
+ * (exit 6).
+ */
+ fd = open(env.tmp_path, O_WRONLY | O_TRUNC);
+ if (fd < 0)
+ exit(6);
+ close(fd);
+ exit(0);
+ }
+ close(pipe_fds[0]);
+ env.skel->bss->monitored_pid = pid;
+ release_exec_child(pipe_fds[1]);
+
+ ASSERT_EQ(wait_exec_child(pid), 0, "discard_child_exit");
+ ASSERT_TRUE(env.skel->bss->called, "lsm_prog_called");
+ ASSERT_EQ(env.skel->bss->restrict_err, 0, "restrict_binprm");
+out_unlink:
+ unlink(garbage_path);
+out:
+ teardown_env(&env);
+}
+
+/*
+ * LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS makes the executed program start
+ * with no_new_privs set; without it, a non-nnp parent's execution
+ * stays non-nnp. Child exit code 5: unexpected NoNewPrivs value.
+ */
+static void test_restrict_binprm_nnp(void)
+{
+ static const char nnp_cmd[] =
+ "grep -q '^NoNewPrivs:[[:space:]]*%d' /proc/self/status || exit 5";
+ struct policy_test_env env;
+ struct lsm_policy_landlock *skel;
+ char cmd[sizeof(nnp_cmd)];
+ int ret;
+
+ if (setup_env(&env))
+ goto out;
+ skel = env.skel;
+
+ if (!ASSERT_OK(prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0),
+ "runner_not_nnp"))
+ goto out;
+
+ reset_prog_state(skel);
+ snprintf(cmd, sizeof(cmd), nnp_cmd, 0);
+ ret = run_exec_child(skel, true, cmd);
+ if (!ASSERT_EQ(ret, 0, "no_flag_child_exit"))
+ goto out;
+ ASSERT_EQ(skel->bss->restrict_err, 0, "no_flag_restrict_binprm");
+
+ reset_prog_state(skel);
+ skel->bss->kfunc_flags = LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS;
+ snprintf(cmd, sizeof(cmd), nnp_cmd, 1);
+ ret = run_exec_child(skel, true, cmd);
+ if (!ASSERT_EQ(ret, 0, "nnp_flag_child_exit"))
+ goto out;
+ ASSERT_EQ(skel->bss->restrict_err, 0, "nnp_flag_restrict_binprm");
+out:
+ teardown_env(&env);
+}
+
+/*
+ * The bprm application emits landlock_enforce_domain, observed here by
+ * a tp_btf program: the single event concludes the operation
+ * (complete == 1), covers the whole post-de_thread() process
+ * (process_wide == 1), and reports the post-flag no_new_privs state.
+ */
+static void test_restrict_binprm_trace(void)
+{
+ struct policy_test_env env;
+ struct lsm_policy_landlock *skel;
+ char cmd[256];
+ int ret;
+
+ if (setup_env(&env))
+ goto out;
+ skel = env.skel;
+
+ if (!ASSERT_OK(prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0),
+ "runner_not_nnp"))
+ goto out;
+
+ reset_prog_state(skel);
+ format_child_cmd(cmd, sizeof(cmd), false, env.tmp_path);
+ ret = run_exec_child(skel, true, cmd);
+ if (!ASSERT_EQ(ret, 0, "trace_child_exit"))
+ goto out;
+ ASSERT_EQ(skel->bss->restrict_err, 0, "restrict_binprm");
+ ASSERT_EQ(skel->bss->enforce_count, 1, "one_enforce_event");
+ ASSERT_TRUE(skel->bss->enforce_complete, "enforce_complete");
+ ASSERT_TRUE(skel->bss->enforce_process_wide, "enforce_process_wide");
+ ASSERT_NEQ(skel->bss->enforce_domain_id, 0, "enforce_domain_id");
+ ASSERT_FALSE(skel->bss->enforce_no_new_privs, "enforce_nnp_off");
+
+ reset_prog_state(skel);
+ skel->bss->kfunc_flags = LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS;
+ format_child_cmd(cmd, sizeof(cmd), false, env.tmp_path);
+ ret = run_exec_child(skel, true, cmd);
+ if (!ASSERT_EQ(ret, 0, "trace_nnp_child_exit"))
+ goto out;
+ ASSERT_EQ(skel->bss->enforce_count, 1, "one_enforce_event_nnp");
+ ASSERT_TRUE(skel->bss->enforce_no_new_privs, "enforce_nnp_on");
+out:
+ teardown_env(&env);
+}
+
+void test_lsm_policy_landlock(void)
+{
+ if (test__start_subtest("restrict_binprm"))
+ test_restrict_binprm();
+ if (test__start_subtest("restrict_binprm_concurrent"))
+ test_restrict_binprm_concurrent();
+ if (test__start_subtest("restrict_binprm_discard"))
+ test_restrict_binprm_discard();
+ if (test__start_subtest("restrict_binprm_nnp"))
+ test_restrict_binprm_nnp();
+ if (test__start_subtest("restrict_binprm_trace"))
+ test_restrict_binprm_trace();
+}
diff --git a/tools/testing/selftests/bpf/progs/lsm_policy_landlock.c b/tools/testing/selftests/bpf/progs/lsm_policy_landlock.c
new file mode 100644
index 000000000000..231b24b87dd4
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/lsm_policy_landlock.c
@@ -0,0 +1,142 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright © 2026 Justin Suess <utilityemal77@gmail.com> */
+
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+
+char _license[] SEC("license") = "GPL";
+
+extern struct lsm_policy_object *
+bpf_lsm_policy_acquire(struct lsm_policy_object *object) __ksym;
+extern int bpf_lsm_policy_apply_bprm(struct lsm_policy_object *object,
+ struct linux_binprm *bprm,
+ u32 flags) __ksym;
+extern struct lsm_policy_object *
+bpf_lsm_policy_from_fd(int fd, u32 flags) __ksym;
+extern void bpf_lsm_policy_release(struct lsm_policy_object *object) __ksym;
+void bpf_rcu_read_lock(void) __ksym;
+void bpf_rcu_read_unlock(void) __ksym;
+
+struct policy_slot {
+ struct lsm_policy_object __kptr *object;
+};
+
+struct {
+ __uint(type, BPF_MAP_TYPE_ARRAY);
+ __uint(max_entries, 1);
+ __type(key, int);
+ __type(value, struct policy_slot);
+} policy_map SEC(".maps");
+
+int monitored_pid;
+int monitored_pid2;
+int ruleset_fd;
+u32 kfunc_flags;
+bool double_call;
+u32 policy_type;
+u64 policy_lsmid;
+bool no_policy;
+int restrict_err;
+int restrict2_err;
+int restrict_ok_count;
+bool called;
+u64 enforce_domain_id;
+int enforce_count;
+bool enforce_complete;
+bool enforce_process_wide;
+bool enforce_no_new_privs;
+
+/*
+ * Runs in the test runner's context through BPF_PROG_RUN, where
+ * @ruleset_fd is meaningful.
+ */
+SEC("syscall")
+int load_policy(void *ctx)
+{
+ struct lsm_policy_object *object, *old;
+ struct policy_slot *slot;
+ int key = 0;
+
+ slot = bpf_map_lookup_elem(&policy_map, &key);
+ if (!slot)
+ return 1;
+
+ object = bpf_lsm_policy_from_fd(ruleset_fd, 0);
+ if (!object)
+ return 2;
+
+ /*
+ * The object's identity is BTF-readable off the trusted kptr: a
+ * program that expects a policy of one specific LSM can check
+ * the lsmid the fd resolved to.
+ */
+ policy_type = object->type;
+ policy_lsmid = object->lsmid;
+
+ old = bpf_kptr_xchg(&slot->object, object);
+ if (old)
+ bpf_lsm_policy_release(old);
+ return 0;
+}
+
+SEC("lsm.s/bprm_creds_for_exec")
+int BPF_PROG(restrict_exec, struct linux_binprm *bprm)
+{
+ struct lsm_policy_object *object;
+ struct policy_slot *slot;
+ int pid = bpf_get_current_pid_tgid() >> 32;
+ int key = 0;
+
+ if (pid != monitored_pid && pid != monitored_pid2)
+ return 0;
+
+ called = true;
+
+ slot = bpf_map_lookup_elem(&policy_map, &key);
+ if (!slot)
+ return 0;
+
+ /*
+ * RCU load + acquire instead of bpf_kptr_xchg(): the slot is
+ * never emptied, so concurrent executions can share it.
+ */
+ bpf_rcu_read_lock();
+ object = slot->object;
+ if (object)
+ object = bpf_lsm_policy_acquire(object);
+ bpf_rcu_read_unlock();
+
+ if (!object) {
+ no_policy = true;
+ return 0;
+ }
+
+ restrict_err = bpf_lsm_policy_apply_bprm(object, bprm, kfunc_flags);
+ if (!restrict_err)
+ __sync_fetch_and_add(&restrict_ok_count, 1);
+ if (double_call)
+ /* Replaces the domain staged by the first call. */
+ restrict2_err = bpf_lsm_policy_apply_bprm(object, bprm,
+ kfunc_flags);
+
+ bpf_lsm_policy_release(object);
+ return 0;
+}
+
+SEC("tp_btf/landlock_enforce_domain")
+int BPF_PROG(on_enforce_domain, struct landlock_domain *domain, bool complete,
+ bool process_wide, bool no_new_privs)
+{
+ int pid = bpf_get_current_pid_tgid() >> 32;
+
+ if (pid != monitored_pid && pid != monitored_pid2)
+ return 0;
+
+ __sync_fetch_and_add(&enforce_count, 1);
+ enforce_domain_id = domain->hierarchy->id;
+ enforce_complete = complete;
+ enforce_process_wide = process_wide;
+ enforce_no_new_privs = no_new_privs;
+ return 0;
+}
--
2.55.0
^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH bpf-next v3 15/15] landlock: Document the BPF policy interface
2026-09-09 19:37 [PATCH bpf-next v3 00/15] BPF interface for applying Landlock rulesets Justin Suess
` (13 preceding siblings ...)
2026-09-09 19:37 ` [PATCH bpf-next v3 14/15] selftests/bpf: Test the LSM policy object kfuncs with Landlock Justin Suess
@ 2026-09-09 19:37 ` Justin Suess
14 siblings, 0 replies; 29+ messages in thread
From: Justin Suess @ 2026-09-09 19:37 UTC (permalink / raw)
To: ast, daniel, andrii, kpsingh, matt, paul, mic, viro, brauner,
kees
Cc: casey, gnoack, jack, song, yonghong.song, martin.lau, eddyz87,
memxor, jolsa, m, bpf, linux-security-module, linux-kernel,
Justin Suess
Describe how the generic LSM policy kfuncs apply to Landlock
rulesets: the program-side flow, the staged restriction and its trace
event, the LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS semantics on an
execution, and why the bprm path carries no no_new_privs/
CAP_SYS_ADMIN precondition. Note the new emission point in the trace
events overview.
Cc: Mickaël Salaün <mic@digikod.net>
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
Notes:
v2->v3:
- No change.
Documentation/security/landlock.rst | 38 +++++++++++++++++++++++++
Documentation/trace/events-landlock.rst | 5 +++-
2 files changed, 42 insertions(+), 1 deletion(-)
diff --git a/Documentation/security/landlock.rst b/Documentation/security/landlock.rst
index 2d6e1076484e..ddc0d149ae8f 100644
--- a/Documentation/security/landlock.rst
+++ b/Documentation/security/landlock.rst
@@ -130,6 +130,44 @@ The reasoning is:
restrictions, because access within the same scope is already
allowed based on ``LANDLOCK_ACCESS_FS_RESOLVE_UNIX``.
+BPF kfuncs
+==========
+
+BPF programs can apply a userspace-created Landlock ruleset to an
+execution, through the generic LSM policy kfuncs (see
+Documentation/security/lsm-development.rst). A syscall program
+(``BPF_PROG_TYPE_SYSCALL``), running in the context of the process
+that set the ruleset up, acquires the ruleset with
+``bpf_lsm_policy_from_fd()`` and typically hands it over through a
+map kptr field; a sleepable LSM BPF program attached to the
+``bprm_creds_for_exec`` or ``bprm_creds_from_file`` hooks then
+enforces it on an execution with ``bpf_lsm_policy_apply_bprm()``.
+
+The restriction is staged in the Landlock blob of the credentials
+prepared for the execution and committed past the exec point of no
+return, so a failed execution leaves the calling task untouched. The
+commitment emits the ``landlock_enforce_domain`` trace event (see
+Documentation/trace/events-landlock.rst). The kfunc flags take the
+``landlock_restrict_self(2)`` flags with their usual semantics, with
+the exception of ``LANDLOCK_RESTRICT_SELF_TSYNC``, which is rejected:
+the restriction targets the execution, not the calling threads.
+
+``LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS`` makes the executed program
+start with no_new_privs set, binding it and all its descendants. It
+does not affect the current execution's privilege computation: the
+bprm credentials, including any setuid elevation, are computed before
+the flag is set.
+
+Unlike ``landlock_restrict_self(2)``, the bprm path has no
+no_new_privs/``CAP_SYS_ADMIN`` precondition: a task that is not
+no_new_privs can carry a BPF-applied domain, and its setuid
+executions still elevate while confined. This is sound because
+attaching the BPF program is itself privileged, granting the same
+power as the syscall's ``CAP_SYS_ADMIN`` carve-out.
+
+Executions may run concurrently: each takes its own reference on
+the shared ruleset with ``bpf_lsm_policy_acquire()``.
+
Tests
=====
diff --git a/Documentation/trace/events-landlock.rst b/Documentation/trace/events-landlock.rst
index af9267cca47d..6cec3194af0b 100644
--- a/Documentation/trace/events-landlock.rst
+++ b/Documentation/trace/events-landlock.rst
@@ -34,7 +34,10 @@ Landlock trace events are organized in four categories:
- ``landlock_add_rule_fs``: a filesystem rule is added to a ruleset
- ``landlock_add_rule_net``: a network port rule is added to a ruleset
- ``landlock_create_domain``: a new domain is created from a ruleset
-- ``landlock_enforce_domain``: a domain is enforced on a thread
+- ``landlock_enforce_domain``: a domain is enforced on a thread. Also
+ emitted at ``execve(2)``'s point of no return when a BPF program has
+ staged a policy on the execution (see the BPF kfuncs section of
+ Documentation/security/landlock.rst)
**Denial events** are emitted when an access is denied:
--
2.55.0
^ permalink raw reply related [flat|nested] 29+ messages in thread
* Re: [PATCH bpf-next v3 07/15] lsm: Add the bpf_lsm_policy_apply_bprm kfunc
2026-09-09 19:37 ` [PATCH bpf-next v3 07/15] lsm: Add the bpf_lsm_policy_apply_bprm kfunc Justin Suess
@ 2026-09-09 19:55 ` sashiko-bot
2026-09-09 20:20 ` Justin Suess
0 siblings, 1 reply; 29+ messages in thread
From: sashiko-bot @ 2026-09-09 19:55 UTC (permalink / raw)
To: Justin Suess; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] The attach-point authorization boundary for `bpf_lsm_policy_apply_bprm` can be bypassed using BPF tail calls.
--
commit 7b53e20161b20c35fb85beb1fba538fe85514074
Author: Justin Suess <utilityemal77@gmail.com>
Date: Wed Sep 9 19:37:10 2026 +0000
lsm: Add the bpf_lsm_policy_apply_bprm kfunc
This commit introduces the bpf_lsm_policy_apply_bprm kfunc, allowing sleepable
LSM BPF programs to restrict credentials prepared in a linux_binprm. It
restricts this kfunc to specific execution hooks (bprm_creds_for_exec and
bprm_creds_from_file) to prevent policies from being applied at unsafe points.
> diff --git a/security/bpf_lsm_kfuncs.c b/security/bpf_lsm_kfuncs.c
> index 43a4bf57fd31e..857e9a316d1cc 100644
> --- a/security/bpf_lsm_kfuncs.c
> +++ b/security/bpf_lsm_kfuncs.c
[ ... ]
> @@ -141,11 +202,18 @@ static int bpf_lsm_policy_kfunc_filter(const struct bpf_prog *prog,
>
> switch (prog->type) {
> case BPF_PROG_TYPE_SYSCALL:
> + if (kfunc_id == bpf_lsm_policy_apply_bprm_ids[0])
> + return -EACCES;
> return 0;
> case BPF_PROG_TYPE_LSM:
> if (kfunc_id == bpf_lsm_policy_from_fd_ids[0])
> return -EACCES;
>
> + if (kfunc_id == bpf_lsm_policy_apply_bprm_ids[0] &&
> + !btf_id_set_contains(&bpf_lsm_policy_bprm_hooks,
> + prog->aux->attach_btf_id))
> + return -EACCES;
[Severity: High]
Can the attach-point authorization boundary be bypassed using BPF tail calls?
This verification-time check inspects the initial program's attach point,
but does it account for runtime context switches via tail calls?
For example, if a privileged user loads Program A attached to
bprm_creds_for_exec that calls bpf_lsm_policy_apply_bprm(), it passes this
filter and can be inserted into a PROG_ARRAY map.
If they then load Program B attached to bprm_check_security and perform a
bpf_tail_call() into Program A, the tail call is permitted because both
hooks share identical BTF function prototypes (int (struct linux_binprm *)).
Does this allow bpf_lsm_policy_apply_bprm() to be executed at
bprm_check_security, creating a TOCTOU vulnerability since the execution
can still be rewritten by an interpreter?
> +
> return 0;
> default:
> return -EACCES;
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260909193719.518517-1-utilityemal77@gmail.com?part=7
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [PATCH bpf-next v3 07/15] lsm: Add the bpf_lsm_policy_apply_bprm kfunc
2026-09-09 19:55 ` sashiko-bot
@ 2026-09-09 20:20 ` Justin Suess
0 siblings, 0 replies; 29+ messages in thread
From: Justin Suess @ 2026-09-09 20:20 UTC (permalink / raw)
To: sashiko-reviews; +Cc: bpf, linux-security-module
On Wed, Sep 09, 2026 at 07:55:00PM +0000, sashiko-bot@kernel.org wrote:
> Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
> - [High] The attach-point authorization boundary for `bpf_lsm_policy_apply_bprm` can be bypassed using BPF tail calls.
> --
>
> commit 7b53e20161b20c35fb85beb1fba538fe85514074
> Author: Justin Suess <utilityemal77@gmail.com>
> Date: Wed Sep 9 19:37:10 2026 +0000
>
> lsm: Add the bpf_lsm_policy_apply_bprm kfunc
>
> This commit introduces the bpf_lsm_policy_apply_bprm kfunc, allowing sleepable
> LSM BPF programs to restrict credentials prepared in a linux_binprm. It
> restricts this kfunc to specific execution hooks (bprm_creds_for_exec and
> bprm_creds_from_file) to prevent policies from being applied at unsafe points.
>
> > diff --git a/security/bpf_lsm_kfuncs.c b/security/bpf_lsm_kfuncs.c
> > index 43a4bf57fd31e..857e9a316d1cc 100644
> > --- a/security/bpf_lsm_kfuncs.c
> > +++ b/security/bpf_lsm_kfuncs.c
>
> [ ... ]
>
> > @@ -141,11 +202,18 @@ static int bpf_lsm_policy_kfunc_filter(const struct bpf_prog *prog,
> >
> > switch (prog->type) {
> > case BPF_PROG_TYPE_SYSCALL:
> > + if (kfunc_id == bpf_lsm_policy_apply_bprm_ids[0])
> > + return -EACCES;
> > return 0;
> > case BPF_PROG_TYPE_LSM:
> > if (kfunc_id == bpf_lsm_policy_from_fd_ids[0])
> > return -EACCES;
> >
> > + if (kfunc_id == bpf_lsm_policy_apply_bprm_ids[0] &&
> > + !btf_id_set_contains(&bpf_lsm_policy_bprm_hooks,
> > + prog->aux->attach_btf_id))
> > + return -EACCES;
>
> [Severity: High]
> Can the attach-point authorization boundary be bypassed using BPF tail calls?
>
> This verification-time check inspects the initial program's attach point,
> but does it account for runtime context switches via tail calls?
>
> For example, if a privileged user loads Program A attached to
> bprm_creds_for_exec that calls bpf_lsm_policy_apply_bprm(), it passes this
> filter and can be inserted into a PROG_ARRAY map.
>
> If they then load Program B attached to bprm_check_security and perform a
> bpf_tail_call() into Program A, the tail call is permitted because both
> hooks share identical BTF function prototypes (int (struct linux_binprm *)).
>
> Does this allow bpf_lsm_policy_apply_bprm() to be executed at
> bprm_check_security, creating a TOCTOU vulnerability since the execution
> can still be rewritten by an interpreter?
>
Yes you can do this, but no it's not a vulnerability. No incorrect state
would be created here (in bprm_check_security, creds are still
uncommitted), so there's no issue in actually just letting a user apply
their policy there...
I'll probably just add bprm_check_security to the allowed hook list.
> > +
> > return 0;
> > default:
> > return -EACCES;
>
> --
> Sashiko AI review · https://sashiko.dev/#/patchset/20260909193719.518517-1-utilityemal77@gmail.com?part=7
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [PATCH bpf-next v3 11/15] landlock: Factor the credential restriction out of landlock_restrict_self()
2026-09-09 19:37 ` [PATCH bpf-next v3 11/15] landlock: Factor the credential restriction out of landlock_restrict_self() Justin Suess
@ 2026-09-09 20:29 ` bot+bpf-ci
0 siblings, 0 replies; 29+ messages in thread
From: bot+bpf-ci @ 2026-09-09 20:29 UTC (permalink / raw)
To: utilityemal77, ast, daniel, andrii, kpsingh, matt, paul, mic,
viro, brauner, kees
Cc: casey, gnoack, jack, song, yonghong.song, martin.lau, eddyz87,
memxor, jolsa, m, bpf, linux-security-module, linux-kernel,
utilityemal77, ast, andrii, daniel, martin.lau, eddyz87,
yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 5989 bytes --]
> diff --git a/security/landlock/cred.c b/security/landlock/cred.c
> index 03449c26247e..f02706f12c7d 100644
> --- a/security/landlock/cred.c
> +++ b/security/landlock/cred.c
> @@ -8,14 +8,146 @@
> */
>
> #include <linux/binfmts.h>
> +#include <linux/bits.h>
> #include <linux/cred.h>
> +#include <linux/err.h>
> +#include <linux/errno.h>
This isn't a bug, but is <linux/errno.h> needed here? The new code
returns PTR_ERR(new_dom) (from <linux/err.h>) and never names an -Exxx
constant.
> +#include <linux/lsm_hooks.h>
> +#include <linux/mutex.h>
> +#include <uapi/linux/landlock.h>
>
> #include "common.h"
> #include "cred.h"
> +#include "domain.h"
> #include "ruleset.h"
> #include "setup.h"
>
> +#include <trace/events/landlock.h>
> +
> +/**
> + * landlock_prepare_restriction - Compute a credential restriction
> + *
> + * @llcred: Landlock credentials to restrict: provides the parent domain and
> + * the previous log configuration. Not modified.
> + * @ruleset: Ruleset to enforce, or NULL for a log-configuration-only change.
> + * @flags: landlock_restrict_self(2) flags. The caller is responsible for
> + * validating them against the set of flags it supports.
> + * @restriction: Computed restriction. On success, holds a reference on
> + * @restriction->domain (if any), which
> + * landlock_apply_restriction() transfers to the restricted
> + * credentials.
> + *
> + * The restriction builds on @llcred's current state: the caller must apply
> + * it to (or stage it for) these same credentials.
> + *
> + * Return: 0 on success, -errno on failure.
> + */
> +int landlock_prepare_restriction(
> + const struct landlock_cred_security *const llcred,
> + struct landlock_ruleset *const ruleset, const u32 flags,
> + struct landlock_restriction *const restriction)
> +{
[ ... ]
> +#ifdef CONFIG_SECURITY_LANDLOCK_LOG
> + new_dom->hierarchy->log_same_exec = log_same_exec;
> + new_dom->hierarchy->log_new_exec = log_new_exec;
> + /*
> + * The creation event fired above, so move the domain out of
> + * LANDLOCK_LOG_UNCOMMITTED: its free_domain event must fire
> + * too, even if the domain is dropped before being enforced.
> + * Audit logging may still be disabled (DISABLED); tracing
> + * observes it anyway.
> + */
> + if ((!log_same_exec && !log_new_exec) || !prev_log_subdomains)
> + new_dom->hierarchy->log_status = LANDLOCK_LOG_DISABLED;
> + else
> + new_dom->hierarchy->log_status = LANDLOCK_LOG_PENDING;
> +#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
The LANDLOCK_LOG_UNCOMMITTED -> PENDING/DISABLED transition moves from
sys_landlock_restrict_self() into landlock_prepare_restriction(), but
comments that document this contract by naming the old site remain
unrevisioned, and no later commit in the 02756181d382..1cc3305917e4
range updates them either.
In security/landlock/domain.c (landlock_init_hierarchy_log):
/*
* The hierarchy is born unobservable: landlock_restrict_self() moves it
* out of LANDLOCK_LOG_UNCOMMITTED once it has emitted the creation
* event, so the matching free_domain event fires for it and not for a
* hierarchy whose creation was never observed.
*/
In security/landlock/trace.c (landlock_trace_free_domain):
* Fires only for a hierarchy whose creation event was emitted, i.e. one that
* left LANDLOCK_LOG_UNCOMMITTED in landlock_restrict_self(). ...
* while a domain that landlock_restrict_self() created and a
* thread-sync failure then aborted still fires free_domain, ...
In include/trace/events/landlock.h (Lifecycle consistency block):
The window is still described as 'A rare thread-sync failure aborts the
just-created domain' even though the new helper's documentation
(cred.c:89-93) states it also covers an aborted execution, introduced
two commits later in 0a88c757f4f0.
After this commit, landlock_prepare_restriction() performs the
transition, and 0a88c757f4f0 adds a second, non-syscall caller
(bprm_apply_policy_object()), so 'landlock_restrict_self()' no longer
enforces this invariant nor provides the only path that can create and
drop a domain.
Behaviour is unaffected, but should these references be updated to
reflect the new code structure?
> +
> + restriction->domain = new_dom;
> + return 0;
> +}
[ ... ]
> diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c
> index cb294a3582ae..9451376ccf50 100644
> --- a/security/landlock/syscalls.c
> +++ b/security/landlock/syscalls.c
> @@ -9,7 +9,6 @@
>
> #include <asm/current.h>
> #include <linux/anon_inodes.h>
> -#include <linux/bitops.h>
> #include <linux/build_bug.h>
> #include <linux/capability.h>
> #include <linux/cleanup.h>
[ ... ]
> @@ -546,10 +544,9 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
> struct landlock_ruleset *ruleset __free(landlock_put_ruleset) = NULL;
> struct landlock_domain *new_dom = NULL;
> struct cred *new_cred;
This isn't a bug, but is the `= NULL` initializer still needed here?
Before the refactor, new_dom was only assigned inside the if (ruleset)
block, so the initializer was load-bearing for the later if (ruleset)
trace_landlock_enforce_domain(new_dom, ...) call. After the split,
new_dom is assigned unconditionally from restriction.domain, and every
path between the declaration and that assignment is a plain return. It
is the only `= NULL` pointer initializer left in the file apart from the
__free() declaration on the line above.
> - struct landlock_cred_security *new_llcred;
> + struct landlock_restriction restriction;
> bool process_wide;
> - bool __maybe_unused log_same_exec, log_new_exec, log_subdomains,
> - prev_log_subdomains;
> + int err;
[ ... ]
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/34398515657
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [PATCH bpf-next v3 04/15] lsm: Add the bpf_lsm_policy_release kfunc and policy object destructor
2026-09-09 19:37 ` [PATCH bpf-next v3 04/15] lsm: Add the bpf_lsm_policy_release kfunc and policy object destructor Justin Suess
@ 2026-09-09 20:29 ` bot+bpf-ci
2026-09-09 21:34 ` Paul Moore
1 sibling, 0 replies; 29+ messages in thread
From: bot+bpf-ci @ 2026-09-09 20:29 UTC (permalink / raw)
To: utilityemal77, ast, daniel, andrii, kpsingh, matt, paul, mic,
viro, brauner, kees
Cc: casey, gnoack, jack, song, yonghong.song, martin.lau, eddyz87,
memxor, jolsa, m, bpf, linux-security-module, linux-kernel,
utilityemal77, ast, andrii, daniel, martin.lau, eddyz87,
yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 2746 bytes --]
> Add security/bpf_lsm_kfuncs.c, the home of the kfuncs exposing LSM
> policy objects to BPF programs, with the first of them:
>
> bpf_lsm_policy_release(object) KF_RELEASE
>
> The kfuncs are the LSM framework's own BPF interface: there is no
> per-LSM kfunc and no intermediate security_*() layer. Each kfunc
> walks the matching hook's implementation list and calls the one
> registered by the LSM whose lsmid the policy object carries. Calling
> a kfunc for an LSM that is not active or has no policy object support
> fails at runtime rather than hiding the kfunc at verification time,
> so BPF program loading is independent of the boot-time LSM
> configuration.
>
> A policy object reference is meant to be handed over through a map
> kptr field, so also register a destructor for struct
> lsm_policy_object: map-held references are dropped on map teardown,
> possibly from a context that cannot sleep, which the
> policy_object_put() hook contract accounts for. For the same reason
> the kfunc is not KF_SLEEPABLE, and the filter adds no per-kfunc rule:
> releasing a reference must be allowed wherever one can be held. The
> filter itself is needed because BPF_PROG_TYPE_LSM and
> BPF_PROG_TYPE_SYSCALL, the two registered program types, share their
> kfunc lookup buckets with other program types.
Is the justification for the kfunc filter fully accurate?
Looking at bpf_prog_type_to_kfunc_hook() in kernel/bpf/btf.c, bucket
selection shows:
case BPF_PROG_TYPE_TRACING:
case BPF_PROG_TYPE_TRACEPOINT:
case BPF_PROG_TYPE_RAW_TRACEPOINT:
case BPF_PROG_TYPE_PERF_EVENT:
case BPF_PROG_TYPE_LSM:
return BTF_KFUNC_HOOK_TRACING;
case BPF_PROG_TYPE_SYSCALL:
return BTF_KFUNC_HOOK_SYSCALL;
BPF_PROG_TYPE_LSM does share BTF_KFUNC_HOOK_TRACING with TRACING,
TRACEPOINT, RAW_TRACEPOINT and PERF_EVENT program types, so the filter
is required for the register_btf_kfunc_id_set(BPF_PROG_TYPE_LSM, ...)
call.
However, BPF_PROG_TYPE_SYSCALL is the sole program type mapping to
BTF_KFUNC_HOOK_SYSCALL. No other program type can reach that bucket, so
the filter's SYSCALL arm simply allows the program type that already
owns the bucket.
The code itself is correct and matches the pattern used by
bpf_ksock_kfunc_filter in net/core/bpf_ksock.c, which registers for the
same two program types.
Should the changelog be narrowed to say the filter is needed for
BPF_PROG_TYPE_LSM specifically, since that's the one sharing its bucket
with other program types?
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/34398515657
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [PATCH bpf-next v3 06/15] lsm: Add the bpf_lsm_policy_acquire kfunc
2026-09-09 19:37 ` [PATCH bpf-next v3 06/15] lsm: Add the bpf_lsm_policy_acquire kfunc Justin Suess
@ 2026-09-09 20:30 ` bot+bpf-ci
0 siblings, 0 replies; 29+ messages in thread
From: bot+bpf-ci @ 2026-09-09 20:30 UTC (permalink / raw)
To: utilityemal77, ast, daniel, andrii, kpsingh, matt, paul, mic,
viro, brauner, kees
Cc: casey, gnoack, jack, song, yonghong.song, martin.lau, eddyz87,
memxor, jolsa, m, bpf, linux-security-module, linux-kernel,
utilityemal77, ast, andrii, daniel, martin.lau, eddyz87,
yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 2066 bytes --]
> diff --git a/security/bpf_lsm_kfuncs.c b/security/bpf_lsm_kfuncs.c
> index 988dcd6f4dd9..43a4bf57fd31 100644
> --- a/security/bpf_lsm_kfuncs.c
> +++ b/security/bpf_lsm_kfuncs.c
> @@ -14,6 +14,36 @@
>
> __bpf_kfunc_start_defs();
>
> +/**
> + * bpf_lsm_policy_acquire - Acquire a reference on a shared policy object
> + * @object: RCU-protected pointer to a policy object, e.g. loaded from
> + * a map kptr field under bpf_rcu_read_lock()
> + *
> + * Acquire a reference of its own on a policy object the program does
> + * not own, so that any number of concurrent program executions can
> + * use the object shared through one map kptr field, without emptying
> + * it as bpf_kptr_xchg() would. The returned reference stays valid
> + * after bpf_rcu_read_unlock() and must be released with
> + * bpf_lsm_policy_release().
> + *
> + * Return: A referenced policy object, or NULL if the object's
> + * reference count concurrently dropped to zero.
> + */
This isn't a bug, but would it be worth spelling out the other NULL
cases here -- no enabled LSM owning @object->lsmid, or the owning LSM's
get hook failing -- the way bpf_lsm_policy_from_fd()'s Return: section
does just below?
The current text names one of the three ways the kfunc yields NULL. The
body also returns NULL when no enabled LSM's policy_object_get static
call matches @object->lsmid, and when the owning LSM's hook fails for a
reason other than a dead refcount.
> +__bpf_kfunc struct lsm_policy_object *
> +bpf_lsm_policy_acquire(struct lsm_policy_object *object)
> +{
> + struct lsm_static_call *scall;
> +
> + lsm_for_each_hook(scall, policy_object_get) {
> + if (scall->hl->lsmid->id != object->lsmid)
> + continue;
> + if (scall->hl->hook.policy_object_get(object))
> + return NULL;
> + return object;
> + }
> + return NULL;
> +}
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/34398515657
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [PATCH bpf-next v3 09/15] selftests/bpf: Add tests for the LSM policy object kfuncs
2026-09-09 19:37 ` [PATCH bpf-next v3 09/15] selftests/bpf: Add tests for the LSM policy object kfuncs Justin Suess
@ 2026-09-09 20:30 ` bot+bpf-ci
0 siblings, 0 replies; 29+ messages in thread
From: bot+bpf-ci @ 2026-09-09 20:30 UTC (permalink / raw)
To: utilityemal77, ast, daniel, andrii, kpsingh, matt, paul, mic,
viro, brauner, kees
Cc: casey, gnoack, jack, song, yonghong.song, martin.lau, eddyz87,
memxor, jolsa, m, bpf, linux-security-module, linux-kernel,
utilityemal77, ast, andrii, daniel, martin.lau, eddyz87,
yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 4728 bytes --]
> diff --git a/tools/testing/selftests/bpf/prog_tests/lsm_policy_kfuncs.c b/tools/testing/selftests/bpf/prog_tests/lsm_policy_kfuncs.c
> new file mode 100644
> index 000000000000..9f4ffb5f47be
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/prog_tests/lsm_policy_kfuncs.c
> @@ -0,0 +1,54 @@
[ ... ]
> +static void test_from_fd_null(void)
> +{
> + LIBBPF_OPTS(bpf_test_run_opts, opts);
> + struct lsm_policy_kfuncs *skel;
> + char tmp_path[] = "/tmp/lsm_policy_kfuncs_XXXXXX";
> + int tmp_fd, err;
> +
> + tmp_fd = mkstemp(tmp_path);
> + if (!ASSERT_GE(tmp_fd, 0, "mkstemp"))
> + return;
> +
> + skel = lsm_policy_kfuncs__open_and_load();
> + if (!ASSERT_OK_PTR(skel, "skel_open_and_load"))
> + goto out_close;
> + skel->bss->plain_fd = tmp_fd;
> +
> + err = bpf_prog_test_run_opts(bpf_program__fd(skel->progs.check_from_fd),
> + &opts);
> + if (!ASSERT_OK(err, "check_from_fd_run") ||
> + !ASSERT_OK(opts.retval, "check_from_fd_retval"))
> + goto out_destroy;
> +
> + ASSERT_TRUE(skel->bss->got_null_for_bad_fd, "bad_fd_null");
> + ASSERT_TRUE(skel->bss->got_null_for_plain_fd, "plain_fd_null");
> + ASSERT_TRUE(skel->bss->got_null_for_bad_flags, "bad_flags_null");
^^^^^^^^^^^^^^^^
A subsystem pattern flags this as potentially concerning: the
"bad_flags_null" assertion passes whether or not the kernel honors the
reserved-flags contract.
The fd used for the flags test is the plain temporary file created above,
which no LSM claims. In bpf_lsm_policy_from_fd() (security/bpf_lsm_kfuncs.c)
there are two independent paths to NULL for this input:
if (flags)
return NULL; /* the check under test */
lsm_for_each_hook(scall, policy_object_from_fd) {
err = scall->hl->hook.policy_object_from_fd(fd, &object);
if (err == -EOPNOTSUPP)
continue;
...
}
return NULL; /* unrelated: nobody claims a /tmp file */
Because the /tmp file is claimed by no LSM, the loop falls through to the
trailing return NULL on its own. Delete the if (flags) guard entirely and
"bad_flags_null" still passes. The assertion therefore holds whether or not
the kernel implements the reserved-flags contract that the commit message
claims this test verifies.
The other two arms do discriminate once an LSM implements the hook: for
fd == -1 the owning LSM's policy_object_from_fd() fails with a
non-EOPNOTSUPP error, and for a plain fd the LSM must return -EOPNOTSUPP
rather than claiming the file. Only the flags arm is unconditioned.
A discriminating flags test needs an fd that would resolve successfully
with flags == 0, which by design belongs in the LSM-specific test rather
than here. Could the commit message and comment clarify that the
reserved-flags contract is only partially covered here, or could this test
be deferred to an LSM that provides real policy objects?
> diff --git a/tools/testing/selftests/bpf/progs/lsm_policy_kfuncs_failure.c b/tools/testing/selftests/bpf/progs/lsm_policy_kfuncs_failure.c
> new file mode 100644
> index 000000000000..04080838aefd
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/progs/lsm_policy_kfuncs_failure.c
> @@ -0,0 +1,154 @@
[ ... ]
> +/*
> + * The fd kfunc is exclusive to syscall programs: it must be rejected
> + * in an LSM program, even on an allowed hook.
> + */
> +SEC("lsm.s/bprm_creds_for_exec")
> +__failure __msg("calling kernel function bpf_lsm_policy_from_fd is not allowed")
> +int BPF_PROG(lsm_get, struct linux_binprm *bprm)
^^^^^^^^
[ ... ]
> +/*
> + * The enforcement kfunc is exclusive to the sleepable bprm LSM
> + * hooks: it must be rejected in a syscall program.
> + */
> +SEC("syscall")
> +__failure __msg("calling kernel function bpf_lsm_policy_apply_bprm is not allowed")
> +int syscall_restrict(void *ctx)
^^^^^^^^^^^^^^^
This isn't a bug, but six of the eight programs in this file are named
after the condition they pin down (tracing_prog, wrong_hook,
nonsleepable_prog, leak_policy, acquire_untrusted, leak_shared_policy),
which makes a RUN_TESTS failure line self-explanatory. The other two are
named after the program type plus an operation that doesn't appear in the
interface: lsm_get and syscall_restrict. There is no 'get' or 'restrict'
kfunc. Would names in the same style as the neighbours - e.g. lsm_from_fd
and syscall_apply_bprm - make the failing subtest easier to place from
the RUN_TESTS output?
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/34398515657
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [PATCH bpf-next v3 05/15] lsm: Add the bpf_lsm_policy_from_fd kfunc
2026-09-09 19:37 ` [PATCH bpf-next v3 05/15] lsm: Add the bpf_lsm_policy_from_fd kfunc Justin Suess
@ 2026-09-09 20:46 ` bot+bpf-ci
0 siblings, 0 replies; 29+ messages in thread
From: bot+bpf-ci @ 2026-09-09 20:46 UTC (permalink / raw)
To: utilityemal77, ast, daniel, andrii, kpsingh, matt, paul, mic,
viro, brauner, kees
Cc: casey, gnoack, jack, song, yonghong.song, martin.lau, eddyz87,
memxor, jolsa, m, bpf, linux-security-module, linux-kernel,
utilityemal77, ast, andrii, daniel, martin.lau, eddyz87,
yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 2481 bytes --]
> diff --git a/security/bpf_lsm_kfuncs.c b/security/bpf_lsm_kfuncs.c
> index e1190215d477..988dcd6f4dd9 100644
> --- a/security/bpf_lsm_kfuncs.c
> +++ b/security/bpf_lsm_kfuncs.c
[ ... ]
> @@ -14,11 +14,50 @@
>
> __bpf_kfunc_start_defs();
>
> +/**
> + * bpf_lsm_policy_from_fd - Get an LSM policy object from a fd
> + * @fd: file descriptor referring to a policy object, resolved in the
> + * file descriptor table of the task running the program
> + * @flags: reserved for future use, must be 0
> + *
> + * Translate @fd, as set up through the owning LSM's own userspace
> + * interface, into a referenced policy object. The fd identifies the
> + * LSM asked to translate it: each LSM recognizes its own fds and
> + * declines every other. Only syscall programs may call this kfunc:
> + * they run in the context of the task invoking them, where the fd is
> + * meaningful. The reference must be released with
> + * bpf_lsm_policy_release().
> + *
> + * Return: A referenced policy object, or NULL if @flags is not 0, if
> + * no enabled LSM recognizes @fd as one of its policy objects, or if
> + * the recognizing LSM fails to translate it.
> + */
> +__bpf_kfunc struct lsm_policy_object *bpf_lsm_policy_from_fd(int fd, u32 flags)
> +{
> + struct lsm_static_call *scall;
> + struct lsm_policy_object *object;
> + int err;
> +
> + if (flags)
> + return NULL;
> +
> + lsm_for_each_hook(scall, policy_object_from_fd) {
> + err = scall->hl->hook.policy_object_from_fd(fd, &object);
> + if (err == -EOPNOTSUPP)
> + /* Not this LSM's fd: let another claim it. */
> + continue;
> + if (err)
> + return NULL;
> + return object;
> + }
> + return NULL;
> +}
> +
> /**
> * bpf_lsm_policy_release - Release a policy object reference
> * @object: policy object to release
> *
> - * Release an acquired reference on a policy object.
> + * Release a reference acquired with bpf_lsm_policy_from_fd().
> */
This isn't a bug, but would keeping the more general wording here
('Release an acquired reference on a policy object') age better than
listing the acquiring kfuncs? A reference can also come out of a map kptr
field via bpf_kptr_xchg(), so the comment might need amending again when
other acquiring kfuncs are added.
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/34398515657
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [PATCH bpf-next v3 14/15] selftests/bpf: Test the LSM policy object kfuncs with Landlock
2026-09-09 19:37 ` [PATCH bpf-next v3 14/15] selftests/bpf: Test the LSM policy object kfuncs with Landlock Justin Suess
@ 2026-09-09 20:46 ` bot+bpf-ci
0 siblings, 0 replies; 29+ messages in thread
From: bot+bpf-ci @ 2026-09-09 20:46 UTC (permalink / raw)
To: utilityemal77, ast, daniel, andrii, kpsingh, matt, paul, mic,
viro, brauner, kees
Cc: casey, gnoack, jack, song, yonghong.song, martin.lau, eddyz87,
memxor, jolsa, m, bpf, linux-security-module, linux-kernel,
utilityemal77, ast, andrii, daniel, martin.lau, eddyz87,
yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 3957 bytes --]
> diff --git a/tools/testing/selftests/bpf/prog_tests/lsm_policy_landlock.c b/tools/testing/selftests/bpf/prog_tests/lsm_policy_landlock.c
> new file mode 100644
> index 000000000000..f01ae5be8aef
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/prog_tests/lsm_policy_landlock.c
> @@ -0,0 +1,525 @@
[ ... ]
> +static int setup_env(struct policy_test_env *env)
> +{
> + env->skel = NULL;
> + env->ruleset_fd = -1;
> + strcpy(env->tmp_path, "/tmp/lsm_policy_landlock_XXXXXX");
> + env->tmp_fd = mkstemp(env->tmp_path);
> + if (!ASSERT_GE(env->tmp_fd, 0, "mkstemp"))
> + return -1;
> +
> + env->ruleset_fd = create_ruleset();
> + if (env->ruleset_fd < 0) {
> + if (errno == EOPNOTSUPP || errno == ENOSYS)
> + test__skip();
^^^^^^^^^^^^
A selftests pattern flags this as potentially concerning: skipping with no
message, so the output carries no hint about which prerequisite is absent.
All five subtests funnel through this path, and both errno values look
classified correctly here - EOPNOTSUPP when Landlock is built but not
listed in CONFIG_LSM or in lsm= at boot, ENOSYS when
CONFIG_SECURITY_LANDLOCK=n. What is not visible in the output is which of
the two happened; a reader sees five skips and cannot tell a config
ordering problem from the feature not being built at all.
Against that, test__skip() in the bpf framework takes no message argument,
and a bare test__skip() is the usual idiom across prog_tests/, so carrying
a reason means adding a separate print rather than passing one to an
existing API.
Would a one-line print naming the missing prerequisite before test__skip()
be worth adding?
> + else
> + ASSERT_GE(env->ruleset_fd, 0,
> + "landlock_create_ruleset");
> + return -1;
> + }
> +
> + env->skel = lsm_policy_landlock__open_and_load();
> + if (!ASSERT_OK_PTR(env->skel, "skel_open_and_load"))
> + return -1;
[ ... ]
> +/*
> + * Exit codes: 4 = unexpected write outcome, 0 = everything as
> + * expected.
> + */
> +static void format_child_cmd(char *cmd, size_t len, bool expect_write_ok,
> + const char *tmp_path)
> +{
> + if (expect_write_ok)
> + snprintf(cmd, len, "echo x > %s || exit 4; exit 0", tmp_path);
> + else
> + snprintf(cmd, len,
> + "if echo x > %s 2>/dev/null; then exit 4; fi; exit 0",
^^^^^^^^^^^^^
Is the redirect order here inverted? A shell applies redirections left to
right, so the failure of "> %s" is reported before 2>/dev/null has been
applied, and the diagnostic goes to the inherited fd 2:
$ sh -c 'if echo x > /nodir123/f 2>/dev/null; then exit 4; fi; exit 0'
sh: 1: cannot create /nodir123/f: Directory nonexistent
$ sh -c 'if echo x 2>/dev/null > /nodir123/f; then exit 4; fi; exit 0'
(no output)
bash behaves the same way, reporting "/nodir123/f: No such file or
directory" for the first form.
Does that message then land in the selftest output? test_progs.c only
reassigns the stdout FILE*:
tools/testing/selftests/bpf/test_progs.c:stdio_hijack_init() {
...
fflush(stdout);
fflush(stderr);
stdout = open_memstream(log_buf, log_cnt);
...
}
and never redirects fd 1 or fd 2 themselves, while the writer here is a
forked child that inherited the real fd 2. So each restricted execution
would print
sh: 1: cannot create /tmp/lsm_policy_landlock_XXXXXX: Permission denied
which is seven executions across test_restrict_binprm(),
test_restrict_binprm_concurrent() and test_restrict_binprm_trace(). The
child's exit status is still 0, so the assertions are unaffected and this
is stray output rather than a false failure.
Would "echo x 2>/dev/null > %s" do what this was written to do?
> + tmp_path);
> +}
[ ... ]
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/34398515657
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [PATCH bpf-next v3 12/15] landlock: Free rulesets after an RCU grace period
2026-09-09 19:37 ` [PATCH bpf-next v3 12/15] landlock: Free rulesets after an RCU grace period Justin Suess
@ 2026-09-09 20:46 ` bot+bpf-ci
0 siblings, 0 replies; 29+ messages in thread
From: bot+bpf-ci @ 2026-09-09 20:46 UTC (permalink / raw)
To: utilityemal77, ast, daniel, andrii, kpsingh, matt, paul, mic,
viro, brauner, kees
Cc: casey, gnoack, jack, song, yonghong.song, martin.lau, eddyz87,
memxor, jolsa, m, bpf, linux-security-module, linux-kernel,
utilityemal77, ast, andrii, daniel, martin.lau, eddyz87,
yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 9916 bytes --]
> diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
> index 34d88ce37dc7..a65e9bce94d4 100644
> --- a/security/landlock/ruleset.c
> +++ b/security/landlock/ruleset.c
[ ... ]
> @@ -346,9 +347,26 @@ static void free_ruleset(struct landlock_ruleset *const ruleset)
> kfree(ruleset);
> }
>
> -void landlock_put_ruleset(struct landlock_ruleset *const ruleset)
> +static void free_ruleset_work(struct work_struct *const work)
> {
> - might_sleep();
> + struct rcu_work *const rcu_work = to_rcu_work(work);
> + struct landlock_ruleset *const ruleset =
> + container_of(rcu_work, struct landlock_ruleset, work_free);
> +
> + free_ruleset(ruleset);
> +}
> +
> +/*
> + * landlock_put_ruleset - Release ruleset reference
> + *
> + * @ruleset: Ruleset to release, or NULL.
> + *
> + * Release a reference. When the last reference is dropped, the ruleset is
> + * freed after an RCU grace period.
> + */
> +void landlock_put_ruleset(struct landlock_ruleset *const ruleset)
> +{
> if (ruleset && refcount_dec_and_test(&ruleset->usage)) {
> - trace_landlock_free_ruleset(ruleset);
> - free_ruleset(ruleset);
> + INIT_RCU_WORK(&ruleset->work_free, free_ruleset_work);
> + queue_rcu_work(system_dfl_wq, &ruleset->work_free);
> }
> }
Deferring the free moves the landlock_free_ruleset tracepoint out of the
caller's context: trace_landlock_free_ruleset() is emitted from
free_ruleset(), which after this patch only runs from free_ruleset_work()
on a kworker, an RCU grace period after the last put.
That changes an observable, in-tree-tested property of the tracepoint,
and the patch does not update the test that asserts the old behaviour, so
TEST_F(trace, free_ruleset_on_close) in
tools/testing/selftests/landlock/trace_test.c now fails deterministically
for three independent reasons:
1. PID filtering drops the event entirely. The fixture calls
tracefs_set_pid_filter(getpid()), which writes set_event_pid. ftrace
records an event only when the *current* task's pid is in that list. The
freeing kworker is a kthread forked from kthreadd, so neither
set_event_pid nor options/event-fork ever adds it, and the
landlock_free_ruleset record is discarded. The existing free_domain tests
handle exactly this by clearing the PID filter before triggering the
deferred free.
2. No wait for the grace period + workqueue. The test reads the buffer
synchronously right after close(ruleset_fd):
close(ruleset_fd);
buf = tracefs_read_buf();
The free_domain tests instead poll (for (int retry = 0; retry < 10;
retry++) { usleep(100000); ... }) precisely because the event now arrives
later.
3. Wrong task regex. The assertion uses REGEX_FREE_RULESET(TRACE_TASK),
but the record, if it were captured at all, carries a kworker name.
trace.h already defines KWORKER_TASK for exactly this case.
Net effect: EXPECT_EQ(1, tracefs_count_matches(buf,
REGEX_FREE_RULESET(TRACE_TASK))) sees 0. The selftest needs the same
treatment the free_domain tests already got (clear the PID filter, poll,
match KWORKER_TASK), otherwise the landlock kselftest suite regresses
with this commit.
---
The tracepoint documentation for landlock_free_ruleset is now stale and
actively misleading for tracer authors. include/trace/events/landlock.h
still says the event is "Emitted when a ruleset's last reference is
dropped (typically when the creating process closes the ruleset file
descriptor)", which after this patch is no longer the emission point: the
last put only queues rcu_work, and the event fires from a kworker one RCU
grace period later. Documentation/trace/events-landlock.rst likewise
lists only "``landlock_free_ruleset``: a ruleset is freed".
The tree already establishes the convention of spelling this out for the
sibling event: the landlock_free_domain kernel-doc states "A committed
domain is freed from a kworker via landlock_put_domain_deferred() ... so
the current task is not the sandboxed task that triggered the free", and
Documentation/trace/events-landlock.rst repeats it with a kworker example
line. landlock_free_ruleset now has the same property and neither
document was updated, so consumers keep assuming the event is attributable
to the task that closed the fd (which is precisely the assumption the
in-tree selftest encodes).
> diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h
> index c12b6e46fa3f..3e730e90ce06 100644
> --- a/security/landlock/ruleset.h
> +++ b/security/landlock/ruleset.h
[ ... ]
> @@ -175,22 +174,41 @@ struct landlock_ruleset {
> /**
> * @usage: Number of alive &struct landlock_cred pointing to this
> * ruleset and also the reference counter for user space. This must
> - * remain valid while the object can be retrieved from an RCU
> - * protected pointer.
> + * remain readable while the object can be retrieved from an RCU
> + * protected pointer.
> */
> refcount_t usage;
> - /**
> - * @lock: Guards modifications to the ruleset. Must not be taken under
> - * rcu_read_lock, but after refcount_inc_not_zero has succeeded. An
> - * RCU reader that needs the lock should bail (e.g., return -EAGAIN to
> - * user space) instead of trying to take it.
> - */
> - struct mutex lock;
> - /**
> - * @quiet_masks: Access rights to skip logging for, per layer level.
> - */
> - struct access_masks quiet_masks;
> - /** @handled_masks: Access rights supported by the ruleset. */
> - struct access_masks handled_masks;
> + union {
> + /**
> + * @work_free: Scheduled via queue_rcu_work when @usage
> + * drops to zero. By the time this work item runs, every
> + * RCU reader that could still observe a pointer to the
> + * containing struct landlock_ruleset is guaranteed to be
> + * done. (INIT_RCU_WORK initializes both the inner
> + * work_struct and the surrounding rcu_work).
> + * The fields @lock, @quiet_masks and @handled_masks are
> + * then unused, so overlay this work item with them to
> + * keep sizeof(struct landlock_ruleset) down.
> + */
> + struct rcu_work work_free;
> + struct {
> + /**
> + * @lock: Guards modifications to the ruleset. Must not
> + * be taken under rcu_read_lock, but after
> + * refcount_inc_not_zero has succeeded. An RCU reader
> + * that needs the lock should bail (e.g., return -EAGAIN
> + * to user space) instead of trying to take it.
> + */
> + struct mutex lock;
> + /**
> + * @quiet_masks: Access rights to skip logging for, per
> + * layer level.
> + */
> + struct access_masks quiet_masks;
> + /**
> + * @handled_masks: Access rights supported by the
> + * ruleset.
> + */
> + struct access_masks handled_masks;
> + };
> + };
The union makes @lock, @quiet_masks and @handled_masks alias the queued
rcu_work, and trace_landlock_free_ruleset() is emitted from
free_ruleset() *while that work item is executing*, so at trace time
those three fields hold workqueue bookkeeping bytes, not ruleset state.
That matters because include/trace/events/landlock.h explicitly
advertises reading the traced pointer's fields beyond TP_STRUCT__entry:
"eBPF programs read these pointers via BTF for richer introspection than
the TP_STRUCT__entry fields". A tracer that pairs landlock_create_ruleset
(whose TP_STRUCT__entry reads handled_masks.fs/net/scope) with
landlock_free_ruleset and reads ruleset->handled_masks or
ruleset->quiet_masks via BTF gets rcu_work contents. The commit message
and the @work_free kernel-doc justify the overlay with "The fields @lock,
@quiet_masks and @handled_masks are then unused", which holds for
in-kernel code but not for the BTF field access the header sanctions.
There is no in-tree precedent that would make this acceptable by analogy:
struct landlock_domain uses the same overlay trick but free_domain()
emits no tracepoint on the unioned object -- trace_landlock_free_domain()
is passed a struct landlock_hierarchy, which has no union. Either the
overlay should exclude the fields the trace interface exposes, or the
"Pointer access" documentation should state that only @usage, @version
and @id are valid at landlock_free_ruleset time.
---
> landlock: Free rulesets after an RCU grace period
>
> Defer every ruleset free behind an RCU grace period, and keep the
> fields that stay readable while a free is pending out of the union
> that overlays the deferred-free work item.
> The policy_object_get LSM hook lets a caller holding only an
> RCU-protected pointer to a ruleset (e.g. loaded from a BPF map kptr
> field under rcu_read_lock()) race a refcount_inc_not_zero() against
> the drop of the last reference. For that to be sound, the ruleset's
> memory, and its reference count in particular, must remain valid
> until every RCU reader that could still observe the pointer is done:
> free the ruleset through queue_rcu_work(), which waits for a grace
> period before running the free work.
> The work item is overlaid with the fields that no one may touch once
> @usage reaches zero: @lock, @quiet_masks and @handled_masks. @usage
> itself stays outside the union so a racing reader observes zero
> instead of the work item's bytes, and the tracing fields @version and
> @id stay outside too because the landlock_free_ruleset trace event
> reads them when the queued work finally runs.
> Since queueing the work never sleeps, the might_sleep() annotation
> is dropped: a following commit releases ruleset references from
> BPF object destructors that cannot sleep.
> Cc: Mickaël Salaün <mic@digikod.net>
> Signed-off-by: Justin Suess <utilityemal77@gmail.com>
This looks like a bug fix for stack argument support. Should this
include:
Fixes: 7a529e9b4e96 ("lsm: Add the LSM policy object lifetime hooks")
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/34398515657
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [PATCH bpf-next v3 13/15] landlock: Implement the LSM policy object hooks
2026-09-09 19:37 ` [PATCH bpf-next v3 13/15] landlock: Implement the LSM policy object hooks Justin Suess
@ 2026-09-09 20:46 ` bot+bpf-ci
0 siblings, 0 replies; 29+ messages in thread
From: bot+bpf-ci @ 2026-09-09 20:46 UTC (permalink / raw)
To: utilityemal77, ast, daniel, andrii, kpsingh, matt, paul, mic,
viro, brauner, kees
Cc: casey, gnoack, jack, song, yonghong.song, martin.lau, eddyz87,
memxor, jolsa, m, bpf, linux-security-module, linux-kernel,
utilityemal77, ast, andrii, daniel, martin.lau, eddyz87,
yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 4479 bytes --]
> diff --git a/security/landlock/cred.h b/security/landlock/cred.h
> index 88fa97fc3bd2..9a2971197892 100644
> --- a/security/landlock/cred.h
> +++ b/security/landlock/cred.h
> @@ -59,6 +59,16 @@ struct landlock_cred_security {
> */
> struct landlock_domain *domain;
>
> +#ifdef CONFIG_BPF_LSM
> + /**
> + * @staged: Restriction staged by the bprm_apply_policy_object() hook,
> + * owning its domain reference, applied at bprm_committing_creds().
> + * Only ever set on credentials prepared for an execution; committed
> + * task credentials never carry a staged restriction.
> + */
> + struct landlock_restriction staged;
> +#endif /* CONFIG_BPF_LSM */
> +
> #ifdef CONFIG_SECURITY_LANDLOCK_LOG
Does adding the staged field here grow struct landlock_file_security for
every open file?
struct landlock_cred_security is embedded by value in the file blob:
security/landlock/fs.h:88-90:
struct landlock_cred_security fown_subject;
and security/landlock/setup.c sets .lbs_file = sizeof(struct
landlock_file_security), so that blob is allocated for every struct
file.
struct landlock_restriction is 16 bytes (domain pointer + u32 flags +
tail padding), so adding it grows landlock_cred_security from 11 bytes
(8+2+1) to 27 bytes (8+16+2+1), making struct landlock_file_security
grow from 32 to 48 bytes (+50%) when CONFIG_BPF_LSM=y.
The struct's kernel-doc says "This structure is packed to minimize the
size of struct landlock_file_security," but the new field is documented
as "Only ever set on credentials prepared for an execution; committed
task credentials never carry a staged restriction," so it is always
zero in the file blob.
hook_file_set_fowner() at security/landlock/fs.c copies this blob with
raw struct assignments, so the 16 exec-only bytes are copied on every
fcntl(F_SETOWN) and socket-owner set. hook_file_free_security() only
touches fown_subject.domain, never staged.
CONFIG_BPF_LSM=y is enabled by mainstream distributions alongside
CONFIG_SECURITY_LANDLOCK, making this the common configuration. Could
the exec-only staging state live in a separate structure that is not
embedded in the per-file blob?
> diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h
> index 1465f8a5c464..22edb140bb7f 100644
> --- a/security/landlock/ruleset.h
> +++ b/security/landlock/ruleset.h
[ ... ]
> @@ -146,6 +147,16 @@ struct landlock_rules {
> u32 num_rules;
> };
>
> +#ifdef CONFIG_BPF_LSM
> +/*
> + * Landlock's lsm_policy_object types. The namespace is private to
> + * Landlock; 0 stays reserved as "unset".
> + */
> +enum landlock_policy_type {
> + LANDLOCK_POLICY_TYPE_RULESET = 1,
> +};
> +#endif /* CONFIG_BPF_LSM */
> +
> /**
> * struct landlock_ruleset - Landlock ruleset
> *
> @@ -157,6 +168,17 @@ struct landlock_ruleset {
> * @rules: Red-black tree storage for rules.
> */
> struct landlock_rules rules;
> +
> +#ifdef CONFIG_BPF_LSM
> + /**
> + * @policy_object: Identity under which the ruleset is handed out
> + * to BPF programs as a referenced kptr: the LSM policy kfuncs
> + * dispatch back to Landlock through its lsmid. Kept outside the
> + * union with @work_free: RCU readers may read its lsmid while a
> + * queued free waits out the grace period.
> + */
> + struct lsm_policy_object policy_object;
> +#endif /* CONFIG_BPF_LSM */
> /**
> * @usage: Number of file descriptors referencing this ruleset. Kept
> * outside the union with @work_free: RCU readers may still call
Should the @usage documentation be updated?
The comment says "Number of file descriptors referencing this ruleset,"
but after this patch a reference can be held with no file descriptor
involved at all.
hook_policy_object_from_fd() calls landlock_get_ruleset_from_fd() to
increment @usage, then the BPF program parks that owned reference in a
map kptr field with bpf_kptr_xchg(). Userspace can close the ruleset fd
afterwards, leaving @usage at 1 with zero file descriptors referencing
the ruleset. hook_policy_object_get() also increments the refcount for
an RCU reader holding no fd.
The rest of this struct's documentation is maintained precisely (three
members carry "Kept outside the union with @work_free" rationales), so
the stale enumeration stands out.
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/34398515657
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [PATCH bpf-next v3 04/15] lsm: Add the bpf_lsm_policy_release kfunc and policy object destructor
2026-09-09 19:37 ` [PATCH bpf-next v3 04/15] lsm: Add the bpf_lsm_policy_release kfunc and policy object destructor Justin Suess
2026-09-09 20:29 ` bot+bpf-ci
@ 2026-09-09 21:34 ` Paul Moore
2026-09-09 22:20 ` Justin Suess
1 sibling, 1 reply; 29+ messages in thread
From: Paul Moore @ 2026-09-09 21:34 UTC (permalink / raw)
To: Justin Suess
Cc: ast, daniel, andrii, kpsingh, matt, mic, viro, brauner, kees,
casey, gnoack, jack, song, yonghong.song, martin.lau, eddyz87,
memxor, jolsa, m, bpf, linux-security-module, linux-kernel
On Wed, Sep 9, 2026 at 3:37 PM Justin Suess <utilityemal77@gmail.com> wrote:
>
> Add security/bpf_lsm_kfuncs.c, the home of the kfuncs exposing LSM
> policy objects to BPF programs, with the first of them:
>
> bpf_lsm_policy_release(object) KF_RELEASE
>
> The kfuncs are the LSM framework's own BPF interface: there is no
> per-LSM kfunc and no intermediate security_*() layer. Each kfunc
> walks the matching hook's implementation list and calls the one
> registered by the LSM whose lsmid the policy object carries. Calling
> a kfunc for an LSM that is not active or has no policy object support
> fails at runtime rather than hiding the kfunc at verification time,
> so BPF program loading is independent of the boot-time LSM
> configuration.
>
> A policy object reference is meant to be handed over through a map
> kptr field, so also register a destructor for struct
> lsm_policy_object: map-held references are dropped on map teardown,
> possibly from a context that cannot sleep, which the
> policy_object_put() hook contract accounts for. For the same reason
> the kfunc is not KF_SLEEPABLE, and the filter adds no per-kfunc rule:
> releasing a reference must be allowed wherever one can be held. The
> filter itself is needed because BPF_PROG_TYPE_LSM and
> BPF_PROG_TYPE_SYSCALL, the two registered program types, share their
> kfunc lookup buckets with other program types.
>
> Cc: Paul Moore <paul@paul-moore.com>
> Cc: KP Singh <kpsingh@kernel.org>
> Signed-off-by: Justin Suess <utilityemal77@gmail.com>
> ---
>
> Notes:
> v2->v3:
> - No change.
>
> MAINTAINERS | 1 +
> security/Makefile | 2 +-
> security/bpf_lsm_kfuncs.c | 98 +++++++++++++++++++++++++++++++++++++++
> 3 files changed, 100 insertions(+), 1 deletion(-)
> create mode 100644 security/bpf_lsm_kfuncs.c
While not a new concern, it has been a few months so I want to mention
it again to see where things stand with the BPF crowd: are the BPF
folks still opposed to BPF kfuncs in security/bpf_lsm_kfuncs.c,
similar to fs/bpf_fs_kfuncs.c?
If the answer remains yes, the BPF devs are not able to tolerate
kfuncs under security/, then I'm afraid this patchset is stuck.
However, if the BPF devs are willing to ACK kfuncs in
security/bpf_lsm_kfuncs.c, I'll add this patchset to the review queue
for the LSM tree.
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 6215fcb07770..ba816d7ee127 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -5081,6 +5081,7 @@ F: kernel/bpf/bpf_lsm.c
> F: kernel/bpf/bpf_lsm_proto.c
> F: kernel/trace/bpf_trace.c
> F: security/bpf/
> +F: security/bpf_lsm_kfuncs.c
We would need the maintenence of security/bpf_lsm_kfuncs.c to fall to
the LSM framework, just as the maintenence of bpf_fs_funcs.c falls to
the VFS.
% ./scripts/get_maintainer.pl -f fs/bpf_fs_kfuncs.c
Alexander Viro
Christian Brauner
Jan Kara
linux-fsdevel@vger
linux-kernel@vger
--
paul-moore.com
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [PATCH bpf-next v3 04/15] lsm: Add the bpf_lsm_policy_release kfunc and policy object destructor
2026-09-09 21:34 ` Paul Moore
@ 2026-09-09 22:20 ` Justin Suess
2026-09-09 23:08 ` Paul Moore
0 siblings, 1 reply; 29+ messages in thread
From: Justin Suess @ 2026-09-09 22:20 UTC (permalink / raw)
To: Paul Moore
Cc: ast, daniel, andrii, kpsingh, matt, mic, viro, brauner, kees,
casey, gnoack, jack, song, yonghong.song, martin.lau, eddyz87,
memxor, jolsa, m, bpf, linux-security-module, linux-kernel
On Wed, Sep 09, 2026 at 05:34:31PM -0400, Paul Moore wrote:
> On Wed, Sep 9, 2026 at 3:37 PM Justin Suess <utilityemal77@gmail.com> wrote:
> >
> > Add security/bpf_lsm_kfuncs.c, the home of the kfuncs exposing LSM
> > policy objects to BPF programs, with the first of them:
> >
> > bpf_lsm_policy_release(object) KF_RELEASE
> >
> > The kfuncs are the LSM framework's own BPF interface: there is no
> > per-LSM kfunc and no intermediate security_*() layer. Each kfunc
> > walks the matching hook's implementation list and calls the one
> > registered by the LSM whose lsmid the policy object carries. Calling
> > a kfunc for an LSM that is not active or has no policy object support
> > fails at runtime rather than hiding the kfunc at verification time,
> > so BPF program loading is independent of the boot-time LSM
> > configuration.
> >
> > A policy object reference is meant to be handed over through a map
> > kptr field, so also register a destructor for struct
> > lsm_policy_object: map-held references are dropped on map teardown,
> > possibly from a context that cannot sleep, which the
> > policy_object_put() hook contract accounts for. For the same reason
> > the kfunc is not KF_SLEEPABLE, and the filter adds no per-kfunc rule:
> > releasing a reference must be allowed wherever one can be held. The
> > filter itself is needed because BPF_PROG_TYPE_LSM and
> > BPF_PROG_TYPE_SYSCALL, the two registered program types, share their
> > kfunc lookup buckets with other program types.
> >
> > Cc: Paul Moore <paul@paul-moore.com>
> > Cc: KP Singh <kpsingh@kernel.org>
> > Signed-off-by: Justin Suess <utilityemal77@gmail.com>
> > ---
> >
> > Notes:
> > v2->v3:
> > - No change.
> >
> > MAINTAINERS | 1 +
> > security/Makefile | 2 +-
> > security/bpf_lsm_kfuncs.c | 98 +++++++++++++++++++++++++++++++++++++++
> > 3 files changed, 100 insertions(+), 1 deletion(-)
> > create mode 100644 security/bpf_lsm_kfuncs.c
>
> While not a new concern, it has been a few months so I want to mention
> it again to see where things stand with the BPF crowd: are the BPF
> folks still opposed to BPF kfuncs in security/bpf_lsm_kfuncs.c,
> similar to fs/bpf_fs_kfuncs.c?
>
> If the answer remains yes, the BPF devs are not able to tolerate
> kfuncs under security/, then I'm afraid this patchset is stuck.
> However, if the BPF devs are willing to ACK kfuncs in
> security/bpf_lsm_kfuncs.c, I'll add this patchset to the review queue
> for the LSM tree.
>
To avocate for the current placement:
Placing these kfuncs outside where it is in security/ would
be very awkward, the kfuncs use lsm internal APIs
(lsm_for_each_hook, active static keys, lsm dispatch table) that
have no buisness outside the LSM framework.
And logically, the kfuncs operate entirely on in-tree lsm objects.
> > diff --git a/MAINTAINERS b/MAINTAINERS
> > index 6215fcb07770..ba816d7ee127 100644
> > --- a/MAINTAINERS
> > +++ b/MAINTAINERS
> > @@ -5081,6 +5081,7 @@ F: kernel/bpf/bpf_lsm.c
> > F: kernel/bpf/bpf_lsm_proto.c
> > F: kernel/trace/bpf_trace.c
> > F: security/bpf/
> > +F: security/bpf_lsm_kfuncs.c
>
> We would need the maintenence of security/bpf_lsm_kfuncs.c to fall to
> the LSM framework, just as the maintenence of bpf_fs_funcs.c falls to
> the VFS.
>
Agreed.
I intend that any security/ kfuncs go through both LSM and BPF
review at a minimum. As a bug in any of these kfuncs or their permitted
calling context / signatures could cause regressions in either subsystem.
Justin
> % ./scripts/get_maintainer.pl -f fs/bpf_fs_kfuncs.c
> Alexander Viro
> Christian Brauner
> Jan Kara
> linux-fsdevel@vger
> linux-kernel@vger
>
> --
> paul-moore.com
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [PATCH bpf-next v3 04/15] lsm: Add the bpf_lsm_policy_release kfunc and policy object destructor
2026-09-09 22:20 ` Justin Suess
@ 2026-09-09 23:08 ` Paul Moore
0 siblings, 0 replies; 29+ messages in thread
From: Paul Moore @ 2026-09-09 23:08 UTC (permalink / raw)
To: Justin Suess
Cc: ast, daniel, andrii, kpsingh, matt, mic, viro, brauner, kees,
casey, gnoack, jack, song, yonghong.song, martin.lau, eddyz87,
memxor, jolsa, m, bpf, linux-security-module, linux-kernel
On Wed, Sep 9, 2026 at 6:21 PM Justin Suess <utilityemal77@gmail.com> wrote:
> On Wed, Sep 09, 2026 at 05:34:31PM -0400, Paul Moore wrote:
> > On Wed, Sep 9, 2026 at 3:37 PM Justin Suess <utilityemal77@gmail.com> wrote:
> > >
> > > Add security/bpf_lsm_kfuncs.c, the home of the kfuncs exposing LSM
> > > policy objects to BPF programs, with the first of them:
> > >
> > > bpf_lsm_policy_release(object) KF_RELEASE
> > >
> > > The kfuncs are the LSM framework's own BPF interface: there is no
> > > per-LSM kfunc and no intermediate security_*() layer. Each kfunc
> > > walks the matching hook's implementation list and calls the one
> > > registered by the LSM whose lsmid the policy object carries. Calling
> > > a kfunc for an LSM that is not active or has no policy object support
> > > fails at runtime rather than hiding the kfunc at verification time,
> > > so BPF program loading is independent of the boot-time LSM
> > > configuration.
> > >
> > > A policy object reference is meant to be handed over through a map
> > > kptr field, so also register a destructor for struct
> > > lsm_policy_object: map-held references are dropped on map teardown,
> > > possibly from a context that cannot sleep, which the
> > > policy_object_put() hook contract accounts for. For the same reason
> > > the kfunc is not KF_SLEEPABLE, and the filter adds no per-kfunc rule:
> > > releasing a reference must be allowed wherever one can be held. The
> > > filter itself is needed because BPF_PROG_TYPE_LSM and
> > > BPF_PROG_TYPE_SYSCALL, the two registered program types, share their
> > > kfunc lookup buckets with other program types.
> > >
> > > Cc: Paul Moore <paul@paul-moore.com>
> > > Cc: KP Singh <kpsingh@kernel.org>
> > > Signed-off-by: Justin Suess <utilityemal77@gmail.com>
> > > ---
> > >
> > > Notes:
> > > v2->v3:
> > > - No change.
> > >
> > > MAINTAINERS | 1 +
> > > security/Makefile | 2 +-
> > > security/bpf_lsm_kfuncs.c | 98 +++++++++++++++++++++++++++++++++++++++
> > > 3 files changed, 100 insertions(+), 1 deletion(-)
> > > create mode 100644 security/bpf_lsm_kfuncs.c
> >
> > While not a new concern, it has been a few months so I want to mention
> > it again to see where things stand with the BPF crowd: are the BPF
> > folks still opposed to BPF kfuncs in security/bpf_lsm_kfuncs.c,
> > similar to fs/bpf_fs_kfuncs.c?
> >
> > If the answer remains yes, the BPF devs are not able to tolerate
> > kfuncs under security/, then I'm afraid this patchset is stuck.
> > However, if the BPF devs are willing to ACK kfuncs in
> > security/bpf_lsm_kfuncs.c, I'll add this patchset to the review queue
> > for the LSM tree.
> >
> To avocate for the current placement:
>
> Placing these kfuncs outside where it is in security/ would
> be very awkward, the kfuncs use lsm internal APIs
> (lsm_for_each_hook, active static keys, lsm dispatch table) that
> have no buisness outside the LSM framework.
>
> And logically, the kfuncs operate entirely on in-tree lsm objects.
>
> > > diff --git a/MAINTAINERS b/MAINTAINERS
> > > index 6215fcb07770..ba816d7ee127 100644
> > > --- a/MAINTAINERS
> > > +++ b/MAINTAINERS
> > > @@ -5081,6 +5081,7 @@ F: kernel/bpf/bpf_lsm.c
> > > F: kernel/bpf/bpf_lsm_proto.c
> > > F: kernel/trace/bpf_trace.c
> > > F: security/bpf/
> > > +F: security/bpf_lsm_kfuncs.c
> >
> > We would need the maintenence of security/bpf_lsm_kfuncs.c to fall to
> > the LSM framework, just as the maintenence of bpf_fs_funcs.c falls to
> > the VFS.
>
> Agreed.
>
> I intend that any security/ kfuncs go through both LSM and BPF
> review at a minimum. As a bug in any of these kfuncs or their permitted
> calling context / signatures could cause regressions in either subsystem.
While some other subsystems are lax on this, it's general practice in
the LSM space to *really* try to get ACK's from other affected
subsystems before merging. Sometimes we move forward anyway, e.g.
patches being ignored for almost a year, but those are exceptions
rather than the rule.
I only bring this up because the MAINTAINERS entry was pointed at the
BPF devs, not the LSM devs. I would expect that patches going into
security/bpf_lsm_kfuncs.c would need to be ACK'd both the LSM
framework and BPF LSM maintainers.
--
paul-moore.com
^ permalink raw reply [flat|nested] 29+ messages in thread
end of thread, other threads:[~2026-09-09 23:08 UTC | newest]
Thread overview: 29+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-09 19:37 [PATCH bpf-next v3 00/15] BPF interface for applying Landlock rulesets Justin Suess
2026-09-09 19:37 ` [PATCH bpf-next v3 01/15] lsm: Add the LSM policy object lifetime hooks Justin Suess
2026-09-09 19:37 ` [PATCH bpf-next v3 02/15] lsm: Add the bprm_apply_policy_object LSM hook Justin Suess
2026-09-09 19:37 ` [PATCH bpf-next v3 03/15] lsm: Move the lsm_for_each_hook() macro to security/lsm.h Justin Suess
2026-09-09 19:37 ` [PATCH bpf-next v3 04/15] lsm: Add the bpf_lsm_policy_release kfunc and policy object destructor Justin Suess
2026-09-09 20:29 ` bot+bpf-ci
2026-09-09 21:34 ` Paul Moore
2026-09-09 22:20 ` Justin Suess
2026-09-09 23:08 ` Paul Moore
2026-09-09 19:37 ` [PATCH bpf-next v3 05/15] lsm: Add the bpf_lsm_policy_from_fd kfunc Justin Suess
2026-09-09 20:46 ` bot+bpf-ci
2026-09-09 19:37 ` [PATCH bpf-next v3 06/15] lsm: Add the bpf_lsm_policy_acquire kfunc Justin Suess
2026-09-09 20:30 ` bot+bpf-ci
2026-09-09 19:37 ` [PATCH bpf-next v3 07/15] lsm: Add the bpf_lsm_policy_apply_bprm kfunc Justin Suess
2026-09-09 19:55 ` sashiko-bot
2026-09-09 20:20 ` Justin Suess
2026-09-09 19:37 ` [PATCH bpf-next v3 08/15] lsm: Document the LSM policy object interface Justin Suess
2026-09-09 19:37 ` [PATCH bpf-next v3 09/15] selftests/bpf: Add tests for the LSM policy object kfuncs Justin Suess
2026-09-09 20:30 ` bot+bpf-ci
2026-09-09 19:37 ` [PATCH bpf-next v3 10/15] landlock: Expose the ruleset fd lookup to the rest of Landlock Justin Suess
2026-09-09 19:37 ` [PATCH bpf-next v3 11/15] landlock: Factor the credential restriction out of landlock_restrict_self() Justin Suess
2026-09-09 20:29 ` bot+bpf-ci
2026-09-09 19:37 ` [PATCH bpf-next v3 12/15] landlock: Free rulesets after an RCU grace period Justin Suess
2026-09-09 20:46 ` bot+bpf-ci
2026-09-09 19:37 ` [PATCH bpf-next v3 13/15] landlock: Implement the LSM policy object hooks Justin Suess
2026-09-09 20:46 ` bot+bpf-ci
2026-09-09 19:37 ` [PATCH bpf-next v3 14/15] selftests/bpf: Test the LSM policy object kfuncs with Landlock Justin Suess
2026-09-09 20:46 ` bot+bpf-ci
2026-09-09 19:37 ` [PATCH bpf-next v3 15/15] landlock: Document the BPF policy interface Justin Suess
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.