* [PATCH v2 01/15] lsm: Add the LSM policy object lifetime hooks
2026-08-31 14:58 [PATCH v2 00/15] BPF interface for applying Landlock rulesets Justin Suess
@ 2026-08-31 14:58 ` Justin Suess
2026-08-31 17:17 ` Casey Schaufler
2026-09-02 13:05 ` Justin Suess
2026-08-31 14:58 ` [PATCH v2 02/15] lsm: Add the bprm_apply_policy_object LSM hook Justin Suess
` (13 subsequent siblings)
14 siblings, 2 replies; 23+ messages in thread
From: Justin Suess @ 2026-08-31 14:58 UTC (permalink / raw)
To: ast, daniel, andrii, kpsingh, paul, mic, viro, brauner, kees
Cc: gnoack, jack, song, yonghong.song, martin.lau, m, bpf,
linux-security-module, linux-kernel, Justin Suess,
Casey Schaufler
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>
---
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 1433809bb166..d06744d72e04 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] 23+ messages in thread* Re: [PATCH v2 01/15] lsm: Add the LSM policy object lifetime hooks
2026-08-31 14:58 ` [PATCH v2 01/15] lsm: Add the LSM policy object lifetime hooks Justin Suess
@ 2026-08-31 17:17 ` Casey Schaufler
2026-08-31 17:41 ` Justin Suess
2026-09-02 13:05 ` Justin Suess
1 sibling, 1 reply; 23+ messages in thread
From: Casey Schaufler @ 2026-08-31 17:17 UTC (permalink / raw)
To: Justin Suess, ast, daniel, andrii, kpsingh, paul, mic, viro,
brauner, kees
Cc: gnoack, jack, song, yonghong.song, martin.lau, m, bpf,
linux-security-module, linux-kernel, Casey Schaufler
On 8/31/2026 7:58 AM, Justin Suess wrote:
> 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:
I am not seeing the cover letter for this patch set. Are you sending
it to the LSM list? Without that I do not have sufficient context to
comment on the patches (1,2,3 and 8) that you have sent.
What I do see has me quite concerned. It's possible the cover letter
will addresses my issues. If nothing else, I assume it describes what
a "policy object" is, and what it's for.
^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: [PATCH v2 01/15] lsm: Add the LSM policy object lifetime hooks
2026-08-31 17:17 ` Casey Schaufler
@ 2026-08-31 17:41 ` Justin Suess
0 siblings, 0 replies; 23+ messages in thread
From: Justin Suess @ 2026-08-31 17:41 UTC (permalink / raw)
To: Casey Schaufler
Cc: ast, daniel, andrii, kpsingh, paul, mic, viro, brauner, kees,
gnoack, jack, song, yonghong.song, martin.lau, m, bpf,
linux-security-module, linux-kernel
On Mon, Aug 31, 2026 at 10:17:43AM -0700, Casey Schaufler wrote:
> On 8/31/2026 7:58 AM, Justin Suess wrote:
> > 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:
>
> I am not seeing the cover letter for this patch set. Are you sending
> it to the LSM list? Without that I do not have sufficient context to
> comment on the patches (1,2,3 and 8) that you have sent.
>
> What I do see has me quite concerned. It's possible the cover letter
> will addresses my issues. If nothing else, I assume it describes what
> a "policy object" is, and what it's for.
>
Indeed it did get sent to linux-security-module, it's up on lore as
such.
https://lore.kernel.org/linux-security-module/20260831145858.3869191-1-utilityemal77@gmail.com/
Possibly got caught in your spam filter?
Indeed it does provide important context about lsm_policy_object.
Thanks,
Justin
^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: [PATCH v2 01/15] lsm: Add the LSM policy object lifetime hooks
2026-08-31 14:58 ` [PATCH v2 01/15] lsm: Add the LSM policy object lifetime hooks Justin Suess
2026-08-31 17:17 ` Casey Schaufler
@ 2026-09-02 13:05 ` Justin Suess
2026-09-02 17:51 ` Casey Schaufler
1 sibling, 1 reply; 23+ messages in thread
From: Justin Suess @ 2026-09-02 13:05 UTC (permalink / raw)
To: ast, daniel, andrii, kpsingh, paul, mic, viro, brauner, kees
Cc: gnoack, jack, song, yonghong.song, martin.lau, m, bpf,
linux-security-module, linux-kernel, Casey Schaufler
On Mon, Aug 31, 2026 at 10:58:43AM -0400, Justin Suess wrote:
> 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>
> ---
> 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;
> +};
For some clarity:
lsm_policy_object is just a handle to a refcounted lsm-private struct.
It can't be forged / created manually because it's a trusted kernel
pointer, so the only way to get it is through policy_object_from_fd.
And you cannot mutate any part of it from BPF.
But it's what enables the generic model. Calling it a "policy object"
may be short sighted though, that term is heavily overloaded in the LSM
space. I don't want to prescribe any restrictions on what an LSM can use
it for, after all some LSM have no notion of "policy" at all or have a
different meaning for it.
For SELinux, this "lsm_policy_object" could be an sid, for AppArmor an aa_label,
for Smack a label, etc. The intention was to allow writing programs that
don't care about any details of a particular LSM.
Justin
> +
> 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 1433809bb166..d06744d72e04 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 [flat|nested] 23+ messages in thread* Re: [PATCH v2 01/15] lsm: Add the LSM policy object lifetime hooks
2026-09-02 13:05 ` Justin Suess
@ 2026-09-02 17:51 ` Casey Schaufler
2026-09-02 18:28 ` Justin Suess
0 siblings, 1 reply; 23+ messages in thread
From: Casey Schaufler @ 2026-09-02 17:51 UTC (permalink / raw)
To: Justin Suess, ast, daniel, andrii, kpsingh, paul, mic, viro,
brauner, kees
Cc: gnoack, jack, song, yonghong.song, martin.lau, m, bpf,
linux-security-module, linux-kernel, Casey Schaufler
On 9/2/2026 6:05 AM, Justin Suess wrote:
> On Mon, Aug 31, 2026 at 10:58:43AM -0400, Justin Suess wrote:
>> 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>
>> ---
>> 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;
>> +};
> For some clarity:
>
> lsm_policy_object is just a handle to a refcounted lsm-private struct.
>
> It can't be forged / created manually because it's a trusted kernel
> pointer, so the only way to get it is through policy_object_from_fd.
> And you cannot mutate any part of it from BPF.
>
> But it's what enables the generic model. Calling it a "policy object"
> may be short sighted though, that term is heavily overloaded in the LSM
> space. I don't want to prescribe any restrictions on what an LSM can use
> it for, after all some LSM have no notion of "policy" at all or have a
> different meaning for it.
>
> For SELinux, this "lsm_policy_object" could be an sid, for AppArmor an aa_label,
> for Smack a label, etc. The intention was to allow writing programs that
> don't care about any details of a particular LSM.
Why aren't you using an lsm_prop pointer? What if the operation you're
planning to perform is relevant to multiple active LSMs? Today you can't
use SELinux and AppArmor together on an upstream Linus kernel, but that
should be changing sometime this decade.
^ permalink raw reply [flat|nested] 23+ messages in thread* Re: [PATCH v2 01/15] lsm: Add the LSM policy object lifetime hooks
2026-09-02 17:51 ` Casey Schaufler
@ 2026-09-02 18:28 ` Justin Suess
0 siblings, 0 replies; 23+ messages in thread
From: Justin Suess @ 2026-09-02 18:28 UTC (permalink / raw)
To: Casey Schaufler
Cc: ast, daniel, andrii, kpsingh, paul, mic, viro, brauner, kees,
gnoack, jack, song, yonghong.song, martin.lau, m, bpf,
linux-security-module, linux-kernel
On Wed, Sep 02, 2026 at 10:51:37AM -0700, Casey Schaufler wrote:
> On 9/2/2026 6:05 AM, Justin Suess wrote:
> > On Mon, Aug 31, 2026 at 10:58:43AM -0400, Justin Suess wrote:
> >> 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>
> >> ---
> >> 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;
> >> +};
> > For some clarity:
> >
> > lsm_policy_object is just a handle to a refcounted lsm-private struct.
> >
> > It can't be forged / created manually because it's a trusted kernel
> > pointer, so the only way to get it is through policy_object_from_fd.
> > And you cannot mutate any part of it from BPF.
> >
> > But it's what enables the generic model. Calling it a "policy object"
> > may be short sighted though, that term is heavily overloaded in the LSM
> > space. I don't want to prescribe any restrictions on what an LSM can use
> > it for, after all some LSM have no notion of "policy" at all or have a
> > different meaning for it.
> >
> > For SELinux, this "lsm_policy_object" could be an sid, for AppArmor an aa_label,
> > for Smack a label, etc. The intention was to allow writing programs that
> > don't care about any details of a particular LSM.
>
> Why aren't you using an lsm_prop pointer? What if the operation you're
So lsm_prop from my understanding allows multiple LSMs to store their
data for one *shared* kernel object in a structured way.
(i.e one object; multiple lsms using it)
lsm_policy_object is sort of the opposite case:
In this case, the object is *owned* by a particular LSM. Think a
Landlock ruleset, smack label, or aa_label etc, that could never
foreeably have a semantic meaning to other LSM.
> planning to perform is relevant to multiple active LSMs? Today you can't
> use SELinux and AppArmor together on an upstream Linus kernel, but that
> should be changing sometime this decade.
>
The object is not relevant to other LSMs by construction. The
lsm_policy_object is an lsm internal object, specific to the LSM.
You'd never make an lsm_policy_object for an inode, dentry, socket, etc.
So if you wanted to use LSM A and LSM B with this API, you'd need to
acquire each of their policy objects in turn. But you apply them both
with the same API, without having to care about LSM details.
Justin
^ permalink raw reply [flat|nested] 23+ messages in thread
* [PATCH v2 02/15] lsm: Add the bprm_apply_policy_object LSM hook
2026-08-31 14:58 [PATCH v2 00/15] BPF interface for applying Landlock rulesets Justin Suess
2026-08-31 14:58 ` [PATCH v2 01/15] lsm: Add the LSM policy object lifetime hooks Justin Suess
@ 2026-08-31 14:58 ` Justin Suess
2026-08-31 14:58 ` [PATCH v2 03/15] lsm: Move the lsm_for_each_hook() macro to security/lsm.h Justin Suess
` (12 subsequent siblings)
14 siblings, 0 replies; 23+ messages in thread
From: Justin Suess @ 2026-08-31 14:58 UTC (permalink / raw)
To: ast, daniel, andrii, kpsingh, paul, mic, viro, brauner, kees
Cc: gnoack, jack, song, yonghong.song, martin.lau, m, bpf,
linux-security-module, linux-kernel, Justin Suess,
Casey Schaufler
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>
---
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 d06744d72e04..d5859553f7f8 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] 23+ messages in thread* [PATCH v2 03/15] lsm: Move the lsm_for_each_hook() macro to security/lsm.h
2026-08-31 14:58 [PATCH v2 00/15] BPF interface for applying Landlock rulesets Justin Suess
2026-08-31 14:58 ` [PATCH v2 01/15] lsm: Add the LSM policy object lifetime hooks Justin Suess
2026-08-31 14:58 ` [PATCH v2 02/15] lsm: Add the bprm_apply_policy_object LSM hook Justin Suess
@ 2026-08-31 14:58 ` Justin Suess
2026-08-31 14:58 ` [PATCH v2 04/15] lsm: Add the bpf_lsm_policy_release kfunc and policy object destructor Justin Suess
` (11 subsequent siblings)
14 siblings, 0 replies; 23+ messages in thread
From: Justin Suess @ 2026-08-31 14:58 UTC (permalink / raw)
To: ast, daniel, andrii, kpsingh, paul, mic, viro, brauner, kees
Cc: gnoack, jack, song, yonghong.song, martin.lau, m, bpf,
linux-security-module, linux-kernel, Justin Suess,
Casey Schaufler
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>
---
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 71aea8fdf014..0a6fa21cc31b 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] 23+ messages in thread* [PATCH v2 04/15] lsm: Add the bpf_lsm_policy_release kfunc and policy object destructor
2026-08-31 14:58 [PATCH v2 00/15] BPF interface for applying Landlock rulesets Justin Suess
` (2 preceding siblings ...)
2026-08-31 14:58 ` [PATCH v2 03/15] lsm: Move the lsm_for_each_hook() macro to security/lsm.h Justin Suess
@ 2026-08-31 14:58 ` Justin Suess
2026-08-31 14:58 ` [PATCH v2 05/15] lsm: Add the bpf_lsm_policy_from_fd kfunc Justin Suess
` (10 subsequent siblings)
14 siblings, 0 replies; 23+ messages in thread
From: Justin Suess @ 2026-08-31 14:58 UTC (permalink / raw)
To: ast, daniel, andrii, kpsingh, paul, mic, viro, brauner, kees
Cc: gnoack, jack, song, yonghong.song, martin.lau, 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>
---
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 f5301c30ea91..2af6a25a1399 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -5037,6 +5037,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] 23+ messages in thread* [PATCH v2 05/15] lsm: Add the bpf_lsm_policy_from_fd kfunc
2026-08-31 14:58 [PATCH v2 00/15] BPF interface for applying Landlock rulesets Justin Suess
` (3 preceding siblings ...)
2026-08-31 14:58 ` [PATCH v2 04/15] lsm: Add the bpf_lsm_policy_release kfunc and policy object destructor Justin Suess
@ 2026-08-31 14:58 ` Justin Suess
2026-08-31 14:58 ` [PATCH v2 06/15] lsm: Add the bpf_lsm_policy_acquire kfunc Justin Suess
` (9 subsequent siblings)
14 siblings, 0 replies; 23+ messages in thread
From: Justin Suess @ 2026-08-31 14:58 UTC (permalink / raw)
To: ast, daniel, andrii, kpsingh, paul, mic, viro, brauner, kees
Cc: gnoack, jack, song, yonghong.song, martin.lau, 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>
---
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] 23+ messages in thread* [PATCH v2 06/15] lsm: Add the bpf_lsm_policy_acquire kfunc
2026-08-31 14:58 [PATCH v2 00/15] BPF interface for applying Landlock rulesets Justin Suess
` (4 preceding siblings ...)
2026-08-31 14:58 ` [PATCH v2 05/15] lsm: Add the bpf_lsm_policy_from_fd kfunc Justin Suess
@ 2026-08-31 14:58 ` Justin Suess
2026-08-31 14:58 ` [PATCH v2 07/15] lsm: Add the bpf_lsm_policy_apply_bprm kfunc Justin Suess
` (8 subsequent siblings)
14 siblings, 0 replies; 23+ messages in thread
From: Justin Suess @ 2026-08-31 14:58 UTC (permalink / raw)
To: ast, daniel, andrii, kpsingh, paul, mic, viro, brauner, kees
Cc: gnoack, jack, song, yonghong.song, martin.lau, 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>
---
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 7aa47342dc65..ba9972c572e1 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -4394,6 +4394,9 @@ BTF_ID(struct, task_struct)
#ifdef CONFIG_CRYPTO
BTF_ID(struct, bpf_crypto_ctx)
#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] 23+ messages in thread* [PATCH v2 07/15] lsm: Add the bpf_lsm_policy_apply_bprm kfunc
2026-08-31 14:58 [PATCH v2 00/15] BPF interface for applying Landlock rulesets Justin Suess
` (5 preceding siblings ...)
2026-08-31 14:58 ` [PATCH v2 06/15] lsm: Add the bpf_lsm_policy_acquire kfunc Justin Suess
@ 2026-08-31 14:58 ` Justin Suess
2026-08-31 14:58 ` [PATCH v2 08/15] lsm: Document the LSM policy object interface Justin Suess
` (7 subsequent siblings)
14 siblings, 0 replies; 23+ messages in thread
From: Justin Suess @ 2026-08-31 14:58 UTC (permalink / raw)
To: ast, daniel, andrii, kpsingh, paul, mic, viro, brauner, kees
Cc: gnoack, jack, song, yonghong.song, martin.lau, 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.
Cc: Paul Moore <paul@paul-moore.com>
Cc: KP Singh <kpsingh@kernel.org>
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
security/bpf_lsm_kfuncs.c | 72 +++++++++++++++++++++++++++++++++++++++
1 file changed, 72 insertions(+)
diff --git a/security/bpf_lsm_kfuncs.c b/security/bpf_lsm_kfuncs.c
index 43a4bf57fd31..743752b5852e 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,8 @@ 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.
*/
static int bpf_lsm_policy_kfunc_filter(const struct bpf_prog *prog,
u32 kfunc_id)
@@ -141,11 +198,26 @@ 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]) {
+ /*
+ * BPF_LSM_CGROUP programs run under classic
+ * RCU and cannot sleep.
+ */
+ if (prog->expected_attach_type == BPF_LSM_CGROUP)
+ return -EACCES;
+
+ if (!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] 23+ messages in thread* [PATCH v2 08/15] lsm: Document the LSM policy object interface
2026-08-31 14:58 [PATCH v2 00/15] BPF interface for applying Landlock rulesets Justin Suess
` (6 preceding siblings ...)
2026-08-31 14:58 ` [PATCH v2 07/15] lsm: Add the bpf_lsm_policy_apply_bprm kfunc Justin Suess
@ 2026-08-31 14:58 ` Justin Suess
2026-08-31 14:58 ` [PATCH v2 09/15] selftests/bpf: Add tests for the LSM policy object kfuncs Justin Suess
` (6 subsequent siblings)
14 siblings, 0 replies; 23+ messages in thread
From: Justin Suess @ 2026-08-31 14:58 UTC (permalink / raw)
To: ast, daniel, andrii, kpsingh, paul, mic, viro, brauner, kees
Cc: gnoack, jack, song, yonghong.song, martin.lau, m, bpf,
linux-security-module, linux-kernel, Justin Suess,
Casey Schaufler
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>
---
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] 23+ messages in thread* [PATCH v2 09/15] selftests/bpf: Add tests for the LSM policy object kfuncs
2026-08-31 14:58 [PATCH v2 00/15] BPF interface for applying Landlock rulesets Justin Suess
` (7 preceding siblings ...)
2026-08-31 14:58 ` [PATCH v2 08/15] lsm: Document the LSM policy object interface Justin Suess
@ 2026-08-31 14:58 ` Justin Suess
2026-08-31 14:58 ` [PATCH v2 10/15] landlock: Expose the ruleset fd lookup to the rest of Landlock Justin Suess
` (5 subsequent siblings)
14 siblings, 0 replies; 23+ messages in thread
From: Justin Suess @ 2026-08-31 14:58 UTC (permalink / raw)
To: ast, daniel, andrii, kpsingh, paul, mic, viro, brauner, kees
Cc: gnoack, jack, song, yonghong.song, martin.lau, 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>
---
.../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] 23+ messages in thread* [PATCH v2 10/15] landlock: Expose the ruleset fd lookup to the rest of Landlock
2026-08-31 14:58 [PATCH v2 00/15] BPF interface for applying Landlock rulesets Justin Suess
` (8 preceding siblings ...)
2026-08-31 14:58 ` [PATCH v2 09/15] selftests/bpf: Add tests for the LSM policy object kfuncs Justin Suess
@ 2026-08-31 14:58 ` Justin Suess
2026-08-31 14:58 ` [PATCH v2 11/15] landlock: Factor the credential restriction out of landlock_restrict_self() Justin Suess
` (4 subsequent siblings)
14 siblings, 0 replies; 23+ messages in thread
From: Justin Suess @ 2026-08-31 14:58 UTC (permalink / raw)
To: ast, daniel, andrii, kpsingh, paul, mic, viro, brauner, kees
Cc: gnoack, jack, song, yonghong.song, martin.lau, 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>
---
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] 23+ messages in thread* [PATCH v2 11/15] landlock: Factor the credential restriction out of landlock_restrict_self()
2026-08-31 14:58 [PATCH v2 00/15] BPF interface for applying Landlock rulesets Justin Suess
` (9 preceding siblings ...)
2026-08-31 14:58 ` [PATCH v2 10/15] landlock: Expose the ruleset fd lookup to the rest of Landlock Justin Suess
@ 2026-08-31 14:58 ` Justin Suess
2026-08-31 14:58 ` [PATCH v2 12/15] landlock: Free rulesets after an RCU grace period Justin Suess
` (3 subsequent siblings)
14 siblings, 0 replies; 23+ messages in thread
From: Justin Suess @ 2026-08-31 14:58 UTC (permalink / raw)
To: ast, daniel, andrii, kpsingh, paul, mic, viro, brauner, kees
Cc: gnoack, jack, song, yonghong.song, martin.lau, 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>
---
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] 23+ messages in thread* [PATCH v2 12/15] landlock: Free rulesets after an RCU grace period
2026-08-31 14:58 [PATCH v2 00/15] BPF interface for applying Landlock rulesets Justin Suess
` (10 preceding siblings ...)
2026-08-31 14:58 ` [PATCH v2 11/15] landlock: Factor the credential restriction out of landlock_restrict_self() Justin Suess
@ 2026-08-31 14:58 ` Justin Suess
2026-08-31 14:58 ` [PATCH v2 13/15] landlock: Implement the LSM policy object hooks Justin Suess
` (2 subsequent siblings)
14 siblings, 0 replies; 23+ messages in thread
From: Justin Suess @ 2026-08-31 14:58 UTC (permalink / raw)
To: ast, daniel, andrii, kpsingh, paul, mic, viro, brauner, kees
Cc: gnoack, jack, song, yonghong.song, martin.lau, 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>
---
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] 23+ messages in thread* [PATCH v2 13/15] landlock: Implement the LSM policy object hooks
2026-08-31 14:58 [PATCH v2 00/15] BPF interface for applying Landlock rulesets Justin Suess
` (11 preceding siblings ...)
2026-08-31 14:58 ` [PATCH v2 12/15] landlock: Free rulesets after an RCU grace period Justin Suess
@ 2026-08-31 14:58 ` Justin Suess
2026-08-31 14:58 ` [PATCH v2 14/15] selftests/bpf: Test the LSM policy object kfuncs with Landlock Justin Suess
2026-08-31 14:58 ` [PATCH v2 15/15] landlock: Document the BPF policy interface Justin Suess
14 siblings, 0 replies; 23+ messages in thread
From: Justin Suess @ 2026-08-31 14:58 UTC (permalink / raw)
To: ast, daniel, andrii, kpsingh, paul, mic, viro, brauner, kees
Cc: gnoack, jack, song, yonghong.song, martin.lau, 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>
---
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] 23+ messages in thread* [PATCH v2 14/15] selftests/bpf: Test the LSM policy object kfuncs with Landlock
2026-08-31 14:58 [PATCH v2 00/15] BPF interface for applying Landlock rulesets Justin Suess
` (12 preceding siblings ...)
2026-08-31 14:58 ` [PATCH v2 13/15] landlock: Implement the LSM policy object hooks Justin Suess
@ 2026-08-31 14:58 ` Justin Suess
2026-08-31 19:53 ` sashiko-bot
2026-08-31 14:58 ` [PATCH v2 15/15] landlock: Document the BPF policy interface Justin Suess
14 siblings, 1 reply; 23+ messages in thread
From: Justin Suess @ 2026-08-31 14:58 UTC (permalink / raw)
To: ast, daniel, andrii, kpsingh, paul, mic, viro, brauner, kees
Cc: gnoack, jack, song, yonghong.song, martin.lau, 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>
---
tools/testing/selftests/bpf/config | 1 +
tools/testing/selftests/bpf/config.x86_64 | 2 +-
.../bpf/prog_tests/lsm_policy_landlock.c | 522 ++++++++++++++++++
.../selftests/bpf/progs/lsm_policy_landlock.c | 142 +++++
4 files changed, 666 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 ea7044f30adc..2fa734497461 100644
--- a/tools/testing/selftests/bpf/config
+++ b/tools/testing/selftests/bpf/config
@@ -120,6 +120,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 42ad817b00ae..13ca4906b67f 100644
--- a/tools/testing/selftests/bpf/config.x86_64
+++ b/tools/testing/selftests/bpf/config.x86_64
@@ -126,7 +126,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..9270b39f5e3a
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/lsm_policy_landlock.c
@@ -0,0 +1,522 @@
+// 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"))
+ 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] 23+ messages in thread* Re: [PATCH v2 14/15] selftests/bpf: Test the LSM policy object kfuncs with Landlock
2026-08-31 14:58 ` [PATCH v2 14/15] selftests/bpf: Test the LSM policy object kfuncs with Landlock Justin Suess
@ 2026-08-31 19:53 ` sashiko-bot
2026-09-02 12:24 ` Justin Suess
0 siblings, 1 reply; 23+ messages in thread
From: sashiko-bot @ 2026-08-31 19:53 UTC (permalink / raw)
To: Justin Suess; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] File descriptors leak on the error path in test_restrict_binprm_discard().
--
commit 7fa3010dd885254cc82b6aa2f6f254ee83a13717
Author: Justin Suess <utilityemal77@gmail.com>
selftests/bpf: Test the LSM policy object kfuncs with Landlock
This commit adds tests for LSM policy object kfuncs interacting with Landlock.
It tests execution confinement, audit log flags, and concurrent application
using a syscall program and BPF RCU lock management.
> 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 0000000000000..9270b39f5e3a7
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/prog_tests/lsm_policy_landlock.c
[ ... ]
> +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;
[ ... ]
> + 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"))
> + goto out_unlink;
[Severity: Low]
Does this code leak the file descriptors created by pipe() if fork() fails?
If fork() fails here, the code jumps directly to the out_unlink label, and it
appears neither pipe_fds[0] nor pipe_fds[1] are closed before exiting the
function.
The spawn_exec_child() helper function introduced in this same commit properly
closes both descriptors on fork failure. Should similar cleanup be added here?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260831145858.3869191-1-utilityemal77@gmail.com?part=14
^ permalink raw reply [flat|nested] 23+ messages in thread* Re: [PATCH v2 14/15] selftests/bpf: Test the LSM policy object kfuncs with Landlock
2026-08-31 19:53 ` sashiko-bot
@ 2026-09-02 12:24 ` Justin Suess
0 siblings, 0 replies; 23+ messages in thread
From: Justin Suess @ 2026-09-02 12:24 UTC (permalink / raw)
To: sashiko-reviews; +Cc: bpf, linux-security-module, linux-kernel
On Mon, Aug 31, 2026 at 07:53:26PM +0000, sashiko-bot@kernel.org wrote:
> Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
> - [Low] File descriptors leak on the error path in test_restrict_binprm_discard().
> --
>
> commit 7fa3010dd885254cc82b6aa2f6f254ee83a13717
> Author: Justin Suess <utilityemal77@gmail.com>
>
> selftests/bpf: Test the LSM policy object kfuncs with Landlock
>
> This commit adds tests for LSM policy object kfuncs interacting with Landlock.
> It tests execution confinement, audit log flags, and concurrent application
> using a syscall program and BPF RCU lock management.
>
> > 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 0000000000000..9270b39f5e3a7
> > --- /dev/null
> > +++ b/tools/testing/selftests/bpf/prog_tests/lsm_policy_landlock.c
> [ ... ]
> > +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;
> [ ... ]
> > + 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"))
> > + goto out_unlink;
>
> [Severity: Low]
> Does this code leak the file descriptors created by pipe() if fork() fails?
>
> If fork() fails here, the code jumps directly to the out_unlink label, and it
> appears neither pipe_fds[0] nor pipe_fds[1] are closed before exiting the
> function.
>
> The spawn_exec_child() helper function introduced in this same commit properly
> closes both descriptors on fork failure. Should similar cleanup be added here?
>
Will fix.
I think that's sashiko's only nit for this patchset.
https://sashiko.dev/#/patchset/20260831145858.3869191-1-utilityemal77@gmail.com
The BPF CI AI review bot didn't run on this since the patchset can't
based on a bpf tree until it catches up to the recent Landlock changes.
Justin
> --
> Sashiko AI review · https://sashiko.dev/#/patchset/20260831145858.3869191-1-utilityemal77@gmail.com?part=14
^ permalink raw reply [flat|nested] 23+ messages in thread
* [PATCH v2 15/15] landlock: Document the BPF policy interface
2026-08-31 14:58 [PATCH v2 00/15] BPF interface for applying Landlock rulesets Justin Suess
` (13 preceding siblings ...)
2026-08-31 14:58 ` [PATCH v2 14/15] selftests/bpf: Test the LSM policy object kfuncs with Landlock Justin Suess
@ 2026-08-31 14:58 ` Justin Suess
14 siblings, 0 replies; 23+ messages in thread
From: Justin Suess @ 2026-08-31 14:58 UTC (permalink / raw)
To: ast, daniel, andrii, kpsingh, paul, mic, viro, brauner, kees
Cc: gnoack, jack, song, yonghong.song, martin.lau, 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>
---
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] 23+ messages in thread