22 comments

[ 2.5 ms ] story [ 31.3 ms ] thread
At first I wondered if musl does it better, so I checked, and the version I have disables cancellation in the guts of `getaddrinfo`.

I've always thought APIs like `pthread_cancel` are too nasty to use. Glad to see well documented evidence of my crank opinion

Netscape used to start a new thread (or maybe it was a subprocess?) to handle DNS lookups, because the API at the time (gethostbyname) was blocking. It's kind of amazing that we're 30 years on and this is still a problem.
Maybe this is naive, but could there just be some amount of worker threads that run forever, wait for and take jobs when needed, and message when the jobs are done? Don't need to be canceled, don't block
Why is running the DNS resolution thread a problem? It should be dequeuing resolution requests and pushing responses and sleeping when there is nothing to do

When someone kills off the curl context surely you simply set a suicide flag on the thread and wake it up so it can be joined.

There might be a way to getaddrinfo asynchronously with io_uring by now. Otherwise just call the synchronous version in another thread and let it time out so the thread exits normally, right? Why bother with pthread_cancel?
> Then it needs to sort them if there is more than one address. And in order to do that it needs to read /etc/gai.conf

I don’t see why glibc would have to do that inside a call to getaddrinfo. can’t it do that once at library initialization? If it has to react to changes to that file while a process is running, couldn’t it have a separate thread for polling that file for changes, or use inotify for a separate thread to be called when it changes? Swapping in the new config atomically might be problematic, but I would think that is solvable.

Even ignoring the issue mentioned it seems wasteful to open, parse, and close that file repeatedly.

Why can't they help fix the C library in question? Cancelation is really tricky to implement for the C library author. It's one of those concepts that, like fork, has implications that pervade everything. Please give your C library maintainers a little leeway if they get cancelation wrong. Especially if it's just a memory leak.
pthread_cancel is not a good design because it operates entirely separately from normal mechanisms of error handling and unwinding. (That is, if you’re using C. If you’re using C++ it can integrate with exception handling.)

A better approach would have been to mimic how kernels internally handle signals received during syscalls. Receiving a signal is supposed to cancel the syscall. But from the kernel’s perspective, a syscall implementation is just some code. It can call other functions, acquire locks, wait for conditions, and do anything else you would expect code to do. All of that needs to be cleanly cancelled and unwound to avoid breaking the rest of the system.

So it works like this: when a signal is sent to a thread, a persistent “interrupted” flag is set for that thread. Like with pthread_cancel, this doesn’t immediately interrupt the thread, but only has an effect once the thread calls one of a specific set of functions. For pthread_cancel, that set consists of a bunch of syscalls and other “cancellation points”. For kernel-internal code, it consists of most functions that wait for a condition. The difference is in what happens afterwards. In pthread_cancel’s case, the thread is immediately aborted with only designated cleanups running. In the kernel, the condition-waiting function simply returns an error code. The caller is expected to handle this like any other error code, i.e. by performing any necessary cleanup and then returning the same error code itself. This continues until the entire chain of calls has been unwound. Classic C manual error handling. It’s nothing special, but because interruption works the same way as regular error handling, it‘s more likely to “just work”. Once everything is unwound, the “interrupted” flag is cleared and the original signal can be handled.

(The error code for interruption is usually EINTR, but don’t confuse this with EINTR handling in userspace, which is a mess. The difference is because userspace generally doesn’t want to abort operations upon receiving EINTR, and because from userspace’s perspective there’s no persistent flag.)

pthread_cancel could have been designed the same way: cancellation points return an error code rather than forcibly unwinding. Admittedly, this system might not work quite as well in userspace as it does in kernels. Kernel code already needs to be scrupulous about proper error handling, whereas userspace code often just aborts if a syscall fails. Still, the system would work fine for well-written userspace code, which is more than can be said for pthread_cancel.

In libdill, cancelling a coroutine makes all blocking calls on that routine return immediately with ECANCELED. The code must check this condition and exit any loop and so eventually the coroutine finishes, having released all resources in the process.
It’s been decades, why doesn’t getaddrinfo have a standardised way to specify a timeout? Set a timeout to 10 seconds and life becomes a lot easier.

Yes I know in Linux you can set the timeout in a config file.

But really the dns setting should be configurable by the calling code. Some code requires fast lookups and doesn’t mind failing which, while others won’t mind waiting longer. It’s not a one size fits all thing.

Why isn't DNS in a service on the operating system instead of libc? You'll want requests to be locally cache anyways. This also makes it easier to just abandon a RPC instead of stopping a thread you don't control.
what's old is new again, I loved java in the early 2000's trying to remotely stop a thread

Thread.destroy() Thread.stop() Thread.suspend()

so much potential for corrupted state.

I'm the author of the GitHub issue that the blog links to, and I'd like to thank Stefan for quickly acknowledging the problem and addressing the issue! I try to keep one of our internal applications up to date with the latest libcurl version within a day or two of a release, so we sometimes hit fresh problems while running our battery of tests.

Ironically, our application has also struggled with blocking DNS resolution in the past, so I appreciate the discussion here. In case anyone is interested, here is a quick reference of the different asynchronous DNS resolution methods that you can use in a native code application for some of the most popular platforms:

  - Windows/Xbox: GetAddrInfoExW / GetAddrInfoExCancel
  - macOS/iOS: CFHostStartInfoResolution / CFHostCancelInfoResolution
  - Linux (glibc): getaddrinfo_a / gai_cancel
  - Android: android.net.DnsResolver.query (requires the use of JNI)
  - PS5: proprietary DNS resolver API
i’ve run into this issue with pthread_cancel

it makes cleanup logic convoluted when you can’t easily pthread_cleanup_push, forcing you to either block on the join or signal cancellation and detach

(comment deleted)
Just use c-ares. Threads with signals are evil
I feel like the entire pthread_cancel and cancellation point feature is only really useful when your application is exiting and resource management doesn't matter anymore. If you want to use it on a long running process, the thread that gets canceled must effectively be completely free of any resource allocation....

Unless of course you're making use of shared memory between processes, then you could still leak even when the entire process is shut down.

pthread_cancel is never the answer. Abrupt thread cancelation is always a risk for leaks and/or even worse, deadlocks. No matter of it was blocked in one libc function. The function is not one instruction you don't know what the poor thread was doing when it was killed.