* Re: [PATCH v8 1/9] landlock: Add a place for flags to layer rules
From: Justin Suess @ 2026-05-24 22:08 UTC (permalink / raw)
To: Tingmao Wang
Cc: Mickaël Salaün, Günther Noack, Jan Kara,
Abhinav Saxena, linux-security-module
In-Reply-To: <42a06f7c-abb5-4f2d-8428-8d122047e8d4@maowtm.org>
On Sun, May 24, 2026 at 07:20:19PM +0100, Tingmao Wang wrote:
> On 5/24/26 15:46, Justin Suess wrote:
> > On Sun, May 24, 2026 at 02:29:40AM +0100, Tingmao Wang wrote:
> >> On 5/23/26 21:48, Mickaël Salaün wrote:
> >>> [...]
> >>>> @@ -647,9 +648,14 @@ bool landlock_unmask_layers(const struct landlock_rule *const rule,
> >>>> */
> >>>> for (size_t i = 0; i < rule->num_layers; i++) {
> >>>> const struct landlock_layer *const layer = &rule->layers[i];
> >>>> + const layer_mask_t layer_bit = BIT_ULL(layer->level - 1);
> >>>>
> >>>
> >>>> /* Clear the bits where the layer in the rule grants access. */
> >>>> masks->access[layer->level - 1] &= ~layer->access;
> >>>> +
> >>>> + /* Collect rule flags for each layer. */
> >>>> + if (rule_flags && layer->flags.quiet)
> >>>> + rule_flags->quiet_masks |= layer_bit;
> >>>
> >>> Why not store the quiet bit in masks? That would not only be "access"
> >>> bits anymore but it makes sense to store all this bits it the same
> >>> place.
> >>>
> >>> We should then probably rename struct layer_access_masks to just struct
> >>> layer_masks.
> >>>
> >>> We need to be careful to not increase too much the size of this struct
> >>> though while keeping the [LANDLOCK_MAX_NUM_LAYERS] approach if possible
> >>> (see Günther's commit that added it).
> >>
> >> Most uses of struct layer_access_masks do not actually care about the rule
> >> flags tho, e.g. in unmask_scoped_access, scope_to_request, or may_refer.
> >> Such a rename would touch 31 places (and only a few of them would actually
> >> touch the quiet flag).
> >>
> >> If we want to refactor to make this be in the layer_access_masks (then
> >> rename it), I guess there are 3 options, which do you prefer?
> >>
> >> 1. Add a u16 bitfield for which layers are quieted. Future rule flags
> >> will be additional bitfields. struct layer_masks becomes 68 bytes (+4).
> >>
> >> struct layer_masks {
> >> access_mask_t access[LANDLOCK_MAX_NUM_LAYERS];
> >> layer_mask_t quiet_layers;
> >> };
> >>
> >> 2. Make the [LANDLOCK_MAX_NUM_LAYERS] array store both the access mask and
> >> the quiet bit (or more bits for future rule flags). Size of struct stays
> >> the same.
> >>
> > This approach seems best.
> >> static_assert(LANDLOCK_NUM_ACCESS_NET <= LANDLOCK_NUM_ACCESS_FS);
> >> static_assert(LANDLOCK_NUM_SCOPE <= LANDLOCK_NUM_ACCESS_FS);
> >> struct layer_mask {
> >> access_mask_t access:LANDLOCK_NUM_ACCESS_FS;
> >> bool quiet:1;
> >> };
> >
> > Other way to do it could be an (anonymous?) union.
> >
> > union {
> > access_mask_t fs_access:LANDLOCK_NUM_ACCESS_FS;
> > access_mask_t net_access:LANDLOCK_NUM_ACCESS_NET;
> > access_mask_t scope_access:LANDLOCK_NUM_SCOPE;
> > }
> >
> > The union should be sized to fit the largest field automatically.
> >
> > That way you don't have to change this when adding new access rights
> > and avoid the brittle static_asserts.
> >
> > Not sure about the alignment implications here though.
>
> Unfortunately this forces struct layer_mask to be 2x as large:
> https://godbolt.org/z/5P9b4rrMW
>
Yeah I guess the compiler can't pack the fields with differing types.
*In theory* you could make everything a _BitInt or something but it
seems better to do what you had below.
> But it turns out I could have just used MAX, seems to compile for me:
>
> struct layer_mask {
> access_mask_t access
> : MAX(LANDLOCK_NUM_ACCESS_FS,
> MAX(LANDLOCK_NUM_ACCESS_NET, LANDLOCK_NUM_SCOPE));
> bool quiet : 1;
> };
This works perfectly.
Mickaël's suggestion (except w/ all three access right classes like
you have here, think he missed LANDLOCK_NUM_SCOPE) is very close
to this.
> struct layer_masks {
> struct layer_mask layer[LANDLOCK_MAX_NUM_LAYERS];
> };
>
> Maybe we could #define LANDLOCK_NUM_ACCESS_MAX to be MAX(...) then use it
> here.
>
> I'm still not sure if putting the collected rule flags in struct
> layer_(access_)masks is a good idea tho. Passing a separate struct
> collected_rule_flags to the functions that needs to deal with rule flags
> (quiet, and later, no inherit / has no inherit descendant) seems quite
> practical to me.
(Not sure how stingy we gotta be with stack space)
There's a *slight* stack space advantage to keeping them together.
If you pass by value, (separate layer_access_masks, collected_rule_flags),
those structs must be individually padded and aligned. Which may or may not
make a difference, it's dependent on alignment and architecture.
Whereas if we keep them all together, we only pad once.
If you pass by pointer, you have to allocate stack space for each
pointer, so passing it all at once saves sizeof(collected_rule_flags*)
bytes in the pass by pointer case.
Either way it's probably a couple bytes at worst, so probably nothing to
worry about.
The more compelling argument is that we don't know how future paths
will use rule flags, so keeping it all together reduces churn later
if a function ends up needing to access flags. Moreover, it makes those
messy function signatures in fs.h/c a little less hairy, and easy to
refactor later.
^ permalink raw reply
* Re: [PATCH v8 2/9] landlock: Add API support and docs for the quiet flags
From: Mickaël Salaün @ 2026-05-24 20:35 UTC (permalink / raw)
To: Tingmao Wang
Cc: Günther Noack, Justin Suess, Jan Kara, Abhinav Saxena,
linux-security-module
In-Reply-To: <b4d25793959493f0a8ef66f03feedac1a15e7595.1775490344.git.m@maowtm.org>
On Mon, Apr 06, 2026 at 04:52:15PM +0100, Tingmao Wang wrote:
> Adds the UAPI for the quiet flags feature (but not the implementation
> yet).
>
> According to pahole, even after adding the struct access_masks quiet_masks
> in struct landlock_hierarchy, the u32 log_* bitfield still only has a size
> of 2 bytes, so there's minimal wasted space.
>
> Signed-off-by: Tingmao Wang <m@maowtm.org>
> ---
>
> Changes in v8:
> - The new Landlock ABI version is now v10 as a result of rebase.
> - Allocate a rule_flags in hook_unix_find() and pass to
> is_access_to_paths_allowed().
>
> Changes in v6:
> - Fix typo in doc
>
> Changes in v5:
> - Doc fixes.
> - Fix build failure without CONFIG_AUDIT / CONFIG_INET (reported by Justin
> Suess)
>
> Changes in v4:
> - Minor update to this commit message.
> - Fix minor formatting
>
> Changes in v3:
> - Updated docs from Mickaël's suggestions.
>
> Changes in v2:
> - Per suggestion, added support for quieting only certain access bits,
> controlled by extra quiet_access_* fields in the ruleset_attr.
> - Added docs for the extra fields and made updates to doc changes in v1.
> In particular, call out that the effect of LANDLOCK_ADD_RULE_QUIET is
> independent from the access bits passed in rule_attr
> - landlock_add_rule will return -EINVAL when LANDLOCK_ADD_RULE_QUIET is
> used but the ruleset does not have any quiet access bits set for the
> given rule type.
> - ABI version bump to v8
> - Syntactic and comment changes per suggestion.
>
> include/uapi/linux/landlock.h | 64 +++++++++++++++++
> security/landlock/domain.h | 5 ++
> security/landlock/fs.c | 11 +--
> security/landlock/fs.h | 2 +-
> security/landlock/net.c | 5 +-
> security/landlock/net.h | 5 +-
> security/landlock/ruleset.c | 12 +++-
> security/landlock/ruleset.h | 12 +++-
> security/landlock/syscalls.c | 72 +++++++++++++++-----
> tools/testing/selftests/landlock/base_test.c | 6 +-
> 10 files changed, 160 insertions(+), 34 deletions(-)
>
> diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h
> index 10a346e55e95..9a41c65623a1 100644
> --- a/include/uapi/linux/landlock.h
> +++ b/include/uapi/linux/landlock.h
> @@ -32,6 +32,19 @@
> * *handle* a wide range or all access rights that they know about at build time
> * (and that they have tested with a kernel that supported them all).
> *
> + * @quiet_access_fs and @quiet_access_net are bitmasks of actions for
> + * which a denial by this layer will not trigger an audit log if the
> + * corresponding object (or its children, for filesystem rules) is marked
> + * with the "quiet" bit via %LANDLOCK_ADD_RULE_QUIET, even if logging
> + * would normally take place per landlock_restrict_self() flags.
> + * quiet_scoped is similar, except that it does not require marking any
@quiet_scoped
> + * objects as quiet - if the ruleset is created with any bits set in
> + * quiet_scoped, then denial of such scoped resources will not trigger any
@quiet_scoped
> + * log. These 3 fields are available since Landlock ABI version 10.
> + *
> + * @quiet_access_fs, @quiet_access_net and @quiet_scoped must be a subset
> + * of @handled_access_fs, @handled_access_net and @scoped respectively.
> + *
> * This structure can grow in future Landlock versions.
> */
> struct landlock_ruleset_attr {
> @@ -51,6 +64,24 @@ struct landlock_ruleset_attr {
> * resources (e.g. IPCs).
> */
> __u64 scoped;
> +
> + /* Since ABI 10: */
No need for this comment and new lines.
> +
> + /**
> + * @quiet_access_fs: Bitmask of filesystem actions which should not be
> + * audit logged if per-object quiet flag is set.
Just "logged".
> + */
> + __u64 quiet_access_fs;
> + /**
> + * @quiet_access_net: Bitmask of network actions which should not be
> + * audit logged if per-object quiet flag is set.
ditto
> + */
> + __u64 quiet_access_net;
> + /**
> + * @quiet_scoped: Bitmask of scoped actions which should not be audit
> + * logged.
> + */
> + __u64 quiet_scoped;
> };
>
> /**
> @@ -69,6 +100,39 @@ struct landlock_ruleset_attr {
> #define LANDLOCK_CREATE_RULESET_ERRATA (1U << 1)
> /* clang-format on */
>
> +/**
> + * DOC: landlock_add_rule_flags
> + *
> + * **Flags**
> + *
> + * %LANDLOCK_ADD_RULE_QUIET
> + * Together with the quiet_* fields in struct landlock_ruleset_attr,
> + * this flag controls whether Landlock will log audit messages when
> + * access to the objects covered by this rule is denied by this layer.
> + *
> + * If audit logging is enabled, when Landlock denies an access, it will
> + * suppress the audit log if all of the following are true:
> + *
> + * - this layer is the innermost layer that denied the access;
> + * - all accesses denied by this layer are part of the quiet_* fields
> + * in the related struct landlock_ruleset_attr;
> + * - the object (or one of its parents, for filesystem rules) is
> + * marked as "quiet" via %LANDLOCK_ADD_RULE_QUIET.
> + *
> + * Because logging is only suppressed by a layer if the layer denies
> + * access, a sandboxed program cannot use this flag to "hide" access
> + * denials, without denying itself the access in the first place.
This is not 100% correct: if a domain only handles/denies/quiet read, and a
parent domain denies write, open(, O_RDwR) would not generate a log,
which is OK.
> + *
> + * The effect of this flag does not depend on the value of
> + * allowed_access in the passed in rule_attr. When this flag is
> + * present, the caller is also allowed to pass in an empty
> + * allowed_access.
The audit/log part in Documentation/userspace-api/landlock.rst and
Documentation/security/landlock.rst should be updated to take this quiet
flags into account.
> + */
> +
> +/* clang-format off */
> +#define LANDLOCK_ADD_RULE_QUIET (1U << 0)
I think this name is correct because this flag will be used by the
supervisor feature, but otherwise it should be named something like
LANDLOCK_ADD_RULE_LOG_QUIET. Tingmao, do you think that makes sense?
If yes, it should be explained in the commit message that this quiet
flag may be used for some kind of notification...
> +/* clang-format on */
> +
> /**
> * DOC: landlock_restrict_self_flags
> *
> diff --git a/security/landlock/domain.h b/security/landlock/domain.h
> index a9d57db0120d..9b8aeac8ebd2 100644
> --- a/security/landlock/domain.h
> +++ b/security/landlock/domain.h
> @@ -114,6 +114,11 @@ struct landlock_hierarchy {
> * %LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON. Set to false by default.
> */
> log_new_exec : 1;
> + /**
> + * @quiet_masks: Bitmasks of access that should be quieted (i.e. not
> + * logged) if the related object is marked as quiet.
> + */
> + struct access_masks quiet_masks;
> #endif /* CONFIG_AUDIT */
> };
>
> diff --git a/security/landlock/fs.c b/security/landlock/fs.c
> index 6f63e0182ef0..06a8d2258558 100644
> --- a/security/landlock/fs.c
> +++ b/security/landlock/fs.c
> @@ -325,7 +325,7 @@ static struct landlock_object *get_inode_object(struct inode *const inode)
> */
> int landlock_append_fs_rule(struct landlock_ruleset *const ruleset,
> const struct path *const path,
> - access_mask_t access_rights)
> + access_mask_t access_rights, const int flags)
> {
> int err;
> struct landlock_id id = {
> @@ -346,7 +346,7 @@ int landlock_append_fs_rule(struct landlock_ruleset *const ruleset,
> if (IS_ERR(id.key.object))
> return PTR_ERR(id.key.object);
> mutex_lock(&ruleset->lock);
> - err = landlock_insert_rule(ruleset, id, access_rights);
> + err = landlock_insert_rule(ruleset, id, access_rights, flags);
> mutex_unlock(&ruleset->lock);
> /*
> * No need to check for an error because landlock_insert_rule()
> @@ -1662,6 +1662,7 @@ static int hook_unix_find(const struct path *const path, struct sock *other,
> static const struct access_masks fs_resolve_unix = {
> .fs = LANDLOCK_ACCESS_FS_RESOLVE_UNIX,
> };
> + struct collected_rule_flags rule_flags = {};
>
> /* Lookup for the purpose of saving coredumps is OK. */
> if (unlikely(flags & SOCK_COREDUMP))
> @@ -1700,9 +1701,9 @@ static int hook_unix_find(const struct path *const path, struct sock *other,
> unix_state_unlock(other);
>
> /* Checks the connections to allow-listed paths. */
> - if (is_access_to_paths_allowed(subject->domain, path,
> - fs_resolve_unix.fs, &layer_masks,
> - &request, NULL, 0, NULL, NULL, NULL))
> + if (is_access_to_paths_allowed(
> + subject->domain, path, fs_resolve_unix.fs, &layer_masks,
> + &rule_flags, &request, NULL, 0, NULL, NULL, NULL, NULL))
> return 0;
>
> landlock_log_denial(subject, &request);
> diff --git a/security/landlock/fs.h b/security/landlock/fs.h
> index bf9948941f2f..cb7e654933ac 100644
> --- a/security/landlock/fs.h
> +++ b/security/landlock/fs.h
> @@ -126,6 +126,6 @@ __init void landlock_add_fs_hooks(void);
>
> int landlock_append_fs_rule(struct landlock_ruleset *const ruleset,
> const struct path *const path,
> - access_mask_t access_hierarchy);
> + access_mask_t access_hierarchy, const int flags);
>
> #endif /* _SECURITY_LANDLOCK_FS_H */
> diff --git a/security/landlock/net.c b/security/landlock/net.c
> index dc82ce4a2bd4..ade2b1750042 100644
> --- a/security/landlock/net.c
> +++ b/security/landlock/net.c
> @@ -20,7 +20,8 @@
> #include "ruleset.h"
>
> int landlock_append_net_rule(struct landlock_ruleset *const ruleset,
> - const u16 port, access_mask_t access_rights)
> + const u16 port, access_mask_t access_rights,
> + const int flags)
> {
> int err;
> const struct landlock_id id = {
> @@ -35,7 +36,7 @@ int landlock_append_net_rule(struct landlock_ruleset *const ruleset,
> ~landlock_get_net_access_mask(ruleset, 0);
>
> mutex_lock(&ruleset->lock);
> - err = landlock_insert_rule(ruleset, id, access_rights);
> + err = landlock_insert_rule(ruleset, id, access_rights, flags);
> mutex_unlock(&ruleset->lock);
>
> return err;
> diff --git a/security/landlock/net.h b/security/landlock/net.h
> index 09960c237a13..72c47f4d6803 100644
> --- a/security/landlock/net.h
> +++ b/security/landlock/net.h
> @@ -16,7 +16,8 @@
> __init void landlock_add_net_hooks(void);
>
> int landlock_append_net_rule(struct landlock_ruleset *const ruleset,
> - const u16 port, access_mask_t access_rights);
> + const u16 port, access_mask_t access_rights,
> + const int flags);
> #else /* IS_ENABLED(CONFIG_INET) */
> static inline void landlock_add_net_hooks(void)
> {
> @@ -24,7 +25,7 @@ static inline void landlock_add_net_hooks(void)
>
> static inline int
> landlock_append_net_rule(struct landlock_ruleset *const ruleset, const u16 port,
> - access_mask_t access_rights)
> + access_mask_t access_rights, const int flags)
> {
> return -EAFNOSUPPORT;
> }
> diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
> index e4e6b730b581..d2d1e3fb6cf2 100644
> --- a/security/landlock/ruleset.c
> +++ b/security/landlock/ruleset.c
> @@ -21,6 +21,7 @@
> #include <linux/slab.h>
> #include <linux/spinlock.h>
> #include <linux/workqueue.h>
> +#include <uapi/linux/landlock.h>
>
> #include "access.h"
> #include "domain.h"
> @@ -255,6 +256,7 @@ static int insert_rule(struct landlock_ruleset *const ruleset,
> if (WARN_ON_ONCE(this->layers[0].level != 0))
> return -EINVAL;
> this->layers[0].access |= (*layers)[0].access;
> + this->layers[0].flags.quiet |= (*layers)[0].flags.quiet;
> return 0;
> }
>
> @@ -305,12 +307,15 @@ static void build_check_layer(void)
> /* @ruleset must be locked by the caller. */
> int landlock_insert_rule(struct landlock_ruleset *const ruleset,
> const struct landlock_id id,
> - const access_mask_t access)
> + const access_mask_t access, const int flags)
> {
> struct landlock_layer layers[] = { {
> .access = access,
> /* When @level is zero, insert_rule() extends @ruleset. */
> .level = 0,
> + .flags = {
> + .quiet = !!(flags & LANDLOCK_ADD_RULE_QUIET),
> + },
> } };
>
> build_check_layer();
> @@ -351,6 +356,7 @@ static int merge_tree(struct landlock_ruleset *const dst,
> return -EINVAL;
>
> layers[0].access = walker_rule->layers[0].access;
> + layers[0].flags = walker_rule->layers[0].flags;
>
> err = insert_rule(dst, id, &layers, ARRAY_SIZE(layers));
> if (err)
> @@ -581,6 +587,10 @@ landlock_merge_ruleset(struct landlock_ruleset *const parent,
> if (err)
> return ERR_PTR(err);
>
> +#ifdef CONFIG_AUDIT
> + new_dom->hierarchy->quiet_masks = ruleset->quiet_masks;
> +#endif /* CONFIG_AUDIT */
> +
> return no_free_ptr(new_dom);
> }
>
> diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h
> index 3b31552f0c95..e369f15ae885 100644
> --- a/security/landlock/ruleset.h
> +++ b/security/landlock/ruleset.h
> @@ -172,8 +172,8 @@ struct landlock_ruleset {
> * @work_free: Enables to free a ruleset within a lockless
> * section. This is only used by
> * landlock_put_ruleset_deferred() when @usage reaches zero.
> - * The fields @lock, @usage, @num_rules, @num_layers and
> - * @access_masks are then unused.
> + * The fields @lock, @usage, @num_rules, @num_layers, @quiet_masks
> + * and @access_masks are then unused.
> */
> struct work_struct work_free;
> struct {
> @@ -199,6 +199,12 @@ struct landlock_ruleset {
> * non-merged ruleset (i.e. not a domain).
> */
> u32 num_layers;
> + /**
> + * @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;
> /**
> * @access_masks: Contains the subset of filesystem and
> * network actions that are restricted by a ruleset.
> @@ -229,7 +235,7 @@ DEFINE_FREE(landlock_put_ruleset, struct landlock_ruleset *,
>
> int landlock_insert_rule(struct landlock_ruleset *const ruleset,
> const struct landlock_id id,
> - const access_mask_t access);
> + const access_mask_t access, const int flags);
>
> struct landlock_ruleset *
> landlock_merge_ruleset(struct landlock_ruleset *const parent,
> diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c
> index accfd2e5a0cd..a71068c41f76 100644
> --- a/security/landlock/syscalls.c
> +++ b/security/landlock/syscalls.c
> @@ -105,8 +105,11 @@ static void build_check_abi(void)
> ruleset_size = sizeof(ruleset_attr.handled_access_fs);
> ruleset_size += sizeof(ruleset_attr.handled_access_net);
> ruleset_size += sizeof(ruleset_attr.scoped);
> + ruleset_size += sizeof(ruleset_attr.quiet_access_fs);
> + ruleset_size += sizeof(ruleset_attr.quiet_access_net);
> + ruleset_size += sizeof(ruleset_attr.quiet_scoped);
> BUILD_BUG_ON(sizeof(ruleset_attr) != ruleset_size);
> - BUILD_BUG_ON(sizeof(ruleset_attr) != 24);
> + BUILD_BUG_ON(sizeof(ruleset_attr) != 48);
>
> path_beneath_size = sizeof(path_beneath_attr.allowed_access);
> path_beneath_size += sizeof(path_beneath_attr.parent_fd);
> @@ -166,7 +169,7 @@ static const struct file_operations ruleset_fops = {
> * If the change involves a fix that requires userspace awareness, also update
> * the errata documentation in Documentation/userspace-api/landlock.rst .
> */
> -const int landlock_abi_version = 9;
> +const int landlock_abi_version = 10;
>
> /**
> * sys_landlock_create_ruleset - Create a new ruleset
> @@ -193,6 +196,8 @@ const int landlock_abi_version = 9;
> * - %EOPNOTSUPP: Landlock is supported by the kernel but disabled at boot time;
> * - %EINVAL: unknown @flags, or unknown access, or unknown scope, or too small
> * @size;
> + * - %EINVAL: quiet_access_fs or quiet_access_net is not a subset of the
> + * corresponding handled_access_fs or handled_access_net;
> * - %E2BIG: @attr or @size inconsistencies;
> * - %EFAULT: @attr or @size inconsistencies;
> * - %ENOMSG: empty &landlock_ruleset_attr.handled_access_fs.
> @@ -249,6 +254,21 @@ SYSCALL_DEFINE3(landlock_create_ruleset,
> if ((ruleset_attr.scoped | LANDLOCK_MASK_SCOPE) != LANDLOCK_MASK_SCOPE)
> return -EINVAL;
>
> + /*
> + * Check that quiet masks are subsets of the respective handled masks.
> + * Because of the checks above this is sufficient to also ensure that
> + * the quiet masks are valid access masks.
> + */
> + if ((ruleset_attr.quiet_access_fs | ruleset_attr.handled_access_fs) !=
> + ruleset_attr.handled_access_fs)
> + return -EINVAL;
> + if ((ruleset_attr.quiet_access_net | ruleset_attr.handled_access_net) !=
> + ruleset_attr.handled_access_net)
> + return -EINVAL;
> + if ((ruleset_attr.quiet_scoped | ruleset_attr.scoped) !=
> + ruleset_attr.scoped)
> + return -EINVAL;
> +
> /* Checks arguments and transforms to kernel struct. */
> ruleset = landlock_create_ruleset(ruleset_attr.handled_access_fs,
> ruleset_attr.handled_access_net,
> @@ -256,6 +276,10 @@ SYSCALL_DEFINE3(landlock_create_ruleset,
> if (IS_ERR(ruleset))
> return PTR_ERR(ruleset);
>
> + ruleset->quiet_masks.fs = ruleset_attr.quiet_access_fs;
> + ruleset->quiet_masks.net = ruleset_attr.quiet_access_net;
> + ruleset->quiet_masks.scope = ruleset_attr.quiet_scoped;
> +
> /* Creates anonymous FD referring to the ruleset. */
> ruleset_fd = anon_inode_getfd("[landlock-ruleset]", &ruleset_fops,
> ruleset, O_RDWR | O_CLOEXEC);
> @@ -320,7 +344,7 @@ static int get_path_from_fd(const s32 fd, struct path *const path)
> }
>
> static int add_rule_path_beneath(struct landlock_ruleset *const ruleset,
> - const void __user *const rule_attr)
> + const void __user *const rule_attr, int flags)
> {
> struct landlock_path_beneath_attr path_beneath_attr;
> struct path path;
> @@ -335,9 +359,10 @@ static int add_rule_path_beneath(struct landlock_ruleset *const ruleset,
>
> /*
> * Informs about useless rule: empty allowed_access (i.e. deny rules)
> - * are ignored in path walks.
> + * are ignored in path walks. However, the rule is not useless if it
> + * is there to hold a quiet flag
Missing trailing period.
> */
> - if (!path_beneath_attr.allowed_access)
> + if (!flags && !path_beneath_attr.allowed_access)
> return -ENOMSG;
>
> /* Checks that allowed_access matches the @ruleset constraints. */
> @@ -345,6 +370,10 @@ static int add_rule_path_beneath(struct landlock_ruleset *const ruleset,
> if ((path_beneath_attr.allowed_access | mask) != mask)
> return -EINVAL;
>
> + /* Check for useless quiet flag. */
Checks... (for consistency with comments above)
> + if (flags & LANDLOCK_ADD_RULE_QUIET && !ruleset->quiet_masks.fs)
> + return -EINVAL;
> +
> /* Gets and checks the new rule. */
> err = get_path_from_fd(path_beneath_attr.parent_fd, &path);
> if (err)
> @@ -352,13 +381,13 @@ static int add_rule_path_beneath(struct landlock_ruleset *const ruleset,
>
> /* Imports the new rule. */
> err = landlock_append_fs_rule(ruleset, &path,
> - path_beneath_attr.allowed_access);
> + path_beneath_attr.allowed_access, flags);
> path_put(&path);
> return err;
> }
>
> static int add_rule_net_port(struct landlock_ruleset *ruleset,
> - const void __user *const rule_attr)
> + const void __user *const rule_attr, int flags)
> {
> struct landlock_net_port_attr net_port_attr;
> int res;
> @@ -371,9 +400,10 @@ static int add_rule_net_port(struct landlock_ruleset *ruleset,
>
> /*
> * Informs about useless rule: empty allowed_access (i.e. deny rules)
> - * are ignored by network actions.
> + * are ignored by network actions. However, the rule is not useless
> + * if it is there to hold a quiet flag
> */
> - if (!net_port_attr.allowed_access)
> + if (!flags && !net_port_attr.allowed_access)
> return -ENOMSG;
>
> /* Checks that allowed_access matches the @ruleset constraints. */
> @@ -381,13 +411,17 @@ static int add_rule_net_port(struct landlock_ruleset *ruleset,
> if ((net_port_attr.allowed_access | mask) != mask)
> return -EINVAL;
>
> + /* Check for useless quiet flag. */
> + if (flags & LANDLOCK_ADD_RULE_QUIET && !ruleset->quiet_masks.net)
> + return -EINVAL;
> +
> /* Denies inserting a rule with port greater than 65535. */
> if (net_port_attr.port > U16_MAX)
> return -EINVAL;
>
> /* Imports the new rule. */
> return landlock_append_net_rule(ruleset, net_port_attr.port,
> - net_port_attr.allowed_access);
> + net_port_attr.allowed_access, flags);
> }
>
> /**
> @@ -398,7 +432,7 @@ static int add_rule_net_port(struct landlock_ruleset *ruleset,
> * @rule_type: Identify the structure type pointed to by @rule_attr:
> * %LANDLOCK_RULE_PATH_BENEATH or %LANDLOCK_RULE_NET_PORT.
> * @rule_attr: Pointer to a rule (matching the @rule_type).
> - * @flags: Must be 0.
> + * @flags: Must be 0 or %LANDLOCK_ADD_RULE_QUIET.
> *
> * This system call enables to define a new rule and add it to an existing
> * ruleset.
> @@ -408,20 +442,25 @@ static int add_rule_net_port(struct landlock_ruleset *ruleset,
> * - %EOPNOTSUPP: Landlock is supported by the kernel but disabled at boot time;
> * - %EAFNOSUPPORT: @rule_type is %LANDLOCK_RULE_NET_PORT but TCP/IP is not
> * supported by the running kernel;
> - * - %EINVAL: @flags is not 0;
> + * - %EINVAL: @flags is not valid;
> * - %EINVAL: The rule accesses are inconsistent (i.e.
> * &landlock_path_beneath_attr.allowed_access or
> * &landlock_net_port_attr.allowed_access is not a subset of the ruleset
> * handled accesses)
> * - %EINVAL: &landlock_net_port_attr.port is greater than 65535;
> + * - %EINVAL: LANDLOCK_ADD_RULE_QUIET is passed but the ruleset has no
> + * quiet access bits set for the corresponding rule type.
> * - %ENOMSG: Empty accesses (e.g. &landlock_path_beneath_attr.allowed_access is
> - * 0);
> + * 0) and no flags;
> * - %EBADF: @ruleset_fd is not a file descriptor for the current thread, or a
> * member of @rule_attr is not a file descriptor as expected;
> * - %EBADFD: @ruleset_fd is not a ruleset file descriptor, or a member of
> * @rule_attr is not the expected file descriptor type;
> * - %EPERM: @ruleset_fd has no write access to the underlying ruleset;
> * - %EFAULT: @rule_attr was not a valid address.
> + *
> + * .. kernel-doc:: include/uapi/linux/landlock.h
> + * :identifiers: landlock_add_rule_flags
> */
> SYSCALL_DEFINE4(landlock_add_rule, const int, ruleset_fd,
> const enum landlock_rule_type, rule_type,
> @@ -432,8 +471,7 @@ SYSCALL_DEFINE4(landlock_add_rule, const int, ruleset_fd,
> if (!is_initialized())
> return -EOPNOTSUPP;
>
> - /* No flag for now. */
> - if (flags)
> + if (flags && flags != LANDLOCK_ADD_RULE_QUIET)
> return -EINVAL;
>
> /* Gets and checks the ruleset. */
> @@ -443,9 +481,9 @@ SYSCALL_DEFINE4(landlock_add_rule, const int, ruleset_fd,
>
> switch (rule_type) {
> case LANDLOCK_RULE_PATH_BENEATH:
> - return add_rule_path_beneath(ruleset, rule_attr);
> + return add_rule_path_beneath(ruleset, rule_attr, flags);
> case LANDLOCK_RULE_NET_PORT:
> - return add_rule_net_port(ruleset, rule_attr);
> + return add_rule_net_port(ruleset, rule_attr, flags);
> default:
> return -EINVAL;
> }
> diff --git a/tools/testing/selftests/landlock/base_test.c b/tools/testing/selftests/landlock/base_test.c
> index 30d37234086c..84e91fcaa1b2 100644
> --- a/tools/testing/selftests/landlock/base_test.c
> +++ b/tools/testing/selftests/landlock/base_test.c
> @@ -76,8 +76,8 @@ TEST(abi_version)
> const struct landlock_ruleset_attr ruleset_attr = {
> .handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE,
> };
> - ASSERT_EQ(9, landlock_create_ruleset(NULL, 0,
> - LANDLOCK_CREATE_RULESET_VERSION));
> + ASSERT_EQ(10, landlock_create_ruleset(NULL, 0,
> + LANDLOCK_CREATE_RULESET_VERSION));
>
> ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 0,
> LANDLOCK_CREATE_RULESET_VERSION));
> @@ -201,7 +201,7 @@ TEST(add_rule_checks_ordering)
> ASSERT_LE(0, ruleset_fd);
>
> /* Checks invalid flags. */
> - ASSERT_EQ(-1, landlock_add_rule(-1, 0, NULL, 1));
> + ASSERT_EQ(-1, landlock_add_rule(-1, 0, NULL, 100));
> ASSERT_EQ(EINVAL, errno);
>
> /* Checks invalid ruleset FD. */
> --
> 2.53.0
>
^ permalink raw reply
* Re: [PATCH v8 1/9] landlock: Add a place for flags to layer rules
From: Mickaël Salaün @ 2026-05-24 20:35 UTC (permalink / raw)
To: Tingmao Wang
Cc: Günther Noack, Justin Suess, Jan Kara, Abhinav Saxena,
linux-security-module
In-Reply-To: <617fdd53-6612-4f6f-b0e0-16d85985487b@maowtm.org>
On Sun, May 24, 2026 at 02:29:40AM +0100, Tingmao Wang wrote:
> On 5/23/26 21:48, Mickaël Salaün wrote:
> > This patch doesn't build.
>
> Missed a hunk in this patch (ended up in the next one), will add.
>
> >> @@ -797,20 +803,28 @@ is_access_to_paths_allowed(const struct landlock_ruleset *const domain,
> >> }
> >>
> >> if (unlikely(dentry_child1)) {
> >> + /*
> >> + * Get the layer masks for the child dentries for use by domain
> >> + * check later. The rule_flags for child1 should have been
> >> + * included in rule_flags_parent1 already (cf.
> >> + * collect_domain_accesses), and is not relevant for domain check,
> >> + * so we don't have to pass it to landlock_unmask_layers.
> >> + */
> >> if (landlock_init_layer_masks(domain, LANDLOCK_MASK_ACCESS_FS,
> >> &_layer_masks_child1,
> >> LANDLOCK_KEY_INODE))
> >> landlock_unmask_layers(find_rule(domain, dentry_child1),
> >> - &_layer_masks_child1);
> >> + &_layer_masks_child1, NULL);
> >> layer_masks_child1 = &_layer_masks_child1;
> >> child1_is_directory = d_is_dir(dentry_child1);
> >> }
> >> if (unlikely(dentry_child2)) {
> >> + /* See above comment for why NULL is passed as rule_flags_masks. */
> >
> > rule_flags_masks doesn't exist.
>
> I guess I was probably referring to the rule_flags argument - will fix.
>
> >> [...]
> >> @@ -647,9 +648,14 @@ bool landlock_unmask_layers(const struct landlock_rule *const rule,
> >> */
> >> for (size_t i = 0; i < rule->num_layers; i++) {
> >> const struct landlock_layer *const layer = &rule->layers[i];
> >> + const layer_mask_t layer_bit = BIT_ULL(layer->level - 1);
> >>
> >
> >> /* Clear the bits where the layer in the rule grants access. */
> >> masks->access[layer->level - 1] &= ~layer->access;
> >> +
> >> + /* Collect rule flags for each layer. */
> >> + if (rule_flags && layer->flags.quiet)
> >> + rule_flags->quiet_masks |= layer_bit;
> >
> > Why not store the quiet bit in masks? That would not only be "access"
> > bits anymore but it makes sense to store all this bits it the same
> > place.
> >
> > We should then probably rename struct layer_access_masks to just struct
> > layer_masks.
> >
> > We need to be careful to not increase too much the size of this struct
> > though while keeping the [LANDLOCK_MAX_NUM_LAYERS] approach if possible
> > (see Günther's commit that added it).
>
> Most uses of struct layer_access_masks do not actually care about the rule
> flags tho, e.g. in unmask_scoped_access, scope_to_request, or may_refer.
> Such a rename would touch 31 places (and only a few of them would actually
> touch the quiet flag).
Most of these places are tests.
>
> If we want to refactor to make this be in the layer_access_masks (then
> rename it), I guess there are 3 options, which do you prefer?
>
> 1. Add a u16 bitfield for which layers are quieted. Future rule flags
> will be additional bitfields. struct layer_masks becomes 68 bytes (+4).
>
> struct layer_masks {
> access_mask_t access[LANDLOCK_MAX_NUM_LAYERS];
> layer_mask_t quiet_layers;
> };
>
> 2. Make the [LANDLOCK_MAX_NUM_LAYERS] array store both the access mask and
> the quiet bit (or more bits for future rule flags). Size of struct stays
> the same.
>
> static_assert(LANDLOCK_NUM_ACCESS_NET <= LANDLOCK_NUM_ACCESS_FS);
> static_assert(LANDLOCK_NUM_SCOPE <= LANDLOCK_NUM_ACCESS_FS);
> struct layer_mask {
> access_mask_t access:LANDLOCK_NUM_ACCESS_FS;
> bool quiet:1;
> };
Yes, like Justin, I prefer this approach too. Some improvements:
// In limits.h:
#define LANDLOCK_MAX_NUM_ACCESSES \
MAX(LANDLOCK_NUM_ACCESS_FS, LANDLOCK_NUM_ACCESS_NET)
// In access.h:
struct layer_mask {
access_mask_t access:LANDLOCK_MAX_NUM_ACCESSES;
#ifdef CONFIG_AUDIT
bool quiet:1; // I'm not sure if using bool would work for all
// architectures though, but we can make sure with the following
// assert.
#endif /* CONFIG_AUDIT */
};
static_assert(sizeof(struct layer_mask), sizeof(access_mask_t));
> struct layer_masks {
> struct layer_mask layer[LANDLOCK_MAX_NUM_LAYERS];
> };
>
> (Maybe we can just make struct layer_masks a typedef to
> layer_mask[LANDLOCK_MAX_NUM_LAYERS] instead? But currently not sure if
> there are any gotchas with a typedef like that)
The only typedefs used in Landlock are for potentially growing types. So
no need for typedef here.
>
> 3. Mirror layer_access_masks::access[] - add a
> rule_flags[LANDLOCK_MAX_NUM_LAYERS] too. struct layer_masks becomes 80
> bytes (+16).
>
> struct rule_flags {
> bool quiet:1;
> };
> struct layer_masks {
> /**
> * @access: The unfulfilled access rights for each layer.
> */
> access_mask_t access[LANDLOCK_MAX_NUM_LAYERS];
> struct rule_flags rule_flags[LANDLOCK_MAX_NUM_LAYERS];
> };
>
> (3 seems very wasteful to me)
>
^ permalink raw reply
* Re: [PATCH v8 1/9] landlock: Add a place for flags to layer rules
From: Tingmao Wang @ 2026-05-24 18:20 UTC (permalink / raw)
To: Justin Suess, Mickaël Salaün
Cc: Günther Noack, Jan Kara, Abhinav Saxena,
linux-security-module
In-Reply-To: <ahMLLwnDO7g64h63@zenbox>
On 5/24/26 15:46, Justin Suess wrote:
> On Sun, May 24, 2026 at 02:29:40AM +0100, Tingmao Wang wrote:
>> On 5/23/26 21:48, Mickaël Salaün wrote:
>>> [...]
>>>> @@ -647,9 +648,14 @@ bool landlock_unmask_layers(const struct landlock_rule *const rule,
>>>> */
>>>> for (size_t i = 0; i < rule->num_layers; i++) {
>>>> const struct landlock_layer *const layer = &rule->layers[i];
>>>> + const layer_mask_t layer_bit = BIT_ULL(layer->level - 1);
>>>>
>>>
>>>> /* Clear the bits where the layer in the rule grants access. */
>>>> masks->access[layer->level - 1] &= ~layer->access;
>>>> +
>>>> + /* Collect rule flags for each layer. */
>>>> + if (rule_flags && layer->flags.quiet)
>>>> + rule_flags->quiet_masks |= layer_bit;
>>>
>>> Why not store the quiet bit in masks? That would not only be "access"
>>> bits anymore but it makes sense to store all this bits it the same
>>> place.
>>>
>>> We should then probably rename struct layer_access_masks to just struct
>>> layer_masks.
>>>
>>> We need to be careful to not increase too much the size of this struct
>>> though while keeping the [LANDLOCK_MAX_NUM_LAYERS] approach if possible
>>> (see Günther's commit that added it).
>>
>> Most uses of struct layer_access_masks do not actually care about the rule
>> flags tho, e.g. in unmask_scoped_access, scope_to_request, or may_refer.
>> Such a rename would touch 31 places (and only a few of them would actually
>> touch the quiet flag).
>>
>> If we want to refactor to make this be in the layer_access_masks (then
>> rename it), I guess there are 3 options, which do you prefer?
>>
>> 1. Add a u16 bitfield for which layers are quieted. Future rule flags
>> will be additional bitfields. struct layer_masks becomes 68 bytes (+4).
>>
>> struct layer_masks {
>> access_mask_t access[LANDLOCK_MAX_NUM_LAYERS];
>> layer_mask_t quiet_layers;
>> };
>>
>> 2. Make the [LANDLOCK_MAX_NUM_LAYERS] array store both the access mask and
>> the quiet bit (or more bits for future rule flags). Size of struct stays
>> the same.
>>
> This approach seems best.
>> static_assert(LANDLOCK_NUM_ACCESS_NET <= LANDLOCK_NUM_ACCESS_FS);
>> static_assert(LANDLOCK_NUM_SCOPE <= LANDLOCK_NUM_ACCESS_FS);
>> struct layer_mask {
>> access_mask_t access:LANDLOCK_NUM_ACCESS_FS;
>> bool quiet:1;
>> };
>
> Other way to do it could be an (anonymous?) union.
>
> union {
> access_mask_t fs_access:LANDLOCK_NUM_ACCESS_FS;
> access_mask_t net_access:LANDLOCK_NUM_ACCESS_NET;
> access_mask_t scope_access:LANDLOCK_NUM_SCOPE;
> }
>
> The union should be sized to fit the largest field automatically.
>
> That way you don't have to change this when adding new access rights
> and avoid the brittle static_asserts.
>
> Not sure about the alignment implications here though.
Unfortunately this forces struct layer_mask to be 2x as large:
https://godbolt.org/z/5P9b4rrMW
But it turns out I could have just used MAX, seems to compile for me:
struct layer_mask {
access_mask_t access
: MAX(LANDLOCK_NUM_ACCESS_FS,
MAX(LANDLOCK_NUM_ACCESS_NET, LANDLOCK_NUM_SCOPE));
bool quiet : 1;
};
struct layer_masks {
struct layer_mask layer[LANDLOCK_MAX_NUM_LAYERS];
};
Maybe we could #define LANDLOCK_NUM_ACCESS_MAX to be MAX(...) then use it
here.
I'm still not sure if putting the collected rule flags in struct
layer_(access_)masks is a good idea tho. Passing a separate struct
collected_rule_flags to the functions that needs to deal with rule flags
(quiet, and later, no inherit / has no inherit descendant) seems quite
practical to me.
^ permalink raw reply
* Re: [PATCH v8 1/9] landlock: Add a place for flags to layer rules
From: Justin Suess @ 2026-05-24 14:46 UTC (permalink / raw)
To: Tingmao Wang
Cc: Mickaël Salaün, Günther Noack, Jan Kara,
Abhinav Saxena, linux-security-module
In-Reply-To: <617fdd53-6612-4f6f-b0e0-16d85985487b@maowtm.org>
On Sun, May 24, 2026 at 02:29:40AM +0100, Tingmao Wang wrote:
> On 5/23/26 21:48, Mickaël Salaün wrote:
> > This patch doesn't build.
>
> Missed a hunk in this patch (ended up in the next one), will add.
>
> >> @@ -797,20 +803,28 @@ is_access_to_paths_allowed(const struct landlock_ruleset *const domain,
> >> }
> >>
> >> if (unlikely(dentry_child1)) {
> >> + /*
> >> + * Get the layer masks for the child dentries for use by domain
> >> + * check later. The rule_flags for child1 should have been
> >> + * included in rule_flags_parent1 already (cf.
> >> + * collect_domain_accesses), and is not relevant for domain check,
> >> + * so we don't have to pass it to landlock_unmask_layers.
> >> + */
> >> if (landlock_init_layer_masks(domain, LANDLOCK_MASK_ACCESS_FS,
> >> &_layer_masks_child1,
> >> LANDLOCK_KEY_INODE))
> >> landlock_unmask_layers(find_rule(domain, dentry_child1),
> >> - &_layer_masks_child1);
> >> + &_layer_masks_child1, NULL);
> >> layer_masks_child1 = &_layer_masks_child1;
> >> child1_is_directory = d_is_dir(dentry_child1);
> >> }
> >> if (unlikely(dentry_child2)) {
> >> + /* See above comment for why NULL is passed as rule_flags_masks. */
> >
> > rule_flags_masks doesn't exist.
>
> I guess I was probably referring to the rule_flags argument - will fix.
>
> >> [...]
> >> @@ -647,9 +648,14 @@ bool landlock_unmask_layers(const struct landlock_rule *const rule,
> >> */
> >> for (size_t i = 0; i < rule->num_layers; i++) {
> >> const struct landlock_layer *const layer = &rule->layers[i];
> >> + const layer_mask_t layer_bit = BIT_ULL(layer->level - 1);
> >>
> >
> >> /* Clear the bits where the layer in the rule grants access. */
> >> masks->access[layer->level - 1] &= ~layer->access;
> >> +
> >> + /* Collect rule flags for each layer. */
> >> + if (rule_flags && layer->flags.quiet)
> >> + rule_flags->quiet_masks |= layer_bit;
> >
> > Why not store the quiet bit in masks? That would not only be "access"
> > bits anymore but it makes sense to store all this bits it the same
> > place.
> >
> > We should then probably rename struct layer_access_masks to just struct
> > layer_masks.
> >
> > We need to be careful to not increase too much the size of this struct
> > though while keeping the [LANDLOCK_MAX_NUM_LAYERS] approach if possible
> > (see Günther's commit that added it).
>
> Most uses of struct layer_access_masks do not actually care about the rule
> flags tho, e.g. in unmask_scoped_access, scope_to_request, or may_refer.
> Such a rename would touch 31 places (and only a few of them would actually
> touch the quiet flag).
>
> If we want to refactor to make this be in the layer_access_masks (then
> rename it), I guess there are 3 options, which do you prefer?
>
> 1. Add a u16 bitfield for which layers are quieted. Future rule flags
> will be additional bitfields. struct layer_masks becomes 68 bytes (+4).
>
> struct layer_masks {
> access_mask_t access[LANDLOCK_MAX_NUM_LAYERS];
> layer_mask_t quiet_layers;
> };
>
> 2. Make the [LANDLOCK_MAX_NUM_LAYERS] array store both the access mask and
> the quiet bit (or more bits for future rule flags). Size of struct stays
> the same.
>
This approach seems best.
> static_assert(LANDLOCK_NUM_ACCESS_NET <= LANDLOCK_NUM_ACCESS_FS);
> static_assert(LANDLOCK_NUM_SCOPE <= LANDLOCK_NUM_ACCESS_FS);
> struct layer_mask {
> access_mask_t access:LANDLOCK_NUM_ACCESS_FS;
> bool quiet:1;
> };
Other way to do it could be an (anonymous?) union.
union {
access_mask_t fs_access:LANDLOCK_NUM_ACCESS_FS;
access_mask_t net_access:LANDLOCK_NUM_ACCESS_NET;
access_mask_t scope_access:LANDLOCK_NUM_SCOPE;
}
The union should be sized to fit the largest field automatically.
That way you don't have to change this when adding new access rights
and avoid the brittle static_asserts.
Not sure about the alignment implications here though.
> struct layer_masks {
> struct layer_mask layer[LANDLOCK_MAX_NUM_LAYERS];
> };
>
> (Maybe we can just make struct layer_masks a typedef to
> layer_mask[LANDLOCK_MAX_NUM_LAYERS] instead? But currently not sure if
> there are any gotchas with a typedef like that)
>
Typedefs are generally discouraged for structs and pointers in the
kernel coding style.
https://docs.kernel.org/process/coding-style.html
> 3. Mirror layer_access_masks::access[] - add a
> rule_flags[LANDLOCK_MAX_NUM_LAYERS] too. struct layer_masks becomes 80
> bytes (+16).
>
> struct rule_flags {
> bool quiet:1;
> };
> struct layer_masks {
> /**
> * @access: The unfulfilled access rights for each layer.
> */
> access_mask_t access[LANDLOCK_MAX_NUM_LAYERS];
> struct rule_flags rule_flags[LANDLOCK_MAX_NUM_LAYERS];
> };
>
> (3 seems very wasteful to me)
^ permalink raw reply
* [PATCH] apparmor: Constify 'nulldfa_src' and 'stacksplitdfa_src' arrays
From: Len Bao @ 2026-05-24 11:34 UTC (permalink / raw)
To: John Johansen, Paul Moore, James Morris, Serge E. Hallyn
Cc: Len Bao, apparmor, linux-security-module, linux-kernel
The 'nulldfa_src' and 'stacksplitdfa_src' arrays are initialized in
their declarations and never changed. So, constify them to reduce the
attack surface.
To make this possible, it is also necessary to change the 'unpack_table'
and 'aa_dfa_unpack' function prototypes to pass, as a first argument, a
pointer to a 'const' blob. At the same type, define the blob exact
pointer type (pointer to const char) since all the calls to the
mentioned functions use this same type.
Before the patch (size lsm.o):
text data bss dec hex
128768 28028 704 157500 2673c
After the patch (size lsm.o):
text data bss dec hex
131264 25532 704 157500 2673c
Signed-off-by: Len Bao <len.bao@gmx.us>
---
security/apparmor/include/match.h | 2 +-
security/apparmor/lsm.c | 4 ++--
security/apparmor/match.c | 6 +++---
3 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/security/apparmor/include/match.h b/security/apparmor/include/match.h
index 7accb1c39..4a92cd044 100644
--- a/security/apparmor/include/match.h
+++ b/security/apparmor/include/match.h
@@ -125,7 +125,7 @@ static inline size_t table_size(size_t len, size_t el_size)
#define aa_state_t unsigned int
-struct aa_dfa *aa_dfa_unpack(void *blob, size_t size, int flags);
+struct aa_dfa *aa_dfa_unpack(const char *blob, size_t size, int flags);
aa_state_t aa_dfa_match_len(struct aa_dfa *dfa, aa_state_t start,
const char *str, int len);
aa_state_t aa_dfa_match(struct aa_dfa *dfa, aa_state_t start,
diff --git a/security/apparmor/lsm.c b/security/apparmor/lsm.c
index 3491e9f60..3f995b6a7 100644
--- a/security/apparmor/lsm.c
+++ b/security/apparmor/lsm.c
@@ -2432,12 +2432,12 @@ static int __init apparmor_nf_ip_init(void)
}
#endif
-static char nulldfa_src[] __aligned(8) = {
+static const char nulldfa_src[] __aligned(8) = {
#include "nulldfa.in"
};
static struct aa_dfa *nulldfa;
-static char stacksplitdfa_src[] __aligned(8) = {
+static const char stacksplitdfa_src[] __aligned(8) = {
#include "stacksplitdfa.in"
};
struct aa_dfa *stacksplitdfa;
diff --git a/security/apparmor/match.c b/security/apparmor/match.c
index 3a2c6cf02..c6f7bea1e 100644
--- a/security/apparmor/match.c
+++ b/security/apparmor/match.c
@@ -31,7 +31,7 @@
*
* NOTE: must be freed by kvfree (not kfree)
*/
-static struct table_header *unpack_table(char *blob, size_t bsize)
+static struct table_header *unpack_table(const char *blob, size_t bsize)
{
struct table_header *table = NULL;
struct table_header th;
@@ -311,11 +311,11 @@ static struct table_header *remap_data16_to_data32(struct table_header *old)
*
* Returns: an unpacked dfa ready for matching or ERR_PTR on failure
*/
-struct aa_dfa *aa_dfa_unpack(void *blob, size_t size, int flags)
+struct aa_dfa *aa_dfa_unpack(const char *blob, size_t size, int flags)
{
int hsize;
int error = -ENOMEM;
- char *data = blob;
+ const char *data = blob;
struct table_header *table = NULL;
struct aa_dfa *dfa = kzalloc_obj(struct aa_dfa);
if (!dfa)
--
2.43.0
^ permalink raw reply related
* Re: [PATCH v8 0/3]
From: Jarkko Sakkinen @ 2026-05-24 5:20 UTC (permalink / raw)
To: keyrings
Cc: David Howells, linux-crypto, linux-integrity, David Woodhouse,
James Bottomley, Stefan Berger, Herbert Xu, Mimi Zohar,
Paul Moore, James Morris, Serge E. Hallyn,
open list:SECURITY SUBSYSTEM, open list
In-Reply-To: <20260524051519.3708075-1-jarkko@kernel.org>
On Sun, May 24, 2026 at 08:15:11AM +0300, Jarkko Sakkinen wrote:
> This series introduces key type for operating with asymmetric keys using
> a TPM2 chip.
>
> Change Log
> ==========
>
> v8:
> - Reset patch change logs given the overhaul of the code and patches.
> - Have only single new subkey type.
> - Make key type only use TPM operations.
> - Use TPM2_Sign for both ECC and RSA keys.
> - Align key descriptions with other key types.
>
> Previous versions
> =================
>
> * v7: https://lore.kernel.org/linux-integrity/20240528210823.28798-1-jarkko@kernel.org/
> * v6: https://lore.kernel.org/linux-integrity/20240528035136.11464-1-jarkko@kernel.org/
> * v5: https://lore.kernel.org/linux-integrity/20240523212515.4875-1-jarkko@kernel.org/
> * v4: https://lore.kernel.org/linux-integrity/20240522005252.17841-1-jarkko@kernel.org/
> * v3: https://lore.kernel.org/linux-integrity/20240521152659.26438-1-jarkko@kernel.org/
> * v2: https://lore.kernel.org/linux-integrity/336755.1716327854@warthog.procyon.org.uk/
> * v1: https://lore.kernel.org/linux-integrity/20240520184727.22038-1-jarkko@kernel.org/
> * Derived from https://lore.kernel.org/all/20200518172704.29608-1-prestwoj@gmail.com/
>
>
> Jarkko Sakkinen (3):
> lib/asn1_encoder: Add asn1_encode_integer_bytes()
> crypto: Migrate TPMKey ASN.1 objects from trusted-keys
> keys: asymmetric: tpm2_asymmetric
>
> crypto/Kconfig | 7 +
> crypto/Makefile | 6 +
> crypto/asymmetric_keys/Kconfig | 17 +
> crypto/asymmetric_keys/Makefile | 1 +
> crypto/asymmetric_keys/tpm2_asymmetric.c | 1096 +++++++++++++++++++++
> crypto/tpm2_key.asn1 | 11 +
> crypto/tpm2_key.c | 150 +++
> include/crypto/tpm2_key.h | 46 +
> include/linux/asn1_encoder.h | 3 +
> include/linux/tpm.h | 10 +
> lib/asn1_encoder.c | 62 ++
> security/keys/trusted-keys/Kconfig | 2 +-
> security/keys/trusted-keys/Makefile | 2 -
> security/keys/trusted-keys/tpm2key.asn1 | 11 -
> security/keys/trusted-keys/trusted_tpm2.c | 119 +--
> 15 files changed, 1421 insertions(+), 122 deletions(-)
> create mode 100644 crypto/asymmetric_keys/tpm2_asymmetric.c
> create mode 100644 crypto/tpm2_key.asn1
> create mode 100644 crypto/tpm2_key.c
> create mode 100644 include/crypto/tpm2_key.h
> delete mode 100644 security/keys/trusted-keys/tpm2key.asn1
>
> --
> 2.47.3
>
There's some initial test code for this too:
https://git.kernel.org/pub/scm/linux/kernel/git/jarkko/linux-tpmdd-test.git/tree/overlay/usr/local/bin/tpmdd_tpm2_asymmetric.sh?h=main
Ugh, that's one hell of an url...
BR, Jarkko
^ permalink raw reply
* [PATCH v8 3/3] keys: asymmetric: tpm2_asymmetric
From: Jarkko Sakkinen @ 2026-05-24 5:15 UTC (permalink / raw)
To: keyrings
Cc: David Howells, linux-crypto, linux-integrity, David Woodhouse,
James Bottomley, Stefan Berger, Herbert Xu, Jarkko Sakkinen,
James Prestwood, Lukas Wunner, Ignat Korchagin, David S. Miller,
Peter Huewe, Jason Gunthorpe, James Bottomley, Mimi Zohar,
Paul Moore, James Morris, Serge E. Hallyn, open list,
open list:SECURITY SUBSYSTEM
In-Reply-To: <20260524051519.3708075-1-jarkko@kernel.org>
tpm2_asymmetric is a key type for external keys generated outside the TPM
chip but later imported to the chip's key hierarchy as leaf keys.
The key type supports ECC-NIST-P256/384/521 and RSA keys and provides
signing and verification operations for each. In addition, for RSA
encryption and decryption operations are supported.
Co-developed-by: James Prestwood <prestwoj@gmail.com>
Signed-off-by: James Prestwood <prestwoj@gmail.com>
Signed-off-by: Jarkko Sakkinen <jarkko@kernel.org>
---
crypto/asymmetric_keys/Kconfig | 17 +
crypto/asymmetric_keys/Makefile | 1 +
crypto/asymmetric_keys/tpm2_asymmetric.c | 1096 ++++++++++++++++++++++
include/linux/tpm.h | 10 +
4 files changed, 1124 insertions(+)
create mode 100644 crypto/asymmetric_keys/tpm2_asymmetric.c
diff --git a/crypto/asymmetric_keys/Kconfig b/crypto/asymmetric_keys/Kconfig
index e50bd9b3e27b..a93e13d5768f 100644
--- a/crypto/asymmetric_keys/Kconfig
+++ b/crypto/asymmetric_keys/Kconfig
@@ -15,6 +15,7 @@ config ASYMMETRIC_PUBLIC_KEY_SUBTYPE
select MPILIB
select CRYPTO_HASH_INFO
select CRYPTO_AKCIPHER
+ select CRYPTO_RSA
select CRYPTO_SIG
select CRYPTO_HASH
help
@@ -23,6 +24,22 @@ config ASYMMETRIC_PUBLIC_KEY_SUBTYPE
appropriate hash algorithms (such as SHA-1) must be available.
ENOPKG will be reported if the requisite algorithm is unavailable.
+config ASYMMETRIC_TPM2_KEY_SUBTYPE
+ tristate "Asymmetric TPM2 crypto algorithm subtype"
+ depends on TCG_TPM
+ select CRYPTO_SHA256
+ select CRYPTO_HASH_INFO
+ select CRYPTO_TPM2_KEY
+ select ASN1
+ select ASN1_ENCODER
+ help
+ This option provides support for asymmetric TPM2 key type handling.
+ Asymmetric operations such as sign and verify are delegated to the
+ TPM, and bound to the kernel crypto subsystem. Both RSA and ECDSA
+ keys are supported.
+
+ ENOPKG will be reported if the requisite algorithm is unavailable.
+
config X509_CERTIFICATE_PARSER
tristate "X.509 certificate parser"
depends on ASYMMETRIC_PUBLIC_KEY_SUBTYPE
diff --git a/crypto/asymmetric_keys/Makefile b/crypto/asymmetric_keys/Makefile
index bc65d3b98dcb..c83b40d021ac 100644
--- a/crypto/asymmetric_keys/Makefile
+++ b/crypto/asymmetric_keys/Makefile
@@ -11,6 +11,7 @@ asymmetric_keys-y := \
signature.o
obj-$(CONFIG_ASYMMETRIC_PUBLIC_KEY_SUBTYPE) += public_key.o
+obj-$(CONFIG_ASYMMETRIC_TPM2_KEY_SUBTYPE) += tpm2_asymmetric.o
#
# X.509 Certificate handling
diff --git a/crypto/asymmetric_keys/tpm2_asymmetric.c b/crypto/asymmetric_keys/tpm2_asymmetric.c
new file mode 100644
index 000000000000..f6598e6fd283
--- /dev/null
+++ b/crypto/asymmetric_keys/tpm2_asymmetric.c
@@ -0,0 +1,1096 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * An asymmetric TPM2 key subtype.
+ */
+
+#include <crypto/hash_info.h>
+#include <crypto/internal/ecc.h>
+#include <crypto/public_key.h>
+#include <crypto/tpm2_key.h>
+#include <keys/asymmetric-parser.h>
+#include <keys/asymmetric-subtype.h>
+#include <linux/asn1_encoder.h>
+#include <linux/keyctl.h>
+#include <linux/module.h>
+#include <linux/slab.h>
+#include <linux/tpm.h>
+#include <linux/unaligned.h>
+
+#undef pr_fmt
+#define pr_fmt(fmt) "tpm2_asymmetric: "fmt
+
+/* TPM2 Structures 12.2.3.5: TPMS_RSA_PARMS */
+struct tpm2_asymmetric_rsa_parms {
+ __be16 symmetric;
+ __be16 scheme;
+ __be16 key_bits;
+ __be32 exponent;
+ __be16 modulus_size;
+} __packed;
+
+/* TPM2 Structures 12.2.3.6: TPMS_ECC_PARMS */
+struct tpm2_asymmetric_ecc_parms {
+ __be16 symmetric;
+ __be16 scheme;
+ __be16 ecc;
+ __be16 kdf;
+};
+
+static const void *tpm2_asymmetric_parms(const struct tpm2_key *key)
+{
+ return &key->data[key->priv_len + 2 + sizeof(*key->desc)];
+}
+
+static u16 tpm2_asymmetric_rsa_mod_size(const struct tpm2_key *key)
+{
+ const struct tpm2_asymmetric_rsa_parms *p = tpm2_asymmetric_parms(key);
+
+ return be16_to_cpu(p->modulus_size);
+}
+
+static const u8 *tpm2_asymmetric_ecc_x(const struct tpm2_key *key)
+{
+ return tpm2_asymmetric_parms(key) + sizeof(struct tpm2_asymmetric_ecc_parms);
+}
+
+static const u8 *tpm2_asymmetric_ecc_y(const struct tpm2_key *key)
+{
+ const u8 *x = tpm2_asymmetric_ecc_x(key);
+ u16 x_size = get_unaligned_be16(&x[0]);
+
+ return &x[2 + x_size];
+}
+
+static unsigned int tpm2_asymmetric_ecc_key_bits(u16 ecc)
+{
+ switch (ecc) {
+ case TPM2_ECC_NIST_P256:
+ return 256;
+ case TPM2_ECC_NIST_P384:
+ return 384;
+ case TPM2_ECC_NIST_P521:
+ return 521;
+ default:
+ return 0;
+ }
+}
+
+static int tpm2_asymmetric_hash_lookup(const char *hash_algo,
+ int *hash_id, int *tpm_hash)
+{
+ int id, alg;
+
+ if (!hash_algo)
+ return -EINVAL;
+
+ id = match_string(hash_algo_name, HASH_ALGO__LAST, hash_algo);
+ if (id < 0)
+ return -ENOPKG;
+
+ alg = tpm2_find_hash_alg(id);
+ if (alg < 0)
+ return -ENOPKG;
+
+ if (hash_id)
+ *hash_id = id;
+
+ if (tpm_hash)
+ *tpm_hash = alg;
+
+ return 0;
+}
+
+static int tpm2_asymmetric_signature_scheme(const struct tpm2_key *key,
+ const char *encoding,
+ const char *hash_algo,
+ u16 *scheme,
+ int *tpm_hash)
+{
+ if (!encoding)
+ return -ENOPKG;
+
+ switch (tpm2_key_type(key)) {
+ case TPM_ALG_RSA:
+ if (strcmp(encoding, "pkcs1") != 0)
+ return -ENOPKG;
+ *scheme = TPM_ALG_RSASSA;
+ break;
+ case TPM_ALG_ECC:
+ if (strcmp(encoding, "x962") != 0)
+ return -ENOPKG;
+ *scheme = TPM_ALG_ECDSA;
+ break;
+ default:
+ return -EOPNOTSUPP;
+ }
+
+ return tpm2_asymmetric_hash_lookup(hash_algo, NULL, tpm_hash);
+}
+
+/*
+ * Load a TPM2 key blob into the TPM.
+ *
+ * On success, @buf is initialized and the authorization session is kept open.
+ * On failure, @buf is destroyed and the authorization session is closed.
+ */
+static int tpm2_asymmetric_load(struct tpm_chip *chip, struct tpm2_key *key,
+ struct tpm_buf *buf, u32 *handle_out)
+{
+ int ret;
+
+ ret = tpm2_start_auth_session(chip);
+ if (ret)
+ return ret;
+
+ ret = tpm_buf_init(buf, TPM2_ST_SESSIONS, TPM2_CC_LOAD);
+ if (ret < 0)
+ goto err_auth;
+
+ ret = tpm_buf_append_name(chip, buf, key->parent, NULL);
+ if (ret)
+ goto err_buf;
+ tpm_buf_append_hmac_session(chip, buf, TPM2_SA_CONTINUE_SESSION |
+ TPM2_SA_ENCRYPT, NULL, 0);
+ tpm_buf_append(buf, &key->data[0], key->priv_len + key->pub_len);
+ if (buf->flags & TPM_BUF_OVERFLOW) {
+ ret = -E2BIG;
+ goto err_buf;
+ }
+ ret = tpm_buf_fill_hmac_session(chip, buf);
+ if (ret)
+ goto err_buf;
+ ret = tpm_transmit_cmd(chip, buf, 4, "TPM2_CC_LOAD");
+ ret = tpm_buf_check_hmac_response(chip, buf, ret);
+ if (ret) {
+ ret = -EIO;
+ goto err_buf;
+ }
+
+ *handle_out = be32_to_cpup((__be32 *)&buf->data[TPM_HEADER_SIZE]);
+ return 0;
+
+err_buf:
+ tpm_buf_destroy(buf);
+
+err_auth:
+ tpm2_end_auth_session(chip);
+ return ret;
+}
+
+static void tpm2_asymmetric_key_destroy(void *payload0, void *payload3)
+{
+ kfree(payload0);
+}
+
+/*
+ * Encrypt using TPM2_RSA_Encrypt with RSAES (PKCS#1 v1.5) scheme.
+ */
+static int tpm2_asymmetric_rsa_encrypt(struct tpm_chip *chip,
+ struct tpm2_key *key,
+ struct kernel_pkey_params *params,
+ const void *in, void *out)
+{
+ u32 key_handle = 0;
+ struct tpm_buf buf;
+ u16 ciphertext_len;
+ u16 scheme;
+ u8 *pos;
+ int ret;
+
+ if (!params->encoding)
+ return -EINVAL;
+
+ if (strcmp(params->encoding, "pkcs1") == 0)
+ scheme = TPM_ALG_RSAES;
+ else if (strcmp(params->encoding, "raw") == 0)
+ scheme = TPM_ALG_NULL;
+ else
+ return -ENOPKG;
+
+ ret = tpm_try_get_ops(chip);
+ if (ret)
+ return ret;
+
+ ret = tpm2_asymmetric_load(chip, key, &buf, &key_handle);
+ if (ret)
+ goto err_ops;
+
+ tpm2_end_auth_session(chip);
+ tpm_buf_destroy(&buf);
+
+ ret = tpm_buf_init(&buf, TPM2_ST_NO_SESSIONS, TPM2_CC_RSA_ENCRYPT);
+ if (ret)
+ goto err_key;
+
+ tpm_buf_append_u32(&buf, key_handle);
+
+ tpm_buf_append_u16(&buf, params->in_len);
+ tpm_buf_append(&buf, in, params->in_len);
+
+ tpm_buf_append_u16(&buf, scheme);
+
+ tpm_buf_append_u16(&buf, 0);
+
+ ret = tpm_transmit_cmd(chip, &buf, 4, "TPM2_RSA_Encrypt");
+ if (ret) {
+ ret = -EIO;
+ goto err_buf;
+ }
+
+ pos = buf.data + TPM_HEADER_SIZE;
+ ciphertext_len = be16_to_cpup((__be16 *)pos);
+ pos += 2;
+ if (pos + ciphertext_len > buf.data + buf.length) {
+ ret = -EIO;
+ goto err_buf;
+ }
+
+ if (params->out_len < ciphertext_len) {
+ ret = -EMSGSIZE;
+ goto err_buf;
+ }
+
+ memcpy(out, pos, ciphertext_len);
+ ret = ciphertext_len;
+
+err_buf:
+ tpm_buf_destroy(&buf);
+
+err_key:
+ tpm2_flush_context(chip, key_handle);
+
+err_ops:
+ tpm_put_ops(chip);
+ return ret;
+}
+
+/*
+ * Convert a TPM2B_PUBLIC_KEY_RSA response into a raw RSA signature.
+ */
+static int tpm2_asymmetric_rsa_parse_signature(struct tpm_buf *buf,
+ off_t *offset,
+ struct kernel_pkey_params *params,
+ void *out)
+{
+ u16 sig_len;
+
+ sig_len = tpm_buf_read_u16(buf, offset);
+ if (buf->flags & TPM_BUF_BOUNDARY_ERROR)
+ return -EIO;
+ if (*offset + sig_len > buf->length)
+ return -EIO;
+ if (sig_len > params->out_len)
+ return -EMSGSIZE;
+
+ memcpy(out, &buf->data[*offset], sig_len);
+ return sig_len;
+}
+
+/*
+ * Convert a TPMT_SIGNATURE ECDSA R/S response into DER SEQUENCE form.
+ */
+static int tpm2_asymmetric_ecc_parse_signature(struct tpm_buf *buf, off_t *offset,
+ struct kernel_pkey_params *params,
+ void *out)
+{
+ u8 der[2 * (2 + ECC_MAX_BYTES + 1)];
+ u8 *encoded, *ptr;
+ const u8 *s;
+ u16 r_size;
+ u16 s_size;
+
+ r_size = tpm_buf_read_u16(buf, offset);
+ if (buf->flags & TPM_BUF_BOUNDARY_ERROR)
+ return -EIO;
+ if (r_size == 0 || r_size > ECC_MAX_BYTES ||
+ *offset + r_size + 2 > buf->length)
+ return -EIO;
+
+ s_size = get_unaligned_be16(&buf->data[*offset + r_size]);
+ s = &buf->data[*offset + r_size + 2];
+ if (s_size == 0 || s_size > ECC_MAX_BYTES ||
+ *offset + r_size + 2 + s_size > buf->length)
+ return -EIO;
+
+ ptr = der;
+ ptr = asn1_encode_integer_bytes(ptr, der + sizeof(der),
+ &buf->data[*offset], r_size);
+ ptr = asn1_encode_integer_bytes(ptr, der + sizeof(der), s, s_size);
+ if (IS_ERR(ptr))
+ return PTR_ERR(ptr);
+
+ encoded = asn1_encode_sequence(out, (u8 *)out + params->out_len,
+ der, ptr - der);
+ if (IS_ERR(encoded))
+ return PTR_ERR(encoded) == -EINVAL ? -EMSGSIZE : PTR_ERR(encoded);
+
+ return encoded - (u8 *)out;
+}
+
+static int tpm2_asymmetric_parse_signature(struct tpm_buf *buf,
+ u16 scheme, int tpm_hash,
+ struct kernel_pkey_params *params,
+ void *out)
+{
+ off_t offset = TPM_HEADER_SIZE + 4;
+ u16 hash_alg;
+ u16 sig_alg;
+
+ sig_alg = tpm_buf_read_u16(buf, &offset);
+ hash_alg = tpm_buf_read_u16(buf, &offset);
+ if (buf->flags & TPM_BUF_BOUNDARY_ERROR)
+ return -EIO;
+ if (sig_alg != scheme || hash_alg != tpm_hash)
+ return -EIO;
+
+ switch (scheme) {
+ case TPM_ALG_RSASSA:
+ return tpm2_asymmetric_rsa_parse_signature(buf, &offset, params, out);
+ case TPM_ALG_ECDSA:
+ return tpm2_asymmetric_ecc_parse_signature(buf, &offset, params, out);
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+/*
+ * Sign a digest using TPM2_Sign.
+ */
+static int tpm2_asymmetric_sign(struct tpm_chip *chip, struct tpm2_key *key,
+ struct kernel_pkey_params *params,
+ const void *in, void *out)
+{
+ struct tpm_buf buf;
+ u32 key_handle = 0;
+ int tpm_hash;
+ u16 scheme;
+ int ret;
+
+ ret = tpm2_asymmetric_signature_scheme(key, params->encoding,
+ params->hash_algo, &scheme,
+ &tpm_hash);
+ if (ret)
+ return ret;
+
+ ret = tpm_try_get_ops(chip);
+ if (ret)
+ return ret;
+
+ ret = tpm2_asymmetric_load(chip, key, &buf, &key_handle);
+ if (ret)
+ goto err_ops;
+
+ tpm_buf_reset(&buf, TPM2_ST_SESSIONS, TPM2_CC_SIGN);
+ ret = tpm_buf_append_name(chip, &buf, key_handle, NULL);
+ if (ret)
+ goto err_key;
+ tpm_buf_append_hmac_session(chip, &buf, TPM2_SA_DECRYPT, NULL, 0);
+
+ /* digest (TPM2B_DIGEST) */
+ tpm_buf_append_u16(&buf, params->in_len);
+ tpm_buf_append(&buf, in, params->in_len);
+
+ /* inScheme (TPMT_SIG_SCHEME) */
+ tpm_buf_append_u16(&buf, scheme);
+ tpm_buf_append_u16(&buf, tpm_hash);
+
+ /* validation (TPMT_TK_HASHCHECK): NULL ticket */
+ tpm_buf_append_u16(&buf, TPM2_ST_HASHCHECK);
+ tpm_buf_append_u32(&buf, TPM2_RH_NULL);
+ tpm_buf_append_u16(&buf, 0);
+
+ if (buf.flags & TPM_BUF_OVERFLOW) {
+ tpm2_end_auth_session(chip);
+ ret = -E2BIG;
+ goto err_key;
+ }
+ ret = tpm_buf_fill_hmac_session(chip, &buf);
+ if (ret)
+ goto err_key;
+ ret = tpm_transmit_cmd(chip, &buf, 4, "TPM2_Sign");
+ ret = tpm_buf_check_hmac_response(chip, &buf, ret);
+ if (ret) {
+ ret = -EIO;
+ goto err_key;
+ }
+
+ ret = tpm2_asymmetric_parse_signature(&buf, scheme, tpm_hash, params, out);
+
+err_key:
+ tpm2_flush_context(chip, key_handle);
+ tpm_buf_destroy(&buf);
+
+err_ops:
+ tpm_put_ops(chip);
+ return ret;
+}
+
+/*
+ * Decrypt using TPM2_RSA_Decrypt with RSAES-PKCS1-v1_5 scheme.
+ */
+static int tpm2_asymmetric_rsa_decrypt(struct tpm_chip *chip,
+ struct tpm2_key *key,
+ struct kernel_pkey_params *params,
+ const void *in, void *out)
+{
+ u32 key_handle = 0;
+ struct tpm_buf buf;
+ u16 decrypted_len;
+ off_t offset;
+ int ret;
+
+ if (!params->encoding || strcmp(params->encoding, "pkcs1") != 0)
+ return -ENOPKG;
+
+ ret = tpm_try_get_ops(chip);
+ if (ret)
+ return ret;
+
+ ret = tpm2_asymmetric_load(chip, key, &buf, &key_handle);
+ if (ret)
+ goto err_ops;
+
+ tpm_buf_reset(&buf, TPM2_ST_SESSIONS, TPM2_CC_RSA_DECRYPT);
+ ret = tpm_buf_append_name(chip, &buf, key_handle, NULL);
+ if (ret)
+ goto err_key;
+ tpm_buf_append_hmac_session(chip, &buf, TPM2_SA_DECRYPT, NULL, 0);
+ tpm_buf_append_u16(&buf, params->in_len);
+ tpm_buf_append(&buf, in, params->in_len);
+ tpm_buf_append_u16(&buf, TPM_ALG_RSAES);
+ tpm_buf_append_u16(&buf, 0);
+ if (buf.flags & TPM_BUF_OVERFLOW) {
+ tpm2_end_auth_session(chip);
+ ret = -E2BIG;
+ goto err_key;
+ }
+ ret = tpm_buf_fill_hmac_session(chip, &buf);
+ if (ret)
+ goto err_key;
+ ret = tpm_transmit_cmd(chip, &buf, 4, "TPM2_RSA_DECRYPT");
+ ret = tpm_buf_check_hmac_response(chip, &buf, ret);
+ if (ret) {
+ ret = -EIO;
+ goto err_key;
+ }
+
+ offset = TPM_HEADER_SIZE + 4;
+ decrypted_len = tpm_buf_read_u16(&buf, &offset);
+ if (buf.flags & TPM_BUF_BOUNDARY_ERROR) {
+ ret = -EIO;
+ goto err_key;
+ }
+ if (offset + decrypted_len > buf.length) {
+ ret = -EIO;
+ goto err_key;
+ }
+
+ if (params->out_len < decrypted_len) {
+ ret = -EMSGSIZE;
+ goto err_key;
+ }
+
+ memcpy(out, &buf.data[offset], decrypted_len);
+ ret = decrypted_len;
+
+err_key:
+ tpm2_flush_context(chip, key_handle);
+ tpm_buf_destroy(&buf);
+
+err_ops:
+ tpm_put_ops(chip);
+ return ret;
+}
+
+/*
+ * Verify an RSA signature using TPM2_VerifySignature with RSASSA scheme.
+ */
+static int tpm2_asymmetric_rsa_verify(const struct key *key,
+ const struct public_key_signature *sig)
+{
+ struct tpm2_key *tpm2_key = key->payload.data[asym_crypto];
+ struct tpm_chip *chip;
+ struct tpm_buf buf;
+ u32 key_handle = 0;
+ int tpm_hash;
+ int ret;
+
+ if (!sig->m)
+ return -ENOPKG;
+
+ if (!sig->encoding || strcmp(sig->encoding, "pkcs1") != 0)
+ return -ENOPKG;
+
+ if (!sig->hash_algo)
+ return -EINVAL;
+
+ chip = tpm_default_chip();
+
+ if (!chip)
+ return -ENODEV;
+
+ ret = tpm2_asymmetric_hash_lookup(sig->hash_algo, NULL, &tpm_hash);
+ if (ret)
+ goto err_chip;
+
+ ret = tpm_try_get_ops(chip);
+ if (ret)
+ goto err_chip;
+
+ ret = tpm2_asymmetric_load(chip, tpm2_key, &buf, &key_handle);
+ if (ret)
+ goto err_ops;
+
+ tpm2_end_auth_session(chip);
+ tpm_buf_destroy(&buf);
+
+ ret = tpm_buf_init(&buf, TPM2_ST_NO_SESSIONS,
+ TPM2_CC_VERIFY_SIGNATURE);
+ if (ret)
+ goto err_key;
+
+ tpm_buf_append_u32(&buf, key_handle);
+
+ tpm_buf_append_u16(&buf, sig->m_size);
+ tpm_buf_append(&buf, sig->m, sig->m_size);
+
+ tpm_buf_append_u16(&buf, TPM_ALG_RSASSA);
+ tpm_buf_append_u16(&buf, tpm_hash);
+ tpm_buf_append_u16(&buf, sig->s_size);
+ tpm_buf_append(&buf, sig->s, sig->s_size);
+
+ ret = tpm_transmit_cmd(chip, &buf, 0, "TPM2_VerifySignature");
+ if (ret)
+ ret = -EKEYREJECTED;
+
+ tpm_buf_destroy(&buf);
+
+err_key:
+ tpm2_flush_context(chip, key_handle);
+
+err_ops:
+ tpm_put_ops(chip);
+
+err_chip:
+ put_device(&chip->dev);
+ return ret;
+}
+
+static int tpm2_asymmetric_rsa_query(const struct kernel_pkey_params *params,
+ struct kernel_pkey_query *info)
+{
+ const struct tpm2_key *key = params->key->payload.data[asym_crypto];
+ u16 max_data_size = TPM2_MAX_DIGEST_SIZE;
+ const u16 mod_size = tpm2_asymmetric_rsa_mod_size(key);
+ int hash_id, ret;
+
+ if (!params->encoding)
+ return -EINVAL;
+
+ memset(info, 0, sizeof(*info));
+ info->key_size = mod_size * 8;
+
+ if (strcmp(params->encoding, "pkcs1") == 0) {
+ if (params->hash_algo) {
+ ret = tpm2_asymmetric_hash_lookup(params->hash_algo, &hash_id, NULL);
+ if (ret)
+ return ret;
+ max_data_size = hash_digest_size[hash_id];
+ }
+
+ info->max_data_size = max_data_size;
+ info->max_sig_size = mod_size;
+ info->max_enc_size = mod_size;
+ info->max_dec_size = mod_size;
+ info->supported_ops = KEYCTL_SUPPORTS_SIGN |
+ KEYCTL_SUPPORTS_VERIFY |
+ KEYCTL_SUPPORTS_ENCRYPT |
+ KEYCTL_SUPPORTS_DECRYPT;
+ return 0;
+ }
+
+ if (strcmp(params->encoding, "raw") == 0) {
+ info->max_data_size = mod_size;
+ info->max_enc_size = mod_size;
+ info->max_dec_size = mod_size;
+ info->supported_ops = KEYCTL_SUPPORTS_ENCRYPT;
+ return 0;
+ }
+
+ return -ENOPKG;
+}
+
+static int tpm2_asymmetric_rsa_validate(const struct tpm2_key *key)
+{
+ const struct tpm2_asymmetric_rsa_parms *p = tpm2_asymmetric_parms(key);
+ u16 key_bits;
+ u16 mod_size;
+
+ if (tpm2_key_policy_size(key) != 0)
+ return -EBADMSG;
+
+ if (key->pub_len < 2 + sizeof(*key->desc) + sizeof(*p))
+ return -EBADMSG;
+
+ if (be16_to_cpu(p->symmetric) != TPM_ALG_NULL)
+ return -EBADMSG;
+
+ if (be16_to_cpu(p->scheme) != TPM_ALG_NULL)
+ return -EBADMSG;
+
+ key_bits = be16_to_cpu(p->key_bits);
+ if (key_bits != 2048 && key_bits != 3072 && key_bits != 4096)
+ return -EBADMSG;
+
+ if (be32_to_cpu(p->exponent) != 0x00000000 &&
+ be32_to_cpu(p->exponent) != 0x00010001)
+ return -EBADMSG;
+
+ mod_size = tpm2_asymmetric_rsa_mod_size(key);
+ if (mod_size != key_bits / 8)
+ return -EBADMSG;
+
+ if (key->pub_len < 2 + sizeof(*key->desc) + sizeof(*p) + mod_size)
+ return -EBADMSG;
+
+ return 0;
+}
+
+static unsigned int tpm2_asymmetric_der_len_size(unsigned int len)
+{
+ if (len < 128)
+ return 1;
+ if (len <= 255)
+ return 2;
+ return 3;
+}
+
+/*
+ * Parse a DER-encoded ECDSA signature: SEQUENCE { INTEGER r, INTEGER s }.
+ *
+ * On success, @r/@r_len and @s/@s_len point into @der with leading zero
+ * pads stripped.
+ */
+static int tpm2_asymmetric_ecc_parse_der_signature(const u8 *der, u32 der_len,
+ const u8 **r, u16 *r_len,
+ const u8 **s, u16 *s_len)
+{
+ const u8 *end = der + der_len;
+ u32 seq_len, int_len;
+ const u8 *p = der;
+
+ if (p >= end || *p++ != 0x30)
+ return -EBADMSG;
+
+ if (p >= end)
+ return -EBADMSG;
+ if (*p < 0x80) {
+ seq_len = *p++;
+ } else if (*p == 0x81) {
+ if (++p >= end)
+ return -EBADMSG;
+ seq_len = *p++;
+ } else {
+ return -EBADMSG;
+ }
+
+ if (p + seq_len > end)
+ return -EBADMSG;
+ end = p + seq_len;
+
+ /* INTEGER r */
+ if (p >= end || *p++ != 0x02)
+ return -EBADMSG;
+ if (p >= end)
+ return -EBADMSG;
+ int_len = *p++;
+ if (int_len == 0 || int_len >= 0x80 || p + int_len > end)
+ return -EBADMSG;
+ while (int_len > 1 && *p == 0x00) {
+ p++;
+ int_len--;
+ }
+ *r = p;
+ *r_len = int_len;
+ p += int_len;
+
+ /* INTEGER s */
+ if (p >= end || *p++ != 0x02)
+ return -EBADMSG;
+ if (p >= end)
+ return -EBADMSG;
+ int_len = *p++;
+ if (int_len == 0 || int_len >= 0x80 || p + int_len > end)
+ return -EBADMSG;
+ while (int_len > 1 && *p == 0x00) {
+ p++;
+ int_len--;
+ }
+ *s = p;
+ *s_len = int_len;
+ p += int_len;
+
+ if (p != end)
+ return -EBADMSG;
+
+ return 0;
+}
+
+/*
+ * Verify an ECDSA signature using TPM2_VerifySignature.
+ *
+ * A DER-encoded signature is parsed into (r, s) components for the TPM command.
+ */
+static int tpm2_asymmetric_ecc_verify(const struct key *key,
+ const struct public_key_signature *sig)
+{
+ struct tpm2_key *tpm2_key = key->payload.data[asym_crypto];
+ struct tpm_chip *chip;
+ const u8 *r, *s_data;
+ struct tpm_buf buf;
+ u32 key_handle = 0;
+ u16 r_len, s_len;
+ int tpm_hash;
+ int ret;
+
+ if (!sig->m)
+ return -ENOPKG;
+
+ if (!sig->encoding || strcmp(sig->encoding, "x962") != 0)
+ return -ENOPKG;
+
+ if (!sig->hash_algo)
+ return -EINVAL;
+
+ chip = tpm_default_chip();
+
+ if (!chip)
+ return -ENODEV;
+
+ ret = tpm2_asymmetric_hash_lookup(sig->hash_algo, NULL, &tpm_hash);
+ if (ret)
+ goto err_chip;
+
+ ret = tpm2_asymmetric_ecc_parse_der_signature(sig->s, sig->s_size,
+ &r, &r_len, &s_data,
+ &s_len);
+ if (ret)
+ goto err_chip;
+
+ ret = tpm_try_get_ops(chip);
+ if (ret)
+ goto err_chip;
+
+ ret = tpm2_asymmetric_load(chip, tpm2_key, &buf, &key_handle);
+ if (ret)
+ goto err_ops;
+
+ tpm2_end_auth_session(chip);
+ tpm_buf_destroy(&buf);
+
+ ret = tpm_buf_init(&buf, TPM2_ST_NO_SESSIONS,
+ TPM2_CC_VERIFY_SIGNATURE);
+ if (ret)
+ goto err_key;
+
+ tpm_buf_append_u32(&buf, key_handle);
+
+ /* digest (TPM2B_DIGEST) */
+ tpm_buf_append_u16(&buf, sig->m_size);
+ tpm_buf_append(&buf, sig->m, sig->m_size);
+
+ /* signature (TPMT_SIGNATURE): ECDSA with the given hash */
+ tpm_buf_append_u16(&buf, TPM_ALG_ECDSA);
+ tpm_buf_append_u16(&buf, tpm_hash);
+
+ /* signatureR (TPM2B_ECC_PARAMETER) */
+ tpm_buf_append_u16(&buf, r_len);
+ tpm_buf_append(&buf, r, r_len);
+
+ /* signatureS (TPM2B_ECC_PARAMETER) */
+ tpm_buf_append_u16(&buf, s_len);
+ tpm_buf_append(&buf, s_data, s_len);
+
+ ret = tpm_transmit_cmd(chip, &buf, 0, "TPM2_VerifySignature");
+ if (ret)
+ ret = -EKEYREJECTED;
+
+ tpm_buf_destroy(&buf);
+
+err_key:
+ tpm2_flush_context(chip, key_handle);
+
+err_ops:
+ tpm_put_ops(chip);
+
+err_chip:
+ put_device(&chip->dev);
+ return ret;
+}
+
+static int tpm2_asymmetric_ecc_query(const struct kernel_pkey_params *params,
+ struct kernel_pkey_query *info)
+{
+ const struct tpm2_key *key = params->key->payload.data[asym_crypto];
+ const struct tpm2_asymmetric_ecc_parms *p = tpm2_asymmetric_parms(key);
+ unsigned int int_len, seq_payload;
+ const u8 *x;
+ u16 ecc, n;
+ int ret;
+
+ ecc = be16_to_cpu(p->ecc);
+ x = tpm2_asymmetric_ecc_x(key);
+ n = get_unaligned_be16(&x[0]);
+ int_len = n + 1;
+
+ if (!params->encoding || strcmp(params->encoding, "x962") != 0)
+ return -ENOPKG;
+
+ ret = tpm2_asymmetric_hash_lookup(params->hash_algo, NULL, NULL);
+ if (ret)
+ return ret;
+
+ /*
+ * SEQUENCE { INTEGER (<=n+1 bytes), INTEGER (<=n+1 bytes) }
+ */
+ seq_payload = 2 * (1 + tpm2_asymmetric_der_len_size(int_len) + int_len);
+
+ memset(info, 0, sizeof(*info));
+ info->key_size = tpm2_asymmetric_ecc_key_bits(ecc);
+ info->max_sig_size = 1 + tpm2_asymmetric_der_len_size(seq_payload) + seq_payload;
+ info->max_data_size = TPM2_MAX_DIGEST_SIZE;
+ info->supported_ops = KEYCTL_SUPPORTS_SIGN | KEYCTL_SUPPORTS_VERIFY;
+
+ return 0;
+}
+
+static int tpm2_asymmetric_ecc_validate(const struct tpm2_key *key)
+{
+ const struct tpm2_asymmetric_ecc_parms *p = tpm2_asymmetric_parms(key);
+ size_t min_len = 2 + sizeof(*key->desc) + sizeof(*p);
+ u16 x_size, y_size;
+ const u8 *x, *y;
+
+ if (tpm2_key_policy_size(key) != 0)
+ return -EBADMSG;
+
+ if (key->pub_len < min_len + 2)
+ return -EBADMSG;
+
+ if (be16_to_cpu(p->symmetric) != TPM_ALG_NULL)
+ return -EBADMSG;
+
+ if (be16_to_cpu(p->scheme) != TPM_ALG_NULL)
+ return -EBADMSG;
+
+ if (be16_to_cpu(p->ecc) != TPM2_ECC_NIST_P256 &&
+ be16_to_cpu(p->ecc) != TPM2_ECC_NIST_P384 &&
+ be16_to_cpu(p->ecc) != TPM2_ECC_NIST_P521)
+ return -EBADMSG;
+
+ if (be16_to_cpu(p->kdf) != TPM_ALG_NULL)
+ return -EBADMSG;
+
+ x = tpm2_asymmetric_ecc_x(key);
+ x_size = get_unaligned_be16(&x[0]);
+ if (x_size > ECC_MAX_BYTES)
+ return -EBADMSG;
+
+ if (key->pub_len < min_len + 2 + x_size + 2)
+ return -EBADMSG;
+
+ y = tpm2_asymmetric_ecc_y(key);
+ y_size = get_unaligned_be16(&y[0]);
+ if (y_size > ECC_MAX_BYTES)
+ return -EBADMSG;
+
+ if (key->pub_len < min_len + 2 + x_size + 2 + y_size)
+ return -EBADMSG;
+
+ if (x_size != y_size)
+ return -EBADMSG;
+
+ return 0;
+}
+
+static const char *tpm2_asymmetric_ecc_name(const struct tpm2_key *key)
+{
+ const struct tpm2_asymmetric_ecc_parms *p;
+
+ p = tpm2_asymmetric_parms(key);
+
+ switch (be16_to_cpu(p->ecc)) {
+ case TPM2_ECC_NIST_P256:
+ return "ecdsa-nist-p256";
+ case TPM2_ECC_NIST_P384:
+ return "ecdsa-nist-p384";
+ case TPM2_ECC_NIST_P521:
+ return "ecdsa-nist-p521";
+ default:
+ return "ecdsa";
+ }
+}
+
+static void tpm2_asymmetric_describe(const struct key *asymmetric_key,
+ struct seq_file *m)
+{
+ const struct tpm2_key *key;
+
+ key = asymmetric_key->payload.data[asym_crypto];
+ if (!key)
+ return;
+
+ switch (tpm2_key_type(key)) {
+ case TPM_ALG_RSA:
+ seq_puts(m, "tpm2.rsa");
+ break;
+ case TPM_ALG_ECC:
+ seq_printf(m, "tpm2.%s", tpm2_asymmetric_ecc_name(key));
+ break;
+ default:
+ seq_puts(m, "tpm2.unknown");
+ break;
+ }
+}
+
+static int tpm2_asymmetric_query(const struct kernel_pkey_params *params,
+ struct kernel_pkey_query *info)
+{
+ struct tpm2_key *key = params->key->payload.data[asym_crypto];
+
+ switch (tpm2_key_type(key)) {
+ case TPM_ALG_RSA:
+ return tpm2_asymmetric_rsa_query(params, info);
+ case TPM_ALG_ECC:
+ return tpm2_asymmetric_ecc_query(params, info);
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+static int tpm2_asymmetric_eds_op(struct kernel_pkey_params *params,
+ const void *in, void *out)
+{
+ struct tpm2_key *key = params->key->payload.data[asym_crypto];
+ struct tpm_chip *chip;
+ int ret;
+
+ chip = tpm_default_chip();
+ if (!chip)
+ return -ENODEV;
+
+ switch (params->op) {
+ case kernel_pkey_encrypt:
+ if (tpm2_key_type(key) != TPM_ALG_RSA) {
+ ret = -EOPNOTSUPP;
+ break;
+ }
+ ret = tpm2_asymmetric_rsa_encrypt(chip, key, params, in, out);
+ break;
+ case kernel_pkey_decrypt:
+ if (tpm2_key_type(key) != TPM_ALG_RSA) {
+ ret = -EOPNOTSUPP;
+ break;
+ }
+ ret = tpm2_asymmetric_rsa_decrypt(chip, key, params, in, out);
+ break;
+ case kernel_pkey_sign:
+ ret = tpm2_asymmetric_sign(chip, key, params, in, out);
+ break;
+ default:
+ ret = -EOPNOTSUPP;
+ break;
+ }
+
+ put_device(&chip->dev);
+ return ret;
+}
+
+static int tpm2_asymmetric_verify_signature(const struct key *asymmetric_key,
+ const struct public_key_signature *sig)
+{
+ struct tpm2_key *key = asymmetric_key->payload.data[asym_crypto];
+
+ switch (tpm2_key_type(key)) {
+ case TPM_ALG_RSA:
+ return tpm2_asymmetric_rsa_verify(asymmetric_key, sig);
+ case TPM_ALG_ECC:
+ return tpm2_asymmetric_ecc_verify(asymmetric_key, sig);
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+static struct asymmetric_key_subtype tpm2_asymmetric_subtype = {
+ .owner = THIS_MODULE,
+ .name = "tpm2_asymmetric_key",
+ .name_len = sizeof("tpm2_asymmetric_key") - 1,
+ .describe = tpm2_asymmetric_describe,
+ .destroy = tpm2_asymmetric_key_destroy,
+ .query = tpm2_asymmetric_query,
+ .eds_op = tpm2_asymmetric_eds_op,
+ .verify_signature = tpm2_asymmetric_verify_signature,
+};
+
+static int tpm2_asymmetric_preparse(struct key_preparsed_payload *prep)
+{
+ struct tpm2_key *key __free(kfree) = NULL;
+ int ret;
+
+ key = tpm2_key_decode(prep->data, prep->datalen);
+ if (IS_ERR(key)) {
+ ret = PTR_ERR(key);
+ key = NULL;
+ return ret;
+ }
+
+ if (key->oid != OID_TPMLoadableKey)
+ return -EBADMSG;
+
+ switch (tpm2_key_type(key)) {
+ case TPM_ALG_RSA:
+ ret = tpm2_asymmetric_rsa_validate(key);
+ break;
+ case TPM_ALG_ECC:
+ ret = tpm2_asymmetric_ecc_validate(key);
+ break;
+ default:
+ ret = -EBADMSG;
+ break;
+ }
+
+ if (ret < 0)
+ return ret;
+
+ __module_get(tpm2_asymmetric_subtype.owner);
+
+ prep->payload.data[asym_subtype] = &tpm2_asymmetric_subtype;
+ prep->payload.data[asym_key_ids] = NULL;
+ prep->payload.data[asym_crypto] = no_free_ptr(key);
+ prep->payload.data[asym_auth] = NULL;
+ prep->quotalen = 100;
+
+ return 0;
+}
+
+static struct asymmetric_key_parser tpm2_asymmetric_parser = {
+ .owner = THIS_MODULE,
+ .name = "tpm2_asymmetric_parser",
+ .parse = tpm2_asymmetric_preparse,
+};
+
+static int __init tpm2_asymmetric_init(void)
+{
+ return register_asymmetric_key_parser(&tpm2_asymmetric_parser);
+}
+
+static void __exit tpm2_asymmetric_exit(void)
+{
+ unregister_asymmetric_key_parser(&tpm2_asymmetric_parser);
+}
+
+module_init(tpm2_asymmetric_init);
+module_exit(tpm2_asymmetric_exit);
+
+MODULE_DESCRIPTION("Asymmetric TPM2 key");
+MODULE_LICENSE("GPL");
diff --git a/include/linux/tpm.h b/include/linux/tpm.h
index 202da079d500..d4d5ddc0173a 100644
--- a/include/linux/tpm.h
+++ b/include/linux/tpm.h
@@ -45,6 +45,7 @@ enum tpm2_session_types {
/* if you add a new hash to this, increment TPM_MAX_HASHES below */
enum tpm_algorithms {
TPM_ALG_ERROR = 0x0000,
+ TPM_ALG_RSA = 0x0001,
TPM_ALG_SHA1 = 0x0004,
TPM_ALG_AES = 0x0006,
TPM_ALG_KEYEDHASH = 0x0008,
@@ -53,6 +54,9 @@ enum tpm_algorithms {
TPM_ALG_SHA512 = 0x000D,
TPM_ALG_NULL = 0x0010,
TPM_ALG_SM3_256 = 0x0012,
+ TPM_ALG_RSASSA = 0x0014,
+ TPM_ALG_RSAES = 0x0015,
+ TPM_ALG_ECDSA = 0x0018,
TPM_ALG_ECC = 0x0023,
TPM_ALG_CFB = 0x0043,
};
@@ -66,6 +70,8 @@ enum tpm_algorithms {
enum tpm2_curves {
TPM2_ECC_NONE = 0x0000,
TPM2_ECC_NIST_P256 = 0x0003,
+ TPM2_ECC_NIST_P384 = 0x0004,
+ TPM2_ECC_NIST_P521 = 0x0005,
};
struct tpm_digest {
@@ -242,6 +248,7 @@ enum tpm2_structures {
TPM2_ST_NO_SESSIONS = 0x8001,
TPM2_ST_SESSIONS = 0x8002,
TPM2_ST_CREATION = 0x8021,
+ TPM2_ST_HASHCHECK = 0x8024,
};
/* Indicates from what layer of the software stack the error comes from */
@@ -276,12 +283,15 @@ enum tpm2_command_codes {
TPM2_CC_NV_READ = 0x014E,
TPM2_CC_CREATE = 0x0153,
TPM2_CC_LOAD = 0x0157,
+ TPM2_CC_RSA_DECRYPT = 0x0159,
TPM2_CC_SEQUENCE_UPDATE = 0x015C,
+ TPM2_CC_SIGN = 0x015D,
TPM2_CC_UNSEAL = 0x015E,
TPM2_CC_CONTEXT_LOAD = 0x0161,
TPM2_CC_CONTEXT_SAVE = 0x0162,
TPM2_CC_FLUSH_CONTEXT = 0x0165,
TPM2_CC_READ_PUBLIC = 0x0173,
+ TPM2_CC_RSA_ENCRYPT = 0x0174,
TPM2_CC_START_AUTH_SESS = 0x0176,
TPM2_CC_VERIFY_SIGNATURE = 0x0177,
TPM2_CC_GET_CAPABILITY = 0x017A,
--
2.47.3
^ permalink raw reply related
* [PATCH v8 2/3] crypto: Migrate TPMKey ASN.1 objects from trusted-keys
From: Jarkko Sakkinen @ 2026-05-24 5:15 UTC (permalink / raw)
To: keyrings
Cc: David Howells, linux-crypto, linux-integrity, David Woodhouse,
James Bottomley, Stefan Berger, Herbert Xu, Jarkko Sakkinen,
David S. Miller, James Bottomley, Mimi Zohar, Paul Moore,
James Morris, Serge E. Hallyn, open list,
open list:SECURITY SUBSYSTEM
In-Reply-To: <20260524051519.3708075-1-jarkko@kernel.org>
Migrate the TPMKey ASN.1 code from trusted-keys to the crypto subsystem,
and put the code behind CRYPTO_TPM2_KEY Kconfig flag.
Signed-off-by: Jarkko Sakkinen <jarkko@kernel.org>
---
crypto/Kconfig | 7 +
crypto/Makefile | 6 +
crypto/tpm2_key.asn1 | 11 ++
crypto/tpm2_key.c | 150 ++++++++++++++++++++++
include/crypto/tpm2_key.h | 46 +++++++
security/keys/trusted-keys/Kconfig | 2 +-
security/keys/trusted-keys/Makefile | 2 -
security/keys/trusted-keys/tpm2key.asn1 | 11 --
security/keys/trusted-keys/trusted_tpm2.c | 119 ++---------------
9 files changed, 232 insertions(+), 122 deletions(-)
create mode 100644 crypto/tpm2_key.asn1
create mode 100644 crypto/tpm2_key.c
create mode 100644 include/crypto/tpm2_key.h
delete mode 100644 security/keys/trusted-keys/tpm2key.asn1
diff --git a/crypto/Kconfig b/crypto/Kconfig
index 103d1f58cb7c..5476d80372a1 100644
--- a/crypto/Kconfig
+++ b/crypto/Kconfig
@@ -3,6 +3,13 @@
# Generic algorithms support
#
+config CRYPTO_TPM2_KEY
+ bool
+ depends on CRYPTO
+ select ASN1
+ select OID_REGISTRY
+ default n
+
#
# async_tx api: hardware offloaded memory transfer/transform support
#
diff --git a/crypto/Makefile b/crypto/Makefile
index 162242593c7c..e232f9b9bee6 100644
--- a/crypto/Makefile
+++ b/crypto/Makefile
@@ -206,3 +206,9 @@ obj-$(CONFIG_CRYPTO_KDF800108_CTR) += kdf_sp800108.o
obj-$(CONFIG_CRYPTO_DF80090A) += df_sp80090a.o
obj-$(CONFIG_CRYPTO_KRB5) += krb5/
+
+ifdef CONFIG_CRYPTO_TPM2_KEY
+$(obj)/tpm2_key.asn1.o: $(obj)/tpm2_key.asn1.h $(obj)/tpm2_key.asn1.c
+$(obj)/tpm2_key.o: $(obj)/tpm2_key.asn1.h
+obj-y += tpm2_key.o tpm2_key.asn1.o
+endif
diff --git a/crypto/tpm2_key.asn1 b/crypto/tpm2_key.asn1
new file mode 100644
index 000000000000..553bf996af59
--- /dev/null
+++ b/crypto/tpm2_key.asn1
@@ -0,0 +1,11 @@
+---
+--- ASN.1 for TPM 2.0 keys
+---
+
+TPMKey ::= SEQUENCE {
+ type OBJECT IDENTIFIER ({tpm2_key_get_type}),
+ emptyAuth [0] EXPLICIT BOOLEAN OPTIONAL ({tpm2_key_get_empty_auth}),
+ parent INTEGER ({tpm2_key_get_parent}),
+ pubkey OCTET STRING ({tpm2_get_public}),
+ privkey OCTET STRING ({tpm2_get_private})
+ }
diff --git a/crypto/tpm2_key.c b/crypto/tpm2_key.c
new file mode 100644
index 000000000000..5704ccdb7c0d
--- /dev/null
+++ b/crypto/tpm2_key.c
@@ -0,0 +1,150 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+#include <crypto/tpm2_key.h>
+#include <linux/oid_registry.h>
+#include <linux/slab.h>
+#include <linux/types.h>
+#include <linux/unaligned.h>
+#include "tpm2_key.asn1.h"
+
+#undef pr_fmt
+#define pr_fmt(fmt) "tpm2_key: "fmt
+
+struct tpm2_key_decoder_context {
+ u32 parent;
+ const u8 *pub;
+ u32 pub_len;
+ const u8 *priv;
+ u32 priv_len;
+ enum OID oid;
+ bool empty_auth;
+};
+
+int tpm2_key_get_parent(void *context, size_t hdrlen,
+ unsigned char tag,
+ const void *value, size_t vlen)
+{
+ struct tpm2_key_decoder_context *decoder = context;
+ const u8 *v = value;
+ int i;
+
+ decoder->parent = 0;
+ for (i = 0; i < vlen; i++) {
+ decoder->parent <<= 8;
+ decoder->parent |= v[i];
+ }
+
+ return 0;
+}
+
+int tpm2_key_get_type(void *context, size_t hdrlen,
+ unsigned char tag,
+ const void *value, size_t vlen)
+{
+ struct tpm2_key_decoder_context *decoder = context;
+
+ decoder->oid = look_up_OID(value, vlen);
+ return 0;
+}
+
+int tpm2_key_get_empty_auth(void *context, size_t hdrlen,
+ unsigned char tag,
+ const void *value, size_t vlen)
+{
+ struct tpm2_key_decoder_context *decoder = context;
+ const u8 *bool_value = value;
+
+ if (!value || vlen != 1)
+ return -EBADMSG;
+
+ decoder->empty_auth = bool_value[0] != 0;
+ return 0;
+}
+
+static inline bool tpm2_key_is_valid(const void *value, size_t vlen)
+{
+ if (vlen < 2 || vlen > TPM2_KEY_BYTES_MAX)
+ return false;
+
+ if (get_unaligned_be16(value) != vlen - 2)
+ return false;
+
+ return true;
+}
+
+int tpm2_get_public(void *context, size_t hdrlen, unsigned char tag,
+ const void *value, size_t vlen)
+{
+ struct tpm2_key_decoder_context *decoder = context;
+
+ if (!tpm2_key_is_valid(value, vlen))
+ return -EBADMSG;
+
+ if (sizeof(struct tpm2_key_desc) > vlen - 2)
+ return -EBADMSG;
+
+ decoder->pub = value;
+ decoder->pub_len = vlen;
+ return 0;
+}
+
+int tpm2_get_private(void *context, size_t hdrlen, unsigned char tag,
+ const void *value, size_t vlen)
+{
+ struct tpm2_key_decoder_context *decoder = context;
+
+ if (!tpm2_key_is_valid(value, vlen))
+ return -EBADMSG;
+
+ decoder->priv = value;
+ decoder->priv_len = vlen;
+ return 0;
+}
+
+/**
+ * tpm2_key_decode() - Decode TPM2 ASN.1 key
+ * @src: ASN.1 source.
+ * @src_len: ASN.1 source length.
+ *
+ * Decodes the TPM2 ASN.1 key and validates that the public key data has all
+ * the shared fields of TPMT_PUBLIC. This is full coverage of the memory that
+ * can be validated before doing any key type specific validation.
+ *
+ * Return:
+ * - TPM2 ASN.1 key on success.
+ * - -EBADMSG when decoding fails.
+ * - -ENOMEM when OOM while allocating struct tpm2_key.
+ */
+struct tpm2_key *tpm2_key_decode(const u8 *src, u32 src_len)
+{
+ struct tpm2_key_decoder_context decoder;
+ struct tpm2_key *key;
+ u8 *data;
+ int ret;
+
+ memset(&decoder, 0, sizeof(decoder));
+ ret = asn1_ber_decoder(&tpm2_key_decoder, &decoder, src, src_len);
+ if (ret < 0) {
+ if (ret != -EBADMSG)
+ pr_info("Decoder error %d\n", ret);
+
+ return ERR_PTR(-EBADMSG);
+ }
+
+ key = kzalloc(sizeof(*key), GFP_KERNEL);
+ if (!key)
+ return ERR_PTR(-ENOMEM);
+
+ data = &key->data[0];
+ memcpy(&data[0], decoder.priv, decoder.priv_len);
+ memcpy(&data[decoder.priv_len], decoder.pub, decoder.pub_len);
+
+ key->oid = decoder.oid;
+ key->priv_len = decoder.priv_len;
+ key->pub_len = decoder.pub_len;
+ key->parent = decoder.parent;
+ key->desc = (struct tpm2_key_desc *)&data[decoder.priv_len + 2];
+ key->empty_auth = decoder.empty_auth;
+ return key;
+}
+EXPORT_SYMBOL_GPL(tpm2_key_decode);
diff --git a/include/crypto/tpm2_key.h b/include/crypto/tpm2_key.h
new file mode 100644
index 000000000000..883afaa596e5
--- /dev/null
+++ b/include/crypto/tpm2_key.h
@@ -0,0 +1,46 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef __LINUX_TPM2_KEY_H__
+#define __LINUX_TPM2_KEY_H__
+
+#include <linux/oid_registry.h>
+#include <linux/slab.h>
+
+#define TPM2_KEY_BYTES_MAX 1024
+
+/* TPM2 Structures 12.2.4: TPMT_PUBLIC */
+struct tpm2_key_desc {
+ __be16 type;
+ __be16 name_alg;
+ __be32 object_attributes;
+ __be16 policy_size;
+} __packed;
+
+/* Decoded TPM2 ASN.1 key. */
+struct tpm2_key {
+ u8 data[2 * TPM2_KEY_BYTES_MAX];
+ struct tpm2_key_desc *desc;
+ u16 priv_len;
+ u16 pub_len;
+ u32 parent;
+ enum OID oid;
+ bool empty_auth;
+};
+
+struct tpm2_key *tpm2_key_decode(const u8 *src, u32 src_len);
+
+static inline const void *tpm2_key_data(const struct tpm2_key *key)
+{
+ return &key->data[0];
+}
+
+static inline u16 tpm2_key_type(const struct tpm2_key *key)
+{
+ return be16_to_cpu(key->desc->type);
+}
+
+static inline int tpm2_key_policy_size(const struct tpm2_key *key)
+{
+ return be16_to_cpu(key->desc->policy_size);
+}
+
+#endif /* __LINUX_TPM2_KEY_H__ */
diff --git a/security/keys/trusted-keys/Kconfig b/security/keys/trusted-keys/Kconfig
index e5a4a53aeab2..09b1ec1d5bc2 100644
--- a/security/keys/trusted-keys/Kconfig
+++ b/security/keys/trusted-keys/Kconfig
@@ -27,9 +27,9 @@ config TRUSTED_KEYS_TPM
select CRYPTO_HASH_INFO
select CRYPTO_LIB_SHA1
select CRYPTO_LIB_UTILS
+ select CRYPTO_TPM2_KEY
select ASN1_ENCODER
select OID_REGISTRY
- select ASN1
select HAVE_TRUSTED_KEYS
help
Enable use of the Trusted Platform Module (TPM) as trusted key
diff --git a/security/keys/trusted-keys/Makefile b/security/keys/trusted-keys/Makefile
index 5fc053a21dad..ac09d2d90051 100644
--- a/security/keys/trusted-keys/Makefile
+++ b/security/keys/trusted-keys/Makefile
@@ -7,9 +7,7 @@ obj-$(CONFIG_TRUSTED_KEYS) += trusted.o
trusted-y += trusted_core.o
trusted-$(CONFIG_TRUSTED_KEYS_TPM) += trusted_tpm1.o
-$(obj)/trusted_tpm2.o: $(obj)/tpm2key.asn1.h
trusted-$(CONFIG_TRUSTED_KEYS_TPM) += trusted_tpm2.o
-trusted-$(CONFIG_TRUSTED_KEYS_TPM) += tpm2key.asn1.o
trusted-$(CONFIG_TRUSTED_KEYS_TEE) += trusted_tee.o
diff --git a/security/keys/trusted-keys/tpm2key.asn1 b/security/keys/trusted-keys/tpm2key.asn1
deleted file mode 100644
index f57f869ad600..000000000000
--- a/security/keys/trusted-keys/tpm2key.asn1
+++ /dev/null
@@ -1,11 +0,0 @@
----
---- ASN.1 for TPM 2.0 keys
----
-
-TPMKey ::= SEQUENCE {
- type OBJECT IDENTIFIER ({tpm2_key_type}),
- emptyAuth [0] EXPLICIT BOOLEAN OPTIONAL,
- parent INTEGER ({tpm2_key_parent}),
- pubkey OCTET STRING ({tpm2_key_pub}),
- privkey OCTET STRING ({tpm2_key_priv})
- }
diff --git a/security/keys/trusted-keys/trusted_tpm2.c b/security/keys/trusted-keys/trusted_tpm2.c
index 6340823f8b53..5b079fe476d1 100644
--- a/security/keys/trusted-keys/trusted_tpm2.c
+++ b/security/keys/trusted-keys/trusted_tpm2.c
@@ -13,11 +13,10 @@
#include <keys/trusted-type.h>
#include <keys/trusted_tpm.h>
+#include <crypto/tpm2_key.h>
#include <linux/unaligned.h>
-#include "tpm2key.asn1.h"
-
static u32 tpm2key_oid[] = { 2, 23, 133, 10, 1, 5 };
static int tpm2_key_encode(struct trusted_key_payload *payload,
@@ -90,105 +89,6 @@ static int tpm2_key_encode(struct trusted_key_payload *payload,
return ret;
}
-struct tpm2_key_context {
- u32 parent;
- const u8 *pub;
- u32 pub_len;
- const u8 *priv;
- u32 priv_len;
-};
-
-static int tpm2_key_decode(struct trusted_key_payload *payload,
- struct trusted_key_options *options,
- u8 **buf)
-{
- int ret;
- struct tpm2_key_context ctx;
- u8 *blob;
-
- memset(&ctx, 0, sizeof(ctx));
-
- ret = asn1_ber_decoder(&tpm2key_decoder, &ctx, payload->blob,
- payload->blob_len);
- if (ret < 0)
- return ret;
-
- if (ctx.priv_len + ctx.pub_len > MAX_BLOB_SIZE)
- return -EINVAL;
-
- blob = kmalloc(ctx.priv_len + ctx.pub_len + 4, GFP_KERNEL);
- if (!blob)
- return -ENOMEM;
-
- *buf = blob;
- options->keyhandle = ctx.parent;
-
- memcpy(blob, ctx.priv, ctx.priv_len);
- blob += ctx.priv_len;
-
- memcpy(blob, ctx.pub, ctx.pub_len);
-
- return 0;
-}
-
-int tpm2_key_parent(void *context, size_t hdrlen,
- unsigned char tag,
- const void *value, size_t vlen)
-{
- struct tpm2_key_context *ctx = context;
- const u8 *v = value;
- int i;
-
- ctx->parent = 0;
- for (i = 0; i < vlen; i++) {
- ctx->parent <<= 8;
- ctx->parent |= v[i];
- }
-
- return 0;
-}
-
-int tpm2_key_type(void *context, size_t hdrlen,
- unsigned char tag,
- const void *value, size_t vlen)
-{
- enum OID oid = look_up_OID(value, vlen);
-
- if (oid != OID_TPMSealedData) {
- char buffer[50];
-
- sprint_oid(value, vlen, buffer, sizeof(buffer));
- pr_debug("OID is \"%s\" which is not TPMSealedData\n",
- buffer);
- return -EINVAL;
- }
-
- return 0;
-}
-
-int tpm2_key_pub(void *context, size_t hdrlen,
- unsigned char tag,
- const void *value, size_t vlen)
-{
- struct tpm2_key_context *ctx = context;
-
- ctx->pub = value;
- ctx->pub_len = vlen;
-
- return 0;
-}
-
-int tpm2_key_priv(void *context, size_t hdrlen,
- unsigned char tag,
- const void *value, size_t vlen)
-{
- struct tpm2_key_context *ctx = context;
-
- ctx->priv = value;
- ctx->priv_len = vlen;
-
- return 0;
-}
/**
* tpm2_buf_append_auth() - append TPMS_AUTH_COMMAND to the buffer.
@@ -372,23 +272,26 @@ static int tpm2_load_cmd(struct tpm_chip *chip,
struct trusted_key_options *options,
u32 *blob_handle)
{
- u8 *blob_ref __free(kfree) = NULL;
+ struct tpm2_key *key __free(kfree) = NULL;
struct tpm_buf buf;
unsigned int private_len;
unsigned int public_len;
unsigned int blob_len;
- u8 *blob, *pub;
+ const u8 *blob, *pub;
int rc;
u32 attrs;
- rc = tpm2_key_decode(payload, options, &blob);
- if (rc) {
+ key = tpm2_key_decode(payload->blob, payload->blob_len);
+ if (IS_ERR(key))
+ key = NULL;
+
+ if (key && key->oid == OID_TPMSealedData) {
+ options->keyhandle = key->parent;
+ blob = tpm2_key_data(key);
+ } else {
/* old form */
blob = payload->blob;
payload->old_format = 1;
- } else {
- /* Bind for cleanup: */
- blob_ref = blob;
}
/* new format carries keyhandle but old format doesn't */
--
2.47.3
^ permalink raw reply related
* [PATCH v8 1/3] lib/asn1_encoder: Add asn1_encode_integer_bytes()
From: Jarkko Sakkinen @ 2026-05-24 5:15 UTC (permalink / raw)
To: keyrings
Cc: David Howells, linux-crypto, linux-integrity, David Woodhouse,
James Bottomley, Stefan Berger, Herbert Xu, Jarkko Sakkinen,
Andrew Morton, James Bottomley, Mimi Zohar, Paul Moore,
James Morris, Serge E. Hallyn, open list,
open list:SECURITY SUBSYSTEM
In-Reply-To: <20260524051519.3708075-1-jarkko@kernel.org>
Add a helper encoding a positive integer from a byte array in big-endian
format.
Signed-off-by: Jarkko Sakkinen <jarkko@kernel.org>
---
include/linux/asn1_encoder.h | 3 ++
lib/asn1_encoder.c | 62 ++++++++++++++++++++++++++++++++++++
2 files changed, 65 insertions(+)
diff --git a/include/linux/asn1_encoder.h b/include/linux/asn1_encoder.h
index d17484dffb74..e206bd425854 100644
--- a/include/linux/asn1_encoder.h
+++ b/include/linux/asn1_encoder.h
@@ -12,6 +12,9 @@ unsigned char *
asn1_encode_integer(unsigned char *data, const unsigned char *end_data,
s64 integer);
unsigned char *
+asn1_encode_integer_bytes(unsigned char *data, const unsigned char *end_data,
+ const unsigned char *integer, u32 integer_len);
+unsigned char *
asn1_encode_oid(unsigned char *data, const unsigned char *end_data,
u32 oid[], int oid_len);
unsigned char *
diff --git a/lib/asn1_encoder.c b/lib/asn1_encoder.c
index 92f35aae13b1..22e0acd6fe08 100644
--- a/lib/asn1_encoder.c
+++ b/lib/asn1_encoder.c
@@ -10,6 +10,8 @@
#include <linux/string.h>
#include <linux/module.h>
+static int asn1_encode_length(unsigned char **data, int *data_len, int len);
+
/**
* asn1_encode_integer() - encode positive integer to ASN.1
* @data: pointer to the pointer to the data
@@ -85,6 +87,66 @@ asn1_encode_integer(unsigned char *data, const unsigned char *end_data,
}
EXPORT_SYMBOL_GPL(asn1_encode_integer);
+/**
+ * asn1_encode_integer_bytes() - encode positive integer bytes to ASN.1
+ * @data: pointer to the pointer to the data
+ * @end_data: end of data pointer, points one beyond last usable byte in @data
+ * @bytes: integer bytes
+ * @bytes_len: amount of bytes
+ *
+ * Encode a positive integer from a byte array in big-endian format. Strip
+ * leading zeros.
+ */
+unsigned char *
+asn1_encode_integer_bytes(unsigned char *data, const unsigned char *end_data,
+ const unsigned char *bytes, u32 bytes_len)
+{
+ static const unsigned char zero;
+ int data_len = end_data - data;
+ bool add_pad = false;
+ int ret;
+
+ if (IS_ERR(data))
+ return data;
+
+ if (!bytes || !bytes_len)
+ return ERR_PTR(-EINVAL);
+
+ /* Strip leading zeros: */
+ while (bytes_len > 1 && bytes[0] == 0) {
+ bytes++;
+ bytes_len--;
+ }
+
+ if (!bytes_len) {
+ bytes = &zero;
+ bytes_len = 1;
+ } else {
+ add_pad = bytes[0] & 0x80;
+ }
+
+ if (data_len < 2)
+ return ERR_PTR(-EINVAL);
+
+ *(data++) = _tag(UNIV, PRIM, INT);
+ data_len--;
+
+ ret = asn1_encode_length(&data, &data_len, bytes_len + add_pad);
+ if (ret)
+ return ERR_PTR(ret);
+
+ if (data_len < bytes_len + add_pad)
+ return ERR_PTR(-EINVAL);
+
+ if (add_pad)
+ *(data++) = 0;
+
+ memcpy(data, bytes, bytes_len);
+ data += bytes_len;
+ return data;
+}
+EXPORT_SYMBOL_GPL(asn1_encode_integer_bytes);
+
/* calculate the base 128 digit values setting the top bit of the first octet */
static int asn1_encode_oid_digit(unsigned char **_data, int *data_len, u32 oid)
{
--
2.47.3
^ permalink raw reply related
* [PATCH v8 0/3]
From: Jarkko Sakkinen @ 2026-05-24 5:15 UTC (permalink / raw)
To: keyrings
Cc: David Howells, linux-crypto, linux-integrity, David Woodhouse,
James Bottomley, Stefan Berger, Herbert Xu, Jarkko Sakkinen,
James Bottomley, Mimi Zohar, Paul Moore, James Morris,
Serge E. Hallyn, open list:SECURITY SUBSYSTEM, open list
This series introduces key type for operating with asymmetric keys using
a TPM2 chip.
Change Log
==========
v8:
- Reset patch change logs given the overhaul of the code and patches.
- Have only single new subkey type.
- Make key type only use TPM operations.
- Use TPM2_Sign for both ECC and RSA keys.
- Align key descriptions with other key types.
Previous versions
=================
* v7: https://lore.kernel.org/linux-integrity/20240528210823.28798-1-jarkko@kernel.org/
* v6: https://lore.kernel.org/linux-integrity/20240528035136.11464-1-jarkko@kernel.org/
* v5: https://lore.kernel.org/linux-integrity/20240523212515.4875-1-jarkko@kernel.org/
* v4: https://lore.kernel.org/linux-integrity/20240522005252.17841-1-jarkko@kernel.org/
* v3: https://lore.kernel.org/linux-integrity/20240521152659.26438-1-jarkko@kernel.org/
* v2: https://lore.kernel.org/linux-integrity/336755.1716327854@warthog.procyon.org.uk/
* v1: https://lore.kernel.org/linux-integrity/20240520184727.22038-1-jarkko@kernel.org/
* Derived from https://lore.kernel.org/all/20200518172704.29608-1-prestwoj@gmail.com/
Jarkko Sakkinen (3):
lib/asn1_encoder: Add asn1_encode_integer_bytes()
crypto: Migrate TPMKey ASN.1 objects from trusted-keys
keys: asymmetric: tpm2_asymmetric
crypto/Kconfig | 7 +
crypto/Makefile | 6 +
crypto/asymmetric_keys/Kconfig | 17 +
crypto/asymmetric_keys/Makefile | 1 +
crypto/asymmetric_keys/tpm2_asymmetric.c | 1096 +++++++++++++++++++++
crypto/tpm2_key.asn1 | 11 +
crypto/tpm2_key.c | 150 +++
include/crypto/tpm2_key.h | 46 +
include/linux/asn1_encoder.h | 3 +
include/linux/tpm.h | 10 +
lib/asn1_encoder.c | 62 ++
security/keys/trusted-keys/Kconfig | 2 +-
security/keys/trusted-keys/Makefile | 2 -
security/keys/trusted-keys/tpm2key.asn1 | 11 -
security/keys/trusted-keys/trusted_tpm2.c | 119 +--
15 files changed, 1421 insertions(+), 122 deletions(-)
create mode 100644 crypto/asymmetric_keys/tpm2_asymmetric.c
create mode 100644 crypto/tpm2_key.asn1
create mode 100644 crypto/tpm2_key.c
create mode 100644 include/crypto/tpm2_key.h
delete mode 100644 security/keys/trusted-keys/tpm2key.asn1
--
2.47.3
^ permalink raw reply
* [PATCH net v2 4/4] netlabel: validate CIPSO option against skb tail in netlbl_skbuff_getattr
From: Qi Tang @ 2026-05-24 4:14 UTC (permalink / raw)
To: davem, kuba, pabeni, edumazet
Cc: netdev, fw, lyutoon, stable, Qi Tang, Paul Moore, Simon Horman,
linux-security-module
In-Reply-To: <20260524041442.2432071-1-tpluszz77@gmail.com>
netlbl_skbuff_getattr() locates the CIPSO option in the IPv4 IP header
via cipso_v4_optptr() and hands the bare pointer to cipso_v4_getattr().
The consumer re-reads cipso[1] (option length), cipso[6] (tag type),
and then cipso_v4_parsetag_*() re-reads further bytes from the skb.
__ip_options_compile() validates these bytes only at parse time. An
nftables LOCAL_IN payload write reachable from an unprivileged user
namespace can rewrite them after parse and before the SELinux/Smack
peer-label consume path (selinux_sock_rcv_skb_compat ->
selinux_netlbl_sock_rcv_skb -> netlbl_skbuff_getattr). This is the
IPv4 analogue of the CALIPSO IPv6 trust-after-modification fixed in
the previous patch: the tag parsers walk the option using attacker-
controlled length bytes, producing slab-out-of-bounds reads whose
contents feed into the MLS access decision.
Validate the option fits within skb_tail_pointer(skb) before invoking
cipso_v4_getattr(). The pre-tag-walk guard "ptr + 8 > tail" covers
the CIPSO option header (type + length + DOI = 6 bytes) plus the
first tag header (type + length = 2 bytes), which are the bytes
cipso_v4_getattr() reads to dispatch on the tag. When the bounds
check fails the packet has been mutated after parse, so return
-EINVAL rather than fall through to the unlabeled path.
Runtime confirmation (Smack peer-label policy + nft LOCAL_IN
mutation of tag_len): UdpInDatagrams increments to 1 and recvfrom
returns the payload, showing netlbl_skbuff_getattr ->
cipso_v4_getattr -> cipso_v4_parsetag_rbm -> netlbl_bitmap_walk runs
end-to-end past the option's true bound; with this patch the
consume path returns -EINVAL at the bounds check and the counter
stays 0.
Cc: stable@vger.kernel.org
Reported-by: Qi Tang <tpluszz77@gmail.com>
Reported-by: Tong Liu <lyutoon@gmail.com>
Fixes: 04f81f0154e4 ("cipso: don't use IPCB() to locate the CIPSO IP option")
Signed-off-by: Qi Tang <tpluszz77@gmail.com>
---
net/netlabel/netlabel_kapi.c | 17 +++++++++++++++--
1 file changed, 15 insertions(+), 2 deletions(-)
diff --git a/net/netlabel/netlabel_kapi.c b/net/netlabel/netlabel_kapi.c
index d0d6220b8d59d..c2d3ea751f4e1 100644
--- a/net/netlabel/netlabel_kapi.c
+++ b/net/netlabel/netlabel_kapi.c
@@ -1393,11 +1393,24 @@ int netlbl_skbuff_getattr(const struct sk_buff *skb,
unsigned char *ptr;
switch (family) {
- case AF_INET:
+ case AF_INET: {
+ const unsigned char *tail = skb_tail_pointer(skb);
+ u8 opt_len, tag_len;
+
ptr = cipso_v4_optptr(skb);
- if (ptr && cipso_v4_getattr(ptr, secattr) == 0)
+ if (!ptr)
+ break;
+ /* CIPSO header (type+len+DOI = 6) + first tag header (type+len = 2) */
+ if (ptr + 8 > tail)
+ return -EINVAL;
+ opt_len = ptr[1]; /* total CIPSO option length */
+ tag_len = ptr[7]; /* first tag length */
+ if (ptr + opt_len > tail || ptr + 6 + tag_len > tail)
+ return -EINVAL;
+ if (cipso_v4_getattr(ptr, secattr) == 0)
return 0;
break;
+ }
#if IS_ENABLED(CONFIG_IPV6)
case AF_INET6: {
const unsigned char *tail = skb_tail_pointer(skb);
--
2.47.3
^ permalink raw reply related
* [PATCH net v2 3/4] netlabel: validate CALIPSO option against skb tail in netlbl_skbuff_getattr
From: Qi Tang @ 2026-05-24 4:14 UTC (permalink / raw)
To: davem, kuba, pabeni, edumazet
Cc: netdev, fw, lyutoon, stable, Qi Tang, Paul Moore, Simon Horman,
Huw Davies, linux-security-module
In-Reply-To: <20260524041442.2432071-1-tpluszz77@gmail.com>
netlbl_skbuff_getattr() locates the CALIPSO option in the IPv6 HBH
header via calipso_optptr() and hands the bare pointer to
calipso_getattr() -> calipso_opt_getattr(). The consumer re-reads
calipso[1] (option data length) and calipso[6] (cat_len/4) and walks
calipso + 10 for cat_len bytes via netlbl_bitmap_walk().
ipv6_hop_calipso() validates these bytes only at parse time inside
ipv6_parse_hopopts(). An nftables PRE_ROUTING payload write reachable
from an unprivileged user namespace can rewrite both bytes between
parse and the SELinux peer-label consume path
(selinux_sock_rcv_skb_compat -> selinux_netlbl_sock_rcv_skb ->
netlbl_skbuff_getattr). The self-consistency check
(cat_len + 8 > len) inside calipso_opt_getattr() is defeated by
mutating both bytes consistently, allowing a ~232-byte
slab-out-of-bounds read from calipso + 10 whose set bits become MLS
categories driving the access decision.
netlbl_skbuff_getattr() has the skb; gate the consume on the option
fitting within skb_tail_pointer(). The IPv6 option layout is
type(1) + length(1) + length bytes of data, so requiring
ptr + 2 + ptr[1] <= skb_tail covers the option and its embedded
bitmap. When the bounds check fails the packet has been mutated
after parse, so return -EINVAL rather than fall through to the
unlabeled path.
Runtime confirmation (SELinux compat path with selinux=1 enforcing=0
and a CALIPSO DOI added via netlabelctl): Udp6InDatagrams increments
to 1 with the mutated cat_len, showing
selinux_socket_sock_rcv_skb -> netlbl_skbuff_getattr ->
calipso_opt_getattr -> netlbl_bitmap_walk runs end-to-end past the
option's true bound; with this patch the consume path returns
-EINVAL at the bounds check and the counter stays 0.
Cc: stable@vger.kernel.org
Reported-by: Qi Tang <tpluszz77@gmail.com>
Reported-by: Tong Liu <lyutoon@gmail.com>
Fixes: 2917f57b6bc1 ("calipso: Allow the lsm to label the skbuff directly.")
Signed-off-by: Qi Tang <tpluszz77@gmail.com>
---
net/netlabel/netlabel_kapi.c | 15 +++++++++++++--
1 file changed, 13 insertions(+), 2 deletions(-)
diff --git a/net/netlabel/netlabel_kapi.c b/net/netlabel/netlabel_kapi.c
index 3583fa63dd01f..d0d6220b8d59d 100644
--- a/net/netlabel/netlabel_kapi.c
+++ b/net/netlabel/netlabel_kapi.c
@@ -1399,11 +1399,22 @@ int netlbl_skbuff_getattr(const struct sk_buff *skb,
return 0;
break;
#if IS_ENABLED(CONFIG_IPV6)
- case AF_INET6:
+ case AF_INET6: {
+ const unsigned char *tail = skb_tail_pointer(skb);
+ u8 opt_data_len;
+
ptr = calipso_optptr(skb);
- if (ptr && calipso_getattr(ptr, secattr) == 0)
+ if (!ptr)
+ break;
+ if (ptr + 2 > tail)
+ return -EINVAL;
+ opt_data_len = ptr[1]; /* IPv6 option data length */
+ if (ptr + 2 + opt_data_len > tail)
+ return -EINVAL;
+ if (calipso_getattr(ptr, secattr) == 0)
return 0;
break;
+ }
#endif /* IPv6 */
}
--
2.47.3
^ permalink raw reply related
* [PATCH net v2 0/4] net: trust-after-modification fixes for IPv4 options + netlabel
From: Qi Tang @ 2026-05-24 4:14 UTC (permalink / raw)
To: davem, kuba, pabeni, edumazet
Cc: netdev, fw, lyutoon, stable, Qi Tang, David Ahern, Ido Schimmel,
Simon Horman, Paul Moore, Casey Schaufler, Huw Davies,
linux-security-module
Four small bounds-check fixes for a recurring pattern in IPv4 options
and CIPSO/CALIPSO consumers. The parse-time validator stores only
the option offset into IPCB / skb metadata. Later consumers (cmsg
echo, mrouted report, netlabel getattr) re-read the length /
pointer / cat_len bytes from the skb body and use them for indexed
memcpy or bitmap walk. An nftables payload mutation reachable from
an unprivileged user namespace (CAP_NET_ADMIN inside the namespace)
rewrites those bytes between parse and consume.
1/4 __ip_options_echo() 40-byte stack OOB write
(KASAN: stack-out-of-bounds,
Write of size 255).
2/4 ipmr_cache_report() Up to 40-byte OOB read of
skb head leaked into the
IGMPMSG cmsg delivered to
mrouted.
3/4 netlbl_skbuff_getattr() / CALIPSO ~232-byte slab OOB read
driving SELinux MLS
category bitmap.
4/4 netlbl_skbuff_getattr() / CIPSO Sibling of 3/4 on the
AF_INET (CIPSO IPv4) path.
Florian Westphal's [PATCH net 05/10] netfilter: disable payload
mangling in userns blocks the unprivileged-userns side of nft
payload-set at the source:
https://lore.kernel.org/netdev/20260522104257.2008-6-fw@strlen.de/
These four consumer-side bounds checks land in the same direction
as defense in depth, also covering root / CAP_NET_ADMIN nft
FORWARD payload mangling in the init userns and any non-nft
mutation path.
Changes v1 -> v2:
- 3/4 + 4/4 return -EINVAL on bounds-check failure instead of
falling through to netlbl_unlabel_getattr() (Paul Moore).
- 3/4 commit message drops the "Smack" mention from the CALIPSO
consume path; Smack does not currently consume CALIPSO (Casey
Schaufler).
- 4/4 inline comment explains the literal 8: CIPSO option header
(type+len+DOI = 6) plus first tag header (type+len = 2) (Paul
Moore).
- All four pick up Cc: stable@vger.kernel.org.
v1: https://lore.kernel.org/netdev/20260514165139.436961-1-tpluszz77@gmail.com/
Qi Tang (4):
ipv4: validate ip_options length in __ip_options_echo() against skb
tail
ipv4: ipmr: clamp ip_hdrlen against skb_headlen in ipmr_cache_report
netlabel: validate CALIPSO option against skb tail in
netlbl_skbuff_getattr
netlabel: validate CIPSO option against skb tail in
netlbl_skbuff_getattr
net/ipv4/ip_options.c | 8 ++++++++
net/ipv4/ipmr.c | 2 +-
net/netlabel/netlabel_kapi.c | 32 ++++++++++++++++++++++++++++----
3 files changed, 37 insertions(+), 5 deletions(-)
--
2.47.3
^ permalink raw reply
* Re: [PATCH v8 1/9] landlock: Add a place for flags to layer rules
From: Tingmao Wang @ 2026-05-24 1:29 UTC (permalink / raw)
To: Mickaël Salaün, Günther Noack
Cc: Justin Suess, Jan Kara, Abhinav Saxena, linux-security-module
In-Reply-To: <20260523.Uephughee8as@digikod.net>
On 5/23/26 21:48, Mickaël Salaün wrote:
> This patch doesn't build.
Missed a hunk in this patch (ended up in the next one), will add.
>> @@ -797,20 +803,28 @@ is_access_to_paths_allowed(const struct landlock_ruleset *const domain,
>> }
>>
>> if (unlikely(dentry_child1)) {
>> + /*
>> + * Get the layer masks for the child dentries for use by domain
>> + * check later. The rule_flags for child1 should have been
>> + * included in rule_flags_parent1 already (cf.
>> + * collect_domain_accesses), and is not relevant for domain check,
>> + * so we don't have to pass it to landlock_unmask_layers.
>> + */
>> if (landlock_init_layer_masks(domain, LANDLOCK_MASK_ACCESS_FS,
>> &_layer_masks_child1,
>> LANDLOCK_KEY_INODE))
>> landlock_unmask_layers(find_rule(domain, dentry_child1),
>> - &_layer_masks_child1);
>> + &_layer_masks_child1, NULL);
>> layer_masks_child1 = &_layer_masks_child1;
>> child1_is_directory = d_is_dir(dentry_child1);
>> }
>> if (unlikely(dentry_child2)) {
>> + /* See above comment for why NULL is passed as rule_flags_masks. */
>
> rule_flags_masks doesn't exist.
I guess I was probably referring to the rule_flags argument - will fix.
>> [...]
>> @@ -647,9 +648,14 @@ bool landlock_unmask_layers(const struct landlock_rule *const rule,
>> */
>> for (size_t i = 0; i < rule->num_layers; i++) {
>> const struct landlock_layer *const layer = &rule->layers[i];
>> + const layer_mask_t layer_bit = BIT_ULL(layer->level - 1);
>>
>
>> /* Clear the bits where the layer in the rule grants access. */
>> masks->access[layer->level - 1] &= ~layer->access;
>> +
>> + /* Collect rule flags for each layer. */
>> + if (rule_flags && layer->flags.quiet)
>> + rule_flags->quiet_masks |= layer_bit;
>
> Why not store the quiet bit in masks? That would not only be "access"
> bits anymore but it makes sense to store all this bits it the same
> place.
>
> We should then probably rename struct layer_access_masks to just struct
> layer_masks.
>
> We need to be careful to not increase too much the size of this struct
> though while keeping the [LANDLOCK_MAX_NUM_LAYERS] approach if possible
> (see Günther's commit that added it).
Most uses of struct layer_access_masks do not actually care about the rule
flags tho, e.g. in unmask_scoped_access, scope_to_request, or may_refer.
Such a rename would touch 31 places (and only a few of them would actually
touch the quiet flag).
If we want to refactor to make this be in the layer_access_masks (then
rename it), I guess there are 3 options, which do you prefer?
1. Add a u16 bitfield for which layers are quieted. Future rule flags
will be additional bitfields. struct layer_masks becomes 68 bytes (+4).
struct layer_masks {
access_mask_t access[LANDLOCK_MAX_NUM_LAYERS];
layer_mask_t quiet_layers;
};
2. Make the [LANDLOCK_MAX_NUM_LAYERS] array store both the access mask and
the quiet bit (or more bits for future rule flags). Size of struct stays
the same.
static_assert(LANDLOCK_NUM_ACCESS_NET <= LANDLOCK_NUM_ACCESS_FS);
static_assert(LANDLOCK_NUM_SCOPE <= LANDLOCK_NUM_ACCESS_FS);
struct layer_mask {
access_mask_t access:LANDLOCK_NUM_ACCESS_FS;
bool quiet:1;
};
struct layer_masks {
struct layer_mask layer[LANDLOCK_MAX_NUM_LAYERS];
};
(Maybe we can just make struct layer_masks a typedef to
layer_mask[LANDLOCK_MAX_NUM_LAYERS] instead? But currently not sure if
there are any gotchas with a typedef like that)
3. Mirror layer_access_masks::access[] - add a
rule_flags[LANDLOCK_MAX_NUM_LAYERS] too. struct layer_masks becomes 80
bytes (+16).
struct rule_flags {
bool quiet:1;
};
struct layer_masks {
/**
* @access: The unfulfilled access rights for each layer.
*/
access_mask_t access[LANDLOCK_MAX_NUM_LAYERS];
struct rule_flags rule_flags[LANDLOCK_MAX_NUM_LAYERS];
};
(3 seems very wasteful to me)
^ permalink raw reply
* Re: [PATCH bpf-next 09/13] lsm: add bpf_prog_load_post_integrity hook
From: Paul Moore @ 2026-05-24 0:55 UTC (permalink / raw)
To: KP Singh
Cc: linux-security-module, bpf, ast, daniel, memxor, James.Bottomley,
bboscaccy, Fan Wu
In-Reply-To: <20260522023234.3778588-10-kpsingh@kernel.org>
On Thu, May 21, 2026 at 10:32 PM KP Singh <kpsingh@kernel.org> wrote:
>
> Add a companion to security_bpf_prog_load. The existing hook fires
> at PROG_LOAD entry where the verdict is at most BPF_SIG_OK; the new
> hook fires from bpf_loader_verify_metadata after the in-kernel
> metadata check, just before sig.verdict is promoted to
> BPF_SIG_METADATA_VERIFIED. Policy LSMs that want to gate on
> metadata verification (not just signature presence) register here.
>
> Signed-off-by: KP Singh <kpsingh@kernel.org>
> ---
> include/linux/lsm_hook_defs.h | 1 +
> include/linux/security.h | 6 ++++++
> security/security.c | 17 +++++++++++++++++
> 3 files changed, 24 insertions(+)
...
> +/**
> + * security_bpf_prog_load_post_integrity() - Notify LSMs that a signed loader
> + * has just verified its metadata map.
> + * @prog: the loader BPF program whose metadata check passed.
> + *
> + * Invoked by bpf_loader_verify_metadata() after the kernel-side hash check
> + * succeeds, before prog->aux->sig_verdict is promoted to
> + * BPF_SIG_METADATA_VERIFIED. A non-zero return aborts the kfunc and leaves
> + * the verdict at BPF_SIG_OK.
> + *
> + * Return: 0 on success, negative errno to deny.
> + */
> +int security_bpf_prog_load_post_integrity(struct bpf_prog *prog)
> +{
> + return call_int_hook(bpf_prog_load_post_integrity, prog);
> +}
Since you're using essentially the same LSM infrastructure and IPE
work that Blaise, Fan, and I developed for policy-based enforcement of
BPF signature verification, perhaps this is where we can find some
common ground to start working together once again.
I would be happy to support and maintain a
security_bpf_prog_load_post_integrity() kfunc wrapper as part of the
LSM framework, similar to what the VFS folks do with
fs/bpf_fs_kfuncs.c, so that either a lskel loader, or a BPF LSM
program if you like, could register a BPF integrity verification
verdict with the LSM. This would provide a single unified approach
for LSMs, including BPF LSMs, to build their BPF program integrity
controls regardless of what the system builder, or admin, chooses for
a BPF signature verification scheme: the loader based scheme you
developed, or Hornet.
There is no technical reason we can't support these things, e.g.
multiple coexisting verification schemes supported by a single LSM
enforcement interface, we just need to be willing to accept that we
have different needs and show a willingness to accept different
solutions as a result.
--
paul-moore.com
^ permalink raw reply
* Re: [RFC PATCH] ipe: support multiple BPF integrity verification LSMs
From: Fan Wu @ 2026-05-24 0:39 UTC (permalink / raw)
To: Paul Moore; +Cc: linux-security-module, wufan, bboscaccy
In-Reply-To: <20260523200859.13527-2-paul@paul-moore.com>
On Sat, May 23, 2026 at 1:09 PM Paul Moore <paul@paul-moore.com> wrote:
>
> Currently IPE always records the last BPF integrity verification verdict,
> which is reasonable with only a single BPF verification LSM, but it
> becomes problematic when multiple mechanisms end up submitting BPF
> program integrity verdicts.
>
> This patch updates IPE to record all of the received BPF program
> integrity verdicts, along with their associated LSM IDs, ultimately using
> the "worst" verdict in the policy enforcement engine. Policy support for
> selecting individual integrity verdicts was intentionally omitted from
> this patch to keep things simple both from a code and policy developer
> perspective, however future work to add selector support should be
> trivial.
>
> Signed-off-by: Paul Moore <paul@paul-moore.com>
> ---
I would say the current code is fine because there is only one provider.
The verdicts for different LSMs may have different semantics,
therefore I would suggest the second verdict provider should extend
the policy to provide a rule property like "LSM=hornet" to
differentiate the integrity provider. However this will need a major
parser and policy validation refactoring with a proper documentation
of the policy semantics and use case examples.
-Fan
^ permalink raw reply
* Re: [RFC PATCH] ipe: support multiple BPF integrity verification LSMs
From: Paul Moore @ 2026-05-23 23:05 UTC (permalink / raw)
To: linux-security-module; +Cc: wufan, bboscaccy
In-Reply-To: <20260523200859.13527-2-paul@paul-moore.com>
On May 23, 2026 3:09:05 PM Paul Moore <paul@paul-moore.com> wrote:
> Currently IPE always records the last BPF integrity verification verdict,
> which is reasonable with only a single BPF verification LSM, but it
> becomes problematic when multiple mechanisms end up submitting BPF
> program integrity verdicts.
>
> This patch updates IPE to record all of the received BPF program
> integrity verdicts, along with their associated LSM IDs, ultimately using
> the "worst" verdict in the policy enforcement engine. Policy support for
> selecting individual integrity verdicts was intentionally omitted from
> this patch to keep things simple both from a code and policy developer
> perspective, however future work to add selector support should be
> trivial.
>
> Signed-off-by: Paul Moore <paul@paul-moore.com>
> ---
> include/linux/security.h | 14 +++++++-------
> security/ipe/eval.h | 7 ++++++-
> security/ipe/hooks.c | 20 +++++++++++++++++---
> 3 files changed, 30 insertions(+), 11 deletions(-)
>
> diff --git a/include/linux/security.h b/include/linux/security.h
> index 598cd2eb1dcd..6a987a0347a0 100644
> --- a/include/linux/security.h
> +++ b/include/linux/security.h
> @@ -103,13 +103,13 @@ enum lsm_integrity_type {
>
> enum lsm_integrity_verdict {
> LSM_INT_VERDICT_NONE = 0,
> - LSM_INT_VERDICT_OK,
> - LSM_INT_VERDICT_UNSIGNED,
> - LSM_INT_VERDICT_PARTIALSIG,
> - LSM_INT_VERDICT_UNKNOWNKEY,
> - LSM_INT_VERDICT_UNEXPECTED,
> - LSM_INT_VERDICT_FAULT,
> - LSM_INT_VERDICT_BADSIG,
> + LSM_INT_VERDICT_OK = 1,
> + LSM_INT_VERDICT_UNSIGNED = 2,
> + LSM_INT_VERDICT_PARTIALSIG = 3,
> + LSM_INT_VERDICT_UNKNOWNKEY = 4,
> + LSM_INT_VERDICT_UNEXPECTED = 5,
> + LSM_INT_VERDICT_FAULT = 6,
> + LSM_INT_VERDICT_BADSIG = 7,
> };
>
> /*
> diff --git a/security/ipe/eval.h b/security/ipe/eval.h
> index b061cb5ade27..90c7b66b9ca8 100644
> --- a/security/ipe/eval.h
> +++ b/security/ipe/eval.h
> @@ -8,6 +8,7 @@
>
> #include <linux/file.h>
> #include <linux/types.h>
> +#include <linux/lsm_count.h>
>
> #include "policy.h"
> #include "hooks.h"
> @@ -39,7 +40,11 @@ struct ipe_inode {
>
> #ifdef CONFIG_IPE_PROP_BPF_SIGNATURE
> struct ipe_bpf_prog {
> - enum lsm_integrity_verdict verdict;
> + struct {
> + const struct lsm_id *lsmid;
> + enum lsm_integrity_verdict verdict;
> + } verdicts[MAX_LSM_COUNT];
> + unsigned int count;
> };
> #endif /* CONFIG_IPE_PROP_BPF_SIGNATURE */
>
> diff --git a/security/ipe/hooks.c b/security/ipe/hooks.c
> index 9271e129a2cf..143bb2ae2b12 100644
> --- a/security/ipe/hooks.c
> +++ b/security/ipe/hooks.c
> @@ -9,6 +9,7 @@
> #include <linux/binfmts.h>
> #include <linux/mman.h>
> #include <linux/blk_types.h>
> +#include <linux/lsm_count.h>
>
> #include "ipe.h"
> #include "hooks.h"
> @@ -355,7 +356,8 @@ int ipe_inode_setintegrity(const struct inode *inode,
> * so that ipe_bpf_prog_load() can later read it for policy evaluation.
> *
> * Return:
> - * * %0 - Always succeeds (policy is evaluated in bpf_prog_load)
> + * * %0 - Recorded the verdict (policy is evaluated in bpf_prog_load)
> + * * %-ENOMEM - Exhausted room for recording verdicts
> */
> int ipe_bpf_prog_load_post_integrity(struct bpf_prog *prog,
> union bpf_attr *attr,
> @@ -365,8 +367,14 @@ int ipe_bpf_prog_load_post_integrity(struct bpf_prog
> *prog,
> enum lsm_integrity_verdict verdict)
> {
> struct ipe_bpf_prog *blob = ipe_bpf_prog(prog);
> + unsigned int count = blob->count;
>
> - blob->verdict = verdict;
> + if (count == MAX_LSM_COUNT)
> + return -ENOMEM;
> +
> + blob->verdicts[count].lsmid = lsmid;
> + blob->verdicts[count].verdict = verdict;
> + blob->count++;
>
> return 0;
> }
> @@ -391,12 +399,18 @@ int ipe_bpf_prog_load(struct bpf_prog *prog,
> struct bpf_token *token,
> bool kernel)
> {
> + unsigned int iter;
> struct ipe_bpf_prog *blob = ipe_bpf_prog(prog);
> struct ipe_eval_ctx ctx = IPE_EVAL_CTX_INIT;
>
> ctx.op = IPE_OP_BPF_PROG_LOAD;
> ctx.hook = IPE_HOOK_BPF_PROG_LOAD;
> - ctx.bpf_verdict = blob->verdict;
> + ctx.bpf_verdict = LSM_INT_VERDICT_NONE;
> + for (iter = 0; iter < blob->count; iter++) {
> + /* pick the "wosrt" verdict */
That should obviously be "worst" :)
> + if (blob->verdicts[iter].verdict > ctx.bpf_verdict)
> + ctx.bpf_verdict = blob->verdicts[iter].verdict;
> + }
> ctx.bpf_keyring_id = attr->keyring_id;
> ctx.bpf_kernel = kernel;
>
> --
> 2.54.0
--
paul-moore.com
^ permalink raw reply
* Re: [PATCH v8 1/9] landlock: Add a place for flags to layer rules
From: Mickaël Salaün @ 2026-05-23 20:48 UTC (permalink / raw)
To: Tingmao Wang, Günther Noack
Cc: Justin Suess, Jan Kara, Abhinav Saxena, linux-security-module
In-Reply-To: <6f418b431ccf88636ea3a1b930d14bfdcd420233.1775490344.git.m@maowtm.org>
This patch doesn't build.
On Mon, Apr 06, 2026 at 04:52:14PM +0100, Tingmao Wang wrote:
> To avoid unnecessarily increasing the size of struct landlock_layer, we
> make the layer level a u8 and use the space to store the flags struct.
>
> Cc: Justin Suess <utilityemal77@gmail.com>
> Signed-off-by: Tingmao Wang <m@maowtm.org>
> Co-developed-by: Justin Suess <utilityemal77@gmail.com>
> Signed-off-by: Justin Suess <utilityemal77@gmail.com>
> ---
>
> Because the no inherit patchset [2] from Justin Suess will depend on
> this rule flags mechanism, I and Justin discussed a bit whether this
> patch should in fact be a standalone thing separate from quiet flags
> (i.e. add the infrastructure for rule flags, but don't actually add any
> rule flags in this commit), so that the two series can be developed and
> merged independently. However in the end I decided to not do this and
> send this patch as-is.
>
> Changes in v8:
> - Rebase on top of mic/next
> - Add Co-developed-by: Justin Suess for handling this rebase initially
> - layer_mask_t was removed in [1] but we still need it for the
> collected_rule_flags. Rather than using raw u16, I've chosen to
> re-define it back in ruleset.h (it was in access.h).
>
> Changes in v7:
> - Take rule_flags separately from landlock_request in
> is_access_to_paths_allowed to avoid writing to the landlock_request
> variable if CONFIG_AUDIT is disabled (to enable compiler elision).
> - Due to the above change, we don't need rule_flags in landlock_request in
> this commit anymore (will be added later).
>
> Changes in v6:
> - Rebased to include the revised disconnected directory handling changes
> (without the "reverting" behaviour)
>
> Changes in v5:
> - Move rule_flags into landlock_request. This lets us get rid of the
> extra parameters to is_access_to_paths_allowed (and later on,
> landlock_log_denial), and thus less code changes.
>
> Changes in v3:
> - Comment changes, move local variables, simplify if branch
>
> Changes in v2:
> - Comment changes
> - Rebased to include disconnected directory handling changes on mic/next
> and add backing up of collected_rule_flags.
>
> [1]: https://lore.kernel.org/all/20260125195853.109967-1-gnoack3000@gmail.com/
> [2]: https://lore.kernel.org/all/20251221194301.247484-1-utilityemal77@gmail.com/
>
> security/landlock/fs.c | 96 +++++++++++++++++++++++--------------
> security/landlock/net.c | 3 +-
> security/landlock/ruleset.c | 8 +++-
> security/landlock/ruleset.h | 32 ++++++++++++-
> 4 files changed, 99 insertions(+), 40 deletions(-)
>
> diff --git a/security/landlock/fs.c b/security/landlock/fs.c
> index c1ecfe239032..6f63e0182ef0 100644
> --- a/security/landlock/fs.c
> +++ b/security/landlock/fs.c
> @@ -717,6 +717,9 @@ static void test_is_eacces_with_write(struct kunit *const test)
> * those identified by @access_request_parent1). This matrix can
> * initially refer to domain layer masks and, when the accesses for the
> * destination and source are the same, to requested layer masks.
> + * @rule_flags_parent1: Pointer to a collected_rule_flags struct
> + * corresponding to the accumulated rule flags for parent1 to be read from
> + * and filled as we traverse the path.
> * @log_request_parent1: Audit request to fill if the related access is denied.
> * @dentry_child1: Dentry to the initial child of the parent1 path. This
> * pointer must be NULL for non-refer actions (i.e. not link nor rename).
> @@ -726,6 +729,7 @@ static void test_is_eacces_with_write(struct kunit *const test)
> * the source. Must be set to 0 when using a simple path request.
> * @layer_masks_parent2: Similar to @layer_masks_parent1 but for a refer
> * action. This must be NULL otherwise.
> + * @rule_flags_parent2: Similar to @rule_flags_parent1 but for parent2.
> * @log_request_parent2: Audit request to fill if the related access is denied.
> * @dentry_child2: Dentry to the initial child of the parent2 path. This
> * pointer is only set for RENAME_EXCHANGE actions and must be NULL
> @@ -739,17 +743,19 @@ static void test_is_eacces_with_write(struct kunit *const test)
> *
> * Return: True if the access request is granted, false otherwise.
> */
> -static bool
> -is_access_to_paths_allowed(const struct landlock_ruleset *const domain,
> - const struct path *const path,
> - const access_mask_t access_request_parent1,
> - struct layer_access_masks *layer_masks_parent1,
> - struct landlock_request *const log_request_parent1,
> - struct dentry *const dentry_child1,
> - const access_mask_t access_request_parent2,
> - struct layer_access_masks *layer_masks_parent2,
> - struct landlock_request *const log_request_parent2,
> - struct dentry *const dentry_child2)
> +static bool is_access_to_paths_allowed(
> + const struct landlock_ruleset *const domain,
> + const struct path *const path,
> + const access_mask_t access_request_parent1,
> + struct layer_access_masks *layer_masks_parent1,
> + struct collected_rule_flags *const rule_flags_parent1,
> + struct landlock_request *const log_request_parent1,
> + struct dentry *const dentry_child1,
> + const access_mask_t access_request_parent2,
> + struct layer_access_masks *layer_masks_parent2,
> + struct collected_rule_flags *const rule_flags_parent2,
> + struct landlock_request *const log_request_parent2,
> + struct dentry *const dentry_child2)
> {
> bool allowed_parent1 = false, allowed_parent2 = false, is_dom_check,
> child1_is_directory = true, child2_is_directory = true;
> @@ -797,20 +803,28 @@ is_access_to_paths_allowed(const struct landlock_ruleset *const domain,
> }
>
> if (unlikely(dentry_child1)) {
> + /*
> + * Get the layer masks for the child dentries for use by domain
> + * check later. The rule_flags for child1 should have been
> + * included in rule_flags_parent1 already (cf.
> + * collect_domain_accesses), and is not relevant for domain check,
> + * so we don't have to pass it to landlock_unmask_layers.
> + */
> if (landlock_init_layer_masks(domain, LANDLOCK_MASK_ACCESS_FS,
> &_layer_masks_child1,
> LANDLOCK_KEY_INODE))
> landlock_unmask_layers(find_rule(domain, dentry_child1),
> - &_layer_masks_child1);
> + &_layer_masks_child1, NULL);
> layer_masks_child1 = &_layer_masks_child1;
> child1_is_directory = d_is_dir(dentry_child1);
> }
> if (unlikely(dentry_child2)) {
> + /* See above comment for why NULL is passed as rule_flags_masks. */
rule_flags_masks doesn't exist.
> if (landlock_init_layer_masks(domain, LANDLOCK_MASK_ACCESS_FS,
> &_layer_masks_child2,
> LANDLOCK_KEY_INODE))
> landlock_unmask_layers(find_rule(domain, dentry_child2),
> - &_layer_masks_child2);
> + &_layer_masks_child2, NULL);
> layer_masks_child2 = &_layer_masks_child2;
> child2_is_directory = d_is_dir(dentry_child2);
> }
> @@ -865,12 +879,14 @@ is_access_to_paths_allowed(const struct landlock_ruleset *const domain,
> }
>
> rule = find_rule(domain, walker_path.dentry);
> - allowed_parent1 =
> - allowed_parent1 ||
> - landlock_unmask_layers(rule, layer_masks_parent1);
> - allowed_parent2 =
> - allowed_parent2 ||
> - landlock_unmask_layers(rule, layer_masks_parent2);
> + allowed_parent1 = allowed_parent1 ||
> + landlock_unmask_layers(rule,
> + layer_masks_parent1,
> + rule_flags_parent1);
> + allowed_parent2 = allowed_parent2 ||
> + landlock_unmask_layers(rule,
> + layer_masks_parent2,
> + rule_flags_parent2);
>
> /* Stops when a rule from each layer grants access. */
> if (allowed_parent1 && allowed_parent2)
> @@ -954,6 +970,7 @@ static int current_check_access_path(const struct path *const path,
> landlock_get_applicable_subject(current_cred(), masks, NULL);
> struct layer_access_masks layer_masks;
> struct landlock_request request = {};
> + struct collected_rule_flags rule_flags = {};
>
> if (!subject)
> return 0;
> @@ -962,8 +979,8 @@ static int current_check_access_path(const struct path *const path,
> access_request, &layer_masks,
> LANDLOCK_KEY_INODE);
> if (is_access_to_paths_allowed(subject->domain, path, access_request,
> - &layer_masks, &request, NULL, 0, NULL,
> - NULL, NULL))
> + &layer_masks, &rule_flags, &request,
> + NULL, 0, NULL, NULL, NULL, NULL))
> return 0;
>
> landlock_log_denial(subject, &request);
> @@ -1026,10 +1043,11 @@ static access_mask_t maybe_remove(const struct dentry *const dentry)
> * Return: True if all the domain access rights are allowed for @dir, false if
> * the walk reached @mnt_root.
> */
> -static bool collect_domain_accesses(const struct landlock_ruleset *const domain,
> - const struct dentry *const mnt_root,
> - struct dentry *dir,
> - struct layer_access_masks *layer_masks_dom)
> +static bool
> +collect_domain_accesses(const struct landlock_ruleset *const domain,
> + const struct dentry *const mnt_root, struct dentry *dir,
> + struct layer_access_masks *layer_masks_dom,
> + struct collected_rule_flags *const rule_flags)
> {
> bool ret = false;
>
> @@ -1048,7 +1066,7 @@ static bool collect_domain_accesses(const struct landlock_ruleset *const domain,
>
> /* Gets all layers allowing all domain accesses. */
> if (landlock_unmask_layers(find_rule(domain, dir),
> - layer_masks_dom)) {
> + layer_masks_dom, rule_flags)) {
> /*
> * Stops when all handled accesses are allowed by at
> * least one rule in each layer.
> @@ -1138,6 +1156,8 @@ static int current_check_refer_path(struct dentry *const old_dentry,
> struct layer_access_masks layer_masks_parent1 = {},
> layer_masks_parent2 = {};
> struct landlock_request request1 = {}, request2 = {};
> + struct collected_rule_flags rule_flags_parent1 = {},
> + rule_flags_parent2 = {};
>
> if (!subject)
> return 0;
> @@ -1169,10 +1189,10 @@ static int current_check_refer_path(struct dentry *const old_dentry,
> subject->domain,
> access_request_parent1 | access_request_parent2,
> &layer_masks_parent1, LANDLOCK_KEY_INODE);
> - if (is_access_to_paths_allowed(subject->domain, new_dir,
> - access_request_parent1,
> - &layer_masks_parent1, &request1,
> - NULL, 0, NULL, NULL, NULL))
> + if (is_access_to_paths_allowed(
> + subject->domain, new_dir, access_request_parent1,
> + &layer_masks_parent1, &rule_flags_parent1,
> + &request1, NULL, 0, NULL, NULL, NULL, NULL))
> return 0;
>
> landlock_log_denial(subject, &request1);
> @@ -1198,11 +1218,12 @@ static int current_check_refer_path(struct dentry *const old_dentry,
> /* new_dir->dentry is equal to new_dentry->d_parent */
> allow_parent1 = collect_domain_accesses(subject->domain, mnt_dir.dentry,
> old_parent,
> - &layer_masks_parent1);
> + &layer_masks_parent1,
> + &rule_flags_parent1);
> allow_parent2 = collect_domain_accesses(subject->domain, mnt_dir.dentry,
> new_dir->dentry,
> - &layer_masks_parent2);
> -
> + &layer_masks_parent2,
> + &rule_flags_parent2);
> if (allow_parent1 && allow_parent2)
> return 0;
>
> @@ -1214,8 +1235,9 @@ static int current_check_refer_path(struct dentry *const old_dentry,
> */
> if (is_access_to_paths_allowed(
> subject->domain, &mnt_dir, access_request_parent1,
> - &layer_masks_parent1, &request1, old_dentry,
> - access_request_parent2, &layer_masks_parent2, &request2,
> + &layer_masks_parent1, &rule_flags_parent1, &request1,
> + old_dentry, access_request_parent2, &layer_masks_parent2,
> + &rule_flags_parent2, &request2,
> exchange ? new_dentry : NULL))
> return 0;
>
> @@ -1745,6 +1767,7 @@ static int hook_file_open(struct file *const file)
> const struct landlock_cred_security *const subject =
> landlock_get_applicable_subject(file->f_cred, any_fs, NULL);
> struct landlock_request request = {};
> + struct collected_rule_flags rule_flags = {};
>
> if (!subject)
> return 0;
> @@ -1771,7 +1794,8 @@ static int hook_file_open(struct file *const file)
> landlock_init_layer_masks(subject->domain,
> full_access_request, &layer_masks,
> LANDLOCK_KEY_INODE),
> - &layer_masks, &request, NULL, 0, NULL, NULL, NULL)) {
> + &layer_masks, &rule_flags, &request, NULL, 0, NULL, NULL,
> + NULL, NULL)) {
> allowed_access = full_access_request;
> } else {
> /*
> diff --git a/security/landlock/net.c b/security/landlock/net.c
> index c368649985c5..dc82ce4a2bd4 100644
> --- a/security/landlock/net.c
> +++ b/security/landlock/net.c
> @@ -48,6 +48,7 @@ static int current_check_access_socket(struct socket *const sock,
> {
> __be16 port;
> struct layer_access_masks layer_masks = {};
> + struct collected_rule_flags rule_flags = {};
> const struct landlock_rule *rule;
> struct landlock_id id = {
> .type = LANDLOCK_KEY_NET_PORT,
> @@ -194,7 +195,7 @@ static int current_check_access_socket(struct socket *const sock,
> if (!access_request)
> return 0;
>
> - if (landlock_unmask_layers(rule, &layer_masks))
> + if (landlock_unmask_layers(rule, &layer_masks, &rule_flags))
> return 0;
>
> audit_net.family = address->sa_family;
> diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
> index 181df7736bb9..e4e6b730b581 100644
> --- a/security/landlock/ruleset.c
> +++ b/security/landlock/ruleset.c
> @@ -628,7 +628,8 @@ landlock_find_rule(const struct landlock_ruleset *const ruleset,
> * remaining unfulfilled access rights and masks has no leftover set bits).
> */
> bool landlock_unmask_layers(const struct landlock_rule *const rule,
> - struct layer_access_masks *masks)
> + struct layer_access_masks *masks,
> + struct collected_rule_flags *const rule_flags)
> {
> if (!masks)
> return true;
> @@ -647,9 +648,14 @@ bool landlock_unmask_layers(const struct landlock_rule *const rule,
> */
> for (size_t i = 0; i < rule->num_layers; i++) {
> const struct landlock_layer *const layer = &rule->layers[i];
> + const layer_mask_t layer_bit = BIT_ULL(layer->level - 1);
>
> /* Clear the bits where the layer in the rule grants access. */
> masks->access[layer->level - 1] &= ~layer->access;
> +
> + /* Collect rule flags for each layer. */
> + if (rule_flags && layer->flags.quiet)
> + rule_flags->quiet_masks |= layer_bit;
Why not store the quiet bit in masks? That would not only be "access"
bits anymore but it makes sense to store all this bits it the same
place.
We should then probably rename struct layer_access_masks to just struct
layer_masks.
We need to be careful to not increase too much the size of this struct
though while keeping the [LANDLOCK_MAX_NUM_LAYERS] approach if possible
(see Günther's commit that added it).
> }
>
> for (size_t i = 0; i < ARRAY_SIZE(masks->access); i++) {
> diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h
> index 889f4b30301a..3b31552f0c95 100644
> --- a/security/landlock/ruleset.h
> +++ b/security/landlock/ruleset.h
> @@ -29,7 +29,18 @@ struct landlock_layer {
> /**
> * @level: Position of this layer in the layer stack. Starts from 1.
> */
> - u16 level;
> + u8 level;
> + /**
> + * @flags: Bitfield for special flags attached to this rule.
> + */
> + struct {
> + /**
> + * @quiet: Suppresses denial audit logs for the object covered by
> + * this rule in this domain. For filesystem rules, this inherits
> + * down the file hierarchy.
> + */
> + bool quiet:1;
> + } flags;
> /**
> * @access: Bitfield of allowed actions on the kernel object. They are
> * relative to the object type (e.g. %LANDLOCK_ACTION_FS_READ).
> @@ -37,6 +48,22 @@ struct landlock_layer {
> access_mask_t access;
> };
>
> +typedef u16 layer_mask_t;
> +
> +/* Makes sure this is enough to include all layers. */
> +static_assert(BITS_PER_TYPE(layer_mask_t) >= LANDLOCK_MAX_NUM_LAYERS);
> +
> +
Two new lines.
> +/**
> + * struct collected_rule_flags - Hold accumulated flags for each layer.
> + */
> +struct collected_rule_flags {
> + /**
> + * @quiet_masks: Layers for which the quiet flag is effective.
> + */
> + layer_mask_t quiet_masks;
> +};
> +
> /**
> * union landlock_key - Key of a ruleset's red-black tree
> */
> @@ -302,7 +329,8 @@ landlock_get_scope_mask(const struct landlock_ruleset *const ruleset,
> }
>
> bool landlock_unmask_layers(const struct landlock_rule *const rule,
> - struct layer_access_masks *masks);
> + struct layer_access_masks *masks,
> + struct collected_rule_flags *const rule_flags);
>
> access_mask_t
> landlock_init_layer_masks(const struct landlock_ruleset *const domain,
> --
> 2.53.0
>
^ permalink raw reply
* [RFC PATCH] ipe: support multiple BPF integrity verification LSMs
From: Paul Moore @ 2026-05-23 20:09 UTC (permalink / raw)
To: linux-security-module; +Cc: wufan, bboscaccy
Currently IPE always records the last BPF integrity verification verdict,
which is reasonable with only a single BPF verification LSM, but it
becomes problematic when multiple mechanisms end up submitting BPF
program integrity verdicts.
This patch updates IPE to record all of the received BPF program
integrity verdicts, along with their associated LSM IDs, ultimately using
the "worst" verdict in the policy enforcement engine. Policy support for
selecting individual integrity verdicts was intentionally omitted from
this patch to keep things simple both from a code and policy developer
perspective, however future work to add selector support should be
trivial.
Signed-off-by: Paul Moore <paul@paul-moore.com>
---
include/linux/security.h | 14 +++++++-------
security/ipe/eval.h | 7 ++++++-
security/ipe/hooks.c | 20 +++++++++++++++++---
3 files changed, 30 insertions(+), 11 deletions(-)
diff --git a/include/linux/security.h b/include/linux/security.h
index 598cd2eb1dcd..6a987a0347a0 100644
--- a/include/linux/security.h
+++ b/include/linux/security.h
@@ -103,13 +103,13 @@ enum lsm_integrity_type {
enum lsm_integrity_verdict {
LSM_INT_VERDICT_NONE = 0,
- LSM_INT_VERDICT_OK,
- LSM_INT_VERDICT_UNSIGNED,
- LSM_INT_VERDICT_PARTIALSIG,
- LSM_INT_VERDICT_UNKNOWNKEY,
- LSM_INT_VERDICT_UNEXPECTED,
- LSM_INT_VERDICT_FAULT,
- LSM_INT_VERDICT_BADSIG,
+ LSM_INT_VERDICT_OK = 1,
+ LSM_INT_VERDICT_UNSIGNED = 2,
+ LSM_INT_VERDICT_PARTIALSIG = 3,
+ LSM_INT_VERDICT_UNKNOWNKEY = 4,
+ LSM_INT_VERDICT_UNEXPECTED = 5,
+ LSM_INT_VERDICT_FAULT = 6,
+ LSM_INT_VERDICT_BADSIG = 7,
};
/*
diff --git a/security/ipe/eval.h b/security/ipe/eval.h
index b061cb5ade27..90c7b66b9ca8 100644
--- a/security/ipe/eval.h
+++ b/security/ipe/eval.h
@@ -8,6 +8,7 @@
#include <linux/file.h>
#include <linux/types.h>
+#include <linux/lsm_count.h>
#include "policy.h"
#include "hooks.h"
@@ -39,7 +40,11 @@ struct ipe_inode {
#ifdef CONFIG_IPE_PROP_BPF_SIGNATURE
struct ipe_bpf_prog {
- enum lsm_integrity_verdict verdict;
+ struct {
+ const struct lsm_id *lsmid;
+ enum lsm_integrity_verdict verdict;
+ } verdicts[MAX_LSM_COUNT];
+ unsigned int count;
};
#endif /* CONFIG_IPE_PROP_BPF_SIGNATURE */
diff --git a/security/ipe/hooks.c b/security/ipe/hooks.c
index 9271e129a2cf..143bb2ae2b12 100644
--- a/security/ipe/hooks.c
+++ b/security/ipe/hooks.c
@@ -9,6 +9,7 @@
#include <linux/binfmts.h>
#include <linux/mman.h>
#include <linux/blk_types.h>
+#include <linux/lsm_count.h>
#include "ipe.h"
#include "hooks.h"
@@ -355,7 +356,8 @@ int ipe_inode_setintegrity(const struct inode *inode,
* so that ipe_bpf_prog_load() can later read it for policy evaluation.
*
* Return:
- * * %0 - Always succeeds (policy is evaluated in bpf_prog_load)
+ * * %0 - Recorded the verdict (policy is evaluated in bpf_prog_load)
+ * * %-ENOMEM - Exhausted room for recording verdicts
*/
int ipe_bpf_prog_load_post_integrity(struct bpf_prog *prog,
union bpf_attr *attr,
@@ -365,8 +367,14 @@ int ipe_bpf_prog_load_post_integrity(struct bpf_prog *prog,
enum lsm_integrity_verdict verdict)
{
struct ipe_bpf_prog *blob = ipe_bpf_prog(prog);
+ unsigned int count = blob->count;
- blob->verdict = verdict;
+ if (count == MAX_LSM_COUNT)
+ return -ENOMEM;
+
+ blob->verdicts[count].lsmid = lsmid;
+ blob->verdicts[count].verdict = verdict;
+ blob->count++;
return 0;
}
@@ -391,12 +399,18 @@ int ipe_bpf_prog_load(struct bpf_prog *prog,
struct bpf_token *token,
bool kernel)
{
+ unsigned int iter;
struct ipe_bpf_prog *blob = ipe_bpf_prog(prog);
struct ipe_eval_ctx ctx = IPE_EVAL_CTX_INIT;
ctx.op = IPE_OP_BPF_PROG_LOAD;
ctx.hook = IPE_HOOK_BPF_PROG_LOAD;
- ctx.bpf_verdict = blob->verdict;
+ ctx.bpf_verdict = LSM_INT_VERDICT_NONE;
+ for (iter = 0; iter < blob->count; iter++) {
+ /* pick the "wosrt" verdict */
+ if (blob->verdicts[iter].verdict > ctx.bpf_verdict)
+ ctx.bpf_verdict = blob->verdicts[iter].verdict;
+ }
ctx.bpf_keyring_id = attr->keyring_id;
ctx.bpf_kernel = kernel;
--
2.54.0
^ permalink raw reply related
* Re: [PATCH] lsm,bpf: fix security_bpf_prog_load() error handling
From: bot+bpf-ci @ 2026-05-23 17:44 UTC (permalink / raw)
To: paul, bpf, linux-security-module
Cc: ast, andrii, daniel, martin.lau, eddyz87, yonghong.song, clm,
ihor.solodrai
In-Reply-To: <20260523160025.16363-2-paul@paul-moore.com>
[-- Attachment #1: Type: text/plain, Size: 1302 bytes --]
> commit a03af62bb4a3613aadacc8cb75f0007d761e785a
> Author: Paul Moore <paul@paul-moore.com>
> Date: Sat May 23 12:00:26 2026 -0400
>
> lsm,bpf: fix security_bpf_prog_load() error handling
>
> If security_bpf_prog_load() fails there is no need to call into
> security_bpf_prog_free() as the LSM will handle the cleanup of any partial
> LSM state before returning to the caller with an error. Thankfully this
> isn't an issue with any of the existing code as the LSMs which currently
> provide BPF hook callback implementations don't allocate any internal
> state, but this is something we want to fix for potential future users.
>
> Cc: bpf@vger.kernel.org
> Cc: linux-security-module@vger.kernel.org
> Signed-off-by: Paul Moore <paul@paul-moore.com>
This looks like a bug fix for the error handling logic introduced in
commit b3beebbe6154 which imported the kernel/bpf/syscall.c file with
incorrect error handling for security_bpf_prog_load() failure. Should
this include:
Fixes: b3beebbe6154 ("adding ci files")
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/26337503379
^ permalink raw reply
* Re: [PATCH] lsm,bpf: fix security_bpf_prog_load() error handling
From: Alexei Starovoitov @ 2026-05-23 17:19 UTC (permalink / raw)
To: Paul Moore; +Cc: bpf, LSM List
In-Reply-To: <19e55c1c1f0.2843.85c95baa4474aabc7814e68940a78392@paul-moore.com>
On Sat, May 23, 2026 at 6:53 PM Paul Moore <paul@paul-moore.com> wrote:
>
> On May 23, 2026 11:25:55 AM Alexei Starovoitov
> <alexei.starovoitov@gmail.com> wrote:
> > On Sat, May 23, 2026 at 6:06 PM Paul Moore <paul@paul-moore.com> wrote:
> >> On Sat, May 23, 2026 at 12:00 PM Paul Moore <paul@paul-moore.com> wrote:
> >>>
> >>> If security_bpf_prog_load() fails there is no need to call into
> >>> security_bpf_prog_free() as the LSM will handle the cleanup of any partial
> >>> LSM state before returning to the caller with an error. Thankfully this
> >>> isn't an issue with any of the existing code as the LSMs which currently
> >>> provide BPF hook callback implementations don't allocate any internal
> >>> state, but this is something we want to fix for potential future users.
> >>>
> >>> Cc: bpf@vger.kernel.org
> >>> Cc: linux-security-module@vger.kernel.org
> >>> Signed-off-by: Paul Moore <paul@paul-moore.com>
> >>> ---
> >>> kernel/bpf/syscall.c | 4 +---
> >>> 1 file changed, 1 insertion(+), 3 deletions(-)
> >>
> >> Alexei, I'm assuming you would prefer to take this via the BPF tree?
> >
> > Paul, I see that you're intentionally trying to piss me off.
> > It's not going to work :)
>
> I promise you that is not the case. I was looking at the sashiko results of
> the latest Hornet patch and it identified this potential issue in the error
> handling code that is an issue independent of Hornet. I posted the quick
> little patch above to fix the issue, and since the diffstat is solely in
> kernel/bpf/syscall.c I figured you would want to merge it via the BPF tree;
> if that is not the case let me know.
It's in a queue. You can monitor it here:
https://patchwork.kernel.org/project/netdevbpf/list/?series=&submitter=&state=&q=&archive=&delegate=121173
But please be advised that when submitters ignore issues
found by bots and maintainers agree with bot findings
we mark patches as changes requested.
^ permalink raw reply
* Re: [PATCH bpf-next 06/13] bpf: resolve loader-style kfunc CALLs against prog BTF
From: Alexei Starovoitov @ 2026-05-23 17:01 UTC (permalink / raw)
To: KP Singh
Cc: LSM List, bpf, Alexei Starovoitov, Daniel Borkmann,
Kumar Kartikeya Dwivedi, James Bottomley, Paul Moore
In-Reply-To: <20260522023234.3778588-7-kpsingh@kernel.org>
On Fri, May 22, 2026 at 4:32 AM KP Singh <kpsingh@kernel.org> wrote:
>
> gen_loader-emitted signed loaders cannot bake vmlinux BTF ids into
> kfunc CALL imm at sign time. Add a new pseudo
> BPF_PSEUDO_KFUNC_CALL_PROG_BTF that gen_loader emits in src_reg, with
> imm holding the FUNC type id in the loader's own prog BTF.
>
> In add_subprog_and_kfunc, look the FUNC up by name in vmlinux BTF,
> patch imm with the resolved id, and rewrite src_reg back to
> BPF_PSEUDO_KFUNC_CALL so downstream passes see a normal kfunc CALL.
> Leave standard src_reg calls unchanged.
>
> Signed-off-by: KP Singh <kpsingh@kernel.org>
> ---
> include/linux/bpf_verifier.h | 6 ++++
> include/uapi/linux/bpf.h | 5 ++++
> kernel/bpf/verifier.c | 54 ++++++++++++++++++++++++++++++++--
> tools/include/uapi/linux/bpf.h | 5 ++++
> 4 files changed, 67 insertions(+), 3 deletions(-)
>
> diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h
> index 185b2aa43a42..396b85830996 100644
> --- a/include/linux/bpf_verifier.h
> +++ b/include/linux/bpf_verifier.h
> @@ -959,6 +959,12 @@ static inline bool bpf_pseudo_kfunc_call(const struct bpf_insn *insn)
> insn->src_reg == BPF_PSEUDO_KFUNC_CALL;
> }
>
> +static inline bool bpf_pseudo_kfunc_call_prog_btf(const struct bpf_insn *insn)
> +{
> + return insn->code == (BPF_JMP | BPF_CALL) &&
> + insn->src_reg == BPF_PSEUDO_KFUNC_CALL_PROG_BTF;
> +}
> +
> __printf(2, 0) void bpf_verifier_vlog(struct bpf_verifier_log *log,
> const char *fmt, va_list args);
> __printf(2, 3) void bpf_verifier_log_write(struct bpf_verifier_env *env,
> diff --git a/include/uapi/linux/bpf.h b/include/uapi/linux/bpf.h
> index 552bc5d9afbd..06056e714e8e 100644
> --- a/include/uapi/linux/bpf.h
> +++ b/include/uapi/linux/bpf.h
> @@ -1382,6 +1382,11 @@ enum {
> * bpf_call->imm == btf_id of a BTF_KIND_FUNC in the running kernel
> */
> #define BPF_PSEUDO_KFUNC_CALL 2
> +/* when bpf_call->src_reg == BPF_PSEUDO_KFUNC_CALL_PROG_BTF,
> + * bpf_call->imm == btf_id of a BTF_KIND_FUNC in the program's
> + * prog BTF. The verifier resolves it to a vmlinux btf_id by name.
> + */
> +#define BPF_PSEUDO_KFUNC_CALL_PROG_BTF 3
>
> enum bpf_addr_space_cast {
> BPF_ADDR_SPACE_CAST = 1,
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index f0e45cfa5b34..1b5d06b9d74a 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
> @@ -3088,6 +3088,47 @@ bool bpf_prog_has_kfunc_call(const struct bpf_prog *prog)
> return !!prog->aux->kfunc_tab;
> }
>
> +/*
> + * Resolve a gen_loader-emitted kfunc CALL by FUNC name in vmlinux BTF,
> + * then rewrite src_reg back to BPF_PSEUDO_KFUNC_CALL. Caller must have
> + * already filtered for BPF_PSEUDO_KFUNC_CALL_PROG_BTF.
> + */
> +static int resolve_loader_kfunc(struct bpf_verifier_env *env,
> + struct bpf_insn *insn, int insn_idx)
> +{
> + struct btf *prog_btf = env->prog->aux->btf;
> + const struct btf_type *t;
> + const char *name;
> + s32 vmlinux_id;
> +
> + if (!prog_btf || !btf_vmlinux || insn->off) {
> + verbose(env, "kfunc call insn %d: PROG_BTF resolution requires prog BTF and insn->off == 0\n",
> + insn_idx);
> + return -EINVAL;
> + }
> + t = btf_type_by_id(prog_btf, insn->imm);
> + if (!t || !btf_type_is_func(t)) {
> + verbose(env, "kfunc call insn %d: imm %d is not a FUNC in prog BTF\n",
> + insn_idx, insn->imm);
> + return -EINVAL;
> + }
> + name = btf_name_by_offset(prog_btf, t->name_off);
> + if (!name || !name[0]) {
> + verbose(env, "kfunc call insn %d: prog-BTF FUNC has no name\n",
> + insn_idx);
> + return -EINVAL;
> + }
> + vmlinux_id = btf_find_by_name_kind(btf_vmlinux, name, BTF_KIND_FUNC);
> + if (vmlinux_id < 0) {
> + verbose(env, "kfunc call insn %d: %s not found in vmlinux BTF\n",
> + insn_idx, name);
> + return vmlinux_id;
> + }
> + insn->imm = vmlinux_id;
> + insn->src_reg = BPF_PSEUDO_KFUNC_CALL;
> + return 0;
Ohh. So this patch does it.
Probably worth squashing the patches then?
Also since the resolution into kernel kfunc is by name only,
it feels odd to create a fake prog BTF proto just for the name.
What is an alternative?
Also note that we were discussing allowing the replacement of kfuncs
by modules with the same name. This kfunc should be
immutable?
^ permalink raw reply
* Re: [PATCH] lsm,bpf: fix security_bpf_prog_load() error handling
From: Paul Moore @ 2026-05-23 16:53 UTC (permalink / raw)
To: Alexei Starovoitov; +Cc: bpf, LSM List
In-Reply-To: <CAADnVQ+WTfgO636GqgxzJ6r=jENpG704kmNAsy-X8=WyjMOV+g@mail.gmail.com>
On May 23, 2026 11:25:55 AM Alexei Starovoitov
<alexei.starovoitov@gmail.com> wrote:
> On Sat, May 23, 2026 at 6:06 PM Paul Moore <paul@paul-moore.com> wrote:
>> On Sat, May 23, 2026 at 12:00 PM Paul Moore <paul@paul-moore.com> wrote:
>>>
>>> If security_bpf_prog_load() fails there is no need to call into
>>> security_bpf_prog_free() as the LSM will handle the cleanup of any partial
>>> LSM state before returning to the caller with an error. Thankfully this
>>> isn't an issue with any of the existing code as the LSMs which currently
>>> provide BPF hook callback implementations don't allocate any internal
>>> state, but this is something we want to fix for potential future users.
>>>
>>> Cc: bpf@vger.kernel.org
>>> Cc: linux-security-module@vger.kernel.org
>>> Signed-off-by: Paul Moore <paul@paul-moore.com>
>>> ---
>>> kernel/bpf/syscall.c | 4 +---
>>> 1 file changed, 1 insertion(+), 3 deletions(-)
>>
>> Alexei, I'm assuming you would prefer to take this via the BPF tree?
>
> Paul, I see that you're intentionally trying to piss me off.
> It's not going to work :)
I promise you that is not the case. I was looking at the sashiko results of
the latest Hornet patch and it identified this potential issue in the error
handling code that is an issue independent of Hornet. I posted the quick
little patch above to fix the issue, and since the diffstat is solely in
kernel/bpf/syscall.c I figured you would want to merge it via the BPF tree;
if that is not the case let me know.
--
paul-moore.com
^ permalink raw reply
* Re: [PATCH bpf-next 05/13] bpf: compute prog->digest at BPF_PROG_LOAD entry
From: Alexei Starovoitov @ 2026-05-23 16:49 UTC (permalink / raw)
To: KP Singh
Cc: LSM List, bpf, Alexei Starovoitov, Daniel Borkmann,
Kumar Kartikeya Dwivedi, James Bottomley, Paul Moore
In-Reply-To: <20260522023234.3778588-6-kpsingh@kernel.org>
On Fri, May 22, 2026 at 4:32 AM KP Singh <kpsingh@kernel.org> wrote:
>
> add_subprog_and_kfunc relocates kfunc CALLs by patching insn->imm
> and src_reg, and bpf_prog_calc_tag has no rule to mask kfunc CALL
> fields.
add_subprog_and_kfunc() doesn't modify the insn stream afaict.
Do you mean that bpf_check_btf_info() resolves CO-RE relos?
but loader prog doesn't use CO-RE.
I don't quite see the point of the patch.
^ permalink raw reply
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox