flak rss random

cgo does clear errno

C functions commonly, though not universally, provide information about a failure through the global variable like errno. Provide, not indicate. If there’s no error, as indicated by the function’s normal return value, the value and meaning of errno is unreliable.

The usual way of error checking in C, where you check the return and only then look at errno, works when you use it, but it’s possible to construct cases with surprising results if you take shortcuts.

        close(-1);
        int fd = dup(1);
        if (errno)
                printf("no dup for you: %s\n", strerror(errno));
        printf("fd: %d\n", fd);

When run this will print something like “no dup for you: Bad file descriptor” followed by “fd: 3”. So it did succeed, but we still see the errno set by calling close with a bad file descriptor.

Go bridges between C and go with cgo, and exposes errno by providing an optional second return value, effectively converting it to the go idiom. But in go, you always check err first, before looking at any other return values. Could this be a problem?

If we rewrite the above code fragment in go, we’ll see the problem. Right?

        C.close(-1)
        fd, err := C.dup(1)
        if err != nil {
                fmt.Printf("no dup for you: %s\n", err)
        }
        fmt.Printf("fd: %d\n", fd)

And... “fd: 3” is all it prints. I was all set to write a warning about this potential trap, and wrote a simple test just to get the precise error message, but there was none. The code unexpectedly worked. Apparently cgo resets errno to zero between calls into C code.

However, there’s surely no way cgo can outsmart my next trap. Let’s write our own C function and put both calls there.

/*
int
baddup(int fd) {
    close(-1);
    return dup(fd);
}
*/
import "C"

        fd, err := C.baddup(1)
        if err != nil {
                fmt.Printf("no dup for you: %s\n", err)
        }
        fmt.Printf("fd: %d\n", fd)

Now we see it. “no dup for you: bad file descriptor” followed by “fd: 3”. (You can tell I ran these examples for real because strerror and cgo capitalize Bad differently. I didn’t previously know that.)

I think cgo is trying to be helpful here, but it can only do so much. Is a half effort worse than no effort? Maybe. You can write go code that manually interfaces with libc and have it look and feel like native go code, but that could be a dangerous habit. Other, more complicated, C functions may set errno incidentally along the way to success. I don’t like leaky abstractions that work in simple cases but fall apart when things get even slightly complicated. I think the errno to err translation is convenient, but it probably should have retained the original errno semantics. Otherwise you need to memorize a list of which functions work and which don’t.

In OpenBSD, libc tries to avoid setting errno when a function succeeds, but there have been bugs and oversights in the past. I doubt we’ve seen the last of such problems; if not in OpenBSD, then elsewhere.

Then there are functions like getaddrinfo which don’t use errno at all, directly returning the error code, for which the cgo err bridge is useless.

As a bonus, I decided to test one more scenario. What if we very literally translate some C code to go, and check the return value, and then try accessing errno? I don’t think there’s any reason you would do this, but it seemed reasonable to check for the sake of completeness.

        fd := C.dup(-1)
        if fd == -1 {
                 C.warn("no dup for you")
        }
        fmt.Printf("fd: %d\n", fd)

And again I was surprised. I was expecting the warn to print “Undefined error: 0” because errno had been cleared. Except that’s not what happened. It printed “test: no dup for you: Bad file descriptor” just like it would in C. So now I’m a little more confused about what’s really going on.

Initial conclusion was wrong. It’s not that cgo clears errno, but it seems to very carefully preserve and check it around calls into C code, returning it when set, but not actually clearing it in all cases.

Posted 15 Apr 2022 17:07 by tedu Updated: 15 Apr 2022 17:51
Tagged: c go programming