bpf: representation and basic operations on circular numbers

This commit adds basic definitions for cnum32/cnum64.
This is a unified numeric range representation for signed and unsigned
domains. Inspired by an old post from Shung-Hsi Yu [1] and paper [2].
Operations correctness is verified using cbmc model checker,
tests source code can be found in a separate repo [3].

The cnum64_cnum32_intersect() function is notable, because it handled
several cases verifier.c:deduce_bounds_64_from_32() does not.
Given:
- a is a 64-bit range
- b is a 32-bit range
- t is a refined 64-bit range, such that ∀ v ∈ a, (u32)v ∈ b: v ∈ t.
cnum64_cnum32_intersect() makes the following deductions:

(A): 'b' is a sub-range of the first or the last 32-bit
     sub-range of 'a':

                                                         64-bit number axis --->

 N*2^32                   (N+1)*2^32                (N+2)*2^32                (N+3)*2^32
 ||------|---|=====|-------||----------|=====|-------||----------|=====|----|--||
         |   |< b >|                   |< b >|                   |< b >|    |
         |   |                                                         |    |
         |<--+--------------------------- a ---------------------------+--->|
             |                                                         |
             |<-------------------------- t -------------------------->|

(B) 'b' does not intersect with the first of the last 32-bit
    sub-range of 'a':

N*2^32                   (N+1)*2^32                (N+2)*2^32                (N+3)*2^32
||--|=====|----|----------||--|=====|---------------||--|=====|------------|--||
    |< b >|    |              |< b >|                   |< b >|            |
               |              |                               |            |
               |<-------------+--------- a -------------------|----------->|
                              |                               |
                              |<-------- t ------------------>|

(C) 'b' crosses 0/U32_MAX boundary:

N*2^32                   (N+1)*2^32                (N+2)*2^32                (N+3)*2^32
||===|---------|------|===||===|----------------|===||===|---------|------|===||
 |b >|         |      |< b||b >|                |< b||b >|         |      |< b|
               |      |                                  |         |
               |<-----+----------------- a --------------+-------->|
                      |                                  |
                      |<---------------- t ------------->|

Current implementation of deduce_bounds_64_from_32() only handles
case (A).

[1] https://lore.kernel.org/all/ZTZxoDJJbX9mrQ9w@u94a/
[2] https://jorgenavas.github.io/papers/ACM-TOPLAS-wrapped.pdf
[3] https://github.com/eddyz87/cnum-verif/tree/master

Signed-off-by: Eduard Zingerman <eddyz87@gmail.com>
Link: https://lore.kernel.org/r/20260424-cnums-everywhere-rfc-v1-v3-1-ca434b39a486@gmail.com
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
This commit is contained in:
Eduard Zingerman 2026-04-24 15:52:42 -07:00 committed by Alexei Starovoitov
parent 789b7c1c64
commit 256f0071f9
4 changed files with 431 additions and 1 deletions

80
include/linux/cnum.h Normal file
View File

@ -0,0 +1,80 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
#ifndef _LINUX_CNUM_H
#define _LINUX_CNUM_H
#include <linux/types.h>
/*
* cnum32: a circular number.
* A unified representation for signed and unsigned ranges.
*
* Assume that a 32-bit range is a circle, with 0 being in the 12 o'clock
* position, numbers placed sequentially in clockwise order and U32_MAX
* in the 11 o'clock position. Signed values map onto the same circle:
* S32_MAX sits at 5 o'clock, S32_MIN sits at 6 o'clock (opposite 0),
* negative values occupy the left half and positive values the right half.
*
* @cnum32 represents an arc on this circle drawn clockwise.
* @base corresponds to the first value of the range.
* @size corresponds to the number of integers in the range excluding @base.
* (The @base is excluded to avoid integer overflow when representing the full
* 0..U32_MAX range, which corresponds to 2^32, which can't be stored in u32).
*
* For example: {U32_MAX, 1} corresponds to signed range [-1, 0],
* {S32_MAX, 1} corresponds to unsigned range [S32_MAX, S32_MIN].
*/
struct cnum32 {
u32 base;
u32 size;
};
#define CNUM32_UNBOUNDED ((struct cnum32){ .base = 0, .size = U32_MAX })
#define CNUM32_EMPTY ((struct cnum32){ .base = U32_MAX, .size = U32_MAX })
struct cnum32 cnum32_from_urange(u32 min, u32 max);
struct cnum32 cnum32_from_srange(s32 min, s32 max);
u32 cnum32_umin(struct cnum32 cnum);
u32 cnum32_umax(struct cnum32 cnum);
s32 cnum32_smin(struct cnum32 cnum);
s32 cnum32_smax(struct cnum32 cnum);
struct cnum32 cnum32_intersect(struct cnum32 a, struct cnum32 b);
void cnum32_intersect_with(struct cnum32 *dst, struct cnum32 src);
void cnum32_intersect_with_urange(struct cnum32 *dst, u32 min, u32 max);
void cnum32_intersect_with_srange(struct cnum32 *dst, s32 min, s32 max);
bool cnum32_contains(struct cnum32 cnum, u32 v);
bool cnum32_is_const(struct cnum32 cnum);
bool cnum32_is_empty(struct cnum32 cnum);
struct cnum32 cnum32_add(struct cnum32 a, struct cnum32 b);
struct cnum32 cnum32_negate(struct cnum32 a);
/* Same as cnum32 but for 64-bit ranges */
struct cnum64 {
u64 base;
u64 size;
};
#define CNUM64_UNBOUNDED ((struct cnum64){ .base = 0, .size = U64_MAX })
#define CNUM64_EMPTY ((struct cnum64){ .base = U64_MAX, .size = U64_MAX })
struct cnum64 cnum64_from_urange(u64 min, u64 max);
struct cnum64 cnum64_from_srange(s64 min, s64 max);
u64 cnum64_umin(struct cnum64 cnum);
u64 cnum64_umax(struct cnum64 cnum);
s64 cnum64_smin(struct cnum64 cnum);
s64 cnum64_smax(struct cnum64 cnum);
struct cnum64 cnum64_intersect(struct cnum64 a, struct cnum64 b);
void cnum64_intersect_with(struct cnum64 *dst, struct cnum64 src);
void cnum64_intersect_with_urange(struct cnum64 *dst, u64 min, u64 max);
void cnum64_intersect_with_srange(struct cnum64 *dst, s64 min, s64 max);
bool cnum64_contains(struct cnum64 cnum, u64 v);
bool cnum64_is_const(struct cnum64 cnum);
bool cnum64_is_empty(struct cnum64 cnum);
struct cnum64 cnum64_add(struct cnum64 a, struct cnum64 b);
struct cnum64 cnum64_negate(struct cnum64 a);
struct cnum32 cnum32_from_cnum64(struct cnum64 cnum);
struct cnum64 cnum64_cnum32_intersect(struct cnum64 a, struct cnum32 b);
#endif /* _LINUX_CNUM_H */

View File

@ -6,7 +6,7 @@ cflags-nogcse-$(CONFIG_X86)$(CONFIG_CC_IS_GCC) := -fno-gcse
endif
CFLAGS_core.o += -Wno-override-init $(cflags-nogcse-yy)
obj-$(CONFIG_BPF_SYSCALL) += syscall.o verifier.o inode.o helpers.o tnum.o log.o token.o liveness.o const_fold.o
obj-$(CONFIG_BPF_SYSCALL) += syscall.o verifier.o inode.o helpers.o tnum.o cnum.o log.o token.o liveness.o const_fold.o
obj-$(CONFIG_BPF_SYSCALL) += bpf_iter.o map_iter.o task_iter.o prog_iter.o link_iter.o
obj-$(CONFIG_BPF_SYSCALL) += hashtab.o arraymap.o percpu_freelist.o bpf_lru_list.o lpm_trie.o map_in_map.o bloom_filter.o
obj-$(CONFIG_BPF_SYSCALL) += local_storage.o queue_stack_maps.o ringbuf.o bpf_insn_array.o

120
kernel/bpf/cnum.c Normal file
View File

@ -0,0 +1,120 @@
// SPDX-License-Identifier: GPL-2.0-only
/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
#include <linux/bits.h>
#define T 32
#include "cnum_defs.h"
#undef T
#define T 64
#include "cnum_defs.h"
#undef T
struct cnum32 cnum32_from_cnum64(struct cnum64 cnum)
{
if (cnum64_is_empty(cnum))
return CNUM32_EMPTY;
if (cnum.size >= U32_MAX)
return (struct cnum32){ .base = 0, .size = U32_MAX };
else
return (struct cnum32){ .base = (u32)cnum.base, .size = cnum.size };
}
/*
* Suppose 'a' and 'b' are laid out as follows:
*
* 64-bit number axis --->
*
* N*2^32 (N+1)*2^32 (N+2)*2^32 (N+3)*2^32
* ||------|---|=====|-------||----------|=====|-------||----------|=====|----|--||
* | |< b >| |< b >| |< b >| |
* | | | |
* |<--+--------------------------- a ---------------------------+--->|
* | |
* |<-------------------------- t -------------------------->|
*
* In such a case it is possible to infer a more tight representation t
* such that v a, (u32)v b: v t.
*/
struct cnum64 cnum64_cnum32_intersect(struct cnum64 a, struct cnum32 b)
{
/*
* To simplify reasoning, rotate the circles so that [virtual] a1 starts
* at u32 boundary, b1 represents b in this new frame of reference.
*/
struct cnum32 b1 = { b.base - (u32)a.base, b.size };
struct cnum64 t = a;
u64 d, b1_max;
if (cnum64_is_empty(a) || cnum32_is_empty(b))
return CNUM64_EMPTY;
if (cnum32_urange_overflow(b1)) {
b1_max = (u32)b1.base + (u32)b1.size; /* overflow here is fine and necessary */
if ((u32)a.size > b1_max && (u32)a.size < b1.base) {
/*
* N*2^32 (N+1)*2^32
* ||=====|------------|=====||=====|---------|---|=====||
* |b1 ->| |<- b1||b1 ->| | |<- b1|
* |<----------------- a1 ------------------>|
* |<-------------- t ------------>|<-- d -->| (after adjustment)
* ^
* b1_max
*/
d = (u32)a.size - b1_max;
t.size -= d;
} else {
/*
* No adjustments possible in the following cases:
*
* ||=====|------------|=====||===|=|-------------|=|===||
* |b1 ->| |<- b1||b1 +>| |<+ b1|
* |<----------------- a1 ------>| |
* |<----------------- (or) a1 ------------------->|
*/
}
} else {
if (t.size < b1.base)
/*
* N*2^32 (N+1)*2^32
* ||----------|--|=======|--||------>
* |<-- a1 -->| |<- b ->|
*/
return CNUM64_EMPTY;
/*
* N*2^32 (N+1)*2^32
* ||-------------|========|-||-----| -------|========|-||
* | |<- b1 ->| | |<- b1 ->|
* |<------------+ a1 ------------>|
* |<------ t ------>| (after adjustment)
*/
t.base += b1.base;
t.size -= b1.base;
b1_max = b1.base + b1.size;
d = 0;
if ((u32)a.size < b1.base)
/*
* N*2^32 (N+1)*2^32
* ||-------------|========|-||------|-------|========|-||
* | |<- b1 ->| | |<- b1 ->|
* |<------------+-- a1 --+-------->|
* |<- t ->|<-- d -->| (after adjustment)
*/
d = (u32)a.size + (BIT_ULL(32) - b1_max);
else if ((u32)a.size >= b1_max)
/*
* N*2^32 (N+1)*2^32
* ||--|========|------------||--|========|-------|-----||
* | |<- b1 ->| |<- b1 ->| |
* |<-+------------------ a1 ------------+------>|
* |<-------------- t --------------->|<- d ->| (after adjustment)
*/
d = (u32)a.size - b1_max;
if (t.size < d)
return CNUM64_EMPTY;
t.size -= d;
}
return t;
}

230
kernel/bpf/cnum_defs.h Normal file
View File

@ -0,0 +1,230 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
#ifndef T
#error "Define T (bit width: 32, 64) before including cnum_defs.h"
#endif
#include <linux/cnum.h>
#include <linux/limits.h>
#include <linux/minmax.h>
#include <linux/compiler_types.h>
#define cnum_t __PASTE(cnum, T)
#define ut __PASTE(u, T)
#define st __PASTE(s, T)
#define UT_MAX __PASTE(__PASTE(U, T), _MAX)
#define ST_MAX __PASTE(__PASTE(S, T), _MAX)
#define ST_MIN __PASTE(__PASTE(S, T), _MIN)
#define EMPTY __PASTE(__PASTE(CNUM, T), _EMPTY)
#define FN(name) __PASTE(__PASTE(cnum, T), __PASTE(_, name))
struct cnum_t FN(from_urange)(ut min, ut max)
{
return (struct cnum_t){ .base = min, .size = (ut)max - min };
}
struct cnum_t FN(from_srange)(st min, st max)
{
ut size = (ut)max - (ut)min;
ut base = size == UT_MAX ? 0 : (ut)min;
return (struct cnum_t){ .base = base, .size = size };
}
/* True if this cnum represents two unsigned ranges. */
static inline bool FN(urange_overflow)(struct cnum_t cnum)
{
/* Same as cnum.base + cnum.size > UT_MAX but avoids overflow */
return cnum.size > UT_MAX - (ut)cnum.base;
}
/*
* cnum{T}_umin / cnum{T}_umax query an unsigned range represented by this cnum.
* If cnum represents a range crossing the UT_MAX/0 boundary, the unbound range
* [0..UT_MAX] is returned.
*/
ut FN(umin)(struct cnum_t cnum)
{
return FN(urange_overflow)(cnum) ? 0 : cnum.base;
}
ut FN(umax)(struct cnum_t cnum)
{
return FN(urange_overflow)(cnum) ? UT_MAX : cnum.base + cnum.size;
}
/* True if this cnum represents two signed ranges. */
static inline bool FN(srange_overflow)(struct cnum_t cnum)
{
return FN(contains)(cnum, (ut)ST_MAX) && FN(contains)(cnum, (ut)ST_MIN);
}
/*
* cnum{T}_smin / cnum{T}_smax query a signed range represented by this cnum.
* If cnum represents a range crossing the ST_MAX/ST_MIN boundary, the unbound range
* [ST_MIN..ST_MAX] is returned.
*/
st FN(smin)(struct cnum_t cnum)
{
return FN(srange_overflow)(cnum)
? ST_MIN
: min((st)cnum.base, (st)(cnum.base + cnum.size));
}
st FN(smax)(struct cnum_t cnum)
{
return FN(srange_overflow)(cnum)
? ST_MAX
: max((st)cnum.base, (st)(cnum.base + cnum.size));
}
/*
* Returns a possibly empty intersection of cnums 'a' and 'b'.
* If 'a' and 'b' intersect in two sub-arcs, the function over-approximates
* and returns either 'a' or 'b', whichever is smaller.
*/
struct cnum_t FN(intersect)(struct cnum_t a, struct cnum_t b)
{
struct cnum_t b1;
ut dbase;
if (FN(is_empty)(a) || FN(is_empty)(b))
return EMPTY;
if (a.base > b.base)
swap(a, b);
/*
* Rotate frame of reference such that a.base is 0.
* 'b1' is 'b' in this frame of reference.
*/
dbase = b.base - a.base;
b1 = (struct cnum_t){ dbase, b.size };
if (FN(urange_overflow)(b1)) {
if (b1.base <= a.size) {
/*
* Rotated frame (a.base at origin):
*
* 0 UT_MAX
* |--------------------------------------------|
* [=== a ==========================] |
* [= b1 tail =] [========= b1 main ==========>]
* ^-- b1.base <= a.size
*
* 'a' and 'b' intersect in two disjoint arcs,
* can't represent as single cnum, over-approximate
* the result.
*/
return a.size <= b.size ? a : b;
} else {
/*
* Rotated frame (a.base at origin):
*
* 0 UT_MAX
* |--------------------------------------------|
* [=== a =============] | |
* [= b1 tail =] [======= b1 main ====>]
* ^-- b1.base > a.size
*
* Only 'b' tail intersects 'a'.
*/
return (struct cnum_t) {
.base = a.base,
.size = min(a.size, (ut)(b1.base + b1.size)),
};
}
} else if (a.size >= b1.base) {
/*
* Rotated frame (a.base at origin):
*
* 0 UT_MAX
* |--------------------------------------------------|
* [=== a ==================================] |
* [== b1 =====================]
*
* 0 UT_MAX
* |--------------------------------------------------|
* [=== a ==================================] |
* [== b1 ====]
* ^-- b1.base <= a.size
* |<-- a.size - dbase -->|
*
* 'a' and 'b' intersect as one cnum.
*/
return (struct cnum_t) {
.base = b.base,
.size = min((ut)(a.size - dbase), b.size),
};
} else {
return EMPTY;
}
}
void FN(intersect_with)(struct cnum_t *dst, struct cnum_t src)
{
*dst = FN(intersect)(*dst, src);
}
void FN(intersect_with_urange)(struct cnum_t *dst, ut min, ut max)
{
FN(intersect_with)(dst, FN(from_urange)(min, max));
}
void FN(intersect_with_srange)(struct cnum_t *dst, st min, st max)
{
FN(intersect_with)(dst, FN(from_srange)(min, max));
}
static inline struct cnum_t FN(normalize)(struct cnum_t cnum)
{
if (cnum.size == UT_MAX && cnum.base != 0 && cnum.base != (ut)ST_MAX)
cnum.base = 0;
return cnum;
}
struct cnum_t FN(add)(struct cnum_t a, struct cnum_t b)
{
if (FN(is_empty)(a) || FN(is_empty)(b))
return EMPTY;
if (a.size > UT_MAX - b.size)
return (struct cnum_t){ 0, (ut)UT_MAX };
else
return FN(normalize)((struct cnum_t){ a.base + b.base, a.size + b.size });
}
struct cnum_t FN(negate)(struct cnum_t a)
{
if (FN(is_empty)(a))
return EMPTY;
return FN(normalize)((struct cnum_t){ -((ut)a.base + a.size), a.size });
}
bool FN(is_empty)(struct cnum_t cnum)
{
return cnum.base == EMPTY.base && cnum.size == EMPTY.size;
}
bool FN(contains)(struct cnum_t cnum, ut v)
{
if (FN(is_empty)(cnum))
return false;
if (FN(urange_overflow)(cnum))
return v >= cnum.base || v <= (ut)cnum.base + cnum.size;
else
return v >= cnum.base && v <= (ut)cnum.base + cnum.size;
}
bool FN(is_const)(struct cnum_t cnum)
{
return cnum.size == 0;
}
#undef EMPTY
#undef cnum_t
#undef ut
#undef st
#undef UT_MAX
#undef ST_MAX
#undef ST_MIN
#undef FN