clone3: fork a process and get a stable handle in the same syscall
Published by RodHat · Updated

fork(2) gives you a child PID. clone(2) also gives you a child PID but with extra steps and a calling convention that varies by architecture: on x86-64 the args go (flags, stack, ptid, ctid, tls), on ARM they go (flags, stack, ptid, tls, ctid), and the man page spells this out in a sidebar that most people skip until they’re debugging a segfault on a machine that isn’t their laptop. The kernel devs knew this was embarrassing.
clone3(2) landed in Linux 5.3 and fixed it with a struct:
struct clone_args {
__u64 flags;
__u64 pidfd; /* [out] pidfd written here if CLONE_PIDFD */
__u64 child_tid; /* [out] CLONE_CHILD_SETTID */
__u64 parent_tid; /* [out] CLONE_PARENT_SETTID */
__u64 exit_signal; /* signal sent to parent on child exit */
__u64 stack;
__u64 stack_size;
__u64 tls;
/* Linux 5.5: */
__u64 set_tid; /* pointer to tid array, for checkpoint/restore */
__u64 set_tid_size;
/* Linux 5.7: */
__u64 cgroup; /* cgroup fd if CLONE_INTO_CGROUP */
};
Same extensible-struct pattern as openat2(2): pass sizeof(struct clone_args) as the size argument, zero-fill the struct (designated initializers handle this), and the kernel tells you with E2BIG if you passed a non-zero field it doesn’t understand instead of silently ignoring it.
There is no glibc wrapper. clone(2) has one; clone3 does not. Use syscall(SYS_clone3, ...) directly.
The only reason you actually need this: CLONE_PIDFD
The pidfd post covers the general pidfd API. The short version: a pidfd is a file descriptor that refers to a specific process instance, not a PID number. PID numbers get recycled; file descriptors do not. You can poll(2) a pidfd and it becomes readable when the process exits.
The problem with pidfd_open(2) on an existing process is that there is a window. You call fork(), get a PID, then call pidfd_open(pid, 0). Between those two calls the child can exit, get reaped, and the PID can land on a completely different process. pidfd_open hands you a handle to the wrong process and returns success.
CLONE_PIDFD closes the window:
#define _GNU_SOURCE
#include <linux/sched.h> /* struct clone_args, CLONE_PIDFD */
#include <sched.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#include <unistd.h>
#include <err.h>
int child_pidfd = -1;
struct clone_args args = {
.flags = CLONE_PIDFD,
.pidfd = (uint64_t)(uintptr_t)&child_pidfd,
.exit_signal = SIGCHLD,
};
pid_t pid = syscall(SYS_clone3, &args, sizeof(args));
if (pid < 0)
err(1, "clone3");
if (pid == 0) {
/* child */
execvp(argv[0], argv);
err(127, "exec");
}
/* parent: child_pidfd is a valid, race-free pidfd */
The kernel writes child_pidfd before returning in the parent. No window. The child hasn’t had a chance to exit yet, and even if it does before you use child_pidfd, you get ESRCH rather than a stale hit on a recycled PID.
CLONE_CLEAR_SIGHAND
If you are not calling exec(2) in the child (a fairly unusual case, but it comes up in worker pool implementations), you probably do not want to inherit all the signal handlers the parent had installed. Before clone3, clearing them meant calling signal(n, SIG_DFL) for every signal in a loop before doing useful work in the child. CLONE_CLEAR_SIGHAND (Linux 5.3, same release) does this for you: the child starts with all handlers reset to SIG_DFL. It has no effect on SIG_IGN handlers, which are inherited by the process image through exec(2) anyway per POSIX.
CLONE_INTO_CGROUP
Available since Linux 5.7. Pass CLONE_INTO_CGROUP in flags and a cgroup v2 fd in args.cgroup, and the child starts its life inside that cgroup instead of inheriting the parent’s. No fork-then-move: the child is never in the wrong cgroup, which matters if you are accounting resources or applying limits from the start.
int cg_fd = open("/sys/fs/cgroup/workers", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (cg_fd < 0)
err(1, "open cgroup");
struct clone_args args = {
.flags = CLONE_PIDFD | CLONE_INTO_CGROUP,
.pidfd = (uint64_t)(uintptr_t)&child_pidfd,
.cgroup = (uint64_t)cg_fd,
.exit_signal = SIGCHLD,
};
ENOSYS and kernel version
clone3 requires Linux 5.3. On older kernels syscall(SYS_clone3, ...) returns ENOSYS. If you need to support anything older than 5.3, detect ENOSYS and fall back to fork(2) plus pidfd_open(2) with the understanding that the race exists. On 5.3 or newer, clone3 with CLONE_PIDFD is strictly better.
pid_t pid = syscall(SYS_clone3, &args, sizeof(args));
if (pid < 0 && errno == ENOSYS) {
/* kernel < 5.3; fall back to fork + pidfd_open */
pid = fork();
if (pid > 0)
child_pidfd = syscall(SYS_pidfd_open, pid, 0);
}
FreeBSD
FreeBSD has had pdfork(2) since 9.0, which is the same idea: pdfork(&pd, 0) forks and writes a process descriptor to pd in a single operation. The FreeBSD API is simpler (no struct, just a pointer and flags), which reflects that it was designed for one purpose rather than growing to absorb a decade of clone feature requests. pdfork followed by pdkill and pdwait4 gives you the same race-free process management that Linux gets from clone3 plus the pidfd family. OpenBSD has nothing equivalent.
Related: pidfd: process file descriptors, openat2 RESOLVE flags, Landlock: unprivileged filesystem sandboxing.
Sources
- clone3(2) man page (man7.org)
- pdfork(2) man page, FreeBSD (FreeBSD)