Re: Is anybody working on extending procdesc support to rfork?
Date: Wed, 07 Jan 2026 11:22:15 UTC
On 7 Jan 2026, at 03:40, Konstantin Belousov <kostikbel@gmail.com> wrote:
>
> On Tue, Jan 06, 2026 at 11:23:10AM -0700, Alan Somers wrote:
>> The procdesc(4) interface is a Capsicum-friendly, race-free
>> alternative to fork and kill. It's a good fit for a new feature going
>> into the Rust standard library. However, it's not quite complete
>> enough to do a good job. Is anybody working on extending it? Or even
>> interested in doing so?
>>
>> The functionality that Rust wants is:
>>
>> * kill - we can use pdkill()
>> * wait - we can use poll() or select()
>> * spawn - we must use pdfork/exec . posix_spawn would be better, but
>> we can't do such a thing without procdesc support in rfork.
>
> So what exactly is missing? Can you propose a scetch of the desired API?
The minimum is an rfork variant that returns a process descriptor and a PID, rather than just a PID (or returns a just a process descriptor, since pdgetpid exists).
But if we’re designing new process-creation APIs there are two things I’d really like:
An `rfork` (or replacement) flag that gives me something like vfork but without the returns-twice behaviour and requires an explicit execve or exit call to return to the previous kernel state for the thread. After the call, I have the child’s file descriptor table, credentials, and so on. I can do whatever I want to set up the child’s environment, and then `execve` returns either failure (couldn’t create the new process, sorry) and cleans up all of the child-process state, or success. Or, if I call `_exit` then the child process’s state is discarded. But in both cases the memory management is just like any other memory management.
This is useful for Rust, C++, and other languages with RAII. You have to be very careful using RAII to manage memory in a vfork context: any newly allocated memory must be owned by something with a scope that *isn’t* between the `vfork` and `execve`. With an API like this, you can write code that looks a bit like this:
```c++
int child = pdrfork(...);
if (child < 0)
{
throw ProcessCreationFailed(errno);
}
std::vector<const std::string> owningArguments;
std::vector<const char*> argv;
std::vector<const std::string> owningEnvironment;
std::vector<const char*> envp;
auto pushArgument(auto &arg)
{
owningArguments.push_back(arg);
arvv.push_back(owningArguments.back().c_str());
};
auto pushEnvironment(auto &envvar)
{
owningEnvironment.push_back(arg);
envp.push_back(owningEnvironment.back().c_str());
};
// Argv[0] is the program name
pushArgument(binaryName);
// Set up other arguments and environment variables.
pushArgument(...);
argv.push_back(nullptr);
pushEnvironment(...);
envp.push_back(nullptr);
// Set up any inherited file descriptors we need, including making sure that the program binary is close on exec and near the end.
...
// If something went wrong, exit the child process scope and throw an exception.
if (somethingWentWrong)
{
_exit(0);
throw ProcessSetupFailed(someExplanation);
}
// Do any setuid, jattach, whatever things.
...
// Enter capability mode
cap_enter();
// Execute the new program in this context
int execReturn = fexecve(programFd, argv.get(), envp.get());
if (execReturn < 0)
{
throw ProcessCreationFailed(errno);
}
return child;
```
All memory for the argument / environment vectors is cleaned up on either the throw or return. With normal `vfork`, you have to make sure that the declaration of the vectors is *before* the `vfork` call. This is very easy to get wrong (and to break in minor refactorings), and getting it wrong leaks memory (so doesn’t crash and is not the kind of bug that gets high-priority fixes).
And this is easy to wrap in a process-creation API that takes a lambda that runs in the child-process setup scope, giving an ergonomic way of doing process creation in higher-level languages.
The other things I’d really like is a way of passing a list of file descriptors to inherit and the numbers that they go in. Something like:
```
struct inherited_file_descriptor
{
int parent_fd;
int child_fd;
};
int pdrfork(int flags, const struct inherited_file_descriptor[]);
```
So I could write:
```
struct inherited_file_descriptor fds[] = {
{0, 0}, {1, 1}, {2, 2}, {RPCSocket, 3},
/* Gap for things opened after dropping privileges, in the child context */
{ tempFD, 8} };
int child = pdrfork(..., fds);
...
```
This requires a flag that is part way between RFCFDG and RFFDG: only the named fds are inherited. This avoids the complicated `dup2` + `closefrom` dance that I always end up doing to ensure that the child inherits the expected FDs, in the correct places, but only the expected ones (the simplest way of doing this is to `dup` all file descriptors that you want to inherit until they’re above the number of the highest one, then `dup2` them into the right place, then `closefrom`, you can make this more efficient by doing a shuffle pass but it’s complex and error prone. Doing this in the kernel would be simpler).
So my desired version is close to the pdrfork outlined above. That said, it would be nice if the API were extensible, which probably means taking a structure with a size where the structure contains the pointer to the inherited file descriptor list.
David