Error handling for journeymen

Programming, for all ages and all languages.
Post Reply
nullplan
Member
Member
Posts: 2033
Joined: Wed Aug 30, 2017 8:24 am

Error handling for journeymen

Post by nullplan »

Hi all,

I am currently writing a dynamic linker for my libc project. In the course of doing so, I have the problem that I kind of want two different error handling policies to apply to certain functions within the linker, depending on whether they are running during the initial linking of the application or later at runtime:

If the code is running during the initial link, I want to print the error message to stderr, note that there has been an error, and then resume operation at a sensible place. For example, if one symbol could not be found (and it was needed for a relocation), I want to continue with the next relocation. But then, once everything is done, I just exit if there has been any error.

But on the other hand, if we are inside of dlopen() or dlsym(), I want to abort everything at the first error, write the error message into a string buffer to be returned in dlerror().

My programming language is C, so exception handling is not an option. It wouldn't help anyway, since what I am looking for here is resume semantics, and C++ exceptions don't have those.

The reason I want those is because I think there is value in providing as many errors as possible during the initial link, rather than letting the user waste their time fixing one issue, when an insurmountable hurdle is lurking a little bit further away. Imagine a program not finding two library files, and one the user knows how to provide, and the other they don't.

As far as I can tell, my options are:
  • Using longjmp(). That's what musl and glibc are doing. I personally really don't like longjmp(), because it is the only standard C function with non-local control flow. Its mere presence makes understanding a codebase harder. So I would really rather not.
  • Duplicating the code. Have a version of the code that continues on error, and another version that aborts. That avoids the issue of overgeneralization, but it comes at the cost of writing significant pieces of code (in particular, the library loader and the relocation processor) twice.
  • Passing the policy as a parameter. That is probably the least stinky option, but it does make my functions more complicated than they have to be. And they already are quite complicated.
I also had a look around the other libcs to see if/how they were handling the same issue:
  • As stated before, musl and glibc use longjmp() to jump out of the error handling function back to the runtime function that was called, and then attempt to clean up any state that was left unclean. glibc dresses it up in something that looks like an exception if you squint a little, but there's no hiding the red flag that is longjmp().
  • dietlibc and mlibc both always bail on first error, so there's no difference in control flow between load time and runtime. The error message goes into a buffer, and the caller then decides whether to print it to stderr or to a buffer.
  • newlib doesn't have a dynlinker, it just leaves everything to Windows.
Does anyone have suggestions how to tackle this?
Carpe diem!
User avatar
Demindiro
Member
Member
Posts: 165
Joined: Fri Jun 11, 2021 6:02 am
Libera.chat IRC: demindiro
Location: Belgium
Contact:

Re: Error handling for journeymen

Post by Demindiro »

If the code is complex enough that error codes would be too tedious, then my thought is to use longjmp(), but wrapped in a "task" abstraction:

- int spawn_task(void (*f)(void *), void *arg), which uses setjmp() and returns the status of the task (0 by default).
- void exit_task(int status), which uses longjmp().
GeneSYS exokernel (Codeberg)
Lemmings! micro-/multikernel (Github, Codeberg)
Waddle container tool (Codeberg)
nullplan
Member
Member
Posts: 2033
Joined: Wed Aug 30, 2017 8:24 am

Re: Error handling for journeymen

Post by nullplan »

I just checked something in the musl source code again, and now I have a red spot on my forehead from facepalming.

My principle objection to using longjmp() is that it makes reclaiming temporary resources harder. In this particular case, the file descriptor for the library. The control flow from the library loader is basically:

Code: Select all

int fd;
if (strchr(fname, '/'))
  fd = open(fname, O_RDONLY);
else
  fd = path_search(fname, env_libpath);
/* error handling elided for brevity */
void *p = map_library(fd);
close(fd);
return p;
As the name says, path_search() searches for the library, while map_library() then does the hard work of reading the file headers and loading the stuff into memory. Of course, map_library() can have multiple errors: The file could be too short, or have wrong magic bytes, or some other problem with the headers. And of course, the mmap() calls could fail at runtime for some reason.

Now, musl has a function to handle errors in the dynlinker. At load time, it prints the error message to stderr and returns, and at runtime, it prints the error message into a buffer and longjmp()s back to dlopen(). But of course, in the above snippet, if you longjmp() out of map_library(), then the file descriptor leaks. Unless you close it in map_library() in that case, but that would be horrible. Or else you somehow publish the file descriptor enough so that dlopen() can see it and close it in the error handling code.

So what does musl's map_library do? Nothing of the sort. It always returns normally. If the file contents are somehow not to musl's liking, it sets errno to ENOEXEC and returns. Then whoever called load_library() can see the failure code and print it, and load_library() still gets to close its FD normally.

Except that the errno mechanism is extremely fragile, since even successful function calls can overwrite it. And indeed musl's close() function will set errno to ENOENT if any AIO is ongoing in the process. So not only does musl not give a detailed error message that might help someone figure what is going on with the library files, it can also lose even that error code in favor of some other misleading code.

I for one will probably do the thing I already identified as the least stinky. I will pass a pointer to an error handling function to these functions. The error handler will get the error message and can do whatever it wants with it. It will then return a code to the caller whether to abort or not. Regardless of the return value, the function will eventually return failure, so the caller knows something broke.
Carpe diem!
Post Reply