diff mbox series

[bpf-next,1/7] bpf: regsafe() must not skip check_ids()

Message ID 20221209135733.28851-2-eddyz87@gmail.com (mailing list archive)
State Accepted
Commit 7c884339bbff80250bfc11d56b5cf48640e6ebdb
Delegated to: BPF
Headers show
Series stricter register ID checking in regsafe() | expand

Checks

Context Check Description
bpf/vmtest-bpf-next-VM_Test-10 success Logs for test_maps on aarch64 with llvm-16
bpf/vmtest-bpf-next-VM_Test-12 success Logs for test_maps on x86_64 with gcc
bpf/vmtest-bpf-next-VM_Test-13 success Logs for test_maps on x86_64 with llvm-16
bpf/vmtest-bpf-next-VM_Test-14 success Logs for test_progs on aarch64 with gcc
bpf/vmtest-bpf-next-VM_Test-15 success Logs for test_progs on aarch64 with llvm-16
bpf/vmtest-bpf-next-VM_Test-17 success Logs for test_progs on x86_64 with gcc
bpf/vmtest-bpf-next-VM_Test-18 success Logs for test_progs on x86_64 with llvm-16
bpf/vmtest-bpf-next-VM_Test-19 success Logs for test_progs_no_alu32 on aarch64 with gcc
bpf/vmtest-bpf-next-VM_Test-20 success Logs for test_progs_no_alu32 on aarch64 with llvm-16
bpf/vmtest-bpf-next-VM_Test-22 success Logs for test_progs_no_alu32 on x86_64 with gcc
bpf/vmtest-bpf-next-VM_Test-23 success Logs for test_progs_no_alu32 on x86_64 with llvm-16
bpf/vmtest-bpf-next-VM_Test-24 success Logs for test_progs_no_alu32_parallel on aarch64 with gcc
bpf/vmtest-bpf-next-VM_Test-25 success Logs for test_progs_no_alu32_parallel on aarch64 with llvm-16
bpf/vmtest-bpf-next-VM_Test-27 success Logs for test_progs_no_alu32_parallel on x86_64 with gcc
bpf/vmtest-bpf-next-VM_Test-28 success Logs for test_progs_no_alu32_parallel on x86_64 with llvm-16
bpf/vmtest-bpf-next-VM_Test-29 success Logs for test_progs_parallel on aarch64 with gcc
bpf/vmtest-bpf-next-VM_Test-30 success Logs for test_progs_parallel on aarch64 with llvm-16
bpf/vmtest-bpf-next-VM_Test-32 success Logs for test_progs_parallel on x86_64 with gcc
bpf/vmtest-bpf-next-VM_Test-33 success Logs for test_progs_parallel on x86_64 with llvm-16
bpf/vmtest-bpf-next-VM_Test-34 success Logs for test_verifier on aarch64 with gcc
bpf/vmtest-bpf-next-VM_Test-35 success Logs for test_verifier on aarch64 with llvm-16
bpf/vmtest-bpf-next-VM_Test-36 success Logs for test_verifier on s390x with gcc
bpf/vmtest-bpf-next-VM_Test-37 success Logs for test_verifier on x86_64 with gcc
bpf/vmtest-bpf-next-VM_Test-38 success Logs for test_verifier on x86_64 with llvm-16
bpf/vmtest-bpf-next-VM_Test-16 success Logs for test_progs on s390x with gcc
bpf/vmtest-bpf-next-VM_Test-21 success Logs for test_progs_no_alu32 on s390x with gcc
bpf/vmtest-bpf-next-VM_Test-26 success Logs for test_progs_no_alu32_parallel on s390x with gcc
bpf/vmtest-bpf-next-VM_Test-31 success Logs for test_progs_parallel on s390x with gcc
netdev/tree_selection success Clearly marked for bpf-next
netdev/fixes_present success Fixes tag not required for -next series
netdev/subject_prefix success Link
netdev/cover_letter success Series has a cover letter
netdev/patch_count success Link
netdev/header_inline success No static functions without inline keyword in header files
netdev/build_32bit success Errors and warnings before: 10 this patch: 10
netdev/cc_maintainers warning 7 maintainers not CCed: kpsingh@kernel.org haoluo@google.com song@kernel.org martin.lau@linux.dev sdf@google.com john.fastabend@gmail.com jolsa@kernel.org
netdev/build_clang success Errors and warnings before: 5 this patch: 5
netdev/module_param success Was 0 now: 0
netdev/verify_signedoff success Signed-off-by tag matches author and committer
netdev/check_selftest success No net selftest shell script
netdev/verify_fixes success No Fixes tag
netdev/build_allmodconfig_warn success Errors and warnings before: 10 this patch: 10
netdev/checkpatch success total: 0 errors, 0 warnings, 0 checks, 49 lines checked
netdev/kdoc success Errors and warnings before: 0 this patch: 0
netdev/source_inline success Was 0 now: 0
bpf/vmtest-bpf-next-VM_Test-11 success Logs for test_maps on s390x with gcc
bpf/vmtest-bpf-next-PR fail PR summary
bpf/vmtest-bpf-next-VM_Test-1 success Logs for ${{ matrix.test }} on ${{ matrix.arch }} with ${{ matrix.toolchain }}
bpf/vmtest-bpf-next-VM_Test-2 success Logs for ShellCheck
bpf/vmtest-bpf-next-VM_Test-3 success Logs for build for aarch64 with gcc
bpf/vmtest-bpf-next-VM_Test-4 success Logs for build for aarch64 with llvm-16
bpf/vmtest-bpf-next-VM_Test-5 fail Logs for build for s390x with gcc
bpf/vmtest-bpf-next-VM_Test-6 success Logs for build for x86_64 with gcc
bpf/vmtest-bpf-next-VM_Test-7 success Logs for build for x86_64 with llvm-16
bpf/vmtest-bpf-next-VM_Test-8 success Logs for llvm-toolchain
bpf/vmtest-bpf-next-VM_Test-9 success Logs for set-matrix

Commit Message

Eduard Zingerman Dec. 9, 2022, 1:57 p.m. UTC
The verifier.c:regsafe() has the following shortcut:

	equal = memcmp(rold, rcur, offsetof(struct bpf_reg_state, parent)) == 0;
	...
	if (equal)
		return true;

Which is executed regardless old register type. This is incorrect for
register types that might have an ID checked by check_ids(), namely:
 - PTR_TO_MAP_KEY
 - PTR_TO_MAP_VALUE
 - PTR_TO_PACKET_META
 - PTR_TO_PACKET

The following pattern could be used to exploit this:

  0: r9 = map_lookup_elem(...)  ; Returns PTR_TO_MAP_VALUE_OR_NULL id=1.
  1: r8 = map_lookup_elem(...)  ; Returns PTR_TO_MAP_VALUE_OR_NULL id=2.
  2: r7 = ktime_get_ns()        ; Unbound SCALAR_VALUE.
  3: r6 = ktime_get_ns()        ; Unbound SCALAR_VALUE.
  4: if r6 > r7 goto +1         ; No new information about the state
                                ; is derived from this check, thus
                                ; produced verifier states differ only
                                ; in 'insn_idx'.
  5: r9 = r8                    ; Optionally make r9.id == r8.id.
  --- checkpoint ---            ; Assume is_state_visisted() creates a
                                ; checkpoint here.
  6: if r9 == 0 goto <exit>     ; Nullness info is propagated to all
                                ; registers with matching ID.
  7: r1 = *(u64 *) r8           ; Not always safe.

Verifier first visits path 1-7 where r8 is verified to be not null
at (6). Later the jump from 4 to 6 is examined. The checkpoint for (6)
looks as follows:
  R8_rD=map_value_or_null(id=2,off=0,ks=4,vs=8,imm=0)
  R9_rwD=map_value_or_null(id=2,off=0,ks=4,vs=8,imm=0)
  R10=fp0

The current state is:
  R0=... R6=... R7=... fp-8=...
  R8=map_value_or_null(id=2,off=0,ks=4,vs=8,imm=0)
  R9=map_value_or_null(id=1,off=0,ks=4,vs=8,imm=0)
  R10=fp0

Note that R8 states are byte-to-byte identical, so regsafe() would
exit early and skip call to check_ids(), thus ID mapping 2->2 will not
be added to 'idmap'. Next, states for R9 are compared: these are not
identical and check_ids() is executed, but 'idmap' is empty, so
check_ids() adds mapping 2->1 to 'idmap' and returns success.

This commit pushes the 'equal' down to register types that don't need
check_ids().

Signed-off-by: Eduard Zingerman <eddyz87@gmail.com>
---
 kernel/bpf/verifier.c | 29 ++++++++---------------------
 1 file changed, 8 insertions(+), 21 deletions(-)

Comments

Andrii Nakryiko Dec. 14, 2022, 12:35 a.m. UTC | #1
On Fri, Dec 9, 2022 at 5:58 AM Eduard Zingerman <eddyz87@gmail.com> wrote:
>
> The verifier.c:regsafe() has the following shortcut:
>
>         equal = memcmp(rold, rcur, offsetof(struct bpf_reg_state, parent)) == 0;
>         ...
>         if (equal)
>                 return true;
>
> Which is executed regardless old register type. This is incorrect for
> register types that might have an ID checked by check_ids(), namely:
>  - PTR_TO_MAP_KEY
>  - PTR_TO_MAP_VALUE
>  - PTR_TO_PACKET_META
>  - PTR_TO_PACKET
>
> The following pattern could be used to exploit this:
>
>   0: r9 = map_lookup_elem(...)  ; Returns PTR_TO_MAP_VALUE_OR_NULL id=1.
>   1: r8 = map_lookup_elem(...)  ; Returns PTR_TO_MAP_VALUE_OR_NULL id=2.
>   2: r7 = ktime_get_ns()        ; Unbound SCALAR_VALUE.
>   3: r6 = ktime_get_ns()        ; Unbound SCALAR_VALUE.
>   4: if r6 > r7 goto +1         ; No new information about the state
>                                 ; is derived from this check, thus
>                                 ; produced verifier states differ only
>                                 ; in 'insn_idx'.
>   5: r9 = r8                    ; Optionally make r9.id == r8.id.
>   --- checkpoint ---            ; Assume is_state_visisted() creates a
>                                 ; checkpoint here.
>   6: if r9 == 0 goto <exit>     ; Nullness info is propagated to all
>                                 ; registers with matching ID.
>   7: r1 = *(u64 *) r8           ; Not always safe.
>
> Verifier first visits path 1-7 where r8 is verified to be not null
> at (6). Later the jump from 4 to 6 is examined. The checkpoint for (6)
> looks as follows:
>   R8_rD=map_value_or_null(id=2,off=0,ks=4,vs=8,imm=0)
>   R9_rwD=map_value_or_null(id=2,off=0,ks=4,vs=8,imm=0)
>   R10=fp0
>
> The current state is:
>   R0=... R6=... R7=... fp-8=...
>   R8=map_value_or_null(id=2,off=0,ks=4,vs=8,imm=0)
>   R9=map_value_or_null(id=1,off=0,ks=4,vs=8,imm=0)
>   R10=fp0
>
> Note that R8 states are byte-to-byte identical, so regsafe() would
> exit early and skip call to check_ids(), thus ID mapping 2->2 will not
> be added to 'idmap'. Next, states for R9 are compared: these are not
> identical and check_ids() is executed, but 'idmap' is empty, so
> check_ids() adds mapping 2->1 to 'idmap' and returns success.
>
> This commit pushes the 'equal' down to register types that don't need
> check_ids().
>
> Signed-off-by: Eduard Zingerman <eddyz87@gmail.com>
> ---
>  kernel/bpf/verifier.c | 29 ++++++++---------------------
>  1 file changed, 8 insertions(+), 21 deletions(-)
>
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index 3194e9d9e4e4..d05c5d0344c6 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
> @@ -12926,15 +12926,6 @@ static bool regsafe(struct bpf_verifier_env *env, struct bpf_reg_state *rold,
>
>         equal = memcmp(rold, rcur, offsetof(struct bpf_reg_state, parent)) == 0;
>
> -       if (rold->type == PTR_TO_STACK)
> -               /* two stack pointers are equal only if they're pointing to
> -                * the same stack frame, since fp-8 in foo != fp-8 in bar
> -                */
> -               return equal && rold->frameno == rcur->frameno;
> -
> -       if (equal)
> -               return true;
> -
>         if (rold->type == NOT_INIT)
>                 /* explored state can't have used this */
>                 return true;
> @@ -12942,6 +12933,8 @@ static bool regsafe(struct bpf_verifier_env *env, struct bpf_reg_state *rold,
>                 return false;
>         switch (base_type(rold->type)) {
>         case SCALAR_VALUE:
> +               if (equal)
> +                       return true;
>                 if (env->explore_alu_limits)
>                         return false;
>                 if (rcur->type == SCALAR_VALUE) {
> @@ -13012,20 +13005,14 @@ static bool regsafe(struct bpf_verifier_env *env, struct bpf_reg_state *rold,
>                 /* new val must satisfy old val knowledge */
>                 return range_within(rold, rcur) &&
>                        tnum_in(rold->var_off, rcur->var_off);
> -       case PTR_TO_CTX:
> -       case CONST_PTR_TO_MAP:
> -       case PTR_TO_PACKET_END:
> -       case PTR_TO_FLOW_KEYS:
> -       case PTR_TO_SOCKET:
> -       case PTR_TO_SOCK_COMMON:
> -       case PTR_TO_TCP_SOCK:
> -       case PTR_TO_XDP_SOCK:
> -               /* Only valid matches are exact, which memcmp() above
> -                * would have accepted
> +       case PTR_TO_STACK:
> +               /* two stack pointers are equal only if they're pointing to
> +                * the same stack frame, since fp-8 in foo != fp-8 in bar
>                  */
> +               return equal && rold->frameno == rcur->frameno;
>         default:
> -               /* Don't know what's going on, just say it's not safe */
> -               return false;
> +               /* Only valid matches are exact, which memcmp() */
> +               return equal;

Is it safe to assume this for any possible register type? Wouldn't
register types that use id and/or ref_obj_id need extra checks here? I
think preexisting default was a safer approach, in which if we forgot
to explicitly add support for some new or updated register type, the
worst thing is that for that *new* register we'd have suboptimal
verification performance, but not safety concerns.


>         }
>
>         /* Shouldn't get here; if we do, say it's not safe */
> --
> 2.34.1
>
Eduard Zingerman Dec. 14, 2022, 1:25 p.m. UTC | #2
On Tue, 2022-12-13 at 16:35 -0800, Andrii Nakryiko wrote:
> On Fri, Dec 9, 2022 at 5:58 AM Eduard Zingerman <eddyz87@gmail.com> wrote:
> > 
> > The verifier.c:regsafe() has the following shortcut:
> > 
> >         equal = memcmp(rold, rcur, offsetof(struct bpf_reg_state, parent)) == 0;
> >         ...
> >         if (equal)
> >                 return true;
> > 
> > Which is executed regardless old register type. This is incorrect for
> > register types that might have an ID checked by check_ids(), namely:
> >  - PTR_TO_MAP_KEY
> >  - PTR_TO_MAP_VALUE
> >  - PTR_TO_PACKET_META
> >  - PTR_TO_PACKET
> > 
> > The following pattern could be used to exploit this:
> > 
> >   0: r9 = map_lookup_elem(...)  ; Returns PTR_TO_MAP_VALUE_OR_NULL id=1.
> >   1: r8 = map_lookup_elem(...)  ; Returns PTR_TO_MAP_VALUE_OR_NULL id=2.
> >   2: r7 = ktime_get_ns()        ; Unbound SCALAR_VALUE.
> >   3: r6 = ktime_get_ns()        ; Unbound SCALAR_VALUE.
> >   4: if r6 > r7 goto +1         ; No new information about the state
> >                                 ; is derived from this check, thus
> >                                 ; produced verifier states differ only
> >                                 ; in 'insn_idx'.
> >   5: r9 = r8                    ; Optionally make r9.id == r8.id.
> >   --- checkpoint ---            ; Assume is_state_visisted() creates a
> >                                 ; checkpoint here.
> >   6: if r9 == 0 goto <exit>     ; Nullness info is propagated to all
> >                                 ; registers with matching ID.
> >   7: r1 = *(u64 *) r8           ; Not always safe.
> > 
> > Verifier first visits path 1-7 where r8 is verified to be not null
> > at (6). Later the jump from 4 to 6 is examined. The checkpoint for (6)
> > looks as follows:
> >   R8_rD=map_value_or_null(id=2,off=0,ks=4,vs=8,imm=0)
> >   R9_rwD=map_value_or_null(id=2,off=0,ks=4,vs=8,imm=0)
> >   R10=fp0
> > 
> > The current state is:
> >   R0=... R6=... R7=... fp-8=...
> >   R8=map_value_or_null(id=2,off=0,ks=4,vs=8,imm=0)
> >   R9=map_value_or_null(id=1,off=0,ks=4,vs=8,imm=0)
> >   R10=fp0
> > 
> > Note that R8 states are byte-to-byte identical, so regsafe() would
> > exit early and skip call to check_ids(), thus ID mapping 2->2 will not
> > be added to 'idmap'. Next, states for R9 are compared: these are not
> > identical and check_ids() is executed, but 'idmap' is empty, so
> > check_ids() adds mapping 2->1 to 'idmap' and returns success.
> > 
> > This commit pushes the 'equal' down to register types that don't need
> > check_ids().
> > 
> > Signed-off-by: Eduard Zingerman <eddyz87@gmail.com>
> > ---
> >  kernel/bpf/verifier.c | 29 ++++++++---------------------
> >  1 file changed, 8 insertions(+), 21 deletions(-)
> > 
> > diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> > index 3194e9d9e4e4..d05c5d0344c6 100644
> > --- a/kernel/bpf/verifier.c
> > +++ b/kernel/bpf/verifier.c
> > @@ -12926,15 +12926,6 @@ static bool regsafe(struct bpf_verifier_env *env, struct bpf_reg_state *rold,
> > 
> >         equal = memcmp(rold, rcur, offsetof(struct bpf_reg_state, parent)) == 0;
> > 
> > -       if (rold->type == PTR_TO_STACK)
> > -               /* two stack pointers are equal only if they're pointing to
> > -                * the same stack frame, since fp-8 in foo != fp-8 in bar
> > -                */
> > -               return equal && rold->frameno == rcur->frameno;
> > -
> > -       if (equal)
> > -               return true;
> > -
> >         if (rold->type == NOT_INIT)
> >                 /* explored state can't have used this */
> >                 return true;
> > @@ -12942,6 +12933,8 @@ static bool regsafe(struct bpf_verifier_env *env, struct bpf_reg_state *rold,
> >                 return false;
> >         switch (base_type(rold->type)) {
> >         case SCALAR_VALUE:
> > +               if (equal)
> > +                       return true;
> >                 if (env->explore_alu_limits)
> >                         return false;
> >                 if (rcur->type == SCALAR_VALUE) {
> > @@ -13012,20 +13005,14 @@ static bool regsafe(struct bpf_verifier_env *env, struct bpf_reg_state *rold,
> >                 /* new val must satisfy old val knowledge */
> >                 return range_within(rold, rcur) &&
> >                        tnum_in(rold->var_off, rcur->var_off);
> > -       case PTR_TO_CTX:
> > -       case CONST_PTR_TO_MAP:
> > -       case PTR_TO_PACKET_END:
> > -       case PTR_TO_FLOW_KEYS:
> > -       case PTR_TO_SOCKET:
> > -       case PTR_TO_SOCK_COMMON:
> > -       case PTR_TO_TCP_SOCK:
> > -       case PTR_TO_XDP_SOCK:
> > -               /* Only valid matches are exact, which memcmp() above
> > -                * would have accepted
> > +       case PTR_TO_STACK:
> > +               /* two stack pointers are equal only if they're pointing to
> > +                * the same stack frame, since fp-8 in foo != fp-8 in bar
> >                  */
> > +               return equal && rold->frameno == rcur->frameno;
> >         default:
> > -               /* Don't know what's going on, just say it's not safe */
> > -               return false;
> > +               /* Only valid matches are exact, which memcmp() */
> > +               return equal;
> 
> Is it safe to assume this for any possible register type? Wouldn't
> register types that use id and/or ref_obj_id need extra checks here? I
> think preexisting default was a safer approach, in which if we forgot
> to explicitly add support for some new or updated register type, the
> worst thing is that for that *new* register we'd have suboptimal
> verification performance, but not safety concerns.

Well, I don't think that this commit changes regsafe() behavior in
this regard. Here is how the code was structured before this commit:

static bool regsafe(struct bpf_verifier_env *env, struct bpf_reg_state *rold,
		    struct bpf_reg_state *rcur, struct bpf_id_pair *idmap)
{
	bool equal;

	if (!(rold->live & REG_LIVE_READ))
		return true;
	equal = memcmp(rold, rcur, offsetof(struct bpf_reg_state, parent)) == 0;
	if (rold->type == PTR_TO_STACK)
		return equal && rold->frameno == rcur->frameno;
--->	if (equal)
		return true;
	if (rold->type == NOT_INIT)
		return true;
	if (rcur->type == NOT_INIT)
		return false;
	switch (base_type(rold->type)) {
	case SCALAR_VALUE:
        	... it's own logic, always returns ...
	case PTR_TO_MAP_KEY:
	case PTR_TO_MAP_VALUE:
        	... it's own logic, always returns ...
	case PTR_TO_PACKET_META:
	case PTR_TO_PACKET:
        	... it's own logic, always returns ...
	case PTR_TO_CTX:
	case CONST_PTR_TO_MAP:
	case PTR_TO_PACKET_END:
	case PTR_TO_FLOW_KEYS:
	case PTR_TO_SOCKET:
	case PTR_TO_SOCK_COMMON:
	case PTR_TO_TCP_SOCK:
	case PTR_TO_XDP_SOCK:
	default:
		return false;
	}

	/* Shouldn't get here; if we do, say it's not safe */
	WARN_ON_ONCE(1);
	return false;
}

So the "safe if byte-to-byte equal" behavior was present already.
I can add an explicit list of types to the "return equal;" branch
and add a default "return false;" branch if you think that it is
more fool-proof.

> 
> 
> >         }
> > 
> >         /* Shouldn't get here; if we do, say it's not safe */
> > --
> > 2.34.1
> >
Andrii Nakryiko Dec. 14, 2022, 7:37 p.m. UTC | #3
On Wed, Dec 14, 2022 at 5:26 AM Eduard Zingerman <eddyz87@gmail.com> wrote:
>
> On Tue, 2022-12-13 at 16:35 -0800, Andrii Nakryiko wrote:
> > On Fri, Dec 9, 2022 at 5:58 AM Eduard Zingerman <eddyz87@gmail.com> wrote:
> > >
> > > The verifier.c:regsafe() has the following shortcut:
> > >
> > >         equal = memcmp(rold, rcur, offsetof(struct bpf_reg_state, parent)) == 0;
> > >         ...
> > >         if (equal)
> > >                 return true;
> > >
> > > Which is executed regardless old register type. This is incorrect for
> > > register types that might have an ID checked by check_ids(), namely:
> > >  - PTR_TO_MAP_KEY
> > >  - PTR_TO_MAP_VALUE
> > >  - PTR_TO_PACKET_META
> > >  - PTR_TO_PACKET
> > >
> > > The following pattern could be used to exploit this:
> > >
> > >   0: r9 = map_lookup_elem(...)  ; Returns PTR_TO_MAP_VALUE_OR_NULL id=1.
> > >   1: r8 = map_lookup_elem(...)  ; Returns PTR_TO_MAP_VALUE_OR_NULL id=2.
> > >   2: r7 = ktime_get_ns()        ; Unbound SCALAR_VALUE.
> > >   3: r6 = ktime_get_ns()        ; Unbound SCALAR_VALUE.
> > >   4: if r6 > r7 goto +1         ; No new information about the state
> > >                                 ; is derived from this check, thus
> > >                                 ; produced verifier states differ only
> > >                                 ; in 'insn_idx'.
> > >   5: r9 = r8                    ; Optionally make r9.id == r8.id.
> > >   --- checkpoint ---            ; Assume is_state_visisted() creates a
> > >                                 ; checkpoint here.
> > >   6: if r9 == 0 goto <exit>     ; Nullness info is propagated to all
> > >                                 ; registers with matching ID.
> > >   7: r1 = *(u64 *) r8           ; Not always safe.
> > >
> > > Verifier first visits path 1-7 where r8 is verified to be not null
> > > at (6). Later the jump from 4 to 6 is examined. The checkpoint for (6)
> > > looks as follows:
> > >   R8_rD=map_value_or_null(id=2,off=0,ks=4,vs=8,imm=0)
> > >   R9_rwD=map_value_or_null(id=2,off=0,ks=4,vs=8,imm=0)
> > >   R10=fp0
> > >
> > > The current state is:
> > >   R0=... R6=... R7=... fp-8=...
> > >   R8=map_value_or_null(id=2,off=0,ks=4,vs=8,imm=0)
> > >   R9=map_value_or_null(id=1,off=0,ks=4,vs=8,imm=0)
> > >   R10=fp0
> > >
> > > Note that R8 states are byte-to-byte identical, so regsafe() would
> > > exit early and skip call to check_ids(), thus ID mapping 2->2 will not
> > > be added to 'idmap'. Next, states for R9 are compared: these are not
> > > identical and check_ids() is executed, but 'idmap' is empty, so
> > > check_ids() adds mapping 2->1 to 'idmap' and returns success.
> > >
> > > This commit pushes the 'equal' down to register types that don't need
> > > check_ids().
> > >
> > > Signed-off-by: Eduard Zingerman <eddyz87@gmail.com>
> > > ---
> > >  kernel/bpf/verifier.c | 29 ++++++++---------------------
> > >  1 file changed, 8 insertions(+), 21 deletions(-)
> > >
> > > diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> > > index 3194e9d9e4e4..d05c5d0344c6 100644
> > > --- a/kernel/bpf/verifier.c
> > > +++ b/kernel/bpf/verifier.c
> > > @@ -12926,15 +12926,6 @@ static bool regsafe(struct bpf_verifier_env *env, struct bpf_reg_state *rold,
> > >
> > >         equal = memcmp(rold, rcur, offsetof(struct bpf_reg_state, parent)) == 0;
> > >
> > > -       if (rold->type == PTR_TO_STACK)
> > > -               /* two stack pointers are equal only if they're pointing to
> > > -                * the same stack frame, since fp-8 in foo != fp-8 in bar
> > > -                */
> > > -               return equal && rold->frameno == rcur->frameno;
> > > -
> > > -       if (equal)
> > > -               return true;
> > > -
> > >         if (rold->type == NOT_INIT)
> > >                 /* explored state can't have used this */
> > >                 return true;
> > > @@ -12942,6 +12933,8 @@ static bool regsafe(struct bpf_verifier_env *env, struct bpf_reg_state *rold,
> > >                 return false;
> > >         switch (base_type(rold->type)) {
> > >         case SCALAR_VALUE:
> > > +               if (equal)
> > > +                       return true;
> > >                 if (env->explore_alu_limits)
> > >                         return false;
> > >                 if (rcur->type == SCALAR_VALUE) {
> > > @@ -13012,20 +13005,14 @@ static bool regsafe(struct bpf_verifier_env *env, struct bpf_reg_state *rold,
> > >                 /* new val must satisfy old val knowledge */
> > >                 return range_within(rold, rcur) &&
> > >                        tnum_in(rold->var_off, rcur->var_off);
> > > -       case PTR_TO_CTX:
> > > -       case CONST_PTR_TO_MAP:
> > > -       case PTR_TO_PACKET_END:
> > > -       case PTR_TO_FLOW_KEYS:
> > > -       case PTR_TO_SOCKET:
> > > -       case PTR_TO_SOCK_COMMON:
> > > -       case PTR_TO_TCP_SOCK:
> > > -       case PTR_TO_XDP_SOCK:
> > > -               /* Only valid matches are exact, which memcmp() above
> > > -                * would have accepted
> > > +       case PTR_TO_STACK:
> > > +               /* two stack pointers are equal only if they're pointing to
> > > +                * the same stack frame, since fp-8 in foo != fp-8 in bar
> > >                  */
> > > +               return equal && rold->frameno == rcur->frameno;
> > >         default:
> > > -               /* Don't know what's going on, just say it's not safe */
> > > -               return false;
> > > +               /* Only valid matches are exact, which memcmp() */
> > > +               return equal;
> >
> > Is it safe to assume this for any possible register type? Wouldn't
> > register types that use id and/or ref_obj_id need extra checks here? I
> > think preexisting default was a safer approach, in which if we forgot
> > to explicitly add support for some new or updated register type, the
> > worst thing is that for that *new* register we'd have suboptimal
> > verification performance, but not safety concerns.
>
> Well, I don't think that this commit changes regsafe() behavior in
> this regard. Here is how the code was structured before this commit:
>
> static bool regsafe(struct bpf_verifier_env *env, struct bpf_reg_state *rold,
>                     struct bpf_reg_state *rcur, struct bpf_id_pair *idmap)
> {
>         bool equal;
>
>         if (!(rold->live & REG_LIVE_READ))
>                 return true;
>         equal = memcmp(rold, rcur, offsetof(struct bpf_reg_state, parent)) == 0;
>         if (rold->type == PTR_TO_STACK)
>                 return equal && rold->frameno == rcur->frameno;
> --->    if (equal)
>                 return true;
>         if (rold->type == NOT_INIT)
>                 return true;
>         if (rcur->type == NOT_INIT)
>                 return false;
>         switch (base_type(rold->type)) {
>         case SCALAR_VALUE:
>                 ... it's own logic, always returns ...
>         case PTR_TO_MAP_KEY:
>         case PTR_TO_MAP_VALUE:
>                 ... it's own logic, always returns ...
>         case PTR_TO_PACKET_META:
>         case PTR_TO_PACKET:
>                 ... it's own logic, always returns ...
>         case PTR_TO_CTX:
>         case CONST_PTR_TO_MAP:
>         case PTR_TO_PACKET_END:
>         case PTR_TO_FLOW_KEYS:
>         case PTR_TO_SOCKET:
>         case PTR_TO_SOCK_COMMON:
>         case PTR_TO_TCP_SOCK:
>         case PTR_TO_XDP_SOCK:
>         default:
>                 return false;
>         }
>
>         /* Shouldn't get here; if we do, say it's not safe */
>         WARN_ON_ONCE(1);
>         return false;
> }
>
> So the "safe if byte-to-byte equal" behavior was present already.
> I can add an explicit list of types to the "return equal;" branch
> and add a default "return false;" branch if you think that it is
> more fool-proof.

Sorry, I didn't claim you made it worse. But given we are refactoring
this piece of code, let's make it more "safe-by-default".

So yeah, I think an explicit list of all the recognized register types
would be better, IMO.


>
> >
> >
> > >         }
> > >
> > >         /* Shouldn't get here; if we do, say it's not safe */
> > > --
> > > 2.34.1
> > >
>
diff mbox series

Patch

diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 3194e9d9e4e4..d05c5d0344c6 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -12926,15 +12926,6 @@  static bool regsafe(struct bpf_verifier_env *env, struct bpf_reg_state *rold,
 
 	equal = memcmp(rold, rcur, offsetof(struct bpf_reg_state, parent)) == 0;
 
-	if (rold->type == PTR_TO_STACK)
-		/* two stack pointers are equal only if they're pointing to
-		 * the same stack frame, since fp-8 in foo != fp-8 in bar
-		 */
-		return equal && rold->frameno == rcur->frameno;
-
-	if (equal)
-		return true;
-
 	if (rold->type == NOT_INIT)
 		/* explored state can't have used this */
 		return true;
@@ -12942,6 +12933,8 @@  static bool regsafe(struct bpf_verifier_env *env, struct bpf_reg_state *rold,
 		return false;
 	switch (base_type(rold->type)) {
 	case SCALAR_VALUE:
+		if (equal)
+			return true;
 		if (env->explore_alu_limits)
 			return false;
 		if (rcur->type == SCALAR_VALUE) {
@@ -13012,20 +13005,14 @@  static bool regsafe(struct bpf_verifier_env *env, struct bpf_reg_state *rold,
 		/* new val must satisfy old val knowledge */
 		return range_within(rold, rcur) &&
 		       tnum_in(rold->var_off, rcur->var_off);
-	case PTR_TO_CTX:
-	case CONST_PTR_TO_MAP:
-	case PTR_TO_PACKET_END:
-	case PTR_TO_FLOW_KEYS:
-	case PTR_TO_SOCKET:
-	case PTR_TO_SOCK_COMMON:
-	case PTR_TO_TCP_SOCK:
-	case PTR_TO_XDP_SOCK:
-		/* Only valid matches are exact, which memcmp() above
-		 * would have accepted
+	case PTR_TO_STACK:
+		/* two stack pointers are equal only if they're pointing to
+		 * the same stack frame, since fp-8 in foo != fp-8 in bar
 		 */
+		return equal && rold->frameno == rcur->frameno;
 	default:
-		/* Don't know what's going on, just say it's not safe */
-		return false;
+		/* Only valid matches are exact, which memcmp() */
+		return equal;
 	}
 
 	/* Shouldn't get here; if we do, say it's not safe */