CGI implementation in libpxd/polluxd
As of 20250817 the polluxd architecture separates CGI processes from the main server process in a way that allows it to:
- Run different CGI instances with different permissions (user, chroot, etc) without requiring perpetual root permissions.
- Separate the main server address space from CGI children (i.e. no key leakage can happen).
Can't you do all of this with fcgi? Yeah, you can, but fcgi requires a whole complicated protocol to do all of those things. libpxd reinvents part of the fcgi wheel with a relatively simple mechanism.
The procedure
There are three processes that are involved in launching a CGI script:
- The main server process which handles Gemini requests.
- The CGI helper process which dispatches the CGI requests.
- The CGI worker process which sets up the CGI environment and executes the CGI script.
The main server process
From the perspective of the main server process what happens (from startup) is:
- The configuration file is read.
- For each CGI location in the config file: a server-helper communication channel is created via socketpair(), and fork() is called to create the CGI helper. Note this is done BEFORE reading any key material and before dropping privileges so that keys aren't in memory yet and chroot etc can still be done.
- The server process reads keys, sets up its listening sockets, drops privileges, and begins serving Gemini requests.
- When a request for a CGI script comes in, the proper CGI helper socket is looked up based on the request.
- The main server sends an arbitrary 4-byte value to the CGI helper socket to indicate it needs a CGI subprocess.
- The main server receives a file descriptor (via SCM_RIGHTS) on the helper socket for server-worker communication.
- The server writes the CGI environment to the server-worker socket via a single call to send().
- Data is read from the server-worker socket until it is closed. This data is forwarded to the Gemini connection.
The CGI helper process
Before reading keys and dropping privileges, the main process fork()s (to create a CGI helper process), chroots, and drops privileges. It then listens for requests from the server-helper socket in a loop.
The request loop:
- Reads 4 bytes of data from the server-helper socket.
- Creates a socketpair() for server-worker communication and fork()s to create the CGI worker process (below).
- The CGI helper (parent) process sends the server half of the server-worker pair back to the main server via SCM_RIGHTS over the server-helper socket (see unix(7)) with the 4-byte value it read from the request socket as normal data. The worker side of the server-worker socketpair gets closed in the helper process.
- The CGI helper loops and waits for another request.
The CGI worker process
The CGI worker starts off with the server-worker socketpair open and runs with the permissions/chroot of the CGI helper which may be different from the main server process.
The CGI worker lifetime:
- The worker closes the server half of the server-worker socketpair.
- Reads the CGI environment from the server-worker socket via one call to recv().
- Parses and validates the received CGI environment data.
- Sets up the CGI environment variables using the received data.
- Moves the server-worker socket onto stdout (dup2()).
- Calls execve() using the PATH_TRANSLATED CGI environment variable.
Once all of this is done, the CGI worker runs the script as an config-specified user in a config-specified chroot that may or may not be the same as the main server process. It never has access to any key material or any substantial part of the address space from the main server process.