Linux Security Modules development
 help / color / mirror / Atom feed
* Re: [PATCH] keys: allow request-key path to be configured via Kconfig
From: Jarkko Sakkinen @ 2026-06-10 13:01 UTC (permalink / raw)
  To: Gary Guo
  Cc: David Howells, Paul Moore, James Morris, Serge E. Hallyn,
	keyrings, linux-security-module, linux-kernel
In-Reply-To: <ailfPUbn8gbUqB1D@kernel.org>

On Wed, Jun 10, 2026 at 03:57:37PM +0300, Jarkko Sakkinen wrote:
> On Mon, Jun 08, 2026 at 11:30:06AM +0100, Gary Guo wrote:
> > On Mon Jun 8, 2026 at 5:59 AM BST, Jarkko Sakkinen wrote:
> > > On Mon, Jun 08, 2026 at 07:50:03AM +0300, Jarkko Sakkinen wrote:
> > >> On Sun, Jun 07, 2026 at 02:49:27PM +0100, Gary Guo wrote:
> > >> > From: Gary Guo <gary@garyguo.net>
> > >> > 
> > >> > Some Linux distributions (e.g. NixOS) does not have /sbin present, and they
> > >> > currently carry patches to replace /sbin/request-key to some other path.
> > >> 
> > >> Sorry but no configuration for introducing API divergence.
> > 
> > What is the API divergence here? Distros can already patch the kernel or place a
> > different binary there, so I don't see what's being gained on not allowing to
> > change this with Kconfig.
> 
> There's lot of out-of-tree drivers too that distributions. I'm not
> finding anything usefel in this argument.
> 
> > 
> > Also to note, the actual binary being called can already be swapped out by
> > CONFIG_STATIC_USERMODEHELPER_PATH, although for the NixOS this is not the proper
> > mechanism as it affects coredump too which isn't a fixed path binary in /sbin.
> 
> I have not seen actual uses of CONFIG_STATIC_USERMODEHELPER_PATH. You
> could probably use it with busybox?
> 
> > 
> > This is really just for distros to be able to configure where /sbin is located.
> > Given usr merge and (some distros) bin/sbin merge, the canonical path of
> > request-key binary is very likely not /sbin/request-key anymore, so it seems to
> > make sense to me to allow this to be changed rather than always go through
> > compatibility symlinks.
> 
> I doubt there's a huge demand other than NixOS. Just basing this on that
> no other noise have been made so far.
> 
> > 
> > How about a something like CONFIG_DEFAULT_USERMODEHELPER_PATH which defaults to
> > /sbin, and then request-key uses that concatenated with "/request-key"?
> > 
> > >
> > > Not sure right now but one option might kernel command-line. Then it is
> > > known at run-time, can be signed etc. Compiled value has no identity in
> > > the same way.
> > >
> > > And I don't care if NixOS has such a problem as I've not have any stake
> > > making of those decisions.
> > >
> > > You really should explain why it makes sense to have such feature i.e.,
> > > why is it useful. And if NixOS considered, why is it useful for NixOS.
> > >
> > > This all should be in  the commit message.
> > 
> > Sure, if you need some more detailed explaination on how NixOS works.
> > 
> > NixOS intentionally not use FHS directory paths, so packages refers to their
> > dependencies via cryptographical hashes in rather than fixed paths for build-time
> > known dependencies, and themselves also live in a path with hashes in them (so
> > two different versions of the same package are in different paths). E.g.
> > /nix/store/wjzk0s5dwk0i7swh3rmh1pl10k6v1w6d-keyutils-1.6.3/bin/request-key
> > 
> > The full system is also built as a package with all installed binaries in
> > $pkg/sw/bin, configuration in $pkg/etc, etc.. The current system is symlinked to
> > /run/current-system, and when a new system is activated this symlink is swapped
> > out and therefore all paths are updated atomically. For request-key, this is
> > symlinked to
> > /run/current-system/sw/bin/request-key
> > 
> > NixOS carries a patch which uses this path instead of /sbin (which does not
> > exist on NixOS at all).
> > 
> > The motivation is really "be able to switch /sbin". I feel that all the above are
> > secondary motivations and is too verbose to include in the commit message.
> > 
> > I am not a maintainer for NixOS's kernel; I use NixOS and just want to develop
> > kernel and test out kernel on my machines without having to patch them.
> 
> I don't frankly care how NixOS works per se in details. Scope this into
> message to problem that it addresses.

Not 100% NAK but this does not have "universal logic" embedded into it"

"Distro's use it" is popularity opinion, which has no place over here.
Mastodon, Threads etc. work for that so much better.

Perhaps if the motivation-stimuli-solution type of logics gets carved
crystal clear we can move forward. I.e. you need to work on this. I've
given my feedback for this version, and it is not good enough, sorry.

BR, Jarkko

^ permalink raw reply

* Re: [PATCH] keys: allow request-key path to be configured via Kconfig
From: Gary Guo @ 2026-06-10 13:31 UTC (permalink / raw)
  To: Jarkko Sakkinen, Gary Guo
  Cc: David Howells, Paul Moore, James Morris, Serge E. Hallyn,
	keyrings, linux-security-module, linux-kernel
In-Reply-To: <ailgG5JF6Qu_6v0P@kernel.org>

On Wed Jun 10, 2026 at 2:01 PM BST, Jarkko Sakkinen wrote:
> On Wed, Jun 10, 2026 at 03:57:37PM +0300, Jarkko Sakkinen wrote:
>> On Mon, Jun 08, 2026 at 11:30:06AM +0100, Gary Guo wrote:
>> > On Mon Jun 8, 2026 at 5:59 AM BST, Jarkko Sakkinen wrote:
>> > > On Mon, Jun 08, 2026 at 07:50:03AM +0300, Jarkko Sakkinen wrote:
>> > >> On Sun, Jun 07, 2026 at 02:49:27PM +0100, Gary Guo wrote:
>> > >> > From: Gary Guo <gary@garyguo.net>
>> > >> > 
>> > >> > Some Linux distributions (e.g. NixOS) does not have /sbin present, and they
>> > >> > currently carry patches to replace /sbin/request-key to some other path.
>> > >> 
>> > >> Sorry but no configuration for introducing API divergence.
>> > 
>> > What is the API divergence here? Distros can already patch the kernel or place a
>> > different binary there, so I don't see what's being gained on not allowing to
>> > change this with Kconfig.
>> 
>> There's lot of out-of-tree drivers too that distributions. I'm not
>> finding anything usefel in this argument.

Out-of-tree drivers are, well, out of tree. This one requires patching the tree.
Unlike many other distros, so far the only patches needed for NixOS is patching
out /sbin.

>> 
>> > 
>> > Also to note, the actual binary being called can already be swapped out by
>> > CONFIG_STATIC_USERMODEHELPER_PATH, although for the NixOS this is not the proper
>> > mechanism as it affects coredump too which isn't a fixed path binary in /sbin.
>> 
>> I have not seen actual uses of CONFIG_STATIC_USERMODEHELPER_PATH. You
>> could probably use it with busybox?

I think it's used for hardening.

>> > 
>> > This is really just for distros to be able to configure where /sbin is located.
>> > Given usr merge and (some distros) bin/sbin merge, the canonical path of
>> > request-key binary is very likely not /sbin/request-key anymore, so it seems to
>> > make sense to me to allow this to be changed rather than always go through
>> > compatibility symlinks.
>> 
>> I doubt there's a huge demand other than NixOS. Just basing this on that
>> no other noise have been made so far.
>> 
>> > 
>> > How about a something like CONFIG_DEFAULT_USERMODEHELPER_PATH which defaults to
>> > /sbin, and then request-key uses that concatenated with "/request-key"?
>> > 
>> > [snip]
>> 
>> I don't frankly care how NixOS works per se in details. Scope this into
>> message to problem that it addresses.

Well, I reckon that's what's going to happen, so in the commit message I just
included "binary is not in /sbin". But the idea is that there's a good reason
that it's not in /sbin.

>
> Not 100% NAK but this does not have "universal logic" embedded into it"
>
> "Distro's use it" is popularity opinion, which has no place over here.
> Mastodon, Threads etc. work for that so much better.

I disagree. Distro is really just a collection of users. I would rather than
phrase this as "user's using it this way". If something needs to be patched to
be used, I think that's rather a good reason to make the change.

I think "user wants to control where UMH lives" is a pretty good motivation, but
it looks like you disagree. Anyhow, if you don't like the idea, I'll just drop
this patch, as I am not the one maintaining these distro patches anyway. I just
think it's the best if Kconfig can meet user demand and more people can run
unpatched kernels.

Best,
Gary

> Perhaps if the motivation-stimuli-solution type of logics gets carved
> crystal clear we can move forward. I.e. you need to work on this. I've
> given my feedback for this version, and it is not good enough, sorry.
>
> BR, Jarkko



^ permalink raw reply

* Re: [PATCH] keys: allow request-key path to be configured via Kconfig
From: Gary Guo @ 2026-06-10 13:37 UTC (permalink / raw)
  To: Jarkko Sakkinen, Gary Guo
  Cc: David Howells, Paul Moore, James Morris, Serge E. Hallyn,
	keyrings, linux-security-module, linux-kernel
In-Reply-To: <ailfPUbn8gbUqB1D@kernel.org>

On Wed Jun 10, 2026 at 1:57 PM BST, Jarkko Sakkinen wrote:
> On Mon, Jun 08, 2026 at 11:30:06AM +0100, Gary Guo wrote:
>> 
>> This is really just for distros to be able to configure where /sbin is located.
>> Given usr merge and (some distros) bin/sbin merge, the canonical path of
>> request-key binary is very likely not /sbin/request-key anymore, so it seems to
>> make sense to me to allow this to be changed rather than always go through
>> compatibility symlinks.
>
> I doubt there's a huge demand other than NixOS. Just basing this on that
> no other noise have been made so far.

Just to add on this, both Fedora and openSUSE for example changes their
CONFIG_MODPROBE_PATH to be /usr/sbin/modprobe after /usr merge. They still have
the /sbin -> /usr/sbin symlink available, so it's not like they cannot work with
/sbin/request-key, but I would think that if the option is available then they
might switch to use /usr/sbin/request-key, too.

After all, why would one perform a symlink walk for no reason?

Best,
Gary

^ permalink raw reply

* Re: [PATCH v3 1/3] landlock: Require LANDLOCK_ACCESS_FS_MAKE_WHITEOUT for RENAME_WHITEOUT
From: Mickaël Salaün @ 2026-06-10 13:38 UTC (permalink / raw)
  To: Günther Noack
  Cc: Christian Brauner, linux-security-module, Paul Moore,
	Amir Goldstein, Miklos Szeredi, Serge Hallyn, Stephen Smalley
In-Reply-To: <20260610092318.3868884-2-gnoack@google.com>

Making MAKE_CHAR not covering MAKE_WHITEOUT is not addressed (see
previous discussion).  MAKE_CHAR should not restrict whiteout creation
*if* MAKE_WHITEOUT is handled.  Specific tests should check that all
these cases are proprely handled.

There is no documentation update related to the new feature.  A note
should also explain what exactly is a whiteout and why it is not
considered a character device (see previous discussions).

The sandboxer is not updated.

There is no audit tests.


On Wed, Jun 10, 2026 at 11:23:16AM +0200, Günther Noack wrote:
> renameat2(2) with the RENAME_WHITEOUT flag places a whiteout character
> device file in the source file location in place of the moved file.
> This creates a directory entry even in cases where all
> LANDLOCK_ACCESS_FS_MAKE_* rights are denied.
> 
> Introduce the LANDLOCK_ACCESS_FS_MAKE_WHITEOUT right, which is checked
> for the origin directory if RENAME_WHITEOUT is passed.
> 
> This does not affect normal renames within layered OverlayFS mounts:
> When OverlayFS invokes rename with RENAME_WHITEOUT as part of a
> "normal" rename operation, it does so in ovl_rename() using the
> credentials that were set at the time of mounting the OverlayFS.
> 
> Bump the Landlock ABI version to 10.
> 
> Suggested-by: Christian Brauner <brauner@kernel.org>
> Suggested-by: Mickaël Salaün <mic@digikod.net>
> Signed-off-by: Günther Noack <gnoack@google.com>
> ---
>  include/uapi/linux/landlock.h                |  3 +++
>  security/landlock/audit.c                    |  1 +
>  security/landlock/fs.c                       | 17 ++++++++++++++---
>  security/landlock/limits.h                   |  2 +-
>  security/landlock/syscalls.c                 |  2 +-
>  tools/testing/selftests/landlock/base_test.c |  4 ++--
>  tools/testing/selftests/landlock/fs_test.c   |  5 +++--
>  7 files changed, 25 insertions(+), 9 deletions(-)
> 
> diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h
> index 10a346e55e95..1f8a1d6d25f1 100644
> --- a/include/uapi/linux/landlock.h
> +++ b/include/uapi/linux/landlock.h
> @@ -328,6 +328,8 @@ struct landlock_net_port_attr {
>   *
>   *   If multiple requirements are not met, the ``EACCES`` error code takes
>   *   precedence over ``EXDEV``.
> + * - %LANDLOCK_ACCESS_FS_MAKE_WHITEOUT: Create a whiteout object through
> + *   :manpage:`rename(2)` with ``RENAME_WHITEOUT``.
>   *
>   * .. warning::
>   *
> @@ -356,6 +358,7 @@ struct landlock_net_port_attr {
>  #define LANDLOCK_ACCESS_FS_TRUNCATE			(1ULL << 14)
>  #define LANDLOCK_ACCESS_FS_IOCTL_DEV			(1ULL << 15)
>  #define LANDLOCK_ACCESS_FS_RESOLVE_UNIX			(1ULL << 16)
> +#define LANDLOCK_ACCESS_FS_MAKE_WHITEOUT		(1ULL << 17)
>  /* clang-format on */
>  
>  /**
> diff --git a/security/landlock/audit.c b/security/landlock/audit.c
> index 8d0edf94037d..09c97083f599 100644
> --- a/security/landlock/audit.c
> +++ b/security/landlock/audit.c
> @@ -38,6 +38,7 @@ static const char *const fs_access_strings[] = {
>  	[BIT_INDEX(LANDLOCK_ACCESS_FS_TRUNCATE)] = "fs.truncate",
>  	[BIT_INDEX(LANDLOCK_ACCESS_FS_IOCTL_DEV)] = "fs.ioctl_dev",
>  	[BIT_INDEX(LANDLOCK_ACCESS_FS_RESOLVE_UNIX)] = "fs.resolve_unix",
> +	[BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_WHITEOUT)] = "fs.make_whiteout",
>  };
>  
>  static_assert(ARRAY_SIZE(fs_access_strings) == LANDLOCK_NUM_ACCESS_FS);
> diff --git a/security/landlock/fs.c b/security/landlock/fs.c
> index c1ecfe239032..67810d5242b2 100644
> --- a/security/landlock/fs.c
> +++ b/security/landlock/fs.c
> @@ -1080,6 +1080,7 @@ static bool collect_domain_accesses(const struct landlock_ruleset *const domain,
>   * @new_dentry: Destination file or directory.
>   * @removable: Sets to true if it is a rename operation.
>   * @exchange: Sets to true if it is a rename operation with RENAME_EXCHANGE.
> + * @whiteout: Sets to true if it is a rename operation with RENAME_WHITEOUT.
>   *
>   * Because of its unprivileged constraints, Landlock relies on file hierarchies
>   * (and not only inodes) to tie access rights to files.  Being able to link or
> @@ -1127,7 +1128,8 @@ static bool collect_domain_accesses(const struct landlock_ruleset *const domain,
>  static int current_check_refer_path(struct dentry *const old_dentry,
>  				    const struct path *const new_dir,
>  				    struct dentry *const new_dentry,
> -				    const bool removable, const bool exchange)
> +				    const bool removable, const bool exchange,
> +				    const bool whiteout)
>  {
>  	const struct landlock_cred_security *const subject =
>  		landlock_get_applicable_subject(current_cred(), any_fs, NULL);
> @@ -1159,6 +1161,14 @@ static int current_check_refer_path(struct dentry *const old_dentry,
>  		access_request_parent2 |= maybe_remove(new_dentry);
>  	}
>  
> +	/*
> +	 * In case of renameat2(2) with RENAME_WHITEOUT, a whiteout object is
> +	 * created in the source location, so we require an additional access
> +	 * right there.
> +	 */
> +	if (whiteout)
> +		access_request_parent1 |= LANDLOCK_ACCESS_FS_MAKE_WHITEOUT;
> +
>  	/* The mount points are the same for old and new paths, cf. EXDEV. */
>  	if (old_dentry->d_parent == new_dir->dentry) {
>  		/*
> @@ -1509,7 +1519,7 @@ static int hook_path_link(struct dentry *const old_dentry,
>  			  struct dentry *const new_dentry)
>  {
>  	return current_check_refer_path(old_dentry, new_dir, new_dentry, false,
> -					false);
> +					false, false);
>  }
>  
>  static int hook_path_rename(const struct path *const old_dir,
> @@ -1520,7 +1530,8 @@ static int hook_path_rename(const struct path *const old_dir,
>  {
>  	/* old_dir refers to old_dentry->d_parent and new_dir->mnt */
>  	return current_check_refer_path(old_dentry, new_dir, new_dentry, true,
> -					!!(flags & RENAME_EXCHANGE));
> +					!!(flags & RENAME_EXCHANGE),
> +					!!(flags & RENAME_WHITEOUT));
>  }
>  
>  static int hook_path_mkdir(const struct path *const dir,
> diff --git a/security/landlock/limits.h b/security/landlock/limits.h
> index b454ad73b15e..e59378e8e897 100644
> --- a/security/landlock/limits.h
> +++ b/security/landlock/limits.h
> @@ -19,7 +19,7 @@
>  #define LANDLOCK_MAX_NUM_LAYERS		16
>  #define LANDLOCK_MAX_NUM_RULES		U32_MAX
>  
> -#define LANDLOCK_LAST_ACCESS_FS		LANDLOCK_ACCESS_FS_RESOLVE_UNIX
> +#define LANDLOCK_LAST_ACCESS_FS		LANDLOCK_ACCESS_FS_MAKE_WHITEOUT
>  #define LANDLOCK_MASK_ACCESS_FS		((LANDLOCK_LAST_ACCESS_FS << 1) - 1)
>  #define LANDLOCK_NUM_ACCESS_FS		__const_hweight64(LANDLOCK_MASK_ACCESS_FS)
>  
> diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c
> index accfd2e5a0cd..d45469d5d464 100644
> --- a/security/landlock/syscalls.c
> +++ b/security/landlock/syscalls.c
> @@ -166,7 +166,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
> diff --git a/tools/testing/selftests/landlock/base_test.c b/tools/testing/selftests/landlock/base_test.c
> index 30d37234086c..6c8113c2ded1 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));
> diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c
> index cdb47fc1fc0a..53d1b659849f 100644
> --- a/tools/testing/selftests/landlock/fs_test.c
> +++ b/tools/testing/selftests/landlock/fs_test.c
> @@ -579,7 +579,7 @@ TEST_F_FORK(layout1, inval)
>  	LANDLOCK_ACCESS_FS_IOCTL_DEV | \
>  	LANDLOCK_ACCESS_FS_RESOLVE_UNIX)
>  
> -#define ACCESS_LAST LANDLOCK_ACCESS_FS_RESOLVE_UNIX
> +#define ACCESS_LAST LANDLOCK_ACCESS_FS_MAKE_WHITEOUT
>  
>  #define ACCESS_ALL ( \
>  	ACCESS_FILE | \
> @@ -593,7 +593,8 @@ TEST_F_FORK(layout1, inval)
>  	LANDLOCK_ACCESS_FS_MAKE_FIFO | \
>  	LANDLOCK_ACCESS_FS_MAKE_BLOCK | \
>  	LANDLOCK_ACCESS_FS_MAKE_SYM | \
> -	LANDLOCK_ACCESS_FS_REFER)
> +	LANDLOCK_ACCESS_FS_REFER | \
> +	LANDLOCK_ACCESS_FS_MAKE_WHITEOUT)
>  
>  /* clang-format on */
>  
> -- 
> 2.54.0.1099.g489fc7bff1-goog
> 

^ permalink raw reply

* Re: [PATCH 2/2] landlock: replace __sk_common struct sock field accesses
From: Mickaël Salaün @ 2026-06-10 13:41 UTC (permalink / raw)
  To: Matthieu Buffet
  Cc: Günther Noack, Mikhail Ivanov, Tingmao Wang,
	konstantin.meskhidze, linux-security-module
In-Reply-To: <20260609211511.85630-2-matthieu@buffet.re>

Thanks for the fixes, but can you please send a full v5 patch series?

On Tue, Jun 09, 2026 at 11:15:11PM +0200, Matthieu Buffet wrote:
> Use the proper macro to access __sk_common.skc_family like everywhere
> else.
> 
> Signed-off-by: Matthieu Buffet <matthieu@buffet.re>
> ---
>  security/landlock/net.c | 2 +-
>  1 file changed, 1 insertion(+), 1 deletion(-)
> 
> diff --git a/security/landlock/net.c b/security/landlock/net.c
> index 111e58fd9325..fc2acf8bd898 100644
> --- a/security/landlock/net.c
> +++ b/security/landlock/net.c
> @@ -71,7 +71,7 @@ static int current_check_access_socket(struct socket *const sock,
>  	 * The socket is not locked, so sk_family can change concurrently
>  	 * due to e.g. setsockopt(IPV6_ADDRFORM).
>  	 */
> -	sock_family = READ_ONCE(sock->sk->__sk_common.skc_family);
> +	sock_family = READ_ONCE(sock->sk->sk_family);
>  
>  	switch (address->sa_family) {
>  	case AF_UNSPEC:
> -- 
> 2.47.3
> 
> 

^ permalink raw reply

* Re: [PATCH v4 0/7] landlock: Add UDP access control support
From: Mickaël Salaün @ 2026-06-10 13:44 UTC (permalink / raw)
  To: Matthieu Buffet
  Cc: Günther Noack, linux-security-module, Mikhail Ivanov,
	konstantin.meskhidze, Tingmao Wang
In-Reply-To: <ed2805ed-72cf-4277-993e-8a0ca73e65ee@buffet.re>

On Sat, Jun 06, 2026 at 07:01:24PM +0200, Matthieu Buffet wrote:
> Hi Mickaël, Günther,
> 
> Thank you both for your reviews, I will follow up with these last fixes in a
> v5.
> 
> On 5/22/2026 11:08 PM, Mickaël Salaün wrote:
> > > I'm just not super happy about the clarity of logs generated for denied
> > > autobinds ("domain=xxxxxx blockers=net.bind_udp"), due to the fact that
> > > addresses and ports are currently only logged if they are non-0. A later
> > > (coordinated LSM-wide) patch could improve readability by replacing != 0
> > > checks with new booleans in struct lsm_network_audit.
> > 
> > Do you plan to send such patch after this series?  I guess we could add
> > has_{port,addr} fields to lsm_network_audit and handle AF_UNSPEC too?
> 
> I have not come up with anything better than adding boolean fields, so if
> you're in, I will draft a proposition along these lines (and cc: LSM
> subsystem maintainers to synchronize the change across LSMs, I guess)

This sounds good to me.

> 
> > > I'm also not
> > > exactly happy with the integration in existing TCP selftests, but
> > > refactoring them has already been discussed earlier.
> > 
> > Can you remind us what was your concern and the potential fix?
> 
> Regarding TCP selftests, I was referencing that discussion about readability
> (length, and usage of conditionals in what are already test variants) :
> https://lore.kernel.org/linux-security-module/22dcebae-dc5d-0bf1-c686-d2f444558106@huawei-partners.com/
> Nothing blocking, refactoring can be done when things are less busy.

Yes, let's keep that in mind and discuss it once this patch series is
merged.

> 
> -- 
> Matthieu
> 

^ permalink raw reply

* Re: [PATCH 2/2] landlock: replace __sk_common struct sock field accesses
From: Mickaël Salaün @ 2026-06-10 13:48 UTC (permalink / raw)
  To: Matthieu Buffet
  Cc: Günther Noack, Mikhail Ivanov, Tingmao Wang,
	konstantin.meskhidze, linux-security-module
In-Reply-To: <20260610.rai6icahch2L@digikod.net>

On Wed, Jun 10, 2026 at 03:41:06PM +0200, Mickaël Salaün wrote:
> Thanks for the fixes, but can you please send a full v5 patch series?

Ok, these are not part of the UDP patch series, but this part of the
same thread, my bad...  I'll pick them, thanks!

> 
> On Tue, Jun 09, 2026 at 11:15:11PM +0200, Matthieu Buffet wrote:
> > Use the proper macro to access __sk_common.skc_family like everywhere
> > else.
> > 
> > Signed-off-by: Matthieu Buffet <matthieu@buffet.re>
> > ---
> >  security/landlock/net.c | 2 +-
> >  1 file changed, 1 insertion(+), 1 deletion(-)
> > 
> > diff --git a/security/landlock/net.c b/security/landlock/net.c
> > index 111e58fd9325..fc2acf8bd898 100644
> > --- a/security/landlock/net.c
> > +++ b/security/landlock/net.c
> > @@ -71,7 +71,7 @@ static int current_check_access_socket(struct socket *const sock,
> >  	 * The socket is not locked, so sk_family can change concurrently
> >  	 * due to e.g. setsockopt(IPV6_ADDRFORM).
> >  	 */
> > -	sock_family = READ_ONCE(sock->sk->__sk_common.skc_family);
> > +	sock_family = READ_ONCE(sock->sk->sk_family);
> >  
> >  	switch (address->sa_family) {
> >  	case AF_UNSPEC:
> > -- 
> > 2.47.3
> > 
> > 

^ permalink raw reply

* Re: [PATCH] keys: Pin request_key_auth payload in instantiate paths
From: eee sss @ 2026-06-10 15:21 UTC (permalink / raw)
  To: Jarkko Sakkinen
  Cc: keyrings, linux-security-module, linux-kernel, David Howells,
	Paul Moore, James Morris, Serge E. Hallyn
In-Reply-To: <aik5YaJhjRuOKE7I@kernel.org>

Thanks, Jarkko. Sorry for the delayed response.

I checked the commit in for-next-keys. The updated commit message and the
cleanup look good to me.

Best,
Shaomin

On Wed, 10 Jun 2026 13:16:01 +0300, Jarkko Sakkinen <jarkko@kernel.org> wrote:
> On Mon, Jun 08, 2026 at 08:42:21AM +0300, Jarkko Sakkinen wrote:
> > On Mon, Jun 08, 2026 at 08:29:11AM +0300, Jarkko Sakkinen wrote:
> > > On Mon, Jun 08, 2026 at 06:10:23AM +0300, Jarkko Sakkinen wrote:
> > > > On Mon, Jun 08, 2026 at 06:06:21AM +0300, Jarkko Sakkinen wrote:
> > > > > On Tue, May 26, 2026 at 10:48:38AM +0800, Shaomin Chen wrote:
> > > > > > keyctl_instantiate_key_common() reads request_key_auth from the assumed
> > > > > > auth key before copying an instantiation payload from userspace. The copy
> > > > > > can fault and sleep. If the request completes and revokes the auth key in
> > > > > > that window, the auth payload can be detached and freed before the
> > > > > > instantiate path uses it again.
> > > > > >
> > > > > > A request-key helper reproducer can trigger this race. One helper child
> > > > > > blocks in KEYCTL_INSTANTIATE_IOV while the original helper instantiates the
> > > > > > requested key and returns. KASAN then reports a use-after-free from the
> > > > > > stale request_key_auth payload in keyctl_instantiate_key_common().
> > > > > >
> > > > > > Give request_key_auth payloads a refcount. Take a payload reference while
> > > > >
> > > > > Please, name concrete things accurately. I.e. 'usage' in this case. If
> > > > > you have a name, use it instead of obfuscating generalizations.
> > > > >
> > > > > > authkey->sem stabilizes the payload and revocation state. Hold that
> > > > > > reference across the instantiate and reject paths. Drop the auth key
> > > > > > owning reference from revoke and destroy.
> > > > > >
> > > > > > Reported-by: Shaomin Chen <eeesssooo020@gmail.com>
> > > > > > Closes: https://lore.kernel.org/r/20260519144403.436694-1-eeesssooo020@gmail.com
> > > > > > Signed-off-by: Shaomin Chen <eeesssooo020@gmail.com>
> > > > > > ---
> > > > > > include/keys/request_key_auth-type.h | 2 ++
> > > > > > security/keys/internal.h | 2 ++
> > > > > > security/keys/keyctl.c | 24 +++++++++++++++-----
> > > > > > security/keys/request_key_auth.c | 33 ++++++++++++++++++++++++++--
> > > > > > 4 files changed, 53 insertions(+), 8 deletions(-)
> > > > >
> > > > > So first, couple of things.
> > > > >
> > > > > I'm not going to test not that well documented involving OOT driver.
> > > >
> > > > Oops, sorry typo. "not that well documented reproducer" :-)
> > > >
> > > > But it is cool we just then need to draw the picture.
> > >
> > > I think I got this:
> > >
> > > A: request_key() B: KEYCTL_INSTANTIATE_IOV
> > > ---------------- -------------------------
> > > create auth key
> > > store rka in auth key
> > > wait for helper
> > > get auth key
> > > load rka from auth key
> > > copy user payload
> > > sleep on #PF
> > > helper completed
> > > detach and free rka
> > > destroy auth key
> > > wake up
> > > use rka->target_key
> > > **USE-AFTER-FREE**
> > >
> > > So nothing really complicated here, is there?
> >
> > Send v2 with the code changes that I proposed as we want to the change
> > as ergonomic as possible.
> >
> > Use this as the commit message:
> >
> > keys: Pin request_key_auth payload in instantiate paths
> >
> > A: request_key() B: KEYCTL_INSTANTIATE_IOV
> > ---------------- -------------------------
> > create auth key
> > store rka in auth key
> > wait for helper
> > get auth key
> > load rka from auth key
> > copy user payload
> > sleep on #PF
> >
> > helper completed
> > detach and free rka
> > destroy auth key
> > wake up
> > use rka->target_key
> > **USE-AFTER-FREE**
> >
> > Give request_key_auth payloads a refcount. Take a payload reference while
> > authkey->sem stabilizes the payload and revocation state. Hold that
> > reference across the instantiate and reject paths. Drop the auth key
> > owning reference from revoke and destroy.
> >
> > [jarkko: Replaced the first two paragraphs of text with a concurrency scenario.]
> >
> > And it includes also the remark at the end.
> >
> > BR, Jarkko
>
> Nothing heard so I pushed:
>
> https://git.kernel.org/pub/scm/linux/kernel/git/jarkko/linux-tpmdd.git/commit/?h=for-next-keys&id=9feb0bb3468e863b2b82a2eabfaeec4c7c44b90c
>
> It has the commit message change.
>
> BR, Jarkko

^ permalink raw reply

* Re: [PATCH] hardening: Default randstruct off with rust for better allmodconfig support
From: Kees Cook @ 2026-06-10 20:37 UTC (permalink / raw)
  To: Mark Brown
  Cc: Gustavo A. R. Silva, Paul Moore, James Morris, Serge E. Hallyn,
	Miguel Ojeda, Boqun Feng, Gary Guo, Björn Roy Baron,
	Benno Lossin, Andreas Hindborg, Alice Ryhl, Trevor Gross,
	Danilo Krummrich, linux-hardening, linux-security-module,
	linux-kernel, rust-for-linux
In-Reply-To: <20260605-rust-reverse-randstruct-dep-v1-1-45ce9ee8d0d1@kernel.org>

On Fri, Jun 05, 2026 at 05:01:46PM +0100, Mark Brown wrote:
> Currently randstruct does not support rust so we have Kconfig dependencies
> which prevent rust being enabled when randstruct is. Unfortunately this
> prevents rust being enabled in allmodconfig, our standard coverage build.
> randstruct gets turned on by default, then the dependency on !RANDSTRUCT
> causes rust to get disabled.
> 
> Work around this by disabling randstruct by default if we have a usable
> rust toolchain, circular dependencies prevent us directly depending on
> !RUST. This means we might end up with a configuration that disables both
> rust and randstruct but hopefully it's more likely go give the expected
> result.
> 
> Signed-off-by: Mark Brown <broonie@kernel.org>

Can we instead just allow it? This has been ready to go for a while,
IIUC:
https://lore.kernel.org/all/CANiq72n=hgH4bqJjp8MsMHAaxaAo75GSBcHGTvFT3NTSaVPGWg@mail.gmail.com/

-Kees

> ---
>  security/Kconfig.hardening | 2 +-
>  1 file changed, 1 insertion(+), 1 deletion(-)
> 
> diff --git a/security/Kconfig.hardening b/security/Kconfig.hardening
> index 86f8768c63d4..1677c4f9637b 100644
> --- a/security/Kconfig.hardening
> +++ b/security/Kconfig.hardening
> @@ -285,7 +285,7 @@ config CC_HAS_RANDSTRUCT
>  
>  choice
>  	prompt "Randomize layout of sensitive kernel structures"
> -	default RANDSTRUCT_FULL if COMPILE_TEST && (GCC_PLUGINS || CC_HAS_RANDSTRUCT)
> +	default RANDSTRUCT_FULL if !RUST_IS_AVAILABLE && COMPILE_TEST && (GCC_PLUGINS || CC_HAS_RANDSTRUCT)
>  	default RANDSTRUCT_NONE
>  	help
>  	  If you enable this, the layouts of structures that are entirely
> 
> ---
> base-commit: e43ffb69e0438cddd72aaa30898b4dc446f664f8
> change-id: 20260605-rust-reverse-randstruct-dep-5a504c861128
> 
> Best regards,
> --  
> Mark Brown <broonie@kernel.org>
> 

-- 
Kees Cook

^ permalink raw reply

* Re: [PATCH v2] hardening: Default randstruct off with rust for better allmodconfig support
From: Kees Cook @ 2026-06-10 20:41 UTC (permalink / raw)
  To: Miguel Ojeda
  Cc: Mark Brown, Gustavo A. R. Silva, Paul Moore, James Morris,
	Serge E. Hallyn, Miguel Ojeda, Boqun Feng, Gary Guo,
	Björn Roy Baron, Benno Lossin, Andreas Hindborg, Alice Ryhl,
	Trevor Gross, Danilo Krummrich, linux-hardening,
	linux-security-module, linux-kernel, rust-for-linux
In-Reply-To: <CANiq72mmzfBg0_y+TMTsUUuO0cJFE0=n60-ttwOynai06_y=zg@mail.gmail.com>

On Fri, Jun 05, 2026 at 07:22:54PM +0200, Miguel Ojeda wrote:
> On Fri, Jun 5, 2026 at 6:51 PM Mark Brown <broonie@kernel.org> wrote:
> >
> > Currently randstruct does not support rust so we have Kconfig dependencies
> > which prevent rust being enabled when randstruct is. Unfortunately this
> > prevents rust being enabled in allmodconfig, our standard coverage build.
> > randstruct gets turned on by default, then the dependency on !RANDSTRUCT
> > causes rust to get disabled.
> >
> > Work around this by disabling randstruct by default if we have a usable
> > rust toolchain and rust support for the architecture, circular
> > dependencies prevent us directly depending on !RUST. This means we might
> > end up with a configuration that disables both rust and randstruct but
> > hopefully it's more likely go give the expected result.
> >
> > Signed-off-by: Mark Brown <broonie@kernel.org>
> 
> Thanks Mark!
> 
> Kees, Gustavo: applying this would help Mark's testing of Rust in
> linux-next, which is important to keep.
> 
> An alternative would be to move forward with `RANDSTRUCT` support:
> 
>   https://lore.kernel.org/rust-for-linux/20260323130224.165738-1-ojeda@kernel.org/
> 
> Either the conditional (on the Rust side) or the unconditional
> approaches (modifying the C side) should be fine, i.e. whatever
> Kees/Gustavo think is best. The unconditional one would make things
> easier on the Rust side, but it is a "bigger" change in terms of
> impact. We can always start with the conditional one instead.

Oops, I missed this v2. :)

For the linux-next testing, are you doing GCC + llvm rustc builds? IIUC,
then the support patch mentioned, I think, doesn't actually solve the
problem?

-Kees

-- 
Kees Cook

^ permalink raw reply

* Re: [PATCH] hardening: Default randstruct off with rust for better allmodconfig support
From: Miguel Ojeda @ 2026-06-10 20:41 UTC (permalink / raw)
  To: Kees Cook
  Cc: Mark Brown, Gustavo A. R. Silva, Paul Moore, James Morris,
	Serge E. Hallyn, Miguel Ojeda, Boqun Feng, Gary Guo,
	Björn Roy Baron, Benno Lossin, Andreas Hindborg, Alice Ryhl,
	Trevor Gross, Danilo Krummrich, linux-hardening,
	linux-security-module, linux-kernel, rust-for-linux
In-Reply-To: <202606101335.648C6993@keescook>

On Wed, Jun 10, 2026 at 10:37 PM Kees Cook <kees@kernel.org> wrote:
>
> Can we instead just allow it? This has been ready to go for a while,
> IIUC:
> https://lore.kernel.org/all/CANiq72n=hgH4bqJjp8MsMHAaxaAo75GSBcHGTvFT3NTSaVPGWg@mail.gmail.com/

Mark sent a v2 where I mentioned that! :) Please see:

  https://lore.kernel.org/rust-for-linux/CANiq72mmzfBg0_y+TMTsUUuO0cJFE0=n60-ttwOynai06_y=zg@mail.gmail.com/

Cheers,
Miguel

^ permalink raw reply

* Re: [PATCH] cred: prevent slab cache merging for cred_jar
From: Kees Cook @ 2026-06-10 20:45 UTC (permalink / raw)
  To: Mohammed EL Kadiri
  Cc: Paul Moore, Serge Hallyn, Vlastimil Babka, linux-security-module,
	linux-hardening, linux-kernel
In-Reply-To: <20260606142558.13809-1-med08elkadiri@gmail.com>

On Sat, Jun 06, 2026 at 03:25:58PM +0100, Mohammed EL Kadiri wrote:
> The cred_jar slab cache holds struct cred objects, which contain
> process credentials: uid, gid, euid, egid, and capability sets.
> Overwriting any of these fields is sufficient for privilege escalation.
> 
> On a default Ubuntu 6.17.0-23-generic system, cred_jar (named "cred"
> in sysfs) has 2 aliases, meaning 2 unrelated object types share its
> slab pages (object_size=184, objs_per_slab=42).
> 
> Cross-cache heap exploitation relies on slab cache merging to achieve
> type confusion between unrelated kernel objects. CVE-2022-29582
> demonstrates this technique: an io_uring use-after-free is leveraged
> across cache boundaries through page-level reallocation, ultimately
> achieving root. struct cred is a primary target in this class of
> attacks due to the direct privilege escalation that results from
> corrupting any of its identity or capability fields.
> 
> Add SLAB_NO_MERGE to ensure cred_jar receives dedicated slab pages,
> so that freed credential slots can only be reallocated as struct cred
> objects. The memory overhead is minimal: one struct cred exists per
> task, and with 42 objects per slab page, the cost of dedicated pages
> is negligible. There is zero performance impact on the allocation
> hot path.
> 
> This follows the precedent set by skbuff_head_cache (net/core/skbuff.c)
> and key_jar (security/keys/key.c) which use SLAB_NO_MERGE for similar
> isolation requirements.
> 
> Signed-off-by: Mohammed EL Kadiri <med08elkadiri@gmail.com>

Yes please. :)

Reviewed-by: Kees Cook <kees@kernel.org>

-- 
Kees Cook

^ permalink raw reply

* Re: [PATCH 04/11] treewide: Convert struct kernel_param_ops initializers to DEFINE_KERNEL_PARAM_OPS
From: jim.cromie @ 2026-06-10 21:06 UTC (permalink / raw)
  To: Petr Pavlu
  Cc: Kees Cook, Luis Chamberlain, Pengpeng Hou, Richard Weinberger,
	Anton Ivanov, Johannes Berg, Rafael J. Wysocki, Len Brown,
	Corey Minyard, Gabriel Somlo, Michael S. Tsirkin, Jani Nikula,
	Joonas Lahtinen, Rodrigo Vivi, Tvrtko Ursulin, David Airlie,
	Simona Vetter, Bart Van Assche, Jason Gunthorpe, Leon Romanovsky,
	Laurent Pinchart, Hans de Goede, Mauro Carvalho Chehab,
	Bjorn Helgaas, Hannes Reinecke, James E.J. Bottomley,
	Martin K. Petersen, Daniel Lezcano, Zhang Rui, Lukasz Luba,
	Greg Kroah-Hartman, Jiri Slaby, Alan Stern, Jason Wang, Xuan Zhuo,
	Eugenio Pérez, Jason Baron, Tiwei Bie, Benjamin Berg,
	Ilpo Järvinen, David E. Box, Maciej W. Rozycki,
	Srinivas Pandruvada, Peter Zijlstra, Heiko Carstens,
	Vasily Gorbik, Sean Christopherson, Paolo Bonzini,
	Thomas Gleixner, Ingo Molnar, Borislav Petkov, Dave Hansen, x86,
	H. Peter Anvin, Vinod Koul, Frank Li, Daniel Gomez, Sami Tolvanen,
	Aaron Tomlin, Alexander Potapenko, Marco Elver, Dmitry Vyukov,
	Andrew Morton, John Johansen, Paul Moore, James Morris,
	Serge E. Hallyn, Andy Shevchenko, Georgia Garcia, kvm, dmaengine,
	linux-modules, kasan-dev, linux-mm, apparmor,
	linux-security-module, linux-um, linux-acpi, openipmi-developer,
	qemu-devel, intel-gfx, dri-devel, linux-rdma, linux-media,
	linux-pci, linux-scsi, linux-pm, linuxppc-dev, linux-serial,
	linux-usb, usb-storage, virtualization, linux-kernel, linux-arch,
	netdev, linux-fsdevel, linux-hardening
In-Reply-To: <da358ae1-91b4-4a16-ac76-ffab99c230b9@suse.com>

On Mon, May 25, 2026 at 7:35 AM Petr Pavlu <petr.pavlu@suse.com> wrote:
>
> On 5/21/26 3:33 PM, Kees Cook wrote:
> > Using Coccinelle, rewrite every struct kernel_param_ops initializer that
> > sets .get into a DEFINE_KERNEL_PARAM_OPS-family macro invocation,
> > for example:
> >
> > @@
> > declarer name DEFINE_KERNEL_PARAM_OPS;
> > identifier OPS;
> > expression SET, GET;
> > @@
> > - const struct kernel_param_ops OPS = {
> > -       .set = SET,
> > -       .get = GET,
> > - };
> > + DEFINE_KERNEL_PARAM_OPS(OPS, SET, GET);
> >
> > Using the macro for initialization means future changes can manipulate
> > the struct layout and callback prototypes without having to change every
> > initializer.
>
> Nit: For consistency, I suggest also converting the few remaining
> kernel_param_ops instances that specify only .set and no .get, such as
> simdisk_param_ops_filename.
>
> --
> Thanks,
> Petr

for the dynamic-debug changes

Reviewed-by: Jim Cromie <jim.cromie@gmail.com>

^ permalink raw reply

* Re: [PATCH] cred: prevent slab cache merging for cred_jar
From: Mohammed EL Kadiri @ 2026-06-10 21:07 UTC (permalink / raw)
  To: Kees Cook
  Cc: Paul Moore, Serge Hallyn, Vlastimil Babka, linux-security-module,
	linux-hardening, linux-kernel
In-Reply-To: <202606101345.D1BDA8BBE2@keescook>

Hi Kees,

Thanks for the review!
Following Vlastimil and Jarkko's feedback on the key_jar patch, should
I send a v2 here as well with similar commit message modification:
removing CVE references, dropping the skbuff comparison, and framing
it as hardening?

Thanks,
Mohammed

On Wed, Jun 10, 2026 at 9:45 PM Kees Cook <kees@kernel.org> wrote:
>
> On Sat, Jun 06, 2026 at 03:25:58PM +0100, Mohammed EL Kadiri wrote:
> > The cred_jar slab cache holds struct cred objects, which contain
> > process credentials: uid, gid, euid, egid, and capability sets.
> > Overwriting any of these fields is sufficient for privilege escalation.
> >
> > On a default Ubuntu 6.17.0-23-generic system, cred_jar (named "cred"
> > in sysfs) has 2 aliases, meaning 2 unrelated object types share its
> > slab pages (object_size=184, objs_per_slab=42).
> >
> > Cross-cache heap exploitation relies on slab cache merging to achieve
> > type confusion between unrelated kernel objects. CVE-2022-29582
> > demonstrates this technique: an io_uring use-after-free is leveraged
> > across cache boundaries through page-level reallocation, ultimately
> > achieving root. struct cred is a primary target in this class of
> > attacks due to the direct privilege escalation that results from
> > corrupting any of its identity or capability fields.
> >
> > Add SLAB_NO_MERGE to ensure cred_jar receives dedicated slab pages,
> > so that freed credential slots can only be reallocated as struct cred
> > objects. The memory overhead is minimal: one struct cred exists per
> > task, and with 42 objects per slab page, the cost of dedicated pages
> > is negligible. There is zero performance impact on the allocation
> > hot path.
> >
> > This follows the precedent set by skbuff_head_cache (net/core/skbuff.c)
> > and key_jar (security/keys/key.c) which use SLAB_NO_MERGE for similar
> > isolation requirements.
> >
> > Signed-off-by: Mohammed EL Kadiri <med08elkadiri@gmail.com>
>
> Yes please. :)
>
> Reviewed-by: Kees Cook <kees@kernel.org>
>
> --
> Kees Cook

^ permalink raw reply

* Re: [PATCH] cred: prevent slab cache merging for cred_jar
From: Kees Cook @ 2026-06-10 22:11 UTC (permalink / raw)
  To: Mohammed EL Kadiri
  Cc: Paul Moore, Serge Hallyn, Vlastimil Babka, linux-security-module,
	linux-hardening, linux-kernel
In-Reply-To: <CAAMeuQSZdNiCaAMYOnQE3q-Y9325PAbwVsJmTmKSS9a=NG7wcQ@mail.gmail.com>

On Wed, Jun 10, 2026 at 10:07:24PM +0100, Mohammed EL Kadiri wrote:
> Hi Kees,
> 
> Thanks for the review!
> Following Vlastimil and Jarkko's feedback on the key_jar patch, should
> I send a v2 here as well with similar commit message modification:
> removing CVE references, dropping the skbuff comparison, and framing
> it as hardening?

It wouldn't hurt, yeah. I have that kind of already in my head while I
read these patches, but it would be better for other folks to see it
framed more accurately.

-Kees

> 
> Thanks,
> Mohammed
> 
> On Wed, Jun 10, 2026 at 9:45 PM Kees Cook <kees@kernel.org> wrote:
> >
> > On Sat, Jun 06, 2026 at 03:25:58PM +0100, Mohammed EL Kadiri wrote:
> > > The cred_jar slab cache holds struct cred objects, which contain
> > > process credentials: uid, gid, euid, egid, and capability sets.
> > > Overwriting any of these fields is sufficient for privilege escalation.
> > >
> > > On a default Ubuntu 6.17.0-23-generic system, cred_jar (named "cred"
> > > in sysfs) has 2 aliases, meaning 2 unrelated object types share its
> > > slab pages (object_size=184, objs_per_slab=42).
> > >
> > > Cross-cache heap exploitation relies on slab cache merging to achieve
> > > type confusion between unrelated kernel objects. CVE-2022-29582
> > > demonstrates this technique: an io_uring use-after-free is leveraged
> > > across cache boundaries through page-level reallocation, ultimately
> > > achieving root. struct cred is a primary target in this class of
> > > attacks due to the direct privilege escalation that results from
> > > corrupting any of its identity or capability fields.
> > >
> > > Add SLAB_NO_MERGE to ensure cred_jar receives dedicated slab pages,
> > > so that freed credential slots can only be reallocated as struct cred
> > > objects. The memory overhead is minimal: one struct cred exists per
> > > task, and with 42 objects per slab page, the cost of dedicated pages
> > > is negligible. There is zero performance impact on the allocation
> > > hot path.
> > >
> > > This follows the precedent set by skbuff_head_cache (net/core/skbuff.c)
> > > and key_jar (security/keys/key.c) which use SLAB_NO_MERGE for similar
> > > isolation requirements.
> > >
> > > Signed-off-by: Mohammed EL Kadiri <med08elkadiri@gmail.com>
> >
> > Yes please. :)
> >
> > Reviewed-by: Kees Cook <kees@kernel.org>
> >
> > --
> > Kees Cook

-- 
Kees Cook

^ permalink raw reply

* [PATCH bpf-next 0/5] Verify BPF signed loader at load time
From: Daniel Borkmann @ 2026-06-10 23:03 UTC (permalink / raw)
  To: ast
  Cc: kpsingh, James.Bottomley, paul, bboscaccy, memxor, torvalds, bpf,
	linux-security-module

The BPF signing scheme signs a light skeleton's loader program and lets
the loader vouch for everything else: bpftool bakes the SHA256 of the
metadata map into the loader's instructions, signs the instructions, and
the loader compares the (frozen, exclusive) map against that hash from
within BPF once it runs. The construction is sound as a trusted hash
chain, but the kernel itself never attests the metadata, and that split
has been the recurring objection from the LSM / integrity side since the
scheme was proposed.

This proposal closes both gaps by having the kernel verify the metadata
at BPF_PROG_LOAD time, before the LSM admission hook and before the
verifier, /without/ growing the UAPI. A signed loader binds its metadata
map(s) through the existing fd_array/fd_array_cnt, and exclusive maps
are already bound to the loader's digest via excl_prog_hash. When a
signature is present, the kernel collects the exclusive maps from the
fd_array and appends their frozen contents to the instructions before
PKCS#7 verification, so the signature covers ...

    insns || metadata_0 || metadata_1 || [...]

... in fd_array order. The in-loader hash check is dropped from the
gen_loader entirely: generated loaders carry no verification logic
anymore, and signing or verifying a skeleton becomes an ordinary CMS
operation over bytes that sit verbatim in the skeleton, reproducible
offline. A signed program is either BPF_SIG_UNSIGNED or BPF_SIG_VERIFIED
with nothing in between.

There is no new UAPI, we now have a single signature scheme, no LSM
code reaching into BPF internals, no new LSM hook, and unsigned loads
are completely unaffected. It is also less complex since the loader
does not need to deal with BTF, an extra kfunc, etc, as proposed in
an earlier series [0]. Tested against full BPF CI which came back
green. For more details and examples, see the documentation patch in
this series.

  [0] https://lore.kernel.org/bpf/20260522023234.3778588-1-kpsingh@kernel.org/

Daniel Borkmann (5):
  bpf: Verify signed loader metadata at load time
  libbpf: Drop in-loader metadata check for load-time verification
  bpftool: Cover loader metadata with the program signature
  selftests/bpf: Verify load-time signed loader metadata
  Documentation/bpf: Add BPF signing and enforcement doc

 Documentation/bpf/index.rst                   |   1 +
 Documentation/bpf/signing.rst                 | 537 ++++++++++++++++++
 kernel/bpf/syscall.c                          | 169 +++++-
 tools/bpf/bpftool/gen.c                       |   2 +
 tools/bpf/bpftool/sign.c                      |  15 +-
 tools/lib/bpf/bpf_gen_internal.h              |   1 -
 tools/lib/bpf/gen_loader.c                    |  76 +--
 tools/lib/bpf/skel_internal.h                 |  27 +-
 .../selftests/bpf/prog_tests/signed_loader.c  | 460 +++++++++------
 9 files changed, 994 insertions(+), 294 deletions(-)
 create mode 100644 Documentation/bpf/signing.rst

-- 
2.43.0


^ permalink raw reply

* [PATCH bpf-next 1/5] bpf: Verify signed loader metadata at load time
From: Daniel Borkmann @ 2026-06-10 23:03 UTC (permalink / raw)
  To: ast
  Cc: kpsingh, James.Bottomley, paul, bboscaccy, memxor, torvalds, bpf,
	linux-security-module
In-Reply-To: <20260610230329.727075-1-daniel@iogearbox.net>

A signed gen_loader program carries the programs, maps and relocations it
installs in a metadata array map. The loader instructions are covered by
the PKCS#7 signature, but the metadata map is not: Today the loader
compares the map contents from within BPF against a hash baked into its
(signed) instructions, using the kernel-cached map hash. The kernel itself
never actually attests that the metadata the loader installs is the
metadata that was signed.

This split is the core of the long-standing objection to the BPF signing
scheme from the LSM / integrity side: the integrity check of a light
skeleton only completes once the loader program runs, that is, after the
security_bpf_prog_load() hook, so at admission time an LSM observes a
program whose payload has not yet been verified [0]. Auditing the chain
link is also not a purely cryptographic operation: whoever signs or reviews
an lskel has to disassemble the loader's preamble to convince themselves
that the embedded hash check is present and correct [1][2]. Two acceptable
fixes were identified in those threads: Complete the integrity check
before the admission hook fires, or add a second hook that collects the
verification result after the loader ran [3]. Let's implement the former,
without growing the UAPI.

A signed loader binds its metadata map(s) through the existing fd_array,
and an exclusive map is already bound to a program digest (excl_prog_hash).
So when a signature is present, collect the exclusive maps from fd_array
and append their frozen contents to the instructions before verification:
the signature now covers insns || metadata_0 || metadata_1 || [...] in the
fd_array order, and verification completes in bpf_prog_load() before the
LSM admission hook and before the verifier runs.

A program is either BPF_SIG_UNSIGNED or BPF_SIG_VERIFIED, with nothing in
between. While collecting the fd_array maps, a non-exclusive map bound to
a signed program is rejected, so every map folded into the signature is
exclusive. A signed loader that fails to cover its metadata thus does not
load, and BPF_SIG_VERIFIED always means the instructions and every
exclusive map are authentic.

The maps must be frozen so the hashed bytes cannot change before the
loader runs; the map <-> program digest binding is enforced by the
verifier for every used map. Binding maps through fd_array_cnt makes the
verifier resolve and excl-check them (excl_prog_sha vs prog->digest)
before it would otherwise compute the digest, so compute prog->digest
up front in bpf_prog_load(), over the unmodified instructions the
signature covers, for a load that folds metadata.

Unsigned programs are not affected. Note, signed loaders generated by
older libbpf/bpftool versions need to be regenerated; some of the recent
fixes we've had on the signed loader side require the latter already to
close gaps.

Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
Link: https://lore.kernel.org/bpf/CAHC9VhSDkwGgPfrBUh7EgBKEJj_JjnY68c0YAmuuLT_i--GskQ@mail.gmail.com [0]
Link: https://lore.kernel.org/bpf/2f71d6c03698eb17d51f7247efde777627ee578a.camel@HansenPartnership.com [1]
Link: https://lore.kernel.org/lkml/ecf0521ed302db672672ebfbc670ecfba36a6e00.camel@HansenPartnership.com [2]
Link: https://lore.kernel.org/bpf/88703f00d5b7a779728451008626efa45e42db3d.camel@HansenPartnership.com [3]
---
 kernel/bpf/syscall.c | 164 +++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 157 insertions(+), 7 deletions(-)

diff --git a/kernel/bpf/syscall.c b/kernel/bpf/syscall.c
index d4188a992bd8..796e28e840d6 100644
--- a/kernel/bpf/syscall.c
+++ b/kernel/bpf/syscall.c
@@ -2895,13 +2895,81 @@ static enum bpf_sig_keyring bpf_classify_keyring(s32 keyring_id)
 	}
 }
 
+static void bpf_prog_put_excl_maps(struct bpf_map **maps, u32 cnt)
+{
+	u32 i;
+
+	if (!maps)
+		return;
+	for (i = 0; i < cnt; i++)
+		bpf_map_put(maps[i]);
+	kfree(maps);
+}
+
+static int bpf_prog_collect_excl_maps(union bpf_attr *attr, bool is_kernel,
+				      struct bpf_map ***mapsp, u32 *cntp)
+{
+	bpfptr_t fds = make_bpfptr(attr->fd_array, is_kernel);
+	struct bpf_map **maps;
+	u32 i, n = 0;
+	int fd, err;
+
+	*mapsp = NULL;
+	*cntp = 0;
+
+	if (!attr->fd_array || !attr->fd_array_cnt)
+		return 0;
+	/*
+	 * Stricter than the verifier, which dedups fd_array entries against
+	 * used_maps: every entry here is folded into the signed data
+	 * individually, so cap the raw count.
+	 */
+	if (attr->fd_array_cnt > MAX_USED_MAPS)
+		return -E2BIG;
+
+	maps = kcalloc(attr->fd_array_cnt, sizeof(*maps), GFP_KERNEL);
+	if (!maps)
+		return -ENOMEM;
+
+	for (i = 0; i < attr->fd_array_cnt; i++) {
+		struct bpf_map *map;
+
+		if (copy_from_bpfptr_offset(&fd, fds, i * sizeof(int),
+					    sizeof(int))) {
+			err = -EFAULT;
+			goto err_put;
+		}
+		map = bpf_map_get(fd);
+		if (IS_ERR(map)) {
+			err = PTR_ERR(map);
+			goto err_put;
+		}
+		if (!map->excl_prog_sha) {
+			bpf_map_put(map);
+			err = -EINVAL;
+			goto err_put;
+		}
+		maps[n++] = map;
+	}
+
+	*mapsp = maps;
+	*cntp = n;
+	return 0;
+err_put:
+	bpf_prog_put_excl_maps(maps, n);
+	return err;
+}
+
 static int bpf_prog_verify_signature(struct bpf_prog *prog, union bpf_attr *attr,
-				     bool is_kernel, s32 *keyring_serial)
+				     bool is_kernel, s32 *keyring_serial,
+				     struct bpf_map **excl_maps, u32 excl_cnt)
 {
 	bpfptr_t usig = make_bpfptr(attr->signature, is_kernel);
-	struct bpf_dynptr_kern sig_ptr, insns_ptr;
+	struct bpf_dynptr_kern sig_ptr, data_ptr;
 	struct bpf_key *key = NULL;
-	void *sig;
+	void *sig, *data = NULL;
+	u32 i, off, insns_sz;
+	u64 data_sz;
 	int err = 0;
 
 	/*
@@ -2925,20 +2993,78 @@ static int bpf_prog_verify_signature(struct bpf_prog *prog, union bpf_attr *attr
 		return PTR_ERR(sig);
 	}
 
+	insns_sz = prog->len * sizeof(struct bpf_insn);
+	data_sz = insns_sz;
+	for (i = 0; i < excl_cnt; i++) {
+		if (!READ_ONCE(excl_maps[i]->frozen) ||
+		    !excl_maps[i]->ops->map_direct_value_addr) {
+			err = -EPERM;
+			goto out;
+		}
+		data_sz += excl_maps[i]->value_size;
+	}
+
+	if (bpf_dynptr_check_size(data_sz)) {
+		err = -E2BIG;
+		goto out;
+	}
+	data = kvmalloc(data_sz, GFP_KERNEL);
+	if (!data) {
+		err = -ENOMEM;
+		goto out;
+	}
+	memcpy(data, prog->insnsi, insns_sz);
+	off = insns_sz;
+	for (i = 0; i < excl_cnt; i++) {
+		struct bpf_map *map = excl_maps[i];
+		u64 addr;
+
+		err = map->ops->map_direct_value_addr(map, &addr, 0);
+		if (err)
+			goto out;
+		memcpy(data + off, (void *)(unsigned long)addr, map->value_size);
+		off += map->value_size;
+	}
+
+	bpf_dynptr_init(&data_ptr, data, BPF_DYNPTR_TYPE_LOCAL, 0, data_sz);
 	bpf_dynptr_init(&sig_ptr, sig, BPF_DYNPTR_TYPE_LOCAL, 0,
 			attr->signature_size);
-	bpf_dynptr_init(&insns_ptr, prog->insnsi, BPF_DYNPTR_TYPE_LOCAL, 0,
-			prog->len * sizeof(struct bpf_insn));
 
-	err = bpf_verify_pkcs7_signature((struct bpf_dynptr *)&insns_ptr,
+	err = bpf_verify_pkcs7_signature((struct bpf_dynptr *)&data_ptr,
 					 (struct bpf_dynptr *)&sig_ptr, key);
 	if (!err)
 		*keyring_serial = bpf_key_serial(key);
+out:
+	kvfree(data);
 	bpf_key_put(key);
 	kvfree(sig);
 	return err;
 }
 
+static int bpf_prog_check_excl_used_maps(struct bpf_prog *prog,
+					 struct bpf_map **excl_maps, u32 excl_cnt)
+{
+	u32 i, j;
+
+	for (i = 0; i < prog->aux->used_map_cnt; i++) {
+		struct bpf_map *map = prog->aux->used_maps[i];
+		bool folded = false;
+
+		if (!map->excl_prog_sha)
+			continue;
+		for (j = 0; j < excl_cnt; j++) {
+			if (excl_maps[j] == map) {
+				folded = true;
+				break;
+			}
+		}
+		if (!folded)
+			return -EACCES;
+	}
+
+	return 0;
+}
+
 static int bpf_prog_mark_insn_arrays_ready(struct bpf_prog *prog)
 {
 	int err;
@@ -2968,8 +3094,10 @@ static int bpf_prog_load(union bpf_attr *attr, bpfptr_t uattr, struct bpf_log_at
 {
 	enum bpf_prog_type type = attr->prog_type;
 	struct bpf_prog *prog, *dst_prog = NULL;
+	struct bpf_map **excl_maps = NULL;
 	struct btf *attach_btf = NULL;
 	struct bpf_token *token = NULL;
+	u32 excl_cnt = 0;
 	bool bpf_cap;
 	int err;
 	char license[128];
@@ -3129,10 +3257,17 @@ static int bpf_prog_load(union bpf_attr *attr, bpfptr_t uattr, struct bpf_log_at
 	/* eBPF programs must be GPL compatible to use GPL-ed functions */
 	prog->gpl_compatible = license_is_gpl_compatible(license) ? 1 : 0;
 	if (attr->signature) {
+		err = bpf_prog_collect_excl_maps(attr, uattr.is_kernel,
+						 &excl_maps, &excl_cnt);
+		if (err)
+			goto free_prog;
+
 		err = bpf_prog_verify_signature(prog, attr, uattr.is_kernel,
-						&prog->aux->sig.keyring_serial);
+						&prog->aux->sig.keyring_serial,
+						excl_maps, excl_cnt);
 		if (err)
 			goto free_prog;
+
 		prog->aux->sig.keyring_type = bpf_classify_keyring(attr->keyring_id);
 		prog->aux->sig.verdict = BPF_SIG_VERIFIED;
 	} else {
@@ -3187,12 +3322,25 @@ static int bpf_prog_load(union bpf_attr *attr, bpfptr_t uattr, struct bpf_log_at
 	err = security_bpf_prog_load(prog, attr, token, uattr.is_kernel);
 	if (err)
 		goto free_prog;
+	if (excl_cnt) {
+		err = bpf_prog_calc_tag(prog);
+		if (err < 0)
+			goto free_prog;
+	}
 
 	/* run eBPF verifier */
 	err = bpf_check(&prog, attr, uattr, attr_log);
 	if (err < 0)
 		goto free_used_maps;
 
+	if (prog->aux->sig.verdict == BPF_SIG_VERIFIED) {
+		err = bpf_prog_check_excl_used_maps(prog, excl_maps, excl_cnt);
+		if (err < 0)
+			goto free_used_maps;
+	}
+	bpf_prog_put_excl_maps(excl_maps, excl_cnt);
+	excl_maps = NULL;
+
 	err = bpf_prog_mark_insn_arrays_ready(prog);
 	if (err < 0)
 		goto free_used_maps;
@@ -3225,6 +3373,7 @@ static int bpf_prog_load(union bpf_attr *attr, bpfptr_t uattr, struct bpf_log_at
 	return err;
 
 free_used_maps:
+	bpf_prog_put_excl_maps(excl_maps, excl_cnt);
 	/* In case we have subprogs, we need to wait for a grace
 	 * period before we can tear down JIT memory since symbols
 	 * are already exposed under kallsyms.
@@ -3233,6 +3382,7 @@ static int bpf_prog_load(union bpf_attr *attr, bpfptr_t uattr, struct bpf_log_at
 	return err;
 
 free_prog:
+	bpf_prog_put_excl_maps(excl_maps, excl_cnt);
 	free_uid(prog->aux->user);
 	if (prog->aux->attach_btf)
 		btf_put(prog->aux->attach_btf);
-- 
2.43.0


^ permalink raw reply related

* [PATCH bpf-next 2/5] libbpf: Drop in-loader metadata check for load-time verification
From: Daniel Borkmann @ 2026-06-10 23:03 UTC (permalink / raw)
  To: ast
  Cc: kpsingh, James.Bottomley, paul, bboscaccy, memxor, torvalds, bpf,
	linux-security-module
In-Reply-To: <20260610230329.727075-1-daniel@iogearbox.net>

The signed gen_loader used to police its own metadata map from within
BPF: emit_signature_match() read the kernel-cached map->sha[] back
through hardcoded struct bpf_map offsets and compared it against a hash
that compute_sha_update_offsets() baked into the signed instructions,
after a BPF_OBJ_GET_INFO_BY_FD round-trip to populate map->sha[].

The kernel now verifies the metadata at BPF_PROG_LOAD time by folding
the frozen contents of the loader's exclusive fd_array maps into the
signature, so the loader no longer checks anything itself. Generated
loaders thus carry no verification logic of their own anymore: Nothing
in the signing chain depends on emitted loader bytecode doing the right
thing.

On the loading side, skel_internal.h now sets fd_array_cnt for a signed
load so the kernel scans fd_array for the exclusive metadata map -
still frozen, as the kernel requires - and the BPF_OBJ_GET_INFO_BY_FD
round-trip to populate map->sha[] is gone. The struct bpf_map layout
BUILD_BUG_ON()s on the kernel side are removed as well: they only
pinned the ABI for the in-BPF read of map->sha[] that is no longer
needed. Note: gen_hash is retained; it still marks a loader as signed
so an untrusted host cannot re-dimension maps or override initial
values now covered by the signature.

Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
---
 kernel/bpf/syscall.c             |  5 ---
 tools/lib/bpf/bpf_gen_internal.h |  1 -
 tools/lib/bpf/gen_loader.c       | 76 +++-----------------------------
 tools/lib/bpf/skel_internal.h    | 27 +-----------
 4 files changed, 9 insertions(+), 100 deletions(-)

diff --git a/kernel/bpf/syscall.c b/kernel/bpf/syscall.c
index 796e28e840d6..9efb17ded696 100644
--- a/kernel/bpf/syscall.c
+++ b/kernel/bpf/syscall.c
@@ -1595,11 +1595,6 @@ static int map_create_alloc(union bpf_attr *attr, bpfptr_t uattr, struct bpf_ver
 			goto free_map;
 		}
 
-		/* See libbpf: emit_signature_match() */
-		BUILD_BUG_ON(offsetof(struct bpf_map, excl) != SHA256_DIGEST_SIZE);
-		BUILD_BUG_ON(!__same_type(map->excl, u32));
-		BUILD_BUG_ON(offsetof(struct bpf_map, sha)  != 0);
-		BUILD_BUG_ON(!__same_type(map->sha, u8[SHA256_DIGEST_SIZE]));
 		map->excl = 1;
 	} else if (attr->excl_prog_hash_size) {
 		bpf_log(log, "Invalid excl_prog_hash_size.\n");
diff --git a/tools/lib/bpf/bpf_gen_internal.h b/tools/lib/bpf/bpf_gen_internal.h
index 49af4260b8e6..042569187752 100644
--- a/tools/lib/bpf/bpf_gen_internal.h
+++ b/tools/lib/bpf/bpf_gen_internal.h
@@ -51,7 +51,6 @@ struct bpf_gen {
 	__u32 nr_ksyms;
 	int fd_array;
 	int nr_fd_array;
-	int hash_insn_offset[SHA256_DWORD_SIZE];
 };
 
 void bpf_gen__init(struct bpf_gen *gen, int log_level, int nr_progs, int nr_maps);
diff --git a/tools/lib/bpf/gen_loader.c b/tools/lib/bpf/gen_loader.c
index d79695f01c87..baed23850997 100644
--- a/tools/lib/bpf/gen_loader.c
+++ b/tools/lib/bpf/gen_loader.c
@@ -111,7 +111,6 @@ static void emit2(struct bpf_gen *gen, struct bpf_insn insn1, struct bpf_insn in
 
 static int add_data(struct bpf_gen *gen, const void *data, __u32 size);
 static void emit_sys_close_blob(struct bpf_gen *gen, int blob_off);
-static void emit_signature_match(struct bpf_gen *gen);
 
 void bpf_gen__init(struct bpf_gen *gen, int log_level, int nr_progs, int nr_maps)
 {
@@ -154,8 +153,6 @@ void bpf_gen__init(struct bpf_gen *gen, int log_level, int nr_progs, int nr_maps
 	/* R7 contains the error code from sys_bpf. Copy it into R0 and exit. */
 	emit(gen, BPF_MOV64_REG(BPF_REG_0, BPF_REG_7));
 	emit(gen, BPF_EXIT_INSN());
-	if (OPTS_GET(gen->opts, gen_hash, false))
-		emit_signature_match(gen);
 }
 
 static int add_data(struct bpf_gen *gen, const void *data, __u32 size)
@@ -377,8 +374,6 @@ static void emit_sys_close_blob(struct bpf_gen *gen, int blob_off)
 	__emit_sys_close(gen);
 }
 
-static void compute_sha_update_offsets(struct bpf_gen *gen);
-
 int bpf_gen__finish(struct bpf_gen *gen, int nr_progs, int nr_maps)
 {
 	int i;
@@ -408,9 +403,6 @@ int bpf_gen__finish(struct bpf_gen *gen, int nr_progs, int nr_maps)
 	if (!gen->error) {
 		struct gen_loader_opts *opts = gen->opts;
 
-		if (OPTS_GET(opts, gen_hash, false))
-			compute_sha_update_offsets(gen);
-
 		opts->insns = gen->insn_start;
 		opts->insns_sz = gen->insn_cur - gen->insn_start;
 		opts->data = gen->data_start;
@@ -460,22 +452,6 @@ void bpf_gen__free(struct bpf_gen *gen)
 	_val;							\
 })
 
-static void compute_sha_update_offsets(struct bpf_gen *gen)
-{
-	__u64 sha[SHA256_DWORD_SIZE];
-	__u64 sha_dw;
-	int i;
-
-	libbpf_sha256(gen->data_start, gen->data_cur - gen->data_start, (__u8 *)sha);
-	for (i = 0; i < SHA256_DWORD_SIZE; i++) {
-		struct bpf_insn *insn =
-			(struct bpf_insn *)(gen->insn_start + gen->hash_insn_offset[i]);
-		sha_dw = tgt_endian(sha[i]);
-		insn[0].imm = (__u32)sha_dw;
-		insn[1].imm = sha_dw >> 32;
-	}
-}
-
 void bpf_gen__load_btf(struct bpf_gen *gen, const void *btf_raw_data,
 		       __u32 btf_raw_size)
 {
@@ -557,8 +533,9 @@ void bpf_gen__map_create(struct bpf_gen *gen,
 	 * Conditionally update max_entries from the host-supplied loader
 	 * ctx. This sizes the map at runtime, but for a signed loader
 	 * (gen_hash) it would let an untrusted host re-dimension the
-	 * program's maps after emit_signature_match(), outside what the
-	 * signature attests to. Keep the signer-provided max_entries
+	 * program's maps, outside what the signature attests to: the
+	 * metadata blob is covered by the program signature and verified
+	 * by the kernel at load time. Keep the signer-provided max_entries
 	 * baked into the blob in that case.
 	 */
 	if (map_idx >= 0 && !OPTS_GET(gen->opts, gen_hash, false))
@@ -596,45 +573,6 @@ void bpf_gen__map_create(struct bpf_gen *gen,
 		emit_sys_close_stack(gen, stack_off(inner_map_fd));
 }
 
-static void emit_signature_match(struct bpf_gen *gen)
-{
-	__s64 off;
-	int i;
-
-	/*
-	 * Reject if the metadata map is not exclusive. Without exclusivity
-	 * the cached map->sha[] verified above can be stale: another BPF
-	 * program with map access could have mutated the contents between
-	 * BPF_OBJ_GET_INFO_BY_FD and loader execution.
-	 */
-	emit2(gen, BPF_LD_IMM64_RAW_FULL(BPF_REG_1, BPF_PSEUDO_MAP_IDX,
-					 0, 0, 0, 0));
-	emit(gen, BPF_LDX_MEM(BPF_W, BPF_REG_2, BPF_REG_1, SHA256_DIGEST_LENGTH));
-	off = -(gen->insn_cur - gen->insn_start - gen->cleanup_label) / 8 - 2;
-	if (is_simm16(off)) {
-		emit(gen, BPF_MOV64_IMM(BPF_REG_7, -EINVAL));
-		emit(gen, BPF_JMP_IMM(BPF_JNE, BPF_REG_2, 1, off));
-	} else {
-		gen->error = -ERANGE;
-	}
-
-	for (i = 0; i < SHA256_DWORD_SIZE; i++) {
-		emit2(gen, BPF_LD_IMM64_RAW_FULL(BPF_REG_1, BPF_PSEUDO_MAP_IDX,
-						 0, 0, 0, 0));
-		emit(gen, BPF_LDX_MEM(BPF_DW, BPF_REG_2, BPF_REG_1, i * sizeof(__u64)));
-		gen->hash_insn_offset[i] = gen->insn_cur - gen->insn_start;
-		emit2(gen, BPF_LD_IMM64_RAW_FULL(BPF_REG_3, 0, 0, 0, 0, 0));
-
-		off = -(gen->insn_cur - gen->insn_start - gen->cleanup_label) / 8 - 2;
-		if (is_simm16(off)) {
-			emit(gen, BPF_MOV64_IMM(BPF_REG_7, -EINVAL));
-			emit(gen, BPF_JMP_REG(BPF_JNE, BPF_REG_2, BPF_REG_3, off));
-		} else {
-			gen->error = -ERANGE;
-		}
-	}
-}
-
 void bpf_gen__record_attach_target(struct bpf_gen *gen, const char *attach_name,
 				   enum bpf_attach_type type)
 {
@@ -1211,10 +1149,10 @@ void bpf_gen__map_update_elem(struct bpf_gen *gen, int map_idx, void *pvalue,
 	 * }
 	 *
 	 * The runtime initial_value comes from the host-supplied loader
-	 * ctx and would overwrite the blob value after emit_signature_match()
-	 * has already validated map->sha[]. For a signed loader (gen_hash)
-	 * the attested blob value must be authoritative, so skip the override
-	 * and leave the hashed value in place.
+	 * ctx and would overwrite the blob value that the program signature
+	 * covers and the kernel verifies at load time. For a signed loader
+	 * (gen_hash) the attested blob value must be authoritative, so skip
+	 * the override and leave the signed value in place.
 	 */
 	if (!OPTS_GET(gen->opts, gen_hash, false)) {
 		emit(gen, BPF_LDX_MEM(BPF_DW, BPF_REG_3, BPF_REG_6,
diff --git a/tools/lib/bpf/skel_internal.h b/tools/lib/bpf/skel_internal.h
index 74503d358bc8..8555ab8af554 100644
--- a/tools/lib/bpf/skel_internal.h
+++ b/tools/lib/bpf/skel_internal.h
@@ -320,25 +320,6 @@ static inline int skel_link_create(int prog_fd, int target_fd,
 	return skel_sys_bpf(BPF_LINK_CREATE, &attr, attr_sz);
 }
 
-static inline int skel_obj_get_info_by_fd(int fd)
-{
-	const size_t attr_sz = offsetofend(union bpf_attr, info);
-	__u8 sha[SHA256_DIGEST_LENGTH];
-	struct bpf_map_info info;
-	__u32 info_len = sizeof(info);
-	union bpf_attr attr;
-
-	memset(&info, 0, sizeof(info));
-	info.hash = (long) &sha;
-	info.hash_size = SHA256_DIGEST_LENGTH;
-
-	memset(&attr, 0, attr_sz);
-	attr.info.bpf_fd = fd;
-	attr.info.info = (long) &info;
-	attr.info.info_len = info_len;
-	return skel_sys_bpf(BPF_OBJ_GET_INFO_BY_FD, &attr, attr_sz);
-}
-
 static inline int skel_map_freeze(int fd)
 {
 	const size_t attr_sz = offsetofend(union bpf_attr, map_fd);
@@ -384,12 +365,6 @@ static inline int bpf_load_and_run(struct bpf_load_and_run_opts *opts)
 		set_err;
 		goto out;
 	}
-	err = skel_obj_get_info_by_fd(map_fd);
-	if (err < 0) {
-		opts->errstr = "failed to fetch obj info";
-		set_err;
-		goto out;
-	}
 #endif
 
 	memset(&attr, 0, prog_load_attr_sz);
@@ -400,6 +375,8 @@ static inline int bpf_load_and_run(struct bpf_load_and_run_opts *opts)
 #ifndef __KERNEL__
 	attr.signature = (long) opts->signature;
 	attr.signature_size = opts->signature_sz;
+	if (opts->signature)
+		attr.fd_array_cnt = 1;
 #else
 	if (opts->signature || opts->signature_sz)
 		pr_warn("signatures are not supported from bpf_preload\n");
-- 
2.43.0


^ permalink raw reply related

* [PATCH bpf-next 3/5] bpftool: Cover loader metadata with the program signature
From: Daniel Borkmann @ 2026-06-10 23:03 UTC (permalink / raw)
  To: ast
  Cc: kpsingh, James.Bottomley, paul, bboscaccy, memxor, torvalds, bpf,
	linux-security-module
In-Reply-To: <20260610230329.727075-1-daniel@iogearbox.net>

bpftool_prog_sign() signed only the loader instructions. The metadata
blob the loader installs was left to an in-loader hash check, which
the kernel now performs at load time over insns || metadata.

Sign that same concatenation: pass the metadata blob (gen_loader_opts
data) through to bpftool_prog_sign() and feed insns || metadata to
CMS_final(). The excl_prog_hash stays a digest of the instructions
alone; it binds the metadata map to the loader and is matched against
prog->digest by the verifier, independent of what the signature covers.

The signed artifact is now plain data: both bytes the signature
covers are embedded verbatim in the generated skeleton, so signing
and verifying an lskel is an ordinary CMS operation that a signer or
auditor can perform (or reproduce) offline, without analyzing loader
bytecode to establish what the signature actually attests to [0].

Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
Link: https://lore.kernel.org/lkml/ecf0521ed302db672672ebfbc670ecfba36a6e00.camel@HansenPartnership.com [0]
---
 tools/bpf/bpftool/gen.c  |  2 ++
 tools/bpf/bpftool/sign.c | 15 ++++++++++++++-
 2 files changed, 16 insertions(+), 1 deletion(-)

diff --git a/tools/bpf/bpftool/gen.c b/tools/bpf/bpftool/gen.c
index 6ae7262ebe0c..a01d06d22d1a 100644
--- a/tools/bpf/bpftool/gen.c
+++ b/tools/bpf/bpftool/gen.c
@@ -793,6 +793,8 @@ static int gen_trace(struct bpf_object *obj, const char *obj_name, const char *h
 	if (sign_progs) {
 		sopts.insns = opts.insns;
 		sopts.insns_sz = opts.insns_sz;
+		sopts.data = opts.data;
+		sopts.data_sz = opts.data_sz;
 		sopts.excl_prog_hash = prog_sha;
 		sopts.excl_prog_hash_sz = sizeof(prog_sha);
 		sopts.signature = sig_buf;
diff --git a/tools/bpf/bpftool/sign.c b/tools/bpf/bpftool/sign.c
index f9b742f4bb10..4ce020a141dc 100644
--- a/tools/bpf/bpftool/sign.c
+++ b/tools/bpf/bpftool/sign.c
@@ -135,9 +135,21 @@ int bpftool_prog_sign(struct bpf_load_and_run_opts *opts)
 	CMS_ContentInfo *cms = NULL;
 	long actual_sig_len = 0;
 	X509 *x509 = NULL;
+	void *data = NULL;
+	size_t data_sz;
 	int err = 0;
 
-	bd_in = BIO_new_mem_buf(opts->insns, opts->insns_sz);
+	data_sz = (size_t)opts->insns_sz + opts->data_sz;
+	data = malloc(data_sz);
+	if (!data) {
+		err = -ENOMEM;
+		goto cleanup;
+	}
+	memcpy(data, opts->insns, opts->insns_sz);
+	if (opts->data_sz)
+		memcpy((char *)data + opts->insns_sz, opts->data, opts->data_sz);
+
+	bd_in = BIO_new_mem_buf(data, data_sz);
 	if (!bd_in) {
 		err = -ENOMEM;
 		goto cleanup;
@@ -212,6 +224,7 @@ int bpftool_prog_sign(struct bpf_load_and_run_opts *opts)
 	X509_free(x509);
 	EVP_PKEY_free(private_key);
 	BIO_free(bd_in);
+	free(data);
 	DISPLAY_OSSL_ERR(err < 0);
 	return err;
 }
-- 
2.43.0


^ permalink raw reply related

* [PATCH bpf-next 4/5] selftests/bpf: Verify load-time signed loader metadata
From: Daniel Borkmann @ 2026-06-10 23:03 UTC (permalink / raw)
  To: ast
  Cc: kpsingh, James.Bottomley, paul, bboscaccy, memxor, torvalds, bpf,
	linux-security-module
In-Reply-To: <20260610230329.727075-1-daniel@iogearbox.net>

The signed gen_loader no longer checks its metadata map from within
BPF; the kernel does it at BPF_PROG_LOAD by folding the loader's frozen
exclusive fd_array maps into the signature. Exercise that path end to
end. Extend with more test cases (e.g. map-less program, asserting the
LSM admission hook observes BPF_SIG_UNSIGNED and BPF_SIG_VERIFIED), and
retire the subtests that asserted the old in-loader check, which no
longer exists.

  # LDLIBS=-static PKG_CONFIG='pkg-config --static' ./vmtest.sh -- ./test_progs -t signed_loader
  [...]
  [    1.842848] clocksource: Switched to clocksource tsc
  #409/1   signed_loader/loadtime_no_map:OK
  #409/2   signed_loader/loadtime_with_map:OK
  #409/3   signed_loader/metadata_match:OK
  #409/4   signed_loader/signature_enforced:OK
  #409/5   signed_loader/signed_nonexcl_fd_array_rejected:OK
  #409/6   signed_loader/signature_too_large:OK
  #409/7   signed_loader/signature_bad_keyring:OK
  #409/8   signed_loader/metadata_ctx_max_entries_ignored:OK
  #409/9   signed_loader/metadata_ctx_initial_value_ignored:OK
  #409/10  signed_loader/signature_authenticates_insns:OK
  #409/11  signed_loader/hash_requires_frozen:OK
  #409/12  signed_loader/no_update_after_freeze:OK
  #409/13  signed_loader/freeze_writable_mmap:OK
  #409/14  signed_loader/no_writable_mmap_frozen:OK
  #409/15  signed_loader/map_hash_matches_libbpf:OK
  #409/16  signed_loader/map_hash_multi_element:OK
  #409/17  signed_loader/map_hash_bad_size:OK
  #409/18  signed_loader/map_hash_unsupported_type:OK
  #409/19  signed_loader/lsm_signature_verdict:OK
  #409     signed_loader:OK
  Summary: 1/19 PASSED, 0 SKIPPED, 0 FAILED

Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
---
 .../selftests/bpf/prog_tests/signed_loader.c  | 460 +++++++++++-------
 1 file changed, 274 insertions(+), 186 deletions(-)

diff --git a/tools/testing/selftests/bpf/prog_tests/signed_loader.c b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
index 5fc417e31fc6..8a6a6ea4e093 100644
--- a/tools/testing/selftests/bpf/prog_tests/signed_loader.c
+++ b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
@@ -19,8 +19,6 @@
 #include "test_signed_loader_data.skel.h"
 #include "test_signed_loader_lsm.skel.h"
 
-#define SIG_MATCH_INSNS 33 /* excl (5) + 4 * sha-dword (7) */
-
 enum {
 	BPF_SIG_UNSIGNED = 0,
 	BPF_SIG_VERIFIED,
@@ -35,7 +33,8 @@ enum {
 };
 
 static int load_loader(const void *insns, __u32 insns_sz, int map_fd,
-		       const void *sig, __u32 sig_sz, __s32 keyring_id)
+		       const void *sig, __u32 sig_sz, __s32 keyring_id,
+		       __u32 fd_array_cnt)
 {
 	union bpf_attr attr;
 	int fd;
@@ -52,6 +51,7 @@ static int load_loader(const void *insns, __u32 insns_sz, int map_fd,
 		attr.signature_size = sig_sz;
 		attr.keyring_id = keyring_id;
 	}
+	attr.fd_array_cnt = fd_array_cnt;
 	memcpy(attr.prog_name, "__loader.prog", sizeof("__loader.prog"));
 	fd = syscall(__NR_bpf, BPF_PROG_LOAD, &attr,
 		     offsetofend(union bpf_attr, keyring_id));
@@ -62,14 +62,12 @@ static int run_gen_loader(const void *insns, __u32 insns_sz,
 			  const void *data, __u32 data_sz,
 			  const void *excl, __u32 excl_sz,
 			  const void *sig, __u32 sig_sz,
-			  bool get_hash, void *ctx, __u32 ctx_sz, bool *loader_ran)
+			  void *ctx, __u32 ctx_sz, bool *loader_ran)
 {
 	LIBBPF_OPTS(bpf_map_create_opts, mopts,
 		    .excl_prog_hash = excl,
 		    .excl_prog_hash_size = excl_sz);
-	__u8 hbuf[SHA256_DIGEST_LENGTH];
-	struct bpf_map_info info;
-	__u32 ilen = sizeof(info), key = 0;
+	__u32 key = 0;
 	union bpf_attr attr;
 	int map_fd, prog_fd, ret;
 
@@ -87,15 +85,6 @@ static int run_gen_loader(const void *insns, __u32 insns_sz,
 		ret = -errno;
 		goto out_map;
 	}
-	if (get_hash) {
-		memset(&info, 0, sizeof(info));
-		info.hash = ptr_to_u64(hbuf);
-		info.hash_size = sizeof(hbuf);
-		if (bpf_map_get_info_by_fd(map_fd, &info, &ilen)) {
-			ret = -errno;
-			goto out_map;
-		}
-	}
 
 	memset(&attr, 0, sizeof(attr));
 	attr.prog_type = BPF_PROG_TYPE_SYSCALL;
@@ -108,6 +97,7 @@ static int run_gen_loader(const void *insns, __u32 insns_sz,
 		attr.signature = ptr_to_u64(sig);
 		attr.signature_size = sig_sz;
 		attr.keyring_id = KEY_SPEC_SESSION_KEYRING;
+		attr.fd_array_cnt = 1;
 	}
 	memcpy(attr.prog_name, "__loader.prog", sizeof("__loader.prog"));
 	prog_fd = syscall(__NR_bpf, BPF_PROG_LOAD, &attr,
@@ -236,79 +226,6 @@ static int sign_buf(const char *dir, const void *buf, __u32 len,
 	return ret;
 }
 
-static void check_sig_match_shape(const struct bpf_insn *in, int n)
-{
-	int a = -1, cleanup = -1, i, base, t, br[5], nb = 0;
-
-	/* BPF_PSEUDO_MAP_IDX (the struct bpf_map * form) is used only here. */
-	for (i = 0; i + 1 < n; i++) {
-		if (in[i].code == (BPF_LD | BPF_IMM | BPF_DW) &&
-		    in[i].src_reg == BPF_PSEUDO_MAP_IDX) {
-			a = i;
-			break;
-		}
-	}
-	if (!ASSERT_GE(a, 0, "emit_signature_match present"))
-		return;
-	if (!ASSERT_LE(a + SIG_MATCH_INSNS, n, "block fits in program"))
-		return;
-
-	/* excl check: r2 = *(u32 *)(map + 32); if r2 != 1 goto cleanup */
-	ASSERT_EQ(in[a + 2].code, (BPF_LDX | BPF_MEM | BPF_W), "excl load width");
-	ASSERT_EQ(in[a + 2].off, SHA256_DIGEST_LENGTH, "excl field offset");
-	ASSERT_EQ(in[a + 4].code, (BPF_JMP | BPF_JNE | BPF_K), "excl branch op");
-	ASSERT_EQ(in[a + 4].imm, 1, "excl compared to 1");
-	br[nb++] = a + 4;
-
-	/* 4 sha-dword checks: r2 = *(u64 *)(map + i*8); if r2 != r3 goto cleanup */
-	for (i = 0; i < 4; i++) {
-		base = a + 5 + i * 7;
-		ASSERT_EQ(in[base + 2].code, (BPF_LDX | BPF_MEM | BPF_DW), "sha load width");
-		ASSERT_EQ(in[base + 2].off, i * 8, "sha dword offset");
-		ASSERT_EQ(in[base + 3].code, (BPF_LD | BPF_IMM | BPF_DW), "sha imm64 (H_meta)");
-		ASSERT_EQ(in[base + 6].code, (BPF_JMP | BPF_JNE | BPF_X), "sha branch op");
-		br[nb++] = base + 6;
-	}
-
-	/*
-	 * Locate the real cleanup label so we can pin the exact jump target,
-	 * not just "some backward label". bpf_gen__init() emits the cleanup
-	 * block as a prog-fd close loop whose first instruction is the label
-	 * every error branch jumps to.
-	 */
-	for (i = 0; i + 2 < a; i++) {
-		if (in[i].code == (BPF_LDX | BPF_MEM | BPF_W) &&
-		    in[i].dst_reg == BPF_REG_1 && in[i].src_reg == BPF_REG_10 &&
-		    in[i + 1].code == (BPF_JMP | BPF_JSLE | BPF_K) &&
-		    in[i + 1].dst_reg == BPF_REG_1 && in[i + 1].imm == 0 &&
-		    in[i + 1].off == 1 &&
-		    in[i + 2].code == (BPF_JMP | BPF_CALL) &&
-		    in[i + 2].imm == BPF_FUNC_sys_close) {
-			cleanup = i;
-			break;
-		}
-	}
-	if (!ASSERT_GE(cleanup, 0, "cleanup label located"))
-		return;
-	for (i = 0; i < nb; i++) {
-		t = br[i] + 1 + in[br[i]].off;
-		ASSERT_EQ(t, cleanup, "sig-match lands on cleanup");
-	}
-	/*
-	 * Same invariant for every other cleanup-bound jump in the program:
-	 * emit_check_err() is the only source of "if (r7 < 0) goto cleanup",
-	 * so each of those must also resolve exactly to cleanup.
-	 */
-	for (i = 0, t = 0; i < n; i++) {
-		if (in[i].code != (BPF_JMP | BPF_JSLT | BPF_K) ||
-		    in[i].dst_reg != BPF_REG_7 || in[i].imm != 0 || in[i].off >= 0)
-			continue;
-		ASSERT_EQ(i + 1 + in[i].off, cleanup, "err-check lands on cleanup");
-		t++;
-	}
-	ASSERT_GT(t, 0, "found emit_check_err jumps");
-}
-
 struct gen_loader_fixture {
 	struct test_signed_loader *skel;
 	struct gen_loader_opts gopts;
@@ -372,16 +289,6 @@ static void gen_loader_fixture_fini(struct gen_loader_fixture *f)
 	test_signed_loader__destroy(f->skel);
 }
 
-static void metadata_check_shape(void)
-{
-	struct gen_loader_fixture f;
-
-	if (gen_loader_fixture_init(&f) == 0)
-		check_sig_match_shape((const struct bpf_insn *)f.gopts.insns,
-				      f.gopts.insns_sz / sizeof(struct bpf_insn));
-	gen_loader_fixture_fini(&f);
-}
-
 static void metadata_match(void)
 {
 	struct gen_loader_fixture f;
@@ -391,94 +298,58 @@ static void metadata_match(void)
 	if (gen_loader_fixture_init(&f) == 0) {
 		r = run_gen_loader(f.gopts.insns, f.gopts.insns_sz, f.blob,
 				   f.data_sz, f.excl, sizeof(f.excl), NULL, 0,
-				   true, f.ctx, f.ctx_sz, &ran);
+				   f.ctx, f.ctx_sz, &ran);
 		ASSERT_TRUE(ran, "loader ran");
 		ASSERT_EQ(r, 0, "honest loader retval");
 	}
 	gen_loader_fixture_fini(&f);
 }
 
-static void metadata_sha_mismatch(void)
-{
-	struct gen_loader_fixture f;
-	bool ran;
-	int r;
-
-	if (gen_loader_fixture_init(&f) == 0) {
-		/*
-		 * blob[0] lives in the loader's fd_array scratch (first add_data in
-		 * bpf_gen__init); a 0-map program never reads it, so flipping it
-		 * changes only map->sha. The metadata check is the only thing that
-		 * can notice -> isolates emit_signature_match.
-		 */
-		f.blob[0] ^= 0xff;
-		r = run_gen_loader(f.gopts.insns, f.gopts.insns_sz, f.blob,
-				   f.data_sz, f.excl, sizeof(f.excl), NULL, 0,
-				   true, f.ctx, f.ctx_sz, &ran);
-		ASSERT_TRUE(ran, "loader ran");
-		ASSERT_EQ(r, -EINVAL, "tampered blob rejected by emit_signature_match");
-	}
-	gen_loader_fixture_fini(&f);
-}
-
-static void metadata_not_exclusive(void)
-{
-	struct gen_loader_fixture f;
-	bool ran;
-	int r;
-
-	if (gen_loader_fixture_init(&f) == 0) {
-		/*
-		 * Correct blob but a non-exclusive metadata map: the verifier does
-		 * not reject (excl_prog_sha unset), so the runtime map->excl == 1
-		 * check in the loader must.
-		 */
-		r = run_gen_loader(f.gopts.insns, f.gopts.insns_sz, f.blob,
-				   f.data_sz, NULL, 0, NULL, 0, true, f.ctx,
-				   f.ctx_sz, &ran);
-		ASSERT_TRUE(ran, "loader ran");
-		ASSERT_EQ(r, -EINVAL, "non-exclusive metadata map rejected");
-	}
-	gen_loader_fixture_fini(&f);
-}
-
-static void metadata_hash_not_computed(void)
+static void signature_enforced(void)
 {
+	static const __u8 junk[64] = { 0x30, 0x42, 0x13, 0x37, };
 	struct gen_loader_fixture f;
-	bool ran;
-	int r;
+	int fd;
 
 	if (gen_loader_fixture_init(&f) == 0) {
 		/*
-		 * Correct, exclusive, frozen map, but its hash was never computed
-		 * (no OBJ_GET_INFO_BY_FD), so map->sha stays zero. The loader must
-		 * fail closed rather than treat an unset hash as a match.
+		 * A present-but-invalid signature (the cert bytes are not a
+		 * PKCS#7 signature) must be rejected at load: the signature
+		 * path is honored, not ignored. (The valid path is covered by
+		 * the signed lskels.)
 		 */
-		r = run_gen_loader(f.gopts.insns, f.gopts.insns_sz, f.blob,
-				   f.data_sz, f.excl, sizeof(f.excl), NULL, 0,
-				   false, f.ctx, f.ctx_sz, &ran);
-		ASSERT_TRUE(ran, "loader ran");
-		ASSERT_EQ(r, -EINVAL, "uncomputed metadata hash rejected");
+		fd = load_loader(f.gopts.insns, f.gopts.insns_sz, -1, junk,
+				 sizeof(junk), KEY_SPEC_SESSION_KEYRING, 0);
+		ASSERT_LT(fd, 0, "invalid signature rejected at load");
 	}
 	gen_loader_fixture_fini(&f);
 }
 
-static void signature_enforced(void)
+static void signed_nonexcl_fd_array_rejected(void)
 {
 	static const __u8 junk[64] = { 0x30, 0x42, 0x13, 0x37, };
 	struct gen_loader_fixture f;
-	int fd;
+	int map_fd, fd;
 
 	if (gen_loader_fixture_init(&f) == 0) {
 		/*
-		 * A present-but-invalid signature (the cert bytes are not a
-		 * PKCS#7 signature) must be rejected at load: the signature
-		 * path is honored, not ignored. (The valid path is covered by
-		 * the signed lskels.)
+		 * A signed program may only bind exclusive maps through fd_array
+		 * (their contents are folded into the signature). Binding a
+		 * non-exclusive map is rejected, before the signature is even
+		 * examined.
 		 */
-		fd = load_loader(f.gopts.insns, f.gopts.insns_sz, -1, junk,
-				 sizeof(junk), KEY_SPEC_SESSION_KEYRING);
-		ASSERT_LT(fd, 0, "invalid signature rejected at load");
+		map_fd = bpf_map_create(BPF_MAP_TYPE_ARRAY, "nonexcl", 4,
+					f.data_sz, 1, NULL);
+		if (ASSERT_OK_FD(map_fd, "nonexcl_map")) {
+			fd = load_loader(f.gopts.insns, f.gopts.insns_sz, map_fd,
+					 junk, sizeof(junk),
+					 KEY_SPEC_SESSION_KEYRING, 1);
+			ASSERT_EQ(fd, -EINVAL,
+				  "non-exclusive map in signed fd_array rejected");
+			if (fd >= 0)
+				close(fd);
+			close(map_fd);
+		}
 	}
 	gen_loader_fixture_fini(&f);
 }
@@ -495,7 +366,7 @@ static void signature_too_large(void)
 		 * is rejected before the buffer is read.
 		 */
 		fd = load_loader(f.gopts.insns, f.gopts.insns_sz, -1, junk,
-				 64 << 20, KEY_SPEC_SESSION_KEYRING);
+				 64 << 20, KEY_SPEC_SESSION_KEYRING, 0);
 		ASSERT_EQ(fd, -EINVAL, "oversized signature rejected");
 	}
 	gen_loader_fixture_fini(&f);
@@ -515,7 +386,7 @@ static void signature_bad_keyring(void)
 		 * large positive serial takes the user-keyring path and won't exist.
 		 */
 		fd = load_loader(f.gopts.insns, f.gopts.insns_sz, -1, junk,
-				 sizeof(junk), INT_MAX);
+				 sizeof(junk), INT_MAX, 0);
 		ASSERT_EQ(fd, -EINVAL, "signature with bad keyring_id rejected");
 	}
 	gen_loader_fixture_fini(&f);
@@ -575,7 +446,7 @@ static void metadata_ctx_max_entries_ignored(void)
 	memcpy(blob, gopts.data, data_sz);
 
 	r = run_gen_loader(gopts.insns, gopts.insns_sz, blob, data_sz,
-			   excl, sizeof(excl), NULL, 0, true, ctx, ctx_sz, &ran);
+			   excl, sizeof(excl), NULL, 0, ctx, ctx_sz, &ran);
 	if (!ASSERT_TRUE(ran, "loader ran") ||
 	    !ASSERT_EQ(r, 0, "loader retval"))
 		goto free_blob;
@@ -661,7 +532,7 @@ static void metadata_ctx_initial_value_ignored(void)
 	memcpy(blob, gopts.data, data_sz);
 
 	r = run_gen_loader(gopts.insns, gopts.insns_sz, blob, data_sz,
-			   excl, sizeof(excl), NULL, 0, true, ctx, ctx_sz, &ran);
+			   excl, sizeof(excl), NULL, 0, ctx, ctx_sz, &ran);
 	if (!ASSERT_TRUE(ran, "loader ran") ||
 	    !ASSERT_EQ(r, 0, "loader retval"))
 		goto free_blob;
@@ -714,6 +585,7 @@ static void signature_authenticates_insns(void)
 	__u8 excl[SHA256_DIGEST_LENGTH], sig[8192];
 	__u32 sig_sz = sizeof(sig), insns_sz, data_sz, ctx_sz;
 	unsigned char *insns = NULL, *tampered = NULL, *blob = NULL;
+	unsigned char *signbuf = NULL;
 	int nr_maps = 0, nr_progs = 0, r;
 	struct bpf_program *p;
 	struct bpf_map *m;
@@ -760,13 +632,19 @@ static void signature_authenticates_insns(void)
 	memcpy(blob, gopts.data, data_sz);
 	libbpf_sha256(insns, insns_sz, excl);
 
-	if (!ASSERT_OK(sign_buf(dir, insns, insns_sz, sig, &sig_sz), "sign-file"))
+	signbuf = malloc((size_t)insns_sz + data_sz);
+	if (!ASSERT_OK_PTR(signbuf, "signbuf"))
+		goto cleanup;
+	memcpy(signbuf, insns, insns_sz);
+	memcpy(signbuf + insns_sz, blob, data_sz);
+	if (!ASSERT_OK(sign_buf(dir, signbuf, insns_sz + data_sz, sig, &sig_sz),
+		       "sign-file"))
 		goto cleanup;
 
 	memset(ctx, 0, ctx_sz);
 	((struct bpf_loader_ctx *)ctx)->sz = ctx_sz;
 	r = run_gen_loader(insns, insns_sz, blob, data_sz, excl, sizeof(excl),
-			   sig, sig_sz, true, ctx, ctx_sz, &ran);
+			   sig, sig_sz, ctx, ctx_sz, &ran);
 	ASSERT_TRUE(ran, "valid signature: loader loaded and ran");
 	ASSERT_EQ(r, 0, "valid signature accepted");
 	close_loader_ctx_fds(ctx, nr_maps, nr_progs);
@@ -776,13 +654,14 @@ static void signature_authenticates_insns(void)
 	memset(ctx, 0, ctx_sz);
 	((struct bpf_loader_ctx *)ctx)->sz = ctx_sz;
 	r = run_gen_loader(tampered, insns_sz, blob, data_sz, excl, sizeof(excl),
-			   sig, sig_sz, true, ctx, ctx_sz, &ran);
+			   sig, sig_sz, ctx, ctx_sz, &ran);
 	ASSERT_FALSE(ran, "tampered loader rejected before run");
 	ASSERT_EQ(r, -EKEYREJECTED, "signature is bound to the instructions");
 cleanup:
 	free(insns);
 	free(tampered);
 	free(blob);
+	free(signbuf);
 	free(ctx);
 	test_signed_loader__destroy(skel);
 	run_setup("cleanup", dir);
@@ -1007,10 +886,11 @@ static void lsm_signature_verdict(void)
 {
 	char dir_tmpl[] = "/tmp/signed_loader_lsmXXXXXX", *dir = NULL;
 	struct test_signed_loader_lsm *lsm = NULL;
+	__u32 sig_sz = 8192, msig_sz = 8192;
 	int map_fd = -1, prog_fd = -1;
 	bool have_fixture = false;
 	struct gen_loader_fixture f;
-	__u32 sig_sz = 8192;
+	unsigned char *buf;
 	__s32 ses_serial;
 	__u8 sig[8192];
 
@@ -1029,7 +909,7 @@ static void lsm_signature_verdict(void)
 	if (!ASSERT_OK_FD(map_fd, "meta_map_unsigned"))
 		goto out;
 	lsm->bss->seen = 0;
-	prog_fd = load_loader(f.gopts.insns, f.gopts.insns_sz, map_fd, NULL, 0, 0);
+	prog_fd = load_loader(f.gopts.insns, f.gopts.insns_sz, map_fd, NULL, 0, 0, 0);
 	close(map_fd);
 	map_fd = -1;
 	if (!ASSERT_OK_FD(prog_fd, "unsigned loader load"))
@@ -1062,22 +942,51 @@ static void lsm_signature_verdict(void)
 		goto out;
 	lsm->bss->seen = 0;
 	prog_fd = load_loader(f.gopts.insns, f.gopts.insns_sz, map_fd, sig,
-			      sig_sz, KEY_SPEC_SESSION_KEYRING);
+			      sig_sz, KEY_SPEC_SESSION_KEYRING, 0);
 	close(map_fd);
 	map_fd = -1;
-	if (!ASSERT_OK_FD(prog_fd, "signed loader load"))
-		goto out;
-	close(prog_fd);
+	ASSERT_EQ(prog_fd, -EACCES, "unfolded metadata rejected");
+	if (prog_fd >= 0)
+		close(prog_fd);
 	prog_fd = -1;
 
 	ses_serial = syscall(__NR_keyctl, KEYCTL_GET_KEYRING_ID,
 			     KEY_SPEC_SESSION_KEYRING, 0);
 	ASSERT_EQ(lsm->bss->seen, 1, "signed: one observed load");
-	ASSERT_EQ(lsm->bss->sig_verdict, BPF_SIG_VERIFIED, "signed verdict");
+	ASSERT_EQ(lsm->bss->sig_verdict, BPF_SIG_VERIFIED,
+		  "admission saw a valid signature");
 	ASSERT_EQ(lsm->bss->sig_keyring_type, BPF_SIG_KEYRING_USER, "signed keyring type");
 	ASSERT_GT(ses_serial, 0, "session keyring serial resolved");
 	ASSERT_EQ(lsm->bss->sig_keyring_serial, ses_serial,
 		  "signed: validated against session keyring");
+
+	buf = malloc((size_t)f.gopts.insns_sz + f.data_sz);
+	if (!ASSERT_OK_PTR(buf, "meta_signbuf"))
+		goto out;
+	memcpy(buf, f.gopts.insns, f.gopts.insns_sz);
+	memcpy(buf + f.gopts.insns_sz, f.blob, f.data_sz);
+	if (!ASSERT_OK(sign_buf(dir, buf, f.gopts.insns_sz + f.data_sz,
+				sig, &msig_sz), "sign insns||metadata")) {
+		free(buf);
+		goto out;
+	}
+	free(buf);
+
+	map_fd = setup_meta_map(&f);
+	if (!ASSERT_OK_FD(map_fd, "meta_map_bound"))
+		goto out;
+	lsm->bss->seen = 0;
+	prog_fd = load_loader(f.gopts.insns, f.gopts.insns_sz, map_fd, sig,
+			      msig_sz, KEY_SPEC_SESSION_KEYRING, 1);
+	close(map_fd);
+	map_fd = -1;
+	if (!ASSERT_OK_FD(prog_fd, "metadata-bound loader load"))
+		goto out;
+	close(prog_fd);
+	prog_fd = -1;
+	ASSERT_EQ(lsm->bss->seen, 1, "metadata: one observed load");
+	ASSERT_EQ(lsm->bss->sig_verdict, BPF_SIG_VERIFIED,
+		  "metadata-bound verdict");
 out:
 	if (map_fd >= 0)
 		close(map_fd);
@@ -1090,20 +999,199 @@ static void lsm_signature_verdict(void)
 	test_signed_loader_lsm__destroy(lsm);
 }
 
+/*
+ * Load-time metadata verification: the kernel folds the frozen metadata map
+ * into the signature (insns || metadata) and checks it at BPF_PROG_LOAD via
+ * fd_array_cnt, rather than the loader checking from within BPF. Sign that
+ * concatenation, hand the kernel the map, and confirm the signed loader loads,
+ * runs, and installs its target.
+ */
+static int loadtime_drive(const char *dir, const void *insns, __u32 insns_sz,
+			  const void *data, __u32 data_sz, const __u8 *excl,
+			  void *ctx, __u32 ctx_sz, int *load_ret, bool *ran)
+{
+	LIBBPF_OPTS(bpf_map_create_opts, mopts,
+		    .excl_prog_hash = excl,
+		    .excl_prog_hash_size = SHA256_DIGEST_LENGTH);
+	__u32 sig_sz = 8192, key = 0;
+	unsigned char *buf = NULL;
+	int map_fd, prog_fd, ret = 0;
+	union bpf_attr attr;
+	__u8 sig[8192];
+
+	*ran = false;
+	*load_ret = 0;
+
+	/*
+	 * Metadata map, bound to the loader digest and frozen, exactly as
+	 * skel_internal.h's bpf_load_and_run() sets it up.
+	 */
+	map_fd = bpf_map_create(BPF_MAP_TYPE_ARRAY, "__loader.map", 4,
+				data_sz, 1, &mopts);
+	if (map_fd < 0)
+		return -errno;
+	if (bpf_map_update_elem(map_fd, &key, data, 0) || bpf_map_freeze(map_fd)) {
+		ret = -errno;
+		goto out_map;
+	}
+
+	/* Sign insns || metadata, the same bytes the kernel reconstructs. */
+	buf = malloc((size_t)insns_sz + data_sz);
+	if (!buf) {
+		ret = -ENOMEM;
+		goto out_map;
+	}
+	memcpy(buf, insns, insns_sz);
+	memcpy(buf + insns_sz, data, data_sz);
+	ret = sign_buf(dir, buf, insns_sz + data_sz, sig, &sig_sz);
+	if (ret)
+		goto out_map;
+
+	memset(&attr, 0, sizeof(attr));
+	attr.prog_type = BPF_PROG_TYPE_SYSCALL;
+	attr.insns = ptr_to_u64(insns);
+	attr.insn_cnt = insns_sz / sizeof(struct bpf_insn);
+	attr.license = ptr_to_u64("Dual BSD/GPL");
+	attr.prog_flags = BPF_F_SLEEPABLE;
+	attr.fd_array = ptr_to_u64(&map_fd);
+	attr.signature = ptr_to_u64(sig);
+	attr.signature_size = sig_sz;
+	attr.keyring_id = KEY_SPEC_SESSION_KEYRING;
+	attr.fd_array_cnt = 1;
+	memcpy(attr.prog_name, "__loader.prog", sizeof("__loader.prog"));
+	prog_fd = syscall(__NR_bpf, BPF_PROG_LOAD, &attr,
+			  offsetofend(union bpf_attr, keyring_id));
+	if (prog_fd < 0) {
+		*load_ret = -errno;
+		ret = -errno;
+		goto out_map;
+	}
+
+	memset(&attr, 0, sizeof(attr));
+	attr.test.prog_fd = prog_fd;
+	attr.test.ctx_in = ptr_to_u64(ctx);
+	attr.test.ctx_size_in = ctx_sz;
+	if (syscall(__NR_bpf, BPF_PROG_RUN, &attr,
+		    offsetofend(union bpf_attr, test)) < 0) {
+		ret = -errno;
+		goto out_prog;
+	}
+	*ran = true;
+	ret = (int)attr.test.retval;
+out_prog:
+	close(prog_fd);
+out_map:
+	free(buf);
+	close(map_fd);
+	return ret;
+}
+
+static void loadtime_verify(struct bpf_object *obj, int expect_maps)
+{
+	LIBBPF_OPTS(gen_loader_opts, gopts, .gen_hash = true);
+	char dir_tmpl[] = "/tmp/signed_loader_ltXXXXXX", *dir = NULL;
+	int nr_maps = 0, nr_progs = 0, load_ret = 0, r;
+	__u8 excl[SHA256_DIGEST_LENGTH];
+	struct bpf_prog_desc *pd;
+	struct bpf_map_desc *md;
+	unsigned char *blob = NULL;
+	struct bpf_program *p;
+	struct bpf_map *m;
+	__u32 ctx_sz, data_sz;
+	void *ctx = NULL;
+	bool ran = false;
+
+	syscall(__NR_request_key, "keyring", "_uid.0", NULL,
+		KEY_SPEC_SESSION_KEYRING);
+	dir = mkdtemp(dir_tmpl);
+	if (!ASSERT_OK_PTR(dir, "mkdtemp"))
+		return;
+	if (!ASSERT_OK(run_setup("setup", dir), "verify_sig_setup")) {
+		rmdir(dir);
+		return;
+	}
+
+	if (!ASSERT_OK(bpf_object__gen_loader(obj, &gopts), "gen_loader"))
+		goto out;
+	if (!ASSERT_OK(bpf_object__load(obj), "gen_load"))
+		goto out;
+
+	bpf_object__for_each_program(p, obj)
+		nr_progs++;
+	bpf_object__for_each_map(m, obj)
+		nr_maps++;
+	if (!ASSERT_EQ(nr_maps, expect_maps, "fixture map count"))
+		goto out;
+
+	ctx_sz = sizeof(struct bpf_loader_ctx) +
+		 nr_maps * sizeof(struct bpf_map_desc) +
+		 nr_progs * sizeof(struct bpf_prog_desc);
+	ctx = calloc(1, ctx_sz);
+	if (!ASSERT_OK_PTR(ctx, "ctx_alloc"))
+		goto out;
+	((struct bpf_loader_ctx *)ctx)->sz = ctx_sz;
+
+	data_sz = gopts.data_sz;
+	blob = malloc(data_sz);
+	if (!ASSERT_OK_PTR(blob, "blob_alloc"))
+		goto out;
+	memcpy(blob, gopts.data, data_sz);
+
+	/* excl_prog_hash = SHA256(loader insns) == the loader's prog->digest. */
+	libbpf_sha256(gopts.insns, gopts.insns_sz, excl);
+
+	r = loadtime_drive(dir, gopts.insns, gopts.insns_sz, blob, data_sz,
+			   excl, ctx, ctx_sz, &load_ret, &ran);
+	ASSERT_OK(load_ret, "signed loader loaded (insns || metadata)");
+	ASSERT_TRUE(ran, "loader ran");
+	ASSERT_EQ(r, 0, "loader installed its target");
+
+	md = (struct bpf_map_desc *)((char *)ctx + sizeof(struct bpf_loader_ctx));
+	pd = (struct bpf_prog_desc *)(md + nr_maps);
+	ASSERT_GT(pd[0].prog_fd, 0, "target program installed");
+	if (nr_maps)
+		ASSERT_GT(md[0].map_fd, 0, "target map installed");
+
+	close_loader_ctx_fds(ctx, nr_maps, nr_progs);
+out:
+	free(blob);
+	free(ctx);
+	if (dir)
+		run_setup("cleanup", dir);
+}
+
+static void loadtime_no_map(void)
+{
+	struct test_signed_loader *skel = test_signed_loader__open();
+
+	if (!ASSERT_OK_PTR(skel, "skel_open"))
+		return;
+	loadtime_verify(skel->obj, 0);
+	test_signed_loader__destroy(skel);
+}
+
+static void loadtime_with_map(void)
+{
+	struct test_signed_loader_map *skel = test_signed_loader_map__open();
+
+	if (!ASSERT_OK_PTR(skel, "skel_open"))
+		return;
+	loadtime_verify(skel->obj, 1);
+	test_signed_loader_map__destroy(skel);
+}
+
 void test_signed_loader(void)
 {
-	if (test__start_subtest("metadata_check_shape"))
-		metadata_check_shape();
+	if (test__start_subtest("loadtime_no_map"))
+		loadtime_no_map();
+	if (test__start_subtest("loadtime_with_map"))
+		loadtime_with_map();
 	if (test__start_subtest("metadata_match"))
 		metadata_match();
-	if (test__start_subtest("metadata_sha_mismatch"))
-		metadata_sha_mismatch();
-	if (test__start_subtest("metadata_not_exclusive"))
-		metadata_not_exclusive();
-	if (test__start_subtest("metadata_hash_not_computed"))
-		metadata_hash_not_computed();
 	if (test__start_subtest("signature_enforced"))
 		signature_enforced();
+	if (test__start_subtest("signed_nonexcl_fd_array_rejected"))
+		signed_nonexcl_fd_array_rejected();
 	if (test__start_subtest("signature_too_large"))
 		signature_too_large();
 	if (test__start_subtest("signature_bad_keyring"))
-- 
2.43.0


^ permalink raw reply related

* [PATCH bpf-next 5/5] Documentation/bpf: Add BPF signing and enforcement doc
From: Daniel Borkmann @ 2026-06-10 23:03 UTC (permalink / raw)
  To: ast
  Cc: kpsingh, James.Bottomley, paul, bboscaccy, memxor, torvalds, bpf,
	linux-security-module
In-Reply-To: <20260610230329.727075-1-daniel@iogearbox.net>

Describe the BPF signing design end to end: why a trusted loader is
needed, the signature(insns || metadata) contract, load-time
verification via fd_array (exclusive + frozen maps), the binary
BPF_SIG_{UNSIGNED,VERIFIED} verdict, and how [BPF] LSMs can enforce
policy on it.

This writes down the contract on the discussion points with the LSM /
integrity folks [0][1]: by the time security_bpf_prog_load() is
called, signature verification has fully completed and covers the
instructions plus the frozen contents of every bound exclusive map;
there is no intermediate "loader verified, payload pending" state
to reason about; and what BPF_SIG_VERIFIED means at each hook is
spelled out explicitly, including the post-verifier coverage check
that keeps the verdict binary.

Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
Link: https://lore.kernel.org/bpf/bc823ddbaf63e0e177eb46d1cc15076e4e2e689d.camel@HansenPartnership.com [0]
Link: https://lore.kernel.org/bpf/CAHC9VhSDkwGgPfrBUh7EgBKEJj_JjnY68c0YAmuuLT_i--GskQ@mail.gmail.com [1]
---
 Documentation/bpf/index.rst   |   1 +
 Documentation/bpf/signing.rst | 537 ++++++++++++++++++++++++++++++++++
 2 files changed, 538 insertions(+)
 create mode 100644 Documentation/bpf/signing.rst

diff --git a/Documentation/bpf/index.rst b/Documentation/bpf/index.rst
index 0d5c6f659266..638a00d42bc2 100644
--- a/Documentation/bpf/index.rst
+++ b/Documentation/bpf/index.rst
@@ -28,6 +28,7 @@ that goes into great technical depth about the BPF Architecture.
    classic_vs_extended.rst
    bpf_iterators
    bpf_licensing
+   signing
    test_debug
    clang-notes
    linux-notes
diff --git a/Documentation/bpf/signing.rst b/Documentation/bpf/signing.rst
new file mode 100644
index 000000000000..24997ea50345
--- /dev/null
+++ b/Documentation/bpf/signing.rst
@@ -0,0 +1,537 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+============
+BPF signing
+============
+
+This document describes how BPF programs are cryptographically signed, how the
+kernel verifies them at load time, and how Linux Security Modules (LSMs) -
+including the BPF LSM - use the resulting verdict to enforce policy. It is
+written for developers who want to produce signed BPF objects, understand what
+the signature actually guarantees, or build a policy on top of it.
+
+Motivation
+==========
+
+A signed BPF program lets the kernel establish that the bytecode being loaded
+originates from a trusted producer and was not modified in transit. On its own
+the kernel does not *require* signatures - an unsigned program loads exactly as
+before - but it records a verdict (see `The verdict`_) that an LSM can gate on.
+This is the building block for policies such as "only run BPF that was signed by
+a key in the trusted keyring", as enforced for instance by IPE.
+
+Signing is orthogonal to the existing permission model: it does not replace the
+capability checks or the verifier. A signed load still requires the usual
+privileges (``CAP_BPF`` and any program-type-specific capability, subject to
+``kernel.unprivileged_bpf_disabled``), and the loader's instructions are still
+checked by the verifier like any other program. A valid signature establishes
+*origin and integrity*, not safety - it lets a policy trust where the bytecode
+came from, it does not let a load skip any check it would otherwise face.
+
+The hard part is *what* gets signed. A naive scheme would sign a program's
+instruction buffer at build time and verify that signature at
+``BPF_PROG_LOAD``. That does not survive contact with real BPF objects, because
+the bytes the kernel finally loads are not the bytes the developer built and
+signed. Between the two, libbpf and the kernel rewrite the program:
+
+- **map file descriptors** are patched into ``ld_imm64`` instructions
+  (``BPF_PSEUDO_MAP_FD``), and a map's fd is assigned at load time, so it
+  differs on every run;
+- **CO-RE relocations** rewrite field offsets, sizes and existence flags against
+  the *running* kernel's BTF, so the result differs from one kernel to the next;
+- **kfunc and ksym references** are resolved to ids/addresses in the running
+  kernel;
+- **global data** (``.rodata``/``.data``/``.bss``) is created and seeded as maps
+  at load.
+
+So a signature over the original instructions cannot match the relocated
+instructions the verifier ends up checking, and the relocated form cannot be
+produced ahead of time because it depends on the target kernel. There is no
+fixed byte string that is both signable at build time and what the kernel
+actually loads - which is why a program cannot simply be signed and loaded
+directly.
+
+The trusted loader
+==================
+
+The solution is to move that setup work *into* a small BPF program - the
+**loader** - and sign the loader instead of the individual programs. libbpf's
+``gen_loader`` machinery (``bpftool gen skeleton -L``, the "light skeleton")
+emits a ``BPF_PROG_TYPE_SYSCALL`` program whose body performs the bpf() syscalls
+that create maps, apply relocations, and load the real programs. The payload it
+installs - the serialized programs, map descriptions, relocation data and
+initial values - lives in a separate array map, the **metadata map**
+(``__loader.map``).
+
+So the unit of trust is the loader, and the signing contract is::
+
+    Sig(I_loader || D_meta)
+
+where ``I_loader`` is the loader's instruction stream and ``D_meta`` is the
+content of the metadata map. Verifying the loader's signature establishes that
+both the loader *and* the payload it is about to install are authentic. The
+loader is reproducible: ``gen_loader`` builds it from primitives so the same
+object yields the same bytes on any build host.
+
+Why the loader is signable when the program is not
+--------------------------------------------------
+
+The loader sidesteps every rewrite listed above, because the bytes that are
+signed are *relocation-invariant*:
+
+- The loader's own instructions are a fixed sequence of bpf() syscalls emitted
+  by ``gen_loader``; they carry no CO-RE relocations and resolve no ksyms, so
+  they are identical on every kernel. The metadata map is referenced by *index*
+  into ``fd_array`` (``BPF_PSEUDO_MAP_IDX``), not by a baked-in file descriptor,
+  so even that reference does not change between build and load. The loader
+  instruction bytes the kernel verifies are exactly the bytes that were signed.
+- The metadata map is opaque, frozen data - the serialized target programs,
+  their relocation records, map descriptions and initial values. Its bytes are
+  identical at build time and at load time, so they are simply appended to the
+  instructions and covered by the same signature (there is no separate metadata
+  hash to compute or compare).
+
+All the host-specific rewriting - creating maps, patching their fds into the
+target programs, applying CO-RE, resolving ksyms, seeding global data - still
+happens, but it happens *inside the loader at runtime*, on the verified
+metadata, **after** the kernel has verified the ``insns || metadata`` signature.
+The kernel never has to verify the relocated target programs: it verifies the
+loader and its inputs once, and trust transfers to whatever that now-trusted,
+deterministic loader installs. The relocation step is moved from "before the
+signature can be checked" to "after a trusted program runs" - which is exactly
+what makes it signable.
+
+Because the metadata map is the loader's only untrusted input, two existing map
+properties are reused to keep it trustworthy across the load:
+
+Exclusive maps
+    A map created with ``excl_prog_hash`` (see ``BPF_MAP_CREATE``) may only be
+    accessed by a program whose digest matches that hash. The verifier enforces
+    ``map->excl_prog_sha == prog->digest`` for every map a program uses, so the
+    metadata map is bound to exactly the signed loader and cannot be shared with
+    or mutated by another program.
+
+Frozen maps
+    The metadata map is frozen (``BPF_MAP_FREEZE``) before the loader is loaded.
+    Freezing blocks further userspace writes, so the bytes folded into the
+    signature cannot change before the loader runs. (Freezing does not make the
+    map read-only to the loader program itself, which still writes created file
+    descriptors back into the blob's scratch area.)
+
+Load-time verification
+=======================
+
+Rather than have the loader check its own metadata from within BPF, the kernel
+verifies it directly at ``BPF_PROG_LOAD``, with no new UAPI. The mechanism
+reuses the existing ``fd_array``:
+
+#. Userspace creates the metadata map with ``excl_prog_hash`` set to the
+   loader's digest, populates it, and freezes it.
+#. The loader is loaded with ``signature``/``signature_size``/``keyring_id``
+   set, the metadata map referenced through ``fd_array``, and ``fd_array_cnt``
+   set so the kernel knows the array's length.
+#. When a signature is present and ``fd_array_cnt`` is non-zero, every map in
+   ``fd_array`` must be exclusive (carry ``excl_prog_sha``); a non-exclusive map
+   there is rejected (``-EINVAL``). The kernel appends each map's frozen contents
+   to the instruction buffer and verifies the PKCS#7 signature over the
+   concatenation ``insns || metadata_0 || metadata_1 || ...`` in ``fd_array``
+   order.
+
+A signed program therefore takes one of exactly two shapes, both fully
+supported:
+
+- **No bound maps** (``fd_array_cnt == 0``): there is nothing to append, so the
+  kernel verifies the signature over the instructions alone. A valid signature
+  yields ``BPF_SIG_VERIFIED`` and the program loads. This is the ordinary case
+  for a directly-loaded signed program with no separate payload; it is *not*
+  rejected for "missing" metadata, because it has none to cover.
+- **Exclusive bound maps** (``fd_array_cnt > 0``): every entry is exclusive and
+  folded, so the signature covers ``insns || metadata``.
+
+There is no third shape: a non-exclusive map in a signed program's ``fd_array``
+is rejected rather than silently left out of the signature, so a program bound
+to a signed loader never has a map the signature does not cover.
+
+The digest binding (``excl_prog_sha == prog->digest``) is enforced by the
+verifier as usual; because that check runs while ``fd_array`` is resolved -
+before the verifier would otherwise compute the tag - ``prog->digest`` is
+computed up front, over the unmodified (signature-covered) instructions, for any
+load that folds metadata.
+
+After the verifier has populated ``used_maps``, the kernel additionally requires
+that every *exclusive* map the program uses is one that was folded into the
+signature, and rejects the load (``-EACCES``) otherwise. This backstops the
+``fd_array`` rule above for an exclusive map the program reaches by other means
+(for example a directly-referenced fd): such a map is code-bearing but not
+covered, so the load is rejected. Together they keep the verdict binary - a
+signed program cannot read from an exclusive (code-bearing) map its signature
+does not cover, and a different but equally digest-bound map cannot be
+substituted at the ``fd_array`` slot the loader reads. Non-exclusive maps the
+program reaches by other means are runtime data, not part of the signed
+artifact, and need not be covered.
+
+The verdict
+===========
+
+A program is either unsigned or fully verified - there is no intermediate
+state. The outcome is recorded in ``prog->aux->sig.verdict``:
+
+.. code-block:: c
+
+    enum bpf_sig_verdict {
+            BPF_SIG_UNSIGNED = 0,
+            BPF_SIG_VERIFIED,
+    };
+
+``BPF_SIG_VERIFIED`` means the signature is valid and covers the instructions
+*and* the frozen contents of every exclusive map the program uses:
+
+- For an ordinary, directly-loaded signed program the instructions are the whole
+  artifact and it uses no exclusive maps, so a valid instruction signature is
+  the complete verification.
+- For a signed loader the metadata map is exclusive, so its contents are folded
+  in and the signature covers ``insns || metadata``.
+
+There is deliberately no "instructions verified but metadata not" verdict: a
+signed loader that fails to cover its metadata is *rejected* (see above), not
+recorded with a weaker verdict. ``BPF_SIG_VERIFIED`` therefore always means the
+program and everything the signature is responsible for are authentic, which is
+what a policy can rely on.
+
+Alongside the verdict the kernel records which keyring validated the signature;
+see `Keyrings`_.
+
+Enforcement via LSMs
+====================
+
+Signing only *records* a verdict; an LSM turns it into policy. The verdict and
+keyring fields live in ``struct bpf_prog_aux``, so a BPF LSM program can read
+them directly (see Documentation/bpf/prog_lsm.rst for writing and attaching BPF
+LSM programs); the same fields are equally available to in-tree LSMs such as
+IPE. Two hooks are useful at different points of the load: the dedicated
+``security_bpf_prog_load()`` gates admission before the verifier runs, and the
+existing ``security_bpf_prog()`` observes a program that has fully loaded.
+
+Admission: ``security_bpf_prog_load()``
+---------------------------------------
+
+The existing hook, called at ``BPF_PROG_LOAD`` entry, **for every load**,
+before the verifier runs. By this point the verdict and keyring fields are set,
+so the hook can see whether - and how strongly - the program was signed, which
+keyring validated it, the load ``attr``, the BPF token and whether the load came
+from the kernel.
+
+This is the place for *coarse admission* that must also see unsigned and
+not-yet-verified loads: require a signature at all, restrict the acceptable
+keyring, restrict which token/credentials may load BPF, apply per-program-type
+rules, or audit every attempt. It is the primary deny point.
+
+One subtlety: this runs before the verifier, so although the verdict is already
+``BPF_SIG_VERIFIED`` for a valid signature, the kernel has not *yet* confirmed
+that the program only uses exclusive maps the signature covers. That check
+happens after verification, and a load that violates it is rejected (``-EACCES``)
+regardless of the LSM. So ``BPF_SIG_VERIFIED`` *here* means "validly signed"; a
+program that would read an uncovered exclusive map is still rejected before it
+ever loads, and by the time it has fully loaded (see the next hook) the verdict
+carries its full meaning.
+
+A more realistic admission policy than "is it signed at all": accept programs
+signed by a system keyring, accept a user-keyring signature only if the
+key/keyring it was verified against is on an explicit allowlist, and emit a
+tamper-evident record of every decision so that even denied attempts are
+auditable. (Illustrative - error checking elided.)
+
+.. code-block:: c
+
+    /* Serials of user keys/keyrings we additionally trust. */
+    struct {
+            __uint(type, BPF_MAP_TYPE_HASH);
+            __type(key, __s32);             /* keyring_serial */
+            __type(value, __u8);
+            __uint(max_entries, 64);
+    } trusted_user_keys SEC(".maps");
+
+    /* Audit stream consumed by a userspace logger. */
+    struct {
+            __uint(type, BPF_MAP_TYPE_RINGBUF);
+            __uint(max_entries, 1 << 16);
+    } audit SEC(".maps");
+
+    struct decision { __u32 prog_type, verdict, ktype; __s32 serial, ret; };
+
+    SEC("lsm/bpf_prog_load")
+    int BPF_PROG(admit, struct bpf_prog *prog, union bpf_attr *attr,
+                 struct bpf_token *token, bool kernel)
+    {
+            __u32 verdict = prog->aux->sig.verdict;
+            __u32 ktype   = prog->aux->sig.keyring_type;
+            __s32 serial  = prog->aux->sig.keyring_serial;
+            struct decision *d;
+            int ret = 0;
+
+            if (kernel)
+                    return 0;                       /* trust in-kernel loads */
+
+            if (verdict != BPF_SIG_VERIFIED)
+                    ret = -EPERM;                   /* must be validly signed */
+            else if (ktype == BPF_SIG_KEYRING_USER &&
+                     !bpf_map_lookup_elem(&trusted_user_keys, &serial))
+                    ret = -EPERM;                   /* key/keyring not allowlisted */
+
+            d = bpf_ringbuf_reserve(&audit, sizeof(*d), 0);
+            if (d) {
+                    d->prog_type = attr->prog_type;
+                    d->verdict = verdict;
+                    d->ktype = ktype;
+                    d->serial = serial;
+                    d->ret = ret;
+                    bpf_ringbuf_submit(d, 0);       /* record allow *and* deny */
+            }
+            return ret;
+    }
+
+Observing a verified load: ``security_bpf_prog()``
+--------------------------------------------------
+
+There is deliberately no separate "metadata attested" hook. The coverage check
+above is enforced by the kernel unconditionally, so a signed loader that fails
+to cover its metadata never loads and an LSM never has to re-establish that
+fact. To *act on* a program that has successfully and fully loaded, use the
+existing ``security_bpf_prog()`` hook (``lsm/bpf_prog``), which fires from
+``bpf_prog_new_fd()`` - after the verifier, after the coverage check, and after
+``bpf_prog_alloc_id()``. Relative to the admission hook this point is strictly
+later and stronger:
+
+- the program has an id (``prog->aux->id``), so it can be recorded or correlated
+  with later events;
+- ``verdict == BPF_SIG_VERIFIED`` *here* means **fully** verified - a program
+  that used an uncovered exclusive map was already rejected, so it cannot reach
+  this point;
+- it observes only programs that actually loaded; a failed load never mints an
+  fd, so it never reaches this hook.
+
+It takes only the ``prog`` and a non-zero return still aborts (the fd is not
+handed out), so it can veto as well as observe. One wrinkle: it also fires on
+other paths that mint a new program fd - notably ``bpf_prog_get_fd_by_id()`` -
+not just on a fresh load. Because the program already has its id here, an LSM
+can tell the two apart with a small hash map: the *first* time an id is seen is
+the load; a later sighting of the same id is just another fd to a program that
+already exists.
+
+To bound the map and let a reused id read as a fresh load, this can be paired
+with ``security_bpf_prog_free()`` (``lsm/bpf_prog_free``), which deletes the
+entry on teardown - keyed by the same ``prog`` pointer, since
+``bpf_prog_free_id()`` has already cleared ``prog->aux->id`` to ``0`` by the time
+that hook runs. (Illustrative - privileged LSM, error checking elided.)
+
+.. code-block:: c
+
+    struct rec { __u32 id, ktype; __s32 serial; };
+
+    struct {
+            __uint(type, BPF_MAP_TYPE_HASH);
+            __type(key, __u64);             /* struct bpf_prog * -- stable id */
+            __type(value, struct rec);
+            __uint(max_entries, 4096);
+    } live SEC(".maps");
+
+    SEC("lsm/bpf_prog")            /* fires after load and on every later fd */
+    int BPF_PROG(observe, struct bpf_prog *prog)
+    {
+            __u64 key = (__u64)(unsigned long)prog;
+            struct rec r;
+
+            if (prog->aux->sig.verdict != BPF_SIG_VERIFIED)
+                    return 0;
+            if (bpf_map_lookup_elem(&live, &key))
+                    return 0;               /* seen before: a later fd, not a load */
+
+            /* First sighting == this program just loaded; id is valid here. */
+            r.id     = prog->aux->id;
+            r.ktype  = prog->aux->sig.keyring_type;
+            r.serial = prog->aux->sig.keyring_serial;
+            bpf_map_update_elem(&live, &key, &r, BPF_NOEXIST);
+            /* ... newly-loaded verified-program action, e.g. record r.id ... */
+            return 0;
+    }
+
+Putting them together: to *require* verified BPF, deny at the admission hook
+unless the verdict is ``BPF_SIG_VERIFIED`` (and, if desired, restrict the
+keyring). The kernel then guarantees that any program which actually loads with
+that verdict covered all of its exclusive maps, rejecting any that did not - so
+a deny-by-default admission policy needs no second enforcement point. Use
+``security_bpf_prog()`` to record or finally gate the verified programs once
+they carry an id. The ``verdict``, ``keyring_type`` and ``keyring_serial`` fields
+let a policy distinguish, for example, "verified and signed by a builtin key"
+from "verified by a user key". Policy LSMs such as IPE consume the same hooks to
+enforce system policy without writing any BPF.
+
+Keyrings
+========
+
+``keyring_id`` selects the trusted keyring the PKCS#7 signature is verified
+against. The well-known ids ``0`` (builtin), ``VERIFY_USE_SECONDARY_KEYRING``
+and ``VERIFY_USE_PLATFORM_KEYRING`` select the corresponding system keyrings;
+any other value is treated as the serial of a user/session key or keyring.
+The keyring is looked up first, before the signature bytes are examined, so a
+signature naming a non-existent keyring is rejected up front, and a failed
+verification aborts the load - so a program that loads successfully with a
+signature always has consistent keyring fields recorded.
+
+Two fields are recorded in ``prog->aux->sig`` for an LSM to inspect:
+
+``keyring_type`` (``enum bpf_sig_keyring``)
+    Classified purely from ``keyring_id`` whenever the program is signed:
+    ``BPF_SIG_KEYRING_BUILTIN``, ``_SECONDARY``, ``_PLATFORM`` for the system
+    keyrings, or ``_USER`` for a user/session keyring. It is
+    ``BPF_SIG_KEYRING_NONE`` for an unsigned program.
+
+``keyring_serial`` (``s32``)
+    Set **only** on a successful verification, to the serial of the
+    **user/session key or keyring** that ``keyring_id`` resolved to - the
+    object the signature was verified against, not the individual asymmetric
+    key inside it that matched the signer. Passing
+    ``KEY_SPEC_SESSION_KEYRING``, for example, records the session keyring's
+    serial. The system keyrings are trusted as a whole and expose no serial
+    here, so the serial is ``0`` for builtin, secondary and platform
+    signatures, and ``0`` for unsigned programs. In other words, a non-zero
+    ``keyring_serial`` is exactly "verified against the user key/keyring with
+    this serial".
+
+.. list-table::
+   :header-rows: 1
+
+   * - ``keyring_id``
+     - ``keyring_type``
+     - ``keyring_serial``
+   * - (no signature)
+     - ``BPF_SIG_KEYRING_NONE``
+     - ``0``
+   * - ``0``
+     - ``BPF_SIG_KEYRING_BUILTIN``
+     - ``0``
+   * - ``VERIFY_USE_SECONDARY_KEYRING``
+     - ``BPF_SIG_KEYRING_SECONDARY``
+     - ``0``
+   * - ``VERIFY_USE_PLATFORM_KEYRING``
+     - ``BPF_SIG_KEYRING_PLATFORM``
+     - ``0``
+   * - other (a user/session key serial)
+     - ``BPF_SIG_KEYRING_USER``
+     - serial of the resolved key/keyring
+
+Producing a signed object
+==========================
+
+``bpftool`` generates and signs a light skeleton in one step::
+
+    bpftool gen skeleton -L -S -k <private_key.pem> -i <certificate.x509> \
+            obj.bpf.o > obj.lskel.h
+
+``-L`` selects the light-skeleton (``gen_loader``) backend and ``-S`` enables
+signing; ``-k`` and ``-i`` supply the signing key and its X.509 certificate.
+``bpftool`` signs ``insns || metadata`` - the exact bytes the kernel
+reconstructs - and also computes ``excl_prog_hash`` as the digest of the loader
+instructions so the metadata map can be bound to the loader. The signature,
+certificate and hash are embedded in the generated header; loading the skeleton
+performs the create/populate/freeze/load sequence described above.
+
+At runtime the trusted public key must be present in the chosen keyring (for
+example added to the session keyring, or built into the kernel's builtin trusted
+keyring) for verification to succeed.
+
+UAPI reference
+==============
+
+``BPF_PROG_LOAD`` (``union bpf_attr``):
+
+``signature``, ``signature_size``
+    Pointer to and length of the PKCS#7 signature blob.
+
+``keyring_id``
+    Trusted keyring selector (see `Keyrings`_).
+
+``fd_array``, ``fd_array_cnt``
+    Array of map file descriptors bound to the program. ``fd_array_cnt`` must be
+    set for the kernel to scan the array. When a signature is present, every map
+    in the array must be exclusive; its frozen contents are folded into the
+    verified buffer, and a non-exclusive entry is rejected (``-EINVAL``).
+
+``BPF_MAP_CREATE`` (``union bpf_attr``):
+
+``excl_prog_hash``, ``excl_prog_hash_size``
+    SHA-256 digest of the program permitted to access this (exclusive) map. This
+    binds the metadata map to the loader; it is not a hash of the map *content*.
+    The map content is not hashed separately at all - it is covered, as bytes,
+    by the program signature.
+
+Failure modes
+=============
+
+When a signature is present but the load cannot be authenticated, the load is
+rejected; it is never silently downgraded to unsigned. The common rejections:
+
+.. list-table::
+   :header-rows: 1
+   :widths: 30 15 55
+
+   * - Condition
+     - errno
+     - Notes
+   * - Signature does not validate, or no trusted key in the selected keyring
+       matched the signer
+     - ``-EKEYREJECTED`` / ``-ENOKEY``
+     - Surfaced from the PKCS#7 verification layer: ``-EKEYREJECTED`` for an
+       invalid signature, ``-ENOKEY`` when no key in the keyring matches.
+   * - ``keyring_id`` does not resolve to a usable keyring / key
+     - ``-EINVAL``
+     - The keyring is looked up before the signature bytes are examined.
+   * - ``signature_size`` too large
+     - ``-EINVAL``
+     - A practical PKCS#7 signature is well under the cache-allocation limit.
+   * - Non-exclusive map in a signed program's ``fd_array``
+     - ``-EINVAL``
+     - Every folded map must carry ``excl_prog_sha`` (see `Load-time
+       verification`_).
+   * - ``fd_array_cnt`` exceeds the maximum number of used maps
+     - ``-E2BIG``
+     -
+   * - ``insns || metadata`` exceeds the dynptr size cap (~16 MiB)
+     - ``-E2BIG``
+     - The instructions and folded maps are verified as one ``bpf_dynptr``.
+   * - A folded (exclusive) map is not frozen
+     - ``-EPERM``
+     - Freezing is required so the hashed bytes cannot change before the loader
+       runs.
+   * - Program uses an exclusive map the signature does not cover
+     - ``-EACCES``
+     - The post-verifier binding check; keeps the verdict binary (see `The
+       verdict`_).
+
+An unsigned program (no ``signature``) is never rejected by this path; it simply
+loads with the ``BPF_SIG_UNSIGNED`` verdict, leaving any policy decision to an
+LSM.
+
+Testing
+=======
+
+The ``signed_loader`` test in ``tools/testing/selftests/bpf`` exercises the full
+path: it drives map-less and map-owning objects through ``gen_loader``, signs
+``insns || metadata``, loads with ``fd_array_cnt`` set, runs the loader, and
+confirms the target program and map are installed. ``lsm_signature_verdict``
+additionally attaches a BPF LSM program and asserts the observed verdict
+(``BPF_SIG_UNSIGNED`` and ``BPF_SIG_VERIFIED``), and that a signed loader which
+does not fold its metadata is rejected. The signed light skeletons
+``fentry_test``, ``fexit_test`` and ``atomics`` exercise the same load path
+through real generated-and-signed skeletons.
+
+Notes and limitations
+======================
+
+- The instructions plus folded metadata are verified as one ``bpf_dynptr``,
+  which bounds the combined size (currently ~16 MiB); very large objects can
+  exceed it.
+- The metadata container is a single-element array map, accessed through
+  ``map_direct_value_addr``.
+- The verdict and the LSM hooks are kernel-internal; the verdict is not part of
+  the stable UAPI.
-- 
2.43.0


^ permalink raw reply related

* Re: [bug report] apparmor: add support loading per permission tagging
From: John Johansen @ 2026-06-10 23:12 UTC (permalink / raw)
  To: Dan Carpenter; +Cc: apparmor, linux-security-module
In-Reply-To: <adjOGC9qtr_9XkgS@stanley.mountain>

On 4/10/26 03:16, Dan Carpenter wrote:
> Hello John Johansen,
> 
> Commit 3d28e2397af7 ("apparmor: add support loading per permission
> tagging") from Apr 1, 2025 (linux-next), leads to the following
> Smatch static checker warning:
> 
> 	security/apparmor/policy_unpack.c:883 unpack_tags()
> 	warn: missing error code 'error'
> 

Sorry for late reply, I am slowly working through the backlog
A managed to get a fix for this into 7.1

72971e6f745ad apparmor: fix unpack_tags to properly return error in failure cases

thanks
john


> security/apparmor/policy_unpack.c
>      852 static int unpack_tags(struct aa_ext *e, struct aa_tags_struct *tags,
>      853         const char **info)
>      854 {
>      855         int error = -EPROTO;
>      856         void *pos = e->pos;
>      857
>      858         AA_BUG(!tags);
>      859         /* policy tags are optional */
>      860         if (aa_unpack_nameX(e, AA_STRUCT, "tags")) {
>      861                 u32 version;
>      862
>      863                 if (!aa_unpack_u32(e, &version, "version") || version != 1) {
>      864                         *info = "invalid tags version";
>      865                         goto fail_reset;
>      866                 }
>      867                 error = unpack_strs_table(e, "strs", true, &tags->strs);
>      868                 if (error) {
>      869                         *info = "failed to unpack profile tag.strs";
>      870                         goto fail;
>      871                 }
>      872                 error = unpack_tag_headers(e, tags);
>      873                 if (error) {
>      874                         *info = "failed to unpack profile tag.headers";
>      875                         goto fail;
>      876                 }
>      877                 error = unpack_tagsets(e, tags);
>      878                 if (error) {
>      879                         *info = "failed to unpack profile tag.sets";
>      880                         goto fail;
>      881                 }
>      882                 if (!aa_unpack_nameX(e, AA_STRUCTEND, NULL))
> --> 883                         goto fail;
> 
> set the error code here
> 
>      884
>      885                 if (!verify_tags(tags, info))
>      886                         goto fail;
> 
> and here
> 
>      887         }
>      888
>      889         return 0;
>      890
>      891 fail:
>      892         aa_destroy_tags(tags);
>      893 fail_reset:
>      894         e->pos = pos;
>      895         return error;
>      896 }
> 
> This email is a free service from the Smatch-CI project [smatch.sf.net].
> 
> regards,
> dan carpenter


^ permalink raw reply

* Re: [PATCH bpf-next 3/5] bpftool: Cover loader metadata with the program signature
From: bot+bpf-ci @ 2026-06-10 23:48 UTC (permalink / raw)
  To: daniel, ast
  Cc: kpsingh, James.Bottomley, paul, bboscaccy, memxor, torvalds, bpf,
	linux-security-module, ast, andrii, daniel, martin.lau, eddyz87,
	yonghong.song, clm, ihor.solodrai
In-Reply-To: <20260610230329.727075-4-daniel@iogearbox.net>

[-- Attachment #1: Type: text/plain, Size: 1746 bytes --]

> bpftool: Cover loader metadata with the program signature
>
>     bpftool_prog_sign() signed only the loader instructions. The metadata
>     blob the loader installs was left to an in-loader hash check, which
>     the kernel now performs at load time over insns || metadata.
>
>     Sign that same concatenation: pass the metadata blob (gen_loader_opts
>     data) through to bpftool_prog_sign() and feed insns || metadata to
>     CMS_final(). The excl_prog_hash stays a digest of the instructions
>     alone; it binds the metadata map to the loader and is matched against
>     prog->digest by the verifier, independent of what the signature covers.
>
>     The signed artifact is now plain data: both bytes the signature
>     covers are embedded verbatim in the generated skeleton, so signing
>     and verifying an lskel is an ordinary CMS operation that a signer or
>     auditor can perform (or reproduce) offline, without analyzing loader
>     bytecode to establish what the signature actually attests to [0].
>
>     Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>

This closes a gap where the loader metadata was left outside the program
signature. The signing path in bpftool_prog_sign() was introduced by commit
40863f4d6ef2 ("bpftool: Add support for signing BPF programs"), which fed
only opts->insns to the signature and set only sopts.insns/sopts.insns_sz in
gen_trace(). Should this carry a Fixes: tag?

  Fixes: 40863f4d6ef2 ("bpftool: Add support for signing BPF programs")


---
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/27312517811

^ permalink raw reply

* Re: [PATCH] cred: prevent slab cache merging for cred_jar
From: Paul Moore @ 2026-06-10 23:53 UTC (permalink / raw)
  To: Mohammed EL Kadiri, Kees Cook
  Cc: Serge Hallyn, Vlastimil Babka, linux-security-module,
	linux-hardening, linux-kernel
In-Reply-To: <202606101510.7F6F4118@keescook>

On Wed, Jun 10, 2026 at 6:11 PM Kees Cook <kees@kernel.org> wrote:
> On Wed, Jun 10, 2026 at 10:07:24PM +0100, Mohammed EL Kadiri wrote:
> > Hi Kees,
> >
> > Thanks for the review!
> > Following Vlastimil and Jarkko's feedback on the key_jar patch, should
> > I send a v2 here as well with similar commit message modification:
> > removing CVE references, dropping the skbuff comparison, and framing
> > it as hardening?
>
> It wouldn't hurt, yeah. I have that kind of already in my head while I
> read these patches, but it would be better for other folks to see it
> framed more accurately.

Just as an FYI, the patch seems reasonable to me, but considering
where we are in the dev cycle I figured it best to wait until after
the upcoming merge window to do anything with it.

> > On Wed, Jun 10, 2026 at 9:45 PM Kees Cook <kees@kernel.org> wrote:
> > >
> > > On Sat, Jun 06, 2026 at 03:25:58PM +0100, Mohammed EL Kadiri wrote:
> > > > The cred_jar slab cache holds struct cred objects, which contain
> > > > process credentials: uid, gid, euid, egid, and capability sets.
> > > > Overwriting any of these fields is sufficient for privilege escalation.
> > > >
> > > > On a default Ubuntu 6.17.0-23-generic system, cred_jar (named "cred"
> > > > in sysfs) has 2 aliases, meaning 2 unrelated object types share its
> > > > slab pages (object_size=184, objs_per_slab=42).
> > > >
> > > > Cross-cache heap exploitation relies on slab cache merging to achieve
> > > > type confusion between unrelated kernel objects. CVE-2022-29582
> > > > demonstrates this technique: an io_uring use-after-free is leveraged
> > > > across cache boundaries through page-level reallocation, ultimately
> > > > achieving root. struct cred is a primary target in this class of
> > > > attacks due to the direct privilege escalation that results from
> > > > corrupting any of its identity or capability fields.
> > > >
> > > > Add SLAB_NO_MERGE to ensure cred_jar receives dedicated slab pages,
> > > > so that freed credential slots can only be reallocated as struct cred
> > > > objects. The memory overhead is minimal: one struct cred exists per
> > > > task, and with 42 objects per slab page, the cost of dedicated pages
> > > > is negligible. There is zero performance impact on the allocation
> > > > hot path.
> > > >
> > > > This follows the precedent set by skbuff_head_cache (net/core/skbuff.c)
> > > > and key_jar (security/keys/key.c) which use SLAB_NO_MERGE for similar
> > > > isolation requirements.
> > > >
> > > > Signed-off-by: Mohammed EL Kadiri <med08elkadiri@gmail.com>
> > >
> > > Yes please. :)
> > >
> > > Reviewed-by: Kees Cook <kees@kernel.org>

-- 
paul-moore.com

^ permalink raw reply

* Re: [PATCH v4 0/7] landlock: Add UDP access control support
From: Matthieu Buffet @ 2026-06-11  0:27 UTC (permalink / raw)
  To: Mickaël Salaün
  Cc: Günther Noack, linux-security-module, Mikhail Ivanov,
	konstantin.meskhidze, Tingmao Wang
In-Reply-To: <20260525.aeng6Xeula5o@digikod.net>

On 5/25/2026 10:28 PM, Mickaël Salaün wrote:
> Also, some interesting (and some other not relevant) findings here:
> https://sashiko.dev/#/patchset/20260502124306.3975990-1-matthieu%40buffet.re

No false positive in there afaict (e.g. connect(AF_UNSPEC) does not 
trigger an autobind but was indeed incorrectly subjected to a bind(0) 
access check).
I have just finished merging small fixes. Thanks!

-- 
Matthieu

^ permalink raw reply


This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox