Page Menu
Home
FreeBSD
Search
Configure Global Search
Log In
Files
F173205617
D58688.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
53 KB
Referenced Files
None
Subscribers
None
D58688.diff
View Options
diff --git a/lib/libsys/Symbol.sys.map b/lib/libsys/Symbol.sys.map
--- a/lib/libsys/Symbol.sys.map
+++ b/lib/libsys/Symbol.sys.map
@@ -392,9 +392,11 @@
FBSD_1.9 {
pddupfd;
+ pdexec;
pdopenpid;
pdrfork;
pdrfork_thread;
+ pdsetfd;
renameat2;
};
diff --git a/lib/libsys/_libsys.h b/lib/libsys/_libsys.h
--- a/lib/libsys/_libsys.h
+++ b/lib/libsys/_libsys.h
@@ -1,8 +1,10 @@
/*
* Public system call stubs provided by libsys.
*
+ *
* Do not use directly, include <libsys.h> instead.
*
+ *
* DO NOT EDIT-- this file is automatically @generated.
*/
@@ -477,6 +479,8 @@
typedef int (__sys_renameat2_t)(int, const char *, int, const char *, int);
typedef int (__sys_pdopenpid_t)(pid_t, int);
typedef int (__sys_pddupfd_t)(int, int, int);
+typedef int (__sys_pdsetfd_t)(int, int, int);
+typedef int (__sys_pdexec_t)(int, int, char **, char **, int);
_Noreturn void __sys__exit(int rval);
int __sys_fork(void);
@@ -889,6 +893,8 @@
int __sys_renameat2(int oldfd, const char * old, int newfd, const char * new, int flags);
int __sys_pdopenpid(pid_t pid, int flags);
int __sys_pddupfd(int pd, int fd, int flags);
+int __sys_pdsetfd(int procfd, int remotefd, int localfd);
+int __sys_pdexec(int procfd, int fd, char ** argv, char ** envv, int flags);
__END_DECLS
#endif /* __LIBSYS_H_ */
diff --git a/lib/libsys/pdfork.2 b/lib/libsys/pdfork.2
--- a/lib/libsys/pdfork.2
+++ b/lib/libsys/pdfork.2
@@ -30,7 +30,7 @@
.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
.\" SUCH DAMAGE.
.\"
-.Dd April 19, 2026
+.Dd July 23, 2026
.Dt PDFORK 2
.Os
.Sh NAME
@@ -40,7 +40,9 @@
.Nm pdgetpid ,
.Nm pdkill ,
.Nm pdwait ,
-.Nm pddupfd
+.Nm pddupfd ,
+.Nm pdexec ,
+.Nm pdsetfd
.Nd System calls to manage process descriptors
.Sh LIBRARY
.Lb libc
@@ -66,6 +68,10 @@
.Fc
.Ft int
.Fn pddupfd "int fd" "int remotefd" "int flags"
+.Ft int
+.Fn pdexec "int fd" "int execfd" "char **argv" "char **envv" "int flags"
+.Ft int
+.Fn pdsetfd "int fd" "int remotefd" "int localfd"
.Sh DESCRIPTION
Process descriptors are special file descriptors that represent processes,
and are created using
@@ -197,6 +203,156 @@
argument is reserved and must be zero.
Certain file descriptor types cannot be copied this way, namely
kqueues.
+.Ss Creating a process without fork
+The
+.Fn pdrfork
+function with an
+.Fa rfflags
+of
+.Dv RFEMBRYO
+creates an
+.Em embryonic
+process: one that exists but has never run.
+It has no address space and no program, an empty file descriptor table
+and default signal handling.
+Because an embryo inherits nothing,
+.Dv RFEMBRYO
+describes the new process completely and may not be combined with the
+other
+.Xr rfork 2
+flags, which say what is to be inherited.
+On success, a process descriptor for it is stored in the integer pointed
+to by
+.Fa fdp ,
+the pid of the new process is returned, and the
+.Fa pdflags
+argument takes the same flags as
+.Fn pdfork .
+.Pp
+Unlike the traditional
+.Fn fork Ns / Ns Fn execve
+pattern, the new process is never a copy of the parent: its descriptors
+are configured explicitly rather than inherited and then closed.
+.Pp
+The
+.Fn pdexec
+function gives the embryonic process a program and runs it.
+The executable referenced by the file descriptor
+.Fa execfd
+is loaded (as with
+.Xr fexecve 2 ) ,
+and the
+.Fa argv
+and
+.Fa envv
+arguments specify the argument and environment vectors.
+The
+.Fa flags
+argument is reserved and must be zero.
+.Pp
+Loading a program and running it is one operation, which is what
+.Xr execve 2
+does, aimed at another process rather than at the caller.
+Leaving the process stopped with its program in place, so that it can be
+configured further once the image is known, is future work.
+.Pp
+The exec is performed by the target process itself, on its own thread and
+in its own address space, rather than by the caller.
+The process therefore has no address space at all until
+.Fn pdexec ,
+and a failure to load the program is fatal to it: there is nothing for a
+process that has never run to fall back to, so it is destroyed and its
+creator learns of this through the process descriptor.
+.Pp
+.Fn pdexec
+requires the
+.Dv CAP_PDKILL
+capability right on
+.Fa fd
+in addition to
+.Dv CAP_PDSETFD ,
+since starting a process is the same authority that killing one is
+gated by.
+.Pp
+The
+.Fn pdsetfd
+function installs a file descriptor into the new process before it is
+started, mirroring
+.Fn pddupfd .
+The descriptor
+.Fa localfd
+of the calling process is duplicated as descriptor
+.Fa remotefd
+in the process specified by the
+.Fa fd
+process descriptor.
+It may be called multiple times to set up the new process's standard
+I/O and any other descriptors it needs.
+If
+.Fa remotefd
+is already set, it is replaced.
+The retained capability rights of
+.Fa localfd
+are preserved on
+.Fa remotefd .
+.Fn pdsetfd
+requires the
+.Dv CAP_PDSETFD
+capability right on
+.Fa fd ;
+see
+.Sx SECURITY CONSIDERATIONS .
+.Pp
+When descriptor installation into the embryonic process is
+.Em restricted
+(see
+.Sx SECURITY CONSIDERATIONS ) ,
+.Fn pdsetfd
+refuses, with
+.Er EPERM ,
+to install as
+.Fa remotefd
+0, 1 or 2 a descriptor whose backing may change out from under the
+process after it starts, such as a
+.Xr procfs 5
+node.
+These descriptors carry implicit significance as the standard input,
+output and error streams, so the program may write to them without ever
+naming them; the same protection is applied to a set-user-ID or
+set-group-ID program's descriptors across
+.Xr execve 2 .
+Descriptors numbered 3 and above are not restricted.
+(The first time it is scheduled, execution begins.)
+After this call, the process descriptor behaves identically to one
+created by
+.Fn pdfork .
+.Pp
+An embryonic process inherits as little as possible from the process
+that created it.
+Where the traditional pattern begins with a copy of the parent and
+undoes whatever is unwanted, this one begins with nothing and adds
+whatever is wanted: inheritance is opt-in.
+The file descriptor table is empty, all signals have their default
+disposition and none are blocked, and no memory is shared.
+What the new process does take from its creator is limited to the
+properties it must have in order to exist at all: its credentials,
+resource limits, process group, current working directory and root
+directory, and scheduling parameters.
+.Pp
+A consequence worth noting is that state
+.Xr execve 2
+would have preserved is
+.Em not
+preserved here, because there is no prior program to preserve it from.
+Signals set to
+.Dv SIG_IGN
+by the creator, for instance, are reset to their default disposition
+rather than remaining ignored.
+Callers emulating
+.Xr fork 2 Ns / Ns Xr execve 2
+semantics, such as an implementation of
+.Xr posix_spawn 3 ,
+must therefore establish such state explicitly.
.Sh INTERACTION OF PROCESS DESCRIPTORS AND Xr WAITPID 2
.Pp
The
@@ -304,8 +460,10 @@
.Fn pdgetpid ,
.Fn pdkill ,
.Fn pdwait ,
+.Fn pddupfd ,
+.Fn pdrfork ,
and
-.Fn pddupfd
+.Fn pdsetfd
return 0 on success and -1 on failure.
.Sh ERRORS
These functions may return the same error numbers as their PID-based equivalents
@@ -410,6 +568,118 @@
refers to a file that cannot be duplicated across a process boundary,
such as a kqueue.
.El
+.Pp
+The
+.Fn pdrfork
+.Pp
+The
+.Fn pdsetfd
+function may fail with:
+.Bl -tag -width Er
+.It Bq Er EBADF
+.Fa fd
+or
+.Fa localfd
+is not a valid file descriptor.
+.It Bq Er EINVAL
+.Fa fd
+is not a process file descriptor.
+.It Bq Er EINVAL
+The process specified by the
+.Fa fd
+process descriptor has already been started.
+.It Bq Er EPERM
+Descriptor installation into the embryonic process is
+.Em restricted
+(see
+.Sx SECURITY CONSIDERATIONS ) ,
+.Fa remotefd
+is 0, 1 or 2, and
+.Fa localfd
+references a file whose backing may change after execution begins.
+.El
+.Sh SECURITY CONSIDERATIONS
+Like most types of file descriptors, process descriptors may be passed
+to another process.
+A process created with
+.Fn pdrfork
+runs with the credentials of the process that called
+.Fn pdrfork ,
+.Em not
+the credentials of whichever process eventually starts it.
+Therefore, handing off a process descriptor to an embryonic process
+should be considered to have security risks similar to marking a binary
+set-user-ID \(em in both cases a possibly less-privileged process has
+the ability to initialize a more-privileged process with resources,
+which can lead to
+.Em confused deputy
+privilege escalations if the executable in question was not specifically
+designed with this use case in mind.
+.Pp
+This situation motivates the notion of a
+.Em restricted
+embryonic process.
+Descriptor installation with
+.Fn pdsetfd
+is restricted when either of the following holds:
+.Bl -bullet
+.It
+The embryonic process is executing a set-user-ID or set-group-ID
+program.
+.It
+The calling process runs as a different user or group than the
+embryonic process, and the embryonic process will not run in capability
+mode (see
+.Xr capsicum 4 ) .
+.El
+.Pp
+Two aspects of this condition warrant explanation:
+.Bl -bullet
+.It
+The set-user-ID and set-group-ID condition is largely redundant: it adds
+coverage only when the embryonic process's set IDs coincide with the
+calling process's, or when the embryonic process is in capability mode
+(where the second condition does not apply).
+It could be argued that no restriction is warranted in those cases, but
+out of an abundance of caution, and out of respect for the intent of the
+set-user-ID and set-group-ID bits, the restriction is applied anyway.
+.It
+Capability mode is excluded from the second condition because a
+sandboxed embryonic process's ambient credentials are not meant to
+matter: all authority should stem from the open file descriptors alone.
+.El
+.Pp
+For a restricted process,
+.Fn pdsetfd
+declines to install a descriptor referring to a file deemed unsafe as
+descriptor 0, 1 or 2, protecting those descriptor numbers from certain
+confused-deputy problems.
+(The
+.Sx DESCRIPTION
+describes which files are deemed unsafe.)
+However, this still leaves higher-numbered descriptors unprotected, and
+low-numbered ones still only semi-protected, as confused-deputy
+problems involving a
+.Dq safe
+descriptor at 0, 1 or 2 are still possible.
+.Pp
+To mitigate these remaining issues,
+the operations on an unstarted process are gated by separate capability
+rights so that authority can be delegated at a fine granularity:
+.Fn pdsetfd
+requires
+.Dv CAP_PDSETFD ,
+while
+.Xr pdkill 2
+merely requires
+.Dv CAP_PDKILL .
+If the
+.Fn pdrfork
+caller uses
+.Xr cap_rights_limit 2
+to delegate a descriptor that permits starting or killing the process
+without permitting its descriptor table to be reconfigured, then the
+security concern is mitigated.
.Sh SEE ALSO
.Xr close 2 ,
.Xr fork 2 ,
@@ -440,6 +710,12 @@
.Fn pddupfd
system calls first appeared in
.Fx 16.0 .
+The
+.Fn pdrfork
+and
+.Fn pdsetfd
+system calls first appeared in
+.Fx 16.0 .
.Pp
Support for process descriptors mode was developed as part of the
.Tn TrustedBSD
@@ -466,3 +742,9 @@
.Fn pddupfd
functions were developed by
.An Konstantin Belousov Aq Mt kib@FreeBSD.org .
+The
+.Fn pdrfork
+and
+.Fn pdsetfd
+functions were developed by
+.An John Ericson Aq Mt subscribe@johnericson.me .
diff --git a/lib/libsys/syscalls.map b/lib/libsys/syscalls.map
--- a/lib/libsys/syscalls.map
+++ b/lib/libsys/syscalls.map
@@ -1,6 +1,7 @@
/*
* FreeBSD system call symbols.
*
+ *
* DO NOT EDIT-- this file is automatically @generated.
*/
@@ -829,4 +830,8 @@
__sys_pdopenpid;
_pddupfd;
__sys_pddupfd;
+ _pdsetfd;
+ __sys_pdsetfd;
+ _pdexec;
+ __sys_pdexec;
};
diff --git a/share/man/man4/rights.4 b/share/man/man4/rights.4
--- a/share/man/man4/rights.4
+++ b/share/man/man4/rights.4
@@ -484,6 +484,9 @@
.It Dv CAP_PDKILL
Permit
.Xr pdkill 2 .
+.It Dv CAP_PDSETFD
+Permit
+.Xr pdsetfd 2 .
.It Dv CAP_PTRACE
Permit debugging using
.Xr ptrace 2
diff --git a/sys/conf/files b/sys/conf/files
--- a/sys/conf/files
+++ b/sys/conf/files
@@ -4039,6 +4039,7 @@
kern/sys_getrandom.c standard
kern/sys_pipe.c standard
kern/sys_procdesc.c standard
+kern/sys_pdnew.c standard
kern/sys_process.c standard
kern/sys_socket.c standard
kern/sys_timerfd.c standard
diff --git a/sys/kern/init_sysent.c b/sys/kern/init_sysent.c
--- a/sys/kern/init_sysent.c
+++ b/sys/kern/init_sysent.c
@@ -1,6 +1,7 @@
/*
* System call switch table.
*
+ *
* DO NOT EDIT-- this file is automatically @generated.
*/
@@ -671,4 +672,6 @@
{ .sy_narg = AS(renameat2_args), .sy_call = (sy_call_t *)sys_renameat2, .sy_auevent = AUE_RENAMEAT, .sy_flags = SYF_CAPENABLED, .sy_thrcnt = SY_THR_STATIC }, /* 602 = renameat2 */
{ .sy_narg = AS(pdopenpid_args), .sy_call = (sy_call_t *)sys_pdopenpid, .sy_auevent = AUE_PDOPENPID, .sy_flags = 0, .sy_thrcnt = SY_THR_STATIC }, /* 603 = pdopenpid */
{ .sy_narg = AS(pddupfd_args), .sy_call = (sy_call_t *)sys_pddupfd, .sy_auevent = AUE_NULL, .sy_flags = 0, .sy_thrcnt = SY_THR_STATIC }, /* 604 = pddupfd */
+ { .sy_narg = AS(pdsetfd_args), .sy_call = (sy_call_t *)sys_pdsetfd, .sy_auevent = AUE_NULL, .sy_flags = SYF_CAPENABLED, .sy_thrcnt = SY_THR_STATIC }, /* 605 = pdsetfd */
+ { .sy_narg = AS(pdexec_args), .sy_call = (sy_call_t *)sys_pdexec, .sy_auevent = AUE_NULL, .sy_flags = SYF_CAPENABLED, .sy_thrcnt = SY_THR_STATIC }, /* 606 = pdexec */
};
diff --git a/sys/kern/kern_descrip.c b/sys/kern/kern_descrip.c
--- a/sys/kern/kern_descrip.c
+++ b/sys/kern/kern_descrip.c
@@ -2346,6 +2346,70 @@
return (error);
}
+/*
+ * Install a file at a caller-chosen descriptor number in the given file
+ * descriptor table, growing the table if necessary and evicting any
+ * descriptor already at that slot. A new reference to fp is taken; the
+ * caller retains its own reference.
+ *
+ * This is used to build up the descriptor table of a not-yet-running
+ * process, where the caller dictates exact descriptor numbers (as with
+ * posix_spawn file actions) rather than letting the kernel allocate the
+ * lowest free one. The target table must not be concurrently accessed.
+ */
+int
+finstall_at(struct thread *td, struct proc *p, struct file *fp,
+ int atfd, int flags, struct filecaps *fcaps)
+{
+ struct filedesc *fdp = p->p_fd;
+ struct file *oldfp;
+ int maxfd;
+
+ if (atfd < 0)
+ return (EBADF);
+ /*
+ * Bound atfd by the file-descriptor limit of p, the process whose
+ * table receives the descriptor -- not td's process, since pdsetfd(2)
+ * installs into an embryonic process's table on behalf of a caller
+ * whose RLIMIT_NOFILE may differ. td is still used below to close any
+ * descriptor being replaced.
+ *
+ * lim_cur_proc() requires the process lock; no caller holds it here,
+ * and it is dropped again before FILEDESC_XLOCK below.
+ */
+ PROC_LOCK(p);
+ maxfd = min((int)lim_cur_proc(p, RLIMIT_NOFILE), maxfilesperproc);
+ PROC_UNLOCK(p);
+ if (atfd >= maxfd)
+ return (EMFILE);
+ if (!fhold(fp))
+ return (EBADF);
+
+ FILEDESC_XLOCK(fdp);
+ if (atfd >= fdp->fd_nfiles)
+ fdgrowtable(fdp, atfd + 1);
+
+ oldfp = fdp->fd_ofiles[atfd].fde_file;
+ if (oldfp != NULL) {
+#ifdef CAPABILITIES
+ seqc_write_begin(&fdp->fd_ofiles[atfd].fde_seqc);
+#endif
+ fdp->fd_ofiles[atfd].fde_file = NULL;
+#ifdef CAPABILITIES
+ seqc_write_end(&fdp->fd_ofiles[atfd].fde_seqc);
+#endif
+ fdefree_last(&fdp->fd_ofiles[atfd]);
+ } else {
+ fdused(fdp, atfd);
+ }
+ _finstall(fdp, fp, atfd, flags, fcaps);
+ FILEDESC_XUNLOCK(fdp);
+
+ if (oldfp != NULL)
+ (void)fdrop(oldfp, td);
+ return (0);
+}
+
/*
* Build a new filedesc structure from another.
*
@@ -2821,8 +2885,8 @@
* Since fdsetugidsafety calls this only for fd 0, 1 and 2, this check is
* sufficient. We also don't check for setugidness since we know we are.
*/
-static bool
-is_unsafe(struct file *fp)
+bool
+fdesc_is_unsafe(struct file *fp)
{
struct vnode *vp;
@@ -2849,7 +2913,7 @@
MPASS(fdp->fd_nfiles >= 3);
for (i = 0; i <= 2; i++) {
fp = fdp->fd_ofiles[i].fde_file;
- if (fp != NULL && is_unsafe(fp)) {
+ if (fp != NULL && fdesc_is_unsafe(fp)) {
FILEDESC_XLOCK(fdp);
knote_fdclose(td, i);
/*
diff --git a/sys/kern/kern_exit.c b/sys/kern/kern_exit.c
--- a/sys/kern/kern_exit.c
+++ b/sys/kern/kern_exit.c
@@ -130,7 +130,7 @@
return (parent);
}
-static void
+void
reaper_clear(struct proc *p, struct proc *rp)
{
struct proc *p1;
diff --git a/sys/kern/kern_fork.c b/sys/kern/kern_fork.c
--- a/sys/kern/kern_fork.c
+++ b/sys/kern/kern_fork.c
@@ -213,6 +213,33 @@
return (EXTERROR(EINVAL,
"Kernel-only flags %#jx", uap->rfflags));
+ /*
+ * RFEMBRYO creates a process with no address space and no program,
+ * to be filled in with pdexec(2) and released with pdstart(2).
+ * Almost every other rfork(2) flag describes what the new process
+ * inherits, and an embryo inherits nothing, so it must appear alone.
+ */
+ if ((uap->rfflags & RFEMBRYO) != 0) {
+ pid_t epid;
+
+ if (uap->rfflags != RFEMBRYO)
+ return (EXTERROR(EINVAL,
+ "RFEMBRYO must be the only flag %#jx",
+ uap->rfflags));
+ if ((uap->pdflags & ~PD_ALLOWED_AT_NEW) != 0)
+ return (EXTERROR(EINVAL,
+ "Bad pdflags %#jx", uap->pdflags));
+ error = kern_pdnew(td, uap->pdflags, &fd, &epid);
+ if (error != 0)
+ return (error);
+ td->td_retval[0] = epid;
+ td->td_retval[1] = 0;
+ error = copyout(&fd, uap->fdp, sizeof(fd));
+ if (error != 0)
+ kern_close(td, fd);
+ return (error);
+ }
+
/* RFSPAWN must not appear with others */
if ((uap->rfflags & RFSPAWN) != 0) {
if (uap->rfflags != RFSPAWN)
@@ -1395,6 +1422,122 @@
#endif
}
+/*
+ * Destroy an embryonic process that was never started.
+ * Called when pdnew() exec fails or when the procdesc is closed
+ * before pdstart().
+ */
+void
+proc_destroy_embryonic(struct proc *p)
+{
+ struct thread *td;
+
+ PROC_LOCK_ASSERT(p, MA_NOTOWNED);
+ MPASS(p->p_state == PRS_NEW);
+ MPASS(p->p_flag & P_INEXEC);
+
+ td = FIRST_THREAD_IN_PROC(p);
+
+ /*
+ * Remove from process group.
+ */
+ sx_xlock(&proctree_lock);
+ PROC_LOCK(p);
+ proc_clear_orphan(p);
+ LIST_REMOVE(p, p_sibling);
+ PROC_UNLOCK(p);
+ /* Remove from the reaper's list and release the reaper ID if last. */
+ reaper_clear(p, p->p_reaper);
+
+ PGRP_LOCK(p->p_pgrp);
+ LIST_REMOVE(p, p_pglist);
+ PGRP_UNLOCK(p->p_pgrp);
+
+ /*
+ * Release the PID. The process descriptor was already disconnected
+ * by procdesc_close(), so there is no procdesc_reap() to do it.
+ */
+ proc_id_clear(PROC_ID_PID, p->p_pid);
+ sx_xunlock(&proctree_lock);
+
+ /*
+ * Remove from global lists.
+ */
+ sx_xlock(PIDHASHLOCK(p->p_pid));
+ LIST_REMOVE(p, p_hash);
+ sx_xunlock(PIDHASHLOCK(p->p_pid));
+
+ tidhash_remove(td);
+
+ sx_xlock(&allproc_lock);
+ LIST_REMOVE(p, p_list);
+ allproc_gen++;
+ /* Undo the prison_proc_link() done by fork_register_proc(). */
+ prison_proc_unlink(p->p_ucred->cr_prison, p);
+ sx_xunlock(&allproc_lock);
+
+ /*
+ * Release resources.
+ */
+ if (p->p_textvp != NULL)
+ vrele(p->p_textvp);
+ if (p->p_textdvp != NULL)
+ vrele(p->p_textdvp);
+ free(p->p_binname, M_PARGS);
+ pargs_drop(p->p_args);
+
+ /*
+ * Free file descriptors and pwd using the embryonic thread.
+ * It is not running, but fdescfree/pdescfree just need
+ * td->td_proc to be correct.
+ */
+ fdescfree(FIRST_THREAD_IN_PROC(p));
+ pdescfree(FIRST_THREAD_IN_PROC(p));
+
+ sigacts_free(p->p_sigacts);
+ if (p->p_vmspace != NULL)
+ vmspace_free(p->p_vmspace);
+
+ callout_drain(&p->p_itcallout);
+ /*
+ * lim_fork() unconditionally callout_init_mtx()s p_limco and arms it
+ * when the parent has a finite RLIMIT_CPU; drain it as exit1() does,
+ * or it fires on recycled proc memory ~1s after the embryo is freed.
+ */
+ callout_drain(&p->p_limco);
+
+ /*
+ * Release the copy-on-write references (credentials, limits) that
+ * thread_cow_get_proc() cached in the embryonic thread; otherwise
+ * they dangle once the proc/thread is recycled via proc_zone.
+ */
+ thread_cow_free(td);
+
+ lim_free(p->p_limit);
+ /*
+ * p_stats (and the thread, p_ksi) are allocated in proc_init() and
+ * freed in proc_fini(); they ride with the proc through proc_zone
+ * recycling, so must NOT be freed here.
+ */
+
+ PROC_LOCK(p);
+ knlist_detach(p->p_klist);
+ p->p_klist = NULL;
+ PROC_UNLOCK(p);
+
+ prison_proc_free(p->p_ucred->cr_prison);
+
+#ifdef MAC
+ mac_proc_destroy(p);
+#endif
+ racct_proc_exit(p);
+ proc_unset_cred(p, true);
+
+ /* Releases the last tree reference and frees the proc via proc_zone. */
+ PROC_TREE_UNREF(p);
+ atomic_add_int(&nprocs, -1);
+}
+
static void
fork_init(void *arg __unused)
{
diff --git a/sys/kern/kern_resource.c b/sys/kern/kern_resource.c
--- a/sys/kern/kern_resource.c
+++ b/sys/kern/kern_resource.c
@@ -1494,7 +1494,13 @@
{
struct proc *p = td->td_proc;
- MPASS(td == curthread);
+ /*
+ * td->td_limit is a per-thread copy-on-write cache, safe to read
+ * locklessly by the owning thread. An embryonic process (pdnew(2))
+ * is not yet running, so its thread's cache is likewise stable and
+ * may be read by the caller setting it up.
+ */
+ MPASS(td == curthread || (p->p_flag & P_INEXEC) != 0);
KASSERT(which >= 0 && which < RLIM_NLIMITS,
("request for invalid resource limit"));
*rlp = td->td_limit->pl_rlimit[which];
diff --git a/sys/kern/sys_pdnew.c b/sys/kern/sys_pdnew.c
new file mode 100644
--- /dev/null
+++ b/sys/kern/sys_pdnew.c
@@ -0,0 +1,662 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 John Ericson
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+/*
+ * Embryonic process creation: pdrfork(RFEMBRYO), pdsetfd().
+ *
+ * pdrfork() with RFEMBRYO creates an unscheduled process with no address
+ * space and no program, returning a process descriptor fd. pdsetfd()
+ * installs file descriptors into it.
+ */
+
+#include <sys/param.h>
+#include <sys/systm.h>
+#include <sys/acct.h>
+#include <sys/capsicum.h>
+#include <sys/eventhandler.h>
+#include <sys/exec.h>
+#include <sys/fcntl.h>
+#include <sys/filedesc.h>
+#include <sys/imgact.h>
+#include <sys/imgact_elf.h>
+#include <sys/jail.h>
+#include <sys/kernel.h>
+#include <sys/kthread.h>
+#include <sys/lock.h>
+#include <sys/malloc.h>
+#include <sys/mount.h>
+#include <sys/mutex.h>
+#include <sys/namei.h>
+#include <sys/priv.h>
+#include <sys/proc.h>
+#include <sys/procdesc.h>
+#include <sys/ptrace.h>
+#include <sys/racct.h>
+#include <sys/resourcevar.h>
+#include <sys/sched.h>
+#include <sys/sdt.h>
+#include <sys/signalvar.h>
+#include <sys/stat.h>
+#include <sys/sx.h>
+#include <sys/syscallsubr.h>
+#include <sys/sysctl.h>
+#include <sys/sysent.h>
+#include <sys/sysproto.h>
+#include <sys/unistd.h>
+#include <sys/vnode.h>
+
+#include <security/audit/audit.h>
+#include <security/mac/mac_framework.h>
+
+#include <vm/vm.h>
+#include <vm/pmap.h>
+#include <vm/vm_extern.h>
+#include <vm/uma.h>
+
+SDT_PROVIDER_DECLARE(proc);
+SDT_PROBE_DECLARE(proc, , , create);
+SDT_PROBE_DECLARE(proc, , , exec);
+SDT_PROBE_DECLARE(proc, , , exec__failure);
+SDT_PROBE_DECLARE(proc, , , exec__success);
+
+/*
+ * Resolve a process descriptor to its process, returning with the
+ * process locked and holding a reference to the process-descriptor
+ * file in *fpp. The caller must fdrop(*fpp) once it is done with the
+ * process.
+ *
+ * That held reference is what keeps the embryonic process alive after
+ * the caller drops the process lock: without it, a concurrent close(2)
+ * of the descriptor (e.g. from a sibling thread sharing the fd table)
+ * could be the last reference, run proc_destroy_embryonic(), and free
+ * the process out from under the caller. On error *fpp is NULL.
+ */
+static int
+procdesc_find(struct thread *td, int pdfd, const cap_rights_t *cap_rights,
+ struct proc **pp, struct file **fpp)
+{
+ int error;
+
+ sx_slock(&proctree_lock);
+ error = fget_procdesc(td, pdfd, cap_rights, EINVAL, fpp, NULL, pp);
+ sx_sunlock(&proctree_lock);
+ return (error);
+}
+
+/*
+ * kern_pdnew: allocate an embryonic process.
+ *
+ * The process is created with no address space and no image: a bare process
+ * object with an empty descriptor table, default signal dispositions, and
+ * nothing blocked. It is in PRS_NEW with P_INEXEC set, and cannot be
+ * started until pdexec() gives it something to run.
+ *
+ * Splitting allocation from image loading keeps the interface a builder --
+ * allocate, configure, commit -- rather than having creation silently also
+ * decide what the process will be.
+ */
+int
+kern_pdnew(struct thread *td, int flags, int *fdp, pid_t *pidp)
+{
+ struct proc *p1, *p2;
+ struct thread *td2;
+ struct filedesc *newfd;
+ struct pwddesc *newpd;
+ struct sigacts *newsigacts;
+ struct file *fp_procdesc;
+ struct filecaps fcaps;
+ int error, fd_num;
+
+ p1 = td->td_proc;
+
+
+ /*
+ * Allocate the process descriptor fd in the parent.
+ *
+ * As for pdfork(2), CAP_PTRACE is withheld unless the caller asks for
+ * it: a descriptor must be deliberately prepared for debugging before
+ * it confers that authority, which matters all the more here, where
+ * the descriptor is routinely handed to someone else to start.
+ */
+ filecaps_fill(&fcaps);
+ if ((flags & PD_PTRACE_CAP) == 0)
+ cap_rights_clear(&fcaps.fc_rights, CAP_PTRACE);
+ error = procdesc_falloc(td, &fp_procdesc, &fd_num, flags, &fcaps);
+ if (error != 0) {
+ filecaps_free(&fcaps);
+ return (error);
+ }
+
+ error = fork_alloc_proc(td, 0, &p2, &td2);
+ if (error != 0) {
+ fdclose(td, fp_procdesc, fd_num);
+ fdrop(fp_procdesc, td);
+ return (error);
+ }
+
+ fork_register_proc(p2, td2, 0);
+
+ /* Fresh file descriptors — empty table. */
+ newpd = pdinit(p1->p_pd, false);
+ newfd = fdinit();
+ newsigacts = sigacts_alloc();
+
+ bzero(&p2->p_startzero,
+ __rangeof(struct proc, p_startzero, p_endzero));
+ /*
+ * Inherit the copied fields (e.g. p_reapsubtree, the reaper
+ * subtree this process belongs to) from the caller, as do_fork()
+ * does; fork_proc_tree() overrides p_reapsubtree for a reaper root.
+ */
+ bcopy(&p1->p_startcopy, &p2->p_startcopy,
+ __rangeof(struct proc, p_startcopy, p_endcopy));
+
+ PROC_LOCK(p2);
+
+ bzero(&td2->td_startzero,
+ __rangeof(struct thread, td_startzero, td_endzero));
+ /*
+ * Unlike do_fork(), do not bulk-copy the caller's td_startcopy
+ * range. An embryo is not a copy of its creator, so inheritance
+ * here is opt-in: start from zero and take only what the new thread
+ * genuinely needs.
+ *
+ * Bulk-copying would be actively wrong, not merely impure. That
+ * range holds userspace pointers -- the robust mutex list heads, the
+ * fast sigblock word, the extended error pointer -- which name
+ * addresses in the *caller's* address space. The embryo gets a
+ * fresh vmspace from exec, where those addresses mean something
+ * else entirely; umtx_thread_cleanup() would later write
+ * OWNER_DIED bits through them into unrelated memory of the new
+ * program. execve() zeroes them via umtx_exec() and
+ * sigfastblock_clear(), but neither can run here: umtx_exec()
+ * asserts p == curproc, and sigfastblock_clear() acts on curthread,
+ * which is the caller rather than td2. Zeroing is the correct
+ * post-exec state (cf. thread_create(), kern_thr.c).
+ *
+ * td_sigmask is deliberately left empty rather than inherited: a
+ * process created this way blocks nothing until it says otherwise.
+ * (execve() preserves the mask, but there is no prior program here
+ * whose mask could be preserved.)
+ */
+ bzero(&td2->td_startcopy,
+ __rangeof(struct thread, td_startcopy, td_endcopy));
+
+ /*
+ * Scheduling parameters are the one thing inherited: sched_fork()
+ * below derives the child's priority from td_base_pri, so these
+ * must already be set when it runs.
+ */
+ td2->td_rqindex = td->td_rqindex;
+ td2->td_base_pri = td->td_base_pri;
+ td2->td_priority = td->td_priority;
+ td2->td_pri_class = td->td_pri_class;
+ td2->td_user_pri = td->td_user_pri;
+ td2->td_base_user_pri = td->td_base_user_pri;
+ td2->td_flags = TDF_INMEM;
+ td2->td_lend_user_pri = PRI_MAX;
+
+ thread_lock(td);
+ sched_fork(td, td2);
+ thread_unlock(td);
+
+ p2->p_flag = P_INMEM | P_INEXEC;
+ p2->p_flag2 = 0;
+ p2->p_swtick = ticks;
+
+ p2->p_sigacts = newsigacts;
+ p2->p_sigparent = SIGCHLD;
+
+ p2->p_textvp = NULL;
+ p2->p_textdvp = NULL;
+ p2->p_binname = NULL;
+ /*
+ * p_args is in the p_startcopy range, so the bcopy above left a
+ * borrowed reference to the caller's args. Clear it: nothing reads
+ * an embryo's p_args before exec_finalize() installs an owned copy,
+ * and this keeps proc_destroy_embryonic()'s pargs_drop() correct on
+ * the failure path (NULL) as well as the success path (owned).
+ */
+ p2->p_args = NULL;
+
+ p2->p_fd = newfd;
+ p2->p_fdtol = NULL;
+ p2->p_pd = newpd;
+ p2->p_vmspace = NULL; /* pdexec(2) gives it one */
+
+ /* lim_fork() requires both process locks; p_limit is copy-on-write. */
+ PROC_LOCK(p1);
+ lim_fork(p1, p2);
+ thread_cow_get_proc(td2, p2);
+ pstats_fork(p1->p_stats, p2->p_stats);
+ PROC_UNLOCK(p1);
+
+ p2->p_sysent = p1->p_sysent;
+
+ PROC_UNLOCK(p2);
+
+ /* Process group and parent attachment. */
+ p2->p_peers = NULL;
+ p2->p_leader = p2;
+
+ sx_xlock(&proctree_lock);
+ PGRP_LOCK(p1->p_pgrp);
+ PROC_LOCK(p2);
+ PROC_LOCK(p1);
+ fork_proc_tree(p1, p2, false);
+
+ /* Set up the process descriptor. */
+ procdesc_new(p2, flags);
+
+ /*
+ * Account for the references that will hold the zombie once the process
+ * exits, exactly as do_fork() does for an RFPROCDESC child: the
+ * descriptor always keeps one, and unless the caller opted out with
+ * PD_NOWAITPID the creating process keeps another so it can waitpid(2)
+ * the child -- the fork()+exec() semantics posix_spawn(3) relies on.
+ */
+ p2->p_zombieref = PZOMBIEREF_PROCDESC;
+ if ((flags & PD_NOWAITPID) == 0)
+ p2->p_zombieref |= PZOMBIEREF_PARENT | PZOMBIEREF_NEEDPARENT;
+
+ /*
+ * Do NOT announce the new process to fork observers here. An embryo
+ * that is destroyed before pdstart() never runs, so process_fork and
+ * process_exit must never see it: an unbalanced fork leaks per-proc
+ * state in handlers that pair the two (filemon, hwpmc). The
+ * process_fork event and the proc:::create probe are fired from
+ * pdstart() instead, once the process commits to running.
+ */
+
+ /*
+ * Link the process descriptor to p2. Our reference is held until
+ * construction finishes below: without it a concurrent close(2) of
+ * the descriptor (from a sibling thread that guessed fd_num) could be
+ * the last reference, run proc_destroy_embryonic(), and free p2 out
+ * from under us.
+ */
+ procdesc_finit(p2->p_procdesc, fp_procdesc);
+
+ racct_proc_fork_done(p2);
+ /*
+ * The process is complete as far as it goes. Drop our construction
+ * reference; the descriptor fd now keeps it alive.
+ */
+ if (pidp != NULL)
+ *pidp = p2->p_pid;
+ fdrop(fp_procdesc, td);
+ *fdp = fd_num;
+ return (0);
+}
+
+
+int
+sys_pdsetfd(struct thread *td, struct pdsetfd_args *uap)
+{
+ struct proc *p;
+ struct file *fp, *fp_pd;
+ struct filecaps fcaps;
+ cap_rights_t rights;
+ bool restricted;
+ int error;
+
+ /*
+ * pdsetfd() gets its own `CAP_*` unlike the other two syscalls
+ * in the non-fork-exec process spawning lifecycle (pdnew() and
+ * pdstart()). See the man pages for why.
+ */
+ error = procdesc_find(td, uap->procfd,
+ cap_rights_init(&rights, CAP_PDSETFD), &p, &fp_pd);
+ if (error != 0)
+ return (error);
+
+ /* Must be an embryonic (P_INEXEC, PRS_NEW) process. */
+ if (p->p_state != PRS_NEW || (p->p_flag & P_INEXEC) == 0) {
+ PROC_UNLOCK(p);
+ fdrop(fp_pd, td);
+ return (EINVAL);
+ }
+ /*
+ * Decide whether descriptor installation into this embryo is
+ * "restricted" (see the guard below). Two independent triggers:
+ *
+ * - The embryo's exec conferred a set-user-ID/set-group-ID
+ * credential (P_SUGID) -- the same condition execve() uses to
+ * drive fdsetugidsafety().
+ *
+ * - The caller (the injector) is a different user or group than the
+ * embryo's creator, so the embryo may carry authority the caller
+ * lacks even without any set-id bit -- e.g. a root-created
+ * procdesc delegated (via SCM_RIGHTS) to a weaker process. This
+ * half is skipped when the embryo itself runs in capability mode:
+ * a sandboxed process draws its authority from its file
+ * descriptors, not its ambient uid/gid, so the divergence is not
+ * meaningful and CAP_PDSETFD plus the preserved filecaps govern
+ * instead. It is the embryo's capability mode that matters, not
+ * the caller's: the check protects the embryo, so what counts is
+ * whether the embryo's ambient identity confers real authority.
+ *
+ * The embryo has not run, so its credential reflects exactly
+ * pdnew()'s exec and cannot change under us before pdstart().
+ */
+ restricted = (p->p_flag & P_SUGID) != 0 ||
+ ((p->p_ucred->cr_flags & CRED_FLAG_CAPMODE) == 0 &&
+ (td->td_ucred->cr_uid != p->p_ucred->cr_uid ||
+ td->td_ucred->cr_gid != p->p_ucred->cr_gid));
+ PROC_UNLOCK(p);
+
+ /*
+ * Look up the fd to install from the caller's fd table, capturing
+ * its capability rights so the installed descriptor carries exactly
+ * the same (possibly restricted) rights. Using fget() + NULL fcaps
+ * would instead grant the child full rights (filecaps_fill()), a
+ * capability escape for a sandboxed caller. This mirrors pddupfd().
+ */
+ error = fget_cap(td, uap->localfd, cap_rights_init(&rights), NULL,
+ &fp, &fcaps);
+ if (error != 0) {
+ fdrop(fp_pd, td);
+ return (error);
+ }
+
+ /*
+ * When installation is restricted (a set-id embryo, or a caller who
+ * does not share the embryo's user/group; see above), refuse to
+ * install an "unsafe" descriptor -- one whose backing vnode can change
+ * out from under the process after it starts, e.g. a procfs node -- at
+ * descriptors 0, 1 or 2, the same protection fdsetugidsafety() applies
+ * across an ordinary set-id execve().
+ *
+ * This policy targets exactly 0/1/2 because those fds carry implicit
+ * significance in the C library (stdin/stdout/stderr): a privileged
+ * program may write to them via stdio without ever naming them, so a
+ * hostile descriptor there is a confused-deputy write primitive.
+ * Descriptors >= 3 have no such implicit meaning and FreeBSD already
+ * permits unsafe ones across execve(). That choice is a property of
+ * libc's runtime contract, independent of how the process was spawned
+ * (fork+exec closes or keeps fds uniformly too), so pdnew() simply
+ * honors FreeBSD's existing policy rather than inventing its own.
+ */
+ if (restricted && uap->remotefd >= 0 && uap->remotefd <= 2 &&
+ fdesc_is_unsafe(fp)) {
+ filecaps_free(&fcaps);
+ fdrop(fp, td);
+ fdrop(fp_pd, td);
+ return (EPERM);
+ }
+
+ /*
+ * Install into the embryonic process's fd table at the
+ * caller-chosen descriptor number. finstall_at() takes its own
+ * reference, so drop ours afterward regardless of outcome; on
+ * success it moves fcaps into the new descriptor, so only free them
+ * on failure. The fp_pd reference held across this call keeps the
+ * process and its fd table alive against a concurrent close(2).
+ */
+ error = finstall_at(td, p, fp, uap->remotefd, 0, &fcaps);
+ if (error != 0)
+ filecaps_free(&fcaps);
+ fdrop(fp, td);
+ fdrop(fp_pd, td);
+ return (error);
+}
+
+/*
+ * Argument block handed from pdexec(2) to the trampoline that runs in the
+ * embryo. It is freed by whichever side finishes with it: normally the
+ * trampoline, via kern_execve().
+ */
+struct pdexec_args_blk {
+ struct image_args args;
+ int execfd;
+};
+
+/*
+ * The trampoline.
+ *
+ * This runs as the embryonic process's own thread, on its first and only
+ * trip out of the kernel, and simply execs. Everything execve(2) does then
+ * happens in the right context: the image is activated into curproc's
+ * vmspace, the argument strings are copied out with a plain copyout(9),
+ * umtx_exec() and sigfastblock_clear() act on curthread, and the descriptor
+ * table is processed by fdcloseexec() as usual. None of that has to be
+ * taught about acting on another process.
+ *
+ * On success kern_execve() does not come back here; the thread returns to
+ * userspace running the new program. On failure there is nothing to return
+ * to, since this process has never had a program, so it exits.
+ */
+static void
+pdexec_trampoline(void *arg)
+{
+ struct pdexec_args_blk *blk = arg;
+ struct thread *td = curthread;
+ int error;
+
+ error = kern_execve(td, &blk->args, NULL, NULL);
+ free(blk, M_TEMP);
+
+ /*
+ * EJUSTRETURN is how a successful execve(2) reports that the register
+ * state has been replaced and must not be touched again; returning
+ * from here does exactly that, and the process starts running its new
+ * program.
+ */
+ if (error == EJUSTRETURN)
+ return;
+
+ /*
+ * The image could not be loaded. A process that has never run has
+ * nothing to fall back to, so it dies here; its creator learns of it
+ * through the process descriptor.
+ */
+ exit1(td, 0, SIGABRT);
+ /* NOTREACHED */
+}
+
+/*
+ * Release an embryonic process: mark it complete, announce it, and let it
+ * run.
+ *
+ * Called with the process locked; returns it unlocked and scheduled.
+ */
+static void
+pdstart_proc(struct proc *p)
+{
+ struct thread *td2;
+ struct proc *pp;
+
+ PROC_LOCK_ASSERT(p, MA_OWNED);
+
+ /*
+ * Clear P_INEXEC and transition to normal state. Wake any
+ * P_INEXEC_WAIT waiter (e.g. a racing pddupfd() parked in
+ * execve_block_wait()), as do_execve() does when it clears the
+ * flag; otherwise the waiter sleeps forever.
+ */
+ MPASS(p->p_execblock == 0);
+ if ((p->p_flag & P_INEXEC_WAIT) != 0)
+ wakeup(&p->p_execblock);
+ p->p_flag &= ~(P_INEXEC | P_INEXEC_WAIT);
+
+ PROC_SLOCK(p);
+ p->p_state = PRS_NORMAL;
+ PROC_SUNLOCK(p);
+
+ microuptime(&p->p_stats->p_start);
+
+ PROC_UNLOCK(p);
+
+ /*
+ * Announce the process to fork observers now that it has committed
+ * to running -- deferred from creation so that an embryo destroyed
+ * before it ever ran is never seen, which would leave process_fork
+ * unbalanced in handlers that pair it with process_exit (filemon,
+ * hwpmc). Fire with no process lock held, as do_fork() does, and
+ * before the thread is scheduled so handlers are registered before
+ * it can run. Inherit from the embryo's parent -- its creator, or
+ * the reaper if that has since exited -- not the possibly different
+ * caller; PHOLD keeps it alive across the call.
+ */
+ sx_slock(&proctree_lock);
+ pp = p->p_pptr;
+ PHOLD(pp);
+ sx_sunlock(&proctree_lock);
+ EVENTHANDLER_DIRECT_INVOKE(process_fork, pp, p, RFPROC);
+ SDT_PROBE3(proc, , , create, p, pp, RFPROC);
+ PRELE(pp);
+
+ /* Schedule the process's thread. */
+ td2 = FIRST_THREAD_IN_PROC(p);
+ thread_lock(td2);
+ TD_SET_CAN_RUN(td2);
+ sched_add(td2, SRQ_BORING);
+}
+
+/*
+ * kern_pdexec: give an embryonic process a program, and run it.
+ *
+ * The exec itself is not performed here. It is performed by the target, on
+ * its own thread, in its own address space -- see pdexec_trampoline(). All
+ * this call does is prepare the handoff and let the process go.
+ *
+ * The executable is named by a descriptor of the *caller*, but the exec runs
+ * in the target, which has its own (empty) descriptor table. So the
+ * descriptor is installed into the target first, close-on-exec, at the same
+ * number: the exec that follows both uses it and closes it, and a shebang
+ * interpreter can still reach the script through /dev/fd/N while it lasts.
+ */
+static int
+kern_pdexec(struct thread *td, int procfd, int execfd, struct image_args *args)
+{
+ struct pdexec_args_blk *blk;
+ struct filecaps fcaps;
+ struct proc *p2;
+ struct thread *td2;
+ struct file *fp_pd, *efp;
+ cap_rights_t rights;
+ int error;
+
+ /*
+ * Deciding what a process will run is at least as much authority as
+ * configuring it, and releasing it is what pdkill(2) guards, so
+ * require both rights.
+ */
+ cap_rights_init(&rights, CAP_PDSETFD);
+ cap_rights_set(&rights, CAP_PDKILL);
+ error = procdesc_find(td, procfd, &rights, &p2, &fp_pd);
+ if (error != 0)
+ return (error);
+
+ /* Must be an embryo that has not been given a program yet. */
+ if (p2->p_state != PRS_NEW || (p2->p_flag & P_INEXEC) == 0 ||
+ p2->p_textvp != NULL) {
+ PROC_UNLOCK(p2);
+ fdrop(fp_pd, td);
+ return (EINVAL);
+ }
+ PROC_UNLOCK(p2);
+ td2 = FIRST_THREAD_IN_PROC(p2);
+
+ /*
+ * Hand the executable to the target, preserving the rights the
+ * caller retained on it rather than granting the child full rights.
+ */
+ error = fget_cap(td, execfd, &cap_fexecve_rights, NULL, &efp, &fcaps);
+ if (error != 0) {
+ fdrop(fp_pd, td);
+ return (error);
+ }
+ error = finstall_at(td, p2, efp, execfd, O_CLOEXEC, &fcaps);
+ if (error != 0)
+ filecaps_free(&fcaps);
+ fdrop(efp, td);
+ if (error != 0) {
+ fdrop(fp_pd, td);
+ return (error);
+ }
+
+ /*
+ * Give the process an empty address space, and build its kernel
+ * stack and trampoline.
+ *
+ * Both happen here rather than at creation so that an embryo which
+ * has not been given a program has no address space at all: there is
+ * nothing to describe until there is something to run. The vmspace
+ * comes first because cpu_fork() needs a pmap to preload from on some
+ * architectures; the exec that follows replaces it, exactly as
+ * execve(2) replaces the address space of any other process.
+ */
+ p2->p_vmspace = vmspace_alloc(p2->p_sysent->sv_minuser,
+ p2->p_sysent->sv_maxuser, pmap_pinit);
+ if (p2->p_vmspace == NULL) {
+ fdrop(fp_pd, td);
+ return (ENOMEM);
+ }
+ cpu_fork(td, p2, td2, RFPROC);
+
+ blk = malloc(sizeof(*blk), M_TEMP, M_WAITOK);
+ blk->args = *args;
+ blk->execfd = execfd;
+ blk->args.fd = execfd;
+
+ cpu_fork_kthread_handler(td2, pdexec_trampoline, blk);
+
+ /*
+ * Release the process. From here the target runs the trampoline and
+ * execs itself; there is nothing further for this call to do, and the
+ * result of the exec is reported through the process descriptor
+ * rather than through this syscall.
+ */
+ PROC_LOCK(p2);
+ pdstart_proc(p2);
+ fdrop(fp_pd, td);
+ return (0);
+}
+
+int
+sys_pdexec(struct thread *td, struct pdexec_args *uap)
+{
+ struct image_args args;
+ int error;
+
+ if (uap->flags != 0)
+ return (EINVAL);
+
+ error = exec_copyin_args(&args, NULL, uap->argv, uap->envv);
+ if (error != 0)
+ return (error);
+
+ error = kern_pdexec(td, uap->procfd, uap->fd, &args);
+ if (error != 0) {
+ exec_free_args(&args);
+ return (error);
+ }
+
+ td->td_retval[0] = 0;
+ return (0);
+}
diff --git a/sys/kern/sys_procdesc.c b/sys/kern/sys_procdesc.c
--- a/sys/kern/sys_procdesc.c
+++ b/sys/kern/sys_procdesc.c
@@ -422,7 +422,21 @@
} else {
PROC_LOCK(p);
AUDIT_ARG_PROCESS(p);
- if (pd->pd_fpcount == 0) /* last procdesc */ {
+ if (p->p_state == PRS_NEW &&
+ (p->p_flag & P_INEXEC) != 0) {
+ /*
+ * Embryonic process that was never started.
+ * It has no running thread, so we can't send
+ * SIGKILL. Directly tear it down.
+ */
+ pd->pd_proc = NULL;
+ p->p_procdesc = NULL;
+ pd->pd_pid = -1;
+ procdesc_free(pd);
+ PROC_UNLOCK(p);
+ sx_xunlock(&proctree_lock);
+ proc_destroy_embryonic(p);
+ } else if (pd->pd_fpcount == 0) /* last procdesc */ {
/*
* If the process is not yet dead, we need to kill it,
* but we can't wait around synchronously for it to go
diff --git a/sys/kern/syscalls.c b/sys/kern/syscalls.c
--- a/sys/kern/syscalls.c
+++ b/sys/kern/syscalls.c
@@ -1,6 +1,7 @@
/*
* System call names.
*
+ *
* DO NOT EDIT-- this file is automatically @generated.
*/
@@ -610,4 +611,6 @@
"renameat2", /* 602 = renameat2 */
"pdopenpid", /* 603 = pdopenpid */
"pddupfd", /* 604 = pddupfd */
+ "pdsetfd", /* 605 = pdsetfd */
+ "pdexec", /* 606 = pdexec */
};
diff --git a/sys/kern/syscalls.master b/sys/kern/syscalls.master
--- a/sys/kern/syscalls.master
+++ b/sys/kern/syscalls.master
@@ -3443,4 +3443,20 @@
);
}
+605 AUE_NULL STD|CAPENABLED {
+ int pdsetfd(
+ int procfd,
+ int remotefd,
+ int localfd
+ );
+ }
+606 AUE_NULL STD|CAPENABLED {
+ int pdexec(
+ int procfd,
+ int fd,
+ _In_z_ char **argv,
+ _In_z_ char **envv,
+ int flags
+ );
+ }
; vim: syntax=off
diff --git a/sys/kern/systrace_args.c b/sys/kern/systrace_args.c
--- a/sys/kern/systrace_args.c
+++ b/sys/kern/systrace_args.c
@@ -1,8 +1,10 @@
/*
* System call argument to DTrace register array conversion.
*
+ *
* This file is part of the DTrace syscall provider.
*
+ *
* DO NOT EDIT-- this file is automatically @generated.
*/
@@ -3572,6 +3574,26 @@
*n_args = 3;
break;
}
+ /* pdsetfd */
+ case 605: {
+ struct pdsetfd_args *p = params;
+ iarg[a++] = p->procfd; /* int */
+ iarg[a++] = p->remotefd; /* int */
+ iarg[a++] = p->localfd; /* int */
+ *n_args = 3;
+ break;
+ }
+ /* pdexec */
+ case 606: {
+ struct pdexec_args *p = params;
+ iarg[a++] = p->procfd; /* int */
+ iarg[a++] = p->fd; /* int */
+ uarg[a++] = (intptr_t)p->argv; /* char ** */
+ uarg[a++] = (intptr_t)p->envv; /* char ** */
+ iarg[a++] = p->flags; /* int */
+ *n_args = 5;
+ break;
+ }
default:
*n_args = 0;
break;
@@ -9567,6 +9589,44 @@
break;
};
break;
+ /* pdsetfd */
+ case 605:
+ switch (ndx) {
+ case 0:
+ p = "int";
+ break;
+ case 1:
+ p = "int";
+ break;
+ case 2:
+ p = "int";
+ break;
+ default:
+ break;
+ };
+ break;
+ /* pdexec */
+ case 606:
+ switch (ndx) {
+ case 0:
+ p = "int";
+ break;
+ case 1:
+ p = "int";
+ break;
+ case 2:
+ p = "userland char **";
+ break;
+ case 3:
+ p = "userland char **";
+ break;
+ case 4:
+ p = "int";
+ break;
+ default:
+ break;
+ };
+ break;
default:
break;
};
@@ -11605,6 +11665,16 @@
if (ndx == 0 || ndx == 1)
p = "int";
break;
+ /* pdsetfd */
+ case 605:
+ if (ndx == 0 || ndx == 1)
+ p = "int";
+ break;
+ /* pdexec */
+ case 606:
+ if (ndx == 0 || ndx == 1)
+ p = "int";
+ break;
default:
break;
};
diff --git a/sys/sys/capsicum.h b/sys/sys/capsicum.h
--- a/sys/sys/capsicum.h
+++ b/sys/sys/capsicum.h
@@ -304,7 +304,9 @@
/* Allows procctl(P_PROCDESC) */
#define CAP_PROCCTL CAPRIGHT(1, 0x0000000002000000ULL)
-#define CAP_UNUSED1_27 CAPRIGHT(1, 0x0000000004000000ULL)
+/* Allows for pdsetfd(2). */
+#define CAP_PDSETFD CAPRIGHT(1, 0x0000000004000000ULL)
+
#define CAP_UNUSED1_28 CAPRIGHT(1, 0x0000000008000000ULL)
#define CAP_UNUSED1_29 CAPRIGHT(1, 0x0000000010000000ULL)
#define CAP_UNUSED1_30 CAPRIGHT(1, 0x0000000020000000ULL)
@@ -337,7 +339,7 @@
#define CAP_UNUSED1_57 CAPRIGHT(1, 0x0100000000000000ULL)
/* All default bits for index 1. */
-#define CAP_ALL1 CAPRIGHT(1, 0x0000000003FFFFFFULL)
+#define CAP_ALL1 CAPRIGHT(1, 0x0000000007FFFFFFULL)
/* Backward compatibility. */
#define CAP_POLL_EVENT CAP_EVENT
diff --git a/sys/sys/filedesc.h b/sys/sys/filedesc.h
--- a/sys/sys/filedesc.h
+++ b/sys/sys/filedesc.h
@@ -262,10 +262,13 @@
struct filecaps *fcaps);
int finstall_refed(struct thread *td, struct file *fp, int *resultfd, int flags,
struct filecaps *fcaps);
+int finstall_at(struct thread *td, struct proc *p, struct file *fp,
+ int atfd, int flags, struct filecaps *fcaps);
int fdalloc(struct thread *td, int minfd, int *result);
int fdallocn(struct thread *td, int minfd, int *fds, int n);
int fdcheckstd(struct thread *td);
void fdclose(struct thread *td, struct file *fp, int idx);
+bool fdesc_is_unsafe(struct file *fp);
void fdcloseexec(struct thread *td);
void fdsetugidsafety(struct thread *td);
struct filedesc *fdcopy(struct filedesc *fdp, struct proc *p1);
diff --git a/sys/sys/proc.h b/sys/sys/proc.h
--- a/sys/sys/proc.h
+++ b/sys/sys/proc.h
@@ -851,7 +851,11 @@
#define P_HWPMC 0x00800000 /* Process is using HWPMCs */
#define P_JAILED 0x01000000 /* Process is in jail. */
#define P_TOTAL_STOP 0x02000000 /* Stopped in stop_all_proc. */
-#define P_INEXEC 0x04000000 /* Process is in execve(). */
+#define P_INEXEC 0x04000000 /* Process is not yet scheduled. Either
+ it is in execve() (the original
+ use-case) or it is an embryonic
+ process whose state is still being
+ set up (builder pattern). */
#define P_STATCHILD 0x08000000 /* Child process stopped or exited. */
#define P_INMEM 0x10000000 /* Loaded into memory, always set. */
#define P_ASYNC_EXIT 0x20000000 /* XXX */
@@ -1188,6 +1192,7 @@
void fork_exit(void (*)(void *, struct trapframe *), void *,
struct trapframe *);
void fork_return(struct thread *, struct trapframe *);
+void proc_destroy_embryonic(struct proc *);
int inferior(struct proc *p);
void itimer_proc_continue(struct proc *p);
void kqtimer_proc_continue(struct proc *p);
@@ -1229,6 +1234,7 @@
void pstats_free(struct pstats *ps);
void proc_clear_orphan(struct proc *p);
void reaper_abandon_children(struct proc *p, bool exiting);
+void reaper_clear(struct proc *p, struct proc *rp);
int securelevel_ge(struct ucred *cr, int level);
int securelevel_gt(struct ucred *cr, int level);
void sess_hold(struct session *);
diff --git a/sys/sys/procdesc.h b/sys/sys/procdesc.h
--- a/sys/sys/procdesc.h
+++ b/sys/sys/procdesc.h
@@ -113,6 +113,7 @@
void procdesc_jobstate(struct proc *p);
int kern_pdgetpid(struct thread *, int fd, const cap_rights_t *,
pid_t *pidp);
+int kern_pdnew(struct thread *, int pdflags, int *fdp, pid_t *pidp);
void procdesc_new(struct proc *, int);
void procdesc_finit(struct procdesc *, struct file *);
pid_t procdesc_pid(struct file *);
@@ -151,6 +152,8 @@
int pdwait(int, int *, int, struct __wrusage *, struct __siginfo *);
int pddupfd(int, int, int);
pid_t pdrfork_thread(int *, int, int, void *, int (*)(void *), void *);
+int pdexec(int, int, char **, char **, int);
+int pdsetfd(int, int, int);
__END_DECLS
#endif /* _KERNEL */
@@ -166,5 +169,7 @@
#define PD_ALLOWED_AT_FORK \
(PD_DAEMON | PD_CLOEXEC | PD_NOWAITPID | PD_PTRACE_CAP)
#define PD_ALLOWED_AT_OPENPID (PD_DAEMON | PD_CLOEXEC | PD_PTRACE_CAP)
+#define PD_ALLOWED_AT_NEW \
+ (PD_DAEMON | PD_CLOEXEC | PD_NOWAITPID | PD_PTRACE_CAP)
#endif /* !_SYS_PROCDESC_H_ */
diff --git a/sys/sys/syscall.h b/sys/sys/syscall.h
--- a/sys/sys/syscall.h
+++ b/sys/sys/syscall.h
@@ -1,6 +1,7 @@
/*
* System call numbers.
*
+ *
* DO NOT EDIT-- this file is automatically @generated.
*/
@@ -543,4 +544,6 @@
#define SYS_renameat2 602
#define SYS_pdopenpid 603
#define SYS_pddupfd 604
-#define SYS_MAXSYSCALL 605
+#define SYS_pdsetfd 605
+#define SYS_pdexec 606
+#define SYS_MAXSYSCALL 607
diff --git a/sys/sys/syscall.mk b/sys/sys/syscall.mk
--- a/sys/sys/syscall.mk
+++ b/sys/sys/syscall.mk
@@ -1,6 +1,7 @@
#
# FreeBSD system call object files.
#
+#
# DO NOT EDIT-- this file is automatically @generated.
#
@@ -446,4 +447,6 @@
pdwait.o \
renameat2.o \
pdopenpid.o \
- pddupfd.o
+ pddupfd.o \
+ pdsetfd.o \
+ pdexec.o
diff --git a/sys/sys/sysproto.h b/sys/sys/sysproto.h
--- a/sys/sys/sysproto.h
+++ b/sys/sys/sysproto.h
@@ -1,6 +1,7 @@
/*
* System call prototypes.
*
+ *
* DO NOT EDIT-- this file is automatically @generated.
*/
@@ -1941,6 +1942,18 @@
char fd_l_[PADL_(int)]; int fd; char fd_r_[PADR_(int)];
char flags_l_[PADL_(int)]; int flags; char flags_r_[PADR_(int)];
};
+struct pdsetfd_args {
+ char procfd_l_[PADL_(int)]; int procfd; char procfd_r_[PADR_(int)];
+ char remotefd_l_[PADL_(int)]; int remotefd; char remotefd_r_[PADR_(int)];
+ char localfd_l_[PADL_(int)]; int localfd; char localfd_r_[PADR_(int)];
+};
+struct pdexec_args {
+ char procfd_l_[PADL_(int)]; int procfd; char procfd_r_[PADR_(int)];
+ char fd_l_[PADL_(int)]; int fd; char fd_r_[PADR_(int)];
+ char argv_l_[PADL_(char **)]; char ** argv; char argv_r_[PADR_(char **)];
+ char envv_l_[PADL_(char **)]; char ** envv; char envv_r_[PADR_(char **)];
+ char flags_l_[PADL_(int)]; int flags; char flags_r_[PADR_(int)];
+};
int sys__exit(struct thread *, struct _exit_args *);
int sys_fork(struct thread *, struct fork_args *);
int sys_read(struct thread *, struct read_args *);
@@ -2353,6 +2366,8 @@
int sys_renameat2(struct thread *, struct renameat2_args *);
int sys_pdopenpid(struct thread *, struct pdopenpid_args *);
int sys_pddupfd(struct thread *, struct pddupfd_args *);
+int sys_pdsetfd(struct thread *, struct pdsetfd_args *);
+int sys_pdexec(struct thread *, struct pdexec_args *);
#ifdef COMPAT_43
@@ -3357,6 +3372,8 @@
#define SYS_AUE_renameat2 AUE_RENAMEAT
#define SYS_AUE_pdopenpid AUE_PDOPENPID
#define SYS_AUE_pddupfd AUE_NULL
+#define SYS_AUE_pdsetfd AUE_NULL
+#define SYS_AUE_pdexec AUE_NULL
#undef PAD_
#undef PADL_
diff --git a/sys/sys/unistd.h b/sys/sys/unistd.h
--- a/sys/sys/unistd.h
+++ b/sys/sys/unistd.h
@@ -184,6 +184,7 @@
#define RFCFDG (1<<12) /* close all fds, zero fd table */
#define RFTHREAD (1<<13) /* enable kernel thread support */
#define RFSIGSHARE (1<<14) /* share signal handlers */
+#define RFEMBRYO (1<<15) /* create an embryonic process (pdrfork) */
#define RFLINUXTHPN (1<<16) /* do linux clone exit parent notification */
#define RFSTOPPED (1<<17) /* leave child in a stopped state */
#define RFHIGHPID (1<<18) /* use a pid higher than 10 (idleproc) */
@@ -198,8 +199,8 @@
/* user: vfork(2) semantics, clear signals */
#define RFSPAWN (1U<<31)
#define RFFLAGS (RFFDG | RFPROC | RFMEM | RFNOWAIT | RFCFDG | \
- RFTHREAD | RFSIGSHARE | RFLINUXTHPN | RFSTOPPED | RFHIGHPID | RFTSIGZMB | \
- RFPROCDESC | RFSPAWN | RFPPWAIT)
+ RFTHREAD | RFSIGSHARE | RFEMBRYO | RFLINUXTHPN | RFSTOPPED | RFHIGHPID | \
+ RFTSIGZMB | RFPROCDESC | RFSPAWN | RFPPWAIT)
#define RFKERNELONLY (RFSTOPPED | RFHIGHPID | RFPROCDESC)
/* kcmp() options. */
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Fri, Sep 25, 7:14 AM (18 h, 57 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
39587475
Default Alt Text
D58688.diff (53 KB)
Attached To
Mode
D58688: Add `pdrfork(RFEMBRYO)`, `pdsetfd` and `pdexec` syscalls
Attached
Detach File
Event Timeline
Log In to Comment