[PATCH bpf] bpf: array: reject max_entries > INT_MAX to prevent signed-iterator overflow

meishaoming posted 1 patch 1 month, 1 week ago
There is a newer version of this series
kernel/bpf/arraymap.c | 3 +++
1 file changed, 3 insertions(+)
[PATCH bpf] bpf: array: reject max_entries > INT_MAX to prevent signed-iterator overflow
Posted by meishaoming 1 month, 1 week ago
BPF_MAP_TYPE_PROG_ARRAY (and other array maps) take max_entries as a u32
from userspace via bpf(BPF_MAP_CREATE), but array_map_alloc_check() only
guards value_size > INT_MAX and leaves max_entries unchecked.

When the map is later freed, fd_array_map_free() iterates the ptrs[] array
with a signed int loop variable:

    for (i = 0; i < array->map.max_entries; i++)
        BUG_ON(array->ptrs[i] != NULL);

On arm64 (and any LP64 arch) int is 32-bit signed, while max_entries is
u32. With max_entries having bit 31 set (e.g. 0xFF00000A, observed from a
syzkaller run), the loop runs past i = 0x7FFFFFFF: i++ wraps to 0x80000000,
which as a signed int is negative. The address computation for
&array->ptrs[i] sign-extends the 32-bit index (sxtw on arm64) into a
64-bit negative offset:

    array + sxtw(0x80000000) * 8 + offsetof(bpf_array, ptrs)
  = array + 0xFFFFFFFC00000000 + 0x108
  -> wraps to an unmapped address, level-1 translation fault, panic

This is reachable today because the existing overflow backstop in
array_map_alloc() lives inside the `if (!bypass_spec_v1)` block. Since
commit 2c78ee898d8f ("bpf: Implement CAP_BPF") renamed the old
`if (unpriv)` guard to `if (!bypass_spec_v1)`, the semantics inverted:
a root caller with CAP_PERFMON (or mitigations=off) takes the bypass
path and skips the -E2BIG check entirely, so the raw attr->max_entries
is stored into map->max_entries unchanged and the ~32 GB vmalloc
(overcommit) region is handed out. The earlier unprivileged-path fix
for max_entries overflow was carried along into the bypass block, so
it does not cover the root path described here.

Reject max_entries > INT_MAX in array_map_alloc_check(), the common entry
point shared by all callers including the bypass path. This mirrors the
existing value_size > INT_MAX guard a few lines above. The cut half of the
u32 range (0x80000000..0xFFFFFFFF) cannot back a usable map today:
  - a prog_array at the lower bound already needs ~16 GB of contiguous
    virtual memory (and ~32 GB for the 0xFF00000A value seen in the field),
    which either fails allocation or drives the system into OOM/softlockup
    long before becoming a working map;
  - legitimate prog_array usage is bounded by MAX_TAIL_CALL_CNT = 33 and
    typical array maps are orders of magnitude below INT_MAX.

No valid BPF use case is affected; the change converts a deferred
whole-system BUG_ON panic (or OOM/softlockup) into an accurate -E2BIG at
creation time.

Fixes: 2c78ee898d8f ("bpf: Implement CAP_BPF")
Signed-off-by: meishaoming <meishaoming@xiaomi.com>
---
 kernel/bpf/arraymap.c | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/kernel/bpf/arraymap.c b/kernel/bpf/arraymap.c
index 248b4818178c..868bc9f66cdb 100644
--- a/kernel/bpf/arraymap.c
+++ b/kernel/bpf/arraymap.c
@@ -74,6 +74,9 @@ int array_map_alloc_check(union bpf_attr *attr)
        /* avoid overflow on round_up(map->value_size) */
        if (attr->value_size > INT_MAX)
                return -E2BIG;
+       /* avoid signed int iterator overflow in fd_array_map_free() */
+       if (attr->max_entries > INT_MAX)
+               return -E2BIG;
        /* percpu map value size is bound by PCPU_MIN_UNIT_SIZE */
        if (percpu && round_up(attr->value_size, 8) > PCPU_MIN_UNIT_SIZE)
                return -E2BIG;
--
2.50.1 (Apple Git-155)

#/******���ʼ����丽������С�׹�˾�ı�����Ϣ�������ڷ��͸������ַ���г��ĸ��˻�Ⱥ�顣��ֹ�κ����������κ���ʽʹ�ã�������������ȫ���򲿷ֵ�й¶�����ơ���ɢ�������ʼ��е���Ϣ������������˱��ʼ������������绰���ʼ�֪ͨ�����˲�ɾ�����ʼ��� This e-mail and its attachments contain confidential information from XIAOMI, which is intended only for the person or entity whose address is listed above. Any use of the information contained herein in any way (including, but not limited to, total or partial disclosure, reproduction, or dissemination) by persons other than the intended recipient(s) is prohibited. If you receive this e-mail in error, please notify the sender by phone or email immediately and delete it!******/#
[PATCH v2 bpf] bpf: array: reject max_entries > INT_MAX to prevent signed-iterator overflow
Posted by meishaoming 1 month, 1 week ago
BPF_MAP_TYPE_PROG_ARRAY (and other array maps) take max_entries as a u32
from userspace via bpf(BPF_MAP_CREATE), but array_map_alloc_check() only
guards value_size > INT_MAX and leaves max_entries unchecked.

When the map is later freed, fd_array_map_free() iterates the ptrs[] array
with a signed int loop variable:

    for (i = 0; i < array->map.max_entries; i++)
        BUG_ON(array->ptrs[i] != NULL);

On arm64 (and any LP64 arch) int is 32-bit signed, while max_entries is
u32. With max_entries having bit 31 set (e.g. 0xFF00000A, observed from a
syzkaller run), the loop runs past i = 0x7FFFFFFF: i++ wraps to 0x80000000,
which as a signed int is negative. The address computation for
&array->ptrs[i] sign-extends the 32-bit index (sxtw on arm64) into a
64-bit negative offset:

    array + sxtw(0x80000000) * 8 + offsetof(bpf_array, ptrs)
  = array + 0xFFFFFFFC00000000 + 0x108
  -> wraps to an unmapped address, level-1 translation fault, panic

This is reachable today because the existing overflow backstop in
array_map_alloc() lives inside the `if (!bypass_spec_v1)` block. Since
commit 2c78ee898d8f ("bpf: Implement CAP_BPF") renamed the old
`if (unpriv)` guard to `if (!bypass_spec_v1)`, the semantics inverted:
a root caller with CAP_PERFMON (or mitigations=off) takes the bypass
path and skips the -E2BIG check entirely, so the raw attr->max_entries
is stored into map->max_entries unchanged and the ~32 GB vmalloc
(overcommit) region is handed out. The earlier unprivileged-path fix
for max_entries overflow was carried along into the bypass block, so
it does not cover the root path described here.

Reject max_entries > INT_MAX in array_map_alloc_check(), the common entry
point shared by all callers including the bypass path. This mirrors the
existing value_size > INT_MAX guard a few lines above. The cut half of the
u32 range (0x80000000..0xFFFFFFFF) cannot back a usable map today:
  - a prog_array at the lower bound already needs ~16 GB of contiguous
    virtual memory (and ~32 GB for the 0xFF00000A value seen in the field),
    which either fails allocation or drives the system into OOM/softlockup
    long before becoming a working map;
  - legitimate prog_array usage is bounded by MAX_TAIL_CALL_CNT = 33 and
    typical array maps are orders of magnitude below INT_MAX.

No valid BPF use case is affected; the change converts a deferred
whole-system BUG_ON panic (or OOM/softlockup) into an accurate -E2BIG at
creation time.

Fixes: 2c78ee898d8f ("bpf: Implement CAP_BPF")
Signed-off-by: meishaoming <meishaoming@xiaomi.com>
---
Changes in v2:
  - Rebase onto the bpf tree (base a13307e97d5c) so Patchwork CI can
    apply it cleanly; no code change.
---
 kernel/bpf/arraymap.c | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/kernel/bpf/arraymap.c b/kernel/bpf/arraymap.c
index 248b4818178c..868bc9f66cdb 100644
--- a/kernel/bpf/arraymap.c
+++ b/kernel/bpf/arraymap.c
@@ -74,6 +74,9 @@ int array_map_alloc_check(union bpf_attr *attr)
        /* avoid overflow on round_up(map->value_size) */
        if (attr->value_size > INT_MAX)
                return -E2BIG;
+       /* avoid signed int iterator overflow in fd_array_map_free() */
+       if (attr->max_entries > INT_MAX)
+               return -E2BIG;
        /* percpu map value size is bound by PCPU_MIN_UNIT_SIZE */
        if (percpu && round_up(attr->value_size, 8) > PCPU_MIN_UNIT_SIZE)
                return -E2BIG;

base-commit: a13307e97d5c54b65720bb71fa379960ded1e51a
--
2.50.1 (Apple Git-155)

#/******���ʼ����丽������С�׹�˾�ı�����Ϣ�������ڷ��͸������ַ���г��ĸ��˻�Ⱥ�顣��ֹ�κ����������κ���ʽʹ�ã�������������ȫ���򲿷ֵ�й¶�����ơ���ɢ�������ʼ��е���Ϣ������������˱��ʼ������������绰���ʼ�֪ͨ�����˲�ɾ�����ʼ��� This e-mail and its attachments contain confidential information from XIAOMI, which is intended only for the person or entity whose address is listed above. Any use of the information contained herein in any way (including, but not limited to, total or partial disclosure, reproduction, or dissemination) by persons other than the intended recipient(s) is prohibited. If you receive this e-mail in error, please notify the sender by phone or email immediately and delete it!******/#
Re: [PATCH v2 bpf] bpf: array: reject max_entries > INT_MAX to prevent signed-iterator overflow
Posted by Andrii Nakryiko 1 month, 1 week ago
On Thu, Aug 20, 2026 at 2:22 AM meishaoming <meishaoming@xiaomi.com> wrote:
>
> BPF_MAP_TYPE_PROG_ARRAY (and other array maps) take max_entries as a u32
> from userspace via bpf(BPF_MAP_CREATE), but array_map_alloc_check() only
> guards value_size > INT_MAX and leaves max_entries unchecked.
>
> When the map is later freed, fd_array_map_free() iterates the ptrs[] array
> with a signed int loop variable:
>
>     for (i = 0; i < array->map.max_entries; i++)
>         BUG_ON(array->ptrs[i] != NULL);
>
> On arm64 (and any LP64 arch) int is 32-bit signed, while max_entries is
> u32. With max_entries having bit 31 set (e.g. 0xFF00000A, observed from a
> syzkaller run), the loop runs past i = 0x7FFFFFFF: i++ wraps to 0x80000000,
> which as a signed int is negative. The address computation for
> &array->ptrs[i] sign-extends the 32-bit index (sxtw on arm64) into a
> 64-bit negative offset:
>
>     array + sxtw(0x80000000) * 8 + offsetof(bpf_array, ptrs)
>   = array + 0xFFFFFFFC00000000 + 0x108
>   -> wraps to an unmapped address, level-1 translation fault, panic
>
> This is reachable today because the existing overflow backstop in
> array_map_alloc() lives inside the `if (!bypass_spec_v1)` block. Since
> commit 2c78ee898d8f ("bpf: Implement CAP_BPF") renamed the old
> `if (unpriv)` guard to `if (!bypass_spec_v1)`, the semantics inverted:
> a root caller with CAP_PERFMON (or mitigations=off) takes the bypass
> path and skips the -E2BIG check entirely, so the raw attr->max_entries
> is stored into map->max_entries unchanged and the ~32 GB vmalloc
> (overcommit) region is handed out. The earlier unprivileged-path fix
> for max_entries overflow was carried along into the bypass block, so
> it does not cover the root path described here.
>
> Reject max_entries > INT_MAX in array_map_alloc_check(), the common entry
> point shared by all callers including the bypass path. This mirrors the
> existing value_size > INT_MAX guard a few lines above. The cut half of the
> u32 range (0x80000000..0xFFFFFFFF) cannot back a usable map today:
>   - a prog_array at the lower bound already needs ~16 GB of contiguous
>     virtual memory (and ~32 GB for the 0xFF00000A value seen in the field),
>     which either fails allocation or drives the system into OOM/softlockup
>     long before becoming a working map;
>   - legitimate prog_array usage is bounded by MAX_TAIL_CALL_CNT = 33 and
>     typical array maps are orders of magnitude below INT_MAX.
>
> No valid BPF use case is affected; the change converts a deferred
> whole-system BUG_ON panic (or OOM/softlockup) into an accurate -E2BIG at
> creation time.
>
> Fixes: 2c78ee898d8f ("bpf: Implement CAP_BPF")
> Signed-off-by: meishaoming <meishaoming@xiaomi.com>
> ---
> Changes in v2:
>   - Rebase onto the bpf tree (base a13307e97d5c) so Patchwork CI can
>     apply it cleanly; no code change.
> ---
>  kernel/bpf/arraymap.c | 3 +++
>  1 file changed, 3 insertions(+)
>
> diff --git a/kernel/bpf/arraymap.c b/kernel/bpf/arraymap.c
> index 248b4818178c..868bc9f66cdb 100644
> --- a/kernel/bpf/arraymap.c
> +++ b/kernel/bpf/arraymap.c
> @@ -74,6 +74,9 @@ int array_map_alloc_check(union bpf_attr *attr)
>         /* avoid overflow on round_up(map->value_size) */
>         if (attr->value_size > INT_MAX)
>                 return -E2BIG;
> +       /* avoid signed int iterator overflow in fd_array_map_free() */
> +       if (attr->max_entries > INT_MAX)
> +               return -E2BIG;

While uncommon, I'm not sure there is anything wrong with allocating
3000'000'000 single-byte entries, if necessary. So if there is enough
memory, why would we artificially limit the allocatable capacity?

value_size guard is more sensible, IMO (and there is a specific
practical reason because of round_up).

should all the places where we iterate over all elements be fixed to
use u32 instead?


>         /* percpu map value size is bound by PCPU_MIN_UNIT_SIZE */
>         if (percpu && round_up(attr->value_size, 8) > PCPU_MIN_UNIT_SIZE)
>                 return -E2BIG;
>
> base-commit: a13307e97d5c54b65720bb71fa379960ded1e51a
> --
> 2.50.1 (Apple Git-155)
>
> #/******本邮件及其附件含有小米公司的保密信息,仅限于发送给上面地址中列出的个人或群组。禁止任何其他人以任何形式使用(包括但不限于全部或部分地泄露、复制、或散发)本邮件中的信息。如果您错收了本邮件,请您立即电话或邮件通知发件人并删除本邮件! This e-mail and its attachments contain confidential information from XIAOMI, which is intended only for the person or entity whose address is listed above. Any use of the information contained herein in any way (including, but not limited to, total or partial disclosure, reproduction, or dissemination) by persons other than the intended recipient(s) is prohibited. If you receive this e-mail in error, please notify the sender by phone or email immediately and delete it!******/#
[PATCH v3 bpf] bpf: array: use u32 iterator for max_entries in alloc/free paths
Posted by meishaoming 1 month ago
BPF_MAP_TYPE_PROG_ARRAY (and other array maps) take max_entries as a u32
from userspace via bpf(BPF_MAP_CREATE). Several array map alloc/free
paths iterate max_entries with a signed int loop variable:

    for (i = 0; i < array->map.max_entries; i++)

On arm64 (and any LP64 arch) int is 32-bit signed, while max_entries is
u32. With max_entries having bit 31 set (e.g. 0xFF00000A, observed from a
syzkaller run), the loop runs past i = 0x7FFFFFFF: i++ wraps to 0x80000000,
which as a signed int is negative. The address computation for
&array->ptrs[i] sign-extends the 32-bit index (sxtw on arm64) into a
64-bit negative offset:

    array + sxtw(0x80000000) * 8 + offsetof(bpf_array, ptrs)
  = array + 0xFFFFFFFC00000000 + 0x108
  -> wraps to an unmapped address, level-1 translation fault, panic

This is reachable today because the existing overflow backstop in
array_map_alloc() lives inside the `if (!bypass_spec_v1)` block. Since
commit 2c78ee898d8f ("bpf: Implement CAP_BPF") renamed the old
`if (unpriv)` guard to `if (!bypass_spec_v1)`, the semantics inverted:
a root caller with CAP_PERFMON (or mitigations=off) takes the bypass
path and skips the -E2BIG check entirely, so the raw attr->max_entries
is stored into map->max_entries unchanged.

Rather than rejecting max_entries > INT_MAX at creation time — which
would artificially cut half the legitimate u32 range (a 3G-entry
single-byte map is a valid allocation when memory allows) — change the
iterators to u32 i so the index zero-extends into the 64-bit address,
landing inside the allocated map region instead of wrapping negative.
This keeps the full u32 capacity usable and matches the type of
max_entries itself.

Changed all int i iterators over max_entries in arraymap.c to u32:
  - bpf_array_free_percpu      (pptrs[i])
  - bpf_array_alloc_percpu     (pptrs[i])
  - array_map_free_internal_structs
  - array_map_free             (pptrs[i & index_mask])
  - fd_array_map_free          (ptrs[i], the observed crash site)
  - bpf_fd_array_map_clear     (__fd_array_map_delete_elem)
  - perf_event_fd_array_release (ptrs[i])

Fixes: 2c78ee898d8f ("bpf: Implement CAP_BPF")
Signed-off-by: meishaoming <meishaoming@xiaomi.com>
---
Changes in v3:
  - Drop the max_entries > INT_MAX rejection added in v2 (per review,
    it would artificially limit legitimate allocatable capacity when
    memory is available).
  - Switch all int i iterators over max_entries in arraymap.c to u32 i
    so the index zero-extends instead of sign-extending; this keeps the
    full u32 range usable and fixes every alloc/free path at once.
---
 kernel/bpf/arraymap.c | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/kernel/bpf/arraymap.c b/kernel/bpf/arraymap.c
index 248b4818178c..21e08d7fc99e 100644
--- a/kernel/bpf/arraymap.c
+++ b/kernel/bpf/arraymap.c
@@ -22,7 +22,7 @@

 static void bpf_array_free_percpu(struct bpf_array *array)
 {
-       int i;
+       u32 i;

        for (i = 0; i < array->map.max_entries; i++) {
                free_percpu(array->pptrs[i]);
@@ -33,7 +33,7 @@ static void bpf_array_free_percpu(struct bpf_array *array)
 static int bpf_array_alloc_percpu(struct bpf_array *array)
 {
        void __percpu *ptr;
-       int i;
+       u32 i;

        for (i = 0; i < array->map.max_entries; i++) {
                ptr = bpf_map_alloc_percpu(&array->map, array->elem_size, 8,
@@ -460,7 +460,7 @@ static void *array_map_vmalloc_addr(struct bpf_array *array)
 static void array_map_free_internal_structs(struct bpf_map *map)
 {
        struct bpf_array *array = container_of(map, struct bpf_array, map);
-       int i;
+       u32 i;

        /* We only free internal structs on uref dropping to zero */
        if (!bpf_map_has_internal_structs(map))
@@ -474,7 +474,7 @@ static void array_map_free_internal_structs(struct bpf_map *map)
 static void array_map_free(struct bpf_map *map)
 {
        struct bpf_array *array = container_of(map, struct bpf_array, map);
-       int i;
+       u32 i;

        if (!IS_ERR_OR_NULL(map->record)) {
                if (array->map.map_type == BPF_MAP_TYPE_PERCPU_ARRAY) {
@@ -860,7 +860,7 @@ static int fd_array_map_alloc_check(union bpf_attr *attr)
 static void fd_array_map_free(struct bpf_map *map)
 {
        struct bpf_array *array = container_of(map, struct bpf_array, map);
-       int i;
+       u32 i;

        /* make sure it's empty */
        for (i = 0; i < array->map.max_entries; i++)
@@ -1011,7 +1011,7 @@ static u32 prog_fd_array_sys_lookup_elem(void *ptr)
 static void bpf_fd_array_map_clear(struct bpf_map *map, bool need_defer)
 {
        struct bpf_array *array = container_of(map, struct bpf_array, map);
-       int i;
+       u32 i;

        for (i = 0; i < array->map.max_entries; i++) {
                __fd_array_map_delete_elem(map, &i, need_defer);
@@ -1298,7 +1298,7 @@ static void perf_event_fd_array_release(struct bpf_map *map,
 {
        struct bpf_array *array = container_of(map, struct bpf_array, map);
        struct bpf_event_entry *ee;
-       int i;
+       u32 i;

        if (map->map_flags & BPF_F_PRESERVE_ELEMS)
                return;

base-commit: a13307e97d5c54b65720bb71fa379960ded1e51a
--
2.50.1 (Apple Git-155)

#/******本邮件及其附件含有小米公司的保密信息,仅限于发送给上面地址中列出的个人或群组。禁止任何其他人以任何形式使用(包括但不限于全部或部分地泄露、复制、或散发)本邮件中的信息。如果您错收了本邮件,请您立即电话或邮件通知发件人并删除本邮件! This e-mail and its attachments contain confidential information from XIAOMI, which is intended only for the person or entity whose address is listed above. Any use of the information contained herein in any way (including, but not limited to, total or partial disclosure, reproduction, or dissemination) by persons other than the intended recipient(s) is prohibited. If you receive this e-mail in error, please notify the sender by phone or email immediately and delete it!******/#