4.1.1 MLLVD-CR-24-01: Signal Handler’s Alternate Stack Too Small
4.1.2 MLLVD-CR-24-02: Signal Handler Uses Non-Async-Safe Functions
4.1.3 MLLVD-CR-24-03: Virtual IP Address of Tunnel Device Leaks to Net-
work Adjacent Participant
4.1.4 MLLVD-CR-24-04: Deanonymization Through NAT
4.1.5 MLLVD-CR-24-05: Deanonymization Through MTU
4.1.6 MLLVD-CR-24-06: Sideloading Into Setup Process
All pretty straightforward IMO. They lean on "DAITA" aka Defence against AI Traffic Analysis pretty heavily, which I don't fully understand yet, but is probably worth some further reading.
Safe signal handling has so many footguns that it seems worth re-considering the entire API.
Even OpenSSH has had issues with it [1].
It seems very difficult to build good abstractions for it in any programming language, without introducing some function colouring mechanism explicitly for this. Maybe a pure language like Haskell could do it.
Haskell's runtime is so complex that I don't think you can write signal handling functions in Haskell. The best you can do is to mark a sigatomic boolean inside the real signal handler and arrange the runtime for check for that boolean outside the signal handler.
Yup: see https://hackage.haskell.org/package/ghc-internal-9.1001.0/do... where it is clear that setting a handler simply writes to an array inside an MVar. And when the signal handler is run, the runtime starts a green thread to run it, which means user Haskell code does not need to worry about signal handler safe functions at all, since from the OS perspective the signal handler has returned. The user handler function simply runs as a new green thread independent of other threads.
But I like the fact that you brought up this idea. Haskell can't do it but in a parallel universe if there were another language with no runtime but with monads, we can actually solve this.
I am not sure but I think rust already allows safe signal handlers? The borrow checker makes you write thread safe code even without any active threading and signals are just emergency threads with some extra limitations... right? I don't understand this too deeply so I could be wrong here.
Rust does allow for safe signal handling, but it's sort of the same way that it allows for safe and correct interrupt handlers for people writing os kernels (signals are basically interrupts, just from kernel->user instead of hardware->kernel). You're basically constrained to no_std and have to be very careful about communications with the rest of the system using lock free mechanisms.
If handling a signal was equivalent to handling concurrency then it wouldn’t be as much of a problem.
IIRC a signal can take over execution of a running thread, so it will completely invalidate any critical sections etc. You cannot access any shared resource easily, cannot allocate memory and a lot more restrictions of this form.
Yes but the signal handling code acts as if it is on a different thread. So it cannot access the critical sections or mess up any existing state on the thread anyways. Sure the other parts need to be managed manually but just that should still go a long way. ...Right?
Not quite, by default the signal handler hijacks an existing thread. It is possible to keep a dedicated thread around that solely waits for signals, but that’s a workaround and you end up needing to also mask all signals from all other threads for correctness. And then there are also synchronous signals, which can’t be handled this way (eg. segfaults)
Imagine a scenario where the original thread state is in a critical section, in the middle of allocating memory (which may need a mutex for non-thread local allocations) etc.
The code within the signal handler can’t guarantee access to any shared resource, because the previous execution of the thread may have been in the middle of the critical section. With normal concurrency, the thread that doesn’t hold the mutex can just suspend itself and wait.
However, because the thread has been hijacked by the signal handler, the original critical section cannot complete until the signal has been handled, and the signal handling cannot yield to the original code because it is not suspendable.
Signal handling is distinct from a different thread because it blocks the execution of the “preempted thread” until the signal handler completes.
As a example, if the preempted code grabs a lock for a resource, then signal handler completion can not depend on grabbing that lock because that lock will never be released until the preempted code runs again and the preempted code can never run again until the signal handler completes.
A correct signal handler can never wait for a resource held by regular code. This precludes coordination or sharing via normal locks or critical sections.
The best thing you can do is set a global variable value and that’s it. Let your main even loop mind the value and proceed from there. Only do this in a single thread and block singles in all others as the first thing you do. Threads and signals do not mix otherwise.
You want signalfd, which may optionally fed to epoll or any of the other multiplexing syscalls.
Signalfd can mostly be implemented on any platform using a pipe (if you don't have to mix 32-bit and 64-bit processes, or if you don't need the siginfo payload, or if you read your kernel's documentation enough to figure out which "layout" of the union members is active - this is really hairy). Note however the major caveat of running out of pipe buffer.
A more-reliable alternative is to use an async-signal-safe allocator (e.g. an `mmap` wrapper) to atomically store the payloads, and only use a pipe as a flag for whether there's something to look for.
Of course, none of these mechanisms are useful for naturally synchronous signals, such as the `SIGSEGV` from dereferencing an invalid pointer, so the function-coloring approach still needs to be used.
> Another option is to use a proper OS that includes the ability to receive signals as a part of your main event loops
Every 'nix can do that. Your signal handler just writes a byte to a pipe and your main loop reads the pipe or fifo. The pipe/fifo is your event queue, which your main loop reads.
> The best thing you can do is set a global variable value and that’s it.
Seems kinda limiting.
If I've got a slow file download going on in one thread, and my program gets a Ctrl+C signal, waiting for the download to complete before I exit ain't exactly a great user experience.
Use select() or epoll() or kqueue() to see if your socket is ready for reading. That way you can monitor your global variable too. That’s the correct way to do it.
If you have multiple threads, you start one just to mind signals.
Signal handlers are extremely limited in what they can do, that’s the point. They are analogous to hardware interrupt handlers.
In fish-shell we have to forego using the niceties of the rust standard library and make very carefully measured calls to libc posix functions directly, with extra care taken to make sure so memory used (eg for formatting errors or strings) was allocated beforehand.
Or it's nearly impossible for a pure functional language if the result of the async signal means you need to mutate some state elsewhere in the program to deal with the issue.
I think that’s slightly orthogonal. It would still be safe, because you’d design around this restriction from the start, rather than accidentally call or mutate something you were not supposed to.
The problem with safe signal handling is that you need to verify that your entire signal handler call stack is async safe. Assuming purity is a stronger property, signal handling is a safe API without any more work.
The inflexibility due to the purity might cause other issues but that’s more a language level concern. If the signal handling API is safe and inflexible, it still seems better for a lot of use cases than an unsafe by default one.
Monads can be thought of as arbitrary function colourings, hence the prior mention of Haskell potentially being a good fit. Of course monads are implementable in almost any other language, but few have as much syntax sugar or general library support as Haskell does, except maybe Ocaml
Yeah, but how do you design a Monad that does the "tell this other thread to unblock and unwind its state because an external error has triggered? You know, the basic function of an interrupt?
1) Monads used to restrict the computation available in the context of a signal handler (or function coloring etc, basically a way for a compiler or static checker to determine that a block of code does not call unsafe functions)
2) The actual process of handling a signal received by the signal handler
I think me and the parent are referring to 1). 2) is also important, but it is not a signal specific concern. Even without a signal handler, if you want to write an application which handles async input, you have to handle the case of processing the input to do something useful (eg. let’s say you are writing an HTTP server and want to have a network endpoint for safely killing the thing).
I think the generally recommended way to represent 2) in a pure way is to model the signal as a state machine input and handle it like all other communication.
Stack too small - there's no proof the 8k allocated is too small, is it really exploitable?
Non async functions - pretty common problem but difficult to actually exploit. Every developer who has worked with signal handlers has probably made this mistake at some point because the issues it causes are extremely difficult to reproduce (some incredibly unlucky timing is required)
Arp leaking addresses - Not really a Mullvad issue and only exploitable on the local network
Deanonymization attacks - these work against all VPNs and you can always anonymize traffic more but it has a cost to do this.
Sideloading - Yeah this is probably the worst one but is not exploitable on it's own.
https://x41-dsec.de/static/reports/X41-Mullvad-Audit-Public-...
Titles of issues they found:
4.1.1 MLLVD-CR-24-01: Signal Handler’s Alternate Stack Too Small
4.1.2 MLLVD-CR-24-02: Signal Handler Uses Non-Async-Safe Functions
4.1.3 MLLVD-CR-24-03: Virtual IP Address of Tunnel Device Leaks to Net- work Adjacent Participant
4.1.4 MLLVD-CR-24-04: Deanonymization Through NAT
4.1.5 MLLVD-CR-24-05: Deanonymization Through MTU
4.1.6 MLLVD-CR-24-06: Sideloading Into Setup Process
All pretty straightforward IMO. They lean on "DAITA" aka Defence against AI Traffic Analysis pretty heavily, which I don't fully understand yet, but is probably worth some further reading.
https://mullvad.net/en/vpn/daita