The Linux kernel is edging toward a way to create a process without forking one first, and the shape of it became visible on August 4 when LWN covered a process builder patch series from Li Chen. The idea is to open an empty process with pidfd_open(), configure it piece by piece through a new pidfd_config() call, then execute. That is the arrangement Christian Brauner sketched out in June, and it exists for one reason: posix_spawn() on Linux is still implemented on top of fork or vfork, and both of those are the wrong tool for the job. Nothing here is merged. The direction is what matters.
The short answer
A patch series from Li Chen, covered by LWN on August 4, shows what a process builder API could look like on Linux: create an empty process, configure it through repeated calls, then execute it. It follows Christian Brauner's suggestion of extending pidfd_open() and adding a pidfd_config() call in the style of fsconfig(). The goal is a posix_spawn() that no longer has to be built out of fork() or vfork(). None of it is merged, and the syscall surface is still being argued about.
Every program you have ever launched on Linux was created by a system call whose job is to make a copy of something you were about to discard. That has been true since before most of us were writing code, and it keeps working, which is why the itch to fix it never quite becomes an emergency.
On August 4, LWN published an article on the beginning of a process builder API, covering a patch series from Li Chen that puts running code behind a design that has been discussed on and off for years. Chen is the same developer behind the spawn template proposal from late May, and this series is a direct answer to the feedback that one received.
The thing fork() is bad at is the thing everyone uses it for
Start a process on Unix and the canonical sequence is fork() then exec(). The fork duplicates the calling process. The exec then replaces that duplicate entirely.
Copy on write means the pages are not physically copied, but the kernel still builds page tables for an address space that will exist for microseconds. The bill scales with the size of the parent, which produces the result every build engineer has run into: launching a trivial helper from a process with a large heap costs meaningfully more than launching it from a small one, for reasons that have nothing to do with the helper.
There is also the configuration problem, which gets less attention and causes more bugs. Everything you want the child to be different about (which file descriptors it keeps, what its signal handlers look like, where its working directory is, what limits apply) has to happen after the fork and before the exec, running inside a child that shares state with its parent in ways that are easy to get wrong.
vfork() and clone() with CLONE_VFORK cut the address space cost by having the child borrow the parent's memory and freezing the parent until it execs. LWN's earlier coverage of this thread called that pattern a serious footgun. It is fast, it is what real implementations use, and nobody defends it as good design.
Chen's first attempt, and why it did not survive contact
The May proposal took a different angle. It added spawn_template_create(), which builds a cached template from an executable, and spawn_template_spawn(), which launches from that template with per invocation arguments, environment and file descriptor actions carried in a spawn_template_action structure. The theory was that repeatedly launching the same binary should not repeat the same setup work.
The measured payoff was roughly two percent. That is not nothing, but it is not enough to justify two new system calls and a new caching layer in the kernel, and the reaction reflected that. Mateusz Guzik made the sharper version of the objection: the effort was going into shaving fork's overhead rather than removing the need to fork at all. What he wanted was the ability to create a pristine process, not a cheaper copy of an existing one.
What Brauner proposed, and what the new series demonstrates
Christian Brauner's counter proposal is the one that reframed the discussion, and it reuses machinery Linux already has rather than inventing a parallel mechanism.
pidfds are already the modern way to refer to a process: a file descriptor that identifies a specific process unambiguously, immune to the pid reuse races that made older APIs unsafe. Brauner's suggestion is to extend pidfd_open() so it can create an empty process rather than only pointing at an existing one, then add pidfd_config(), a call in the same spirit as fsconfig(), to describe that process incrementally before it runs.
The advantages fall out of the shape. Configuration happens in the parent, as ordinary system calls, with ordinary error returns. A failure halfway through is an errno, not an orphaned child. Nothing is duplicated, so the cost does not track the size of the caller. And because it is expressive enough to cover what posix_spawn() needs, glibc could implement posix_spawn() on top of it directly, instead of the current arrangement where the C library builds a POSIX interface out of a kernel primitive that was never meant for it.
Li Chen's new series is a demonstration of that idea with code attached. It is described openly as having been written with substantial LLM assistance, which is worth flagging in a week when the kernel staging tree announced it would reject LLM generated patches and GCC took a comparable line. Posting a demonstration into an open design thread is a different act from submitting a fix for merge, and it is being received as one.
What to do with this today
Nothing, and that is the honest answer. There is no interface to target, the syscall names could change, and the series is a conversation piece rather than a merge candidate.
What is worth doing is knowing which layer your own code sits on. If you spawn processes through posix_spawn(), Python's subprocess, Rust's std::process::Command or Java's ProcessBuilder, you are already insulated from whatever the kernel settles on, and you will inherit the improvement for free when it lands. If you have hand rolled fork and exec anywhere in a hot path, or reached for vfork deliberately because the fork cost showed up in a profile, this is the thread to follow. It is aimed exactly at you.
Kernel discussions that reach the running code stage tend to take a year or more to reach a released kernel, if they arrive at all. This one has a clear problem, an approach that two respected maintainers appear to agree on, and now a patch series concrete enough to argue with. That is further than this idea has ever been.
Sources and further reading
- The beginning of a process-builder API, LWN.net, August 4, 2026 (syndicated copy)
- Moving beyond fork() + exec(), LWN.net
- Reader discussion on the process-builder proposal, LWN.net
- Race-free process creation in the GNU C Library, LWN.net
- posix_spawn(3) manual page
- pidfd_open(), LWN.net
Frequently asked questions
What is actually wrong with fork() followed by exec()?
Nothing, if you look at it from 1975. The problem is that fork() promises a full duplicate of the calling process, and exec() immediately throws that duplicate away. Modern kernels avoid the literal memory copy through copy on write, but the page tables still have to be built, the address space still has to be walked, and the cost still scales with how big the parent is. A build server with a twelve gigabyte resident set pays more to launch /bin/true than a shell script does. On top of that, everything you want to configure about the child (file descriptors, signal dispositions, working directory, resource limits) has to be done in the window between the fork and the exec, inside a child that is running with a copy of the parent's state and very few safe things it is allowed to do.
Does vfork() not already solve the cost problem?
It solves the cost and creates a different one. vfork() and clone() with CLONE_VFORK avoid duplicating the address space, which is exactly the saving people want, but they do it by having the child share the parent's memory and suspending the parent until the child execs or exits. LWN's coverage of the earlier discussion described the pattern as a serious footgun, and that is a fair summary: any mistake in the child corrupts the parent, the set of operations that are actually legal in that window is narrow and poorly documented, and the parent is stalled for the duration. Library authors use it because the alternative is slower, not because it is comfortable.
So what does the builder approach look like in practice?
The version Christian Brauner outlined works like fsconfig() does for mounting a filesystem. You obtain a handle for a process that exists but has not started, then you make a series of calls against that handle to describe what the process should look like, then you commit. Each configuration step is a normal system call made by the parent, in the parent's context, with normal error handling. If a step fails you get an error return rather than a half constructed child you now have to reap. Li Chen's series is a demonstration of that shape rather than a finished interface, and the syscall surface is still open for argument.
Will this change anything in my code?
Not directly, and probably not for years. Almost nobody calls fork() and exec() by hand any more: you call posix_spawn(), or subprocess in Python, or Command in Rust, or ProcessBuilder in Java, and the runtime picks a mechanism underneath. That is precisely why this work matters. If glibc can implement posix_spawn() on a purpose built kernel interface instead of assembling it from vfork and a careful dance, then every one of those higher level APIs gets faster and less fragile without a single line changing in your program. The people who feel it first will be the ones running process heavy workloads: build systems, CI runners, shells, test harnesses.
The patch series was written with LLM assistance. Does that matter here?
It is worth noting because the kernel community spent the last week drawing lines on exactly this question. Greg Kroah-Hartman announced that the staging tree will reject LLM generated patches, and GCC took a similar position. A demonstration series that is openly described as heavily LLM assisted, posted into a design discussion rather than submitted for merge, sits in a different category from an unsolicited fix dropped on a maintainer. The value of this particular posting is that it makes an abstract API argument concrete enough to criticise. That is a legitimate use, and it is being read as one.