Implement the generic LSM hooks backing the BPF-owned Landlock kfuncs. The new code is gated on CONFIG_BPF_LSM, the only configuration where the kfuncs calling the hooks exist. The policy objects travel in the Landlock member of union lsm_policy_kptr as struct bpf_landlock_ruleset handles, the BTF-visible type the verifier tracks; bpf.c is the only place converting between the handle and struct landlock_ruleset. - policy_kptr_from_fd() translates a ruleset fd, created with landlock_create_ruleset(2) and populated with landlock_add_rule(2), into an owned landlock_ruleset reference. The fd is validated the same way as for the Landlock syscalls (ruleset file type, FMODE_CAN_READ). - policy_kptr_put() releases such a reference. The free is always deferred as the hook may be reached from BPF object destructors that cannot sleep. - bprm_enforce_policy_kptr() shares the landlock_restrict_self(2) path: it calls landlock_prepare_restriction() on the credentials prepared in the binprm and stages the computed restriction in their Landlock blob. Nothing is applied at this point, and there is no flag logic of its own: a new landlock_restrict_self(2) feature implemented in the shared helpers works here as well. The only divergence is the flag mask: LANDLOCK_RESTRICT_SELF_TSYNC is rejected with -EINVAL because the restriction targets the execution, not the calling threads. The restriction is computed in a root memcg charging scope: the domain confines the execution on behalf of the BPF program, so its GFP_KERNEL_ACCOUNT allocations are not charged to the mediated task. 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: a failed execution can no longer return to the calling program at that point and the application cannot fail, so an execution either starts confined by the domain or leaves the calling task untouched. The applied layer is accounted in domain_exec: for audit, the confined execution is the one that enforced the domain, so the LOG_SAME_EXEC and LOG_NEW_EXEC 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 staged restriction's lifetime is fully covered: a second bprm_enforce_policy_kptr() 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() when the prepared credentials are aborted, and the application clears the staging field so committed task credentials never carry a staged restriction. The credential copy helpers (hook_cred_transfer(), landlock_cred_copy()) uphold that invariant by never copying a staged restriction. Cc: Mickaël Salaün Signed-off-by: Justin Suess --- security/landlock/Makefile | 2 + security/landlock/bpf.c | 130 ++++++++++++++++++++++++++++++++++++ security/landlock/bpf.h | 21 ++++++ security/landlock/cred.c | 16 ++++- security/landlock/cred.h | 18 +++++ security/landlock/limits.h | 4 ++ security/landlock/ruleset.c | 2 +- security/landlock/setup.c | 2 + 8 files changed, 191 insertions(+), 4 deletions(-) create mode 100644 security/landlock/bpf.c create mode 100644 security/landlock/bpf.h diff --git a/security/landlock/Makefile b/security/landlock/Makefile index ffa7646d99f3..9ba28c06b5ac 100644 --- a/security/landlock/Makefile +++ b/security/landlock/Makefile @@ -16,3 +16,5 @@ landlock-$(CONFIG_AUDIT) += \ id.o \ audit.o \ domain.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..cf89062d35a7 --- /dev/null +++ b/security/landlock/bpf.c @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Landlock - LSM policy kptr hooks + * + * Implementation of the LSM hooks backing the Landlock kfuncs + * + * Copyright © 2026 Justin Suess + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "bpf.h" +#include "cred.h" +#include "limits.h" +#include "ruleset.h" +#include "setup.h" + +static int hook_policy_kptr_from_fd(int fd, union lsm_policy_kptr *policy) +{ + struct landlock_ruleset *ruleset; + + ruleset = landlock_get_ruleset_from_fd(fd, FMODE_CAN_READ); + if (IS_ERR(ruleset)) + return PTR_ERR(ruleset); + + policy->landlock.ruleset = (struct bpf_landlock_ruleset *)ruleset; + return 0; +} + +static void hook_policy_kptr_put(union lsm_policy_kptr *policy) +{ + struct landlock_ruleset *ruleset = + (struct landlock_ruleset *)policy->landlock.ruleset; + + /* + * May be called from a BPF object destructor that cannot sleep, + * whereas dropping the last ruleset reference frees it and may + * sleep: always defer the free. + */ + landlock_put_ruleset_deferred(ruleset); +} + +/* Charging scope for the BPF-driven domain allocations: root, i.e. nobody. */ +static struct mem_cgroup *get_bpf_memcg(void) +{ +#ifdef CONFIG_MEMCG + return root_mem_cgroup; +#else + return NULL; +#endif /* CONFIG_MEMCG */ +} + +static int hook_bprm_enforce_policy_kptr(struct linux_binprm *bprm, + union lsm_policy_kptr *policy, + u32 flags) +{ + struct landlock_cred_security *bprm_llcred = landlock_cred(bprm->cred); + struct landlock_ruleset *ruleset = + (struct landlock_ruleset *)policy->landlock.ruleset; + struct landlock_restriction restriction; + struct mem_cgroup *old_memcg; + int err; + + /* + * Same flags as landlock_restrict_self(2), except + * LANDLOCK_RESTRICT_SELF_TSYNC: the restriction targets the + * execution, not the calling threads. + */ + if ((flags | LANDLOCK_MASK_RESTRICT_BINPRM) != + LANDLOCK_MASK_RESTRICT_BINPRM) + return -EINVAL; + + /* + * The domain confines the execution on behalf of the BPF + * program, not of the mediated task: do not charge the task's + * memcg for it. + */ + old_memcg = set_active_memcg(get_bpf_memcg()); + err = landlock_prepare_restriction(bprm_llcred, ruleset, flags, + &restriction); + set_active_memcg(old_memcg); + if (err) + return err; + + /* + * Stages the restriction until the point of no return of the + * execution, replacing (and releasing) any previously staged + * one. Nothing is enforced yet: an execution that fails before + * committing its credentials drops the staged restriction in + * hook_cred_free() with no effect on the calling task. + */ + landlock_put_ruleset(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_restriction restriction; + + if (!bprm_llcred->staged.domain) + return; + + restriction = bprm_llcred->staged; + bprm_llcred->staged = (struct landlock_restriction){}; + + landlock_apply_restriction(bprm_llcred, &restriction); +} + +static struct security_hook_list landlock_hooks[] __ro_after_init = { + LSM_HOOK_INIT(policy_kptr_from_fd, hook_policy_kptr_from_fd), + LSM_HOOK_INIT(policy_kptr_put, hook_policy_kptr_put), + LSM_HOOK_INIT(bprm_enforce_policy_kptr, hook_bprm_enforce_policy_kptr), + LSM_HOOK_INIT(bprm_committing_creds, hook_bprm_committing_creds), +}; + +__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..e57729e6a564 --- /dev/null +++ b/security/landlock/bpf.h @@ -0,0 +1,21 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Landlock - LSM policy kptr hooks + * + * Copyright © 2026 Justin Suess + */ + +#ifndef _SECURITY_LANDLOCK_BPF_H +#define _SECURITY_LANDLOCK_BPF_H + +#include + +#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 13b3952c31c5..efbfd0c20475 100644 --- a/security/landlock/cred.c +++ b/security/landlock/cred.c @@ -124,6 +124,12 @@ static void hook_cred_transfer(struct cred *const new, landlock_get_ruleset(old_llcred->domain); *landlock_cred(new) = *old_llcred; + +#ifdef CONFIG_BPF_LSM + /* Only bprm credentials own a staged restriction: never copied. */ + WARN_ON_ONCE(landlock_cred(new)->staged.domain); + landlock_cred(new)->staged = (struct landlock_restriction){}; +#endif /* CONFIG_BPF_LSM */ } static int hook_cred_prepare(struct cred *const new, @@ -135,10 +141,14 @@ static int hook_cred_prepare(struct cred *const new, static void hook_cred_free(struct cred *const cred) { - struct landlock_ruleset *const dom = landlock_cred(cred)->domain; + struct landlock_cred_security *const llcred = landlock_cred(cred); + + landlock_put_ruleset_deferred(llcred->domain); - if (dom) - landlock_put_ruleset_deferred(dom); +#ifdef CONFIG_BPF_LSM + /* Releases a restriction staged for an aborted execution. */ + landlock_put_ruleset_deferred(llcred->staged.domain); +#endif /* CONFIG_BPF_LSM */ } #ifdef CONFIG_AUDIT diff --git a/security/landlock/cred.h b/security/landlock/cred.h index 1d5039b46ce7..74f8c9808dc4 100644 --- a/security/landlock/cred.h +++ b/security/landlock/cred.h @@ -60,6 +60,18 @@ struct landlock_cred_security { */ struct landlock_ruleset *domain; +#ifdef CONFIG_BPF_LSM + /** + * @staged: Restriction staged by the bprm_enforce_policy_kptr() hook, + * owning its domain reference and applied at the point of no return of + * the execution (bprm_committing_creds). Only ever set on the + * credentials prepared for an execution, between the staging and + * either the application or the release of the aborted credentials; + * committed task credentials never carry a staged restriction. + */ + struct landlock_restriction staged; +#endif /* CONFIG_BPF_LSM */ + #ifdef CONFIG_AUDIT /** * @domain_exec: Bitmask identifying the domain layers that were enforced by @@ -100,6 +112,12 @@ static inline void landlock_cred_copy(struct landlock_cred_security *dst, *dst = *src; landlock_get_ruleset(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_ruleset *landlock_get_current_domain(void) diff --git a/security/landlock/limits.h b/security/landlock/limits.h index 08d5f2f6d321..0bedfe650ea0 100644 --- a/security/landlock/limits.h +++ b/security/landlock/limits.h @@ -37,6 +37,10 @@ #define LANDLOCK_LAST_RESTRICT_SELF LANDLOCK_RESTRICT_SELF_TSYNC #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 4dd09ea22c84..176c626f9f10 100644 --- a/security/landlock/ruleset.c +++ b/security/landlock/ruleset.c @@ -520,7 +520,7 @@ static void free_ruleset_work(struct work_struct *const work) free_ruleset(ruleset); } -/* Only called by hook_cred_free(). */ +/* For contexts that cannot sleep, e.g. hook_cred_free(). */ void landlock_put_ruleset_deferred(struct landlock_ruleset *const ruleset) { if (ruleset && refcount_dec_and_test(&ruleset->usage)) { 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 #include +#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.54.0