From 61af40208c0845a01d5636c52fa3581ad17d9078 Mon Sep 17 00:00:00 2001
From: Chris Hanson <cmhanson@eschatologist.net>
Date: Sat, 18 Apr 2026 16:30:18 -0700
Subject: [PATCH] Add posix_spawn_file_actions_addclosefrom_np

Add support for a closefrom(3) action to posix_spawn_file_actions_t.
This can be used to ensure all but specific low-numbered descriptors are
closed in the child process.

Rationale: In a large or complex application or one using many
libraries, the caller of posix_spawn(3) can't necessarily have complete
knowledge of the state of all descriptors that could potentially be
inherited by the child process.
---
 include/spawn.h                               |  2 ++
 lib/libc/gen/posix_spawn.3                    |  1 +
 .../gen/posix_spawn_file_actions_addopen.3    | 25 ++++++++++++++++---
 lib/libc/gen/posix_spawn_file_actions_init.3  |  1 +
 lib/libc/gen/posix_spawn_fileactions.c        | 21 ++++++++++++++++
 sys/kern/kern_exec.c                          | 12 ++++++++-
 sys/sys/spawn.h                               | 10 +++++++-
 .../lib/libc/gen/posix_spawn/h_fileactions.c  | 11 ++++++++
 .../lib/libc/gen/posix_spawn/t_fileactions.c  | 25 ++++++++++++++++++-
 9 files changed, 102 insertions(+), 6 deletions(-)

diff --git a/include/spawn.h b/include/spawn.h
index 32cdd85ca27f..846cb15cc694 100644
--- a/include/spawn.h
+++ b/include/spawn.h
@@ -60,6 +60,8 @@ int posix_spawn_file_actions_addchdir(posix_spawn_file_actions_t * __restrict,
         const char * __restrict);
 int posix_spawn_file_actions_addfchdir(posix_spawn_file_actions_t *, int);
 
+int posix_spawn_file_actions_addclosefrom_np(posix_spawn_file_actions_t *, int);
+
 /*
  * Spawn attributes
  */
diff --git a/lib/libc/gen/posix_spawn.3 b/lib/libc/gen/posix_spawn.3
index 6ca4a59a0810..379da1708fd6 100644
--- a/lib/libc/gen/posix_spawn.3
+++ b/lib/libc/gen/posix_spawn.3
@@ -444,6 +444,7 @@ is returned.
 .Xr vfork 2 ,
 .Xr posix_spawn_file_actions_addchdir 3 ,
 .Xr posix_spawn_file_actions_addclose 3 ,
+.Xr posix_spawn_file_actions_addclosefrom_np 3 ,
 .Xr posix_spawn_file_actions_adddup2 3 ,
 .Xr posix_spawn_file_actions_addfchdir 3 ,
 .Xr posix_spawn_file_actions_addopen 3 ,
diff --git a/lib/libc/gen/posix_spawn_file_actions_addopen.3 b/lib/libc/gen/posix_spawn_file_actions_addopen.3
index 9a23d6f9e899..82f2bf9b7c0d 100644
--- a/lib/libc/gen/posix_spawn_file_actions_addopen.3
+++ b/lib/libc/gen/posix_spawn_file_actions_addopen.3
@@ -42,8 +42,9 @@
 .Sh NAME
 .Nm posix_spawn_file_actions_addopen ,
 .Nm posix_spawn_file_actions_adddup2 ,
-.Nm posix_spawn_file_actions_addclose
-.Nd "add open, dup2 or close action to spawn file actions object"
+.Nm posix_spawn_file_actions_addclose ,
+.Nm posix_spawn_file_actions_addclosefrom_np
+.Nd "add open, dup2, close, or closefrom action to spawn file actions object"
 .Sh LIBRARY
 .Lb libc
 .Sh SYNOPSIS
@@ -54,8 +55,10 @@
 .Fn posix_spawn_file_actions_adddup2 "posix_spawn_file_actions_t * file_actions" "int fildes" "int newfildes"
 .Ft int
 .Fn posix_spawn_file_actions_addclose "posix_spawn_file_actions_t * file_actions" "int fildes"
+.Ft int
+.Fn posix_spawn_file_actions_addclosefrom_np "posix_spawn_file_actions_t * file_actions" "int fildes"
 .Sh DESCRIPTION
-These functions add an open, dup2 or close action to a spawn
+These functions add an open, dup2, close, or closefrom action to a spawn
 file actions object.
 .Pp
 A spawn file actions object is of type
@@ -140,6 +143,21 @@ close(fildes)
 .Pp
 had been called) when a new process is spawned using this file actions
 object.
+.Pp
+The
+.Fn posix_spawn_file_actions_addclosefrom_np
+function, a non-POSIX extension, adds a closefrom action to the object
+referenced by
+.Fa file_actions
+that causes the file descriptor
+.Fa filedes
+and all higher file descriptors to be closed (as if
+.Bd -literal -offset indent
+closefrom(filedes)
+.Ed
+.Pp
+had been called) when a new process is spawned using this file actions
+object.
 .Sh RETURN VALUES
 Upon successful completion, these functions return zero;
 otherwise, an error number is returned to indicate the error.
@@ -164,6 +182,7 @@ Insufficient memory exists to add to the spawn file actions object.
 .Xr close 2 ,
 .Xr dup2 2 ,
 .Xr open 2 ,
+.Xr closefrom 3 ,
 .Xr posix_spawn 3 ,
 .Xr posix_spawn_file_actions_destroy 3 ,
 .Xr posix_spawn_file_actions_init 3 ,
diff --git a/lib/libc/gen/posix_spawn_file_actions_init.3 b/lib/libc/gen/posix_spawn_file_actions_init.3
index d33a09c267b3..366a68d45d16 100644
--- a/lib/libc/gen/posix_spawn_file_actions_init.3
+++ b/lib/libc/gen/posix_spawn_file_actions_init.3
@@ -89,6 +89,7 @@ Insufficient memory exists to initialize the spawn file actions object.
 .Sh SEE ALSO
 .Xr posix_spawn 3 ,
 .Xr posix_spawn_file_actions_addclose 3 ,
+.Xr posix_spawn_file_actions_addclosefrom_np 3 ,
 .Xr posix_spawn_file_actions_adddup2 3 ,
 .Xr posix_spawn_file_actions_addopen 3 ,
 .Xr posix_spawnp 3
diff --git a/lib/libc/gen/posix_spawn_fileactions.c b/lib/libc/gen/posix_spawn_fileactions.c
index 1ff45c97faa5..7ac4170420fc 100644
--- a/lib/libc/gen/posix_spawn_fileactions.c
+++ b/lib/libc/gen/posix_spawn_fileactions.c
@@ -215,3 +215,24 @@ posix_spawn_file_actions_addfchdir(posix_spawn_file_actions_t *fa, int fildes)
 
 	return 0;
 }
+
+int
+posix_spawn_file_actions_addclosefrom_np(posix_spawn_file_actions_t *fa,
+    int fildes)
+{
+	unsigned int i;
+	int error;
+
+	if (fildes < 0)
+		return EBADF;
+
+	error = posix_spawn_file_actions_getentry(fa, &i);
+	if (error)
+		return error;
+
+	fa->fae[i].fae_action = FAE_CLOSEFROM;
+	fa->fae[i].fae_fildes = fildes;
+	fa->len++;
+
+	return 0;
+}
diff --git a/sys/kern/kern_exec.c b/sys/kern/kern_exec.c
index 0d1b498ab303..ccce65d88e57 100644
--- a/sys/kern/kern_exec.c
+++ b/sys/kern/kern_exec.c
@@ -2154,7 +2154,7 @@ handle_posix_spawn_file_actions(struct posix_spawn_file_actions *actions)
 {
 	struct lwp *l = curlwp;
 	register_t retval;
-	int error = 0, newfd;
+	int error = 0, newfd, fd;
 
 	if (actions == NULL)
 		return 0;
@@ -2201,6 +2201,16 @@ handle_posix_spawn_file_actions(struct posix_spawn_file_actions *actions)
 		case FAE_FCHDIR:
 			error = do_sys_fchdir(l, fae->fae_fildes, &retval);
 			break;
+		case FAE_CLOSEFROM:
+			/*
+			 * as above, ignore failures from close().
+			 */
+			while ((fd = l->l_fd->fd_lastfile) >= fae->fae_fildes) {
+				if (fd_getfile(fd) != NULL) {
+					fd_close(fd);
+				}
+			}
+			break;
 		}
 		if (error)
 			return error;
diff --git a/sys/sys/spawn.h b/sys/sys/spawn.h
index b04db01bd03c..ecca5c9ad541 100644
--- a/sys/sys/spawn.h
+++ b/sys/sys/spawn.h
@@ -47,7 +47,15 @@ struct posix_spawnattr {
 	sigset_t		sa_sigmask;
 };
 
-enum fae_action { FAE_OPEN, FAE_DUP2, FAE_CLOSE, FAE_CHDIR, FAE_FCHDIR };
+enum fae_action {
+	FAE_OPEN,
+	FAE_DUP2,
+	FAE_CLOSE,
+	FAE_CHDIR,
+	FAE_FCHDIR,
+	FAE_CLOSEFROM,
+};
+
 typedef struct posix_spawn_file_actions_entry {
 	enum fae_action fae_action;
 
diff --git a/tests/lib/libc/gen/posix_spawn/h_fileactions.c b/tests/lib/libc/gen/posix_spawn/h_fileactions.c
index c658600bbf1c..0d9637b7d1ec 100644
--- a/tests/lib/libc/gen/posix_spawn/h_fileactions.c
+++ b/tests/lib/libc/gen/posix_spawn/h_fileactions.c
@@ -100,6 +100,17 @@ main(int argc, char **argv)
 		fprintf(stderr, "%s: stat results differ\n", getprogname());
 		res = EXIT_FAILURE;
 	}
+	/* file descs 8 and 9 should be closed (via addclosefrom_np) */
+	if (read(8, buf, BUFSIZE) != -1 || errno != EBADF) {
+		fprintf(stderr, "%s: filedesc 8 is not closed\n",
+		    getprogname());
+		res = EXIT_FAILURE;
+	}
+	if (read(9, buf, BUFSIZE) != -1 || errno != EBADF) {
+		fprintf(stderr, "%s: filedesc 9 is not closed\n",
+		    getprogname());
+		res = EXIT_FAILURE;
+	}
 
 	return res;
 }
diff --git a/tests/lib/libc/gen/posix_spawn/t_fileactions.c b/tests/lib/libc/gen/posix_spawn/t_fileactions.c
index 57e27a0ada46..c8763ebcc42d 100644
--- a/tests/lib/libc/gen/posix_spawn/t_fileactions.c
+++ b/tests/lib/libc/gen/posix_spawn/t_fileactions.c
@@ -274,7 +274,7 @@ ATF_TC_HEAD(t_spawn_fileactions, tc)
 }
 ATF_TC_BODY(t_spawn_fileactions, tc)
 {
-	int fd1, fd2, fd3, status;
+	int fd1, fd2, fd3, fd4, fd5, fd6, fd7, status;
 	pid_t pid;
 	char * const args[2] = { __UNCONST("h_fileactions"), NULL };
 	char helper[FILENAME_MAX];
@@ -297,6 +297,29 @@ ATF_TC_BODY(t_spawn_fileactions, tc)
 	RZ(posix_spawn_file_actions_addopen(&fa, 6, "/dev/null", O_RDWR, 0));
 	RZ(posix_spawn_file_actions_adddup2(&fa, 1, 7));
 
+	/*
+	 * Open descriptors 6 & 7 so we can get to 8 & 9 for testing
+	 * closefrom_np, but close 6 & 7 before the spawn since the
+	 * child process will get them via the open and dup2 actions
+	 * above.
+	 */
+	RL(fd4 = open("/dev/null", O_RDONLY));
+	ATF_REQUIRE(fd4 == 6);
+
+	RL(fd5 = open("/dev/null", O_RDONLY));
+	ATF_REQUIRE(fd5 == 7);
+
+	RL(fd6 = open("/dev/null", O_RDONLY));
+	ATF_REQUIRE(fd6 == 8);
+
+	RL(fd7 = open("/dev/null", O_RDONLY));
+	ATF_REQUIRE(fd7 == 9);
+
+	close(fd4);
+	close(fd5);
+
+	RZ(posix_spawn_file_actions_addclosefrom_np(&fa, 8));
+
 	snprintf(helper, sizeof helper, "%s/h_fileactions",
 	    atf_tc_get_config_var(tc, "srcdir"));
 	RZ(posix_spawn(&pid, helper, &fa, NULL, args, NULL));
-- 
2.54.0

