BPF List
 help / color / mirror / Atom feed
* [PATCH bpf-next v3 00/11] BPF keyring and signed loader ML-DSA support
@ 2026-08-26 16:41 Daniel Borkmann
  2026-08-26 16:41 ` [PATCH bpf-next v3 01/11] bpf, keys: Add a bpf keyring for program signature validation Daniel Borkmann
                   ` (10 more replies)
  0 siblings, 11 replies; 19+ messages in thread
From: Daniel Borkmann @ 2026-08-26 16:41 UTC (permalink / raw)
  To: memxor
  Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf,
	dhowells, jarkko, keyrings

Add a dedicated BPF keyring which is integrated for BPF signing. It
is modelled after the dm-verity keyring which was added in commit
033724b ("dm-verity: add dm-verity keyring") and which can eventually
be used also via systemd through the same enrollment method as in
dm-verity's case. It is selected with a new well-known keyring_id
VERIFY_USE_BPF_KEYRING and gives an operator a place to enroll a
BPF-only signing key at boot, specifically scoped to signed BPF
program loading. Next, to demonstrate that BPF signing is algorithm
agnostic, we add a few small tooling changes to support ML-DSA and
integrate everything into BPF selftest for BPF CI to ensure nothing
breaks with future changes. For more details, see individual commits.

v2 -> v3:
  - Added KEY_SPEC_BPF_KEYRING rather than VERIFY_USE_BPF_KEYRING
    then it can also be looked up via lookup_user_key (Christian Brauner)
  - Added CMS_NO_SIGNING_TIME to bpftool patch (Sashiko)
v1 -> v2:
  - Mainly addressing LLM feedback in docs, commit message and
    selftests (BPF CI, sashiko)
    - Fix reasoning on BPF_PROG_MAX_SIGNATURE_SIZE
    - Add reasoning about bpftool MAX_SIG_SIZE change and pinning
      the CMS digest to SHA-512
    - Fix misc signing.rst phrasing
    - Fix a potential NULL deref in bpf_keyring_lookup selftest code
    - Add CONFIG_CRYPTO_SHA512=y to BPF selftest config
    - Clean up key material on ML-DSA signed loader test failure
    - Refactor duplicated bpf_attr setup for bpf keyring tests
    - Fix wording in kernel-parameters.txt

Daniel Borkmann (11):
  bpf, keys: Add a bpf keyring for program signature validation
  bpf: Refuse caller-supplied keyrings when the bpf one is active
  bpf: Raise the bound on a program's signature size
  bpftool: Support ML-DSA program signing
  selftests/bpf: Add a test for the sealed bpf keyring
  selftests/bpf: Rebuild signed lskels when signing key changes
  selftests/bpf: Rename the verify_sig_setup.sh setup into setup-rsa
  selftests/bpf: Add an end-to-end ML-DSA signed loader test
  selftests/bpf: Allow appending to guest kernel cmdline in vmtest.sh
  selftests/bpf: Add tests for bpf keyring in signed loader
  Documentation/bpf: Document the bpf keyring and improve examples

 .../admin-guide/kernel-parameters.txt         |  16 +
 Documentation/bpf/signing.rst                 | 283 ++++++++--
 include/linux/bpf.h                           |  13 +
 include/linux/key.h                           |   2 +
 include/uapi/linux/keyctl.h                   |   1 +
 kernel/bpf/Makefile                           |   3 +
 kernel/bpf/keys.c                             |  73 +++
 kernel/bpf/verifier.c                         |  44 +-
 security/keys/process_keys.c                  |  25 +
 tools/bpf/bpftool/main.h                      |   2 +-
 tools/bpf/bpftool/sign.c                      |  24 +-
 tools/testing/selftests/bpf/Makefile          |   2 +-
 tools/testing/selftests/bpf/config            |   2 +
 .../selftests/bpf/prog_tests/signed_loader.c  | 486 ++++++++++++++++--
 .../bpf/prog_tests/verify_pkcs7_sig.c         |   4 +-
 .../testing/selftests/bpf/verify_sig_setup.sh |  63 ++-
 tools/testing/selftests/bpf/vmtest.sh         |  16 +-
 17 files changed, 955 insertions(+), 104 deletions(-)
 create mode 100644 kernel/bpf/keys.c

-- 
2.43.0


^ permalink raw reply	[flat|nested] 19+ messages in thread

* [PATCH bpf-next v3 01/11] bpf, keys: Add a bpf keyring for program signature validation
  2026-08-26 16:41 [PATCH bpf-next v3 00/11] BPF keyring and signed loader ML-DSA support Daniel Borkmann
@ 2026-08-26 16:41 ` Daniel Borkmann
  2026-08-26 17:54   ` bot+bpf-ci
  2026-08-26 16:41 ` [PATCH bpf-next v3 02/11] bpf: Refuse caller-supplied keyrings when the bpf one is active Daniel Borkmann
                   ` (9 subsequent siblings)
  10 siblings, 1 reply; 19+ messages in thread
From: Daniel Borkmann @ 2026-08-26 16:41 UTC (permalink / raw)
  To: memxor
  Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf,
	dhowells, jarkko, keyrings

BPF program signatures can currently be verified against one of the
system keyrings (builtin, secondary, platform) or against an arbitrary
user/session caller-supplied keyring named through keyring_id. There
is nothing in between: the system keyrings need a kernel rebuild or a
vouched-for enrollment to rotate a key, while a caller-supplied keyring
is fully controlled by the loader and therefore carries no trust on
its own (unless explicitly combined with BPF LSM to protect against
key tampering).

Add a dedicated bpf keyring to fill that gap, modelled after the
dm-verity keyring which was added in commit 033724b1c627 ("dm-verity:
add dm-verity keyring") and which can eventually be used also via
systemd through the same enrollment method as in dm-verity's case. It
is selected with the new KEY_SPEC_BPF_KEYRING special key id and gives
an operator a place to enroll a BPF-only signing key at boot,
specifically scoped to BPF program loading and nothing else in the
kernel's trust hierarchy.

The id is reserved from the KEY_SPEC space so that the latter is not
linked into any process keyring, and lookup_user_key() resolves
KEY_SPEC_BPF_KEYRING constant instead of having to look it up via
/proc/keys first.

By default the keyring is sealed empty at init. Systems that want to
provision keys pass bpf.keyring_unsealed=1, which leaves the keyring
open for the initrd to add keys to. The keyring is only ever consulted
once it is both non-empty and restricted. An unrestricted keyring is
ignored.

Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
---
 .../admin-guide/kernel-parameters.txt         |  8 +++
 include/linux/bpf.h                           |  7 ++
 include/linux/key.h                           |  2 +
 include/uapi/linux/keyctl.h                   |  1 +
 kernel/bpf/Makefile                           |  3 +
 kernel/bpf/keys.c                             | 68 +++++++++++++++++++
 kernel/bpf/verifier.c                         | 14 +++-
 security/keys/process_keys.c                  | 25 +++++++
 8 files changed, 126 insertions(+), 2 deletions(-)
 create mode 100644 kernel/bpf/keys.c

diff --git a/Documentation/admin-guide/kernel-parameters.txt b/Documentation/admin-guide/kernel-parameters.txt
index e4643634a9b1..2beb61092bb3 100644
--- a/Documentation/admin-guide/kernel-parameters.txt
+++ b/Documentation/admin-guide/kernel-parameters.txt
@@ -667,6 +667,14 @@ Kernel parameters
 
 			See Documentation/admin-guide/bootconfig.rst
 
+	bpf.keyring_unsealed=
+			[BPF] When set to 1, leave the bpf keyring unsealed
+			after initialization so that userspace can provision
+			keys. Once the keyring is restricted it becomes active
+			and can be used for BPF program signature verification.
+
+			See Documentation/bpf/signing.rst
+
 	bttv.card=	[HW,V4L] bttv (bt848 + bt878 based grabber cards)
 	bttv.radio=	Most important insmod options are available as
 			kernel args too.
diff --git a/include/linux/bpf.h b/include/linux/bpf.h
index b3cd28d9e3b5..62b095f7f524 100644
--- a/include/linux/bpf.h
+++ b/include/linux/bpf.h
@@ -1735,6 +1735,7 @@ enum bpf_sig_keyring {
 	BPF_SIG_KEYRING_SECONDARY,
 	BPF_SIG_KEYRING_PLATFORM,
 	BPF_SIG_KEYRING_USER,
+	BPF_SIG_KEYRING_BPF,
 };
 
 struct bpf_prog_aux {
@@ -3819,6 +3820,7 @@ struct bpf_key {
 #if defined(CONFIG_KEYS) && defined(CONFIG_BPF_SYSCALL)
 struct bpf_key *bpf_lookup_user_key(s32 serial, u64 flags);
 struct bpf_key *bpf_lookup_system_key(u64 id);
+struct bpf_key *bpf_lookup_keyring(void);
 void bpf_key_put(struct bpf_key *bkey);
 int bpf_verify_pkcs7_signature(const struct bpf_dynptr *data_p,
 			       const struct bpf_dynptr *sig_p,
@@ -3839,6 +3841,11 @@ static inline struct bpf_key *bpf_lookup_system_key(u64 id)
 	return NULL;
 }
 
+static inline struct bpf_key *bpf_lookup_keyring(void)
+{
+	return NULL;
+}
+
 static inline void bpf_key_put(struct bpf_key *bkey)
 {
 }
diff --git a/include/linux/key.h b/include/linux/key.h
index 81b8f05c6898..bd10fe45819d 100644
--- a/include/linux/key.h
+++ b/include/linux/key.h
@@ -440,6 +440,8 @@ extern key_ref_t keyring_search(key_ref_t keyring,
 extern int keyring_restrict(key_ref_t keyring, const char *type,
 			    const char *restriction);
 
+extern void key_register_bpf_keyring(struct key *keyring);
+
 extern struct key *key_lookup(key_serial_t id);
 
 static inline key_serial_t key_serial(const struct key *key)
diff --git a/include/uapi/linux/keyctl.h b/include/uapi/linux/keyctl.h
index 4c8884eea808..fa85b9760391 100644
--- a/include/uapi/linux/keyctl.h
+++ b/include/uapi/linux/keyctl.h
@@ -24,6 +24,7 @@
 #define KEY_SPEC_GROUP_KEYRING		-6	/* - key ID for GID-specific keyring */
 #define KEY_SPEC_REQKEY_AUTH_KEY	-7	/* - key ID for assumed request_key auth key */
 #define KEY_SPEC_REQUESTOR_KEYRING	-8	/* - key ID for request_key() dest keyring */
+#define KEY_SPEC_BPF_KEYRING		-9	/* - key ID for the BPF-specific keyring */
 
 /* request-key default keyrings */
 #define KEY_REQKEY_DEFL_NO_CHANGE		-1
diff --git a/kernel/bpf/Makefile b/kernel/bpf/Makefile
index 90255d80e5be..9a92c348bbda 100644
--- a/kernel/bpf/Makefile
+++ b/kernel/bpf/Makefile
@@ -27,6 +27,9 @@ obj-$(CONFIG_BPF_SYSCALL) += offload.o
 obj-$(CONFIG_BPF_SYSCALL) += net_namespace.o
 obj-$(CONFIG_BPF_SYSCALL) += tcx.o
 endif
+ifeq ($(CONFIG_KEYS),y)
+obj-$(CONFIG_BPF_SYSCALL) += keys.o
+endif
 ifeq ($(CONFIG_PERF_EVENTS),y)
 obj-$(CONFIG_BPF_SYSCALL) += stackmap.o
 endif
diff --git a/kernel/bpf/keys.c b/kernel/bpf/keys.c
new file mode 100644
index 000000000000..08549b3220c1
--- /dev/null
+++ b/kernel/bpf/keys.c
@@ -0,0 +1,68 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/* Copyright (c) 2026 Isovalent */
+
+#include <linux/bpf.h>
+#include <linux/cred.h>
+#include <linux/err.h>
+#include <linux/init.h>
+#include <linux/key.h>
+#include <linux/moduleparam.h>
+#include <linux/slab.h>
+
+#undef MODULE_PARAM_PREFIX
+#define MODULE_PARAM_PREFIX "bpf."
+
+static struct key *bpf_keyring;
+
+static bool bpf_keyring_unsealed __ro_after_init;
+module_param_named(keyring_unsealed, bpf_keyring_unsealed, bool, 0444);
+MODULE_PARM_DESC(keyring_unsealed, "Leave the bpf keyring unsealed");
+
+struct bpf_key *bpf_lookup_keyring(void)
+{
+	struct bpf_key *bkey;
+
+	if (!bpf_keyring)
+		return NULL;
+	if (!READ_ONCE(bpf_keyring->keys.nr_leaves_on_tree) ||
+	    !READ_ONCE(bpf_keyring->restrict_link))
+		return NULL;
+
+	bkey = kmalloc_obj(*bkey);
+	if (!bkey)
+		return NULL;
+
+	bkey->key = bpf_keyring;
+	bkey->has_ref = false;
+	return bkey;
+}
+
+static int __init bpf_keyring_init(void)
+{
+	struct key *keyring;
+
+	keyring = keyring_alloc(".bpf",
+				GLOBAL_ROOT_UID, GLOBAL_ROOT_GID,
+				current_cred(), KEY_POS_SEARCH |
+				KEY_USR_VIEW | KEY_USR_READ |
+				KEY_USR_WRITE | KEY_USR_SEARCH |
+				KEY_USR_SETATTR, KEY_ALLOC_NOT_IN_QUOTA,
+				NULL, NULL);
+	if (IS_ERR(keyring)) {
+		pr_err("bpf: cannot allocate bpf keyring: %ld\n",
+		       PTR_ERR(keyring));
+		return 0;
+	}
+	if (!bpf_keyring_unsealed &&
+	    keyring_restrict(make_key_ref(keyring, true), NULL, NULL)) {
+		pr_err("bpf: cannot seal bpf keyring\n");
+		key_revoke(keyring);
+		key_put(keyring);
+		return 0;
+	}
+
+	bpf_keyring = keyring;
+	key_register_bpf_keyring(keyring);
+	return 0;
+}
+late_initcall(bpf_keyring_init);
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index e036ae20bf6b..b45d201c2535 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -24,6 +24,7 @@
 #include <linux/bpf_lsm.h>
 #include <linux/security.h>
 #include <linux/verification.h>
+#include <linux/keyctl.h>
 #include <linux/btf_ids.h>
 #include <linux/poison.h>
 #include <linux/module.h>
@@ -20981,6 +20982,8 @@ static enum bpf_sig_keyring bpf_classify_keyring(s32 keyring_id)
 		return BPF_SIG_KEYRING_SECONDARY;
 	case (s32)(unsigned long)VERIFY_USE_PLATFORM_KEYRING:
 		return BPF_SIG_KEYRING_PLATFORM;
+	case KEY_SPEC_BPF_KEYRING:
+		return BPF_SIG_KEYRING_BPF;
 	default:
 		return BPF_SIG_KEYRING_USER;
 	}
@@ -21016,10 +21019,17 @@ static int bpf_prog_verify_signature(struct bpf_verifier_env *env,
 	if (!attr->signature_size ||
 	    attr->signature_size > KMALLOC_MAX_CACHE_SIZE)
 		return -EINVAL;
-	if (system_keyring_id_check(attr->keyring_id) == 0)
+	if (attr->keyring_id == KEY_SPEC_BPF_KEYRING) {
+		key = bpf_lookup_keyring();
+		if (!key) {
+			verbose(env, "the bpf keyring is empty or has not been restricted\n");
+			return -ENOKEY;
+		}
+	} else if (system_keyring_id_check(attr->keyring_id) == 0) {
 		key = bpf_lookup_system_key(attr->keyring_id);
-	else
+	} else {
 		key = bpf_lookup_user_key(attr->keyring_id, 0);
+	}
 	if (!key) {
 		verbose(env, "cannot resolve signing keyring with keyring_id %d\n",
 			attr->keyring_id);
diff --git a/security/keys/process_keys.c b/security/keys/process_keys.c
index a63c46bb2d14..44358388e395 100644
--- a/security/keys/process_keys.c
+++ b/security/keys/process_keys.c
@@ -22,6 +22,9 @@
 /* Session keyring create vs join semaphore */
 static DEFINE_MUTEX(key_session_mutex);
 
+/* BPF keyring reachable through KEY_SPEC_BPF_KEYRING */
+static struct key *bpf_keyring __ro_after_init;
+
 /* The root user's tracking struct */
 struct key_user root_key_user = {
 	.usage		= REFCOUNT_INIT(3),
@@ -590,6 +593,20 @@ bool lookup_user_key_possessed(const struct key *key,
 	return key == match_data->raw_data;
 }
 
+/**
+ * key_register_bpf_keyring - Publish the BPF keyring for KEY_SPEC_BPF_KEYRING
+ * @keyring: The keyring to publish
+ *
+ * Make @keyring reachable by userspace through the KEY_SPEC_BPF_KEYRING
+ * special key ID, so that provisioning it does not require scraping its
+ * serial out of /proc/keys first. Called once, from an initcall, and never
+ * undone.
+ */
+void key_register_bpf_keyring(struct key *keyring)
+{
+	bpf_keyring = keyring;
+}
+
 /*
  * Look up a key ID given us by userspace with a given permissions mask to get
  * the key it refers to.
@@ -741,6 +758,14 @@ key_ref_t lookup_user_key(key_serial_t id, unsigned long lflags,
 		key_ref = make_key_ref(key, 1);
 		break;
 
+	case KEY_SPEC_BPF_KEYRING:
+		key = bpf_keyring;
+		if (!key)
+			goto error;
+		__key_get(key);
+		key_ref = make_key_ref(key, 0);
+		break;
+
 	default:
 		key_ref = ERR_PTR(-EINVAL);
 		if (id < 1)
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* [PATCH bpf-next v3 02/11] bpf: Refuse caller-supplied keyrings when the bpf one is active
  2026-08-26 16:41 [PATCH bpf-next v3 00/11] BPF keyring and signed loader ML-DSA support Daniel Borkmann
  2026-08-26 16:41 ` [PATCH bpf-next v3 01/11] bpf, keys: Add a bpf keyring for program signature validation Daniel Borkmann
@ 2026-08-26 16:41 ` Daniel Borkmann
  2026-08-26 17:41   ` bot+bpf-ci
  2026-08-26 16:41 ` [PATCH bpf-next v3 03/11] bpf: Raise the bound on a program's signature size Daniel Borkmann
                   ` (8 subsequent siblings)
  10 siblings, 1 reply; 19+ messages in thread
From: Daniel Borkmann @ 2026-08-26 16:41 UTC (permalink / raw)
  To: memxor
  Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf,
	dhowells, jarkko, keyrings

Nothing changes for systems that do not use the bpf keyring. Without
bpf.keyring_unsealed=1 a caller-supplied keyring behaves exactly as
before, which also lets it serve as the staging step for software
installed onto a running system whose signing key is not enrolled
anywhere yet.

Passing bpf.keyring_unsealed=1 states that the bpf keyring is the trust
anchor for this boot, so from the first program load onwards a caller-
supplied keyring is refused with -EPERM. Deriving this from the boot
flag rather than from the keyring's runtime state keeps the decision
immutable from userspace.

Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
---
 .../admin-guide/kernel-parameters.txt         |  8 +++++
 include/linux/bpf.h                           |  6 ++++
 kernel/bpf/keys.c                             |  5 ++++
 kernel/bpf/verifier.c                         | 29 ++++++++++++-------
 4 files changed, 37 insertions(+), 11 deletions(-)

diff --git a/Documentation/admin-guide/kernel-parameters.txt b/Documentation/admin-guide/kernel-parameters.txt
index 2beb61092bb3..e70fb15757d4 100644
--- a/Documentation/admin-guide/kernel-parameters.txt
+++ b/Documentation/admin-guide/kernel-parameters.txt
@@ -673,6 +673,14 @@ Kernel parameters
 			keys. Once the keyring is restricted it becomes active
 			and can be used for BPF program signature verification.
 
+			Setting this also means that the bpf keyring is the
+			only non-system keyring a loader may select for the
+			rest of the boot: caller-supplied user/session
+			keyrings are refused with -EPERM, whether or not
+			provisioning actually completed. The system keyrings
+			stay selectable. Leaving it unset keeps the prior
+			behaviour, where a caller-supplied keyring is allowed.
+
 			See Documentation/bpf/signing.rst
 
 	bttv.card=	[HW,V4L] bttv (bt848 + bt878 based grabber cards)
diff --git a/include/linux/bpf.h b/include/linux/bpf.h
index 62b095f7f524..571f7792a5c4 100644
--- a/include/linux/bpf.h
+++ b/include/linux/bpf.h
@@ -3821,6 +3821,7 @@ struct bpf_key {
 struct bpf_key *bpf_lookup_user_key(s32 serial, u64 flags);
 struct bpf_key *bpf_lookup_system_key(u64 id);
 struct bpf_key *bpf_lookup_keyring(void);
+bool bpf_keyring_enforced(void);
 void bpf_key_put(struct bpf_key *bkey);
 int bpf_verify_pkcs7_signature(const struct bpf_dynptr *data_p,
 			       const struct bpf_dynptr *sig_p,
@@ -3846,6 +3847,11 @@ static inline struct bpf_key *bpf_lookup_keyring(void)
 	return NULL;
 }
 
+static inline bool bpf_keyring_enforced(void)
+{
+	return false;
+}
+
 static inline void bpf_key_put(struct bpf_key *bkey)
 {
 }
diff --git a/kernel/bpf/keys.c b/kernel/bpf/keys.c
index 08549b3220c1..bffc356839ac 100644
--- a/kernel/bpf/keys.c
+++ b/kernel/bpf/keys.c
@@ -18,6 +18,11 @@ static bool bpf_keyring_unsealed __ro_after_init;
 module_param_named(keyring_unsealed, bpf_keyring_unsealed, bool, 0444);
 MODULE_PARM_DESC(keyring_unsealed, "Leave the bpf keyring unsealed");
 
+bool bpf_keyring_enforced(void)
+{
+	return bpf_keyring_unsealed;
+}
+
 struct bpf_key *bpf_lookup_keyring(void)
 {
 	struct bpf_key *bkey;
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index b45d201c2535..095057277994 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -21019,21 +21019,28 @@ static int bpf_prog_verify_signature(struct bpf_verifier_env *env,
 	if (!attr->signature_size ||
 	    attr->signature_size > KMALLOC_MAX_CACHE_SIZE)
 		return -EINVAL;
-	if (attr->keyring_id == KEY_SPEC_BPF_KEYRING) {
-		key = bpf_lookup_keyring();
-		if (!key) {
-			verbose(env, "the bpf keyring is empty or has not been restricted\n");
-			return -ENOKEY;
-		}
-	} else if (system_keyring_id_check(attr->keyring_id) == 0) {
+	if (!system_keyring_id_check(attr->keyring_id)) {
 		key = bpf_lookup_system_key(attr->keyring_id);
 	} else {
-		key = bpf_lookup_user_key(attr->keyring_id, 0);
+		if (attr->keyring_id != KEY_SPEC_BPF_KEYRING) {
+			if (bpf_keyring_enforced()) {
+				verbose(env, "caller-supplied keyring refused, use bpf keyring\n");
+				return -EPERM;
+			}
+			key = bpf_lookup_user_key(attr->keyring_id, 0);
+		} else {
+			key = bpf_lookup_keyring();
+		}
 	}
 	if (!key) {
-		verbose(env, "cannot resolve signing keyring with keyring_id %d\n",
-			attr->keyring_id);
-		return -EINVAL;
+		if (attr->keyring_id == KEY_SPEC_BPF_KEYRING) {
+			verbose(env, "the bpf keyring is empty or has not been restricted\n");
+			return -ENOKEY;
+		} else {
+			verbose(env, "cannot resolve signing keyring with keyring_id %d\n",
+				attr->keyring_id);
+			return -EINVAL;
+		}
 	}
 
 	sig = kvmemdup_bpfptr(usig, attr->signature_size);
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* [PATCH bpf-next v3 03/11] bpf: Raise the bound on a program's signature size
  2026-08-26 16:41 [PATCH bpf-next v3 00/11] BPF keyring and signed loader ML-DSA support Daniel Borkmann
  2026-08-26 16:41 ` [PATCH bpf-next v3 01/11] bpf, keys: Add a bpf keyring for program signature validation Daniel Borkmann
  2026-08-26 16:41 ` [PATCH bpf-next v3 02/11] bpf: Refuse caller-supplied keyrings when the bpf one is active Daniel Borkmann
@ 2026-08-26 16:41 ` Daniel Borkmann
  2026-08-26 16:41 ` [PATCH bpf-next v3 04/11] bpftool: Support ML-DSA program signing Daniel Borkmann
                   ` (7 subsequent siblings)
  10 siblings, 0 replies; 19+ messages in thread
From: Daniel Borkmann @ 2026-08-26 16:41 UTC (permalink / raw)
  To: memxor
  Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf,
	dhowells, jarkko, keyrings

signature_size is bounded by KMALLOC_MAX_CACHE_SIZE, which is 8 KiB on a
4 KiB page system. Back then we chose it somewhat arbitrarily and was
picked when a BPF program signature was RSA or ECDSA. ML-DSA (FIPS-204)
verification is wired through the X.509 and PKCS#7 parsers, and BPF
reaches them too via verify_pkcs7_signature() without having to know the
concrete algorithm. The bound becomes a bit too small, thus add an
explicit BPF_PROG_MAX_SIGNATURE_SIZE of 64 KiB and use that instead
to cover all options.

KMALLOC_MAX_CACHE_SIZE is PAGE_SIZE-derived, so what was accepted so
far depended on the page size which is not optimal as it should be the
same behavior on every configuration. On 64 KiB page kernels this lowers
the ceiling from 128 KiB to 64 KiB. Nothing that could have been verified
is affected as the largest signature the kernel implements is ML-DSA-87
at 4627 bytes.

Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
---
 kernel/bpf/verifier.c                             | 15 ++++++++++-----
 .../selftests/bpf/prog_tests/signed_loader.c      |  5 +++--
 2 files changed, 13 insertions(+), 7 deletions(-)

diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 095057277994..9440b6c71994 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -20973,6 +20973,14 @@ int bpf_fixup_kfunc_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
 	return 0;
 }
 
+/*
+ * Upper bound on the PKCS#7 signature blob passed with a program. Comfortably
+ * above the largest signature the kernel can verify, and far below anything
+ * that would make rejecting a load expensive. Deliberately a fixed number so
+ * that what the syscall accepts does not depend on PAGE_SIZE.
+ */
+#define BPF_PROG_MAX_SIGNATURE_SIZE	(64 * 1024)
+
 static enum bpf_sig_keyring bpf_classify_keyring(s32 keyring_id)
 {
 	switch (keyring_id) {
@@ -21012,13 +21020,10 @@ static int bpf_prog_verify_signature(struct bpf_verifier_env *env,
 	u64 data_sz;
 	int err = 0;
 
-	/*
-	 * Don't attempt to use kmalloc_large or vmalloc for signatures.
-	 * Practical signature for BPF program should be below this limit.
-	 */
 	if (!attr->signature_size ||
-	    attr->signature_size > KMALLOC_MAX_CACHE_SIZE)
+	    attr->signature_size > BPF_PROG_MAX_SIGNATURE_SIZE)
 		return -EINVAL;
+
 	if (!system_keyring_id_check(attr->keyring_id)) {
 		key = bpf_lookup_system_key(attr->keyring_id);
 	} else {
diff --git a/tools/testing/selftests/bpf/prog_tests/signed_loader.c b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
index 77381d345435..0c5294738d6c 100644
--- a/tools/testing/selftests/bpf/prog_tests/signed_loader.c
+++ b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
@@ -571,8 +571,9 @@ static void signature_too_large(void)
 
 	if (gen_loader_fixture_init(&f) == 0) {
 		/*
-		 * signature_size beyond the kernel's bound (KMALLOC_MAX_CACHE_SIZE)
-		 * is rejected before the buffer is read.
+		 * signature_size beyond the kernel's bound
+		 * (BPF_PROG_MAX_SIGNATURE_SIZE) 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, 0);
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* [PATCH bpf-next v3 04/11] bpftool: Support ML-DSA program signing
  2026-08-26 16:41 [PATCH bpf-next v3 00/11] BPF keyring and signed loader ML-DSA support Daniel Borkmann
                   ` (2 preceding siblings ...)
  2026-08-26 16:41 ` [PATCH bpf-next v3 03/11] bpf: Raise the bound on a program's signature size Daniel Borkmann
@ 2026-08-26 16:41 ` Daniel Borkmann
  2026-08-26 16:41 ` [PATCH bpf-next v3 05/11] selftests/bpf: Add a test for the sealed bpf keyring Daniel Borkmann
                   ` (6 subsequent siblings)
  10 siblings, 0 replies; 19+ messages in thread
From: Daniel Borkmann @ 2026-08-26 16:41 UTC (permalink / raw)
  To: memxor
  Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf,
	dhowells, jarkko, keyrings

Add bpftool support for ML-DSA program signing and drop the flag for
ML-DSA keys on affected OpenSSL versions, the same way as commit
0ad9a71933e7 ("modsign: Enable ML-DSA module signing").

Also, an ML-DSA-87 signature is 4627 bytes on its own, so the blob does
not fit into the 4 KiB of MAX_SIG_SIZE anymore and signing would fail
otherwise. Bump to 16 KiB. MAX_SIG_SIZE only sizes the buffer for
what bpftool itself emits (unrelated to BPF_PROG_MAX_SIGNATURE_SIZE).

Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
---
 tools/bpf/bpftool/main.h |  2 +-
 tools/bpf/bpftool/sign.c | 24 +++++++++++++++++++++---
 2 files changed, 22 insertions(+), 4 deletions(-)

diff --git a/tools/bpf/bpftool/main.h b/tools/bpf/bpftool/main.h
index 78b6e0ebb85d..9315a1db1f7c 100644
--- a/tools/bpf/bpftool/main.h
+++ b/tools/bpf/bpftool/main.h
@@ -57,7 +57,7 @@ static inline void *u64_to_ptr(__u64 ptr)
 	})
 
 #define ERR_MAX_LEN	1024
-#define MAX_SIG_SIZE	4096
+#define MAX_SIG_SIZE	16384
 
 #define BPF_TAG_FMT	"%02hhx%02hhx%02hhx%02hhx%02hhx%02hhx%02hhx%02hhx"
 
diff --git a/tools/bpf/bpftool/sign.c b/tools/bpf/bpftool/sign.c
index 88726a6db6d0..adbfd4ae5c02 100644
--- a/tools/bpf/bpftool/sign.c
+++ b/tools/bpf/bpftool/sign.c
@@ -130,6 +130,12 @@ __u32 register_session_key(const char *key_der_path)
 
 int bpftool_prog_sign(struct bpf_load_and_run_opts *opts)
 {
+	unsigned int signer_flags = CMS_NOCERTS | CMS_BINARY | CMS_NOSMIMECAP |
+#ifdef CMS_NO_SIGNING_TIME
+				    CMS_NO_SIGNING_TIME |
+#endif
+				    CMS_USE_KEYID | CMS_NOATTR;
+	const EVP_MD *cms_digest = EVP_sha256();
 	BIO *bd_in = NULL, *bd_out = NULL;
 	EVP_PKEY *private_key = NULL;
 	CMS_ContentInfo *cms = NULL;
@@ -167,6 +173,20 @@ int bpftool_prog_sign(struct bpf_load_and_run_opts *opts)
 		goto cleanup;
 	}
 
+#if OPENSSL_VERSION_NUMBER >= 0x30000000L && OPENSSL_VERSION_NUMBER < 0x40000000L
+	if (EVP_PKEY_is_a(private_key, "ML-DSA-44") ||
+	    EVP_PKEY_is_a(private_key, "ML-DSA-65") ||
+	    EVP_PKEY_is_a(private_key, "ML-DSA-87")) {
+		/*
+		 * See workaround in 0ad9a71933e7 ("modsign: Enable ML-DSA
+		 * module signing"). Kernel only accepts sha512, see also
+		 * 8bbdeb7a25b4 ("pkcs7, x509: Add ML-DSA support").
+		 */
+		signer_flags &= ~CMS_NOATTR;
+		cms_digest = EVP_sha512();
+	}
+#endif
+
 	cms = CMS_sign(NULL, NULL, NULL, NULL,
 		       CMS_NOCERTS | CMS_PARTIAL | CMS_BINARY | CMS_DETACHED |
 			       CMS_STREAM);
@@ -175,9 +195,7 @@ int bpftool_prog_sign(struct bpf_load_and_run_opts *opts)
 		goto cleanup;
 	}
 
-	if (!CMS_add1_signer(cms, x509, private_key, EVP_sha256(),
-			     CMS_NOCERTS | CMS_BINARY | CMS_NOSMIMECAP |
-			     CMS_USE_KEYID | CMS_NOATTR)) {
+	if (!CMS_add1_signer(cms, x509, private_key, cms_digest, signer_flags)) {
 		err = -EINVAL;
 		goto cleanup;
 	}
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* [PATCH bpf-next v3 05/11] selftests/bpf: Add a test for the sealed bpf keyring
  2026-08-26 16:41 [PATCH bpf-next v3 00/11] BPF keyring and signed loader ML-DSA support Daniel Borkmann
                   ` (3 preceding siblings ...)
  2026-08-26 16:41 ` [PATCH bpf-next v3 04/11] bpftool: Support ML-DSA program signing Daniel Borkmann
@ 2026-08-26 16:41 ` Daniel Borkmann
  2026-08-26 17:41   ` bot+bpf-ci
  2026-08-26 16:41 ` [PATCH bpf-next v3 06/11] selftests/bpf: Rebuild signed lskels when signing key changes Daniel Borkmann
                   ` (5 subsequent siblings)
  10 siblings, 1 reply; 19+ messages in thread
From: Daniel Borkmann @ 2026-08-26 16:41 UTC (permalink / raw)
  To: memxor
  Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf,
	dhowells, jarkko, keyrings

bpf_keyring_sealed checks that a load naming the bpf keyring fails while
the keyring has not been provisioned. It uses a junk signature as the
size check and the keyring lookup both happen before any crypto, so the
error under test is reached without a real signature and the ordering is
what gets verified:

  # LDLIBS=-static PKG_CONFIG='pkg-config --static' ./vmtest.sh -- ./test_progs -t signed_loader
  [...]
  #425/9   signed_loader/signed_module_kfunc_rejected:OK
  #425/10  signed_loader/signature_failure_logs:OK
  #425/11  signed_loader/signature_too_large:OK
  #425/12  signed_loader/signature_zero_size:OK
  #425/13  signed_loader/signature_bad_keyring:OK
  #425/14  signed_loader/bpf_keyring_sealed:OK
  #425/15  signed_loader/metadata_ctx_max_entries_ignored:OK
  #425/16  signed_loader/metadata_ctx_initial_value_ignored:OK
  #425/17  signed_loader/signature_authenticates_insns:OK
  #425/18  signed_loader/signature_authenticates_metadata:OK
  #425/19  signed_loader/hash_requires_frozen:OK
  [...]
  #425     signed_loader:OK
  Summary: 1/30 PASSED, 0 SKIPPED, 0/0 FAILED

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

diff --git a/tools/testing/selftests/bpf/prog_tests/signed_loader.c b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
index 0c5294738d6c..e30166c00be6 100644
--- a/tools/testing/selftests/bpf/prog_tests/signed_loader.c
+++ b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
@@ -32,8 +32,13 @@ enum {
 	BPF_SIG_KEYRING_SECONDARY,
 	BPF_SIG_KEYRING_PLATFORM,
 	BPF_SIG_KEYRING_USER,
+	BPF_SIG_KEYRING_BPF,
 };
 
+#ifndef KEY_SPEC_BPF_KEYRING
+#define KEY_SPEC_BPF_KEYRING	-9
+#endif
+
 static int load_loader(const void *insns, __u32 insns_sz, int map_fd,
 		       const void *sig, __u32 sig_sz, __s32 keyring_id,
 		       __u32 fd_array_cnt)
@@ -627,6 +632,83 @@ static void signature_bad_keyring(void)
 	gen_loader_fixture_fini(&f);
 }
 
+static bool keyring_unsealed_boot(void)
+{
+	char val = 0;
+	int fd;
+
+	fd = open("/sys/module/bpf/parameters/keyring_unsealed", O_RDONLY);
+	if (fd < 0)
+		return false;
+	if (read(fd, &val, 1) != 1)
+		val = 0;
+	close(fd);
+	return val == 'Y' || val == '1';
+}
+
+static int bpf_keyring_lookup(int *nr_keys)
+{
+	char line[512], type[32], desc[64];
+	int serial = -ENOENT;
+	FILE *f;
+
+	f = fopen("/proc/keys", "r");
+	if (!f)
+		return -errno;
+
+	while (fgets(line, sizeof(line), f)) {
+		unsigned int hex;
+		char *sum;
+
+		if (sscanf(line, "%x %*s %*s %*s %*s %*s %*s %31s %63s",
+			   &hex, type, desc) != 3)
+			continue;
+		if (strcmp(type, "keyring") || strcmp(desc, ".bpf:"))
+			continue;
+
+		serial = (int)hex;
+		if (nr_keys) {
+			sum = strstr(line, ".bpf: ");
+			*nr_keys = !sum || !strncmp(sum + 6, "empty", 5) ?
+				   0 : atoi(sum + 6);
+		}
+		break;
+	}
+	fclose(f);
+	return serial;
+}
+
+static void bpf_keyring_sealed(void)
+{
+	static const __u8 junk[64] = {};
+	struct gen_loader_fixture f;
+	int serial, key, fd;
+
+	if (keyring_unsealed_boot()) {
+		printf("%s:SKIP:the bpf keyring was unsealed at boot\n", __func__);
+		test__skip();
+		return;
+	}
+	serial = bpf_keyring_lookup(NULL);
+	if (serial >= 0) {
+		key = syscall(__NR_add_key, "user", "sealprobe", "x", 1,
+			      KEY_SPEC_BPF_KEYRING);
+		if (key >= 0)
+			syscall(__NR_keyctl, KEYCTL_UNLINK, key,
+				KEY_SPEC_BPF_KEYRING);
+		ASSERT_EQ(key < 0 ? -errno : 0, -EPERM,
+			  "nothing links into a sealed keyring");
+	}
+	if (gen_loader_fixture_init(&f) == 0) {
+		fd = load_loader(f.gopts.insns, f.gopts.insns_sz, -1, junk,
+				 sizeof(junk), KEY_SPEC_BPF_KEYRING, 0);
+		ASSERT_EQ(fd, -ENOKEY, "sealed bpf keyring rejected");
+		if (fd >= 0)
+			close(fd);
+	}
+	gen_loader_fixture_fini(&f);
+}
+
 /*
  * A signed loader must ignore ctx-supplied map dimensions: the host cannot
  * resize a signed program's maps via the loader ctx. Drive a one-map program
@@ -1806,6 +1888,8 @@ void test_signed_loader(void)
 		signature_zero_size();
 	if (test__start_subtest("signature_bad_keyring"))
 		signature_bad_keyring();
+	if (test__start_subtest("bpf_keyring_sealed"))
+		bpf_keyring_sealed();
 	if (test__start_subtest("metadata_ctx_max_entries_ignored"))
 		metadata_ctx_max_entries_ignored();
 	if (test__start_subtest("metadata_ctx_initial_value_ignored"))
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* [PATCH bpf-next v3 06/11] selftests/bpf: Rebuild signed lskels when signing key changes
  2026-08-26 16:41 [PATCH bpf-next v3 00/11] BPF keyring and signed loader ML-DSA support Daniel Borkmann
                   ` (4 preceding siblings ...)
  2026-08-26 16:41 ` [PATCH bpf-next v3 05/11] selftests/bpf: Add a test for the sealed bpf keyring Daniel Borkmann
@ 2026-08-26 16:41 ` Daniel Borkmann
  2026-08-26 16:41 ` [PATCH bpf-next v3 07/11] selftests/bpf: Rename the verify_sig_setup.sh setup into setup-rsa Daniel Borkmann
                   ` (4 subsequent siblings)
  10 siblings, 0 replies; 19+ messages in thread
From: Daniel Borkmann @ 2026-08-26 16:41 UTC (permalink / raw)
  To: memxor
  Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf,
	dhowells, jarkko, keyrings

The signing key is regenerated whenever verify_sig_setup.sh changes,
but the signed light skeletons only depend on the BPF object and on
bpftool, not on the key they are signed with. Thus, add the certificate
as a prereq so a new key forces the skeletons to be signed again.

Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
---
 tools/testing/selftests/bpf/Makefile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tools/testing/selftests/bpf/Makefile b/tools/testing/selftests/bpf/Makefile
index 5f1a3bfc0569..05b3ee64290f 100644
--- a/tools/testing/selftests/bpf/Makefile
+++ b/tools/testing/selftests/bpf/Makefile
@@ -663,7 +663,7 @@ $(TRUNNER_BPF_LSKELS): %.lskel.h: %.bpf.o $(BPFTOOL) | $(TRUNNER_OUTPUT)
 	}) && \
 	rm -f $$(<:.o=.llinked1.o) $$(<:.o=.llinked2.o) $$(<:.o=.llinked3.o)
 
-$(TRUNNER_BPF_LSKELS_SIGNED): %.lskel.h: %.bpf.o $(BPFTOOL) | $(TRUNNER_OUTPUT)
+$(TRUNNER_BPF_LSKELS_SIGNED): %.lskel.h: %.bpf.o $(BPFTOOL) $(VERIFICATION_CERT) | $(TRUNNER_OUTPUT)
 	$(Q)$(if $(PERMISSIVE),if [ ! -f $$< ]; then			\
 		$$(RM) $$@;						\
 		printf '  %-12s %s\n' 'SKIP-SKEL' '$$(notdir $$@)' 1>&2; \
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* [PATCH bpf-next v3 07/11] selftests/bpf: Rename the verify_sig_setup.sh setup into setup-rsa
  2026-08-26 16:41 [PATCH bpf-next v3 00/11] BPF keyring and signed loader ML-DSA support Daniel Borkmann
                   ` (5 preceding siblings ...)
  2026-08-26 16:41 ` [PATCH bpf-next v3 06/11] selftests/bpf: Rebuild signed lskels when signing key changes Daniel Borkmann
@ 2026-08-26 16:41 ` Daniel Borkmann
  2026-08-26 17:29   ` bot+bpf-ci
  2026-08-26 16:41 ` [PATCH bpf-next v3 08/11] selftests/bpf: Add an end-to-end ML-DSA signed loader test Daniel Borkmann
                   ` (3 subsequent siblings)
  10 siblings, 1 reply; 19+ messages in thread
From: Daniel Borkmann @ 2026-08-26 16:41 UTC (permalink / raw)
  To: memxor
  Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf,
	dhowells, jarkko, keyrings

The script's "setup" action generates an RSA key, enrolls it and builds
a keyring around it. The name says nothing about the algorithm, which is
fine while there is only one, but we'll add "setup-mldsa" soon, therefore
rename the existing one into "setup-rsa". No functional change.

Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
---
 .../selftests/bpf/prog_tests/signed_loader.c   | 18 +++++++++---------
 .../bpf/prog_tests/verify_pkcs7_sig.c          |  4 ++--
 .../testing/selftests/bpf/verify_sig_setup.sh  |  8 ++++----
 3 files changed, 15 insertions(+), 15 deletions(-)

diff --git a/tools/testing/selftests/bpf/prog_tests/signed_loader.c b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
index e30166c00be6..fbf14c7dd560 100644
--- a/tools/testing/selftests/bpf/prog_tests/signed_loader.c
+++ b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
@@ -462,7 +462,7 @@ static void signed_btf_fd_array_rejected(void)
 	dir = mkdtemp(dir_tmpl);
 	if (!ASSERT_OK_PTR(dir, "mkdtemp"))
 		return;
-	if (!ASSERT_OK(run_setup("setup", dir), "verify_sig_setup")) {
+	if (!ASSERT_OK(run_setup("setup-rsa", dir), "verify_sig_setup")) {
 		rmdir(dir);
 		return;
 	}
@@ -914,7 +914,7 @@ static void signature_authenticates_insns(void)
 	dir = mkdtemp(dir_tmpl);
 	if (!ASSERT_OK_PTR(dir, "mkdtemp"))
 		return;
-	if (!ASSERT_OK(run_setup("setup", dir), "verify_sig_setup")) {
+	if (!ASSERT_OK(run_setup("setup-rsa", dir), "verify_sig_setup")) {
 		rmdir(dir);
 		return;
 	}
@@ -1014,7 +1014,7 @@ static void signature_authenticates_metadata(void)
 	dir = mkdtemp(dir_tmpl);
 	if (!ASSERT_OK_PTR(dir, "mkdtemp"))
 		return;
-	if (!ASSERT_OK(run_setup("setup", dir), "verify_sig_setup")) {
+	if (!ASSERT_OK(run_setup("setup-rsa", dir), "verify_sig_setup")) {
 		rmdir(dir);
 		return;
 	}
@@ -1350,7 +1350,7 @@ static void lsm_signature_verdict(void)
 	dir = mkdtemp(dir_tmpl);
 	if (!ASSERT_OK_PTR(dir, "mkdtemp"))
 		goto out;
-	if (!ASSERT_OK(run_setup("setup", dir), "verify_sig_setup")) {
+	if (!ASSERT_OK(run_setup("setup-rsa", dir), "verify_sig_setup")) {
 		rmdir(dir);
 		dir = NULL;
 		goto out;
@@ -1533,7 +1533,7 @@ static void loadtime_verify(struct bpf_object *obj, int expect_maps)
 	dir = mkdtemp(dir_tmpl);
 	if (!ASSERT_OK_PTR(dir, "mkdtemp"))
 		return;
-	if (!ASSERT_OK(run_setup("setup", dir), "verify_sig_setup")) {
+	if (!ASSERT_OK(run_setup("setup-rsa", dir), "verify_sig_setup")) {
 		rmdir(dir);
 		return;
 	}
@@ -1631,7 +1631,7 @@ static void signed_no_fd_array(void)
 	dir = mkdtemp(dir_tmpl);
 	if (!ASSERT_OK_PTR(dir, "mkdtemp"))
 		return;
-	if (!ASSERT_OK(run_setup("setup", dir), "verify_sig_setup")) {
+	if (!ASSERT_OK(run_setup("setup-rsa", dir), "verify_sig_setup")) {
 		rmdir(dir);
 		return;
 	}
@@ -1702,7 +1702,7 @@ static void signed_map_by_fd_rejected(void)
 	dir = mkdtemp(dir_tmpl);
 	if (!ASSERT_OK_PTR(dir, "mkdtemp"))
 		goto out_map;
-	if (!ASSERT_OK(run_setup("setup", dir), "verify_sig_setup")) {
+	if (!ASSERT_OK(run_setup("setup-rsa", dir), "verify_sig_setup")) {
 		rmdir(dir);
 		goto out_map;
 	}
@@ -1764,7 +1764,7 @@ static void signed_sparse_fd_array_rejected(void)
 	dir = mkdtemp(dir_tmpl);
 	if (!ASSERT_OK_PTR(dir, "mkdtemp"))
 		goto out_map;
-	if (!ASSERT_OK(run_setup("setup", dir), "verify_sig_setup")) {
+	if (!ASSERT_OK(run_setup("setup-rsa", dir), "verify_sig_setup")) {
 		rmdir(dir);
 		goto out_map;
 	}
@@ -1818,7 +1818,7 @@ static void signed_module_kfunc_rejected(void)
 	dir = mkdtemp(dir_tmpl);
 	if (!ASSERT_OK_PTR(dir, "mkdtemp"))
 		return;
-	if (!ASSERT_OK(run_setup("setup", dir), "verify_sig_setup")) {
+	if (!ASSERT_OK(run_setup("setup-rsa", dir), "verify_sig_setup")) {
 		rmdir(dir);
 		return;
 	}
diff --git a/tools/testing/selftests/bpf/prog_tests/verify_pkcs7_sig.c b/tools/testing/selftests/bpf/prog_tests/verify_pkcs7_sig.c
index f327feb8e38c..12b146d205d7 100644
--- a/tools/testing/selftests/bpf/prog_tests/verify_pkcs7_sig.c
+++ b/tools/testing/selftests/bpf/prog_tests/verify_pkcs7_sig.c
@@ -257,7 +257,7 @@ static void test_verify_pkcs7_sig_from_map(void)
 	if (!ASSERT_OK_PTR(tmp_dir, "mkdtemp"))
 		return;
 
-	ret = _run_setup_process(tmp_dir, "setup");
+	ret = _run_setup_process(tmp_dir, "setup-rsa");
 	if (!ASSERT_OK(ret, "_run_setup_process"))
 		goto close_prog;
 
@@ -458,7 +458,7 @@ static void test_pkcs7_sig_fsverity(void)
 	snprintf(data_path, PATH_MAX, "%s/data-file", tmp_dir);
 	snprintf(sig_path, PATH_MAX, "%s/sig-file", tmp_dir);
 
-	ret = _run_setup_process(tmp_dir, "setup");
+	ret = _run_setup_process(tmp_dir, "setup-rsa");
 	if (!ASSERT_OK(ret, "_run_setup_process"))
 		goto out;
 
diff --git a/tools/testing/selftests/bpf/verify_sig_setup.sh b/tools/testing/selftests/bpf/verify_sig_setup.sh
index 09179fb551f0..202e6e6418fe 100755
--- a/tools/testing/selftests/bpf/verify_sig_setup.sh
+++ b/tools/testing/selftests/bpf/verify_sig_setup.sh
@@ -28,7 +28,7 @@ authorityKeyIdentifier=keyid
 
 usage()
 {
-	echo "Usage: $0 <setup|cleanup <existing_tmp_dir>"
+	echo "Usage: $0 <setup-rsa|cleanup <existing_tmp_dir>"
 	exit 1
 }
 
@@ -47,7 +47,7 @@ genkey()
 		${tmp_dir}/signing_key.der -outform der
 }
 
-setup()
+setup_rsa()
 {
 	local tmp_dir="$1"
 
@@ -108,8 +108,8 @@ main()
 
 	[[ ! -d "${tmp_dir}" ]] && echo "Directory ${tmp_dir} doesn't exist" && exit 1
 
-	if [[ "${action}" == "setup" ]]; then
-		setup "${tmp_dir}"
+	if [[ "${action}" == "setup-rsa" ]]; then
+		setup_rsa "${tmp_dir}"
 	elif [[ "${action}" == "genkey" ]]; then
 		genkey "${tmp_dir}"
 	elif [[ "${action}" == "cleanup" ]]; then
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* [PATCH bpf-next v3 08/11] selftests/bpf: Add an end-to-end ML-DSA signed loader test
  2026-08-26 16:41 [PATCH bpf-next v3 00/11] BPF keyring and signed loader ML-DSA support Daniel Borkmann
                   ` (6 preceding siblings ...)
  2026-08-26 16:41 ` [PATCH bpf-next v3 07/11] selftests/bpf: Rename the verify_sig_setup.sh setup into setup-rsa Daniel Borkmann
@ 2026-08-26 16:41 ` Daniel Borkmann
  2026-08-26 17:41   ` bot+bpf-ci
  2026-08-26 16:41 ` [PATCH bpf-next v3 09/11] selftests/bpf: Allow appending to guest kernel cmdline in vmtest.sh Daniel Borkmann
                   ` (2 subsequent siblings)
  10 siblings, 1 reply; 19+ messages in thread
From: Daniel Borkmann @ 2026-08-26 16:41 UTC (permalink / raw)
  To: memxor
  Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf,
	dhowells, jarkko, keyrings

The BPF signing is algorithm agnostic, but so far the BPF CI only
has tested a single one. BPF hands verify_pkcs7_signature() a keyring
and byte ranges, and everything below it already understands ML-DSA,
so add a test for ML-DSA signed program to validate it works as well.

  # LDLIBS=-static PKG_CONFIG='pkg-config --static' ./vmtest.sh -- ./test_progs -t signed_loader
  [...]
  #425/10  signed_loader/signature_failure_logs:OK
  #425/11  signed_loader/signature_too_large:OK
  #425/12  signed_loader/signature_zero_size:OK
  #425/13  signed_loader/signature_bad_keyring:OK
  #425/14  signed_loader/bpf_keyring_sealed:OK
  #425/15  signed_loader/mldsa_signed_load:OK
  #425/16  signed_loader/metadata_ctx_max_entries_ignored:OK
  #425/17  signed_loader/metadata_ctx_initial_value_ignored:OK
  #425/18  signed_loader/signature_authenticates_insns:OK
  #425/19  signed_loader/signature_authenticates_metadata:OK
  #425/20  signed_loader/hash_requires_frozen:OK
  [...]
  #425     signed_loader:OK
  Summary: 1/31 PASSED, 0 SKIPPED, 0/0 FAILED

Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
---
 tools/testing/selftests/bpf/config            |   2 +
 .../selftests/bpf/prog_tests/signed_loader.c  | 125 +++++++++++++++++-
 .../testing/selftests/bpf/verify_sig_setup.sh |  57 +++++++-
 3 files changed, 177 insertions(+), 7 deletions(-)

diff --git a/tools/testing/selftests/bpf/config b/tools/testing/selftests/bpf/config
index ea7044f30adc..2ec15117abb5 100644
--- a/tools/testing/selftests/bpf/config
+++ b/tools/testing/selftests/bpf/config
@@ -51,6 +51,8 @@ CONFIG_IPV6_SEG6_LWTUNNEL=y
 CONFIG_IPV6_SIT=y
 CONFIG_IPV6_TUNNEL=y
 CONFIG_KEYS=y
+CONFIG_CRYPTO_MLDSA=y
+CONFIG_CRYPTO_SHA512=y
 CONFIG_LIRC=y
 CONFIG_LIVEPATCH=y
 CONFIG_LWTUNNEL=y
diff --git a/tools/testing/selftests/bpf/prog_tests/signed_loader.c b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
index fbf14c7dd560..039b5d044545 100644
--- a/tools/testing/selftests/bpf/prog_tests/signed_loader.c
+++ b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
@@ -39,6 +39,12 @@ enum {
 #define KEY_SPEC_BPF_KEYRING	-9
 #endif
 
+/* verify_sig_setup.sh exits with this when openssl cannot do ML-DSA. */
+#define SETUP_SKIP		(-77)
+
+/* FIPS-204 ML-DSA-87 signature size, see include/crypto/mldsa.h. */
+#define MLDSA87_SIGNATURE_SIZE	4627
+
 static int load_loader(const void *insns, __u32 insns_sz, int map_fd,
 		       const void *sig, __u32 sig_sz, __s32 keyring_id,
 		       __u32 fd_array_cnt)
@@ -161,12 +167,30 @@ static int run_setup(const char *cmd, const char *dir)
 	}
 	if (waitpid(pid, &status, 0) < 0)
 		return -errno;
-	return (WIFEXITED(status) &&
-		WEXITSTATUS(status) == 0) ? 0 : -EINVAL;
+	if (!WIFEXITED(status))
+		return -EINVAL;
+	return -WEXITSTATUS(status);
 }
 
-static int sign_buf(const char *dir, const void *buf, __u32 len,
-		    void *sig, __u32 *sig_sz)
+static void genkey_dir_fini(const char *dir)
+{
+	static const char * const files[] = {
+		"signing_key.der", "signing_key.pem", "x509.genkey",
+	};
+	char path[PATH_MAX];
+	size_t i;
+
+	if (!dir)
+		return;
+	for (i = 0; i < ARRAY_SIZE(files); i++) {
+		snprintf(path, sizeof(path), "%s/%s", dir, files[i]);
+		unlink(path);
+	}
+	rmdir(dir);
+}
+
+static int sign_buf_digest(const char *dir, const void *buf, __u32 len,
+			   void *sig, __u32 *sig_sz, const char *digest)
 {
 	char data_tmpl[PATH_MAX], key[PATH_MAX];
 	char sigpath[PATH_MAX + sizeof(".p7s")];
@@ -195,7 +219,7 @@ static int sign_buf(const char *dir, const void *buf, __u32 len,
 	}
 	if (pid == 0) {
 		snprintf(key, sizeof(key), "%s/signing_key.pem", dir);
-		execlp("./sign-file", "./sign-file", "-d", "sha256",
+		execlp("./sign-file", "./sign-file", "-d", digest,
 		       key, key, data_tmpl, NULL);
 		exit(1);
 	}
@@ -233,6 +257,12 @@ static int sign_buf(const char *dir, const void *buf, __u32 len,
 	return ret;
 }
 
+static int sign_buf(const char *dir, const void *buf, __u32 len,
+		    void *sig, __u32 *sig_sz)
+{
+	return sign_buf_digest(dir, buf, len, sig, sig_sz, "sha256");
+}
+
 struct gen_loader_fixture {
 	struct test_signed_loader *skel;
 	struct gen_loader_opts gopts;
@@ -1607,6 +1637,89 @@ static void loadtime_with_map(void)
 	test_signed_loader_map__destroy(skel);
 }
 
+/*
+ * End-to-end signed load with a post-quantum key. ML-DSA (FIPS-204) is wired
+ * through the X.509 and PKCS#7 parsers, and BPF reaches them via
+ * verify_pkcs7_signature() without knowing the algorithm, so an ML-DSA key in
+ * the keyring should verify an ML-DSA signed program with no BPF-side work.
+ */
+static void mldsa_signed_load(void)
+{
+	char dir_tmpl[] = "/tmp/bpfmldsaXXXXXX";
+	int map_fd = -1, prog_fd = -1, err;
+	__u8 *sig = NULL, *buf = NULL;
+	struct gen_loader_fixture f;
+	bool have_fixture = false;
+	__u32 sig_sz = 16384;
+	char *dir;
+
+	syscall(__NR_request_key, "keyring", "_uid.0", NULL,
+		KEY_SPEC_SESSION_KEYRING);
+	dir = mkdtemp(dir_tmpl);
+	if (!ASSERT_OK_PTR(dir, "mkdtemp"))
+		return;
+
+	err = run_setup("setup-mldsa", dir);
+	if (err == SETUP_SKIP) {
+		printf("%s:SKIP:openssl has no ML-DSA support (needs 3.5+)\n",
+		       __func__);
+		test__skip();
+		genkey_dir_fini(dir);
+		return;
+	}
+	if (!ASSERT_OK(err, "verify_sig_setup setup-mldsa")) {
+		genkey_dir_fini(dir);
+		return;
+	}
+
+	sig = malloc(sig_sz);
+	if (!ASSERT_OK_PTR(sig, "sig buf"))
+		goto out;
+	have_fixture = true;
+	if (gen_loader_fixture_init(&f) != 0)
+		goto out;
+
+	buf = malloc((size_t)f.gopts.insns_sz + f.data_sz);
+	if (!ASSERT_OK_PTR(buf, "signbuf"))
+		goto out;
+	memcpy(buf, f.gopts.insns, f.gopts.insns_sz);
+	memcpy(buf + f.gopts.insns_sz, f.blob, f.data_sz);
+
+	/*
+	 * ML-DSA hashes the message itself, but openssl before 4.0 cannot
+	 * produce a CMS message without signedAttrs for it, and with those in
+	 * play only SHA-512 is permitted for the messageDigest attribute.
+	 */
+	if (!ASSERT_OK(sign_buf_digest(dir, buf, f.gopts.insns_sz + f.data_sz,
+				       sig, &sig_sz, "sha512"),
+		       "sign insns||metadata with ML-DSA"))
+		goto out;
+
+	/*
+	 * Guard against the setup silently handing back some other key type:
+	 * an RSA or ECDSA signature is a few hundred bytes, where an ML-DSA-87
+	 * one cannot be smaller than the raw signature it carries.
+	 */
+	ASSERT_GT(sig_sz, MLDSA87_SIGNATURE_SIZE, "ML-DSA-87 signature size");
+
+	map_fd = setup_meta_map(&f);
+	if (!ASSERT_OK_FD(map_fd, "meta_map"))
+		goto out;
+	prog_fd = load_loader(f.gopts.insns, f.gopts.insns_sz, map_fd, sig,
+			      sig_sz, KEY_SPEC_SESSION_KEYRING, 1);
+	ASSERT_OK_FD(prog_fd, "ML-DSA signed loader load");
+out:
+	if (prog_fd >= 0)
+		close(prog_fd);
+	if (map_fd >= 0)
+		close(map_fd);
+	if (have_fixture)
+		gen_loader_fixture_fini(&f);
+	free(buf);
+	free(sig);
+	run_setup("cleanup", dir);
+}
+
 /*
  * A signed program need not bind any map. A plain BPF_PROG_TYPE_SYSCALL
  * program with no fd_array is signed over its instructions alone: the kernel
@@ -1890,6 +2003,8 @@ void test_signed_loader(void)
 		signature_bad_keyring();
 	if (test__start_subtest("bpf_keyring_sealed"))
 		bpf_keyring_sealed();
+	if (test__start_subtest("mldsa_signed_load"))
+		mldsa_signed_load();
 	if (test__start_subtest("metadata_ctx_max_entries_ignored"))
 		metadata_ctx_max_entries_ignored();
 	if (test__start_subtest("metadata_ctx_initial_value_ignored"))
diff --git a/tools/testing/selftests/bpf/verify_sig_setup.sh b/tools/testing/selftests/bpf/verify_sig_setup.sh
index 202e6e6418fe..2737c1a2bcfd 100755
--- a/tools/testing/selftests/bpf/verify_sig_setup.sh
+++ b/tools/testing/selftests/bpf/verify_sig_setup.sh
@@ -28,7 +28,7 @@ authorityKeyIdentifier=keyid
 
 usage()
 {
-	echo "Usage: $0 <setup-rsa|cleanup <existing_tmp_dir>"
+	echo "Usage: $0 <setup-rsa|setup-mldsa|cleanup <existing_tmp_dir>"
 	exit 1
 }
 
@@ -57,6 +57,57 @@ setup_rsa()
 	keyctl link $key_id $keyring_id
 }
 
+mldsa_supported()
+{
+	local tmp_dir="$1"
+
+	genkey_mldsa "${tmp_dir}" || return 1
+	: > ${tmp_dir}/probe
+	# Same digest as the caller signs with, see sign_buf_digest().
+	./sign-file -d sha512 ${tmp_dir}/signing_key.pem \
+		${tmp_dir}/signing_key.pem ${tmp_dir}/probe || return 1
+	rm -f ${tmp_dir}/probe ${tmp_dir}/probe.p7s
+}
+
+genkey_mldsa()
+{
+	local tmp_dir="$1"
+
+	echo "${x509_genkey_content}" > ${tmp_dir}/x509.genkey
+
+	# No -<digest> here: ML-DSA hashes the message itself, and openssl
+	# rejects an explicit digest for it.
+	openssl req -new -nodes -utf8 -days 36500 \
+			-batch -x509 -newkey ML-DSA-87 \
+			-config ${tmp_dir}/x509.genkey \
+			-outform PEM -out ${tmp_dir}/signing_key.pem \
+			-keyout ${tmp_dir}/signing_key.pem 2>&1
+
+	openssl x509 -in ${tmp_dir}/signing_key.pem -out \
+		${tmp_dir}/signing_key.der -outform der
+}
+
+mldsa_skip()
+{
+	local tmp_dir="$1"
+
+	rm -f ${tmp_dir}/x509.genkey ${tmp_dir}/signing_key.pem \
+		${tmp_dir}/signing_key.der ${tmp_dir}/probe \
+		${tmp_dir}/probe.p7s
+	exit 77
+}
+
+setup_mldsa()
+{
+	local tmp_dir="$1"
+
+	mldsa_supported "${tmp_dir}" || mldsa_skip "${tmp_dir}"
+	key_id=$(cat ${tmp_dir}/signing_key.der |
+		 keyctl padd asymmetric ebpf_testing_key @s)
+	keyring_id=$(keyctl newring ebpf_testing_keyring @s)
+	keyctl link $key_id $keyring_id
+}
+
 cleanup() {
 	local tmp_dir="$1"
 
@@ -91,7 +142,7 @@ catch()
 	local exit_code="$1"
 	local log_file="$2"
 
-	if [[ "${exit_code}" -ne 0 ]]; then
+	if [[ "${exit_code}" -ne 0 && "${exit_code}" -ne 77 ]]; then
 		cat "${log_file}" >&3
 	fi
 
@@ -110,6 +161,8 @@ main()
 
 	if [[ "${action}" == "setup-rsa" ]]; then
 		setup_rsa "${tmp_dir}"
+	elif [[ "${action}" == "setup-mldsa" ]]; then
+		setup_mldsa "${tmp_dir}"
 	elif [[ "${action}" == "genkey" ]]; then
 		genkey "${tmp_dir}"
 	elif [[ "${action}" == "cleanup" ]]; then
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* [PATCH bpf-next v3 09/11] selftests/bpf: Allow appending to guest kernel cmdline in vmtest.sh
  2026-08-26 16:41 [PATCH bpf-next v3 00/11] BPF keyring and signed loader ML-DSA support Daniel Borkmann
                   ` (7 preceding siblings ...)
  2026-08-26 16:41 ` [PATCH bpf-next v3 08/11] selftests/bpf: Add an end-to-end ML-DSA signed loader test Daniel Borkmann
@ 2026-08-26 16:41 ` Daniel Borkmann
  2026-08-26 16:41 ` [PATCH bpf-next v3 10/11] selftests/bpf: Add tests for bpf keyring in signed loader Daniel Borkmann
  2026-08-26 16:41 ` [PATCH bpf-next v3 11/11] Documentation/bpf: Document the bpf keyring and improve examples Daniel Borkmann
  10 siblings, 0 replies; 19+ messages in thread
From: Daniel Borkmann @ 2026-08-26 16:41 UTC (permalink / raw)
  To: memxor
  Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf,
	dhowells, jarkko, keyrings

vmtest.sh currently hardcodes the guest command line, so there is no
way to ask for such a setting without editing the script. Append
$KERNEL_CMDLINE_EXTRA when set so it can be used for testing the
BPF keyring:

   # KERNEL_CMDLINE_EXTRA="bpf.keyring_unsealed=1" \
	./vmtest.sh -- ./test_progs -t signed_loader

Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
---
 tools/testing/selftests/bpf/vmtest.sh | 16 +++++++++++++++-
 1 file changed, 15 insertions(+), 1 deletion(-)

diff --git a/tools/testing/selftests/bpf/vmtest.sh b/tools/testing/selftests/bpf/vmtest.sh
index 6a3d026d76bd..e7e0b419a0b8 100755
--- a/tools/testing/selftests/bpf/vmtest.sh
+++ b/tools/testing/selftests/bpf/vmtest.sh
@@ -107,6 +107,14 @@ Options:
 	-s)		Instead of powering off the VM, start an interactive
 			shell. If <command> is specified, the shell runs after
 			the command finishes executing
+
+Environment variables:
+
+	KERNEL_CMDLINE_EXTRA
+			Extra arguments to append to the guest kernel command
+			line, for tests that need a boot-time setting. e.g:
+
+	  KERNEL_CMDLINE_EXTRA="bpf.keyring_unsealed=1" $0 -- ./test_progs -t signed_loader
 EOF
 }
 
@@ -286,6 +294,12 @@ EOF
 		QEMU_FLAGS=("${HOST_FLAGS[@]}")
 	fi
 
+	local kernel_cmdline="root=/dev/vda rw console=${QEMU_CONSOLE}"
+
+	if [[ -n "${KERNEL_CMDLINE_EXTRA:-}" ]]; then
+		kernel_cmdline+=" ${KERNEL_CMDLINE_EXTRA}"
+	fi
+
 	${QEMU_BINARY} \
 		-nodefaults \
 		-display none \
@@ -294,7 +308,7 @@ EOF
 		-m 4G \
 		-drive file="${rootfs_img}",format=raw,index=1,media=disk,if=virtio,cache=none \
 		-kernel "${kernel_bzimage}" \
-		-append "root=/dev/vda rw console=${QEMU_CONSOLE}"
+		-append "${kernel_cmdline}"
 }
 
 copy_logs()
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* [PATCH bpf-next v3 10/11] selftests/bpf: Add tests for bpf keyring in signed loader
  2026-08-26 16:41 [PATCH bpf-next v3 00/11] BPF keyring and signed loader ML-DSA support Daniel Borkmann
                   ` (8 preceding siblings ...)
  2026-08-26 16:41 ` [PATCH bpf-next v3 09/11] selftests/bpf: Allow appending to guest kernel cmdline in vmtest.sh Daniel Borkmann
@ 2026-08-26 16:41 ` Daniel Borkmann
  2026-08-26 17:54   ` bot+bpf-ci
  2026-08-26 16:41 ` [PATCH bpf-next v3 11/11] Documentation/bpf: Document the bpf keyring and improve examples Daniel Borkmann
  10 siblings, 1 reply; 19+ messages in thread
From: Daniel Borkmann @ 2026-08-26 16:41 UTC (permalink / raw)
  To: memxor
  Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf,
	dhowells, jarkko, keyrings

bpf_keyring_provisioned walks the keyring through its whole lifecycle in
one boot for ease of testing. It enrolls a freshly generated key into the
bpf keyring, confirms a load is still refused with -ENOKEY while the keyring
carries no restriction, then restricts it, and only then does the same
signed BPF program load with the bpf keyring. A caller-supplied keyring is
asserted to be refused both before and after the restriction, since what
refuses it is bpf.keyring_unsealed=1 rather than the state of the keyring.

Unsealing is a boot-time decision which also refuses the session keyring
that every other subtest here signs against, so such a boot goes straight
to this test and a regular run covers the rest. Without bpf.keyring_unsealed=1
on the vmtest guest command line the subtest is not registered at all, since
a sealed keyring can never be provisioned.

Regular run:

  # LDLIBS=-static PKG_CONFIG='pkg-config --static' ./vmtest.sh -- ./test_progs -t signed_loader
  [...]
  #425/12  signed_loader/signature_zero_size:OK
  #425/13  signed_loader/signature_bad_keyring:OK
  #425/14  signed_loader/bpf_keyring_sealed:OK
  [...]
  #425/30  signed_loader/signed_map_by_fd_rejected:OK
  #425/31  signed_loader/signed_sparse_fd_array_rejected:OK
  #425     signed_loader:OK
  Summary: 1/31 PASSED, 0 SKIPPED, 0/0 FAILED

Unsealed run:

  # KERNEL_CMDLINE_EXTRA="bpf.keyring_unsealed=1" \
    LDLIBS=-static PKG_CONFIG='pkg-config --static' ./vmtest.sh -- ./test_progs -t signed_loader
  #425/1   signed_loader/bpf_keyring_provisioned:OK
  #425     signed_loader:OK
  Summary: 1/1 PASSED, 0 SKIPPED, 0/0 FAILED

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

diff --git a/tools/testing/selftests/bpf/prog_tests/signed_loader.c b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
index 039b5d044545..eb78525d3359 100644
--- a/tools/testing/selftests/bpf/prog_tests/signed_loader.c
+++ b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
@@ -45,9 +45,9 @@ enum {
 /* FIPS-204 ML-DSA-87 signature size, see include/crypto/mldsa.h. */
 #define MLDSA87_SIGNATURE_SIZE	4627
 
-static int load_loader(const void *insns, __u32 insns_sz, int map_fd,
-		       const void *sig, __u32 sig_sz, __s32 keyring_id,
-		       __u32 fd_array_cnt)
+static int load_loader_log(const void *insns, __u32 insns_sz, int map_fd,
+			   const void *sig, __u32 sig_sz, __s32 keyring_id,
+			   __u32 fd_array_cnt, char *log_buf, __u32 log_sz)
 {
 	union bpf_attr attr;
 	int fd;
@@ -59,18 +59,31 @@ static int load_loader(const void *insns, __u32 insns_sz, int map_fd,
 	attr.license = ptr_to_u64("Dual BSD/GPL");
 	attr.prog_flags = BPF_F_SLEEPABLE;
 	attr.fd_array = ptr_to_u64(&map_fd);
+	attr.fd_array_cnt = fd_array_cnt;
 	if (sig) {
 		attr.signature = ptr_to_u64(sig);
 		attr.signature_size = sig_sz;
 		attr.keyring_id = keyring_id;
 	}
-	attr.fd_array_cnt = fd_array_cnt;
+	if (log_buf) {
+		attr.log_level = 1;
+		attr.log_buf = ptr_to_u64(log_buf);
+		attr.log_size = log_sz;
+	}
 	memcpy(attr.prog_name, "__loader.prog", sizeof("__loader.prog"));
 	fd = syscall(__NR_bpf, BPF_PROG_LOAD, &attr,
 		     offsetofend(union bpf_attr, keyring_id));
 	return fd < 0 ? -errno : fd;
 }
 
+static int load_loader(const void *insns, __u32 insns_sz, int map_fd,
+		       const void *sig, __u32 sig_sz, __s32 keyring_id,
+		       __u32 fd_array_cnt)
+{
+	return load_loader_log(insns, insns_sz, map_fd, sig, sig_sz, keyring_id,
+			       fd_array_cnt, NULL, 0);
+}
+
 static int run_gen_loader(const void *insns, __u32 insns_sz,
 			  const void *data, __u32 data_sz,
 			  const void *excl, __u32 excl_sz,
@@ -205,6 +218,7 @@ static int sign_buf_digest(const char *dir, const void *buf, __u32 len,
 	fd = mkstemp(data_tmpl);
 	if (fd < 0)
 		return -errno;
+	snprintf(sigpath, sizeof(sigpath), "%s.p7s", data_tmpl);
 	if (write(fd, buf, len) != (ssize_t)len) {
 		close(fd);
 		ret = -EIO;
@@ -229,30 +243,28 @@ static int sign_buf_digest(const char *dir, const void *buf, __u32 len,
 		goto out;
 	}
 
-	snprintf(sigpath, sizeof(sigpath), "%s.p7s", data_tmpl);
 	if (stat(sigpath, &st) < 0) {
 		ret = -errno;
 		goto out;
 	}
 	if (st.st_size > (off_t)*sig_sz) {
 		ret = -E2BIG;
-		goto out_sig;
+		goto out;
 	}
 	fd = open(sigpath, O_RDONLY);
 	if (fd < 0) {
 		ret = -errno;
-		goto out_sig;
+		goto out;
 	}
 	if (read(fd, sig, st.st_size) != st.st_size) {
 		close(fd);
 		ret = -EIO;
-		goto out_sig;
+		goto out;
 	}
 	close(fd);
 	*sig_sz = st.st_size;
-out_sig:
-	unlink(sigpath);
 out:
+	unlink(sigpath);
 	unlink(data_tmpl);
 	return ret;
 }
@@ -564,7 +576,6 @@ static void signature_failure_logs(void)
 	static const __u8 junk[64] = { 0x30, 0x42, 0x13, 0x37, };
 	char log_buf[1024] = {};
 	struct gen_loader_fixture f;
-	union bpf_attr attr;
 	int fd;
 
 	if (gen_loader_fixture_init(&f) == 0) {
@@ -573,22 +584,9 @@ static void signature_failure_logs(void)
 		 * failure is reported through the verifier log. A present-but-
 		 * invalid signature is rejected and the log says why.
 		 */
-		memset(&attr, 0, sizeof(attr));
-		attr.prog_type = BPF_PROG_TYPE_SYSCALL;
-		attr.insns = ptr_to_u64(f.gopts.insns);
-		attr.insn_cnt = f.gopts.insns_sz / sizeof(struct bpf_insn);
-		attr.license = ptr_to_u64("Dual BSD/GPL");
-		attr.prog_flags = BPF_F_SLEEPABLE;
-		attr.signature = ptr_to_u64(junk);
-		attr.signature_size = sizeof(junk);
-		attr.keyring_id = KEY_SPEC_SESSION_KEYRING;
-		attr.log_level = 1;
-		attr.log_buf = ptr_to_u64(log_buf);
-		attr.log_size = sizeof(log_buf);
-		memcpy(attr.prog_name, "__loader.prog", sizeof("__loader.prog"));
-
-		fd = syscall(__NR_bpf, BPF_PROG_LOAD, &attr,
-			     offsetofend(union bpf_attr, keyring_id));
+		fd = load_loader_log(f.gopts.insns, f.gopts.insns_sz, -1, junk,
+				     sizeof(junk), KEY_SPEC_SESSION_KEYRING, 0,
+				     log_buf, sizeof(log_buf));
 		ASSERT_LT(fd, 0, "invalid signature rejected at load");
 		if (fd >= 0)
 			close(fd);
@@ -708,6 +706,23 @@ static int bpf_keyring_lookup(int *nr_keys)
 	return serial;
 }
 
+static long keyctl_ret(int cmd, unsigned long arg2, unsigned long arg3)
+{
+	long ret = syscall(__NR_keyctl, cmd, arg2, arg3);
+
+	return ret < 0 ? -errno : ret;
+}
+
+/*
+ * What the bpf keyring still needs once it got provisioned: KEY_POS_SEARCH
+ * for the in-kernel search during verification, and the user view/read bits
+ * so it stays visible in /proc/keys, rest is dropped so the enrolled is
+ * therefore final.
+ */
+#define BPF_KEYRING_PERM_LOCKED		0x08030000
+/* What bpf_keyring_init() grants at boot. */
+#define BPF_KEYRING_PERM_INITIAL	0x082f0000
+
 static void bpf_keyring_sealed(void)
 {
 	static const __u8 junk[64] = {};
@@ -721,6 +736,9 @@ static void bpf_keyring_sealed(void)
 	}
 	serial = bpf_keyring_lookup(NULL);
 	if (serial >= 0) {
+		ASSERT_EQ(keyctl_ret(KEYCTL_GET_KEYRING_ID,
+				     KEY_SPEC_BPF_KEYRING, 0), serial,
+			  "KEY_SPEC_BPF_KEYRING resolves to the bpf keyring");
 		key = syscall(__NR_add_key, "user", "sealprobe", "x", 1,
 			      KEY_SPEC_BPF_KEYRING);
 		if (key >= 0)
@@ -739,6 +757,183 @@ static void bpf_keyring_sealed(void)
 	gen_loader_fixture_fini(&f);
 }
 
+static int try_load(const struct gen_loader_fixture *f, const void *sig,
+		    __u32 sig_sz, __s32 keyring_id, char *log_buf, __u32 log_sz)
+{
+	int map_fd, prog_fd;
+
+	map_fd = setup_meta_map(f);
+	if (!ASSERT_OK_FD(map_fd, "meta_map"))
+		return map_fd;
+	prog_fd = load_loader_log(f->gopts.insns, f->gopts.insns_sz, map_fd,
+				  sig, sig_sz, keyring_id, 1, log_buf, log_sz);
+	close(map_fd);
+	if (prog_fd >= 0)
+		close(prog_fd);
+	return prog_fd;
+}
+
+/*
+ * This needs bpf.keyring_unsealed=1 on the guest kernel command line, which
+ * vmtest.sh can pass via KERNEL_CMDLINE_EXTRA. There is no way to unseal the
+ * keyring from here, so without it the test skips. It also only works once
+ * per boot, as restricting a keyring cannot be undone.
+ */
+static void bpf_keyring_provisioned(void)
+{
+	char dir_tmpl[] = "/tmp/bpfkeyringXXXXXX";
+	char bad_tmpl[] = "/tmp/bpfkeyringbadXXXXXX";
+	__u8 *sig = NULL, *bad = NULL, *buf = NULL;
+	int serial, err;
+	int nr_keys = 0, der_fd = -1;
+	struct gen_loader_fixture f;
+	__u32 sig_sz = 8192, bad_sz;
+	bool have_fixture = false;
+	char *dir, *bad_dir = NULL;
+	char log_buf[1024] = {};
+	char path[PATH_MAX];
+	__u8 der[4096];
+	ssize_t der_sz;
+
+	serial = bpf_keyring_lookup(&nr_keys);
+	if (serial < 0) {
+		printf("%s:SKIP:no bpf keyring (needs CONFIG_KEYS)\n", __func__);
+		test__skip();
+		return;
+	}
+	if (nr_keys != 0) {
+		printf("%s:SKIP:the bpf keyring has already been provisioned\n",
+		       __func__);
+		test__skip();
+		return;
+	}
+
+	dir = mkdtemp(dir_tmpl);
+	if (!ASSERT_OK_PTR(dir, "mkdtemp"))
+		return;
+	if (!ASSERT_OK(run_setup("genkey", dir), "verify_sig_setup genkey"))
+		goto rmdir;
+
+	snprintf(path, sizeof(path), "%s/signing_key.der", dir);
+	der_fd = open(path, O_RDONLY);
+	if (!ASSERT_OK_FD(der_fd, "open signing_key.der"))
+		goto rmdir;
+	der_sz = read(der_fd, der, sizeof(der));
+	close(der_fd);
+	if (!ASSERT_GT(der_sz, 0, "read signing_key.der"))
+		goto rmdir;
+
+	ASSERT_EQ(keyctl_ret(KEYCTL_GET_KEYRING_ID, KEY_SPEC_BPF_KEYRING, 0),
+		  serial, "KEY_SPEC_BPF_KEYRING resolves to the bpf keyring");
+
+	err = syscall(__NR_add_key, "asymmetric", "", der, (size_t)der_sz,
+		      KEY_SPEC_BPF_KEYRING);
+	if (err < 0 && errno == EPERM) {
+		printf("%s:SKIP:the bpf keyring is sealed, need bpf.keyring_unsealed=1\n",
+		       __func__);
+		test__skip();
+		goto rmdir;
+	}
+	if (!ASSERT_GE(err, 0, "add the signing key to the bpf keyring"))
+		goto rmdir;
+
+	sig = malloc(sig_sz);
+	if (!ASSERT_OK_PTR(sig, "sig buf"))
+		goto out;
+	have_fixture = true;
+	if (gen_loader_fixture_init(&f) != 0)
+		goto out;
+
+	buf = malloc((size_t)f.gopts.insns_sz + f.data_sz);
+	if (!ASSERT_OK_PTR(buf, "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,
+				&sig_sz), "sign insns||metadata"))
+		goto out;
+
+	ASSERT_EQ(try_load(&f, sig, sig_sz, KEY_SPEC_BPF_KEYRING, NULL, 0),
+		  -ENOKEY, "unrestricted keyring still not consulted");
+
+	ASSERT_EQ(try_load(&f, sig, sig_sz, KEY_SPEC_SESSION_KEYRING, NULL, 0),
+		  -EPERM, "caller-supplied keyring refused before provisioning");
+
+	if (!ASSERT_OK(syscall(__NR_keyctl, KEYCTL_RESTRICT_KEYRING,
+			       KEY_SPEC_BPF_KEYRING, NULL, NULL),
+		       "restrict bpf keyring"))
+		goto out;
+
+	if (!ASSERT_OK_FD(try_load(&f, sig, sig_sz, KEY_SPEC_BPF_KEYRING,
+				   NULL, 0),
+			  "load signed by a key in the .bpf keyring"))
+		goto out;
+
+	bad_dir = mkdtemp(bad_tmpl);
+	if (!ASSERT_OK_PTR(bad_dir, "mkdtemp unenrolled"))
+		goto out;
+	if (!ASSERT_OK(run_setup("genkey", bad_dir), "verify_sig_setup genkey unenrolled"))
+		goto out;
+	bad_sz = 8192;
+	bad = malloc(bad_sz);
+	if (!ASSERT_OK_PTR(bad, "bad sig buf"))
+		goto out;
+	if (!ASSERT_OK(sign_buf(bad_dir, buf, f.gopts.insns_sz + f.data_sz, bad,
+				&bad_sz), "sign with an unenrolled key"))
+		goto out;
+
+	ASSERT_EQ(try_load(&f, bad, bad_sz, KEY_SPEC_BPF_KEYRING, log_buf,
+			   sizeof(log_buf)), -ENOKEY,
+		  "key outside the bpf keyring refused");
+	ASSERT_HAS_SUBSTR(log_buf, "signature verification failed",
+			  "the bpf keyring was consulted");
+
+	f.blob[0] ^= 0xff;
+	err = try_load(&f, sig, sig_sz, KEY_SPEC_BPF_KEYRING, NULL, 0);
+	f.blob[0] ^= 0xff;
+	ASSERT_EQ(err, -EKEYREJECTED, "tampered metadata refused");
+
+	ASSERT_EQ(try_load(&f, sig, sig_sz, KEY_SPEC_SESSION_KEYRING, NULL, 0),
+		  -EPERM, "caller-supplied keyring refused once .bpf is in use");
+
+	err = keyctl_ret(KEYCTL_UNLINK, KEY_SPEC_SESSION_KEYRING, serial);
+	ASSERT_EQ(err, -ENOENT, "keyring writable while the user bits are there");
+
+	err = keyctl_ret(KEYCTL_SETPERM, serial, BPF_KEYRING_PERM_LOCKED);
+	if (!ASSERT_OK(err, "drop the user bits on the bpf keyring"))
+		goto out;
+
+	ASSERT_OK_FD(try_load(&f, sig, sig_sz, KEY_SPEC_BPF_KEYRING, NULL, 0),
+		     "load still verified against the locked keyring");
+	ASSERT_EQ(keyctl_ret(KEYCTL_GET_KEYRING_ID, KEY_SPEC_BPF_KEYRING, 0),
+		  -EACCES, "the special id grants no rights of its own");
+
+	err = keyctl_ret(KEYCTL_UNLINK, KEY_SPEC_SESSION_KEYRING, serial);
+	ASSERT_EQ(err, -EACCES, "unlink refused");
+	err = keyctl_ret(KEYCTL_CLEAR, serial, 0);
+	ASSERT_EQ(err, -EACCES, "clear refused");
+	err = keyctl_ret(KEYCTL_REVOKE, serial, 0);
+	ASSERT_EQ(err, -EACCES, "revoke refused");
+	err = keyctl_ret(KEYCTL_INVALIDATE, serial, 0);
+	ASSERT_EQ(err, -EACCES, "invalidate refused");
+	err = keyctl_ret(KEYCTL_SET_TIMEOUT, serial, 1);
+	ASSERT_EQ(err, -EACCES, "timeout refused");
+	err = keyctl_ret(KEYCTL_SETPERM, serial, BPF_KEYRING_PERM_INITIAL);
+	ASSERT_EQ(err, -EACCES, "the bits cannot be granted back");
+
+	ASSERT_EQ(bpf_keyring_lookup(&nr_keys), serial, "keyring still there");
+	ASSERT_EQ(nr_keys, 1, "the enrolled key survived");
+out:
+	if (have_fixture)
+		gen_loader_fixture_fini(&f);
+	genkey_dir_fini(bad_dir);
+	free(buf);
+	free(bad);
+	free(sig);
+rmdir:
+	genkey_dir_fini(dir);
+}
+
 /*
  * A signed loader must ignore ctx-supplied map dimensions: the host cannot
  * resize a signed program's maps via the loader ctx. Drive a one-map program
@@ -1975,6 +2170,11 @@ static void signed_module_kfunc_rejected(void)
 
 void test_signed_loader(void)
 {
+	if (keyring_unsealed_boot()) {
+		if (test__start_subtest("bpf_keyring_provisioned"))
+			bpf_keyring_provisioned();
+		return;
+	}
 	if (test__start_subtest("loadtime_no_map"))
 		loadtime_no_map();
 	if (test__start_subtest("loadtime_with_map"))
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* [PATCH bpf-next v3 11/11] Documentation/bpf: Document the bpf keyring and improve examples
  2026-08-26 16:41 [PATCH bpf-next v3 00/11] BPF keyring and signed loader ML-DSA support Daniel Borkmann
                   ` (9 preceding siblings ...)
  2026-08-26 16:41 ` [PATCH bpf-next v3 10/11] selftests/bpf: Add tests for bpf keyring in signed loader Daniel Borkmann
@ 2026-08-26 16:41 ` Daniel Borkmann
  2026-08-26 17:41   ` bot+bpf-ci
  10 siblings, 1 reply; 19+ messages in thread
From: Daniel Borkmann @ 2026-08-26 16:41 UTC (permalink / raw)
  To: memxor
  Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf,
	dhowells, jarkko, keyrings

Key generation is detailled for RSA and ML-DSA, the load example sets
keyring_id to the bpf keyring with the session keyring shown only as
the staging variant, and the LSM admission example anchors on the bpf
keyring while allowlisting staged serials rather than treating a user
keyring as ordinary trust.

Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
---
 Documentation/bpf/signing.rst | 283 +++++++++++++++++++++++++++++-----
 1 file changed, 246 insertions(+), 37 deletions(-)

diff --git a/Documentation/bpf/signing.rst b/Documentation/bpf/signing.rst
index e73eaaebd8b1..9ea24dad5e9f 100644
--- a/Documentation/bpf/signing.rst
+++ b/Documentation/bpf/signing.rst
@@ -254,21 +254,25 @@ returned. Only after the program has fully loaded, at the next hook
 (``security_bpf_prog()``), does ``BPF_SIG_VERIFIED`` carry its full meaning:
 validly signed *and* fully verified.
 
-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.)
+A more realistic admission policy than "is it signed at all": base trust in
+the bpf keyring, accept a staging signature only while the key/keyring the
+program 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. */
+    /*
+     * Serials of caller-supplied keyrings we are willing to stage. Empty
+     * on a system that has committed to the bpf keyring, where the kernel
+     * refuses them anyway.
+     */
     struct {
             __uint(type, BPF_MAP_TYPE_HASH);
             __type(key, __s32);             /* keyring_serial */
             __type(value, __u8);
             __uint(max_entries, 64);
-    } trusted_user_keys SEC(".maps");
+    } staging_keys SEC(".maps");
 
     /* Audit stream consumed by a userspace logger. */
     struct {
@@ -291,11 +295,19 @@ auditable. (Illustrative - error checking elided.)
             if (kernel)
                     return 0;                       /* trust in-kernel loads */
 
-            if (verdict != BPF_SIG_VERIFIED)
+            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 */
+            } else switch (ktype) {
+            case BPF_SIG_KEYRING_BUILTIN:
+            case BPF_SIG_KEYRING_BPF:
+                    break;
+            case BPF_SIG_KEYRING_USER:
+                    if (!bpf_map_lookup_elem(&staging_keys, &serial))
+                            ret = -EPERM;
+                    break;
+            default:
+                    ret = -EPERM;                   /* keyring not in policy */
+            }
 
             d = bpf_ringbuf_reserve(&audit, sizeof(*d), 0);
             if (d) {
@@ -309,6 +321,11 @@ auditable. (Illustrative - error checking elided.)
             return ret;
     }
 
+Such a policy is what makes a caller-supplied keyring usable at all on a system
+that does not boot with ``bpf.keyring_unsealed=1``: the allowlist bounds which
+staged keys count, and the LSM itself has to protect them from being tampered
+with.
+
 Observing a verified load: ``security_bpf_prog()``
 --------------------------------------------------
 
@@ -381,8 +398,9 @@ 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". A policy LSM such as IPE could consume the same
+let a policy distinguish "verified against the operator's bpf keyring" from
+"verified against a keyring the loader supplied itself", which is the
+distinction that matters most. A policy LSM such as IPE could consume the same
 hooks to enforce system policy without writing any BPF, though none implements
 this today.
 
@@ -390,33 +408,158 @@ 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.
+against. Four values are well-known; anything else is resolved as a caller-
+supplied key or keyring, named either by its serial or by one of the other
+``KEY_SPEC_*`` ids:
+
+.. list-table::
+   :header-rows: 1
+
+   * - ``keyring_id``
+     - Keyring
+   * - ``0``
+     - builtin trusted keyring
+   * - ``VERIFY_USE_SECONDARY_KEYRING`` (``1``)
+     - secondary trusted keyring
+   * - ``VERIFY_USE_PLATFORM_KEYRING`` (``2``)
+     - platform keyring
+   * - ``KEY_SPEC_BPF_KEYRING`` (``-9``)
+     - the bpf keyring
+   * - anything else
+     - a caller-supplied user/session key or keyring
+
+The keyring is resolved first, before the signature bytes are examined, so a
+signature naming a keyring that cannot be used is rejected up front, and a
+failed verification aborts the load - a program that loads successfully with
+a signature therefore always has consistent keyring fields recorded.
+
+The bpf keyring
+---------------
+
+A system keyring needs a kernel rebuild or a vouched-for enrollment to rotate a
+key, and grants BPF-signing trust to keys trusted for everything else in the
+kernel too. A caller-supplied keyring, at the other extreme, is filled by the
+very process that loads the program and so carries no trust of its own.
+
+The bpf keyring fills that gap and is the trust anchor which a signed BPF
+deployment should be built on top of: a keyring named ``.bpf``, selected with
+``KEY_SPEC_BPF_KEYRING``, that an operator provisions at boot with a key
+scoped to BPF program loading and nothing else in the kernel's trust hierarchy.
+It is owned by the operator rather than by the loader, and rotatable across a
+reboot without touching the kernel image. It is modelled after the dm-verity
+keyring (see ``dm_verity.keyring_unsealed=``) and provisioned the same way: an
+initrd runs the ``keyctl`` steps below before handing off to the rootfs.
+
+Provisioning
+~~~~~~~~~~~~
+
+The keyring is created during ``late_initcall`` and is **sealed empty** by
+default: it carries a reject-all restriction, so no key can ever be added and
+``KEY_SPEC_BPF_KEYRING`` fails with ``-ENOKEY`` for the whole boot.
+
+``bpf.keyring_unsealed=1`` leaves it unrestricted at init so the initrd can
+provision it. The keyring is not linked into any process keyring, but the
+``KEY_SPEC_BPF_KEYRING`` special key id addresses it directly, so its serial
+does not have to be looked up first. Steps would be as follows::
+
+    keyctl padd asymmetric "" -9 < signing_key.der
+    keyctl restrict_keyring -9
+
+Both steps are required: the keyring is consulted only once it is **non-empty
+and restricted**. An unrestricted keyring is ignored even when it holds keys,
+so a half-provisioned keyring is inert rather than a weaker trust anchor, and a
+load naming it fails with ``-ENOKEY`` and a verifier log. Restricting cannot
+be undone.
+
+More than one key is enrolled by repeating the ``keyctl padd`` step; the
+restriction is applied once, after the last of them::
+
+    for key in /etc/bpf/keys/*.der; do
+        keyctl padd asymmetric "" -9 < $key
+    done
+
+    keyctl restrict_keyring -9
+    keyctl show -9
+
+The restriction bounds what can be added, never what can be taken away. A key
+that is already enrolled can still be unlinked, and the keyring cleared or
+revoked, by anything running as root. That does not weaken the anchor, since
+a keyring left empty is no longer consulted and a load naming it fails with
+``-ENOKEY``, but it does take signed loading out until the next boot. Dropping
+the user permissions the keyring no longer needs would close that; as a third
+step in the initrd::
+
+    keyctl setperm -9 0x08030000
+
+What remains is ``KEY_POS_SEARCH`` for the in-kernel search during verification,
+plus ``KEY_USR_VIEW`` and ``KEY_USR_READ`` so the keyring stays visible in
+``/proc/keys``. ``KEY_SPEC_BPF_KEYRING`` names the keyring but conveys no rights
+of its own - the keyring is not possessed by anyone, so the ``KEY_POS_*`` bits
+are only ever reached by the in-kernel search, and userspace is left with what
+the ``KEY_USR_*`` bits grant. Once they are dropped, addressing it by the serial
+``/proc/keys`` reports gets no further than the special id does.
+
+Provisioning has to complete before control passes to the rootfs. The keyring
+takes any number of keys for as long as it is unrestricted, so it is the
+restriction that bounds the enrolled set, not the first enrollment. Before
+the initrd hands off control, it must therefore restrict the keyring after
+the last enrollment.
+
+Enforcement
+~~~~~~~~~~~
+
+``bpf.keyring_unsealed=1`` states that the bpf keyring is *the* trust anchor for
+this boot, so it does more than unseal. From the first program load onwards a
+caller-supplied user/session keyring is refused with ``-EPERM`` and a verifier
+log message, whether or not provisioning ever completed. The system keyrings
+stay selectable.
+
+Enforcement is readable at ``/sys/module/bpf/parameters/keyring_unsealed``, and
+read-only there: the flag is ``__ro_after_init`` behind a 0444 parameter. It is
+also derived from the boot flag rather than from the keyring's runtime state,
+so there is no window early in boot during which a caller-supplied keyring is
+still accepted.
+
+Caller-supplied keyrings are for staging
+----------------------------------------
+
+A ``keyring_id`` naming a user or session key or keyring is a *staging*
+mechanism, not a trust anchor: it is filled by the same userspace that loads the
+program, so verifying against it establishes only that the loader signed what it
+loaded. Its purpose is to let software installed onto a running system - whose
+signing key is not enrolled anywhere yet - run signed until that key reaches the
+bpf keyring on the next boot.
+
+A system that has committed to the bpf keyring refuses this path outright (see
+`Enforcement`_). A system that has not can still allow it, but a policy must
+never treat ``BPF_SIG_KEYRING_USER`` as equivalent to the bpf or system
+keyrings; it should allowlist the specific serials it is willing to stage and
+pair that with a BPF LSM policy protecting those keys from tampering, as in
+`Enforcement via LSMs`_.
+
+Recorded fields
+---------------
 
 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.
+    keyrings, ``_BPF`` for the bpf keyring, or ``_USER`` for a caller-supplied
+    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
+    **caller-supplied 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".
+    serial. The system keyrings and the bpf keyring are trusted as a whole and
+    expose no serial here, so the serial is ``0`` for them, and ``0`` for
+    unsigned programs. A non-zero ``keyring_serial`` is therefore exactly
+    "verified against the caller-supplied key/keyring with this serial", which
+    is exactly the case a policy has to scrutinise.
 
 .. list-table::
    :header-rows: 1
@@ -436,16 +579,49 @@ Two fields are recorded in ``prog->aux->sig`` for an LSM to inspect:
    * - ``VERIFY_USE_PLATFORM_KEYRING``
      - ``BPF_SIG_KEYRING_PLATFORM``
      - ``0``
-   * - other (a user/session key serial)
+   * - ``KEY_SPEC_BPF_KEYRING``
+     - ``BPF_SIG_KEYRING_BPF``
+     - ``0``
+   * - other (a caller-supplied key serial)
      - ``BPF_SIG_KEYRING_USER``
      - serial of the resolved key/keyring
 
-Producing a signed object
-==========================
+Producing and loading a signed object
+=====================================
+
+Generating a signing key
+------------------------
+
+Signing is algorithm agnostic: the algorithm comes from the X.509 certificate
+and the PKCS#7 ``SignerInfo``. Anything the X.509 and PKCS#7 parsers understand
+works with no BPF-side change. Both examples below read the certificate request
+config from ``x509.genkey``, for which ``certs/x509.genkey`` serves as a
+template (see Documentation/admin-guide/module-signing.rst). RSA::
+
+    openssl req -new -nodes -utf8 -sha256 -days 36500 -batch -x509 \
+            -config x509.genkey -outform PEM \
+            -out signing_key.pem -keyout signing_key.pem
+    openssl x509 -in signing_key.pem -outform der -out signing_key.der
+
+ML-DSA-87 (FIPS-204), which needs openssl 3.5 or later and ``CONFIG_CRYPTO_MLDSA``
+in the kernel. Note the absence of a digest option: ML-DSA hashes the message
+itself and openssl rejects an explicit digest for it::
+
+    openssl req -new -nodes -utf8 -days 36500 -batch -x509 \
+            -newkey ML-DSA-87 -config x509.genkey -outform PEM \
+            -out signing_key.pem -keyout signing_key.pem
+    openssl x509 -in signing_key.pem -outform der -out signing_key.der
+
+``bpftool`` handles the following internally: openssl before 4.0 cannot combine
+ML-DSA with ``CMS_NOATTR``, so it falls back to signedAttrs, where only SHA-512
+is permitted. This mirrors what module signing does as well.
+
+Signing
+-------
 
 ``bpftool`` generates and signs a light skeleton in one step::
 
-    bpftool gen skeleton -L -S -k <private_key.pem> -i <certificate.x509> \
+    bpftool gen skeleton -L -S -k signing_key.pem -i signing_key.der \
             obj.bpf.o > obj.lskel.h
 
 ``-L`` selects the light-skeleton (``gen_loader``) backend and ``-S`` enables
@@ -454,12 +630,36 @@ signing; ``-k`` and ``-i`` supply the signing key and its X.509 certificate.
 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 and
 hash are embedded in the generated header; the certificate is used only for
-signing and is not included. Loading the skeleton performs the
-create/populate/freeze/load sequence described above.
+signing and is not included.
+
+Loading
+-------
+
+The generated skeleton exposes ``keyring_id``, which selects the keyring the
+kernel verifies against. Set it between open and load; loading then 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.
+    struct obj *skel = obj__open();
+
+    skel->keyring_id = KEY_SPEC_BPF_KEYRING;
+    err = obj__load(skel);
+
+For the staging case the same object is loaded against a keyring the caller
+populated itself, which only works on a system that has not set
+``bpf.keyring_unsealed=1``::
+
+    /*
+     * Staging only: this keyring is under the loader's own control and
+     * carries no trust of its own. See "Caller-supplied keyrings are for
+     * staging".
+     */
+    key_id = add_key("asymmetric", "", der, der_sz, KEY_SPEC_SESSION_KEYRING);
+    skel->keyring_id = KEY_SPEC_SESSION_KEYRING;
+    err = obj__load(skel);
+
+Either way the trusted public key must already be in the chosen keyring for
+verification to succeed. For the bpf keyring that enrollment happens once at
+boot, see `Provisioning`_.
 
 UAPI reference
 ==============
@@ -487,6 +687,13 @@ UAPI reference
     The map content is not hashed separately at all - it is covered, as bytes,
     by the program signature.
 
+Kernel command line:
+
+``bpf.keyring_unsealed=``
+    Set to ``1`` to leave the bpf keyring unsealed for provisioning, and to make
+    it the only non-system keyring a loader may select for the rest of the boot
+    (see `The bpf keyring`_).
+
 Notes and limitations
 ======================
 
@@ -495,3 +702,5 @@ Notes and limitations
   exceed it.
 - The metadata container is a single-element array map, accessed through
   ``map_direct_value_addr``.
+- The bpf keyring needs ``CONFIG_KEYS``; without it there is no bpf keyring
+  and ``KEY_SPEC_BPF_KEYRING`` never resolves.
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 19+ messages in thread

* Re: [PATCH bpf-next v3 07/11] selftests/bpf: Rename the verify_sig_setup.sh setup into setup-rsa
  2026-08-26 16:41 ` [PATCH bpf-next v3 07/11] selftests/bpf: Rename the verify_sig_setup.sh setup into setup-rsa Daniel Borkmann
@ 2026-08-26 17:29   ` bot+bpf-ci
  0 siblings, 0 replies; 19+ messages in thread
From: bot+bpf-ci @ 2026-08-26 17:29 UTC (permalink / raw)
  To: daniel, memxor
  Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf,
	dhowells, jarkko, keyrings, ast, andrii, daniel, martin.lau,
	eddyz87, yonghong.song, clm, ihor.solodrai

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

> diff --git a/tools/testing/selftests/bpf/verify_sig_setup.sh b/tools/testing/selftests/bpf/verify_sig_setup.sh
> index 09179fb551f09..202e6e6418fe2 100755
> --- a/tools/testing/selftests/bpf/verify_sig_setup.sh
> +++ b/tools/testing/selftests/bpf/verify_sig_setup.sh
> @@ -28,7 +28,7 @@ authorityKeyIdentifier=keyid
>
>  usage()
>  {
> -	echo "Usage: $0 <setup|cleanup <existing_tmp_dir>"
> +	echo "Usage: $0 <setup-rsa|cleanup <existing_tmp_dir>"
>  	exit 1
>  }

This isn't a bug, but while updating this usage line for the rename, would
it be worth listing the other actions main() accepts (genkey,
fsverity-create-sign, fsverity-enable) and closing the '<' bracket, so the
help text matches the dispatch?

> @@ -47,7 +47,7 @@ genkey()
>  		${tmp_dir}/signing_key.der -outform der
>  }
>
> -setup()
> +setup_rsa()
>  {
>  	local tmp_dir="$1"
>
> @@ -108,8 +108,8 @@ main()
>
>  	[[ ! -d "${tmp_dir}" ]] && echo "Directory ${tmp_dir} doesn't exist" && exit 1
>
> -	if [[ "${action}" == "setup" ]]; then
> -		setup "${tmp_dir}"
> +	if [[ "${action}" == "setup-rsa" ]]; then
> +		setup_rsa "${tmp_dir}"
>  	elif [[ "${action}" == "genkey" ]]; then
>  		genkey "${tmp_dir}"
>  	elif [[ "${action}" == "cleanup" ]]; then


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

^ permalink raw reply	[flat|nested] 19+ messages in thread

* Re: [PATCH bpf-next v3 02/11] bpf: Refuse caller-supplied keyrings when the bpf one is active
  2026-08-26 16:41 ` [PATCH bpf-next v3 02/11] bpf: Refuse caller-supplied keyrings when the bpf one is active Daniel Borkmann
@ 2026-08-26 17:41   ` bot+bpf-ci
  0 siblings, 0 replies; 19+ messages in thread
From: bot+bpf-ci @ 2026-08-26 17:41 UTC (permalink / raw)
  To: daniel, memxor
  Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf,
	dhowells, jarkko, keyrings, ast, andrii, daniel, martin.lau,
	eddyz87, yonghong.song, clm, ihor.solodrai

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

> diff --git a/Documentation/admin-guide/kernel-parameters.txt b/Documentation/admin-guide/kernel-parameters.txt
> index 2beb61092bb31..e70fb15757d40 100644
> --- a/Documentation/admin-guide/kernel-parameters.txt
> +++ b/Documentation/admin-guide/kernel-parameters.txt

[ ... ]

> diff --git a/include/linux/bpf.h b/include/linux/bpf.h
> index 039674b2f8752..3a7eb2185c354 100644
> --- a/include/linux/bpf.h
> +++ b/include/linux/bpf.h

[ ... ]

> diff --git a/kernel/bpf/keys.c b/kernel/bpf/keys.c
> index 08549b3220c13..bffc356839ac5 100644
> --- a/kernel/bpf/keys.c
> +++ b/kernel/bpf/keys.c
> @@ -18,6 +18,11 @@ static bool bpf_keyring_unsealed __ro_after_init;
>  module_param_named(keyring_unsealed, bpf_keyring_unsealed, bool, 0444);
>  MODULE_PARM_DESC(keyring_unsealed, "Leave the bpf keyring unsealed");
>
> +bool bpf_keyring_enforced(void)
> +{
> +	return bpf_keyring_unsealed;
> +}

This isn't a bug, but would a short comment here noting that
bpf.keyring_unsealed doubles as the enforcement opt-in help, given
"unsealed" and "enforced" read as opposites?

The accessor name and the flag it returns read in opposite directions:
unsealed describes the keyring being left open for provisioning, while
enforced describes caller-supplied keyrings being locked out. The
rationale lives in the changelog and later in signing.rst, but keys.c
itself carries no comment about this coupling.

>
>  struct bpf_key *bpf_lookup_keyring(void)
>  {

[ ... ]

> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index b45d201c25357..0950572779948 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
> @@ -21019,21 +21019,28 @@ static int bpf_prog_verify_signature(struct bpf_verifier_env *env,
>  	if (!attr->signature_size ||
>  	    attr->signature_size > KMALLOC_MAX_CACHE_SIZE)
>  		return -EINVAL;
> -	if (attr->keyring_id == KEY_SPEC_BPF_KEYRING) {
> -		key = bpf_lookup_keyring();
> -		if (!key) {
> -			verbose(env, "the bpf keyring is empty or has not been restricted\n");
> -			return -ENOKEY;
> -		}
> -	} else if (system_keyring_id_check(attr->keyring_id) == 0) {
> +	if (!system_keyring_id_check(attr->keyring_id)) {
>  		key = bpf_lookup_system_key(attr->keyring_id);
>  	} else {
> -		key = bpf_lookup_user_key(attr->keyring_id, 0);
> +		if (attr->keyring_id != KEY_SPEC_BPF_KEYRING) {
> +			if (bpf_keyring_enforced()) {
> +				verbose(env, "caller-supplied keyring refused, use bpf keyring\n");
> +				return -EPERM;
> +			}
> +			key = bpf_lookup_user_key(attr->keyring_id, 0);
> +		} else {
> +			key = bpf_lookup_keyring();
> +		}
>  	}
>  	if (!key) {
> -		verbose(env, "cannot resolve signing keyring with keyring_id %d\n",
> -			attr->keyring_id);
> -		return -EINVAL;
> +		if (attr->keyring_id == KEY_SPEC_BPF_KEYRING) {
> +			verbose(env, "the bpf keyring is empty or has not been restricted\n");
> +			return -ENOKEY;
> +		} else {
> +			verbose(env, "cannot resolve signing keyring with keyring_id %d\n",
> +				attr->keyring_id);
> +			return -EINVAL;
> +		}
>  	}

This isn't a bug, but would a flat else-if chain here keep the
KEY_SPEC_BPF_KEYRING test in one place and avoid the extra nesting
level, or is the grouping of the non-system cases deliberate?

The keyring selection goes from the flat else-if chain the preceding
commit in this series introduced to a nested else { if ... else ... }
form, and attr->keyring_id == KEY_SPEC_BPF_KEYRING is now tested
twice - once negated for branch selection, once positively in the
shared if (!key) error path. The if (!key) block also uses else after a
return. A flat chain would express the same thing with one test of each
condition and one less nesting level:

	if (!system_keyring_id_check(attr->keyring_id)) {
		key = bpf_lookup_system_key(attr->keyring_id);
	} else if (attr->keyring_id == KEY_SPEC_BPF_KEYRING) {
		key = bpf_lookup_keyring();
	} else if (bpf_keyring_enforced()) {
		verbose(env, "caller-supplied keyring refused, use bpf keyring\n");
		return -EPERM;
	} else {
		key = bpf_lookup_user_key(attr->keyring_id, 0);
	}

Flat else-if chains at this indent level are the dominant idiom in
verifier.c (72 occurrences).

>
>  	sig = kvmemdup_bpfptr(usig, attr->signature_size);


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

^ permalink raw reply	[flat|nested] 19+ messages in thread

* Re: [PATCH bpf-next v3 05/11] selftests/bpf: Add a test for the sealed bpf keyring
  2026-08-26 16:41 ` [PATCH bpf-next v3 05/11] selftests/bpf: Add a test for the sealed bpf keyring Daniel Borkmann
@ 2026-08-26 17:41   ` bot+bpf-ci
  0 siblings, 0 replies; 19+ messages in thread
From: bot+bpf-ci @ 2026-08-26 17:41 UTC (permalink / raw)
  To: daniel, memxor
  Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf,
	dhowells, jarkko, keyrings, ast, andrii, daniel, martin.lau,
	eddyz87, yonghong.song, clm, ihor.solodrai

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

> diff --git a/tools/testing/selftests/bpf/prog_tests/signed_loader.c b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
> index 0c5294738d6ce..e30166c00be6a 100644
> --- a/tools/testing/selftests/bpf/prog_tests/signed_loader.c
> +++ b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
> @@ -32,8 +32,13 @@ enum {
>  	BPF_SIG_KEYRING_SECONDARY,
>  	BPF_SIG_KEYRING_PLATFORM,
>  	BPF_SIG_KEYRING_USER,
> +	BPF_SIG_KEYRING_BPF,
>  };

This isn't a bug, but is BPF_SIG_KEYRING_BPF referenced anywhere yet, or
is it here to keep the mirror of enum bpf_sig_keyring complete for a
follow-up?

[ ... ]

> @@ -627,6 +632,83 @@ static void signature_bad_keyring(void)
>  	gen_loader_fixture_fini(&f);
>  }
>
> +static bool keyring_unsealed_boot(void)
> +{
> +	char val = 0;
> +	int fd;
> +
> +	fd = open("/sys/module/bpf/parameters/keyring_unsealed", O_RDONLY);
> +	if (fd < 0)
> +		return false;
> +	if (read(fd, &val, 1) != 1)
> +		val = 0;
> +	close(fd);
> +	return val == 'Y' || val == '1';
> +}

This isn't a bug, but since the bool param is always rendered as Y/N by
param_get_bool(), would a plain `val == 'Y'` read more directly here?


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

^ permalink raw reply	[flat|nested] 19+ messages in thread

* Re: [PATCH bpf-next v3 11/11] Documentation/bpf: Document the bpf keyring and improve examples
  2026-08-26 16:41 ` [PATCH bpf-next v3 11/11] Documentation/bpf: Document the bpf keyring and improve examples Daniel Borkmann
@ 2026-08-26 17:41   ` bot+bpf-ci
  0 siblings, 0 replies; 19+ messages in thread
From: bot+bpf-ci @ 2026-08-26 17:41 UTC (permalink / raw)
  To: daniel, memxor
  Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf,
	dhowells, jarkko, keyrings, ast, andrii, daniel, martin.lau,
	eddyz87, yonghong.song, clm, ihor.solodrai

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

> diff --git a/Documentation/bpf/signing.rst b/Documentation/bpf/signing.rst
> index e73eaaebd8b15..9ea24dad5e9f9 100644
> --- a/Documentation/bpf/signing.rst
> +++ b/Documentation/bpf/signing.rst

[ ... ]

> @@ -254,21 +254,25 @@ returned. Only after the program has fully loaded, at the next hook
>  (``security_bpf_prog()``), does ``BPF_SIG_VERIFIED`` carry its full meaning:
>  validly signed *and* fully verified.
>
> -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.)
> +A more realistic admission policy than "is it signed at all": base trust in
> +the bpf keyring, accept a staging signature only while the key/keyring the
> +program 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. */
> +    /*
> +     * Serials of caller-supplied keyrings we are willing to stage. Empty
> +     * on a system that has committed to the bpf keyring, where the kernel
> +     * refuses them anyway.
> +     */
>      struct {
>              __uint(type, BPF_MAP_TYPE_HASH);
>              __type(key, __s32);             /* keyring_serial */
>              __type(value, __u8);
>              __uint(max_entries, 64);
> -    } trusted_user_keys SEC(".maps");
> +    } staging_keys SEC(".maps");
>
>      /* Audit stream consumed by a userspace logger. */
>      struct {
> @@ -291,11 +295,19 @@ auditable. (Illustrative - error checking elided.)
>              if (kernel)
>                      return 0;                       /* trust in-kernel loads */
>
> -            if (verdict != BPF_SIG_VERIFIED)
> +            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 */
> +            } else switch (ktype) {
> +            case BPF_SIG_KEYRING_BUILTIN:
> +            case BPF_SIG_KEYRING_BPF:
> +                    break;
> +            case BPF_SIG_KEYRING_USER:
> +                    if (!bpf_map_lookup_elem(&staging_keys, &serial))
> +                            ret = -EPERM;
> +                    break;
> +            default:
> +                    ret = -EPERM;                   /* keyring not in policy */
> +            }

Does this switch statement match what the surrounding prose describes?

Before the change the example accepted all three system keyrings (builtin,
secondary, platform), matching its prose ("accept programs signed by a
system keyring"). The new switch accepts only BPF_SIG_KEYRING_BUILTIN and
BPF_SIG_KEYRING_BPF, so BPF_SIG_KEYRING_SECONDARY and BPF_SIG_KEYRING_PLATFORM
now fall through to the default case and are denied with -EPERM.

The intro text (line 257) says only "base trust in the bpf keyring, accept a
staging signature only while the key/keyring ... is on an explicit
allowlist", but does not mention that the builtin keyring is also accepted,
nor that secondary and platform are refused. The rest of the document groups
the three system keyrings together as trustworthy: "The system keyrings stay
selectable" and "a policy must never treat BPF_SIG_KEYRING_USER as equivalent
to the bpf or system keyrings".

Should the prose clarify which system keyrings this policy accepts and why
secondary/platform are excluded, or should the switch list
BPF_SIG_KEYRING_SECONDARY and BPF_SIG_KEYRING_PLATFORM alongside BUILTIN?

[ ... ]

> @@ -454,12 +630,36 @@ 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 and
>  hash are embedded in the generated header; the certificate is used only for
> -signing and is not included. Loading the skeleton performs the
> -create/populate/freeze/load sequence described above.
> +signing and is not included.
> +
> +Loading
> +-------
> +
> +The generated skeleton exposes ``keyring_id``, which selects the keyring the
> +kernel verifies against. Set it between open and load; loading then 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.
> +    struct obj *skel = obj__open();
> +
> +    skel->keyring_id = KEY_SPEC_BPF_KEYRING;
> +    err = obj__load(skel);
> +
> +For the staging case the same object is loaded against a keyring the caller
> +populated itself, which only works on a system that has not set
> +``bpf.keyring_unsealed=1``::
> +
> +    /*
> +     * Staging only: this keyring is under the loader's own control and
> +     * carries no trust of its own. See "Caller-supplied keyrings are for
> +     * staging".
> +     */
> +    key_id = add_key("asymmetric", "", der, der_sz, KEY_SPEC_SESSION_KEYRING);
> +    skel->keyring_id = KEY_SPEC_SESSION_KEYRING;
> +    err = obj__load(skel);

[ ... ]

> @@ -436,16 +579,49 @@ Two fields are recorded in ``prog->aux->sig`` for an LSM to inspect:
>     - ``VERIFY_USE_PLATFORM_KEYRING``
>       - ``BPF_SIG_KEYRING_PLATFORM``
>       - ``0``
> -   * - other (a user/session key serial)
> +   * - ``KEY_SPEC_BPF_KEYRING``
> +     - ``BPF_SIG_KEYRING_BPF``
> +     - ``0``
> +   * - other (a caller-supplied key serial)
>       - ``BPF_SIG_KEYRING_USER``
>       - serial of the resolved key/keyring
>
> -Producing a signed object
> -==========================
> +Producing and loading a signed object
> +=====================================
> +
> +Generating a signing key
> +------------------------
> +
> +Signing is algorithm agnostic: the algorithm comes from the X.509 certificate
> +and the PKCS#7 ``SignerInfo``. Anything the X.509 and PKCS#7 parsers understand
> +works with no BPF-side change. Both examples below read the certificate request
> +config from ``x509.genkey``, for which ``certs/x509.genkey`` serves as a
> +template (see Documentation/admin-guide/module-signing.rst). RSA::
> +
> +    openssl req -new -nodes -utf8 -sha256 -days 36500 -batch -x509 \
> +            -config x509.genkey -outform PEM \
> +            -out signing_key.pem -keyout signing_key.pem
> +    openssl x509 -in signing_key.pem -outform der -out signing_key.der
> +
> +ML-DSA-87 (FIPS-204), which needs openssl 3.5 or later and ``CONFIG_CRYPTO_MLDSA``
> +in the kernel. Note the absence of a digest option: ML-DSA hashes the message
> +itself and openssl rejects an explicit digest for it::
> +
> +    openssl req -new -nodes -utf8 -days 36500 -batch -x509 \
> +            -newkey ML-DSA-87 -config x509.genkey -outform PEM \
> +            -out signing_key.pem -keyout signing_key.pem
> +    openssl x509 -in signing_key.pem -outform der -out signing_key.der

Is the claim that "openssl rejects an explicit digest for it" correct?

OpenSSL silently ignores the digest for ML-DSA rather than rejecting it.
In apps/lib/apps.c do_sign_init(), when EVP_PKEY_get_default_digest_name()
returns 2 (UNDEF), the digest is forced to NULL. This was verified directly:

  $ openssl req -new -nodes -utf8 -sha256 -days 1 -batch -x509 \
        -newkey ML-DSA-87 -config t.genkey -outform PEM \
        -out m.pem -keyout m.pem
  (exit 0; resulting cert: 'Signature Algorithm: ML-DSA-87')

The in-tree module signing build corroborates this: certs/Makefile's
cmd_gen_key passes '-$(CONFIG_MODULE_SIG_HASH)' unconditionally, including
for ml-dsa-87, and that build succeeds. Omitting the digest is correct and
clearer, but the stated reason is misleading.

The same incorrect justification appears in the comment above genkey_mldsa()
in tools/testing/selftests/bpf/verify_sig_setup.sh (added in 3eb252cf73a48).

Should both be reworded to say that openssl ignores an explicit digest for
ML-DSA, rather than stating it rejects one?

> +
> +``bpftool`` handles the following internally: openssl before 4.0 cannot combine
> +ML-DSA with ``CMS_NOATTR``, so it falls back to signedAttrs, where only SHA-512
> +is permitted. This mirrors what module signing does as well.

Is the CONFIG_CRYPTO_MLDSA requirement complete?

The stated kernel requirement lists only CONFIG_CRYPTO_MLDSA, but
CONFIG_CRYPTO_SHA512 is needed as well. Because bpftool falls back to
signedAttrs with EVP_sha512() on openssl < 4.0, the kernel side verifies the
messageDigest attribute through pkcs7_digest() -> crypto_alloc_shash() with
hash_algo == "sha512".

CONFIG_CRYPTO_MLDSA selects only CRYPTO_SIG and CRYPTO_LIB_MLDSA (which
selects CRYPTO_LIB_SHA3), never CRYPTO_SHA512. A kernel built with
CONFIG_CRYPTO_MLDSA=y but CONFIG_CRYPTO_SHA512=n will fail such loads.

The same series adds both symbols to tools/testing/selftests/bpf/config in
3eb252cf73a48:

    +CONFIG_CRYPTO_MLDSA=y
    +CONFIG_CRYPTO_SHA512=y

Should the documentation list CONFIG_CRYPTO_SHA512 alongside
CONFIG_CRYPTO_MLDSA?



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

^ permalink raw reply	[flat|nested] 19+ messages in thread

* Re: [PATCH bpf-next v3 08/11] selftests/bpf: Add an end-to-end ML-DSA signed loader test
  2026-08-26 16:41 ` [PATCH bpf-next v3 08/11] selftests/bpf: Add an end-to-end ML-DSA signed loader test Daniel Borkmann
@ 2026-08-26 17:41   ` bot+bpf-ci
  0 siblings, 0 replies; 19+ messages in thread
From: bot+bpf-ci @ 2026-08-26 17:41 UTC (permalink / raw)
  To: daniel, memxor
  Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf,
	dhowells, jarkko, keyrings, ast, andrii, daniel, martin.lau,
	eddyz87, yonghong.song, clm, ihor.solodrai

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

> diff --git a/tools/testing/selftests/bpf/config b/tools/testing/selftests/bpf/config
> index ea7044f30adc3..2ec15117abb50 100644
> --- a/tools/testing/selftests/bpf/config
> +++ b/tools/testing/selftests/bpf/config
> @@ -51,6 +51,8 @@ CONFIG_IPV6_SEG6_LWTUNNEL=y
>  CONFIG_IPV6_SIT=y
>  CONFIG_IPV6_TUNNEL=y
>  CONFIG_KEYS=y
> +CONFIG_CRYPTO_MLDSA=y
> +CONFIG_CRYPTO_SHA512=y
>  CONFIG_LIRC=y
>  CONFIG_LIVEPATCH=y
>  CONFIG_LWTUNNEL=y

This isn't a bug, but would these two fit more naturally in the
CONFIG_CRYPTO_* block at the top of the file, next to CONFIG_CRYPTO_SHA256,
rather than after CONFIG_KEYS? The file is otherwise maintained in
alphabetical order, and CONFIG_CRYPTO_SHA512 reads oddly 40 lines away from
its CONFIG_CRYPTO_SHA256 sibling.

[ ... ]

> diff --git a/tools/testing/selftests/bpf/verify_sig_setup.sh b/tools/testing/selftests/bpf/verify_sig_setup.sh
> index 202e6e6418fe2..2737c1a2bcfd6 100755
> --- a/tools/testing/selftests/bpf/verify_sig_setup.sh
> +++ b/tools/testing/selftests/bpf/verify_sig_setup.sh

[ ... ]

> @@ -57,6 +57,57 @@ setup_rsa()
>  	keyctl link $key_id $keyring_id
>  }
>
> +mldsa_supported()
> +{
> +	local tmp_dir="$1"
> +
> +	genkey_mldsa "${tmp_dir}" || return 1
> +	: > ${tmp_dir}/probe
> +	# Same digest as the caller signs with, see sign_buf_digest().
> +	./sign-file -d sha512 ${tmp_dir}/signing_key.pem \
> +		${tmp_dir}/signing_key.pem ${tmp_dir}/probe || return 1
> +	rm -f ${tmp_dir}/probe ${tmp_dir}/probe.p7s
> +}
> +
> +genkey_mldsa()
> +{
> +	local tmp_dir="$1"
> +
> +	echo "${x509_genkey_content}" > ${tmp_dir}/x509.genkey
> +
> +	# No -<digest> here: ML-DSA hashes the message itself, and openssl
> +	# rejects an explicit digest for it.
> +	openssl req -new -nodes -utf8 -days 36500 \
> +			-batch -x509 -newkey ML-DSA-87 \
> +			-config ${tmp_dir}/x509.genkey \
> +			-outform PEM -out ${tmp_dir}/signing_key.pem \
> +			-keyout ${tmp_dir}/signing_key.pem 2>&1
> +
> +	openssl x509 -in ${tmp_dir}/signing_key.pem -out \
> +		${tmp_dir}/signing_key.der -outform der
> +}
> +
> +mldsa_skip()
> +{
> +	local tmp_dir="$1"
> +
> +	rm -f ${tmp_dir}/x509.genkey ${tmp_dir}/signing_key.pem \
> +		${tmp_dir}/signing_key.der ${tmp_dir}/probe \
> +		${tmp_dir}/probe.p7s
> +	exit 77
> +}

A subsystem pattern flags this as potentially concerning: mldsa_supported()
collapses every possible failure into a single 'unsupported' verdict. The
calling code in signed_loader.c then reports:

    printf("%s:SKIP:openssl has no ML-DSA support (needs 3.5+)\n", __func__);

But `return 1` in mldsa_supported() is reached for any of: openssl missing or
too old; `openssl req -newkey ML-DSA-87` failing for an unrelated reason;
`openssl x509` failing; `./sign-file` not built or linked against an openssl
that cannot CMS-sign ML-DSA; a write failure creating ${tmp_dir}/probe.

Every one of those reports as 'openssl has no ML-DSA support', and with the
CHANGE-8 modification to catch() at line 145, the openssl/sign-file
diagnostics are deleted for exit 77 cases. In a default
(SELFTESTS_VERBOSE=0) run there is nothing left to distinguish 'ML-DSA
genuinely absent' from 'ML-DSA present but the signing path is broken.'

Would narrowing the probe help here? For example, only treating a
genkey_mldsa failure as unsupported, and treating a ./sign-file failure with
a working key as a real error would keep the broken case visible.

> +
> +setup_mldsa()
> +{
> +	local tmp_dir="$1"
> +
> +	mldsa_supported "${tmp_dir}" || mldsa_skip "${tmp_dir}"
> +	key_id=$(cat ${tmp_dir}/signing_key.der |
> +		 keyctl padd asymmetric ebpf_testing_key @s)
> +	keyring_id=$(keyctl newring ebpf_testing_keyring @s)
> +	keyctl link $key_id $keyring_id
> +}

[ ... ]

> @@ -91,7 +142,7 @@ catch()
>  	local exit_code="$1"
>  	local log_file="$2"
>
> -	if [[ "${exit_code}" -ne 0 ]]; then
> +	if [[ "${exit_code}" -ne 0 && "${exit_code}" -ne 77 ]]; then
>  		cat "${log_file}" >&3
>  	fi

[ ... ]


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

^ permalink raw reply	[flat|nested] 19+ messages in thread

* Re: [PATCH bpf-next v3 10/11] selftests/bpf: Add tests for bpf keyring in signed loader
  2026-08-26 16:41 ` [PATCH bpf-next v3 10/11] selftests/bpf: Add tests for bpf keyring in signed loader Daniel Borkmann
@ 2026-08-26 17:54   ` bot+bpf-ci
  0 siblings, 0 replies; 19+ messages in thread
From: bot+bpf-ci @ 2026-08-26 17:54 UTC (permalink / raw)
  To: daniel, memxor
  Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf,
	dhowells, jarkko, keyrings, ast, andrii, daniel, martin.lau,
	eddyz87, yonghong.song, clm, ihor.solodrai

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

> diff --git a/tools/testing/selftests/bpf/prog_tests/signed_loader.c b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
> index 039b5d044545d..eb78525d33595 100644
> --- a/tools/testing/selftests/bpf/prog_tests/signed_loader.c
> +++ b/tools/testing/selftests/bpf/prog_tests/signed_loader.c

[ ... ]

> @@ -1975,6 +2170,11 @@ static void signed_module_kfunc_rejected(void)
>
>  void test_signed_loader(void)
>  {
> +	if (keyring_unsealed_boot()) {
> +		if (test__start_subtest("bpf_keyring_provisioned"))
> +			bpf_keyring_provisioned();
> +		return;
> +	}
>  	if (test__start_subtest("loadtime_no_map"))
>  		loadtime_no_map();
>  	if (test__start_subtest("loadtime_with_map"))

Does the unconditional return here skip subtests that would still work on
an unsealed boot?

The commit message explains that an unsealed boot refuses the session
keyring, so signature-dependent subtests would fail. However, 11 of the
31 subtests never populate attr.signature or attr.keyring_id at all:
metadata_match, metadata_ctx_max_entries_ignored,
metadata_ctx_initial_value_ignored, hash_requires_frozen,
no_update_after_freeze, freeze_writable_mmap, no_writable_mmap_frozen,
map_hash_matches_libbpf, map_hash_multi_element, map_hash_bad_size, and
map_hash_unsupported_type. These subtests test map hashing, freezing,
and metadata features without signatures.

They would pass unchanged on an unsealed boot but are silently dropped by
the return. A side effect is that a targeted run like
'test_progs -t signed_loader/map_hash_bad_size' on an unsealed guest
reports the test as passing while having executed nothing, because
returning before any test__start_subtest() leaves error_cnt at 0 and
registers no subtest.

Would it make sense to let bpf_keyring_provisioned() run first, then fall
through to the unsigned subtests? That would keep the same protection
without the coverage and reporting loss.


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

^ permalink raw reply	[flat|nested] 19+ messages in thread

* Re: [PATCH bpf-next v3 01/11] bpf, keys: Add a bpf keyring for program signature validation
  2026-08-26 16:41 ` [PATCH bpf-next v3 01/11] bpf, keys: Add a bpf keyring for program signature validation Daniel Borkmann
@ 2026-08-26 17:54   ` bot+bpf-ci
  0 siblings, 0 replies; 19+ messages in thread
From: bot+bpf-ci @ 2026-08-26 17:54 UTC (permalink / raw)
  To: daniel, memxor
  Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf,
	dhowells, jarkko, keyrings, ast, andrii, daniel, martin.lau,
	eddyz87, yonghong.song, clm, ihor.solodrai

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

> diff --git a/kernel/bpf/keys.c b/kernel/bpf/keys.c
> new file mode 100644
> index 0000000000000..08549b3220c13
> --- /dev/null
> +++ b/kernel/bpf/keys.c
> @@ -0,0 +1,68 @@

[ ... ]

> +struct bpf_key *bpf_lookup_keyring(void)
> +{
> +	struct bpf_key *bkey;
> +
> +	if (!bpf_keyring)
> +		return NULL;
> +	if (!READ_ONCE(bpf_keyring->keys.nr_leaves_on_tree) ||
> +	    !READ_ONCE(bpf_keyring->restrict_link))
> +		return NULL;

A subsystem pattern flags this as potentially concerning: the activation
gate samples bpf_keyring->keys.nr_leaves_on_tree and
bpf_keyring->restrict_link while holding neither bpf_keyring->sem nor
rcu_read_lock().

Both fields are written by assoc_array_apply_edit() and
keyring_restrict() under down_write(&keyring->sem), and the stores are
unmarked, so the READ_ONCE() loads form marked-read/unmarked-write pairs.

The two existing analogues, drivers/md/dm-verity-verify-sig.c and
fs/verity/signature.c, perform the same unlocked check with plain loads
and no annotation at all, so this patch is if anything stricter than
precedent.

Could a keyring maintainer confirm whether the gate is safe as written,
or whether all three sites need the keyring semaphore?

> +
> +	bkey = kmalloc_obj(*bkey);
> +	if (!bkey)
> +		return NULL;
> +
> +	bkey->key = bpf_keyring;
> +	bkey->has_ref = false;
> +	return bkey;
> +}

Does this overload struct bpf_key::has_ref in a way that breaks its
existing consumers?

bpf_lookup_keyring() introduces a third meaning for has_ref: a valid
struct key pointer with has_ref == false. The existing contract,
documented in bpf_lookup_system_key()'s kernel-doc, is that
has_ref == false means bkey->key is not a real pointer at all - it is a
sentinel understood only by verify_pkcs7_signature().

bpf_lookup_user_key() sets has_ref = true for real keys;
bpf_lookup_system_key() sets has_ref = false and stores
(struct key *)(unsigned long)id. This new code stores a real, live,
borrowed keyring pointer but flags it like a sentinel.

The borrow itself is safe - bpf_keyring keeps keyring_alloc()'s initial
reference for the lifetime of the system. But the two existing readers
of has_ref now behave wrongly for this case:

1) bpf_verify_pkcs7_signature() in kernel/bpf/helpers.c skips validation
   for has_ref == false:

     if (trusted_keyring->has_ref) {
             ret = key_validate(trusted_keyring->key);
             if (ret < 0)
                     return ret;
     }

   Revocation and invalidation of the .bpf keyring are still caught
   downstream by search_nested_keyrings(). Expiry is not caught anywhere.

   keyring_search_rcu() never tests the root keyring's expiry, and
   keyring_search_iterator() only tests the expiry of candidate leaf
   keys. Because the perm mask includes KEY_USR_SETATTR, root can
   'keyctl timeout <.bpf serial> <n>'; after the timeout the .bpf keyring
   keeps validating BPF program signatures, whereas the same operation on
   a caller-supplied keyring correctly makes the load fail with
   -EKEYEXPIRED.

2) bpf_key_serial() in include/linux/bpf.h is:

     return key->has_ref ? key->key->serial : 0;

   so the caller records prog->aux->sig.keyring_serial = 0 even though a
   genuine serial is available here. BPF LSM programs read this field
   directly, so they cannot see which keyring was used; they only get
   the new keyring_type == BPF_SIG_KEYRING_BPF.

Would it be clearer to either call key_validate() explicitly in
bpf_lookup_keyring(), or split the sentinel meaning out of has_ref with
a separate flag?

> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index e036ae20bf6b9..b45d201c25357 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c

[ ... ]

> @@ -21016,10 +21019,17 @@ static int bpf_prog_verify_signature(struct bpf_verifier_env *env,
>  	if (!attr->signature_size ||
>  	    attr->signature_size > KMALLOC_MAX_CACHE_SIZE)
>  		return -EINVAL;
> -	if (system_keyring_id_check(attr->keyring_id) == 0)
> +	if (attr->keyring_id == KEY_SPEC_BPF_KEYRING) {
> +		key = bpf_lookup_keyring();
> +		if (!key) {
> +			verbose(env, "the bpf keyring is empty or has not been restricted\n");
> +			return -ENOKEY;

bpf_lookup_keyring() returns NULL for four distinct reasons:

  kernel/bpf/keys.c:
    if (!bpf_keyring)                              /* (a) never created */
            return NULL;
    if (!READ_ONCE(bpf_keyring->keys.nr_leaves_on_tree) ||
        !READ_ONCE(bpf_keyring->restrict_link))
            return NULL;                           /* (b) empty or (c) unrestricted */
    bkey = kmalloc_obj(*bkey);
    if (!bkey)
            return NULL;                           /* (d) allocation failure */

(a) covers 'the keyring was never created' - if keyring_alloc() or
keyring_restrict() failed at late_initcall, both leave bpf_keyring NULL
after only pr_err(). It also covers CONFIG_KEYS=n builds, where
bpf_lookup_keyring() is the static inline stub that unconditionally
returns NULL.

(d) is an -ENOMEM condition reported to userspace as -ENOKEY together
with the message "empty or has not been restricted", which is actively
misleading when debugging an OOM.

Could the message and errno distinguish these cases so the feedback is
actionable when provisioning the keyring?


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

^ permalink raw reply	[flat|nested] 19+ messages in thread

end of thread, other threads:[~2026-08-26 17:54 UTC | newest]

Thread overview: 19+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-26 16:41 [PATCH bpf-next v3 00/11] BPF keyring and signed loader ML-DSA support Daniel Borkmann
2026-08-26 16:41 ` [PATCH bpf-next v3 01/11] bpf, keys: Add a bpf keyring for program signature validation Daniel Borkmann
2026-08-26 17:54   ` bot+bpf-ci
2026-08-26 16:41 ` [PATCH bpf-next v3 02/11] bpf: Refuse caller-supplied keyrings when the bpf one is active Daniel Borkmann
2026-08-26 17:41   ` bot+bpf-ci
2026-08-26 16:41 ` [PATCH bpf-next v3 03/11] bpf: Raise the bound on a program's signature size Daniel Borkmann
2026-08-26 16:41 ` [PATCH bpf-next v3 04/11] bpftool: Support ML-DSA program signing Daniel Borkmann
2026-08-26 16:41 ` [PATCH bpf-next v3 05/11] selftests/bpf: Add a test for the sealed bpf keyring Daniel Borkmann
2026-08-26 17:41   ` bot+bpf-ci
2026-08-26 16:41 ` [PATCH bpf-next v3 06/11] selftests/bpf: Rebuild signed lskels when signing key changes Daniel Borkmann
2026-08-26 16:41 ` [PATCH bpf-next v3 07/11] selftests/bpf: Rename the verify_sig_setup.sh setup into setup-rsa Daniel Borkmann
2026-08-26 17:29   ` bot+bpf-ci
2026-08-26 16:41 ` [PATCH bpf-next v3 08/11] selftests/bpf: Add an end-to-end ML-DSA signed loader test Daniel Borkmann
2026-08-26 17:41   ` bot+bpf-ci
2026-08-26 16:41 ` [PATCH bpf-next v3 09/11] selftests/bpf: Allow appending to guest kernel cmdline in vmtest.sh Daniel Borkmann
2026-08-26 16:41 ` [PATCH bpf-next v3 10/11] selftests/bpf: Add tests for bpf keyring in signed loader Daniel Borkmann
2026-08-26 17:54   ` bot+bpf-ci
2026-08-26 16:41 ` [PATCH bpf-next v3 11/11] Documentation/bpf: Document the bpf keyring and improve examples Daniel Borkmann
2026-08-26 17:41   ` bot+bpf-ci

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