I found an elegant solution for the problem of Haskell vs Linux capabilities explained in "QUIC and Linux capabilities". To know why the CAP_NET_BIND_SERVICE capability is necessary, please read this article in advance.
On Linux, the following is the procedure to boot a secure multi-threaded server with CAP_NET_BIND_SERVICE:
- Executed by
root. - Reading a TLS private key.
- Setting
SECBIT_KEEP_CAPSbyprctl(2)-- Without this, all capabilities are lost aftersetuid(2). - Switching the
rootuser tonobody(or something) bysetuid(2). - Dropping capabilities except
CAP_NET_BIND_SERVICEbycapset(2). - Spawning native threads.
CAP_NET_BIND_SERVICEis inherited by all native threads.
GHC RTS executes Haskell code after spawning native threads. So, there are two problems to implement a secure multi-threaded server with CAP_NET_BIND_SERVICE in Haskell.
- How to set
SECBIT_KEEP_CAPSto all native threads? - How to drop capabilities except
CAP_NET_BIND_SERVICEof all native threads?
For 1), by reading the source code of GHC RTS, I finally found a C level hook called FlagDefaultsHook(). The user manual has the section of Hooks to change RTS behaviour, but this hook is not written, sign. GHC RTS executes this hook before spawning native threads. So, if the following code is linked your Haskell program, all native threads keeps all capabilities after setuid(2), yay!
void FlagDefaultsHook () { if (geteuid() == 0) { prctl(PR_SET_SECUREBITS, SECBIT_KEEP_CAPS, 0L, 0L, 0L); } }
For 2), I considered that signals can be used. On Linux, we can get the thread IDs of all native threads in a process by scanning /proc/<process id>/task/. And Linux provides tgkill(2) to send a signal to the native thread specified a thread ID.
I first tried to use installHandler of Haskell to install a signal handler. But it appeared that an improper native thread catches the signal from tgkill(2), sigh. So, I used sigaction(2) again in FlagDefaultsHook().
The following is the procedure to implement a secure multi-threaded server with CAP_NET_BIND_SERVICE in Haskell:
- Executed by
root. - GHC RTS executes
FlagDefaultsHook():- Setting
SECBIT_KEEP_CAPSbyprctl(2). - Setting a signal handler to drop capabilities except
CAP_NET_BIND_SERVICEbysigaction(2).
- Setting
- GHC RTS spawns native threads.
- GHC RTS executes Haskell code:
You can see a concrete implementation in this commit.
One awkward thing is that the capabilities of the process itself remains in a wrong value. It seems to me that capset(2) for a process is not permitted if it is multi-threaded. However, if I understand correctly, there is no way to access or inherit the capabilities of the process in GHC RTS. So, I don't care it so much.