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

Standardize error checking across libbpfgo #100

Merged

Conversation

grantseltzer
Copy link
Contributor

This handles correcting how we check errors in libbpfgo
in accordance with how libbpf recommends handling errors
leading into libbpf.

Particularly this follows the following rules:

  • APIs that return an error code (int) should have
    error codes checked directly. The error codes
    correspond with error codes in the syscall package.

    For example:

    errCodeInt := C.libbpf_api_function()
    if errCodeInt != 0 {
      log.Errorf("uh oh: %s\n", syscall.Errno(errCodeInt))
    }
    
  • APIs that return a pointer should be checked for
    NULL which indicates error. The error code is
    stored in errno. We can get the value of errno
    using the second return.

    For example:

    ptr, errno := C.libbpf_api_function()
    if ptr == nil {
      log.Errorf("uh oh: %s\n", errno)
    }
    

    We can also check if errno corresponds with a specific
    error (it implements the standard error interface).

    For example:

    ptr, errno := C.libbpf_api_function()
    if ptr == nil {
      if errno.Is(syscall.ENODENT) {
         // handle accordingly
      } else {
        log.Errorf("uh oh: %s\n", errno)
      }
    }
    
    

Signed-off-by: grantseltzer grantseltzer@gmail.com

libbpfgo.go Outdated
return fmt.Errorf("failed to load BPF object")
cErr := C.bpf_object__load(m.obj)
if cErr != 0 {
return fmt.Errorf("failed to load BPF object", syscall.Errno(int(-cErr)))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this right ? Follow my thoughts:

You call bpf_object__load(NULL)... then you will face:

if (!attr)
        return libbpf_err(-EINVAL);

and libbpf_err(-EINVAL)::

static inline int libbpf_err(int ret)
{
        if (ret < 0)
                errno = -ret;
        return ret;
}

So it will return a negative number, but errno will still be positive on the error return. Why do you need to set it to negative again ? And also it is supposed to be an unsigned integer:

type Errno uintptr

and all general errnos are positive as well:

// Errors
const (
        E2BIG           = Errno(0x7)
        EACCES          = Errno(0xd)
        EADDRINUSE      = Errno(0x62)
        EADDRNOTAVAIL   = Errno(0x63)
        EADV            = Errno(0x44)
        EAFNOSUPPORT    = Errno(0x61)
        EAGAIN          = Errno(0xb)
        EALREADY        = Errno(0x72)
        EBADE           = Errno(0x34)
        EBADF           = Errno(0x9)
        EBADFD          = Errno(0x4d)
        EBADMSG         = Errno(0x4a)
        EBADR           = Errno(0x35)
        EBADRQC         = Errno(0x38)
        EBADSLT         = Errno(0x39)
        EBFONT          = Errno(0x3b)
        EBUSY           = Errno(0x10)
        ECANCELED       = Errno(0x7d)
        ECHILD          = Errno(0xa)
        ECHRNG          = Errno(0x2c)
        ECOMM           = Errno(0x46)
        ECONNABORTED    = Errno(0x67)
        ECONNREFUSED    = Errno(0x6f)
        ECONNRESET      = Errno(0x68)
        EDEADLK         = Errno(0x23)
...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, shouldn't you have a %s to stringify the errno ? Like shown at:

func (e Errno) Error() string {
        if 0 <= int(e) && int(e) < len(errors) {
                s := errors[e]
                if s != "" {
                        return s
                }
        }
        return "errno " + itoa.Itoa(int(e))
}

Was that your intent here ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are other places doing the same thing. I'll clarify this first before moving on the review (as I'd like to understand the reasoning of that).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok you are correct, I didn't mean to have negatives there, that was an inconsistency and mistake :-)

Also yes, %s should be used since the error interface implements stringer. Just missing the %s here.

@grantseltzer
Copy link
Contributor Author

@derekparker
Copy link
Contributor

Hey @grantseltzer I think there's some inconsistencies with the error handling in libbpf (at least that's what I'm observing).

If you check the other PR I submitted (#101) you can see errC == -1 however errno == ENOENT (which is 2, not 1). libbpf seems to use ENOENT to indicate reading from an empty map, so it's valuable to have that information from the callers perspective to know it wasn't a "real" error, just an empty map with nothing to be read (at least that's how I understand it).

@derekparker
Copy link
Contributor

Also I believe some of Andrii's comments to be mistaken in his review on your documentation PR: https://patchwork.kernel.org/project/netdevbpf/patch/20211220054048.54845-1-grantseltzer@gmail.com/.

He claims that certain buffers for input buffers, such as the keys buffer argument to lookup_and_delete_batch or even just delete_batch but that's not the case. Your original documentation is correct in that the buffer is filled in by the call to the bpf batch operation, not used as an input set of keys (if that makes sense). This can be verified by looking at the kernel tests themselves: https://git.kernel.org/pub/scm/linux/kernel/git/bpf/bpf-next.git/tree/tools/testing/selftests/bpf/map_tests/htab_map_batch_ops.c

@grantseltzer
Copy link
Contributor Author

@derekparker Are you saying that keys is an output parameter for bpf_map_delete_batch()? I don't think that's the case. I do think it's the case for bpf_map_lookup_and_delete_batch() though.

@derekparker
Copy link
Contributor

@derekparker Are you saying that keys is an output parameter for bpf_map_delete_batch()? I don't think that's the case. I do think it's the case for bpf_map_lookup_and_delete_batch() though.

Ack my mistake I think you're right.

@derekparker
Copy link
Contributor

@grantseltzer in my opinion, based on what I've seen during implementation (return value differing from errno in some cases) I think what this library should do is return the errno error wrapped in another error, so that callers can use patterns such as:

err := libbpfgo.SomeFunction()
if errors.Is(err, os.ErrNotExist) {
        // ...
}

That would mean in the implementation we would use a pattern of:

ret, errno := C.bpf_do_some_stuff()
if errno != syscall.Errno(0) {
        return fmt.Errorf("unable to do some bpf stuff: %w")
}

Note the %w to explicitly wrap the error instead of shadowing it.

Thoughts? I could test this change locally against my codebase but I'm pretty sure this would be sufficient and would standardize errors.

@grantseltzer
Copy link
Contributor Author

@derekparker I think that's a good idea, makes a lot of sense. I believe the inconsistencies between returned error and errno values is going to be solved by the time libbpf 1.0 is released, but i'm not entirely sure. Will verify and report back, getting back into the swing of things after vacation at the moment! I hope you had a nice holiday!

@derekparker
Copy link
Contributor

derekparker commented Jan 3, 2022

@derekparker I think that's a good idea, makes a lot of sense. I believe the inconsistencies between returned error and errno values is going to be solved by the time libbpf 1.0 is released, but i'm not entirely sure. Will verify and report back, getting back into the swing of things after vacation at the moment! I hope you had a nice holiday!

Ack, thanks for the response! And likewise, still getting my head back in the game, haha. Hope you had a nice holiday as well!

libbpfgo.go Outdated
@@ -672,7 +661,7 @@ func (b *BPFMap) UpdateBatch(keys, values unsafe.Pointer, count uint32) error {
}
errC := C.bpf_map_update_batch(b.fd, keys, values, &countC, bpfMapBatchOptsToC(&opts))
if errC != 0 {
return fmt.Errorf("failed to update map %s: %v", b.name, errC)
return fmt.Errorf("failed to update map %s: %w", b.name, syscall.Errno(int(errC)))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should wrap the errno value directly, due to inconsistencies with the returned value and errno in libbpf.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough! I investigated and it looks like not every API function is directly moved over to directly returning error codes yet. Errno is better to rely on for now.

@derekparker
Copy link
Contributor

Only one comment otherwise this LGTM. I just think we should explicitly check errno and wrap that to not have to deal with inconsistencies between ret value and errno value.

This handles correcting how we check errors in libbpfgo
in accordance with how libbpf recommends handling errors
leading into libbpf.

Particularly this follows the following rules:

- APIs that return an error code (int) should have
  error codes checked directly. The error codes
  correspond with error codes in the syscall package.

  For example:

  ```
  errCodeInt := C.libbpf_api_function()
  if errCodeInt != 0 {
    log.Errorf("uh oh: %s\n", syscall.Errno(errCodeInt))
  }
  ```

- APIs that return a pointer should be checked for
  NULL which indicates error. The error code is
  stored in errno. We can get the value of errno
  using the second return.

  For example:

  ```
  ptr, errno := C.libbpf_api_function()
  if ptr == nil {
    log.Errorf("uh oh: %s\n", errno)
  }
  ```

  We can also check if errno corresponds with a specific
  error (it implements the standard error interface).

  For example:

  ```
  ptr, errno := C.libbpf_api_function()
  if ptr == nil {
    if errno.Is(syscall.ENODENT) {
       // handle accordingly
    } else {
      log.Errorf("uh oh: %s\n", errno)
    }
  }

  ```

Signed-off-by: grantseltzer <grantseltzer@gmail.com>
Signed-off-by: grantseltzer <grantseltzer@gmail.com>
Signed-off-by: grantseltzer <grantseltzer@gmail.com>
Signed-off-by: grantseltzer <grantseltzer@gmail.com>
derekparker
derekparker previously approved these changes Jan 10, 2022
Copy link
Contributor

@derekparker derekparker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Signed-off-by: grantseltzer <grantseltzer@gmail.com>
Copy link
Contributor

@rafaeldtinoco rafaeldtinoco left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@grantseltzer grantseltzer merged commit 088c639 into aquasecurity:main Jan 10, 2022
if ret < 0 {
return syscall.Errno(-ret)
if ret != 0 {
return syscall.Errno(ret)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

syscall.Errno expect a positive value to get the correct error - are you sure it is correct to remove the negative sign? (also for all of the above)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The discussion about this was made at:

#100 (comment)

I wonder if I missed something there.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You call bpf_object__load(NULL)... then you will face:

if (!attr)
        return libbpf_err(-EINVAL);

and libbpf_err(-EINVAL)::

static inline int libbpf_err(int ret)
{
        if (ret < 0)
                errno = -ret;
        return ret;
}

So it will return a negative number, but errno will still be positive on the error return. Why do you need to set it to negative again ?

Like you wrote here, the return is a negative number, meaning ret is negative. To have a positive value, we should syscall.Errno(-ret)

if C.IS_ERR_OR_NULL(unsafe.Pointer(obj)) {
return nil, errptrError(unsafe.Pointer(obj), "failed to open BPF object %s", args.BPFObjPath)
obj, errno := C.bpf_object__open_file(bpfFile, &opts)
if obj == nil {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will only be null if LIBBPF_STRICT_CLEAN_PTRS is set.
https://github.com/libbpf/libbpf/blob/e99f34e14447866bcbc38944387531c36f6c85fe/src/libbpf_internal.h#L476
Is that the case?

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

Successfully merging this pull request may close these issues.

None yet

4 participants