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


Reply via email to