* [PATCH bpf-next 01/11] bpf: Add a bpf keyring for program signature validation
2026-08-21 21:41 [PATCH bpf-next 00/11] BPF keyring and signed loader ML-DSA support Daniel Borkmann
@ 2026-08-21 21:41 ` Daniel Borkmann
2026-08-21 21:56 ` sashiko-bot
2026-08-21 22:50 ` bot+bpf-ci
2026-08-21 21:41 ` [PATCH bpf-next 02/11] bpf: Refuse caller-supplied keyrings when the bpf one is active Daniel Borkmann
` (9 subsequent siblings)
10 siblings, 2 replies; 25+ messages in thread
From: Daniel Borkmann @ 2026-08-21 21:41 UTC (permalink / raw)
To: memxor; +Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf
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 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 BPF program loading and nothing else in the
kernel's trust hierarchy.
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/verification.h | 10 +++
kernel/bpf/Makefile | 3 +
kernel/bpf/keys.c | 67 +++++++++++++++++++
kernel/bpf/verifier.c | 13 +++-
6 files changed, 106 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 ffa5626411ac..240e527c864b 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/verification.h b/include/linux/verification.h
index dec7f2beabfd..1cb59ddda250 100644
--- a/include/linux/verification.h
+++ b/include/linux/verification.h
@@ -18,6 +18,16 @@
#define VERIFY_USE_SECONDARY_KEYRING ((struct key *)1UL)
#define VERIFY_USE_PLATFORM_KEYRING ((struct key *)2UL)
+/*
+ * The id of BPF's ".bpf" keyring, reserved from the same space. It is
+ * explicitly not a sentinel like the two above as BPF resolves it to
+ * the keyring itself and passes that, so verify_pkcs7_signature() never
+ * sees this value, and system_keyring_id_check() must keep rejecting it.
+ * Left as a plain integer so that handing it over as @trusted_keys does
+ * not compile.
+ */
+#define VERIFY_USE_BPF_KEYRING 3
+
static inline int system_keyring_id_check(u64 id)
{
if (id > (unsigned long)VERIFY_USE_PLATFORM_KEYRING)
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..dc4d3a33158a
--- /dev/null
+++ b/kernel/bpf/keys.c
@@ -0,0 +1,67 @@
+// 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;
+ return 0;
+}
+late_initcall(bpf_keyring_init);
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index e036ae20bf6b..3be8d51d35ac 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -20981,6 +20981,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 VERIFY_USE_BPF_KEYRING:
+ return BPF_SIG_KEYRING_BPF;
default:
return BPF_SIG_KEYRING_USER;
}
@@ -21016,10 +21018,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 == VERIFY_USE_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);
--
2.43.0
^ permalink raw reply related [flat|nested] 25+ messages in thread* Re: [PATCH bpf-next 01/11] bpf: Add a bpf keyring for program signature validation
2026-08-21 21:41 ` [PATCH bpf-next 01/11] bpf: Add a bpf keyring for program signature validation Daniel Borkmann
@ 2026-08-21 21:56 ` sashiko-bot
2026-08-21 22:18 ` Daniel Borkmann
2026-08-21 22:50 ` bot+bpf-ci
1 sibling, 1 reply; 25+ messages in thread
From: sashiko-bot @ 2026-08-21 21:56 UTC (permalink / raw)
To: Daniel Borkmann; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] `VERIFY_USE_BPF_KEYRING` aliases a valid user keyring ID, making user keyring ID 3 unusable for BPF signature verification.
--
--- Patch [1]: [PATCH bpf-next 01/11] bpf: Add a bpf keyring for program signature validation ---
commit b7fa8763dbdc44199a6b443ce9ddca2d84edc3b7
Author: Daniel Borkmann <daniel@iogearbox.net>
bpf: Add a bpf keyring for program signature validation
This commit adds a dedicated bpf keyring for program signature validation,
selectable via a new well-known keyring_id VERIFY_USE_BPF_KEYRING. The
keyring is sealed empty at init but can be left open for provisioning
via a boot parameter.
> diff --git a/include/linux/verification.h b/include/linux/verification.h
> index dec7f2beabfd4..1cb59ddda2509 100644
> --- a/include/linux/verification.h
> +++ b/include/linux/verification.h
> @@ -18,6 +18,16 @@
> #define VERIFY_USE_SECONDARY_KEYRING ((struct key *)1UL)
> #define VERIFY_USE_PLATFORM_KEYRING ((struct key *)2UL)
>
> +/*
> + * The id of BPF's ".bpf" keyring, reserved from the same space. It is
> + * explicitly not a sentinel like the two above as BPF resolves it to
> + * the keyring itself and passes that, so verify_pkcs7_signature() never
> + * sees this value, and system_keyring_id_check() must keep rejecting it.
> + * Left as a plain integer so that handing it over as @trusted_keys does
> + * not compile.
> + */
> +#define VERIFY_USE_BPF_KEYRING 3
[Severity: Low]
Does this constant overlap with the valid user keyring ID space?
Since user keyring IDs generated by key_alloc_serial() are positive integers
that can start from 3 (avoiding 0, 1, and 2), 3 is technically a valid,
assignable user keyring ID.
[ ... ]
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index 16de43f172382..8075bdbee6254 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
> @@ -20908,10 +20910,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 == VERIFY_USE_BPF_KEYRING) {
> + key = bpf_lookup_keyring();
[Severity: Low]
If a user happens to generate a keyring with ID 3 and attempts to use it for
BPF signature verification here in bpf_prog_verify_signature(), wouldn't
this code incorrectly resolve it to the system .bpf keyring instead of their
intended user keyring?
While the probability of receiving serial ID 3 is very small, it appears to
create a structural API design flaw because a sentinel value intercepts a
valid user-space ID.
> + 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);
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260821214111.1120748-1-daniel@iogearbox.net?part=1
^ permalink raw reply [flat|nested] 25+ messages in thread* Re: [PATCH bpf-next 01/11] bpf: Add a bpf keyring for program signature validation
2026-08-21 21:56 ` sashiko-bot
@ 2026-08-21 22:18 ` Daniel Borkmann
0 siblings, 0 replies; 25+ messages in thread
From: Daniel Borkmann @ 2026-08-21 22:18 UTC (permalink / raw)
To: sashiko-reviews; +Cc: bpf
On 8/21/26 11:56 PM, sashiko-bot@kernel.org wrote:
> Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
> - [Low] `VERIFY_USE_BPF_KEYRING` aliases a valid user keyring ID, making user keyring ID 3 unusable for BPF signature verification.
(Once bpf keyring is active user/session keys are disallowed, they are only
for staging when bpf keyring is not yet used, so nothing gets overlapped.)
^ permalink raw reply [flat|nested] 25+ messages in thread
* Re: [PATCH bpf-next 01/11] bpf: Add a bpf keyring for program signature validation
2026-08-21 21:41 ` [PATCH bpf-next 01/11] bpf: Add a bpf keyring for program signature validation Daniel Borkmann
2026-08-21 21:56 ` sashiko-bot
@ 2026-08-21 22:50 ` bot+bpf-ci
1 sibling, 0 replies; 25+ messages in thread
From: bot+bpf-ci @ 2026-08-21 22:50 UTC (permalink / raw)
To: daniel, memxor
Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf, ast,
andrii, daniel, martin.lau, eddyz87, yonghong.song, clm,
ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 2154 bytes --]
> diff --git a/include/linux/verification.h b/include/linux/verification.h
> index dec7f2beabfd4..1cb59ddda2509 100644
> --- a/include/linux/verification.h
> +++ b/include/linux/verification.h
> @@ -18,6 +18,16 @@
> #define VERIFY_USE_SECONDARY_KEYRING ((struct key *)1UL)
> #define VERIFY_USE_PLATFORM_KEYRING ((struct key *)2UL)
>
> +/*
> + * The id of BPF's ".bpf" keyring, reserved from the same space. It is
> + * explicitly not a sentinel like the two above as BPF resolves it to
> + * the keyring itself and passes that, so verify_pkcs7_signature() never
> + * sees this value, and system_keyring_id_check() must keep rejecting it.
> + * Left as a plain integer so that handing it over as @trusted_keys does
> + * not compile.
Does the comment accurately describe what the code enforces?
The comment states the value is "left as a plain integer so that handing
it over as @trusted_keys does not compile." Passing the integer 3 where
struct key *trusted_keys is expected triggers -Wint-conversion, but that
warning is not promoted to -Werror anywhere in scripts/Makefile.warn or
scripts/Makefile.extrawarn.
The kernel requires -Werror=implicit-function-declaration,
-Werror=implicit-int, -Werror=return-type, -Werror=strict-prototypes, and
-Werror=incompatible-pointer-types, but -Werror=int-conversion appears
nowhere. int-conversion only became a hard error by compiler default in
GCC 14 and Clang 16, while Documentation/process/changes.rst still lists
GCC 8.1 as the minimum supported version.
On a GCC 8.1-13 build, the misuse compiles with only a warning rather than
an error. Could the phrasing be adjusted to reflect that this is a warning
rather than a guaranteed compile-time failure across the full supported
toolchain range?
> + */
> +#define VERIFY_USE_BPF_KEYRING 3
> +
> static inline int system_keyring_id_check(u64 id)
> {
> if (id > (unsigned long)VERIFY_USE_PLATFORM_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/32530917987
^ permalink raw reply [flat|nested] 25+ messages in thread
* [PATCH bpf-next 02/11] bpf: Refuse caller-supplied keyrings when the bpf one is active
2026-08-21 21:41 [PATCH bpf-next 00/11] BPF keyring and signed loader ML-DSA support Daniel Borkmann
2026-08-21 21:41 ` [PATCH bpf-next 01/11] bpf: Add a bpf keyring for program signature validation Daniel Borkmann
@ 2026-08-21 21:41 ` Daniel Borkmann
2026-08-21 22:39 ` bot+bpf-ci
2026-08-21 21:41 ` [PATCH bpf-next 03/11] bpf: Raise the bound on a program's signature size Daniel Borkmann
` (8 subsequent siblings)
10 siblings, 1 reply; 25+ messages in thread
From: Daniel Borkmann @ 2026-08-21 21:41 UTC (permalink / raw)
To: memxor; +Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf
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 | 7 +++++
include/linux/bpf.h | 6 ++++
kernel/bpf/keys.c | 5 ++++
kernel/bpf/verifier.c | 29 ++++++++++++-------
4 files changed, 36 insertions(+), 11 deletions(-)
diff --git a/Documentation/admin-guide/kernel-parameters.txt b/Documentation/admin-guide/kernel-parameters.txt
index 2beb61092bb3..543f245cc255 100644
--- a/Documentation/admin-guide/kernel-parameters.txt
+++ b/Documentation/admin-guide/kernel-parameters.txt
@@ -673,6 +673,13 @@ 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 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. 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 240e527c864b..f6ef16c938cb 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 dc4d3a33158a..60cb85295c89 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 3be8d51d35ac..a93a8dc427d8 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -21018,21 +21018,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 == VERIFY_USE_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 != VERIFY_USE_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 == VERIFY_USE_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] 25+ messages in thread* Re: [PATCH bpf-next 02/11] bpf: Refuse caller-supplied keyrings when the bpf one is active
2026-08-21 21:41 ` [PATCH bpf-next 02/11] bpf: Refuse caller-supplied keyrings when the bpf one is active Daniel Borkmann
@ 2026-08-21 22:39 ` bot+bpf-ci
0 siblings, 0 replies; 25+ messages in thread
From: bot+bpf-ci @ 2026-08-21 22:39 UTC (permalink / raw)
To: daniel, memxor
Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf, ast,
andrii, daniel, martin.lau, eddyz87, yonghong.song, clm,
ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 3745 bytes --]
> diff --git a/Documentation/admin-guide/kernel-parameters.txt b/Documentation/admin-guide/kernel-parameters.txt
> index 2beb61092bb31..543f245cc2557 100644
> --- a/Documentation/admin-guide/kernel-parameters.txt
> +++ b/Documentation/admin-guide/kernel-parameters.txt
> @@ -673,6 +673,13 @@ 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 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. Leaving it unset keeps the prior
> + behaviour, where a caller-supplied keyring is allowed.
> +
Is this paragraph accurate? It states the bpf keyring is "the only
keyring a loader may select" but the implementation allows system
keyrings to remain selectable:
kernel/bpf/verifier.c:bpf_prog_verify_signature() {
if (!system_keyring_id_check(attr->keyring_id)) {
key = bpf_lookup_system_key(attr->keyring_id);
} else {
if (attr->keyring_id != VERIFY_USE_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();
}
}
}
system_keyring_id_check() accepts keyring_id values 0 (builtin), 1
(secondary), and 2 (platform), so a loader can still pass any of those
three ids and succeed. Only the user/session branch is refused.
The paragraph's lead-in sentence describes a stronger guarantee ("the only
keyring") than the code provides, while the clause after the colon correctly
describes the narrower behaviour ("caller-supplied user/session keyrings are
refused"). This is the text an operator reads when deciding their boot-time
trust configuration.
The more accurate phrasing appears in Documentation/bpf/signing.rst added
later in the series, which states "the only non-system keyring a loader may
select" and "The system keyrings stay selectable."
Could kernel-parameters.txt use the same "non-system keyring" qualifier?
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index 3be8d51d35ac4..a93a8dc427d8c 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
> @@ -21018,21 +21018,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 == VERIFY_USE_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 != VERIFY_USE_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();
> + }
> }
[ ... ]
---
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/32530917987
^ permalink raw reply [flat|nested] 25+ messages in thread
* [PATCH bpf-next 03/11] bpf: Raise the bound on a program's signature size
2026-08-21 21:41 [PATCH bpf-next 00/11] BPF keyring and signed loader ML-DSA support Daniel Borkmann
2026-08-21 21:41 ` [PATCH bpf-next 01/11] bpf: Add a bpf keyring for program signature validation Daniel Borkmann
2026-08-21 21:41 ` [PATCH bpf-next 02/11] bpf: Refuse caller-supplied keyrings when the bpf one is active Daniel Borkmann
@ 2026-08-21 21:41 ` Daniel Borkmann
2026-08-21 22:39 ` bot+bpf-ci
2026-08-21 21:41 ` [PATCH bpf-next 04/11] bpftool: Support ML-DSA program signing Daniel Borkmann
` (7 subsequent siblings)
10 siblings, 1 reply; 25+ messages in thread
From: Daniel Borkmann @ 2026-08-21 21:41 UTC (permalink / raw)
To: memxor; +Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf
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.
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
---
kernel/bpf/verifier.c | 14 +++++++++-----
.../selftests/bpf/prog_tests/signed_loader.c | 5 +++--
2 files changed, 12 insertions(+), 7 deletions(-)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index a93a8dc427d8..575c4e5e4443 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -20972,6 +20972,13 @@ 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.
+ */
+#define BPF_PROG_MAX_SIGNATURE_SIZE (64 * 1024)
+
static enum bpf_sig_keyring bpf_classify_keyring(s32 keyring_id)
{
switch (keyring_id) {
@@ -21011,13 +21018,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] 25+ messages in thread* Re: [PATCH bpf-next 03/11] bpf: Raise the bound on a program's signature size
2026-08-21 21:41 ` [PATCH bpf-next 03/11] bpf: Raise the bound on a program's signature size Daniel Borkmann
@ 2026-08-21 22:39 ` bot+bpf-ci
0 siblings, 0 replies; 25+ messages in thread
From: bot+bpf-ci @ 2026-08-21 22:39 UTC (permalink / raw)
To: daniel, memxor
Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf, ast,
andrii, daniel, martin.lau, eddyz87, yonghong.song, clm,
ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 2608 bytes --]
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index a93a8dc427d8c..575c4e5e44432 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
> @@ -20972,6 +20972,13 @@ 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.
> + */
> +#define BPF_PROG_MAX_SIGNATURE_SIZE (64 * 1024)
> +
> static enum bpf_sig_keyring bpf_classify_keyring(s32 keyring_id)
> {
> switch (keyring_id) {
> @@ -21011,13 +21018,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;
> +
Does this replacement actually lower the accepted signature size on
kernels with PAGE_SIZE >= 64 KiB?
The old bound was page-size dependent (include/linux/slab.h):
#define KMALLOC_SHIFT_HIGH (PAGE_SHIFT + 1)
#define KMALLOC_MAX_CACHE_SIZE (1UL << KMALLOC_SHIFT_HIGH)
so the previously accepted maximum was:
PAGE_SIZE 4 KiB -> 8 KiB (new 64 KiB: raised)
PAGE_SIZE 16 KiB -> 32 KiB (new 64 KiB: raised)
PAGE_SIZE 64 KiB -> 128 KiB (new 64 KiB: HALVED)
On CONFIG_ARM64_64K_PAGES or ppc64 64K-page kernels, a BPF_PROG_LOAD
carrying a signature blob between 64 KiB and 128 KiB was accepted by the
pre-patch kernel and now returns -EINVAL. There is no other place that
validates attr->signature_size, so nothing softens the new rejection.
The changelog only describes the 4 KiB-page case ("signature_size is
bounded by KMALLOC_MAX_CACHE_SIZE, which is 8 KiB on a 4 KiB page system")
and frames the change purely as "Raise the bound"; the reduction on
64 KiB-page configurations is not mentioned.
Is the intent to make the ABI page-size independent? If so, should the
changelog say that explicitly? Or if the intent is only to raise, should
the constant be >= 128 KiB (or max(64 KiB, KMALLOC_MAX_CACHE_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/32530917987
^ permalink raw reply [flat|nested] 25+ messages in thread
* [PATCH bpf-next 04/11] bpftool: Support ML-DSA program signing
2026-08-21 21:41 [PATCH bpf-next 00/11] BPF keyring and signed loader ML-DSA support Daniel Borkmann
` (2 preceding siblings ...)
2026-08-21 21:41 ` [PATCH bpf-next 03/11] bpf: Raise the bound on a program's signature size Daniel Borkmann
@ 2026-08-21 21:41 ` Daniel Borkmann
2026-08-21 22:39 ` bot+bpf-ci
2026-08-21 21:41 ` [PATCH bpf-next 05/11] selftests/bpf: Add a test for the sealed bpf keyring Daniel Borkmann
` (6 subsequent siblings)
10 siblings, 1 reply; 25+ messages in thread
From: Daniel Borkmann @ 2026-08-21 21:41 UTC (permalink / raw)
To: memxor; +Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf
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").
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
---
tools/bpf/bpftool/main.h | 2 +-
tools/bpf/bpftool/sign.c | 22 +++++++++++++++++++---
2 files changed, 20 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..818a4f900bbb 100644
--- a/tools/bpf/bpftool/sign.c
+++ b/tools/bpf/bpftool/sign.c
@@ -130,6 +130,9 @@ __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 |
+ 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 +170,21 @@ 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")) {
+ /*
+ * ML-DSA + CMS_NOATTR is not supported in openssl-3.5 and
+ * before, so fall back to signedAttrs. ML-DSA does its own
+ * hashing and ignores the CMS digest, except when signedAttrs
+ * are used, where only SHA-512 is permitted.
+ */
+ 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 +193,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] 25+ messages in thread* Re: [PATCH bpf-next 04/11] bpftool: Support ML-DSA program signing
2026-08-21 21:41 ` [PATCH bpf-next 04/11] bpftool: Support ML-DSA program signing Daniel Borkmann
@ 2026-08-21 22:39 ` bot+bpf-ci
0 siblings, 0 replies; 25+ messages in thread
From: bot+bpf-ci @ 2026-08-21 22:39 UTC (permalink / raw)
To: daniel, memxor
Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf, ast,
andrii, daniel, martin.lau, eddyz87, yonghong.song, clm,
ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 2904 bytes --]
> bpftool: Support ML-DSA program signing
>
> 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").
This isn't a bug, but the changelog describes the change as dropping
CMS_NOATTR "the same way as commit 0ad9a71933e7", though the diff does two
further things a reviewer cannot infer from it.
First, MAX_SIG_SIZE grows 4x (4096 to 16384):
> diff --git a/tools/bpf/bpftool/main.h b/tools/bpf/bpftool/main.h
> --- a/tools/bpf/bpftool/main.h
> +++ b/tools/bpf/bpftool/main.h
> @@ -57,7 +57,7 @@ #define REQ_ARGS(cnt) \
> })
>
> #define ERR_MAX_LEN 1024
> -#define MAX_SIG_SIZE 4096
> +#define MAX_SIG_SIZE 16384
which resizes the on-stack sig_buf[] in both gen_trace() and try_loader().
The number is also unrelated to the BPF_PROG_MAX_SIGNATURE_SIZE of 64 KiB
introduced two patches earlier in the same series, so the choice of 16384 is
unexplained.
Second, the ML-DSA branch also pins the CMS digest to SHA-512:
> diff --git a/tools/bpf/bpftool/sign.c b/tools/bpf/bpftool/sign.c
> --- a/tools/bpf/bpftool/sign.c
> +++ b/tools/bpf/bpftool/sign.c
> @@ -130,6 +130,9 @@ __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 |
> + CMS_USE_KEYID | CMS_NOATTR;
> + const EVP_MD *cms_digest = EVP_sha256();
[ ... ]
> @@ -167,6 +170,21 @@ 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")) {
> + /*
> + * ML-DSA + CMS_NOATTR is not supported in openssl-3.5 and
> + * before, so fall back to signedAttrs. ML-DSA does its own
> + * hashing and ignores the CMS digest, except when signedAttrs
> + * are used, where only SHA-512 is permitted.
> + */
> + signer_flags &= ~CMS_NOATTR;
> + cms_digest = EVP_sha512();
> + }
> +#endif
which the referenced modsign commit does not do. The referenced
scripts/sign-file.c keeps taking its digest from the -d argument and only
clears CMS_NOATTR. The reasoning for the SHA-512 pin is in the code comment
but not in the changelog.
Would it be worth a sentence on why MAX_SIG_SIZE lands on 16384 (rather
than matching the 64 KiB kernel bound added earlier in the series), and
noting that unlike sign-file.c this also pins the CMS digest to SHA-512?
---
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/32530917987
^ permalink raw reply [flat|nested] 25+ messages in thread
* [PATCH bpf-next 05/11] selftests/bpf: Add a test for the sealed bpf keyring
2026-08-21 21:41 [PATCH bpf-next 00/11] BPF keyring and signed loader ML-DSA support Daniel Borkmann
` (3 preceding siblings ...)
2026-08-21 21:41 ` [PATCH bpf-next 04/11] bpftool: Support ML-DSA program signing Daniel Borkmann
@ 2026-08-21 21:41 ` Daniel Borkmann
2026-08-21 21:53 ` sashiko-bot
2026-08-21 22:50 ` bot+bpf-ci
2026-08-21 21:41 ` [PATCH bpf-next 06/11] selftests/bpf: Rebuild signed lskels when signing key changes Daniel Borkmann
` (5 subsequent siblings)
10 siblings, 2 replies; 25+ messages in thread
From: Daniel Borkmann @ 2026-08-21 21:41 UTC (permalink / raw)
To: memxor; +Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf
bpf_keyring_sealed checks that a load naming the bpf keyring fails with
-ENOKEY 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
[...]
#424/9 signed_loader/signed_module_kfunc_rejected:OK
#424/10 signed_loader/signature_failure_logs:OK
#424/11 signed_loader/signature_too_large:OK
#424/12 signed_loader/signature_zero_size:OK
#424/13 signed_loader/signature_bad_keyring:OK
#424/14 signed_loader/bpf_keyring_sealed:OK
#424/15 signed_loader/metadata_ctx_max_entries_ignored:OK
#424/16 signed_loader/metadata_ctx_initial_value_ignored:OK
#424/17 signed_loader/signature_authenticates_insns:OK
#424/18 signed_loader/signature_authenticates_metadata:OK
#424/19 signed_loader/hash_requires_frozen:OK
[...]
#424 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 | 27 +++++++++++++++++++
1 file changed, 27 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..94b57e7cdab3 100644
--- a/tools/testing/selftests/bpf/prog_tests/signed_loader.c
+++ b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
@@ -32,8 +32,11 @@ enum {
BPF_SIG_KEYRING_SECONDARY,
BPF_SIG_KEYRING_PLATFORM,
BPF_SIG_KEYRING_USER,
+ BPF_SIG_KEYRING_BPF,
};
+#define BPF_KEYRING_BPF 3
+
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 +630,28 @@ static void signature_bad_keyring(void)
gen_loader_fixture_fini(&f);
}
+static void bpf_keyring_sealed(void)
+{
+ static const __u8 junk[64] = {};
+ struct gen_loader_fixture f;
+ int fd;
+
+ if (gen_loader_fixture_init(&f) == 0) {
+ /*
+ * Without bpf.keyring_unsealed=1 on the command line the bpf
+ * keyring is sealed empty during boot, so it is never handed
+ * out and a load naming it fails with -ENOKEY before the
+ * signature bytes are examined.
+ */
+ fd = load_loader(f.gopts.insns, f.gopts.insns_sz, -1, junk,
+ sizeof(junk), BPF_KEYRING_BPF, 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 +1831,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] 25+ messages in thread* Re: [PATCH bpf-next 05/11] selftests/bpf: Add a test for the sealed bpf keyring
2026-08-21 21:41 ` [PATCH bpf-next 05/11] selftests/bpf: Add a test for the sealed bpf keyring Daniel Borkmann
@ 2026-08-21 21:53 ` sashiko-bot
2026-08-21 22:50 ` bot+bpf-ci
1 sibling, 0 replies; 25+ messages in thread
From: sashiko-bot @ 2026-08-21 21:53 UTC (permalink / raw)
To: Daniel Borkmann; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] API conflict and UAPI omission: The BPF system keyring sentinel value (3) collides with the valid user key serial number space.
--
--- Patch [5]: [PATCH bpf-next 05/11] selftests/bpf: Add a test for the sealed bpf keyring ---
commit 3de34ebcd8baa817e75280e15e9985c407307d98
Author: Daniel Borkmann <daniel@iogearbox.net>
selftests/bpf: Add a test for the sealed bpf keyring
This commit adds a selftest to ensure that loading a program naming the BPF
keyring fails with -ENOKEY when the keyring has not been provisioned. It
verifies the ordering of error checks prior to real signature processing.
> diff --git a/tools/testing/selftests/bpf/prog_tests/signed_loader.c b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
> index 0c5294738d6ce..94b57e7cdab39 100644
> --- a/tools/testing/selftests/bpf/prog_tests/signed_loader.c
> +++ b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
> @@ -32,8 +32,11 @@ enum {
> BPF_SIG_KEYRING_SECONDARY,
> BPF_SIG_KEYRING_PLATFORM,
> BPF_SIG_KEYRING_USER,
> + BPF_SIG_KEYRING_BPF,
> };
>
> +#define BPF_KEYRING_BPF 3
> +
[Severity: Medium]
Does this hardcoded magic number (3) for the BPF keyring collide with the
valid user key serial number space?
Since key_alloc_serial() in security/keys/key.c only excludes values less
than 3, a user's dynamically allocated key serial could theoretically be
assigned serial 3.
If a user creates a keyring and happens to get assigned serial 3, and then
passes keyring_id = 3 to bpf_prog_load, wouldn't the verifier intercept it
via VERIFY_USE_BPF_KEYRING (which is defined as 3 in
include/linux/verification.h) and incorrectly route it to the .bpf system
keyring instead of the user's provided keyring?
Additionally, should this value be exposed via a UAPI header rather than
requiring user-space to hardcode it in the selftest?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260821214111.1120748-1-daniel@iogearbox.net?part=5
^ permalink raw reply [flat|nested] 25+ messages in thread* Re: [PATCH bpf-next 05/11] selftests/bpf: Add a test for the sealed bpf keyring
2026-08-21 21:41 ` [PATCH bpf-next 05/11] selftests/bpf: Add a test for the sealed bpf keyring Daniel Borkmann
2026-08-21 21:53 ` sashiko-bot
@ 2026-08-21 22:50 ` bot+bpf-ci
1 sibling, 0 replies; 25+ messages in thread
From: bot+bpf-ci @ 2026-08-21 22:50 UTC (permalink / raw)
To: daniel, memxor
Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf, ast,
andrii, daniel, martin.lau, eddyz87, yonghong.song, clm,
ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 3631 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..94b57e7cdab39 100644
> --- a/tools/testing/selftests/bpf/prog_tests/signed_loader.c
> +++ b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
[ ... ]
> @@ -627,6 +630,28 @@ static void signature_bad_keyring(void)
> gen_loader_fixture_fini(&f);
> }
>
> +static void bpf_keyring_sealed(void)
> +{
> + static const __u8 junk[64] = {};
> + struct gen_loader_fixture f;
> + int fd;
> +
> + if (gen_loader_fixture_init(&f) == 0) {
> + /*
> + * Without bpf.keyring_unsealed=1 on the command line the bpf
> + * keyring is sealed empty during boot, so it is never handed
> + * out and a load naming it fails with -ENOKEY before the
> + * signature bytes are examined.
> + */
> + fd = load_loader(f.gopts.insns, f.gopts.insns_sz, -1, junk,
> + sizeof(junk), BPF_KEYRING_BPF, 0);
> + ASSERT_EQ(fd, -ENOKEY, "sealed bpf keyring rejected");
A subsystem pattern flags this as potentially concerning: the subtest is
named bpf_keyring_sealed and asserts -ENOKEY, but does -ENOKEY actually
distinguish sealing from the keyring merely being empty?
Looking at bpf_prog_verify_signature() in kernel/bpf/verifier.c, the
-ENOKEY path is taken when:
if (!key) {
if (attr->keyring_id == VERIFY_USE_BPF_KEYRING) {
verbose(env, "the bpf keyring is empty or has not been restricted\\n");
return -ENOKEY;
and key is NULL whenever either condition in bpf_lookup_keyring()
(kernel/bpf/keys.c) holds:
if (!READ_ONCE(bpf_keyring->keys.nr_leaves_on_tree) ||
!READ_ONCE(bpf_keyring->restrict_link))
return NULL;
In the boot state this test exercises (bpf.keyring_unsealed unset), both
conditions are true at once: bpf_keyring_init() calls keyring_restrict() and
the keyring has zero keys. The asserted -ENOKEY is therefore produced by the
emptiness check on its own.
If the sealing under test regressed - say keyring_restrict() were dropped
from bpf_keyring_init(), or it failed and took the pr_err("cannot seal bpf
keyring") path that leaves bpf_keyring NULL - the keyring would still be
empty at boot and the load would still return -ENOKEY. Would the subtest
still pass in that case?
This is confirmed later in the series: baedf3d941d9c ("selftests/bpf: Add
tests for bpf keyring in signed loader") adds bpf_keyring_provisioned(),
which enrolls a key and then asserts -ENOKEY with the message "unrestricted
keyring still not consulted" - a populated-but-unrestricted keyring yields
the same errno this subtest asserts for an empty-and-restricted one.
The subtest does verify the keyring_id routing: without the
VERIFY_USE_BPF_KEYRING special case, keyring_id 3 would return -EINVAL
instead, so -ENOKEY vs -EINVAL is a real distinction. The question is
whether the sealing aspect of the name is fully backed by what the kernel
path actually checks.
Would renaming the subtest and assert message to describe what is checked
(an unprovisioned bpf keyring is never handed out) rather than the sealing
mechanism avoid claiming more than the -ENOKEY assertion verifies, or should
the sealing claim be left to bpf_keyring_provisioned() which separates the
two conditions?
> + if (fd >= 0)
> + close(fd);
> + }
> + gen_loader_fixture_fini(&f);
> +}
[ ... ]
---
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/32530917987
^ permalink raw reply [flat|nested] 25+ messages in thread
* [PATCH bpf-next 06/11] selftests/bpf: Rebuild signed lskels when signing key changes
2026-08-21 21:41 [PATCH bpf-next 00/11] BPF keyring and signed loader ML-DSA support Daniel Borkmann
` (4 preceding siblings ...)
2026-08-21 21:41 ` [PATCH bpf-next 05/11] selftests/bpf: Add a test for the sealed bpf keyring Daniel Borkmann
@ 2026-08-21 21:41 ` Daniel Borkmann
2026-08-21 21:41 ` [PATCH bpf-next 07/11] selftests/bpf: Rename the verify_sig_setup.sh setup into setup-rsa Daniel Borkmann
` (4 subsequent siblings)
10 siblings, 0 replies; 25+ messages in thread
From: Daniel Borkmann @ 2026-08-21 21:41 UTC (permalink / raw)
To: memxor; +Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf
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] 25+ messages in thread* [PATCH bpf-next 07/11] selftests/bpf: Rename the verify_sig_setup.sh setup into setup-rsa
2026-08-21 21:41 [PATCH bpf-next 00/11] BPF keyring and signed loader ML-DSA support Daniel Borkmann
` (5 preceding siblings ...)
2026-08-21 21:41 ` [PATCH bpf-next 06/11] selftests/bpf: Rebuild signed lskels when signing key changes Daniel Borkmann
@ 2026-08-21 21:41 ` Daniel Borkmann
2026-08-21 21:41 ` [PATCH bpf-next 08/11] selftests/bpf: Add an end-to-end ML-DSA signed loader test Daniel Borkmann
` (3 subsequent siblings)
10 siblings, 0 replies; 25+ messages in thread
From: Daniel Borkmann @ 2026-08-21 21:41 UTC (permalink / raw)
To: memxor; +Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf
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 94b57e7cdab3..4b2416903d90 100644
--- a/tools/testing/selftests/bpf/prog_tests/signed_loader.c
+++ b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
@@ -460,7 +460,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;
}
@@ -857,7 +857,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;
}
@@ -957,7 +957,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;
}
@@ -1293,7 +1293,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;
@@ -1476,7 +1476,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;
}
@@ -1574,7 +1574,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;
}
@@ -1645,7 +1645,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;
}
@@ -1707,7 +1707,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;
}
@@ -1761,7 +1761,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] 25+ messages in thread* [PATCH bpf-next 08/11] selftests/bpf: Add an end-to-end ML-DSA signed loader test
2026-08-21 21:41 [PATCH bpf-next 00/11] BPF keyring and signed loader ML-DSA support Daniel Borkmann
` (6 preceding siblings ...)
2026-08-21 21:41 ` [PATCH bpf-next 07/11] selftests/bpf: Rename the verify_sig_setup.sh setup into setup-rsa Daniel Borkmann
@ 2026-08-21 21:41 ` Daniel Borkmann
2026-08-21 21:50 ` sashiko-bot
2026-08-21 22:39 ` bot+bpf-ci
2026-08-21 21:41 ` [PATCH bpf-next 09/11] selftests/bpf: Allow appending to guest kernel cmdline in vmtest.sh Daniel Borkmann
` (2 subsequent siblings)
10 siblings, 2 replies; 25+ messages in thread
From: Daniel Borkmann @ 2026-08-21 21:41 UTC (permalink / raw)
To: memxor; +Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf
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
[...]
#424/10 signed_loader/signature_failure_logs:OK
#424/11 signed_loader/signature_too_large:OK
#424/12 signed_loader/signature_zero_size:OK
#424/13 signed_loader/signature_bad_keyring:OK
#424/14 signed_loader/bpf_keyring_sealed:OK
#424/15 signed_loader/mldsa_signed_load:OK
#424/16 signed_loader/metadata_ctx_max_entries_ignored:OK
#424/17 signed_loader/metadata_ctx_initial_value_ignored:OK
#424/18 signed_loader/signature_authenticates_insns:OK
#424/19 signed_loader/signature_authenticates_metadata:OK
#424/20 signed_loader/hash_requires_frozen:OK
[...]
#424 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 | 1 +
.../selftests/bpf/prog_tests/signed_loader.c | 106 +++++++++++++++++-
.../testing/selftests/bpf/verify_sig_setup.sh | 57 +++++++++-
3 files changed, 157 insertions(+), 7 deletions(-)
diff --git a/tools/testing/selftests/bpf/config b/tools/testing/selftests/bpf/config
index ea7044f30adc..4e6d13dbf266 100644
--- a/tools/testing/selftests/bpf/config
+++ b/tools/testing/selftests/bpf/config
@@ -51,6 +51,7 @@ CONFIG_IPV6_SEG6_LWTUNNEL=y
CONFIG_IPV6_SIT=y
CONFIG_IPV6_TUNNEL=y
CONFIG_KEYS=y
+CONFIG_CRYPTO_MLDSA=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 4b2416903d90..a1fa1c37815b 100644
--- a/tools/testing/selftests/bpf/prog_tests/signed_loader.c
+++ b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
@@ -37,6 +37,12 @@ enum {
#define BPF_KEYRING_BPF 3
+/* 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)
@@ -159,12 +165,13 @@ 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 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")];
@@ -193,7 +200,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);
}
@@ -231,6 +238,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;
@@ -1550,6 +1563,87 @@ 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;
+
+ 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();
+ rmdir(dir);
+ return;
+ }
+ if (!ASSERT_OK(err, "verify_sig_setup setup-mldsa")) {
+ rmdir(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
@@ -1833,6 +1927,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] 25+ messages in thread* Re: [PATCH bpf-next 08/11] selftests/bpf: Add an end-to-end ML-DSA signed loader test
2026-08-21 21:41 ` [PATCH bpf-next 08/11] selftests/bpf: Add an end-to-end ML-DSA signed loader test Daniel Borkmann
@ 2026-08-21 21:50 ` sashiko-bot
2026-08-21 22:39 ` bot+bpf-ci
1 sibling, 0 replies; 25+ messages in thread
From: sashiko-bot @ 2026-08-21 21:50 UTC (permalink / raw)
To: Daniel Borkmann; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] Temporary directory and generated key files are leaked if `verify_sig_setup.sh setup-mldsa` fails.
--
--- Patch [8]: [PATCH bpf-next 08/11] selftests/bpf: Add an end-to-end ML-DSA signed loader test ---
commit 3cc419c08f9d1d4d5a77de9eb5cccbc21b94232e
Author: Daniel Borkmann <daniel@iogearbox.net>
selftests/bpf: Add an end-to-end ML-DSA signed loader test
This commit adds an end-to-end ML-DSA signed loader test to the BPF CI. It
introduces a new test case that verifies an ML-DSA signed program can be
loaded successfully using the existing pkcs7 signature verification.
> diff --git a/tools/testing/selftests/bpf/prog_tests/signed_loader.c b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
> index 4b2416903d90d..a1fa1c37815bf 100644
> --- a/tools/testing/selftests/bpf/prog_tests/signed_loader.c
> +++ b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
[ ... ]
> @@ -1550,6 +1563,87 @@ 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;
> +
> + 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();
> + rmdir(dir);
> + return;
> + }
> + if (!ASSERT_OK(err, "verify_sig_setup setup-mldsa")) {
> + rmdir(dir);
[Severity: Low]
If run_setup("setup-mldsa", dir) fails after creating files in the temporary
directory (for example, if keyctl padd fails in the bash script), does this
code leak the temporary directory and its contents?
Since rmdir() does not recursively remove files, it will fail with ENOTEMPTY
if there are still generated files like signing_key.der inside the directory.
Would it be safer to call run_setup("cleanup", dir) here instead of rmdir()
to ensure all temporary files are properly cleaned up?
> + return;
> + }
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260821214111.1120748-1-daniel@iogearbox.net?part=8
^ permalink raw reply [flat|nested] 25+ messages in thread* Re: [PATCH bpf-next 08/11] selftests/bpf: Add an end-to-end ML-DSA signed loader test
2026-08-21 21:41 ` [PATCH bpf-next 08/11] selftests/bpf: Add an end-to-end ML-DSA signed loader test Daniel Borkmann
2026-08-21 21:50 ` sashiko-bot
@ 2026-08-21 22:39 ` bot+bpf-ci
1 sibling, 0 replies; 25+ messages in thread
From: bot+bpf-ci @ 2026-08-21 22:39 UTC (permalink / raw)
To: daniel, memxor
Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf, ast,
andrii, daniel, martin.lau, eddyz87, yonghong.song, clm,
ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 4801 bytes --]
> diff --git a/tools/testing/selftests/bpf/config b/tools/testing/selftests/bpf/config
> index ea7044f30adc3..4e6d13dbf2662 100644
> --- a/tools/testing/selftests/bpf/config
> +++ b/tools/testing/selftests/bpf/config
> @@ -51,6 +51,7 @@ CONFIG_IPV6_SEG6_LWTUNNEL=y
> CONFIG_IPV6_SIT=y
> CONFIG_IPV6_TUNNEL=y
> CONFIG_KEYS=y
> +CONFIG_CRYPTO_MLDSA=y
> CONFIG_LIRC=y
> CONFIG_LIVEPATCH=y
> CONFIG_LWTUNNEL=y
Does this fragment need CONFIG_CRYPTO_SHA512=y alongside
CONFIG_CRYPTO_MLDSA?
mldsa_signed_load() signs with sign_buf_digest(..., "sha512"), so the
PKCS#7 blob carries signedAttrs with a SHA-512 messageDigest. On the
kernel side pkcs7_digest() takes the 'sinfo->authattrs' path and does:
tfm = crypto_alloc_shash(sinfo->sig->hash_algo, 0, 0);
if (IS_ERR(tfm))
return (PTR_ERR(tfm) == -ENOENT) ? -ENOPKG : PTR_ERR(tfm);
Without CRYPTO_SHA512 that returns -ENOPKG and the load fails. It happens
to work today: the fragment has CONFIG_MODULE_SIG=y and the module-signing
default choice selects CRYPTO_SHA512, so the dependency is satisfied only
as a side effect of an unrelated option's default. It would break silently
if that default or CONFIG_MODULE_SIG ever changed. The file already lists
the RSA path's digest explicitly (CONFIG_CRYPTO_SHA256=y at line 15), so
an explicit CONFIG_CRYPTO_SHA512=y here would match the file's own
convention.
> diff --git a/tools/testing/selftests/bpf/prog_tests/signed_loader.c b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
> index 4b2416903d90d..a1fa1c37815bf 100644
> --- a/tools/testing/selftests/bpf/prog_tests/signed_loader.c
> +++ b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
[ ... ]
> @@ -1550,6 +1563,87 @@ 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;
> +
> + dir = mkdtemp(dir_tmpl);
> + if (!ASSERT_OK_PTR(dir, "mkdtemp"))
> + return;
> +
> + err = run_setup("setup-mldsa", dir);
> + if (err == SETUP_SKIP) {
Should mldsa_signed_load() create a session keyring before running
verify_sig_setup.sh?
Every other subtest in this file that calls run_setup() and then loads
with KEY_SPEC_SESSION_KEYRING first does:
syscall(__NR_request_key, "keyring", "_uid.0", NULL,
KEY_SPEC_SESSION_KEYRING);
dir = mkdtemp(dir_tmpl);
Nine sites, one per setup-rsa user. prog_tests/verify_pkcs7_sig.c:252
spells out why: '/* Trigger creation of session keyring. */'.
mldsa_signed_load() is the only run_setup() user in the file that omits
it.
Why it matters: the setup script adds the key with 'keyctl padd
asymmetric ebpf_testing_key @s', i.e. add_key(2), which calls
lookup_user_key(ringid, KEY_LOOKUP_CREATE, KEY_NEED_WRITE). The
KEY_SPEC_SESSION_KEYRING case with KEY_LOOKUP_CREATE runs
join_session_keyring(NULL) whenever cred->session_keyring is NULL (or is
the UID keyring). That creates a brand-new session keyring inside the
forked script process only; it dies with the script.
The parent test process then loads with attr.keyring_id =
KEY_SPEC_SESSION_KEYRING, which resolves with
bpf_lookup_user_key(attr->keyring_id, 0) - no KEY_LOOKUP_CREATE - so it
gets the user-session keyring instead, which never received
ebpf_testing_key. The load then fails and ASSERT_OK_FD(prog_fd, "ML-DSA
signed loader load") reports a failure.
In a full run this is hidden: loadtime_no_map() is the first subtest and
loadtime_verify() installs the session keyring for the whole process,
which is why the log in the commit message shows #424/15
mldsa_signed_load:OK.
Running the subtest on its own, e.g.
./test_progs -a 'signed_loader/mldsa_signed_load'
skips every subtest that would have created the keyring, and the new
subtest fails.
> 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
[ ... ]
---
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/32530917987
^ permalink raw reply [flat|nested] 25+ messages in thread
* [PATCH bpf-next 09/11] selftests/bpf: Allow appending to guest kernel cmdline in vmtest.sh
2026-08-21 21:41 [PATCH bpf-next 00/11] BPF keyring and signed loader ML-DSA support Daniel Borkmann
` (7 preceding siblings ...)
2026-08-21 21:41 ` [PATCH bpf-next 08/11] selftests/bpf: Add an end-to-end ML-DSA signed loader test Daniel Borkmann
@ 2026-08-21 21:41 ` Daniel Borkmann
2026-08-21 21:41 ` [PATCH bpf-next 10/11] selftests/bpf: Add tests for bpf keyring in signed loader Daniel Borkmann
2026-08-21 21:41 ` [PATCH bpf-next 11/11] Documentation/bpf: Document the bpf keyring and improve examples Daniel Borkmann
10 siblings, 0 replies; 25+ messages in thread
From: Daniel Borkmann @ 2026-08-21 21:41 UTC (permalink / raw)
To: memxor; +Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf
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 to it when set to it can be used for testing
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] 25+ messages in thread* [PATCH bpf-next 10/11] selftests/bpf: Add tests for bpf keyring in signed loader
2026-08-21 21:41 [PATCH bpf-next 00/11] BPF keyring and signed loader ML-DSA support Daniel Borkmann
` (8 preceding siblings ...)
2026-08-21 21:41 ` [PATCH bpf-next 09/11] selftests/bpf: Allow appending to guest kernel cmdline in vmtest.sh Daniel Borkmann
@ 2026-08-21 21:41 ` Daniel Borkmann
2026-08-21 21:53 ` sashiko-bot
2026-08-21 22:50 ` bot+bpf-ci
2026-08-21 21:41 ` [PATCH bpf-next 11/11] Documentation/bpf: Document the bpf keyring and improve examples Daniel Borkmann
10 siblings, 2 replies; 25+ messages in thread
From: Daniel Borkmann @ 2026-08-21 21:41 UTC (permalink / raw)
To: memxor; +Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf
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
[...]
#424/12 signed_loader/signature_zero_size:OK
#424/13 signed_loader/signature_bad_keyring:OK
#424/14 signed_loader/bpf_keyring_sealed:OK
[...]
#424/30 signed_loader/signed_map_by_fd_rejected:OK
#424/31 signed_loader/signed_sparse_fd_array_rejected:OK
#424 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
#424/1 signed_loader/bpf_keyring_provisioned:OK
#424 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 | 362 +++++++++++++++++-
1 file changed, 356 insertions(+), 6 deletions(-)
diff --git a/tools/testing/selftests/bpf/prog_tests/signed_loader.c b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
index a1fa1c37815b..9d2384071a42 100644
--- a/tools/testing/selftests/bpf/prog_tests/signed_loader.c
+++ b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
@@ -69,6 +69,33 @@ static int load_loader(const void *insns, __u32 insns_sz, int map_fd,
return fd < 0 ? -errno : fd;
}
+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;
+
+ memset(&attr, 0, sizeof(attr));
+ attr.prog_type = BPF_PROG_TYPE_SYSCALL;
+ attr.insns = ptr_to_u64(insns);
+ attr.insn_cnt = insns_sz / sizeof(struct bpf_insn);
+ attr.license = ptr_to_u64("Dual BSD/GPL");
+ attr.prog_flags = BPF_F_SLEEPABLE;
+ attr.fd_array = ptr_to_u64(&map_fd);
+ attr.fd_array_cnt = fd_array_cnt;
+ attr.signature = ptr_to_u64(sig);
+ attr.signature_size = sig_sz;
+ attr.keyring_id = keyring_id;
+ 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 run_gen_loader(const void *insns, __u32 insns_sz,
const void *data, __u32 data_sz,
const void *excl, __u32 excl_sz,
@@ -170,6 +197,23 @@ static int run_setup(const char *cmd, const char *dir)
return -WEXITSTATUS(status);
}
+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)
{
@@ -186,6 +230,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;
@@ -210,30 +255,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;
}
@@ -643,6 +686,68 @@ 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 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 is provisioned: KEY_POS_SEARCH for
+ * the in-kernel search during verification, and the user view/read bits so it
+ * stays visible in /proc/keys. Write, search and setattr are what every path
+ * that removes a key goes through, so dropping them is what makes the enrolled
+ * set final.
+ */
+#define BPF_KEYRING_PERM_LOCKED 0x08030000
+
static void bpf_keyring_sealed(void)
{
static const __u8 junk[64] = {};
@@ -665,6 +770,246 @@ static void bpf_keyring_sealed(void)
gen_loader_fixture_fini(&f);
}
+/*
+ * 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";
+ int map_fd = -1, prog_fd = -1, serial, err;
+ __u8 *sig = NULL, *bad = NULL, *buf = NULL;
+ 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;
+
+ err = syscall(__NR_add_key, "asymmetric", "", der, (size_t)der_sz,
+ serial);
+ 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;
+
+ /*
+ * Still inert at this point: the keyring is non-empty but carries no
+ * restriction, so it is not handed out yet.
+ */
+ 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;
+
+ map_fd = setup_meta_map(&f);
+ if (!ASSERT_OK_FD(map_fd, "meta_map_unrestricted"))
+ goto out;
+ prog_fd = load_loader(f.gopts.insns, f.gopts.insns_sz, map_fd, sig,
+ sig_sz, BPF_KEYRING_BPF, 1);
+ close(map_fd);
+ map_fd = -1;
+ ASSERT_EQ(prog_fd, -ENOKEY, "unrestricted keyring still not consulted");
+ if (prog_fd >= 0)
+ close(prog_fd);
+ prog_fd = -1;
+
+ /*
+ * Enforcement follows the boot flag rather than the keyring's state, so
+ * a caller-supplied keyring is already refused here, while nothing has
+ * been provisioned yet.
+ */
+ map_fd = setup_meta_map(&f);
+ if (!ASSERT_OK_FD(map_fd, "meta_map_session_unprovisioned"))
+ goto out;
+ prog_fd = load_loader(f.gopts.insns, f.gopts.insns_sz, map_fd, sig,
+ sig_sz, KEY_SPEC_SESSION_KEYRING, 1);
+ close(map_fd);
+ map_fd = -1;
+ ASSERT_EQ(prog_fd, -EPERM, "caller-supplied keyring refused before provisioning");
+ if (prog_fd >= 0)
+ close(prog_fd);
+ prog_fd = -1;
+
+ /* Restricting it is what turns it on. */
+ if (!ASSERT_OK(syscall(__NR_keyctl, KEYCTL_RESTRICT_KEYRING, serial,
+ NULL, NULL), "restrict bpf keyring"))
+ goto out;
+
+ map_fd = setup_meta_map(&f);
+ if (!ASSERT_OK_FD(map_fd, "meta_map_restricted"))
+ goto out;
+ prog_fd = load_loader(f.gopts.insns, f.gopts.insns_sz, map_fd, sig,
+ sig_sz, BPF_KEYRING_BPF, 1);
+ close(map_fd);
+ map_fd = -1;
+ if (!ASSERT_OK_FD(prog_fd, "load signed by a key in the .bpf keyring"))
+ goto out;
+ close(prog_fd);
+ prog_fd = -1;
+
+ 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;
+
+ map_fd = setup_meta_map(&f);
+ if (!ASSERT_OK_FD(map_fd, "meta_map_unenrolled"))
+ goto out;
+ prog_fd = load_loader_log(f.gopts.insns, f.gopts.insns_sz, map_fd, bad,
+ bad_sz, BPF_KEYRING_BPF, 1, log_buf,
+ sizeof(log_buf));
+ close(map_fd);
+ map_fd = -1;
+ ASSERT_EQ(prog_fd, -ENOKEY, "key outside the bpf keyring refused");
+ ASSERT_HAS_SUBSTR(log_buf, "signature verification failed",
+ "the bpf keyring was consulted");
+ if (prog_fd >= 0)
+ close(prog_fd);
+ prog_fd = -1;
+
+ f.blob[0] ^= 0xff;
+ map_fd = setup_meta_map(&f);
+ f.blob[0] ^= 0xff;
+ if (!ASSERT_OK_FD(map_fd, "meta_map_tampered"))
+ goto out;
+ prog_fd = load_loader(f.gopts.insns, f.gopts.insns_sz, map_fd, sig,
+ sig_sz, BPF_KEYRING_BPF, 1);
+ close(map_fd);
+ map_fd = -1;
+ ASSERT_EQ(prog_fd, -EKEYREJECTED, "tampered metadata refused");
+ if (prog_fd >= 0)
+ close(prog_fd);
+ prog_fd = -1;
+
+ map_fd = setup_meta_map(&f);
+ if (!ASSERT_OK_FD(map_fd, "meta_map_session"))
+ goto out;
+ prog_fd = load_loader(f.gopts.insns, f.gopts.insns_sz, map_fd, sig,
+ sig_sz, KEY_SPEC_SESSION_KEYRING, 1);
+ close(map_fd);
+ map_fd = -1;
+ ASSERT_EQ(prog_fd, -EPERM, "caller-supplied keyring refused once .bpf is in use");
+ if (prog_fd >= 0)
+ close(prog_fd);
+ prog_fd = -1;
+
+ /*
+ * The restriction bounds what can be added and not what can be taken
+ * away, so the keyring is still writable here. Probe it with a key that
+ * is not a member: the permission check on the keyring is what is under
+ * test, and -ENOENT means it passed and only the removal itself did not
+ * find anything.
+ */
+ err = keyctl_ret(KEYCTL_UNLINK, KEY_SPEC_SESSION_KEYRING, serial);
+ ASSERT_EQ(err, -ENOENT, "keyring writable while the user bits are there");
+
+ /* Dropping the bits it no longer needs is what makes the set final. */
+ err = keyctl_ret(KEYCTL_SETPERM, serial, BPF_KEYRING_PERM_LOCKED);
+ if (!ASSERT_OK(err, "drop the user bits on the bpf keyring"))
+ goto out;
+
+ /* Verification runs on KEY_POS_SEARCH, so a load is unaffected. */
+ map_fd = setup_meta_map(&f);
+ if (!ASSERT_OK_FD(map_fd, "meta_map_locked"))
+ goto out;
+ prog_fd = load_loader(f.gopts.insns, f.gopts.insns_sz, map_fd, sig,
+ sig_sz, BPF_KEYRING_BPF, 1);
+ close(map_fd);
+ map_fd = -1;
+ ASSERT_OK_FD(prog_fd, "load still verified against the locked keyring");
+ if (prog_fd >= 0)
+ close(prog_fd);
+ prog_fd = -1;
+
+ 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, 0x082f0000);
+ ASSERT_EQ(err, -EACCES, "the bits cannot be granted back");
+
+ /* Nothing above got through: the keyring still holds its one key. */
+ ASSERT_EQ(bpf_keyring_lookup(&nr_keys), serial, "keyring still there");
+ ASSERT_EQ(nr_keys, 1, "the enrolled key survived");
+out:
+ if (prog_fd >= 0)
+ close(prog_fd);
+ if (map_fd >= 0)
+ close(map_fd);
+ 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
@@ -1899,6 +2244,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] 25+ messages in thread* Re: [PATCH bpf-next 10/11] selftests/bpf: Add tests for bpf keyring in signed loader
2026-08-21 21:41 ` [PATCH bpf-next 10/11] selftests/bpf: Add tests for bpf keyring in signed loader Daniel Borkmann
@ 2026-08-21 21:53 ` sashiko-bot
2026-08-21 22:50 ` bot+bpf-ci
1 sibling, 0 replies; 25+ messages in thread
From: sashiko-bot @ 2026-08-21 21:53 UTC (permalink / raw)
To: Daniel Borkmann; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] A ternary operator logic error causes a NULL pointer dereference if `strstr` returns `NULL` in `bpf_keyring_lookup`.
--
--- Patch [10]: [PATCH bpf-next 10/11] selftests/bpf: Add tests for bpf keyring in signed loader ---
commit c9f1ea97a7848e9a19ec91707712827e46792acd
Author: Daniel Borkmann <daniel@iogearbox.net>
selftests/bpf: Add tests for bpf keyring in signed loader
This commit adds selftests for the bpf keyring in the signed loader. It
verifies the keyring through its lifecycle, including key generation,
enrollment, and testing loading restrictions based on the keyring state.
> diff --git a/tools/testing/selftests/bpf/prog_tests/signed_loader.c b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
> --- a/tools/testing/selftests/bpf/prog_tests/signed_loader.c
> +++ b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
[ ... ]
> +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);
[Severity: Medium]
Could this ternary expression result in a NULL pointer dereference?
If strstr does not find ".bpf: ", sum will be NULL. In that case, the
ternary condition (sum && !strncmp(sum + 6, "empty", 5)) evaluates to
false. The execution will then fall through to the false branch,
calling atoi(sum + 6).
Since sum is NULL, this leads to a dereference of NULL + 6, which
would crash the selftest binary with a segmentation fault.
This situation could occur if the keyring description is parsed as
.bpf: by sscanf above, but the line does not contain the exact
substring ".bpf: " (with the trailing space).
> + }
> + break;
> + }
> + fclose(f);
> + return serial;
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260821214111.1120748-1-daniel@iogearbox.net?part=10
^ permalink raw reply [flat|nested] 25+ messages in thread* Re: [PATCH bpf-next 10/11] selftests/bpf: Add tests for bpf keyring in signed loader
2026-08-21 21:41 ` [PATCH bpf-next 10/11] selftests/bpf: Add tests for bpf keyring in signed loader Daniel Borkmann
2026-08-21 21:53 ` sashiko-bot
@ 2026-08-21 22:50 ` bot+bpf-ci
1 sibling, 0 replies; 25+ messages in thread
From: bot+bpf-ci @ 2026-08-21 22:50 UTC (permalink / raw)
To: daniel, memxor
Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf, ast,
andrii, daniel, martin.lau, eddyz87, yonghong.song, clm,
ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 5053 bytes --]
> diff --git a/tools/testing/selftests/bpf/prog_tests/signed_loader.c b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
> index a1fa1c37815bf..9d2384071a42d 100644
> --- a/tools/testing/selftests/bpf/prog_tests/signed_loader.c
> +++ b/tools/testing/selftests/bpf/prog_tests/signed_loader.c
> @@ -69,6 +69,33 @@ static int load_loader(const void *insns, __u32 insns_sz, int map_fd,
> return fd < 0 ? -errno : fd;
> }
>
> +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;
> +
> + memset(&attr, 0, sizeof(attr));
> + attr.prog_type = BPF_PROG_TYPE_SYSCALL;
> + attr.insns = ptr_to_u64(insns);
> + attr.insn_cnt = insns_sz / sizeof(struct bpf_insn);
> + attr.license = ptr_to_u64("Dual BSD/GPL");
> + attr.prog_flags = BPF_F_SLEEPABLE;
> + attr.fd_array = ptr_to_u64(&map_fd);
> + attr.fd_array_cnt = fd_array_cnt;
> + attr.signature = ptr_to_u64(sig);
> + attr.signature_size = sig_sz;
> + attr.keyring_id = keyring_id;
> + 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;
> +}
This isn't a bug, but could load_loader() just forward to load_loader_log()
with a NULL log buffer, the way sign_buf() forwards to sign_buf_digest()?
That would also let signature_failure_logs() drop its open-coded bpf_attr
block.
[ ... ]
> @@ -665,6 +770,246 @@ static void bpf_keyring_sealed(void)
> gen_loader_fixture_fini(&f);
> }
>
> +/*
> + * 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)
> +{
[ ... ]
> + 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;
> +
> + map_fd = setup_meta_map(&f);
> + if (!ASSERT_OK_FD(map_fd, "meta_map_unrestricted"))
> + goto out;
> + prog_fd = load_loader(f.gopts.insns, f.gopts.insns_sz, map_fd, sig,
> + sig_sz, BPF_KEYRING_BPF, 1);
> + close(map_fd);
> + map_fd = -1;
> + ASSERT_EQ(prog_fd, -ENOKEY, "unrestricted keyring still not consulted");
> + if (prog_fd >= 0)
> + close(prog_fd);
> + prog_fd = -1;
This isn't a bug, but would a small local helper - something like try_load()
returning the load result - let these seven blocks collapse to one line each
and keep the keyring lifecycle the subtest is actually documenting in view?
This eleven-line create-map / load / close / reset block is repeated verbatim
seven times inside the one function.
[ ... ]
> + /* Dropping the bits it no longer needs is what makes the set final. */
> + err = keyctl_ret(KEYCTL_SETPERM, serial, BPF_KEYRING_PERM_LOCKED);
> + if (!ASSERT_OK(err, "drop the user bits on the bpf keyring"))
> + goto out;
> +
> + /* Verification runs on KEY_POS_SEARCH, so a load is unaffected. */
> + map_fd = setup_meta_map(&f);
> + if (!ASSERT_OK_FD(map_fd, "meta_map_locked"))
> + goto out;
> + prog_fd = load_loader(f.gopts.insns, f.gopts.insns_sz, map_fd, sig,
> + sig_sz, BPF_KEYRING_BPF, 1);
> + close(map_fd);
> + map_fd = -1;
> + ASSERT_OK_FD(prog_fd, "load still verified against the locked keyring");
> + if (prog_fd >= 0)
> + close(prog_fd);
> + prog_fd = -1;
> +
> + 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, 0x082f0000);
> + ASSERT_EQ(err, -EACCES, "the bits cannot be granted back");
This isn't a bug, but since 0x082f0000 is the keyring's boot-time permission
set from bpf_keyring_init(), would a companion macro next to
BPF_KEYRING_PERM_LOCKED (say BPF_KEYRING_PERM_INITIAL) make the "the bits
cannot be granted back" assertion read for itself?
---
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/32530917987
^ permalink raw reply [flat|nested] 25+ messages in thread
* [PATCH bpf-next 11/11] Documentation/bpf: Document the bpf keyring and improve examples
2026-08-21 21:41 [PATCH bpf-next 00/11] BPF keyring and signed loader ML-DSA support Daniel Borkmann
` (9 preceding siblings ...)
2026-08-21 21:41 ` [PATCH bpf-next 10/11] selftests/bpf: Add tests for bpf keyring in signed loader Daniel Borkmann
@ 2026-08-21 21:41 ` Daniel Borkmann
2026-08-21 22:50 ` bot+bpf-ci
10 siblings, 1 reply; 25+ messages in thread
From: Daniel Borkmann @ 2026-08-21 21:41 UTC (permalink / raw)
To: memxor; +Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf
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 | 274 +++++++++++++++++++++++++++++-----
1 file changed, 237 insertions(+), 37 deletions(-)
diff --git a/Documentation/bpf/signing.rst b/Documentation/bpf/signing.rst
index e73eaaebd8b1..e35997746267 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,10 @@ auditable. (Illustrative - error checking elided.)
return ret;
}
+Such a policy is what makes a caller-supplied keyring usable at all before
+``bpf.keyring_unsealed=1`` is set: 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 +397,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 +407,152 @@ 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 taken as the serial of a
+caller-supplied user or session key or keyring:
+
+.. 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
+ * - ``VERIFY_USE_BPF_KEYRING`` (``3``)
+ - the bpf keyring
+ * - anything else
+ - serial of 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
+``VERIFY_USE_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
+``VERIFY_USE_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, so it is
+addressed by the serial ``/proc/keys`` reports. Steps would be as follows::
+
+ serial=$(awk '$8 == "keyring" && $9 == ".bpf:" { print strtonum("0x" $1) }' \
+ /proc/keys)
+
+ keyctl padd asymmetric "" $serial < signing_key.der
+ keyctl restrict_keyring $serial
+
+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 "" $serial < $key
+ done
+
+ keyctl restrict_keyring $serial
+ keyctl show $serial
+
+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 $serial 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`` and ``keyctl show``.
+
+Provisioning has to complete before control passes to the rootfs. The keyring
+is unrestricted for as long as it is unsealed, so the first writer wins: an
+initrd that hands off before restricting leaves that window open to whatever
+runs next.
+
+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``. It
+is therefore immutable from userspace, and 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 +572,47 @@ 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)
+ * - ``VERIFY_USE_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. 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 3.5 and earlier 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 +621,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 = 3; /* VERIFY_USE_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 +678,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 +693,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 ``VERIFY_USE_BPF_KEYRING`` never resolves.
--
2.43.0
^ permalink raw reply related [flat|nested] 25+ messages in thread* Re: [PATCH bpf-next 11/11] Documentation/bpf: Document the bpf keyring and improve examples
2026-08-21 21:41 ` [PATCH bpf-next 11/11] Documentation/bpf: Document the bpf keyring and improve examples Daniel Borkmann
@ 2026-08-21 22:50 ` bot+bpf-ci
0 siblings, 0 replies; 25+ messages in thread
From: bot+bpf-ci @ 2026-08-21 22:50 UTC (permalink / raw)
To: daniel, memxor
Cc: brauner, kpsingh, ast, john.fastabend, a.s.protopopov, bpf, ast,
andrii, daniel, martin.lau, eddyz87, yonghong.song, clm,
ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 12124 bytes --]
> diff --git a/Documentation/bpf/signing.rst b/Documentation/bpf/signing.rst
> index e73eaaebd8b15..e359977462673 100644
> --- a/Documentation/bpf/signing.rst
> +++ b/Documentation/bpf/signing.rst
[ ... ]
> @@ -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 */
> + }
The illustrative BPF program uses BPF_SIG_KEYRING_BUILTIN,
BPF_SIG_KEYRING_BPF, BPF_SIG_KEYRING_USER and (earlier) BPF_SIG_VERIFIED
as compile-time constants, which requires enum bpf_sig_keyring / enum
bpf_sig_verdict to be present in vmlinux BTF (i.e., in the generated
vmlinux.h). A subsystem pattern flags this as potentially concerning:
Nothing in the kernel references those enum types by name: the aux fields
are plain integers (struct { s32 keyring_serial; u8 keyring_type; u8
verdict; } sig; in include/linux/bpf.h), the only user of the enum type is
the file-static bpf_classify_keyring() in kernel/bpf/verifier.c, and
there is no BTF_TYPE_EMIT() for them - contrast BTF_TYPE_EMIT(enum
bpf_features) in bpf_check(), which exists precisely because an
otherwise-unreferenced enum does not reach BTF. If the enum is not
emitted, these case labels do not compile.
Notably the series' own BPF-side test program does not use the
enumerators either: progs/test_signed_loader_lsm.c reads the raw fields
into ints and the comparison happens in userspace against a hand-copied
enum (prog_tests/signed_loader.c). A static function's return type may
still pull the enum into vmlinux BTF depending on the pahole encoder, so
perhaps this works in practice - but it might be worth checking whether
the example compiles on a system where those enums do not appear in
vmlinux.h.
>
> +Such a policy is what makes a caller-supplied keyring usable at all before
> +``bpf.keyring_unsealed=1`` is set: the allowlist bounds which staged keys
> +count, and the LSM itself has to protect them from being tampered with.
Can the phrasing "before bpf.keyring_unsealed=1 is set" be read as
describing a temporal transition? bpf.keyring_unsealed is a boot
parameter backed by __ro_after_init storage exposed 0444 (kernel/bpf/
keys.c), and bpf_keyring_enforced() reads it directly, so within a boot
it is either always 1 or always 0 - there is no "before it is set". The
same commit adds, roughly 190 lines later, "there is no window early in
boot during which a caller-supplied keyring is still accepted", which
could be read as contradicting the phrasing here. The intended meaning is
presumably "on a system that does not boot with bpf.keyring_unsealed=1"
(the phrasing used correctly in the 'Caller-supplied keyrings are for
staging' section: "A system that has committed to the bpf keyring refuses
this path outright").
[ ... ]
> @@ -390,33 +407,152 @@ Keyrings
> +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
> +``VERIFY_USE_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
> +``VERIFY_USE_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, so it is
> +addressed by the serial ``/proc/keys`` reports. Steps would be as follows::
> +
> + serial=$(awk '$8 == "keyring" && $9 == ".bpf:" { print strtonum("0x" $1) }' \
> + /proc/keys)
Does strtonum() work in the initrd environment this snippet targets?
strtonum() is a GNU awk extension. The surrounding text explicitly places
these commands in an initrd ("an initrd runs the keyctl steps below
before handing off to the rootfs", "as a third step in the initrd"),
where the awk implementation is typically busybox awk or mawk, neither of
which provides strtonum(); the snippet would fail there with an
unknown-function error and leave serial empty, after which keyctl padd ...
"" silently targets nothing useful. The field selection itself is correct
(security/keys/proc.c prints serial, flags, usage, timeout, perm, uid,
gid, type as fields 1-8, and keyring_describe() emits ".bpf: <n>" /
".bpf: empty", so $8/$9 match - the same layout the selftest parses in
prog_tests/signed_loader.c). Only the hex conversion is non-portable; the
raw $1 is already a form keyctl accepts.
[ ... ]
> +Provisioning has to complete before control passes to the rootfs. The keyring
> +is unrestricted for as long as it is unsealed, so the first writer wins: an
> +initrd that hands off before restricting leaves that window open to whatever
> +runs next.
Does "the first writer wins" state the opposite of the implemented
semantics? Until keyctl restrict_keyring runs, bpf_keyring->restrict_link
is NULL (kernel/bpf/keys.c only restricts when !bpf_keyring_unsealed), so
the keyring accepts an unbounded number of further keyctl padd links, and
every key added before the restriction becomes part of the trust anchor.
It is the first party to *restrict* the keyring - not the first party to
write to it - that closes the window; the series' own selftest relies on
exactly that ordering (prog_tests/signed_loader.c enrolls, asserts
-ENOKEY while unrestricted, then calls KEYCTL_RESTRICT_KEYRING). An
operator reading "the first writer wins" could conclude that enrolling
their key early is itself protective and that the keyctl restrict_keyring
step is only about activation, when it is the only thing that bounds the
enrolled set.
> +
> +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``. It
> +is therefore immutable from userspace, and there is no window early in boot
> +during which a caller-supplied keyring is still accepted.
Does "readable at <sysfs path>. It is therefore immutable from userspace"
establish the right causal link? Being readable in sysfs implies nothing
about mutability. The two independent reasons the value cannot change are
(a) static bool bpf_keyring_unsealed __ro_after_init; with
module_param_named(keyring_unsealed, ..., 0444) (kernel/bpf/keys.c), i.e.
the attribute is mode 0444 and the variable is write-protected after
init, and (b) bpf_keyring_enforced() returns the boot flag rather than
the keyring's runtime state - which is precisely how the implementing
commit 393136a14e636 phrases it ("Deriving this from the boot flag rather
than from the keyring's runtime state keeps the decision immutable from
userspace"). As written the sentence attributes immutability to the wrong
property, and a reader could infer that a writable sysfs attribute would
be the mechanism for toggling enforcement.
[ ... ]
> @@ -436,16 +572,47 @@ Two fields are recorded in ``prog->aux->sig`` for an LSM to inspect:
>
> -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. 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
Both new key-generation examples (RSA and ML-DSA-87) pass '-config
x509.genkey', but signing.rst never says what x509.genkey is, never shows
its contents, and never points at a place that does. A reader copying
either command verbatim gets 'Can't open x509.genkey for reading' from
openssl. The only in-tree definitions are certs/x509.genkey and
Documentation/admin-guide/module-signing.rst (which spells out the [req]/
[req_distinguished_name]/[myexts] content); the selftest the examples
were lifted from (tools/testing/selftests/bpf/verify_sig_setup.sh,
x509_genkey_content) writes the file itself before invoking openssl. A
one-line pointer to certs/x509.genkey or Documentation/admin-guide/
module-signing.rst would make the examples runnable.
> +
> +``bpftool`` handles the following internally: openssl 3.5 and earlier 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.
Does the version boundary match what bpftool actually does?
tools/bpf/bpftool/sign.c guards the fallback with '#if
OPENSSL_VERSION_NUMBER >= 0x30000000L && OPENSSL_VERSION_NUMBER <
0x40000000L', i.e. it drops CMS_NOATTR and switches cms_digest to
EVP_sha512() on every OpenSSL 3.x, not only on 3.5 and earlier.
crypto/asymmetric_keys/Kconfig states the constraint as 'OpenSSL < 4.0
(and thus any released version)'. As written the doc implies a user on
OpenSSL 3.6+ gets the CMS_NOATTR path, which they do not. (The
imprecision is inherited from the in-code comment in sign.c/
scripts/sign-file.c, so a fix probably wants to touch both;
behaviourally this is invisible to users since bpftool handles it, hence
not urgent.)
---
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/32530917987
^ permalink raw reply [flat|nested] 25+ messages in thread