[PATCH] jail: Add Capsicum rights for jail descriptor operations

Runxi Yu <[email protected]> Sun, 5 Jul 2026 09:26:51 +0000
Newsgroups gmane.os.freebsd.devel.hackers,gmane.os.freebsd.jail,gmane.os.freebsd.security.general
Message-ID <[email protected]>
From: Kory Heard <[email protected]>

Add CAP_JAIL_ATTACH, CAP_JAIL_REMOVE, and CAP_JAIL_SET,
and use them when JAIL_USE_DESC
on jail_attach_jd(2), jail_remove_jd(2), and jail_set(2).

Note that that rights are attenuated according to creds
at descriptor creation.

Co-authored-by: Runxi Yu <[email protected]>
---
Sending to here instead of phabricator for now
because my phabricator auth seems broken.

 share/man/man4/rights.4            |  28 ++
 sys/kern/kern_jail.c               |  50 ++-
 sys/kern/kern_jaildesc.c           |  48 ++-
 sys/kern/subr_capability.c         |   5 +
 sys/sys/caprights.h                |   3 +
 sys/sys/capsicum.h                 |   9 +-
 sys/sys/jaildesc.h                 |   4 +-
 tests/sys/kern/Makefile            |   3 +
 tests/sys/kern/jaildesc_cap_test.c | 512 +++++++++++++++++++++++++++++
 9 files changed, 611 insertions(+), 51 deletions(-)
 create mode 100644 tests/sys/kern/jaildesc_cap_test.c

diff --git a/share/man/man4/rights.4 b/share/man/man4/rights.4
index 396222a84579..8b86b0a281bf 100644
--- a/share/man/man4/rights.4
+++ b/share/man/man4/rights.4
@@ -336,6 +336,33 @@ global scope for some objects.
 The list of permitted ioctl commands can be further limited with the
 .Xr cap_ioctls_limit 2
 system call.
+.It Dv CAP_JAIL_ATTACH
+Permit
+.Xr jail_attach_jd 2
+on a jail descriptor.
+.It Dv CAP_JAIL_REMOVE
+Permit
+.Xr jail_remove_jd 2
+on a jail descriptor.
+.It Dv CAP_JAIL_SET
+Permit
+.Xr jail_set 2
+with the
+.Dv JAIL_USE_DESC
+flag on a jail descriptor.
+.Pp
+Unlike most rights,
+these three are not necessarily present on a newly created descriptor.
+A jail descriptor returned by
+.Xr jail_get 2
+or
+.Xr jail_set 2
+is granted only those of
+.Dv CAP_JAIL_ATTACH ,
+.Dv CAP_JAIL_REMOVE ,
+and
+.Dv CAP_JAIL_SET
+that the creating credential is itself privileged to exercise.
 .It Dv CAP_KQUEUE
 An alias to
 .Dv CAP_KQUEUE_CHANGE
@@ -685,6 +712,7 @@ is also required.
 .Xr getsockname 2 ,
 .Xr getsockopt 2 ,
 .Xr ioctl 2 ,
+.Xr jail 2 ,
 .Xr kevent 2 ,
 .Xr kqueue 2 ,
 .Xr linkat 2 ,
diff --git a/sys/kern/kern_jail.c b/sys/kern/kern_jail.c
index dd4df0353015..fee922d59ce5 100644
--- a/sys/kern/kern_jail.c
+++ b/sys/kern/kern_jail.c
@@ -49,6 +49,7 @@
 #include <sys/epoch.h>
 #include <sys/event.h>
 #include <sys/taskqueue.h>
+#include <sys/capsicum.h>
 #include <sys/fcntl.h>
 #include <sys/jail.h>
 #include <sys/jaildesc.h>
@@ -1020,7 +1021,6 @@ kern_jail_set(struct thread *td, struct uio *optuio, int flags)
 	struct vfsopt *opt;
 	struct vfsoptlist *opts;
 	struct prison *pr, *deadpr, *dinspr, *inspr, *mypr, *ppr, *tpr;
-	struct ucred *jdcred;
 	struct vnode *root;
 	char *domain, *errmsg, *host, *name, *namelc, *p, *path, *uuid;
 	char *g_path, *osrelstr;
@@ -1128,7 +1128,7 @@ kern_jail_set(struct thread *td, struct uio *optuio, int flags)
 			 */
 			struct prison *jdpr;
 
-			error = jaildesc_find(td, jfd_in, &jdpr, NULL);
+			error = jaildesc_find(td, jfd_in, &cap_no_rights, &jdpr);
 			if (error != 0) {
 				vfs_opterror(opts, error == ENOENT ?
 				    "descriptor to dead jail" :
@@ -1152,8 +1152,9 @@ kern_jail_set(struct thread *td, struct uio *optuio, int flags)
 	}
 
 	/*
-	 * Delay the permission check if using a jail descriptor,
-	 * until we get the descriptor's credentials.
+	 * When using a jail descriptor, authority comes from the
+	 * descriptor's capability rights, checked in jaildesc_find();
+	 * otherwise check the calling thread's privilege.
 	 */
 	if (!(flags & JAIL_USE_DESC)) {
 		error = priv_check(td, PRIV_JAIL_SET);
@@ -1550,8 +1551,17 @@ kern_jail_set(struct thread *td, struct uio *optuio, int flags)
 		goto done_deref;
 	}
 	if (flags & JAIL_USE_DESC) {
+		cap_rights_t set_attach_rights;
+		const cap_rights_t *descrightsp;
+
 		/* Get the jail from its descriptor. */
-		error = jaildesc_find(td, jfd_in, &pr, &jdcred);
+		if (flags & JAIL_ATTACH) {
+			cap_rights_init(&set_attach_rights, CAP_JAIL_SET,
+			    CAP_JAIL_ATTACH);
+			descrightsp = &set_attach_rights;
+		} else
+			descrightsp = &cap_jail_set_rights;
+		error = jaildesc_find(td, jfd_in, descrightsp, &pr);
 		if (error) {
 			vfs_opterror(opts, error == ENOENT ?
 			    "descriptor to dead jail" :
@@ -1559,12 +1569,6 @@ kern_jail_set(struct thread *td, struct uio *optuio, int flags)
 			goto done_deref;
 		}
 		drflags |= PD_DEREF;
-		error = priv_check_cred(jdcred, PRIV_JAIL_SET);
-		if (error == 0 && (flags & JAIL_ATTACH))
-			error = priv_check_cred(jdcred, PRIV_JAIL_ATTACH);
-		crfree(jdcred);
-		if (error)
-			goto done_deref;
 		mtx_lock(&pr->pr_mtx);
 		drflags |= PD_LOCKED;
 		if (cuflags == JAIL_CREATE) {
@@ -2619,7 +2623,7 @@ kern_jail_get(struct thread *td, struct uio *optuio, int flags)
 		}
 		if (flags & JAIL_USE_DESC) {
 			/* Get the jail from its descriptor. */
-			error = jaildesc_find(td, jfd_in, &pr, NULL);
+			error = jaildesc_find(td, jfd_in, &cap_no_rights, &pr);
 			if (error) {
 				vfs_opterror(opts, error == ENOENT ?
 				    "descriptor to dead jail" :
@@ -2635,7 +2639,7 @@ kern_jail_get(struct thread *td, struct uio *optuio, int flags)
 			/* Look up jails based on the descriptor's prison. */
 			struct prison *jdpr;
 
-			error = jaildesc_find(td, jfd_in, &jdpr, NULL);
+			error = jaildesc_find(td, jfd_in, &cap_no_rights, &jdpr);
 			if (error != 0) {
 				vfs_opterror(opts, error == ENOENT ?
 				    "descriptor to dead jail" :
@@ -3024,22 +3028,18 @@ int
 sys_jail_remove_jd(struct thread *td, struct jail_remove_jd_args *uap)
 {
 	struct prison *pr;
-	struct ucred *jdcred;
 	int error;
 
-	error = jaildesc_find(td, uap->fd, &pr, &jdcred);
+	error = jaildesc_find(td, uap->fd, &cap_jail_remove_rights, &pr);
 	if (error)
 		return (error);
-	error = priv_check_cred(jdcred, PRIV_JAIL_REMOVE);
-	crfree(jdcred);
 #ifdef MAC
-	if (error == 0)
-		error = mac_prison_check_remove(td->td_ucred, pr);
-#endif
+	error = mac_prison_check_remove(td->td_ucred, pr);
 	if (error) {
 		prison_free(pr);
 		return (error);
 	}
+#endif
 	sx_xlock(&allprison_lock);
 	mtx_lock(&pr->pr_mtx);
 	prison_remove(pr);
@@ -3115,7 +3115,6 @@ int
 sys_jail_attach_jd(struct thread *td, struct jail_attach_jd_args *uap)
 {
 	struct prison *pr;
-	struct ucred *jdcred;
 	int drflags, error;
 
 	/* Only let a single thread in the process try to attach at a time. */
@@ -3126,18 +3125,15 @@ sys_jail_attach_jd(struct thread *td, struct jail_attach_jd_args *uap)
 	sx_slock(&allprison_lock);
 	drflags = PD_LIST_SLOCKED;
 	pr = NULL;
-	error = jaildesc_find(td, uap->fd, &pr, &jdcred);
+	error = jaildesc_find(td, uap->fd, &cap_jail_attach_rights, &pr);
 	if (error)
 		goto done;
 	drflags |= PD_DEREF;
-	error = priv_check_cred(jdcred, PRIV_JAIL_ATTACH);
 #ifdef MAC
-	if (error == 0)
-		error = mac_prison_check_attach(td->td_ucred, pr);
-#endif
-	crfree(jdcred);
+	error = mac_prison_check_attach(td->td_ucred, pr);
 	if (error)
 		goto done;
+#endif
 
 	/* Do not allow a process to attach to a prison that is not alive. */
 	if (!prison_isalive(pr)) {
diff --git a/sys/kern/kern_jaildesc.c b/sys/kern/kern_jaildesc.c
index e2e3246ea92b..cace3e7608bb 100644
--- a/sys/kern/kern_jaildesc.c
+++ b/sys/kern/kern_jaildesc.c
@@ -27,6 +27,7 @@
  */
 
 #include <sys/param.h>
+#include <sys/capsicum.h>
 #include <sys/fcntl.h>
 #include <sys/file.h>
 #include <sys/filedesc.h>
@@ -38,6 +39,7 @@
 #include <sys/mutex.h>
 #include <sys/poll.h>
 #include <sys/priv.h>
+#include <sys/proc.h>
 #include <sys/stat.h>
 #include <sys/sysproto.h>
 #include <sys/systm.h>
@@ -106,31 +108,21 @@ jaildesc_get_prison_impl(struct file *fp, struct prison **prp)
 }
 
 /*
- * Given a jail descriptor number, return its prison and/or its
- * credential.  They are returned held, and will need to be released
- * by the caller.
+ * Given a jail descriptor number, return its prison.  It is returned
+ * held, and will need to be released by the caller.
  */
 int
-jaildesc_find(struct thread *td, int fd, struct prison **prp,
-    struct ucred **ucredp)
+jaildesc_find(struct thread *td, int fd, const cap_rights_t *rightsp,
+    struct prison **prp)
 {
 	struct file *fp;
 	int error;
 
-	error = fget(td, fd, &cap_no_rights, &fp);
+	error = fget(td, fd, rightsp, &fp);
 	if (error != 0)
 		return (error);
 
 	error = jaildesc_get_prison_impl(fp, prp);
-	if (error == 0) {
-		/*
-		 * jaildesc_get_prison validated the file and held the prison
-		 * for us if the caller wants it, so we just need to grab the
-		 * ucred on the way out.
-		 */
-		if (ucredp != NULL)
-			*ucredp = crhold(fp->f_cred);
-	}
 
 	fdrop(fp, td);
 	return (error);
@@ -144,8 +136,11 @@ jaildesc_find(struct thread *td, int fd, struct prison **prp,
 int
 jaildesc_alloc(struct thread *td, struct file **fpp, int *fdp, int owning)
 {
+	struct filecaps fcaps;
+	struct ucred *cred;
 	struct file *fp;
 	struct jaildesc *jd;
+	bool jail_set_ok;
 	int error;
 
 	if (owning) {
@@ -153,14 +148,31 @@ jaildesc_alloc(struct thread *td, struct file **fpp, int *fdp, int owning)
 		if (error != 0)
 			return (error);
 	}
+
+	/*
+	 * A jaildesc is born with only the rights that the creating credential
+	 * is itself privileged to exercise
+	 */
+	cred = td->td_ucred;
+	jail_set_ok = priv_check_cred(cred, PRIV_JAIL_SET) == 0;
+	filecaps_init(&fcaps);
+	CAP_ALL(&fcaps.fc_rights);
+	fcaps.fc_fcntls = CAP_FCNTL_ALL;
+	if (!jail_set_ok)
+		cap_rights_clear(&fcaps.fc_rights, CAP_JAIL_SET);
+	if (priv_check_cred(cred, PRIV_JAIL_REMOVE) != 0)
+		cap_rights_clear(&fcaps.fc_rights, CAP_JAIL_REMOVE);
+	if (priv_check_cred(cred, PRIV_JAIL_ATTACH) != 0)
+		cap_rights_clear(&fcaps.fc_rights, CAP_JAIL_ATTACH);
+
 	jd = malloc(sizeof(*jd), M_JAILDESC, M_WAITOK | M_ZERO);
-	error = falloc_caps(td, &fp, fdp, 0, NULL);
+	error = falloc_caps(td, &fp, fdp, 0, &fcaps);
 	if (error != 0) {
 		free(jd, M_JAILDESC);
 		return (error);
 	}
-	finit(fp, priv_check_cred(fp->f_cred, PRIV_JAIL_SET) == 0 ?
-	    FREAD | FWRITE : FREAD, DTYPE_JAILDESC, jd, &jaildesc_ops);
+	finit(fp, jail_set_ok ? FREAD | FWRITE : FREAD, DTYPE_JAILDESC, jd,
+	    &jaildesc_ops);
 	JAILDESC_LOCK_INIT(jd);
 	knlist_init_mtx(&jd->jd_selinfo.si_note, &jd->jd_lock);
 	if (owning)
diff --git a/sys/kern/subr_capability.c b/sys/kern/subr_capability.c
index 6e23525186ea..868b498f9206 100644
--- a/sys/kern/subr_capability.c
+++ b/sys/kern/subr_capability.c
@@ -79,6 +79,11 @@ const cap_rights_t cap_inotify_add_rights =
 const cap_rights_t cap_inotify_rm_rights =
     CAP_RIGHTS_INITIALIZER(CAP_INOTIFY_RM);
 const cap_rights_t cap_ioctl_rights = CAP_RIGHTS_INITIALIZER(CAP_IOCTL);
+const cap_rights_t cap_jail_attach_rights =
+    CAP_RIGHTS_INITIALIZER(CAP_JAIL_ATTACH);
+const cap_rights_t cap_jail_remove_rights =
+    CAP_RIGHTS_INITIALIZER(CAP_JAIL_REMOVE);
+const cap_rights_t cap_jail_set_rights = CAP_RIGHTS_INITIALIZER(CAP_JAIL_SET);
 const cap_rights_t cap_listen_rights = CAP_RIGHTS_INITIALIZER(CAP_LISTEN);
 const cap_rights_t cap_linkat_source_rights =
     CAP_RIGHTS_INITIALIZER(CAP_LINKAT_SOURCE);
diff --git a/sys/sys/caprights.h b/sys/sys/caprights.h
index 904d9b4e843a..90a05b4ba51f 100644
--- a/sys/sys/caprights.h
+++ b/sys/sys/caprights.h
@@ -82,6 +82,9 @@ extern const cap_rights_t cap_getsockname_rights;
 extern const cap_rights_t cap_inotify_add_rights;
 extern const cap_rights_t cap_inotify_rm_rights;
 extern const cap_rights_t cap_ioctl_rights;
+extern const cap_rights_t cap_jail_attach_rights;
+extern const cap_rights_t cap_jail_remove_rights;
+extern const cap_rights_t cap_jail_set_rights;
 extern const cap_rights_t cap_linkat_source_rights;
 extern const cap_rights_t cap_linkat_target_rights;
 extern const cap_rights_t cap_listen_rights;
diff --git a/sys/sys/capsicum.h b/sys/sys/capsicum.h
index 9ef2f0d48d38..d7ae827911a6 100644
--- a/sys/sys/capsicum.h
+++ b/sys/sys/capsicum.h
@@ -301,9 +301,10 @@
 #define	CAP_INOTIFY_ADD		CAPRIGHT(1, 0x0000000000200000ULL)
 #define	CAP_INOTIFY_RM		CAPRIGHT(1, 0x0000000000400000ULL)
 
-#define	CAP_UNUSED1_24		CAPRIGHT(1, 0x0000000000800000ULL)
-#define	CAP_UNUSED1_25		CAPRIGHT(1, 0x0000000001000000ULL)
-#define	CAP_UNUSED1_26		CAPRIGHT(1, 0x0000000002000000ULL)
+#define	CAP_JAIL_ATTACH		CAPRIGHT(1, 0x0000000000800000ULL)
+#define	CAP_JAIL_REMOVE		CAPRIGHT(1, 0x0000000001000000ULL)
+#define	CAP_JAIL_SET		CAPRIGHT(1, 0x0000000002000000ULL)
+
 #define	CAP_UNUSED1_27		CAPRIGHT(1, 0x0000000004000000ULL)
 #define	CAP_UNUSED1_28		CAPRIGHT(1, 0x0000000008000000ULL)
 #define	CAP_UNUSED1_29		CAPRIGHT(1, 0x0000000010000000ULL)
@@ -337,7 +338,7 @@
 #define	CAP_UNUSED1_57		CAPRIGHT(1, 0x0100000000000000ULL)
 
 /* All used bits for index 1. */
-#define	CAP_ALL1		CAPRIGHT(1, 0x00000000007FFFFFULL)
+#define	CAP_ALL1		CAPRIGHT(1, 0x0000000003FFFFFFULL)
 
 /* Backward compatibility. */
 #define	CAP_POLL_EVENT		CAP_EVENT
diff --git a/sys/sys/jaildesc.h b/sys/sys/jaildesc.h
index 22a03bfbb1fa..21b8b30da577 100644
--- a/sys/sys/jaildesc.h
+++ b/sys/sys/jaildesc.h
@@ -74,8 +74,8 @@ struct jaildesc {
 #define	JDF_REMOVED	0x00000002	/* jail was removed */
 #define	JDF_OWNING	0x00000004	/* closing descriptor removes jail */
 
-int jaildesc_find(struct thread *td, int fd, struct prison **prp,
-    struct ucred **ucredp);
+int jaildesc_find(struct thread *td, int fd, const cap_rights_t *rightsp,
+    struct prison **prp);
 int jaildesc_alloc(struct thread *td, struct file **fpp, int *fdp, int owning);
 int jaildesc_get_prison(struct file *jd, struct prison **prp);
 void jaildesc_set_prison(struct file *jd, struct prison *pr);
diff --git a/tests/sys/kern/Makefile b/tests/sys/kern/Makefile
index f7e06968520a..b22635ac671f 100644
--- a/tests/sys/kern/Makefile
+++ b/tests/sys/kern/Makefile
@@ -25,6 +25,7 @@ ATF_TESTS_C+=	getdirentries_test
 ATF_TESTS_C+=	jail_lookup_root
 ATF_TESTS_C+=	jail_thread
 ATF_TESTS_C+=	jaildesc
+ATF_TESTS_C+=	jaildesc_cap_test
 ATF_TESTS_C+=	inotify_test
 ATF_TESTS_C+=	kill_zombie
 .if ${MK_OPENSSL} != "no"
@@ -100,6 +101,8 @@ LIBADD.copy_file_range+=		md
 LIBADD.jail_lookup_root+=		jail util
 LIBADD.jail_thread+=			jail pthread
 LIBADD.jaildesc+=			kvm pthread
+CFLAGS.jaildesc_cap_test+=		-I${SRCTOP}/tests
+LIBADD.jaildesc_cap_test+=		jail
 LIBADD.ssl_sendfile+=			pthread crypto ssl
 CFLAGS.sys_getrandom+=			-I${SRCTOP}/sys/contrib/zstd/lib
 LIBADD.sys_getrandom+=			zstd
diff --git a/tests/sys/kern/jaildesc_cap_test.c b/tests/sys/kern/jaildesc_cap_test.c
new file mode 100644
index 000000000000..4d3e8e80919d
--- /dev/null
+++ b/tests/sys/kern/jaildesc_cap_test.c
@@ -0,0 +1,512 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Kory Heard <[email protected]>
+ */
+
+/*
+ * Test Capsicum capability rights for jail descriptors:
+ * CAP_JAIL_ATTACH, CAP_JAIL_REMOVE, and CAP_JAIL_SET.
+ */
+
+#include <sys/param.h>
+#include <sys/capsicum.h>
+#include <sys/jail.h>
+#include <sys/wait.h>
+
+#include <errno.h>
+#include <jail.h>
+#include <pwd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <atf-c.h>
+
+#include "freebsd_test_suite/macros.h"
+
+/*
+ * Create a jail and return an owning descriptor for it.
+ */
+static int
+create_jail_with_desc(const char *name)
+{
+	char descstr[16];
+	int jid;
+
+	descstr[0] = '\0';
+	jid = jail_setv(JAIL_CREATE | JAIL_GET_DESC,
+	    "name", name,
+	    "path", "/",
+	    "persist", "true",
+	    "desc", descstr,
+	    NULL);
+	if (jid < 0)
+		return (-1);
+
+	return ((int)strtol(descstr, NULL, 10));
+}
+
+/*
+ * Return a non-owning descriptor for an existing jail.
+ */
+static int
+get_jail_desc(const char *name)
+{
+	char descstr[16];
+
+	descstr[0] = '\0';
+	if (jail_getv(JAIL_GET_DESC,
+	    "name", name,
+	    "desc", descstr,
+	    NULL) < 0)
+		return (-1);
+
+	return ((int)strtol(descstr, NULL, 10));
+}
+
+/*
+ * Remove a jail by name.
+ */
+static void
+remove_jail_by_name(const char *name)
+{
+	int jid;
+
+	jid = jail_getid(name);
+	if (jid > 0)
+		jail_remove(jid);
+}
+
+/*
+ * Modify a jail. Sets allow.raw_sockets as a test.
+ */
+static int
+modify_jail_via_desc(int fd)
+{
+	char descstr[16];
+
+	snprintf(descstr, sizeof(descstr), "%d", fd);
+	return (jail_setv(JAIL_UPDATE | JAIL_USE_DESC,
+	    "desc", descstr,
+	    "allow.raw_sockets", "true",
+	    NULL));
+}
+
+/*
+ * Modify a jail and attach to it in a single jail_set(JAIL_USE_DESC |
+ * JAIL_ATTACH). This enters the jail, so callers should run it in a child
+ * process.
+ */
+static int
+modify_attach_jail_via_desc(int fd)
+{
+	char descstr[16];
+
+	snprintf(descstr, sizeof(descstr), "%d", fd);
+	return (jail_setv(JAIL_UPDATE | JAIL_USE_DESC | JAIL_ATTACH,
+	    "desc", descstr,
+	    "allow.raw_sockets", "true",
+	    NULL));
+}
+
+/*
+ * Verify CAP_JAIL_SET permits jail_set and denies without it.
+ */
+ATF_TC_WITH_CLEANUP(cap_jail_set);
+ATF_TC_HEAD(cap_jail_set, tc)
+{
+	atf_tc_set_md_var(tc, "require.user", "root");
+	atf_tc_set_md_var(tc, "descr",
+	    "Test CAP_JAIL_SET permits jail_set and denies without it");
+}
+ATF_TC_BODY(cap_jail_set, tc)
+{
+	cap_rights_t rights;
+	int fd, error;
+
+	ATF_REQUIRE_FEATURE("security_capabilities");
+
+	remove_jail_by_name("cap_set_test");
+
+	fd = create_jail_with_desc("cap_set_test");
+	ATF_REQUIRE_MSG(fd >= 0, "create_jail_with_desc failed: %s",
+	    strerror(errno));
+
+	/* jail_set with CAP_JAIL_SET should succeed. */
+	
+	cap_rights_init(&rights, CAP_JAIL_SET);
+	error = cap_rights_limit(fd, &rights);
+	ATF_REQUIRE_MSG(error == 0, "cap_rights_limit failed: %s",
+	    strerror(errno));
+
+	error = modify_jail_via_desc(fd);
+	ATF_REQUIRE_MSG(error >= 0, "jail_set with CAP_JAIL_SET failed: %s",
+	    strerror(errno));
+
+	/* Now limit to empty rights and it must fail. */
+	
+	cap_rights_init(&rights);
+	error = cap_rights_limit(fd, &rights);
+	ATF_REQUIRE_MSG(error == 0, "cap_rights_limit failed: %s",
+	    strerror(errno));
+
+	error = modify_jail_via_desc(fd);
+	ATF_REQUIRE_MSG(error == -1 && errno == ENOTCAPABLE,
+	    "jail_set without CAP_JAIL_SET should fail with ENOTCAPABLE, got %s",
+	    error >= 0 ? "success" : strerror(errno));
+
+	close(fd);
+}
+ATF_TC_CLEANUP(cap_jail_set, tc)
+{
+	remove_jail_by_name("cap_set_test");
+}
+
+/*
+ * Verify a descriptor limited to CAP_JAIL_ATTACH cannot remove.
+ */
+ATF_TC_WITH_CLEANUP(cap_jail_attach);
+ATF_TC_HEAD(cap_jail_attach, tc)
+{
+	atf_tc_set_md_var(tc, "require.user", "root");
+	atf_tc_set_md_var(tc, "descr",
+	    "Test CAP_JAIL_ATTACH permits attach and denies remove");
+}
+ATF_TC_BODY(cap_jail_attach, tc)
+{
+	cap_rights_t rights;
+	int fd, error;
+
+	ATF_REQUIRE_FEATURE("security_capabilities");
+
+	remove_jail_by_name("cap_attach_test");
+
+	fd = create_jail_with_desc("cap_attach_test");
+	ATF_REQUIRE_MSG(fd >= 0, "create_jail_with_desc failed: %s",
+	    strerror(errno));
+
+	/* Limit to CAP_JAIL_ATTACH. */
+	
+	cap_rights_init(&rights, CAP_JAIL_ATTACH);
+	error = cap_rights_limit(fd, &rights);
+	ATF_REQUIRE_MSG(error == 0, "cap_rights_limit failed: %s",
+	    strerror(errno));
+
+	/* jail_remove_jd should fail. */
+	
+	error = jail_remove_jd(fd);
+	ATF_REQUIRE_MSG(error == -1 && errno == ENOTCAPABLE,
+	    "jail_remove_jd should fail with ENOTCAPABLE, got %s",
+	    error == 0 ? "success" : strerror(errno));
+
+	close(fd);
+}
+ATF_TC_CLEANUP(cap_jail_attach, tc)
+{
+	remove_jail_by_name("cap_attach_test");
+}
+
+/*
+ * Test CAP_JAIL_REMOVE: verify it permits remove and denies attach.
+ */
+ATF_TC_WITH_CLEANUP(cap_jail_remove);
+ATF_TC_HEAD(cap_jail_remove, tc)
+{
+	atf_tc_set_md_var(tc, "require.user", "root");
+	atf_tc_set_md_var(tc, "descr",
+	    "Test CAP_JAIL_REMOVE permits remove and denies attach");
+}
+ATF_TC_BODY(cap_jail_remove, tc)
+{
+	cap_rights_t rights;
+	int fd, fd_for_attach, error, status;
+	pid_t pid;
+
+	ATF_REQUIRE_FEATURE("security_capabilities");
+
+	remove_jail_by_name("cap_remove_test");
+
+	fd = create_jail_with_desc("cap_remove_test");
+	ATF_REQUIRE_MSG(fd >= 0, "create_jail_with_desc failed: %s",
+	    strerror(errno));
+
+	fd_for_attach = dup(fd);
+	ATF_REQUIRE(fd_for_attach >= 0);
+
+	/* Limit both to CAP_JAIL_REMOVE. */
+	cap_rights_init(&rights, CAP_JAIL_REMOVE);
+	error = cap_rights_limit(fd, &rights);
+	ATF_REQUIRE_MSG(error == 0, "cap_rights_limit failed: %s",
+	    strerror(errno));
+	error = cap_rights_limit(fd_for_attach, &rights);
+	ATF_REQUIRE_MSG(error == 0, "cap_rights_limit failed: %s",
+	    strerror(errno));
+
+	/* jail_attach_jd should fail. */
+	pid = fork();
+	ATF_REQUIRE(pid >= 0);
+	if (pid == 0) {
+		error = jail_attach_jd(fd_for_attach);
+		if (error == -1 && errno == ENOTCAPABLE)
+			_exit(0);
+		_exit(1);
+	}
+	waitpid(pid, &status, 0);
+	ATF_REQUIRE_MSG(WIFEXITED(status) && WEXITSTATUS(status) == 0,
+	    "jail_attach_jd should fail with ENOTCAPABLE");
+
+	/* jail_remove_jd should succeed. */
+	error = jail_remove_jd(fd);
+	ATF_REQUIRE_MSG(error == 0, "jail_remove_jd failed: %s",
+	    strerror(errno));
+
+	close(fd_for_attach);
+	close(fd);
+}
+ATF_TC_CLEANUP(cap_jail_remove, tc)
+{
+	remove_jail_by_name("cap_remove_test");
+}
+
+/*
+ * Test that CAP_JAIL_ATTACH | CAP_JAIL_REMOVE permits both operations.
+ */
+ATF_TC_WITH_CLEANUP(cap_jail_both);
+ATF_TC_HEAD(cap_jail_both, tc)
+{
+	atf_tc_set_md_var(tc, "require.user", "root");
+	atf_tc_set_md_var(tc, "descr",
+	    "Test combined rights permit both attach and remove");
+}
+ATF_TC_BODY(cap_jail_both, tc)
+{
+	cap_rights_t rights;
+	int fd, error, status;
+	pid_t pid;
+
+	ATF_REQUIRE_FEATURE("security_capabilities");
+
+	remove_jail_by_name("cap_both_test");
+
+	fd = create_jail_with_desc("cap_both_test");
+	ATF_REQUIRE_MSG(fd >= 0, "create_jail_with_desc failed: %s",
+	    strerror(errno));
+
+	cap_rights_init(&rights, CAP_JAIL_ATTACH, CAP_JAIL_REMOVE);
+	error = cap_rights_limit(fd, &rights);
+	ATF_REQUIRE_MSG(error == 0, "cap_rights_limit failed: %s",
+	    strerror(errno));
+
+	pid = fork();
+	ATF_REQUIRE(pid >= 0);
+	if (pid == 0) {
+		error = jail_attach_jd(fd);
+		_exit(error == 0 ? 0 : 1);
+	}
+	waitpid(pid, &status, 0);
+	ATF_REQUIRE_MSG(WIFEXITED(status) && WEXITSTATUS(status) == 0,
+	    "jail_attach_jd should succeed with combined rights");
+
+	error = jail_remove_jd(fd);
+	ATF_REQUIRE_MSG(error == 0, "jail_remove_jd failed: %s",
+	    strerror(errno));
+
+	close(fd);
+}
+ATF_TC_CLEANUP(cap_jail_both, tc)
+{
+	remove_jail_by_name("cap_both_test");
+}
+
+/*
+ * A descriptor created by a privileged process is born with all three
+ * jail-management rights.
+ */
+ATF_TC_WITH_CLEANUP(cap_jail_born_privileged);
+ATF_TC_HEAD(cap_jail_born_privileged, tc)
+{
+	atf_tc_set_md_var(tc, "require.user", "root");
+	atf_tc_set_md_var(tc, "descr",
+	    "A root-created jail descriptor is born with all jail rights");
+}
+ATF_TC_BODY(cap_jail_born_privileged, tc)
+{
+	cap_rights_t rights;
+	int fd;
+
+	ATF_REQUIRE_FEATURE("security_capabilities");
+
+	remove_jail_by_name("cap_born_priv_test");
+
+	fd = create_jail_with_desc("cap_born_priv_test");
+	ATF_REQUIRE_MSG(fd >= 0, "create_jail_with_desc failed: %s",
+	    strerror(errno));
+
+	ATF_REQUIRE_MSG(cap_rights_get(fd, &rights) == 0,
+	    "cap_rights_get failed: %s", strerror(errno));
+	ATF_REQUIRE(cap_rights_is_set(&rights, CAP_JAIL_ATTACH));
+	ATF_REQUIRE(cap_rights_is_set(&rights, CAP_JAIL_REMOVE));
+	ATF_REQUIRE(cap_rights_is_set(&rights, CAP_JAIL_SET));
+
+	close(fd);
+}
+ATF_TC_CLEANUP(cap_jail_born_privileged, tc)
+{
+	remove_jail_by_name("cap_born_priv_test");
+}
+
+/*
+ * An fd obtained by an unprivileged process is born without the
+ * jail-management rights that process is not privileged to exercise,
+ * even though the fd itself was never explicitly limited.
+ */
+ATF_TC_WITH_CLEANUP(cap_jail_born_unprivileged);
+ATF_TC_HEAD(cap_jail_born_unprivileged, tc)
+{
+	atf_tc_set_md_var(tc, "require.user", "root");
+	atf_tc_set_md_var(tc, "descr",
+	    "An unprivileged jail descriptor is born without jail rights");
+}
+ATF_TC_BODY(cap_jail_born_unprivileged, tc)
+{
+	struct passwd *pw;
+	int fd, status;
+	pid_t pid;
+
+	ATF_REQUIRE_FEATURE("security_capabilities");
+
+	pw = getpwnam("nobody");
+	if (pw == NULL)
+		atf_tc_skip("the 'nobody' user is not available");
+
+	remove_jail_by_name("cap_born_unpriv_test");
+
+	fd = create_jail_with_desc("cap_born_unpriv_test");
+	ATF_REQUIRE_MSG(fd >= 0, "create_jail_with_desc failed: %s",
+	    strerror(errno));
+	close(fd);
+
+	pid = fork();
+	ATF_REQUIRE(pid >= 0);
+	if (pid == 0) {
+		cap_rights_t rights;
+		int cfd;
+
+		if (setgid(pw->pw_gid) != 0 || setuid(pw->pw_uid) != 0)
+			_exit(1);
+
+		cfd = get_jail_desc("cap_born_unpriv_test");
+		if (cfd < 0)
+			_exit(2);
+		if (cap_rights_get(cfd, &rights) != 0)
+			_exit(3);
+		
+		if (!cap_rights_is_set(&rights, CAP_FSTAT))
+			_exit(4);
+		if (cap_rights_is_set(&rights, CAP_JAIL_ATTACH) ||
+		    cap_rights_is_set(&rights, CAP_JAIL_REMOVE) ||
+		    cap_rights_is_set(&rights, CAP_JAIL_SET))
+			_exit(5);
+		
+		_exit(0);
+	}
+	waitpid(pid, &status, 0);
+	ATF_REQUIRE_MSG(WIFEXITED(status) && WEXITSTATUS(status) == 0,
+	    "unprivileged descriptor carried unexpected rights (status %d)",
+	    WIFEXITED(status) ? WEXITSTATUS(status) : -1);
+}
+ATF_TC_CLEANUP(cap_jail_born_unprivileged, tc)
+{
+	remove_jail_by_name("cap_born_unpriv_test");
+}
+
+/*
+ * Folding an attach into jail_set(2) with JAIL_ATTACH enters the jail and
+ * must therefore require CAP_JAIL_ATTACH, not just CAP_JAIL_SET.
+ */
+ATF_TC_WITH_CLEANUP(cap_jail_set_attach_bypass);
+ATF_TC_HEAD(cap_jail_set_attach_bypass, tc)
+{
+	atf_tc_set_md_var(tc, "require.user", "root");
+	atf_tc_set_md_var(tc, "descr",
+	    "CAP_JAIL_SET alone cannot attach via jail_set(JAIL_ATTACH)");
+}
+ATF_TC_BODY(cap_jail_set_attach_bypass, tc)
+{
+	cap_rights_t rights;
+	int fd, fd_both, error, status;
+	pid_t pid;
+
+	ATF_REQUIRE_FEATURE("security_capabilities");
+
+	remove_jail_by_name("cap_set_attach_test");
+
+	fd = create_jail_with_desc("cap_set_attach_test");
+	ATF_REQUIRE_MSG(fd >= 0, "create_jail_with_desc failed: %s",
+	    strerror(errno));
+
+	fd_both = dup(fd);
+	ATF_REQUIRE(fd_both >= 0);
+
+	cap_rights_init(&rights, CAP_JAIL_SET);
+	error = cap_rights_limit(fd, &rights);
+	ATF_REQUIRE_MSG(error == 0, "cap_rights_limit failed: %s",
+	    strerror(errno));
+
+	cap_rights_init(&rights, CAP_JAIL_SET, CAP_JAIL_ATTACH);
+	error = cap_rights_limit(fd_both, &rights);
+	ATF_REQUIRE_MSG(error == 0, "cap_rights_limit failed: %s",
+	    strerror(errno));
+
+	error = modify_jail_via_desc(fd);
+	ATF_REQUIRE_MSG(error >= 0, "jail_set with CAP_JAIL_SET failed: %s",
+	    strerror(errno));
+
+	pid = fork();
+	ATF_REQUIRE(pid >= 0);
+	if (pid == 0) {
+		error = modify_attach_jail_via_desc(fd);
+		if (error == -1 && errno == ENOTCAPABLE)
+			_exit(0);
+		_exit(error >= 0 ? 1 : 2);
+	}
+	waitpid(pid, &status, 0);
+	ATF_REQUIRE_MSG(WIFEXITED(status) && WEXITSTATUS(status) == 0,
+	    "jail_set(JAIL_ATTACH) with only CAP_JAIL_SET should fail with "
+	    "ENOTCAPABLE (child status %d)",
+	    WIFEXITED(status) ? WEXITSTATUS(status) : -1);
+
+	pid = fork();
+	ATF_REQUIRE(pid >= 0);
+	if (pid == 0) {
+		error = modify_attach_jail_via_desc(fd_both);
+		_exit(error >= 0 ? 0 : 1);
+	}
+	waitpid(pid, &status, 0);
+	ATF_REQUIRE_MSG(WIFEXITED(status) && WEXITSTATUS(status) == 0,
+	    "jail_set(JAIL_ATTACH) with CAP_JAIL_ATTACH should succeed "
+	    "(child status %d)",
+	    WIFEXITED(status) ? WEXITSTATUS(status) : -1);
+
+	close(fd_both);
+	close(fd);
+}
+ATF_TC_CLEANUP(cap_jail_set_attach_bypass, tc)
+{
+	remove_jail_by_name("cap_set_attach_test");
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+	ATF_TP_ADD_TC(tp, cap_jail_set);
+	ATF_TP_ADD_TC(tp, cap_jail_attach);
+	ATF_TP_ADD_TC(tp, cap_jail_remove);
+	ATF_TP_ADD_TC(tp, cap_jail_both);
+	ATF_TP_ADD_TC(tp, cap_jail_born_privileged);
+	ATF_TP_ADD_TC(tp, cap_jail_born_unprivileged);
+	ATF_TP_ADD_TC(tp, cap_jail_set_attach_bypass);
+
+	return (atf_no_error());
+}
-- 
2.54.0