あどけない話

インターネットに関する技術的な話など

Haskell vs Linux capabilities

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 by prctl(2) -- Without this, all capabilities are lost after setuid(2).
  • Switching the root user to nobody (or something) by setuid(2).
  • Dropping capabilities except CAP_NET_BIND_SERVICE by capset(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.

  1. How to set SECBIT_KEEP_CAPS to all native threads?
  2. 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 by prctl(2).
    • Setting a signal handler to drop capabilities except CAP_NET_BIND_SERVICE by sigaction(2).
  • GHC RTS spawns native threads.
  • GHC RTS executes Haskell code:
    • Reading a TLS private key.
    • Switching the root user to nobody (or something) by setuid(2).
    • Sending signals to all native threads to drop capabilities except CAP_NET_BIND_SERVICE by tgkill(2).

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.