Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ttysnoop fails on newer kernel #4884

Open
jeromemarchand opened this issue Jan 24, 2024 · 3 comments
Open

ttysnoop fails on newer kernel #4884

jeromemarchand opened this issue Jan 24, 2024 · 3 comments

Comments

@jeromemarchand
Copy link
Contributor

ttysnoop fails on recent kernel.

bcc v0.29.1
kernel 6.8.0-rc0
libbpf 1.2.0

# uname -r
6.8.0-0.rc0.20240112git70d201a40823.5.fc40.x86_64
# /usr/share/bcc/tools/ttysnoop /dev/tty0
bpf: Failed to load program: Permission denied
Arg#0 type PTR in kfunc__vmlinux__tty_write() is not supported yet.
0: R1=ctx() R10=fp0
; KFUNC_PROBE(tty_write, struct kiocb *iocb, struct iov_iter *from)
0: (79) r2 = *(u64 *)(r1 +0)
func 'tty_write' arg0 has btf_id 797 type STRUCT 'kiocb'
1: R1=ctx() R2_w=ptr_kiocb()
; if (iocb->ki_filp->f_inode->i_ino != 13)
1: (79) r2 = *(u64 *)(r2 +0)          ; R2_w=ptr_file()
; if (iocb->ki_filp->f_inode->i_ino != 13)
2: (79) r2 = *(u64 *)(r2 +168)        ; R2_w=ptr_inode()
; if (iocb->ki_filp->f_inode->i_ino != 13)
3: (79) r2 = *(u64 *)(r2 +64)         ; R2_w=scalar()
; if (iocb->ki_filp->f_inode->i_ino != 13)
4: (55) if r2 != 0xd goto pc+395      ; R2_w=13
; KFUNC_PROBE(tty_write, struct kiocb *iocb, struct iov_iter *from)
5: (79) r1 = *(u64 *)(r1 +8)
func 'tty_write' arg1 has btf_id 303 type STRUCT 'iov_iter'
6: R1_w=ptr_iov_iter()
; if (from->iter_type != ITER_UBUF && from->iter_type != ITER_IOVEC)
6: (71) r2 = *(u8 *)(r1 +0)           ; R1_w=ptr_iov_iter() R2_w=scalar(smin=smin32=0,smax=umax=smax32=umax32=255,var_off=(0x0; 0xff))
; if (from->iter_type != ITER_UBUF && from->iter_type != ITER_IOVEC)
7: (25) if r2 > 0x1 goto pc+392       ; R2_w=scalar(smin=smin32=0,smax=umax=smax32=umax32=1,var_off=(0x0; 0x1))
; if (from->data_source != WRITE)
8: (71) r3 = *(u8 *)(r1 +3)           ; R1=ptr_iov_iter() R3=scalar(smin=smin32=0,smax=umax=smax32=umax32=255,var_off=(0x0; 0xff))
; if (from->data_source != WRITE)
9: (15) if r3 == 0x0 goto pc+390      ; R3=scalar(smin=umin=smin32=umin32=1,smax=umax=smax32=umax32=255,var_off=(0x0; 0xff))
; switch (from->iter_type) {
10: (15) if r2 == 0x0 goto pc+8       ; R2=1
11: (b7) r7 = 0                       ; R7_w=0
12: (b7) r3 = 0                       ; R3_w=0
13: (7b) *(u64 *)(r10 -16) = r3       ; R3_w=0 R10=fp0 fp-16_w=0
14: (55) if r2 != 0x1 goto pc+10      ; R2=1
; kvec  = from->kvec;
15: (79) r1 = *(u64 *)(r1 +16)        ; R1_w=scalar()
; count = kvec->iov_len;
16: (bf) r2 = r1                      ; R1_w=scalar(id=1) R2_w=scalar(id=1)
17: (07) r2 += 8                      ; R2_w=scalar()
18: (05) goto pc+3
; 
22: (79) r2 = *(u64 *)(r2 +0)
R2 invalid mem access 'scalar'
processed 20 insns (limit 1000000) max_states_per_insn 0 total_states 2 peak_states 2 mark_read 1

Traceback (most recent call last):
  File "/usr/share/bcc/tools/ttysnoop", line 236, in <module>
    b = BPF(text=bpf_text)
        ^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/bcc/__init__.py", line 487, in __init__
    self._trace_autoload()
  File "/usr/lib/python3.12/site-packages/bcc/__init__.py", line 1483, in _trace_autoload
    self.attach_kfunc(fn_name=func_name)
  File "/usr/lib/python3.12/site-packages/bcc/__init__.py", line 1145, in attach_kfunc
    fn = self.load_func(fn_name, BPF.TRACING)
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/bcc/__init__.py", line 526, in load_func
    raise Exception("Failed to load BPF program %s: %s" %
Exception: Failed to load BPF program b'kfunc__vmlinux__tty_write': Permission denied

It seems related to the kernel commit 94e1c70a3452 ("bpf: support 'arg:xxx' btf_decl_tag-based hints for global subprog args") which added the failing check and error message in btf_prepare_func_args().

@jeromemarchand
Copy link
Contributor Author

It fails on x86_64, but not on s390x. I haven't tested other arches.

yonghong-song pushed a commit that referenced this issue Jan 30, 2024
Jerome Marchand reported that ttysnoop.py won't work properly
with newer kernels (#4884). I did some investigation and found
that some kernel data structure change caused verification failure.
The failure is caused by the following:
  ; kvec  = from->kvec;
  // R1=ptr_iov_iter()
  15: (79) r1 = *(u64 *)(r1 +16)        ; R1_w=scalar()
  ; count = kvec->iov_len;
  16: (bf) r2 = r1                      ; R1_w=scalar(id=1) R2_w=scalar(id=1)
  17: (07) r2 += 8                      ; R2_w=scalar()
  18: (05) goto pc+3
  ;
  22: (79) r2 = *(u64 *)(r2 +0)
  R2 invalid mem access 'scalar'

So basically, loading 'iov_iter + 16' returns a scalar but verifier
expects it to be a pointer.

In v6.4, we have
    struct iovec
    {
        void __user *iov_base;  /* BSD uses caddr_t (1003.1g requires void *) */
        __kernel_size_t iov_len; /* Must be size_t (1003.1g) */
    };
    struct iov_iter {
        u8 iter_type;
        bool copy_mc;
        bool nofault;
        bool data_source;
        bool user_backed;
        union {
                size_t iov_offset;
                int last_offset;
        };
        union {
                struct iovec __ubuf_iovec;
                struct {
                        union {
                                const struct iovec *__iov;
                                const struct kvec *kvec;
                                const struct bio_vec *bvec;
                                struct xarray *xarray;
                                struct pipe_inode_info *pipe;
                                void __user *ubuf;
                        };
                        size_t count;
                };
        };
        union {
                unsigned long nr_segs;
                struct {
                        unsigned int head;
                        unsigned int start_head;
                };
                loff_t xarray_start;
        };
    };

The kernel traversal chain will be
   "struct iov_iter" -> "struct iovec __ubuf_iovec" -> "void __user *iov_base".
Since the "iov_base" type is a ptr to void, the kernel considers the
loaded value as a scalar which caused verification failure.

But for old kernel like 5.19, we do not have this issue.
    struct iovec
    {
        void __user *iov_base;  /* BSD uses caddr_t (1003.1g requires void *) */
        __kernel_size_t iov_len; /* Must be size_t (1003.1g) */
    };
    struct iov_iter {
        u8 iter_type;
        bool nofault;
        bool data_source;
        bool user_backed;
        size_t iov_offset;
        size_t count;
        union {
                const struct iovec *iov;
                const struct kvec *kvec;
                const struct bio_vec *bvec;
                struct xarray *xarray;
                struct pipe_inode_info *pipe;
                void __user *ubuf;
        };
        union {
                unsigned long nr_segs;
                struct {
                        unsigned int head;
                        unsigned int start_head;
                };
                loff_t xarray_start;
        };
    };

The kernel traversal chain will be
    "struct iov_iter" -> "const struct iovec *iov"
Note that "const struct iovec *iov" is used since it is the *first* member
inside the union. The traversal stops once we hit a pointer.
So the kernel verifier returns a 'struct iovec' object (untrusted, cannot
be used as a parameter to a call) and verifier can proceed.

To fix the problem, let us use bpf_probe_read_kernel() instead
so ttysnoop.py can continue to work with newer kernel.

Signed-off-by: Yonghong Song <[email protected]>
@yonghong-song
Copy link
Collaborator

@jeromemarchand #4888 should fix the issue. Could you verify whether the issue is fixed in your environment or not?

yonghong-song pushed a commit that referenced this issue Jan 30, 2024
Jerome Marchand reported that ttysnoop.py won't work properly
with newer kernels (#4884). I did some investigation and found
that some kernel data structure change caused verification failure.
The failure is caused by the following:
  ; kvec  = from->kvec;
  // R1=ptr_iov_iter()
  15: (79) r1 = *(u64 *)(r1 +16)        ; R1_w=scalar()
  ; count = kvec->iov_len;
  16: (bf) r2 = r1                      ; R1_w=scalar(id=1) R2_w=scalar(id=1)
  17: (07) r2 += 8                      ; R2_w=scalar()
  18: (05) goto pc+3
  ;
  22: (79) r2 = *(u64 *)(r2 +0)
  R2 invalid mem access 'scalar'

So basically, loading 'iov_iter + 16' returns a scalar but verifier
expects it to be a pointer.

In v6.4, we have
    struct iovec
    {
        void __user *iov_base;  /* BSD uses caddr_t (1003.1g requires void *) */
        __kernel_size_t iov_len; /* Must be size_t (1003.1g) */
    };
    struct iov_iter {
        u8 iter_type;
        bool copy_mc;
        bool nofault;
        bool data_source;
        bool user_backed;
        union {
                size_t iov_offset;
                int last_offset;
        };
        union {
                struct iovec __ubuf_iovec;
                struct {
                        union {
                                const struct iovec *__iov;
                                const struct kvec *kvec;
                                const struct bio_vec *bvec;
                                struct xarray *xarray;
                                struct pipe_inode_info *pipe;
                                void __user *ubuf;
                        };
                        size_t count;
                };
        };
        union {
                unsigned long nr_segs;
                struct {
                        unsigned int head;
                        unsigned int start_head;
                };
                loff_t xarray_start;
        };
    };

The kernel traversal chain will be
   "struct iov_iter" -> "struct iovec __ubuf_iovec" -> "void __user *iov_base".
Since the "iov_base" type is a ptr to void, the kernel considers the
loaded value as a scalar which caused verification failure.

But for old kernel like 5.19, we do not have this issue.
    struct iovec
    {
        void __user *iov_base;  /* BSD uses caddr_t (1003.1g requires void *) */
        __kernel_size_t iov_len; /* Must be size_t (1003.1g) */
    };
    struct iov_iter {
        u8 iter_type;
        bool nofault;
        bool data_source;
        bool user_backed;
        size_t iov_offset;
        size_t count;
        union {
                const struct iovec *iov;
                const struct kvec *kvec;
                const struct bio_vec *bvec;
                struct xarray *xarray;
                struct pipe_inode_info *pipe;
                void __user *ubuf;
        };
        union {
                unsigned long nr_segs;
                struct {
                        unsigned int head;
                        unsigned int start_head;
                };
                loff_t xarray_start;
        };
    };

The kernel traversal chain will be
    "struct iov_iter" -> "const struct iovec *iov"
Note that "const struct iovec *iov" is used since it is the *first* member
inside the union. The traversal stops once we hit a pointer.
So the kernel verifier returns a 'struct iovec' object (untrusted, cannot
be used as a parameter to a call) and verifier can proceed.

To fix the problem, let us use bpf_probe_read_kernel() instead
so ttysnoop.py can continue to work with newer kernel.

Signed-off-by: Yonghong Song <[email protected]>
@jeromemarchand
Copy link
Contributor Author

Yes, it fixes the issue.

yonghong-song added a commit that referenced this issue Jan 30, 2024
Jerome Marchand reported that ttysnoop.py won't work properly
with newer kernels (#4884). I did some investigation and found
that some kernel data structure change caused verification failure.
The failure is caused by the following:
  ; kvec  = from->kvec;
  // R1=ptr_iov_iter()
  15: (79) r1 = *(u64 *)(r1 +16)        ; R1_w=scalar()
  ; count = kvec->iov_len;
  16: (bf) r2 = r1                      ; R1_w=scalar(id=1) R2_w=scalar(id=1)
  17: (07) r2 += 8                      ; R2_w=scalar()
  18: (05) goto pc+3
  ;
  22: (79) r2 = *(u64 *)(r2 +0)
  R2 invalid mem access 'scalar'

So basically, loading 'iov_iter + 16' returns a scalar but verifier
expects it to be a pointer.

In v6.4, we have
    struct iovec
    {
        void __user *iov_base;  /* BSD uses caddr_t (1003.1g requires void *) */
        __kernel_size_t iov_len; /* Must be size_t (1003.1g) */
    };
    struct iov_iter {
        u8 iter_type;
        bool copy_mc;
        bool nofault;
        bool data_source;
        bool user_backed;
        union {
                size_t iov_offset;
                int last_offset;
        };
        union {
                struct iovec __ubuf_iovec;
                struct {
                        union {
                                const struct iovec *__iov;
                                const struct kvec *kvec;
                                const struct bio_vec *bvec;
                                struct xarray *xarray;
                                struct pipe_inode_info *pipe;
                                void __user *ubuf;
                        };
                        size_t count;
                };
        };
        union {
                unsigned long nr_segs;
                struct {
                        unsigned int head;
                        unsigned int start_head;
                };
                loff_t xarray_start;
        };
    };

The kernel traversal chain will be
   "struct iov_iter" -> "struct iovec __ubuf_iovec" -> "void __user *iov_base".
Since the "iov_base" type is a ptr to void, the kernel considers the
loaded value as a scalar which caused verification failure.

But for old kernel like 5.19, we do not have this issue.
    struct iovec
    {
        void __user *iov_base;  /* BSD uses caddr_t (1003.1g requires void *) */
        __kernel_size_t iov_len; /* Must be size_t (1003.1g) */
    };
    struct iov_iter {
        u8 iter_type;
        bool nofault;
        bool data_source;
        bool user_backed;
        size_t iov_offset;
        size_t count;
        union {
                const struct iovec *iov;
                const struct kvec *kvec;
                const struct bio_vec *bvec;
                struct xarray *xarray;
                struct pipe_inode_info *pipe;
                void __user *ubuf;
        };
        union {
                unsigned long nr_segs;
                struct {
                        unsigned int head;
                        unsigned int start_head;
                };
                loff_t xarray_start;
        };
    };

The kernel traversal chain will be
    "struct iov_iter" -> "const struct iovec *iov"
Note that "const struct iovec *iov" is used since it is the *first* member
inside the union. The traversal stops once we hit a pointer.
So the kernel verifier returns a 'struct iovec' object (untrusted, cannot
be used as a parameter to a call) and verifier can proceed.

To fix the problem, let us use bpf_probe_read_kernel() instead
so ttysnoop.py can continue to work with newer kernel.

Signed-off-by: Yonghong Song <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants