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_CAPS
byprctl(2)
-- Without this, all capabilities are lost aftersetuid(2)
. - Switching the
root
user tonobody
(or something) bysetuid(2)
. - Dropping capabilities except
CAP_NET_BIND_SERVICE
bycapset(2)
. - Spawning native threads.
CAP_NET_BIND_SERVICE
is 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_CAPS
to all native threads? - How to drop capabilities except
CAP_NET_BIND_SERVICE
of 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_CAPS
byprctl(2)
. - Setting a signal handler to drop capabilities except
CAP_NET_BIND_SERVICE
bysigaction(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.