Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

bpf: Harden and/or/xor value tracking in verifier

This patch addresses a latent unsoundness issue in the
scalar(32)_min_max_and/or/xor functions. While it is not a bugfix,
it ensures that the functions produce sound outputs for all inputs.

The issue occurs in these functions when setting signed bounds. The
following example illustrates the issue for scalar_min_max_and(),
but it applies to the other functions.

In scalar_min_max_and() the following clause is executed when ANDing
positive numbers:

/* ANDing two positives gives a positive, so safe to
* cast result into s64.
*/
dst_reg->smin_value = dst_reg->umin_value;
dst_reg->smax_value = dst_reg->umax_value;

However, if umin_value and umax_value of dst_reg cross the sign boundary
(i.e., if (s64)dst_reg->umin_value > (s64)dst_reg->umax_value), then we
will end up with smin_value > smax_value, which is unsound.

Previous works [1, 2] have discovered and reported this issue. Our tool
Agni [2, 3] consideres it a false positive. This is because, during the
verification of the abstract operator scalar_min_max_and(), Agni restricts
its inputs to those passing through reg_bounds_sync(). This mimics
real-world verifier behavior, as reg_bounds_sync() is invariably executed
at the tail of every abstract operator. Therefore, such behavior is
unlikely in an actual verifier execution.

However, it is still unsound for an abstract operator to set signed bounds
such that smin_value > smax_value. This patch fixes it, making the abstract
operator sound for all (well-formed) inputs.

It is worth noting that while the previous code updated the signed bounds
(using the output unsigned bounds) only when the *input signed* bounds
were positive, the new code updates them whenever the *output unsigned*
bounds do not cross the sign boundary.

An alternative approach to fix this latent unsoundness would be to
unconditionally set the signed bounds to unbounded [S64_MIN, S64_MAX], and
let reg_bounds_sync() refine the signed bounds using the unsigned bounds
and the tnum. We found that our approach produces more precise (tighter)
bounds.

For example, consider these inputs to BPF_AND:

/* dst_reg */
var_off.value: 8608032320201083347
var_off.mask: 615339716653692460
smin_value: 8070450532247928832
smax_value: 8070450532247928832
umin_value: 13206380674380886586
umax_value: 13206380674380886586
s32_min_value: -2110561598
s32_max_value: -133438816
u32_min_value: 4135055354
u32_max_value: 4135055354

/* src_reg */
var_off.value: 8584102546103074815
var_off.mask: 9862641527606476800
smin_value: 2920655011908158522
smax_value: 7495731535348625717
umin_value: 7001104867969363969
umax_value: 8584102543730304042
s32_min_value: -2097116671
s32_max_value: 71704632
u32_min_value: 1047457619
u32_max_value: 4268683090

After going through tnum_and() -> scalar32_min_max_and() ->
scalar_min_max_and() -> reg_bounds_sync(), our patch produces the following
bounds for s32:

s32_min_value: -1263875629
s32_max_value: -159911942

Whereas, setting the signed bounds to unbounded in scalar_min_max_and()
produces:

s32_min_value: -1263875629
s32_max_value: -1

As observed, our patch produces a tighter s32 bound. We also confirmed
using Agni and SMT verification that our patch always produces signed
bounds that are equal to or more precise than setting the signed bounds to
unbounded in scalar_min_max_and().

[1] https://sanjit-bhat.github.io/assets/pdf/ebpf-verifier-range-analysis22.pdf
[2] https://link.springer.com/chapter/10.1007/978-3-031-37709-9_12
[3] https://github.com/bpfverif/agni

Co-developed-by: Matan Shachnai <m.shachnai@rutgers.edu>
Signed-off-by: Matan Shachnai <m.shachnai@rutgers.edu>
Co-developed-by: Srinivas Narayana <srinivas.narayana@rutgers.edu>
Signed-off-by: Srinivas Narayana <srinivas.narayana@rutgers.edu>
Co-developed-by: Santosh Nagarakatte <santosh.nagarakatte@rutgers.edu>
Signed-off-by: Santosh Nagarakatte <santosh.nagarakatte@rutgers.edu>
Signed-off-by: Harishankar Vishwanathan <harishankar.vishwanathan@gmail.com>
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
Acked-by: Daniel Borkmann <daniel@iogearbox.net>
Link: https://lore.kernel.org/bpf/20240402212039.51815-1-harishankar.vishwanathan@gmail.com
Link: https://lore.kernel.org/bpf/20240416115303.331688-1-harishankar.vishwanathan@gmail.com

authored by

Harishankar Vishwanathan and committed by
Daniel Borkmann
1f586614 dac045fc

+40 -54
+40 -54
kernel/bpf/verifier.c
··· 13320 13320 bool src_known = tnum_subreg_is_const(src_reg->var_off); 13321 13321 bool dst_known = tnum_subreg_is_const(dst_reg->var_off); 13322 13322 struct tnum var32_off = tnum_subreg(dst_reg->var_off); 13323 - s32 smin_val = src_reg->s32_min_value; 13324 13323 u32 umax_val = src_reg->u32_max_value; 13325 13324 13326 13325 if (src_known && dst_known) { ··· 13332 13333 */ 13333 13334 dst_reg->u32_min_value = var32_off.value; 13334 13335 dst_reg->u32_max_value = min(dst_reg->u32_max_value, umax_val); 13335 - if (dst_reg->s32_min_value < 0 || smin_val < 0) { 13336 - /* Lose signed bounds when ANDing negative numbers, 13337 - * ain't nobody got time for that. 13338 - */ 13339 - dst_reg->s32_min_value = S32_MIN; 13340 - dst_reg->s32_max_value = S32_MAX; 13341 - } else { 13342 - /* ANDing two positives gives a positive, so safe to 13343 - * cast result into s64. 13344 - */ 13336 + 13337 + /* Safe to set s32 bounds by casting u32 result into s32 when u32 13338 + * doesn't cross sign boundary. Otherwise set s32 bounds to unbounded. 13339 + */ 13340 + if ((s32)dst_reg->u32_min_value <= (s32)dst_reg->u32_max_value) { 13345 13341 dst_reg->s32_min_value = dst_reg->u32_min_value; 13346 13342 dst_reg->s32_max_value = dst_reg->u32_max_value; 13343 + } else { 13344 + dst_reg->s32_min_value = S32_MIN; 13345 + dst_reg->s32_max_value = S32_MAX; 13347 13346 } 13348 13347 } 13349 13348 ··· 13350 13353 { 13351 13354 bool src_known = tnum_is_const(src_reg->var_off); 13352 13355 bool dst_known = tnum_is_const(dst_reg->var_off); 13353 - s64 smin_val = src_reg->smin_value; 13354 13356 u64 umax_val = src_reg->umax_value; 13355 13357 13356 13358 if (src_known && dst_known) { ··· 13362 13366 */ 13363 13367 dst_reg->umin_value = dst_reg->var_off.value; 13364 13368 dst_reg->umax_value = min(dst_reg->umax_value, umax_val); 13365 - if (dst_reg->smin_value < 0 || smin_val < 0) { 13366 - /* Lose signed bounds when ANDing negative numbers, 13367 - * ain't nobody got time for that. 13368 - */ 13369 - dst_reg->smin_value = S64_MIN; 13370 - dst_reg->smax_value = S64_MAX; 13371 - } else { 13372 - /* ANDing two positives gives a positive, so safe to 13373 - * cast result into s64. 13374 - */ 13369 + 13370 + /* Safe to set s64 bounds by casting u64 result into s64 when u64 13371 + * doesn't cross sign boundary. Otherwise set s64 bounds to unbounded. 13372 + */ 13373 + if ((s64)dst_reg->umin_value <= (s64)dst_reg->umax_value) { 13375 13374 dst_reg->smin_value = dst_reg->umin_value; 13376 13375 dst_reg->smax_value = dst_reg->umax_value; 13376 + } else { 13377 + dst_reg->smin_value = S64_MIN; 13378 + dst_reg->smax_value = S64_MAX; 13377 13379 } 13378 13380 /* We may learn something more from the var_off */ 13379 13381 __update_reg_bounds(dst_reg); ··· 13383 13389 bool src_known = tnum_subreg_is_const(src_reg->var_off); 13384 13390 bool dst_known = tnum_subreg_is_const(dst_reg->var_off); 13385 13391 struct tnum var32_off = tnum_subreg(dst_reg->var_off); 13386 - s32 smin_val = src_reg->s32_min_value; 13387 13392 u32 umin_val = src_reg->u32_min_value; 13388 13393 13389 13394 if (src_known && dst_known) { ··· 13395 13402 */ 13396 13403 dst_reg->u32_min_value = max(dst_reg->u32_min_value, umin_val); 13397 13404 dst_reg->u32_max_value = var32_off.value | var32_off.mask; 13398 - if (dst_reg->s32_min_value < 0 || smin_val < 0) { 13399 - /* Lose signed bounds when ORing negative numbers, 13400 - * ain't nobody got time for that. 13401 - */ 13402 - dst_reg->s32_min_value = S32_MIN; 13403 - dst_reg->s32_max_value = S32_MAX; 13404 - } else { 13405 - /* ORing two positives gives a positive, so safe to 13406 - * cast result into s64. 13407 - */ 13405 + 13406 + /* Safe to set s32 bounds by casting u32 result into s32 when u32 13407 + * doesn't cross sign boundary. Otherwise set s32 bounds to unbounded. 13408 + */ 13409 + if ((s32)dst_reg->u32_min_value <= (s32)dst_reg->u32_max_value) { 13408 13410 dst_reg->s32_min_value = dst_reg->u32_min_value; 13409 13411 dst_reg->s32_max_value = dst_reg->u32_max_value; 13412 + } else { 13413 + dst_reg->s32_min_value = S32_MIN; 13414 + dst_reg->s32_max_value = S32_MAX; 13410 13415 } 13411 13416 } 13412 13417 ··· 13413 13422 { 13414 13423 bool src_known = tnum_is_const(src_reg->var_off); 13415 13424 bool dst_known = tnum_is_const(dst_reg->var_off); 13416 - s64 smin_val = src_reg->smin_value; 13417 13425 u64 umin_val = src_reg->umin_value; 13418 13426 13419 13427 if (src_known && dst_known) { ··· 13425 13435 */ 13426 13436 dst_reg->umin_value = max(dst_reg->umin_value, umin_val); 13427 13437 dst_reg->umax_value = dst_reg->var_off.value | dst_reg->var_off.mask; 13428 - if (dst_reg->smin_value < 0 || smin_val < 0) { 13429 - /* Lose signed bounds when ORing negative numbers, 13430 - * ain't nobody got time for that. 13431 - */ 13432 - dst_reg->smin_value = S64_MIN; 13433 - dst_reg->smax_value = S64_MAX; 13434 - } else { 13435 - /* ORing two positives gives a positive, so safe to 13436 - * cast result into s64. 13437 - */ 13438 + 13439 + /* Safe to set s64 bounds by casting u64 result into s64 when u64 13440 + * doesn't cross sign boundary. Otherwise set s64 bounds to unbounded. 13441 + */ 13442 + if ((s64)dst_reg->umin_value <= (s64)dst_reg->umax_value) { 13438 13443 dst_reg->smin_value = dst_reg->umin_value; 13439 13444 dst_reg->smax_value = dst_reg->umax_value; 13445 + } else { 13446 + dst_reg->smin_value = S64_MIN; 13447 + dst_reg->smax_value = S64_MAX; 13440 13448 } 13441 13449 /* We may learn something more from the var_off */ 13442 13450 __update_reg_bounds(dst_reg); ··· 13446 13458 bool src_known = tnum_subreg_is_const(src_reg->var_off); 13447 13459 bool dst_known = tnum_subreg_is_const(dst_reg->var_off); 13448 13460 struct tnum var32_off = tnum_subreg(dst_reg->var_off); 13449 - s32 smin_val = src_reg->s32_min_value; 13450 13461 13451 13462 if (src_known && dst_known) { 13452 13463 __mark_reg32_known(dst_reg, var32_off.value); ··· 13456 13469 dst_reg->u32_min_value = var32_off.value; 13457 13470 dst_reg->u32_max_value = var32_off.value | var32_off.mask; 13458 13471 13459 - if (dst_reg->s32_min_value >= 0 && smin_val >= 0) { 13460 - /* XORing two positive sign numbers gives a positive, 13461 - * so safe to cast u32 result into s32. 13462 - */ 13472 + /* Safe to set s32 bounds by casting u32 result into s32 when u32 13473 + * doesn't cross sign boundary. Otherwise set s32 bounds to unbounded. 13474 + */ 13475 + if ((s32)dst_reg->u32_min_value <= (s32)dst_reg->u32_max_value) { 13463 13476 dst_reg->s32_min_value = dst_reg->u32_min_value; 13464 13477 dst_reg->s32_max_value = dst_reg->u32_max_value; 13465 13478 } else { ··· 13473 13486 { 13474 13487 bool src_known = tnum_is_const(src_reg->var_off); 13475 13488 bool dst_known = tnum_is_const(dst_reg->var_off); 13476 - s64 smin_val = src_reg->smin_value; 13477 13489 13478 13490 if (src_known && dst_known) { 13479 13491 /* dst_reg->var_off.value has been updated earlier */ ··· 13484 13498 dst_reg->umin_value = dst_reg->var_off.value; 13485 13499 dst_reg->umax_value = dst_reg->var_off.value | dst_reg->var_off.mask; 13486 13500 13487 - if (dst_reg->smin_value >= 0 && smin_val >= 0) { 13488 - /* XORing two positive sign numbers gives a positive, 13489 - * so safe to cast u64 result into s64. 13490 - */ 13501 + /* Safe to set s64 bounds by casting u64 result into s64 when u64 13502 + * doesn't cross sign boundary. Otherwise set s64 bounds to unbounded. 13503 + */ 13504 + if ((s64)dst_reg->umin_value <= (s64)dst_reg->umax_value) { 13491 13505 dst_reg->smin_value = dst_reg->umin_value; 13492 13506 dst_reg->smax_value = dst_reg->umax_value; 13493 13507 } else {