11 comments

[ 2.8 ms ] story [ 33.2 ms ] thread
Why didn't they mention threads?
Something worth mentioning would have been those libc calls where the only way to tell if a return value of 0 is an error is to check errno. And of course, as the article says, errno is only set in error, you need to set it to 0 before making that libc call.

I think strtol was one such function, but there were others.

I don't think this behaviour is "peculiar" as the author says it is; why does the error number matter if you know the call succeeded? GetLastError() on Windows works similarly, although with the additional weird caveat that (undocumentedly) some functions may set it to 0 on success.

The system call wrappers could all have explicitly set errno to 0 on success, but they didn't.

Because it's plainly unnecessary. It'd be a waste today, and even more so on a PDP-11 in the 1970s.

Imho, this is an area where the limitations of C shine through.

Some kernels return error status as a CPU flag or otherwise separately from the returned value. But that's very hard to use in C, so the typical convention for a syscall wrapper is to return a non-negative number for success and -error for failure, but if negative numbers are valid as the return, you've got to do something else.

I think it would have been better if they had designed it so that the error message from the kernel came in a seperate register. That would mean you didnt have to use signed int for the return value. The issue is that one register now is sort of disambigious. It either returns the thing you want or the error but these are seperate types. If you had them in seperate registers you would have the natural type of the thing you are interested in without having to convert it. This would however force you to first check the value in the error register before using the value in the return register but that makes more sense to me than the opposite.
By far the largest issue with errno is that we don't record where inside the kernel the error gets set (or "raised" if this was a non-C language). We had a real customer case recently where a write call was returning ENOSPC, even though the filesystem did not seem to have run out of space, and searching for the place where that error got raised was a multi-week journey.

In Linux it'd be tough to implement this because errors are usually raised as a side effect of returning some negative value, but also because you have code like:

  err = -EIO;
  ... nothing else sets err here ...
  return err;
But instrumenting every function that returns a negative int would be impossible (and wrong). And there are also cases where the error is saved in (eg) a bottom half and returned in the next available system call.
> We had a real customer case recently where a write call was returning ENOSPC, even though the filesystem did not seem to have run out of space

ext4 reserved blocks?

This is such a debugging timesink that I've added two things to bcachefs to address it.

- private error codes; error codes are unique, corresponding to a specific source location. They're mapped to standard error codes when we exit from the bcachefs module; in bcachefs they make for much more useful error messages.

- error_throw tracepoint: any time we throw an error, we invoke a standard tracepoint. Extremely useful for debugging wonky-but-not-broken behavior.

For the kids just learning about software design today, remember that the way things are now is not necessarily ideal, and you do not have to go along with it. You can color outside the lines.

One of the great misfortunes of traditional software [and network] design is a lack of visibility throughout the stack. The author here talks about "multiple return values", which is to find out multiple pieces of information from some other piece of code. But that code calls other code, and that other code calls more, all with its own information that might be useful for you to know.

Good software design is cohesive and loosely coupled. That means you should not know, or depend on, the internal workings and variables of some other component. But at the same time, when problems happen, it is useful to know what happened in some other component, maybe even 3 components down the line. In particular, you can usually determine the cause of failures by examining just the inputs and the outputs of a function. Examine the inputs and outputs of every function in the system, and that's enough to identify or recreate most bugs. (i/o, system resource pressure, and network interruption are the last bits of info you need, but harder to gather)

But I'm not aware of this capability (examining the input and output of components multiple levels away from the current code) existing as a software design pattern. Within one component, sure, but outside it? If you load a different component into yours, maybe it exposes attributes and whatnot to you. But what about the components that component uses? And what if we're leaving the immediate computing environment? I still want to know what was going on further down the line.

Such a solution exists within systems-of-systems, as in distributed tracing. But only to an omnipotent observer in a faraway land. I want my code to know what happened elsewhere, if only to report a more accurate error message than "500 internal server error". I can count at least 20 times in the past 6 months that I have encountered a web app whose frontend literally did nothing when I clicked a button, and only upon opening up the browser's inspection tools did I see a backend API returning an actual error message. But an equal number of times, just "500 error" or similar. I want to see "the add-user api call failed because you do not have permissions to add users", or "the server that tried to process this request ran out of disk space", and I want to press a button that automatically composes an e-mail to the company with a bug report that includes all the details. Can you build that today with existing software design?

Sure, if you spend 120 hours building the distributed tracing and observability infrastructure (or pay Datadog a quarter million for it), and 80 more hours to train the devs how to use it. But we shouldn't need infrastructure. The software can carry relevant data to-and-fro; let it carry more than just "errno".

For thread-safety, POSIX needs a mulligan migration to an API with an opaque context structure. Globals are bad and were never going to work with threads. The gotcha would be the need for mutexes on process shared state like env vars.

If going through all this effort, might as well adopt a fine-grained, capability-based permissions system too so that resources (i.e., fs, process, network) can be sandboxed.