Page MenuHomeFreeBSD

D58688.diff
No OneTemporary

D58688.diff

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

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)

Event Timeline