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
