[PATCH] jail: Add Capsicum rights for jail descriptor operations
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
Date: Sun, 05 Jul 2026 09:26:51 UTC
From: Kory Heard <koryheard@icloud.com>
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 <runxiyu@umich.edu>
---
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 <koryheard@icloud.com>
+ */
+
+/*
+ * 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