bpf.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
* Error validating array access
@ 2022-04-06 19:51 Nikolay Borisov
  2022-04-08 22:58 ` Andrii Nakryiko
  0 siblings, 1 reply; 8+ messages in thread
From: Nikolay Borisov @ 2022-04-06 19:51 UTC (permalink / raw)
  To: bpf

Hello,

I get a validator error with the following function:

#define MAX_PERCPU_BUFSIZE (1<<15)
#define PATH_MAX 4096
#define MAX_PATH_COMPONENTS 20
#define IDX(x) ((x) & (MAX_PERCPU_BUFSIZE-1))
                                                                                 
struct buf_t {
     u8 buf[MAX_PERCPU_BUFSIZE];
};
                                                                                                                                                                 
struct {
     __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
     __uint(max_entries, 1);
     __type(key, u32);
     __type(value, struct buf_t);
} buf_map SEC(".maps");
                                                         
static __always_inline u32 get_dentry_path_str(struct dentry* dentry,
         struct buf_t *string_p)
{
     const char slash = '/';
     const int zero = 0;
                                                                                 
     u32 buf_off = MAX_PERCPU_BUFSIZE - 1;
                                                                                 
     #pragma unroll
     for (int i = 0; i < MAX_PATH_COMPONENTS; i++) {
         struct dentry *d_parent = BPF_CORE_READ(dentry, d_parent);
         if (dentry == d_parent) {
             break;
         }
         // Add this dentry name to path
         struct qstr d_name = BPF_CORE_READ(dentry, d_name);
         // Ensure path is no longer than PATH_MAX-1 and copy the terminating NULL
         unsigned int len = (d_name.len+1) & (PATH_MAX-1);
         // Start writing from the end of the buffer
         unsigned int off = buf_off - len;
         // Is string buffer big enough for dentry name?
         int sz = 0;
         // verify no wrap occurred
         if (off <= buf_off)
             sz = bpf_probe_read_kernel_str(&string_p->buf[IDX(off)], len, (void *)d_name.name);
         else
             break;
                                                                                 
         if (sz > 1) {
             buf_off -= 1; // replace null byte termination with slash sign
             bpf_probe_read(&(string_p->buf[IDX(buf_off)]), 1, &slash);
             buf_off -= sz - 1;
         } else {
             // If sz is 0 or 1 we have an error (path can't be null nor an empty string)
             break;
         }
         dentry = d_parent;
     }
                                                                                 
     // Add leading slash
     buf_off -= 1;
     bpf_probe_read(&(string_p->buf[IDX(buf_off)]), 1, &slash);
     // Null terminate the path string, this replaces the final / with a null
     // char
     bpf_probe_read(&(string_p->buf[MAX_PERCPU_BUFSIZE-1]), 1, &zero);
     return buf_off;
}

Here are the last couple of instructions where off is being calculated.

; unsigned int len = (d_name.len+1) & (PATH_MAX-1);
54: (57) r9 &= 4095                   ; R9_w=inv(id=0,umax_value=4095,var_off=(0x0; 0xfff))
; unsigned int off = buf_off - len;
55: (bf) r8 = r9                      ; R8_w=inv(id=4,umax_value=4095,var_off=(0x0; 0xfff)) R9_w=inv(id=4,umax_value=4095,var_off=(0x0; 0xfff))
56: (a7) r8 ^= 32767                  ; R8_w=inv(id=0,umin_value=28672,umax_value=32767,var_off=(0x7000; 0xfff))
; sz = bpf_probe_read_kernel_str(&string_p->buf[IDX(off)], len, (void *)d_name.name);
57: (79) r6 = *(u64 *)(r10 -72)       ; R6_w=map_value(id=0,off=0,ks=4,vs=32768,imm=0) R10=fp0
58: (0f) r6 += r8                     ; R6_w=map_value(id=0,off=0,ks=4,vs=32768,umin_value=28672,umax_value=32767,var_off=(0x7000; 0xfff)) R8_w=invP(id=0,umin_value=28672,umax_value=32767,var_off=(0x7000; 0xfff))
; sz = bpf_probe_read_kernel_str(&string_p->buf[IDX(off)], len, (void *)d_name.name);
59: (79) r3 = *(u64 *)(r1 +8)         ; R1_w=fp-24 R3_w=inv(id=0)
; sz = bpf_probe_read_kernel_str(&string_p->buf[IDX(off)], len, (void *)d_name.name);
60: (bf) r1 = r6                      ; R1_w=map_value(id=0,off=0,ks=4,vs=32768,umin_value=28672,umax_value=32767,var_off=(0x7000; 0xfff)) R6_w=map_value(id=0,off=0,ks=4,vs=32768,umin_value=28672,umax_value=32767,var_off=(0x7000; 0xfff))
61: (bf) r2 = r9                      ; R2_w=inv(id=4,umax_value=4095,var_off=(0x0; 0xfff)) R9_w=inv(id=4,umax_value=4095,var_off=(0x0; 0xfff))
62: (85) call bpf_probe_read_kernel_str#115
invalid access to map value, value_size=32768 off=32767 size=4095
R1 max value is outside of the allowed memory range
       

 From what I gathered it seems that in the bpf_probe_read_kernel_str the validator is not
able to prove that off+len is always going to be at most MAX_PERCPU_BUFSIZE - 1 which is well within
the bounds of the buffer. My logic goes as following:

buf_off is first set to 32767, we get the first dentry and calculate its len + null terminating char which is going to be at most
4095, afterwards 'off' is being calculated as buf_off - len and finally access to the percpu array is performed via  IDX(off) which guarantees the access is
bounded by MAX_PERCPU_BUFSIZE - 1.

This code was originally based off https://github.com/aquasecurity/tracee/blob/main/pkg/ebpf/c/tracee.bpf.c#L1721-L1777 however it seems
that the way tracee author work around this is to simply start from the middle of the buffer, i.e set buf_off initially to MAX_PERCPU_BUFSIZE>>1 and adjust the
array accesses accordingly when doing the masking.

^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: Error validating array access
  2022-04-06 19:51 Error validating array access Nikolay Borisov
@ 2022-04-08 22:58 ` Andrii Nakryiko
  2022-04-09  6:27   ` Nikolay Borisov
  0 siblings, 1 reply; 8+ messages in thread
From: Andrii Nakryiko @ 2022-04-08 22:58 UTC (permalink / raw)
  To: Nikolay Borisov; +Cc: bpf

On Wed, Apr 6, 2022 at 5:12 PM Nikolay Borisov <nborisov@suse.com> wrote:
>
> Hello,
>
> I get a validator error with the following function:
>
> #define MAX_PERCPU_BUFSIZE (1<<15)
> #define PATH_MAX 4096
> #define MAX_PATH_COMPONENTS 20
> #define IDX(x) ((x) & (MAX_PERCPU_BUFSIZE-1))
>
> struct buf_t {
>      u8 buf[MAX_PERCPU_BUFSIZE];
> };
>
> struct {
>      __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
>      __uint(max_entries, 1);
>      __type(key, u32);
>      __type(value, struct buf_t);
> } buf_map SEC(".maps");
>
> static __always_inline u32 get_dentry_path_str(struct dentry* dentry,
>          struct buf_t *string_p)
> {
>      const char slash = '/';
>      const int zero = 0;
>
>      u32 buf_off = MAX_PERCPU_BUFSIZE - 1;
>
>      #pragma unroll
>      for (int i = 0; i < MAX_PATH_COMPONENTS; i++) {
>          struct dentry *d_parent = BPF_CORE_READ(dentry, d_parent);
>          if (dentry == d_parent) {
>              break;
>          }
>          // Add this dentry name to path
>          struct qstr d_name = BPF_CORE_READ(dentry, d_name);
>          // Ensure path is no longer than PATH_MAX-1 and copy the terminating NULL
>          unsigned int len = (d_name.len+1) & (PATH_MAX-1);
>          // Start writing from the end of the buffer
>          unsigned int off = buf_off - len;
>          // Is string buffer big enough for dentry name?
>          int sz = 0;
>          // verify no wrap occurred
>          if (off <= buf_off)
>              sz = bpf_probe_read_kernel_str(&string_p->buf[IDX(off)], len, (void *)d_name.name);
>          else
>              break;
>
>          if (sz > 1) {
>              buf_off -= 1; // replace null byte termination with slash sign
>              bpf_probe_read(&(string_p->buf[IDX(buf_off)]), 1, &slash);
>              buf_off -= sz - 1;
>          } else {
>              // If sz is 0 or 1 we have an error (path can't be null nor an empty string)
>              break;
>          }
>          dentry = d_parent;
>      }
>
>      // Add leading slash
>      buf_off -= 1;
>      bpf_probe_read(&(string_p->buf[IDX(buf_off)]), 1, &slash);
>      // Null terminate the path string, this replaces the final / with a null
>      // char
>      bpf_probe_read(&(string_p->buf[MAX_PERCPU_BUFSIZE-1]), 1, &zero);
>      return buf_off;
> }
>
> Here are the last couple of instructions where off is being calculated.
>
> ; unsigned int len = (d_name.len+1) & (PATH_MAX-1);
> 54: (57) r9 &= 4095                   ; R9_w=inv(id=0,umax_value=4095,var_off=(0x0; 0xfff))
> ; unsigned int off = buf_off - len;
> 55: (bf) r8 = r9                      ; R8_w=inv(id=4,umax_value=4095,var_off=(0x0; 0xfff)) R9_w=inv(id=4,umax_value=4095,var_off=(0x0; 0xfff))
> 56: (a7) r8 ^= 32767                  ; R8_w=inv(id=0,umin_value=28672,umax_value=32767,var_off=(0x7000; 0xfff))
> ; sz = bpf_probe_read_kernel_str(&string_p->buf[IDX(off)], len, (void *)d_name.name);
> 57: (79) r6 = *(u64 *)(r10 -72)       ; R6_w=map_value(id=0,off=0,ks=4,vs=32768,imm=0) R10=fp0
> 58: (0f) r6 += r8                     ; R6_w=map_value(id=0,off=0,ks=4,vs=32768,umin_value=28672,umax_value=32767,var_off=(0x7000; 0xfff)) R8_w=invP(id=0,umin_value=28672,umax_value=32767,var_off=(0x7000; 0xfff))
> ; sz = bpf_probe_read_kernel_str(&string_p->buf[IDX(off)], len, (void *)d_name.name);
> 59: (79) r3 = *(u64 *)(r1 +8)         ; R1_w=fp-24 R3_w=inv(id=0)
> ; sz = bpf_probe_read_kernel_str(&string_p->buf[IDX(off)], len, (void *)d_name.name);
> 60: (bf) r1 = r6                      ; R1_w=map_value(id=0,off=0,ks=4,vs=32768,umin_value=28672,umax_value=32767,var_off=(0x7000; 0xfff)) R6_w=map_value(id=0,off=0,ks=4,vs=32768,umin_value=28672,umax_value=32767,var_off=(0x7000; 0xfff))
> 61: (bf) r2 = r9                      ; R2_w=inv(id=4,umax_value=4095,var_off=(0x0; 0xfff)) R9_w=inv(id=4,umax_value=4095,var_off=(0x0; 0xfff))
> 62: (85) call bpf_probe_read_kernel_str#115
> invalid access to map value, value_size=32768 off=32767 size=4095
> R1 max value is outside of the allowed memory range
>
>
>  From what I gathered it seems that in the bpf_probe_read_kernel_str the validator is not
> able to prove that off+len is always going to be at most MAX_PERCPU_BUFSIZE - 1 which is well within
> the bounds of the buffer. My logic goes as following:
>
> buf_off is first set to 32767, we get the first dentry and calculate its len + null terminating char which is going to be at most
> 4095, afterwards 'off' is being calculated as buf_off - len and finally access to the percpu array is performed via  IDX(off) which guarantees the access is
> bounded by MAX_PERCPU_BUFSIZE - 1.

IDX(off) is bounded, but verifier needs to be sure that `off + len`
never goes beyond map value. so you should make sure and prove off <=
MAX_PERCPU_BUFSIZE - PATH_MAX. Verifier actually caught a real bug for
you.

>
> This code was originally based off https://github.com/aquasecurity/tracee/blob/main/pkg/ebpf/c/tracee.bpf.c#L1721-L1777 however it seems
> that the way tracee author work around this is to simply start from the middle of the buffer, i.e set buf_off initially to MAX_PERCPU_BUFSIZE>>1 and adjust the
> array accesses accordingly when doing the masking.

^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: Error validating array access
  2022-04-08 22:58 ` Andrii Nakryiko
@ 2022-04-09  6:27   ` Nikolay Borisov
  2022-04-13  0:47     ` Zvi Effron
  0 siblings, 1 reply; 8+ messages in thread
From: Nikolay Borisov @ 2022-04-09  6:27 UTC (permalink / raw)
  To: Andrii Nakryiko; +Cc: bpf



On 9.04.22 г. 1:58 ч., Andrii Nakryiko wrote:
> On Wed, Apr 6, 2022 at 5:12 PM Nikolay Borisov <nborisov@suse.com> wrote:
>>
>> Hello,
>>
>> I get a validator error with the following function:
>>
>> #define MAX_PERCPU_BUFSIZE (1<<15)
>> #define PATH_MAX 4096
>> #define MAX_PATH_COMPONENTS 20
>> #define IDX(x) ((x) & (MAX_PERCPU_BUFSIZE-1))
>>
>> struct buf_t {
>>       u8 buf[MAX_PERCPU_BUFSIZE];
>> };
>>
>> struct {
>>       __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
>>       __uint(max_entries, 1);
>>       __type(key, u32);
>>       __type(value, struct buf_t);
>> } buf_map SEC(".maps");
>>
>> static __always_inline u32 get_dentry_path_str(struct dentry* dentry,
>>           struct buf_t *string_p)
>> {
>>       const char slash = '/';
>>       const int zero = 0;
>>
>>       u32 buf_off = MAX_PERCPU_BUFSIZE - 1;
>>
>>       #pragma unroll
>>       for (int i = 0; i < MAX_PATH_COMPONENTS; i++) {
>>           struct dentry *d_parent = BPF_CORE_READ(dentry, d_parent);
>>           if (dentry == d_parent) {
>>               break;
>>           }
>>           // Add this dentry name to path
>>           struct qstr d_name = BPF_CORE_READ(dentry, d_name);
>>           // Ensure path is no longer than PATH_MAX-1 and copy the terminating NULL
>>           unsigned int len = (d_name.len+1) & (PATH_MAX-1);
>>           // Start writing from the end of the buffer
>>           unsigned int off = buf_off - len;
>>           // Is string buffer big enough for dentry name?
>>           int sz = 0;
>>           // verify no wrap occurred
>>           if (off <= buf_off)
>>               sz = bpf_probe_read_kernel_str(&string_p->buf[IDX(off)], len, (void *)d_name.name);
>>           else
>>               break;
>>
>>           if (sz > 1) {
>>               buf_off -= 1; // replace null byte termination with slash sign
>>               bpf_probe_read(&(string_p->buf[IDX(buf_off)]), 1, &slash);
>>               buf_off -= sz - 1;
>>           } else {
>>               // If sz is 0 or 1 we have an error (path can't be null nor an empty string)
>>               break;
>>           }
>>           dentry = d_parent;
>>       }
>>
>>       // Add leading slash
>>       buf_off -= 1;
>>       bpf_probe_read(&(string_p->buf[IDX(buf_off)]), 1, &slash);
>>       // Null terminate the path string, this replaces the final / with a null
>>       // char
>>       bpf_probe_read(&(string_p->buf[MAX_PERCPU_BUFSIZE-1]), 1, &zero);
>>       return buf_off;
>> }
>>
>> Here are the last couple of instructions where off is being calculated.
>>
>> ; unsigned int len = (d_name.len+1) & (PATH_MAX-1);
>> 54: (57) r9 &= 4095                   ; R9_w=inv(id=0,umax_value=4095,var_off=(0x0; 0xfff))
>> ; unsigned int off = buf_off - len;
>> 55: (bf) r8 = r9                      ; R8_w=inv(id=4,umax_value=4095,var_off=(0x0; 0xfff)) R9_w=inv(id=4,umax_value=4095,var_off=(0x0; 0xfff))
>> 56: (a7) r8 ^= 32767                  ; R8_w=inv(id=0,umin_value=28672,umax_value=32767,var_off=(0x7000; 0xfff))
>> ; sz = bpf_probe_read_kernel_str(&string_p->buf[IDX(off)], len, (void *)d_name.name);
>> 57: (79) r6 = *(u64 *)(r10 -72)       ; R6_w=map_value(id=0,off=0,ks=4,vs=32768,imm=0) R10=fp0
>> 58: (0f) r6 += r8                     ; R6_w=map_value(id=0,off=0,ks=4,vs=32768,umin_value=28672,umax_value=32767,var_off=(0x7000; 0xfff)) R8_w=invP(id=0,umin_value=28672,umax_value=32767,var_off=(0x7000; 0xfff))
>> ; sz = bpf_probe_read_kernel_str(&string_p->buf[IDX(off)], len, (void *)d_name.name);
>> 59: (79) r3 = *(u64 *)(r1 +8)         ; R1_w=fp-24 R3_w=inv(id=0)
>> ; sz = bpf_probe_read_kernel_str(&string_p->buf[IDX(off)], len, (void *)d_name.name);
>> 60: (bf) r1 = r6                      ; R1_w=map_value(id=0,off=0,ks=4,vs=32768,umin_value=28672,umax_value=32767,var_off=(0x7000; 0xfff)) R6_w=map_value(id=0,off=0,ks=4,vs=32768,umin_value=28672,umax_value=32767,var_off=(0x7000; 0xfff))
>> 61: (bf) r2 = r9                      ; R2_w=inv(id=4,umax_value=4095,var_off=(0x0; 0xfff)) R9_w=inv(id=4,umax_value=4095,var_off=(0x0; 0xfff))
>> 62: (85) call bpf_probe_read_kernel_str#115
>> invalid access to map value, value_size=32768 off=32767 size=4095
>> R1 max value is outside of the allowed memory range
>>
>>
>>   From what I gathered it seems that in the bpf_probe_read_kernel_str the validator is not
>> able to prove that off+len is always going to be at most MAX_PERCPU_BUFSIZE - 1 which is well within
>> the bounds of the buffer. My logic goes as following:
>>
>> buf_off is first set to 32767, we get the first dentry and calculate its len + null terminating char which is going to be at most
>> 4095, afterwards 'off' is being calculated as buf_off - len and finally access to the percpu array is performed via  IDX(off) which guarantees the access is
>> bounded by MAX_PERCPU_BUFSIZE - 1.
> 
> IDX(off) is bounded, but verifier needs to be sure that `off + len`
> never goes beyond map value. so you should make sure and prove off <=
> MAX_PERCPU_BUFSIZE - PATH_MAX. Verifier actually caught a real bug for

But that is guaranteed since off = buff_off - len, and buf_off is 
guaranteed to be at most MAX_PERCPU_BUFSIZE -1, meaning off is in the 
worst case MAX_PERCPU_BUFSIZE - len  so the map value access can't go 
beyond the end ?

> you.
> 
>>
>> This code was originally based off https://github.com/aquasecurity/tracee/blob/main/pkg/ebpf/c/tracee.bpf.c#L1721-L1777 however it seems
>> that the way tracee author work around this is to simply start from the middle of the buffer, i.e set buf_off initially to MAX_PERCPU_BUFSIZE>>1 and adjust the
>> array accesses accordingly when doing the masking.
> 

^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: Error validating array access
  2022-04-09  6:27   ` Nikolay Borisov
@ 2022-04-13  0:47     ` Zvi Effron
  2022-04-13  7:08       ` Nikolay Borisov
  0 siblings, 1 reply; 8+ messages in thread
From: Zvi Effron @ 2022-04-13  0:47 UTC (permalink / raw)
  To: Nikolay Borisov; +Cc: Andrii Nakryiko, bpf

On Fri, Apr 8, 2022 at 11:29 PM Nikolay Borisov <nborisov@suse.com> wrote:
>
>
>
> On 9.04.22 г. 1:58 ч., Andrii Nakryiko wrote:
> > On Wed, Apr 6, 2022 at 5:12 PM Nikolay Borisov <nborisov@suse.com> wrote:
> >>
> >> Hello,
> >>
> >> I get a validator error with the following function:
> >>
> >> #define MAX_PERCPU_BUFSIZE (1<<15)
> >> #define PATH_MAX 4096
> >> #define MAX_PATH_COMPONENTS 20
> >> #define IDX(x) ((x) & (MAX_PERCPU_BUFSIZE-1))
> >>
> >> struct buf_t {
> >>       u8 buf[MAX_PERCPU_BUFSIZE];
> >> };
> >>
> >> struct {
> >>       __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
> >>       __uint(max_entries, 1);
> >>       __type(key, u32);
> >>       __type(value, struct buf_t);
> >> } buf_map SEC(".maps");
> >>
> >> static __always_inline u32 get_dentry_path_str(struct dentry* dentry,
> >>           struct buf_t *string_p)
> >> {
> >>       const char slash = '/';
> >>       const int zero = 0;
> >>
> >>       u32 buf_off = MAX_PERCPU_BUFSIZE - 1;
> >>
> >>       #pragma unroll
> >>       for (int i = 0; i < MAX_PATH_COMPONENTS; i++) {
> >>           struct dentry *d_parent = BPF_CORE_READ(dentry, d_parent);
> >>           if (dentry == d_parent) {
> >>               break;
> >>           }
> >>           // Add this dentry name to path
> >>           struct qstr d_name = BPF_CORE_READ(dentry, d_name);
> >>           // Ensure path is no longer than PATH_MAX-1 and copy the terminating NULL
> >>           unsigned int len = (d_name.len+1) & (PATH_MAX-1);
> >>           // Start writing from the end of the buffer
> >>           unsigned int off = buf_off - len;
> >>           // Is string buffer big enough for dentry name?
> >>           int sz = 0;
> >>           // verify no wrap occurred
> >>           if (off <= buf_off)
> >>               sz = bpf_probe_read_kernel_str(&string_p->buf[IDX(off)], len, (void *)d_name.name);
> >>           else
> >>               break;
> >>
> >>           if (sz > 1) {
> >>               buf_off -= 1; // replace null byte termination with slash sign
> >>               bpf_probe_read(&(string_p->buf[IDX(buf_off)]), 1, &slash);
> >>               buf_off -= sz - 1;

Isn't it (theoretically) possible for this to underflow? What happens if
sz > 1 and sz >= buf_off?

> >>           } else {
> >>               // If sz is 0 or 1 we have an error (path can't be null nor an empty string)
> >>               break;
> >>           }
> >>           dentry = d_parent;
> >>       }
> >>
> >>       // Add leading slash
> >>       buf_off -= 1;
> >>       bpf_probe_read(&(string_p->buf[IDX(buf_off)]), 1, &slash);
> >>       // Null terminate the path string, this replaces the final / with a null
> >>       // char
> >>       bpf_probe_read(&(string_p->buf[MAX_PERCPU_BUFSIZE-1]), 1, &zero);
> >>       return buf_off;
> >> }
> >>
> >> Here are the last couple of instructions where off is being calculated.
> >>
> >> ; unsigned int len = (d_name.len+1) & (PATH_MAX-1);
> >> 54: (57) r9 &= 4095                   ; R9_w=inv(id=0,umax_value=4095,var_off=(0x0; 0xfff))
> >> ; unsigned int off = buf_off - len;
> >> 55: (bf) r8 = r9                      ; R8_w=inv(id=4,umax_value=4095,var_off=(0x0; 0xfff)) R9_w=inv(id=4,umax_value=4095,var_off=(0x0; 0xfff))
> >> 56: (a7) r8 ^= 32767                  ; R8_w=inv(id=0,umin_value=28672,umax_value=32767,var_off=(0x7000; 0xfff))
> >> ; sz = bpf_probe_read_kernel_str(&string_p->buf[IDX(off)], len, (void *)d_name.name);
> >> 57: (79) r6 = *(u64 *)(r10 -72)       ; R6_w=map_value(id=0,off=0,ks=4,vs=32768,imm=0) R10=fp0
> >> 58: (0f) r6 += r8                     ; R6_w=map_value(id=0,off=0,ks=4,vs=32768,umin_value=28672,umax_value=32767,var_off=(0x7000; 0xfff)) R8_w=invP(id=0,umin_value=28672,umax_value=32767,var_off=(0x7000; 0xfff))
> >> ; sz = bpf_probe_read_kernel_str(&string_p->buf[IDX(off)], len, (void *)d_name.name);
> >> 59: (79) r3 = *(u64 *)(r1 +8)         ; R1_w=fp-24 R3_w=inv(id=0)
> >> ; sz = bpf_probe_read_kernel_str(&string_p->buf[IDX(off)], len, (void *)d_name.name);
> >> 60: (bf) r1 = r6                      ; R1_w=map_value(id=0,off=0,ks=4,vs=32768,umin_value=28672,umax_value=32767,var_off=(0x7000; 0xfff)) R6_w=map_value(id=0,off=0,ks=4,vs=32768,umin_value=28672,umax_value=32767,var_off=(0x7000; 0xfff))
> >> 61: (bf) r2 = r9                      ; R2_w=inv(id=4,umax_value=4095,var_off=(0x0; 0xfff)) R9_w=inv(id=4,umax_value=4095,var_off=(0x0; 0xfff))
> >> 62: (85) call bpf_probe_read_kernel_str#115
> >> invalid access to map value, value_size=32768 off=32767 size=4095
> >> R1 max value is outside of the allowed memory range
> >>
> >>
> >>   From what I gathered it seems that in the bpf_probe_read_kernel_str the validator is not
> >> able to prove that off+len is always going to be at most MAX_PERCPU_BUFSIZE - 1 which is well within
> >> the bounds of the buffer. My logic goes as following:
> >>
> >> buf_off is first set to 32767, we get the first dentry and calculate its len + null terminating char which is going to be at most
> >> 4095, afterwards 'off' is being calculated as buf_off - len and finally access to the percpu array is performed via  IDX(off) which guarantees the access is
> >> bounded by MAX_PERCPU_BUFSIZE - 1.
> >
> > IDX(off) is bounded, but verifier needs to be sure that `off + len`
> > never goes beyond map value. so you should make sure and prove off <=
> > MAX_PERCPU_BUFSIZE - PATH_MAX. Verifier actually caught a real bug for
>
> But that is guaranteed since off = buff_off - len, and buf_off is
> guaranteed to be at most MAX_PERCPU_BUFSIZE -1, meaning off is in the
> worst case MAX_PERCPU_BUFSIZE - len  so the map value access can't go
> beyond the end ?
>

If there's underflow in the calculation of buff_off (see above) then
buff_off > MAX_PERCPU_BUFSIZE - 1. Which makes off no longer bounded by
MAX_PERCPU_BUFSIZE - len, and it could exceed MAX_PERCPU_BUFSIZE.

> > you.
> >
> >>
> >> This code was originally based off https://github.com/aquasecurity/tracee/blob/main/pkg/ebpf/c/tracee.bpf.c#L1721-L1777 however it seems
> >> that the way tracee author work around this is to simply start from the middle of the buffer, i.e set buf_off initially to MAX_PERCPU_BUFSIZE>>1 and adjust the
> >> array accesses accordingly when doing the masking.
> >

^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: Error validating array access
  2022-04-13  0:47     ` Zvi Effron
@ 2022-04-13  7:08       ` Nikolay Borisov
  2022-04-13 17:29         ` Zvi Effron
  0 siblings, 1 reply; 8+ messages in thread
From: Nikolay Borisov @ 2022-04-13  7:08 UTC (permalink / raw)
  To: Zvi Effron; +Cc: Andrii Nakryiko, bpf

<snip>
>>>>            // Add this dentry name to path
>>>>            struct qstr d_name = BPF_CORE_READ(dentry, d_name);
>>>>            // Ensure path is no longer than PATH_MAX-1 and copy the terminating NULL
>>>>            unsigned int len = (d_name.len+1) & (PATH_MAX-1);
>>>>            // Start writing from the end of the buffer
>>>>            unsigned int off = buf_off - len;
>>>>            // Is string buffer big enough for dentry name?
>>>>            int sz = 0;
>>>>            // verify no wrap occurred
>>>>            if (off <= buf_off)
>>>>                sz = bpf_probe_read_kernel_str(&string_p->buf[IDX(off)], len, (void *)d_name.name);
>>>>            else
>>>>                break;
>>>>
>>>>            if (sz > 1) {
>>>>                buf_off -= 1; // replace null byte termination with slash sign
>>>>                bpf_probe_read(&(string_p->buf[IDX(buf_off)]), 1, &slash);
>>>>                buf_off -= sz - 1;
> 
> Isn't it (theoretically) possible for this to underflow? What happens if
> sz > 1 and sz >= buf_off?

No, because sz is bounded by len since bpf_probe_read_kernel_str would 
copy at most len -1 bytes as per description of the function. Since 
we've ensured len is smaller than buff_off (due to off <= buf_off check) 
then sz is also guaranteed to be less than buf_off.

<snip>

>>> IDX(off) is bounded, but verifier needs to be sure that `off + len`
>>> never goes beyond map value. so you should make sure and prove off <=
>>> MAX_PERCPU_BUFSIZE - PATH_MAX. Verifier actually caught a real bug for
>>
>> But that is guaranteed since off = buff_off - len, and buf_off is
>> guaranteed to be at most MAX_PERCPU_BUFSIZE -1, meaning off is in the
>> worst case MAX_PERCPU_BUFSIZE - len  so the map value access can't go
>> beyond the end ?
>>
> 
> If there's underflow in the calculation of buff_off (see above) then
> buff_off > MAX_PERCPU_BUFSIZE - 1. Which makes off no longer bounded by
> MAX_PERCPU_BUFSIZE - len, and it could exceed MAX_PERCPU_BUFSIZE.

As per my rationale above I don't think buf_off can underflow.

> 

<snip>

^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: Error validating array access
  2022-04-13  7:08       ` Nikolay Borisov
@ 2022-04-13 17:29         ` Zvi Effron
  2022-04-13 19:04           ` Nikolay Borisov
  0 siblings, 1 reply; 8+ messages in thread
From: Zvi Effron @ 2022-04-13 17:29 UTC (permalink / raw)
  To: Nikolay Borisov; +Cc: Andrii Nakryiko, bpf

On Wed, Apr 13, 2022 at 12:08 AM Nikolay Borisov <nborisov@suse.com> wrote:
>
> <snip>
> >>>>            // Add this dentry name to path
> >>>>            struct qstr d_name = BPF_CORE_READ(dentry, d_name);
> >>>>            // Ensure path is no longer than PATH_MAX-1 and copy the terminating NULL
> >>>>            unsigned int len = (d_name.len+1) & (PATH_MAX-1);
> >>>>            // Start writing from the end of the buffer
> >>>>            unsigned int off = buf_off - len;
> >>>>            // Is string buffer big enough for dentry name?
> >>>>            int sz = 0;
> >>>>            // verify no wrap occurred
> >>>>            if (off <= buf_off)
> >>>>                sz = bpf_probe_read_kernel_str(&string_p->buf[IDX(off)], len, (void *)d_name.name);
> >>>>            else
> >>>>                break;
> >>>>
> >>>>            if (sz > 1) {
> >>>>                buf_off -= 1; // replace null byte termination with slash sign
> >>>>                bpf_probe_read(&(string_p->buf[IDX(buf_off)]), 1, &slash);
> >>>>                buf_off -= sz - 1;
> >
> > Isn't it (theoretically) possible for this to underflow? What happens if
> > sz > 1 and sz >= buf_off?
>
> No, because sz is bounded by len since bpf_probe_read_kernel_str would
> copy at most len -1 bytes as per description of the function. Since
> we've ensured len is smaller than buff_off (due to off <= buf_off check)
> then sz is also guaranteed to be less than buf_off.
>
> <snip>
>

That's in a single iteration, though. Each iteration, sz can be 4095 (when
len = PATH_MAX - 1). buff_off can be reduced by up to 4095 (1 + sz - 1). Your
loop allows 20 iterations, which would be a total adjustment to buff_off of
77,786 before the last iteration. This would cause buff_off to underflow (it
starts at 32767).

> >>> IDX(off) is bounded, but verifier needs to be sure that `off + len`
> >>> never goes beyond map value. so you should make sure and prove off <=
> >>> MAX_PERCPU_BUFSIZE - PATH_MAX. Verifier actually caught a real bug for
> >>
> >> But that is guaranteed since off = buff_off - len, and buf_off is
> >> guaranteed to be at most MAX_PERCPU_BUFSIZE -1, meaning off is in the
> >> worst case MAX_PERCPU_BUFSIZE - len  so the map value access can't go
> >> beyond the end ?
> >>
> >
> > If there's underflow in the calculation of buff_off (see above) then
> > buff_off > MAX_PERCPU_BUFSIZE - 1. Which makes off no longer bounded by
> > MAX_PERCPU_BUFSIZE - len, and it could exceed MAX_PERCPU_BUFSIZE.
>
> As per my rationale above I don't think buf_off can underflow.
>
> >
>
> <snip>

^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: Error validating array access
  2022-04-13 17:29         ` Zvi Effron
@ 2022-04-13 19:04           ` Nikolay Borisov
  2022-04-13 19:23             ` Zvi Effron
  0 siblings, 1 reply; 8+ messages in thread
From: Nikolay Borisov @ 2022-04-13 19:04 UTC (permalink / raw)
  To: Zvi Effron; +Cc: Andrii Nakryiko, bpf



On 13.04.22 г. 20:29 ч., Zvi Effron wrote:
> On Wed, Apr 13, 2022 at 12:08 AM Nikolay Borisov <nborisov@suse.com> wrote:
>>
>> <snip>
>>>>>>             // Add this dentry name to path
>>>>>>             struct qstr d_name = BPF_CORE_READ(dentry, d_name);
>>>>>>             // Ensure path is no longer than PATH_MAX-1 and copy the terminating NULL
>>>>>>             unsigned int len = (d_name.len+1) & (PATH_MAX-1);
>>>>>>             // Start writing from the end of the buffer
>>>>>>             unsigned int off = buf_off - len;
>>>>>>             // Is string buffer big enough for dentry name?
>>>>>>             int sz = 0;
>>>>>>             // verify no wrap occurred
>>>>>>             if (off <= buf_off)
>>>>>>                 sz = bpf_probe_read_kernel_str(&string_p->buf[IDX(off)], len, (void *)d_name.name);
>>>>>>             else
>>>>>>                 break;
>>>>>>
>>>>>>             if (sz > 1) {
>>>>>>                 buf_off -= 1; // replace null byte termination with slash sign
>>>>>>                 bpf_probe_read(&(string_p->buf[IDX(buf_off)]), 1, &slash);
>>>>>>                 buf_off -= sz - 1;
>>>
>>> Isn't it (theoretically) possible for this to underflow? What happens if
>>> sz > 1 and sz >= buf_off?
>>
>> No, because sz is bounded by len since bpf_probe_read_kernel_str would
>> copy at most len -1 bytes as per description of the function. Since
>> we've ensured len is smaller than buff_off (due to off <= buf_off check)
>> then sz is also guaranteed to be less than buf_off.
>>
>> <snip>
>>
> 
> That's in a single iteration, though. Each iteration, sz can be 4095 (when
> len = PATH_MAX - 1). buff_off can be reduced by up to 4095 (1 + sz - 1). Your
> loop allows 20 iterations, which would be a total adjustment to buff_off of
> 77,786 before the last iteration. This would cause buff_off to underflow (it
> starts at 32767).

But in the last iteration it would result in an underflow which means 
we'd go into the else arm and break.

> 
>>>>> IDX(off) is bounded, but verifier needs to be sure that `off + len`
>>>>> never goes beyond map value. so you should make sure and prove off <=
>>>>> MAX_PERCPU_BUFSIZE - PATH_MAX. Verifier actually caught a real bug for
>>>>
>>>> But that is guaranteed since off = buff_off - len, and buf_off is
>>>> guaranteed to be at most MAX_PERCPU_BUFSIZE -1, meaning off is in the
>>>> worst case MAX_PERCPU_BUFSIZE - len  so the map value access can't go
>>>> beyond the end ?
>>>>
>>>
>>> If there's underflow in the calculation of buff_off (see above) then
>>> buff_off > MAX_PERCPU_BUFSIZE - 1. Which makes off no longer bounded by
>>> MAX_PERCPU_BUFSIZE - len, and it could exceed MAX_PERCPU_BUFSIZE.
>>
>> As per my rationale above I don't think buf_off can underflow.
>>
>>>
>>
>> <snip>
> 

^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: Error validating array access
  2022-04-13 19:04           ` Nikolay Borisov
@ 2022-04-13 19:23             ` Zvi Effron
  0 siblings, 0 replies; 8+ messages in thread
From: Zvi Effron @ 2022-04-13 19:23 UTC (permalink / raw)
  To: Nikolay Borisov; +Cc: Andrii Nakryiko, bpf

On Wed, Apr 13, 2022 at 12:04 PM Nikolay Borisov <nborisov@suse.com> wrote:
>
>
>
> On 13.04.22 г. 20:29 ч., Zvi Effron wrote:
> > On Wed, Apr 13, 2022 at 12:08 AM Nikolay Borisov <nborisov@suse.com> wrote:
> >>
> >> <snip>
> >>>>>> // Add this dentry name to path
> >>>>>> struct qstr d_name = BPF_CORE_READ(dentry, d_name);
> >>>>>> // Ensure path is no longer than PATH_MAX-1 and copy the terminating NULL
> >>>>>> unsigned int len = (d_name.len+1) & (PATH_MAX-1);
> >>>>>> // Start writing from the end of the buffer
> >>>>>> unsigned int off = buf_off - len;
> >>>>>> // Is string buffer big enough for dentry name?
> >>>>>> int sz = 0;
> >>>>>> // verify no wrap occurred
> >>>>>> if (off <= buf_off)
> >>>>>> sz = bpf_probe_read_kernel_str(&string_p->buf[IDX(off)], len, (void *)d_name.name);
> >>>>>> else
> >>>>>> break;
> >>>>>>
> >>>>>> if (sz > 1) {
> >>>>>> buf_off -= 1; // replace null byte termination with slash sign
> >>>>>> bpf_probe_read(&(string_p->buf[IDX(buf_off)]), 1, &slash);
> >>>>>> buf_off -= sz - 1;
> >>>
> >>> Isn't it (theoretically) possible for this to underflow? What happens if
> >>> sz > 1 and sz >= buf_off?
> >>
> >> No, because sz is bounded by len since bpf_probe_read_kernel_str would
> >> copy at most len -1 bytes as per description of the function. Since
> >> we've ensured len is smaller than buff_off (due to off <= buf_off check)
> >> then sz is also guaranteed to be less than buf_off.
> >>
> >> <snip>
> >>
> >
> > That's in a single iteration, though. Each iteration, sz can be 4095 (when
> > len = PATH_MAX - 1). buff_off can be reduced by up to 4095 (1 + sz - 1). Your
> > loop allows 20 iterations, which would be a total adjustment to buff_off of
> > 77,786 before the last iteration. This would cause buff_off to underflow (it
> > starts at 32767).
>
> But in the last iteration it would result in an underflow which means
> we'd go into the else arm and break.
>

You might be in a case where the verifier does not track what's required to
prove correctness here. In order to prove correctness, relations between
len, sz, off, and buff_off must be tracked. The verifier does not track
relations between variables, just current state of registers.

You can see this when looking at how the verifier is validating via its output.

When it validates IDX(off), all it knows is the register being used has a
minimum value of 28672 and a maximum value of 32767. Similarly, it knows that
len has a maximum value of 4095. It does not know about the relation between
len and off. So when it compares off to buf, it sees that the maximum value of
IDX(off) (32767) plus the maximum value of len (4095) can exceed the size of
buf (32768).

To prove this isn't possible, it would need to know that as IDX(off) varies
upwards, len varies downwards by the same amount. But the verifier does not
track relations between variables (in fact, it doesn't even know about
variables) so it does not know this.

> >
> >>>>> IDX(off) is bounded, but verifier needs to be sure that `off + len`
> >>>>> never goes beyond map value. so you should make sure and prove off <=
> >>>>> MAX_PERCPU_BUFSIZE - PATH_MAX. Verifier actually caught a real bug for
> >>>>
> >>>> But that is guaranteed since off = buff_off - len, and buf_off is
> >>>> guaranteed to be at most MAX_PERCPU_BUFSIZE -1, meaning off is in the
> >>>> worst case MAX_PERCPU_BUFSIZE - len so the map value access can't go
> >>>> beyond the end ?
> >>>>
> >>>
> >>> If there's underflow in the calculation of buff_off (see above) then
> >>> buff_off > MAX_PERCPU_BUFSIZE - 1. Which makes off no longer bounded by
> >>> MAX_PERCPU_BUFSIZE - len, and it could exceed MAX_PERCPU_BUFSIZE.
> >>
> >> As per my rationale above I don't think buf_off can underflow.
> >>
> >>>
> >>
> >> <snip>
> >

^ permalink raw reply	[flat|nested] 8+ messages in thread

end of thread, other threads:[~2022-04-13 19:23 UTC | newest]

Thread overview: 8+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2022-04-06 19:51 Error validating array access Nikolay Borisov
2022-04-08 22:58 ` Andrii Nakryiko
2022-04-09  6:27   ` Nikolay Borisov
2022-04-13  0:47     ` Zvi Effron
2022-04-13  7:08       ` Nikolay Borisov
2022-04-13 17:29         ` Zvi Effron
2022-04-13 19:04           ` Nikolay Borisov
2022-04-13 19:23             ` Zvi Effron

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).