diff --git a/lib/libsys/Symbol.sys.map b/lib/libsys/Symbol.sys.map --- a/lib/libsys/Symbol.sys.map +++ b/lib/libsys/Symbol.sys.map @@ -380,6 +380,7 @@ FBSD_1.8 { getrlimitusage; kcmp; + setcred; }; FBSDprivate_1.0 { diff --git a/lib/libsys/_libsys.h b/lib/libsys/_libsys.h --- a/lib/libsys/_libsys.h +++ b/lib/libsys/_libsys.h @@ -463,6 +463,7 @@ typedef int (__sys_timerfd_settime_t)(int, int, const struct itimerspec *, struct itimerspec *); typedef int (__sys_kcmp_t)(pid_t, pid_t, int, uintptr_t, uintptr_t); typedef int (__sys_getrlimitusage_t)(u_int, int, rlim_t *); +typedef int (__sys_setcred_t)(u_int, const void *, size_t); void __sys_exit(int rval); int __sys_fork(void); @@ -863,6 +864,7 @@ int __sys_timerfd_settime(int fd, int flags, const struct itimerspec * new_value, struct itimerspec * old_value); int __sys_kcmp(pid_t pid1, pid_t pid2, int type, uintptr_t idx1, uintptr_t idx2); int __sys_getrlimitusage(u_int which, int flags, rlim_t * res); +int __sys_setcred(u_int flags, const void * wcred, size_t size); __END_DECLS #endif /* __LIBSYS_H_ */ diff --git a/lib/libsys/syscalls.map b/lib/libsys/syscalls.map --- a/lib/libsys/syscalls.map +++ b/lib/libsys/syscalls.map @@ -805,4 +805,6 @@ __sys_kcmp; _getrlimitusage; __sys_getrlimitusage; + _setcred; + __sys_setcred; }; diff --git a/sys/bsm/audit_kevents.h b/sys/bsm/audit_kevents.h --- a/sys/bsm/audit_kevents.h +++ b/sys/bsm/audit_kevents.h @@ -662,6 +662,7 @@ #define AUE_AIO_READV 43268 /* FreeBSD-specific. */ #define AUE_FSPACECTL 43269 /* FreeBSD-specific. */ #define AUE_TIMERFD 43270 /* FreeBSD/Linux. */ +#define AUE_SETCRED 43271 /* FreeBSD-specific. */ /* * Darwin BSM uses a number of AUE_O_* definitions, which are aliased to the diff --git a/sys/compat/freebsd32/freebsd32_syscall.h b/sys/compat/freebsd32/freebsd32_syscall.h --- a/sys/compat/freebsd32/freebsd32_syscall.h +++ b/sys/compat/freebsd32/freebsd32_syscall.h @@ -508,4 +508,5 @@ #define FREEBSD32_SYS_freebsd32_timerfd_settime 587 #define FREEBSD32_SYS_kcmp 588 #define FREEBSD32_SYS_getrlimitusage 589 -#define FREEBSD32_SYS_MAXSYSCALL 590 +#define FREEBSD32_SYS_setcred 590 +#define FREEBSD32_SYS_MAXSYSCALL 591 diff --git a/sys/compat/freebsd32/freebsd32_syscalls.c b/sys/compat/freebsd32/freebsd32_syscalls.c --- a/sys/compat/freebsd32/freebsd32_syscalls.c +++ b/sys/compat/freebsd32/freebsd32_syscalls.c @@ -595,4 +595,5 @@ "freebsd32_timerfd_settime", /* 587 = freebsd32_timerfd_settime */ "kcmp", /* 588 = kcmp */ "getrlimitusage", /* 589 = getrlimitusage */ + "setcred", /* 590 = setcred */ }; diff --git a/sys/compat/freebsd32/freebsd32_sysent.c b/sys/compat/freebsd32/freebsd32_sysent.c --- a/sys/compat/freebsd32/freebsd32_sysent.c +++ b/sys/compat/freebsd32/freebsd32_sysent.c @@ -657,4 +657,5 @@ { .sy_narg = AS(freebsd32_timerfd_settime_args), .sy_call = (sy_call_t *)freebsd32_timerfd_settime, .sy_auevent = AUE_TIMERFD, .sy_flags = SYF_CAPENABLED, .sy_thrcnt = SY_THR_STATIC }, /* 587 = freebsd32_timerfd_settime */ { .sy_narg = AS(kcmp_args), .sy_call = (sy_call_t *)sys_kcmp, .sy_auevent = AUE_NULL, .sy_flags = 0, .sy_thrcnt = SY_THR_STATIC }, /* 588 = kcmp */ { .sy_narg = AS(getrlimitusage_args), .sy_call = (sy_call_t *)sys_getrlimitusage, .sy_auevent = AUE_NULL, .sy_flags = SYF_CAPENABLED, .sy_thrcnt = SY_THR_STATIC }, /* 589 = getrlimitusage */ + { .sy_narg = AS(setcred_args), .sy_call = (sy_call_t *)sys_setcred, .sy_auevent = AUE_SETCRED, .sy_flags = SYF_CAPENABLED, .sy_thrcnt = SY_THR_STATIC }, /* 590 = setcred */ }; diff --git a/sys/compat/freebsd32/freebsd32_systrace_args.c b/sys/compat/freebsd32/freebsd32_systrace_args.c --- a/sys/compat/freebsd32/freebsd32_systrace_args.c +++ b/sys/compat/freebsd32/freebsd32_systrace_args.c @@ -3378,6 +3378,15 @@ *n_args = 3; break; } + /* setcred */ + case 590: { + struct setcred_args *p = params; + uarg[a++] = p->flags; /* u_int */ + uarg[a++] = (intptr_t)p->wcred; /* const void * */ + uarg[a++] = p->size; /* size_t */ + *n_args = 3; + break; + } default: *n_args = 0; break; @@ -9126,6 +9135,22 @@ break; }; break; + /* setcred */ + case 590: + switch (ndx) { + case 0: + p = "u_int"; + break; + case 1: + p = "userland const void *"; + break; + case 2: + p = "size_t"; + break; + default: + break; + }; + break; default: break; }; @@ -11014,6 +11039,11 @@ if (ndx == 0 || ndx == 1) p = "int"; break; + /* setcred */ + case 590: + if (ndx == 0 || ndx == 1) + p = "int"; + break; default: break; }; diff --git a/sys/kern/init_sysent.c b/sys/kern/init_sysent.c --- a/sys/kern/init_sysent.c +++ b/sys/kern/init_sysent.c @@ -656,4 +656,5 @@ { .sy_narg = AS(timerfd_settime_args), .sy_call = (sy_call_t *)sys_timerfd_settime, .sy_auevent = AUE_TIMERFD, .sy_flags = SYF_CAPENABLED, .sy_thrcnt = SY_THR_STATIC }, /* 587 = timerfd_settime */ { .sy_narg = AS(kcmp_args), .sy_call = (sy_call_t *)sys_kcmp, .sy_auevent = AUE_NULL, .sy_flags = 0, .sy_thrcnt = SY_THR_STATIC }, /* 588 = kcmp */ { .sy_narg = AS(getrlimitusage_args), .sy_call = (sy_call_t *)sys_getrlimitusage, .sy_auevent = AUE_NULL, .sy_flags = SYF_CAPENABLED, .sy_thrcnt = SY_THR_STATIC }, /* 589 = getrlimitusage */ + { .sy_narg = AS(setcred_args), .sy_call = (sy_call_t *)sys_setcred, .sy_auevent = AUE_SETCRED, .sy_flags = SYF_CAPENABLED, .sy_thrcnt = SY_THR_STATIC }, /* 590 = setcred */ }; diff --git a/sys/kern/kern_jail.c b/sys/kern/kern_jail.c --- a/sys/kern/kern_jail.c +++ b/sys/kern/kern_jail.c @@ -3956,6 +3956,7 @@ * Allow jailed processes to manipulate process UNIX * credentials in any way they see fit. */ + case PRIV_CRED_SETCRED: case PRIV_CRED_SETUID: case PRIV_CRED_SETEUID: case PRIV_CRED_SETGID: diff --git a/sys/kern/kern_prot.c b/sys/kern/kern_prot.c --- a/sys/kern/kern_prot.c +++ b/sys/kern/kern_prot.c @@ -73,6 +73,10 @@ #include #include +#ifdef MAC +#include +#endif + #include #ifdef REGRESSION @@ -484,6 +488,303 @@ return (error); } +static int +gidp_cmp(const void *p1, const void *p2) +{ + const gid_t g1 = *(const gid_t *)p1; + const gid_t g2 = *(const gid_t *)p2; + + return ((g1 > g2) - (g1 < g2)); +} + +#ifndef _SYS_SYSPROTO_H_ +struct setcred_args { + u_int flags; /* Flags, including version. */ + const void *wcred; /* Some setcred_vX structure. */ + size_t size; /* Length of setcred_vX structure. */ +}; +#endif +int +sys_setcred(struct thread *td, struct setcred_args *uap) +{ + const u_int flags = uap->flags; + const void *const uwcred = uap->wcred; + const size_t size = uap->size; + struct setcred_v0 wcred; +#ifdef MAC + struct mac mac; +#endif + gid_t smallgroups[CRED_SMALLGROUPS_NB]; + gid_t *groups = NULL; + int error; + + /* + * As the only point of this wrapper function is to copyin() from + * userland, we only interpret the data pieces we need to perform this + * operation and defer further sanity checks to kern_setcred_vX(). + */ + + /* There is only one version for now. */ + if (SETCREDF_TO_VERSION(flags) != 0 || size != sizeof(wcred)) + return (EINVAL); + + error = copyin(uwcred, &wcred, sizeof(wcred)); + if (error != 0) + return (error); + + if (flags & SETCREDF_SUPP_GROUPS) { + /* + * Check for the limit of number of groups right now in order to + * limit the amount of bytes to copy. + */ + if (wcred.sc_supp_groups_nb > ngroups_max) + return (EINVAL); + + /* + * Since we are going to be copying the supplementary groups + * from userland, make room also for the effective GID right + * now, to avoid having to allocate and copy again the + * supplementary groups. + */ + groups = wcred.sc_supp_groups_nb < CRED_SMALLGROUPS_NB ? + smallgroups : malloc((wcred.sc_supp_groups_nb + 1) * + sizeof(*groups), M_TEMP, M_WAITOK); + + error = copyin(wcred.sc_supp_groups, groups + 1, + wcred.sc_supp_groups_nb * sizeof(*groups)); + if (error != 0) + goto finish; + wcred.sc_supp_groups = groups + 1; + } else { + wcred.sc_supp_groups_nb = 0; + wcred.sc_supp_groups = NULL; + } + +#ifdef MAC + if (flags & SETCREDF_MAC_LABEL) { + error = mac_label_copyin(wcred.sc_label, &mac, NULL); + if (error != 0) { + wcred.sc_label = NULL; + goto finish; + } + wcred.sc_label = &mac; + } else + wcred.sc_label = NULL; +#endif + + error = kern_setcred_v0(td, flags, &wcred, groups); + +finish: + if (groups != NULL && groups != smallgroups) + free(groups, M_TEMP); +#ifdef MAC + if (wcred.sc_label != NULL) + free_copied_label(wcred.sc_label); +#endif + return (error); +} + +/* + * CAUTION: This function normalizes groups in 'wcred'. + * + * If 'preallocated_groups' is non-NULL, it must be an already allocated array + * of size 'wcred->sc_supp_groups_nb + 1', with the supplementary groups + * starting at index 1, and 'wcred->sc_supp_groups' then must point to the first + * supplementary group. + */ +int +kern_setcred_v0(struct thread *const td, const u_int flags, + struct setcred_v0 *const wcred, gid_t *preallocated_groups) +{ + struct proc *const p = td->td_proc; + struct ucred *new_cred, *old_cred, *to_free_cred; + struct uidinfo *uip = NULL, *ruip = NULL; +#ifdef MAC + void *mac_set_proc_data = NULL; + bool proc_label_set = false; +#endif + gid_t *groups = NULL; + gid_t smallgroups[CRED_SMALLGROUPS_NB]; + int error; + bool cred_set; + + MPASS(SETCREDF_TO_VERSION(flags) == 0); + /* Bail out on unrecognized flags. */ + if (flags & ~SETCREDF_MASK) + return (EINVAL); + + /* + * Part 1: We allocate and perform preparatory operations with no locks. + */ + + if (flags & SETCREDF_SUPP_GROUPS) { + if (wcred->sc_supp_groups_nb > ngroups_max) + return (EINVAL); + if (preallocated_groups != NULL) { + groups = preallocated_groups; + MPASS(preallocated_groups + 1 == wcred->sc_supp_groups); + } else { + groups = wcred->sc_supp_groups_nb < CRED_SMALLGROUPS_NB ? + smallgroups : + malloc((wcred->sc_supp_groups_nb + 1) * + sizeof(*groups), M_TEMP, M_WAITOK); + memcpy(groups + 1, wcred->sc_supp_groups, + wcred->sc_supp_groups_nb * sizeof(*groups)); + } + } + + if (flags & SETCREDF_MAC_LABEL) { +#ifdef MAC + error = mac_set_proc_prepare(td, wcred->sc_label, + &mac_set_proc_data); + if (error != 0) + goto free_groups; +#else + error = ENOTSUP; + goto free_groups; +#endif + } + + if (flags & SETCREDF_UID) { + AUDIT_ARG_EUID(wcred->sc_uid); + uip = uifind(wcred->sc_uid); + } + if (flags & SETCREDF_RUID) { + AUDIT_ARG_RUID(wcred->sc_ruid); + ruip = uifind(wcred->sc_ruid); + } + if (flags & SETCREDF_SVUID) + AUDIT_ARG_SUID(wcred->sc_svuid); + + if (flags & SETCREDF_GID) + AUDIT_ARG_EGID(wcred->sc_gid); + if (flags & SETCREDF_RGID) + AUDIT_ARG_RGID(wcred->sc_rgid); + if (flags & SETCREDF_SVGID) + AUDIT_ARG_SGID(wcred->sc_svgid); + if (flags & SETCREDF_SUPP_GROUPS) { + int ngrp = wcred->sc_supp_groups_nb; + + /* + * Output the raw supplementary groups array for better + * traceability. + */ + AUDIT_ARG_GROUPSET(groups + 1, ngrp); + ++ngrp; + groups_normalize(&ngrp, groups); + wcred->sc_supp_groups_nb = ngrp - 1; + } + + /* + * We first completely build the new credentials and only then pass them + * to MAC along with the old ones so that modules can check whether the + * requested transition is allowed. + */ + new_cred = crget(); + to_free_cred = new_cred; + if (flags & SETCREDF_SUPP_GROUPS) + crextend(new_cred, wcred->sc_supp_groups_nb + 1); + +#ifdef MAC + mac_cred_setcred_enter(); +#endif + + /* + * Part 2: We grab the process lock as to have a stable view of its + * current credentials, and prepare a copy of them with the requested + * changes applied under that lock. + */ + + PROC_LOCK(p); + old_cred = crcopysafe(p, new_cred); + + /* + * Change user IDs. + */ + if (flags & SETCREDF_UID) + change_euid(new_cred, uip); + if (flags & SETCREDF_RUID) + change_ruid(new_cred, ruip); + if (flags & SETCREDF_SVUID) + change_svuid(new_cred, wcred->sc_svuid); + + /* + * Change groups. + * + * crsetgroups_internal() changes both the effective and supplementary + * ones. + */ + if (flags & SETCREDF_SUPP_GROUPS) { + groups[0] = flags & SETCREDF_GID ? wcred->sc_gid : + new_cred->cr_gid; + crsetgroups_internal(new_cred, wcred->sc_supp_groups_nb + 1, + groups); + } else if (flags & SETCREDF_GID) + change_egid(new_cred, wcred->sc_gid); + if (flags & SETCREDF_RGID) + change_rgid(new_cred, wcred->sc_rgid); + if (flags & SETCREDF_SVGID) + change_svgid(new_cred, wcred->sc_svgid); + +#ifdef MAC + /* + * Change the MAC label. + */ + if (flags & SETCREDF_MAC_LABEL) { + error = mac_set_proc_core(td, new_cred, mac_set_proc_data); + if (error != 0) + goto unlock_finish; + proc_label_set = true; + } + + /* + * MAC security modules checks. + */ + error = mac_cred_check_setcred(flags, old_cred, new_cred); + if (error != 0) + goto unlock_finish; +#endif + /* + * Privilege check. + */ + error = priv_check_cred(old_cred, PRIV_CRED_SETCRED); + if (error != 0) + goto unlock_finish; + + /* + * Set the new credentials, noting that they have changed. + */ + cred_set = proc_set_cred_enforce_proc_lim(p, new_cred); + if (cred_set) { + setsugid(p); + to_free_cred = old_cred; + MPASS(error == 0); + } else + error = EAGAIN; + +unlock_finish: + PROC_UNLOCK(p); + /* + * Part 3: After releasing the process lock, we perform cleanups and + * finishing operations. + */ + +#ifdef MAC + if (mac_set_proc_data != NULL) + mac_set_proc_finish(td, proc_label_set, mac_set_proc_data); + mac_cred_setcred_exit(); +#endif + crfree(to_free_cred); + if (uip != NULL) + uifree(uip); + if (ruip != NULL) + uifree(ruip); +free_groups: + if (groups != preallocated_groups && groups != smallgroups) + free(groups, M_TEMP); /* Deals with 'groups' being NULL. */ + return (error); +} + /* * Use the clause in B.4.2.2 that allows setuid/setgid to be 4.2/4.3BSD * compatible. It says that setting the uid/gid to euid/egid is a special @@ -859,15 +1160,6 @@ return (error); } -static int -gidp_cmp(const void *p1, const void *p2) -{ - const gid_t g1 = *(const gid_t *)p1; - const gid_t g2 = *(const gid_t *)p2; - - return ((g1 > g2) - (g1 < g2)); -} - /* * CAUTION: This function normalizes 'groups', possibly also changing the value * of '*ngrpp' as a consequence. diff --git a/sys/kern/syscalls.c b/sys/kern/syscalls.c --- a/sys/kern/syscalls.c +++ b/sys/kern/syscalls.c @@ -595,4 +595,5 @@ "timerfd_settime", /* 587 = timerfd_settime */ "kcmp", /* 588 = kcmp */ "getrlimitusage", /* 589 = getrlimitusage */ + "setcred", /* 590 = setcred */ }; diff --git a/sys/kern/syscalls.master b/sys/kern/syscalls.master --- a/sys/kern/syscalls.master +++ b/sys/kern/syscalls.master @@ -3341,5 +3341,12 @@ _Out_ rlim_t *res ); } +590 AUE_SETCRED STD|CAPENABLED { + int setcred( + u_int flags, + _In_reads_bytes_(size) const void *wcred, + size_t size + ); + } ; vim: syntax=off diff --git a/sys/kern/systrace_args.c b/sys/kern/systrace_args.c --- a/sys/kern/systrace_args.c +++ b/sys/kern/systrace_args.c @@ -3465,6 +3465,15 @@ *n_args = 3; break; } + /* setcred */ + case 590: { + struct setcred_args *p = params; + uarg[a++] = p->flags; /* u_int */ + uarg[a++] = (intptr_t)p->wcred; /* const void * */ + uarg[a++] = p->size; /* size_t */ + *n_args = 3; + break; + } default: *n_args = 0; break; @@ -9271,6 +9280,22 @@ break; }; break; + /* setcred */ + case 590: + switch (ndx) { + case 0: + p = "u_int"; + break; + case 1: + p = "userland const void *"; + break; + case 2: + p = "size_t"; + break; + default: + break; + }; + break; default: break; }; @@ -11249,6 +11274,11 @@ if (ndx == 0 || ndx == 1) p = "int"; break; + /* setcred */ + case 590: + if (ndx == 0 || ndx == 1) + p = "int"; + break; default: break; }; diff --git a/sys/security/mac/mac_cred.c b/sys/security/mac/mac_cred.c --- a/sys/security/mac/mac_cred.c +++ b/sys/security/mac/mac_cred.c @@ -209,6 +209,55 @@ return (error); } +/* + * Entry hook for setcred(). + * + * Called with no lock held by setcred() so that MAC modules may allocate memory + * in preparation for checking privileges. A call to this hook is always + * followed by a matching call to mac_cred_setcred_exit(). Between these two, + * setcred() may or may not call mac_cred_check_setcred(). + */ +void +mac_cred_setcred_enter(void) +{ + + MAC_POLICY_PERFORM_NOSLEEP(cred_setcred_enter); +} + +MAC_CHECK_PROBE_DEFINE3(cred_check_setcred, "unsigned int", "struct ucred *", + "struct ucred *"); + +/* + * Check hook for setcred(). + * + * When called, the current process' lock is held. It thus cannot perform + * memory allocations, which must be done in advance in + * mac_cred_setcred_enter(). It *MUST NOT* tamper with the process' lock. + */ +int +mac_cred_check_setcred(u_int flags, const struct ucred *old_cred, + struct ucred *new_cred) +{ + int error; + + MAC_POLICY_CHECK_NOSLEEP(cred_check_setcred, flags, old_cred, new_cred); + MAC_CHECK_PROBE3(cred_check_setcred, error, flags, old_cred, new_cred); + + return (error); +} + +/* + * Exit hook for setcred(). + * + * Called with no lock held, exactly once per call to mac_cred_setcred_enter(). + */ +void +mac_cred_setcred_exit(void) +{ + + MAC_POLICY_PERFORM_NOSLEEP(cred_setcred_exit); +} + MAC_CHECK_PROBE_DEFINE2(cred_check_setuid, "struct ucred *", "uid_t"); int diff --git a/sys/security/mac/mac_framework.h b/sys/security/mac/mac_framework.h --- a/sys/security/mac/mac_framework.h +++ b/sys/security/mac/mac_framework.h @@ -72,6 +72,7 @@ struct mount; struct msg; struct msqid_kernel; +struct pipepair; struct proc; struct semid_kernel; struct shmfd; @@ -80,7 +81,6 @@ struct socket; struct sysctl_oid; struct sysctl_req; -struct pipepair; struct thread; struct timespec; struct ucred; @@ -115,6 +115,10 @@ int mac_cred_check_setaudit_addr(struct ucred *cred, struct auditinfo_addr *aia); int mac_cred_check_setauid(struct ucred *cred, uid_t auid); +void mac_cred_setcred_enter(void); +int mac_cred_check_setcred(u_int flags, const struct ucred *old_cred, + struct ucred *new_cred); +void mac_cred_setcred_exit(void); int mac_cred_check_setegid(struct ucred *cred, gid_t egid); int mac_cred_check_seteuid(struct ucred *cred, uid_t euid); int mac_cred_check_setgid(struct ucred *cred, gid_t gid); diff --git a/sys/security/mac/mac_policy.h b/sys/security/mac/mac_policy.h --- a/sys/security/mac/mac_policy.h +++ b/sys/security/mac/mac_policy.h @@ -144,6 +144,10 @@ typedef int (*mpo_cred_check_setaudit_addr_t)(struct ucred *cred, struct auditinfo_addr *aia); typedef int (*mpo_cred_check_setauid_t)(struct ucred *cred, uid_t auid); +typedef void (*mpo_cred_setcred_enter_t)(void); +typedef int (*mpo_cred_check_setcred_t)(u_int flags, + const struct ucred *old_cred, struct ucred *new_cred); +typedef void (*mpo_cred_setcred_exit_t)(void); typedef int (*mpo_cred_check_setegid_t)(struct ucred *cred, gid_t egid); typedef int (*mpo_cred_check_seteuid_t)(struct ucred *cred, uid_t euid); typedef int (*mpo_cred_check_setgid_t)(struct ucred *cred, gid_t gid); @@ -720,6 +724,9 @@ mpo_cred_check_setaudit_t mpo_cred_check_setaudit; mpo_cred_check_setaudit_addr_t mpo_cred_check_setaudit_addr; mpo_cred_check_setauid_t mpo_cred_check_setauid; + mpo_cred_setcred_enter_t mpo_cred_setcred_enter; + mpo_cred_check_setcred_t mpo_cred_check_setcred; + mpo_cred_setcred_exit_t mpo_cred_setcred_exit; mpo_cred_check_setuid_t mpo_cred_check_setuid; mpo_cred_check_seteuid_t mpo_cred_check_seteuid; mpo_cred_check_setgid_t mpo_cred_check_setgid; @@ -1033,8 +1040,9 @@ * 3 7.x * 4 8.x * 5 14.x + * 6 15.x */ -#define MAC_VERSION 5 +#define MAC_VERSION 6 #define MAC_POLICY_SET(mpops, mpname, mpfullname, mpflags, privdata_wanted) \ static struct mac_policy_conf mpname##_mac_policy_conf = { \ diff --git a/sys/security/mac_do/mac_do.c b/sys/security/mac_do/mac_do.c --- a/sys/security/mac_do/mac_do.c +++ b/sys/security/mac_do/mac_do.c @@ -5,15 +5,19 @@ */ #include +#include #include #include #include +#include +#include #include #include #include #include #include #include +#include #include #include #include @@ -21,9 +25,9 @@ #include #include -#include +#include -SYSCTL_DECL(_security_mac); +#include static SYSCTL_NODE(_security_mac, OID_AUTO, do, CTLFLAG_RW|CTLFLAG_MPSAFE, 0, "mac_do policy controls"); @@ -32,514 +36,2058 @@ SYSCTL_INT(_security_mac_do, OID_AUTO, enabled, CTLFLAG_RWTUN, &do_enabled, 0, "Enforce do policy"); +static int print_parse_error = 1; +SYSCTL_INT(_security_mac_do, OID_AUTO, print_parse_error, CTLFLAG_RWTUN, + &print_parse_error, 0, "Print parse errors on setting rules " + "(via sysctl(8))."); + static MALLOC_DEFINE(M_DO, "do_rule", "Rules for mac_do"); #define MAC_RULE_STRING_LEN 1024 -static unsigned mac_do_osd_jail_slot; +static unsigned osd_jail_slot; +static unsigned osd_thread_slot; -#define RULE_UID 1 -#define RULE_GID 2 -#define RULE_ANY 3 +#define IT_INVALID 0 /* Must stay 0. */ +#define IT_UID 1 +#define IT_GID 2 +#define IT_ANY 3 +#define IT_LAST IT_ANY + +static const char *id_type_to_str[] = { + [IT_INVALID] = "invalid", + [IT_UID] = "uid", + [IT_GID] = "gid", + /* See also parse_id_type(). */ + [IT_ANY] = "*", +}; + +#define PARSE_ERROR_SIZE 256 + +struct parse_error { + size_t pos; + char msg[PARSE_ERROR_SIZE]; +}; + +/* + * We assume that 'uid_t' and 'gid_t' are aliases to 'int' in conversions + * required for parsing rules specification strings. + */ +_Static_assert(sizeof(uid_t) == sizeof(int) && (int)(uid_t)-1 == -1 && + sizeof(gid_t) == sizeof(int) && (int)(gid_t)-1 == -1, + "mac_do(4) currently assumes that 'uid_t' and 'gid_t' are aliases to 'int'"); + +/* + * Internal flags. + * + * They either apply as per-type (t) or per-ID (i) but are conflated because all + * per-ID flags are also valid as per-type ones to qualify the "current" (".") + * per-type flag. Also, some of them are in fact exclusive, but we use one-hot + * encoding for simplicity. + * + * There is currently room for "only" 16 bits. As these flags are purely + * internal, they can be renumbered and/or their type changed as needed. + * + * See also the check_*() functions below. + */ +typedef uint16_t flags_t; + +/* (i,gid) Specification concerns primary groups. */ +#define MDF_PRIMARY 1 +/* (i,gid) Specification concerns supplementary groups. */ +#define MDF_SUPP_ALLOW 2 +/* (i,gid) Group must appear as a supplementary group. */ +#define MDF_SUPP_MUST 4 +/* (i,gid) Group must not appear as a supplementary group. */ +#define MDF_SUPP_DONT 8 +#define MDF_SUPP_MASK (MDF_SUPP_ALLOW | MDF_SUPP_MUST | MDF_SUPP_DONT) +#define MDF_ID_MASK (MDF_PRIMARY | MDF_SUPP_MASK) + +/* + * (t) All IDs allowed. + * + * For GIDs, MDF_ANY only concerns primary groups. The MDF_PRIMARY and + * MDF_SUPP_* flags never apply to MDF_ANY, but can be present if MDF_CURRENT is + * present also, as usual. + */ +#define MDF_ANY (1u << 8) +/* (t) Current IDs allowed. */ +#define MDF_CURRENT (1u << 9) +#define MDF_TYPE_COMMON_MASK (MDF_ANY | MDF_CURRENT) +/* (t,gid) All IDs allowed as supplementary groups. */ +#define MDF_ANY_SUPP (1u << 10) +/* (t,gid) Some ID has MDF_SUPP_MUST or MDF_SUPP_DONT. */ +#define MDF_MAY_REJ_SUPP (1u << 11) +/* (t,gid) Whether some target clause concerns primary groups. Used during + * parsing only. */ +#define MDF_HAS_PRIMARY_CLAUSE (1u << 12) +/* (t,gid) Whether some target clause concerns supplementary groups. Used + * during parsing only. */ +#define MDF_HAS_SUPP_CLAUSE (1u << 13) +#define MDF_TYPE_GID_MASK (MDF_ANY_SUPP | MDF_MAY_REJ_SUPP | \ + MDF_HAS_PRIMARY_CLAUSE | MDF_HAS_SUPP_CLAUSE) +#define MDF_TYPE_MASK (MDF_TYPE_COMMON_MASK | MDF_TYPE_GID_MASK) + +/* + * Persistent structures. + */ + +struct id_spec { + int id; + flags_t flags; /* See MDF_* above. */ +}; + +/* + * This limits the number of target clauses per type to 65535. With the current + * value of MAC_RULE_STRING_LEN (1024), this is way more than enough anyway. + */ +typedef uint16_t id_nb_t; +/* We only have a few IT_* types. */ +typedef uint16_t id_type_t; struct rule { - int from_type; - union { - uid_t f_uid; - gid_t f_gid; - }; - int to_type; - uid_t t_uid; - TAILQ_ENTRY(rule) r_entries; + STAILQ_ENTRY(rule) r_entries; + id_type_t from_type; + int from_id; + flags_t uid_flags; /* See MDF_* above. */ + id_nb_t uids_nb; + flags_t gid_flags; /* See MDF_* above. */ + id_nb_t gids_nb; + struct id_spec *uids; + struct id_spec *gids; }; -struct mac_do_rule { - char string[MAC_RULE_STRING_LEN]; - TAILQ_HEAD(rulehead, rule) head; +STAILQ_HEAD(rulehead, rule); + +struct rules { + char string[MAC_RULE_STRING_LEN]; + struct rulehead head; + volatile u_int use_count __aligned(CACHE_LINE_SIZE); }; -static struct mac_do_rule rules0; +/* + * Temporary structures used to build a 'struct rule' above. + */ + +struct id_elem { + STAILQ_ENTRY(id_elem) ie_entries; + struct id_spec spec; +}; + +STAILQ_HEAD(id_list, id_elem); + +#ifdef INVARIANTS +static void +check_type(const id_type_t type) +{ + if (type > IT_LAST) + panic("Invalid type number %u", type); +} static void -toast_rules(struct rulehead *head) +panic_for_unexpected_flags(const id_type_t type, const flags_t flags, + const char *const str) { - struct rule *r; - - while ((r = TAILQ_FIRST(head)) != NULL) { - TAILQ_REMOVE(head, r, r_entries); - free(r, M_DO); - } + panic("ID type %s: Unexpected flags %u (%s), ", id_type_to_str[type], + flags, str); } -static int -parse_rule_element(char *element, struct rule **rule) +static void +check_type_and_id_flags(const id_type_t type, const flags_t flags) { - int error = 0; - char *type, *id, *p; - struct rule *new; + const char *str; - new = malloc(sizeof(*new), M_DO, M_ZERO|M_WAITOK); - - type = strsep(&element, "="); - if (type == NULL) { - error = EINVAL; - goto out; - } - if (strcmp(type, "uid") == 0) { - new->from_type = RULE_UID; - } else if (strcmp(type, "gid") == 0) { - new->from_type = RULE_GID; - } else { - error = EINVAL; - goto out; - } - id = strsep(&element, ":"); - if (id == NULL) { - error = EINVAL; - goto out; - } - if (new->from_type == RULE_UID) - new->f_uid = strtol(id, &p, 10); - if (new->from_type == RULE_GID) - new->f_gid = strtol(id, &p, 10); - if (*p != '\0') { - error = EINVAL; - goto out; - } - if (*element == '\0') { - error = EINVAL; - goto out; - } - if (strcmp(element, "any") == 0 || strcmp(element, "*") == 0) { - new->to_type = RULE_ANY; - } else { - new->to_type = RULE_UID; - new->t_uid = strtol(element, &p, 10); - if (*p != '\0') { - error = EINVAL; - goto out; + check_type(type); + switch (type) { + case IT_UID: + if (flags != 0) { + str = "only 0 allowed"; + goto unexpected_flags; } - } -out: - if (error != 0) { - free(new, M_DO); - *rule = NULL; - } else - *rule = new; - return (error); -} - -static int -parse_rules(char *string, struct rulehead *head) -{ - struct rule *new; - char *element; - int error = 0; - - while ((element = strsep(&string, ",")) != NULL) { - if (strlen(element) == 0) - continue; - error = parse_rule_element(element, &new); - if (error) - goto out; - TAILQ_INSERT_TAIL(head, new, r_entries); - } -out: - if (error != 0) - toast_rules(head); - return (error); -} - -static struct mac_do_rule * -mac_do_rule_find(struct prison *spr, struct prison **prp) -{ - struct prison *pr; - struct mac_do_rule *rules; - - for (pr = spr;; pr = pr->pr_parent) { - mtx_lock(&pr->pr_mtx); - if (pr == &prison0) { - rules = &rules0; - break; + break; + case IT_GID: + if (flags & ~MDF_ID_MASK) { + str = "only bits in MDF_ID_MASK allowed"; + goto unexpected_flags; } - rules = osd_jail_get(pr, mac_do_osd_jail_slot); - if (rules != NULL) - break; - mtx_unlock(&pr->pr_mtx); + if (!powerof2(flags & MDF_SUPP_MASK)) { + str = "only a single flag in MDF_SUPP_MASK allowed"; + goto unexpected_flags; + } + break; + default: + __assert_unreachable(); } - *prp = pr; + return; +unexpected_flags: + panic_for_unexpected_flags(type, flags, str); +} + +static void +check_type_and_id_spec(const id_type_t type, const struct id_spec *const is) +{ + check_type_and_id_flags(type, is->flags); + if (is->id < 0) + panic("ID type %s: Negative id %d", id_type_to_str[type], + is->id); +} + +static void +check_type_and_type_flags(const id_type_t type, const flags_t flags) +{ + const char *str; + + check_type_and_id_flags(type, flags & MDF_ID_MASK); + if (flags & ~MDF_ID_MASK & ~MDF_TYPE_MASK) { + str = "only MDF_ID_MASK | MDF_TYPE_MASK bits allowed"; + goto unexpected_flags; + } + if ((flags & MDF_ANY) && (flags & MDF_CURRENT) && + (type == IT_UID || (flags & MDF_PRIMARY))) { + str = "MDF_ANY and MDF_CURRENT are exclusive for UIDs " + "or primary group GIDs"; + goto unexpected_flags; + } + if ((flags & MDF_ANY_SUPP) && (flags & MDF_CURRENT) && + (flags & MDF_SUPP_MASK)) { + str = "MDF_SUPP_ANY and MDF_CURRENT with supplementary " + "groups specification are exclusive"; + goto unexpected_flags; + } + if (((flags & MDF_PRIMARY) || (flags & MDF_ANY)) && + !(flags & MDF_HAS_PRIMARY_CLAUSE)) { + str = "Presence of folded primary clause not reflected " + "by presence of MDF_HAS_PRIMARY_CLAUSE"; + goto unexpected_flags; + } + if (((flags & MDF_SUPP_MASK) || (flags & MDF_ANY_SUPP)) && + !(flags & MDF_HAS_SUPP_CLAUSE)) { + str = "Presence of folded supplementary clause not reflected " + "by presence of MDF_HAS_SUPP_CLAUSE"; + goto unexpected_flags; + } + return; + +unexpected_flags: + panic_for_unexpected_flags(type, flags, str); +} +#else /* !INVARIANTS */ +#define check_type_and_id_flags(...) +#define check_type_and_id_spec(...) +#define check_type_and_type_flags(...) +#endif /* INVARIANTS */ + +/* + * Returns EALREADY if both flags have some overlap, or EINVAL if flags are + * incompatible, else 0 with flags successfully merged into 'dest'. + */ +static int +coalesce_id_flags(const flags_t src, flags_t *const dest) +{ + flags_t res; + + if ((src & *dest) != 0) + return (EALREADY); + + res = src | *dest; + + /* Check for compatibility of supplementary flags, and coalesce. */ + if (res & MDF_SUPP_MASK) { + /* MDF_SUPP_DONT incompatible with the rest. */ + if ((res & MDF_SUPP_DONT) && (res & MDF_SUPP_MASK & + ~MDF_SUPP_DONT)) + return (EINVAL); + /* Coalesce MDF_SUPP_ALLOW and MDF_SUPP_MUST into MDF_SUPP_MUST. */ + if ((res & MDF_SUPP_ALLOW) && (res & MDF_SUPP_MUST)) + res &= ~MDF_SUPP_ALLOW; + } + + *dest = res; + return (0); +} + +static void +toast_rules(struct rules *const rules) +{ + struct rulehead *const head = &rules->head; + struct rule *rule, *rule_next; + + STAILQ_FOREACH_SAFE(rule, head, r_entries, rule_next) { + free(rule->uids, M_DO); + free(rule->gids, M_DO); + free(rule, M_DO); + } + free(rules, M_DO); +} + +static struct rules * +alloc_rules(void) +{ + struct rules *const rules = malloc(sizeof(*rules), M_DO, M_WAITOK); + + _Static_assert(MAC_RULE_STRING_LEN > 0, "MAC_RULE_STRING_LEN <= 0!"); + rules->string[0] = 0; + STAILQ_INIT(&rules->head); + rules->use_count = 0; return (rules); } -static int -sysctl_rules(SYSCTL_HANDLER_ARGS) +static bool +is_null_or_empty(const char *s) { - char *copy_string, *new_string; - struct rulehead head, saved_head; - struct prison *pr; - struct mac_do_rule *rules; + return (s == NULL || s[0] == 0); +} + +/* + * String to non-negative int. + * + * Returns minus an error (EINVAL or EOVERFLOW) in case of failure. Rejects an + * explicit sign at start, as well as whitespaces. + */ +static int +strtonni(const char *const restrict s, const char **const restrict endptr, + int base) +{ + long l; + char *ep; + + if (s[0] != 0 && !isdigit(s[0])) { + if (endptr != NULL) + *endptr = s; + return (-EINVAL); + } + + l = strtol(s, &ep, base); + if (endptr != NULL) + *endptr = ep; + if (l < 0) + return (-EINVAL); + else if (l == LONG_MAX || l > INT_MAX) + return (-EOVERFLOW); + return ((int)l); +} + +/* + * strsep() variant skipping spaces and tabs. + * + * Skips spaces and tabs at beginning and end of the token before one of the + * 'delim' characters, i.e., at start of string and just before one of the + * delimiter characters (so it doesn't prevent tokens containing spaces and tabs + * in the middle). + */ +static char * +strsep_noblanks(char **const stringp, const char *delim) +{ + char *p = *stringp; + char *ret, *wsp; + size_t idx; + + if (p == NULL) + return (NULL); + + idx = strspn(p, " \t"); + p += idx; + + ret = strsep(&p, delim); + + /* Rewind spaces/tabs at the end. */ + if (p == NULL) + wsp = ret + strlen(ret); + else + wsp = p - 1; + for (; wsp != ret; --wsp) { + switch (wsp[-1]) { + case ' ': + case '\t': + continue; + } + break; + } + *wsp = '\0'; + + *stringp = p; + return (ret); +} + + +static void +alloc_parse_error(struct parse_error **const parse_error, const size_t pos, + const char *const fmt, ...) +{ + struct parse_error *const err = malloc(sizeof(*err), M_DO, M_WAITOK); + va_list ap; + + err->pos = pos; + va_start(ap, fmt); + vsnprintf(err->msg, PARSE_ERROR_SIZE, fmt, ap); + va_end(ap); + + MPASS(*parse_error == NULL); + *parse_error = err; +} + +static void +free_parse_error(struct parse_error *const parse_error) +{ + free(parse_error, M_DO); +} + +static int +parse_id_type(const char *const string, id_type_t *const type, + struct parse_error **const parse_error) +{ + /* + * Special case for "any", as the canonical form for IT_ANY in + * id_type_to_str[] is "*". + */ + if (strcmp(string, "any") == 0) { + *type = IT_ANY; + return (0); + } + + /* Start at 1 to avoid parsing "invalid". */ + for (size_t i = 1; i <= IT_LAST; ++i) { + if (strcmp(string, id_type_to_str[i]) == 0) { + *type = i; + return (0); + } + } + + *type = IT_INVALID; + alloc_parse_error(parse_error, 0, "No valid type found."); + return (EINVAL); +} + +static bool +parse_any(const char *const string) +{ + return (strcmp(string, "*") == 0 || strcmp(string, "any") == 0); +} + +static bool +has_clauses(const id_nb_t nb, const flags_t type_flags) +{ + return ((type_flags & MDF_TYPE_MASK) || nb != 0); +} + +static int +parse_target_clause(char *to, struct rule *const rule, + struct id_list *const uid_list, struct id_list *const gid_list, + struct parse_error **const parse_error) +{ + const char *const start = to; + char *to_type, *to_id; + const char *p; + struct id_list *list; + id_nb_t *nb; + flags_t *tflags; + struct id_elem *ie; + struct id_spec is = {}; + u_int gid_flags = 0; + id_type_t type; int error; - rules = mac_do_rule_find(req->td->td_ucred->cr_prison, &pr); - mtx_unlock(&pr->pr_mtx); - if (req->newptr == NULL) - return (sysctl_handle_string(oidp, rules->string, MAC_RULE_STRING_LEN, req)); + MPASS(*parse_error == NULL); + MPASS(to != NULL); + to_type = strsep_noblanks(&to, "="); + MPASS(to_type != NULL); + error = parse_id_type(to_type, &type, parse_error); + if (error != 0) + goto einval; - new_string = malloc(MAC_RULE_STRING_LEN, M_DO, - M_WAITOK|M_ZERO); - mtx_lock(&pr->pr_mtx); - strlcpy(new_string, rules->string, MAC_RULE_STRING_LEN); - mtx_unlock(&pr->pr_mtx); + to_id = strsep_noblanks(&to, ""); + switch (type) { + case IT_GID: + if (to_id == NULL) { + alloc_parse_error(parse_error, to_type - start, + "No '=' and ID specification after type '%s'.", + to_type); + goto einval; + } - error = sysctl_handle_string(oidp, new_string, MAC_RULE_STRING_LEN, req); - if (error) - goto out; + /* + * This is easily wrapped into a loop for when/if we allow + * multiple flags to be specified at once. + */ + switch (*to_id) { + case '+': + is.flags |= MDF_SUPP_ALLOW; + goto has_supp_clause; + case '!': + is.flags |= MDF_SUPP_MUST; + gid_flags |= MDF_MAY_REJ_SUPP; + goto has_supp_clause; + case '-': + is.flags |= MDF_SUPP_DONT; + gid_flags |= MDF_MAY_REJ_SUPP; + goto has_supp_clause; + has_supp_clause: + gid_flags |= MDF_HAS_SUPP_CLAUSE; + ++to_id; + break; + default: + is.flags |= MDF_PRIMARY; + gid_flags |= MDF_HAS_PRIMARY_CLAUSE; + break; + } - copy_string = strdup(new_string, M_DO); - TAILQ_INIT(&head); - error = parse_rules(copy_string, &head); - free(copy_string, M_DO); - if (error) - goto out; - TAILQ_INIT(&saved_head); - mtx_lock(&pr->pr_mtx); - TAILQ_CONCAT(&saved_head, &rules->head, r_entries); - TAILQ_CONCAT(&rules->head, &head, r_entries); - strlcpy(rules->string, new_string, MAC_RULE_STRING_LEN); - mtx_unlock(&pr->pr_mtx); - toast_rules(&saved_head); + list = gid_list; + nb = &rule->gids_nb; + tflags = &rule->gid_flags; + /* "*" or "any"? */ + if (parse_any(to_id)) { + /* + * We check that we have not seen any other clause of + * the same category (i.e., concerning primary or + * supplementary groups). + */ + if (is.flags & MDF_PRIMARY) { + if (*tflags & MDF_HAS_PRIMARY_CLAUSE) { + alloc_parse_error(parse_error, + to_id - start, + "'any' specified after another " + "(primary) GID."); + goto einval; + } + *tflags |= gid_flags | MDF_ANY; + } else { + /* + * If a supplementary group flag was present, it + * must be MDF_SUPP_ALLOW ("+"). + */ + if ((is.flags & MDF_SUPP_MASK) != MDF_SUPP_ALLOW) { + alloc_parse_error(parse_error, + to_id - start, + "'any' specified with another " + "flag than '+'."); + goto einval; + } + if (*tflags & MDF_HAS_SUPP_CLAUSE) { + alloc_parse_error(parse_error, + to_id - start, + "'+any' specified after another " + "(supplementary) GID."); + goto einval; + } + *tflags |= gid_flags | MDF_ANY_SUPP; + } + goto check_type_and_finish; + } else { + /* + * Check that we haven't already seen "any" for the same + * category. + */ + if ((is.flags & MDF_PRIMARY)) { + if (*tflags & MDF_ANY) { + alloc_parse_error(parse_error, + to_id - start, + "Some (primary) GID specified after " + "'any'."); + goto einval; + } + } else if (*tflags & MDF_ANY_SUPP && + is.flags & MDF_SUPP_ALLOW) { + alloc_parse_error(parse_error, + to_id - start, + "Some (supplementary) GID specified after " + "'+any'."); + goto einval; + } + *tflags |= gid_flags; + } + break; + + case IT_UID: + if (to_id == NULL) { + alloc_parse_error(parse_error, to_type - start, + "No '=' and ID specification after type '%s'.", + to_type); + goto einval; + } + + list = uid_list; + nb = &rule->uids_nb; + tflags = &rule->uid_flags; + + /* "*" or "any"? */ + if (parse_any(to_id)) { + /* There must not be any other clause. */ + if (has_clauses(*nb, *tflags)) { + alloc_parse_error(parse_error, to_id - start, + "'any' specified after another UID."); + goto einval; + } + *tflags |= MDF_ANY; + goto check_type_and_finish; + } else { + /* + * Check that we haven't already seen "any" for the same + * category. + */ + if (*tflags & MDF_ANY) { + alloc_parse_error(parse_error, to_id - start, + "Some UID specified after 'any'."); + goto einval; + } + } + break; + + case IT_ANY: + /* No ID allowed. */ + if (to_id != NULL) { + alloc_parse_error(parse_error, to_type - start, + "No '=' and ID allowed after type '%s'.", to_type); + goto einval; + } + /* + * We can't have IT_ANY after any other IT_*, it must be the + * only one. + */ + if (has_clauses(rule->uids_nb, rule->uid_flags) || + has_clauses(rule->gids_nb, rule->gid_flags)) { + alloc_parse_error(parse_error, to_type - start, + "Target clause of type '%s' coming after another " + "clause (must be alone).", to_type); + goto einval; + } + rule->uid_flags |= MDF_ANY; + rule->gid_flags |= MDF_ANY | MDF_ANY_SUPP | + MDF_HAS_PRIMARY_CLAUSE | MDF_HAS_SUPP_CLAUSE; + goto finish; + + default: + /* parse_id_type() returns no other types currently. */ + __assert_unreachable(); + } + + /* These cases have been treated above. */ + MPASS((type == IT_UID || type == IT_GID) && !parse_any(to_id)); + + /* "."? */ + if (strcmp(to_id, ".") == 0) { + if (*tflags & MDF_CURRENT) { + /* Duplicate "." . Try to coalesce. */ + error = coalesce_id_flags(is.flags, tflags); + if (error != 0) { + alloc_parse_error(parse_error, to_id - start, + "Incompatible flags with prior clause " + "with same target."); + goto einval; + } + } else + *tflags |= MDF_CURRENT | is.flags; + goto check_type_and_finish; + } + + /* Parse an ID. */ + is.id = strtonni(to_id, &p, 10); + if (is.id < 0 || *p != '\0') { + alloc_parse_error(parse_error, to_id - start, + "Cannot parse a numerical ID (base 10, no sign)."); + goto einval; + } + + /* + * We check for duplicate IDs and coalesce their 'struct id_spec' only + * at end of parse_single_rule() because it is much more performant then + * (using sorted arrays). + */ + ++*nb; + if (*nb == 0) { + alloc_parse_error(parse_error, 0, + "Too many target clauses of type '%s'.", to_type); + return (EOVERFLOW); + } + ie = malloc(sizeof(*ie), M_DO, M_WAITOK); + ie->spec = is; + STAILQ_INSERT_TAIL(list, ie, ie_entries); + check_type_and_id_spec(type, &is); +check_type_and_finish: + check_type_and_type_flags(type, *tflags); +finish: + return (0); +einval: + /* We must have built a parse error on error. */ + MPASS(*parse_error != NULL); + return (EINVAL); +} + +static int +int_cmp(const int i1, const int i2) +{ + return ((i1 > i2) - (i1 < i2)); +} + +static int +id_spec_cmp(const void *const p1, const void *const p2) +{ + const struct id_spec *const is1 = p1; + const struct id_spec *const is2 = p2; + + return (int_cmp(is1->id, is2->id)); +} + +/* + * Transfer content of 'list' into 'array', freeing and emptying list. + * + * 'nb' must be 'list''s length and not be greater than 'array''s size. The + * destination array is sorted by ID. Structures 'struct id_spec' with same IDs + * are coalesced if that makes sense (not including duplicate clauses), else + * EINVAL is returned. On success, 'nb' is updated (lowered) to account for + * coalesced specifications. The parameter 'type' is only for testing purposes + * (INVARIANTS). + */ +static int +pour_list_into_rule(const id_type_t type, struct id_list *const list, + struct id_spec *const array, id_nb_t *const nb, + struct parse_error **const parse_error) +{ + struct id_elem *ie, *ie_next; + size_t idx = 0; + + /* Fill the array. */ + STAILQ_FOREACH_SAFE(ie, list, ie_entries, ie_next) { + MPASS(idx < *nb); + array[idx] = ie->spec; + free(ie, M_DO); + ++idx; + } + MPASS(idx == *nb); + STAILQ_INIT(list); + + /* Sort it (by ID). */ + qsort(array, *nb, sizeof(*array), id_spec_cmp); + + /* Coalesce same IDs. */ + if (*nb != 0) { + size_t ref_idx = 0; + + for (idx = 1; idx < *nb; ++idx) { + const int id = array[idx].id; + + if (id == array[ref_idx].id) { + const int error = + coalesce_id_flags(array[idx].flags, + &array[ref_idx].flags); + + if (error != 0) { + alloc_parse_error(parse_error, 0, + "Incompatible or duplicate flags " + "for ID %d.", id); + return (EINVAL); + } + check_type_and_id_flags(type, + array[ref_idx].flags); + } + else { + ++ref_idx; + if (ref_idx != idx) + array[ref_idx] = array[idx]; + } + } + *nb = ref_idx + 1; + } + + return (0); +} + +/* + * See also first comments for parse_rule() below. + * + * The second part of a rule, called (or ), is a comma-separated + * (',') list of '=' clauses similar to that of the + * part, with the extensions that may also be "*" or "any" or ".", and that + * may contain at most one of the '+', '-' and '!' characters when + * is "gid" (no flags are allowed for "uid"). No two clauses in a single + * list may list the same . "*" and "any" both designate any ID for + * the , and are aliases to each other. In front of "any" (or "*"; only + * for GIDs), only the '+' flags is allowed. "." designates the process' + * current IDs for the . The precise meaning of flags and "." is + * explained in functions checking privileges below. + */ +static int +parse_single_rule(char *rule, struct rules *const rules, + struct parse_error **const parse_error) +{ + const char *const start = rule; + const char *from_type, *from_id, *p; + char *to_list; + struct id_list uid_list, gid_list; + struct id_elem *ie, *ie_next; + struct rule *new; + int error; + + MPASS(*parse_error == NULL); + STAILQ_INIT(&uid_list); + STAILQ_INIT(&gid_list); + + /* Freed when the 'struct rules' container is freed. */ + new = malloc(sizeof(*new), M_DO, M_WAITOK | M_ZERO); + + from_type = strsep_noblanks(&rule, "="); + MPASS(from_type != NULL); /* Because 'rule' was not NULL. */ + error = parse_id_type(from_type, &new->from_type, parse_error); + if (error != 0) + goto einval; + switch (new->from_type) { + case IT_UID: + case IT_GID: + break; + default: + alloc_parse_error(parse_error, 0, "Type '%s' not allowed in " + "the \"from\" part of rules."); + goto einval; + } + + from_id = strsep_noblanks(&rule, ":"); + if (is_null_or_empty(from_id)) { + alloc_parse_error(parse_error, 0, "No ID specified."); + goto einval; + } + + new->from_id = strtonni(from_id, &p, 10); + if (new->from_id < 0 || *p != '\0') { + alloc_parse_error(parse_error, from_id - start, + "Cannot parse a numerical ID (base 10, no sign)."); + goto einval; + } + + /* + * We will now parse the "to" list. + * + * In order to ease parsing, we will begin by building lists of target + * UIDs and GIDs in local variables 'uid_list' and 'gid_list'. The + * number of each type of IDs will be filled directly in 'new'. At end + * of parse, we will allocate both arrays of IDs to be placed into the + * 'uids' and 'gids' members, sort them, and discard the tail queues + * used to build them. This conversion to sorted arrays at end of parse + * allows to minimize memory allocations and enables searching IDs in + * O(log(n)) instead of linearly. + */ + to_list = strsep_noblanks(&rule, ","); + if (to_list == NULL) { + alloc_parse_error(parse_error, 0, "No target list."); + goto einval; + } + do { + error = parse_target_clause(to_list, new, &uid_list, &gid_list, + parse_error); + if (error != 0) { + (*parse_error)->pos += to_list - start; + goto einval; + } + + to_list = strsep_noblanks(&rule, ","); + } while (to_list != NULL); + + if (new->uids_nb != 0) { + new->uids = malloc(sizeof(*new->uids) * new->uids_nb, M_DO, + M_WAITOK); + error = pour_list_into_rule(IT_UID, &uid_list, new->uids, + &new->uids_nb, parse_error); + if (error != 0) + goto einval; + } + MPASS(STAILQ_EMPTY(&uid_list)); + if (!has_clauses(new->uids_nb, new->uid_flags)) { + /* No UID specified, default is "uid=.". */ + MPASS(new->uid_flags == 0); + new->uid_flags = MDF_CURRENT; + check_type_and_type_flags(IT_UID, new->uid_flags); + } + + if (new->gids_nb != 0) { + new->gids = malloc(sizeof(*new->gids) * new->gids_nb, M_DO, + M_WAITOK); + error = pour_list_into_rule(IT_GID, &gid_list, new->gids, + &new->gids_nb, parse_error); + if (error != 0) + goto einval; + } + MPASS(STAILQ_EMPTY(&gid_list)); + if (!has_clauses(new->gids_nb, new->gid_flags)) { + /* No GID specified, default is "gid=.,gid=!.". */ + MPASS(new->gid_flags == 0); + new->gid_flags |= MDF_CURRENT | MDF_PRIMARY | MDF_SUPP_MUST | + MDF_HAS_PRIMARY_CLAUSE | MDF_HAS_SUPP_CLAUSE; + check_type_and_type_flags(IT_GID, new->gid_flags); + } + + STAILQ_INSERT_TAIL(&rules->head, new, r_entries); + return (0); + +einval: + free(new->gids, M_DO); + free(new->uids, M_DO); + free(new, M_DO); + STAILQ_FOREACH_SAFE(ie, &gid_list, ie_entries, ie_next) + free(ie, M_DO); + STAILQ_FOREACH_SAFE(ie, &uid_list, ie_entries, ie_next) + free(ie, M_DO); + MPASS(*parse_error != NULL); + return (EINVAL); +} + +/* + * Parse rules specification and produce rule structures out of it. + * + * Returns 0 on success, with '*rulesp' made to point to a 'struct rule' + * representing the rules. On error, the returned value is non-zero and + * '*rulesp' is unchanged. If 'string' has length greater or equal to + * MAC_RULE_STRING_LEN, ENAMETOOLONG is returned. If it is not in the expected + * format, EINVAL is returned. If an error is returned, '*parse_error' is set + * to point to a 'struct parse_error' giving an error message for the problem, + * else '*parse_error' is set to NULL. + * + * Expected format: A semi-colon-separated list of rules of the form + * ":". The part is of the form "=" where + * is "uid" or "gid", an UID or GID (depending on ) and is + * "*", "any" or a comma-separated list of '=' clauses (see the + * comment for parse_single_rule() for more details). For convenience, empty + * rules are allowed (and do nothing), and spaces and tabs are allowed (and + * removed) around each token (tokens are natural ones, except that + * '' as a whole is considered a single token, so no blanks are + * allowed between '' and ''). + * + * Examples: + * - "uid=1001:uid=1010,gid=1010;uid=1002:any" + * - "gid=1010:gid=1011,gid=1012,gid=1013" + */ +static int +parse_rules(const char *const string, struct rules **const rulesp, + struct parse_error **const parse_error) +{ + const size_t len = strlen(string); + char *copy, *p, *rule; + struct rules *rules; + int error = 0; + + *parse_error = NULL; + + if (len >= MAC_RULE_STRING_LEN) { + alloc_parse_error(parse_error, 0, + "Rule specification string is too long (%zu, max %zu)", + len, MAC_RULE_STRING_LEN - 1); + return (ENAMETOOLONG); + } + + rules = alloc_rules(); + bcopy(string, rules->string, len + 1); + MPASS(rules->string[len] == '\0'); /* Catch some races. */ + + copy = malloc(len + 1, M_DO, M_WAITOK); + bcopy(string, copy, len + 1); + MPASS(copy[len] == '\0'); /* Catch some races. */ + + p = copy; + while ((rule = strsep_noblanks(&p, ";")) != NULL) { + if (rule[0] == '\0') + continue; + error = parse_single_rule(rule, rules, parse_error); + if (error != 0) { + (*parse_error)->pos += rule - copy; + toast_rules(rules); + goto out; + } + } + + *rulesp = rules; out: - free(new_string, M_DO); + free(copy, M_DO); + return (error); +} + +/* + * Find rules applicable to the passed prison. + * + * Returns the applicable rules (and never NULL). 'pr' must be unlocked. + * 'aprp' is set to the (ancestor) prison holding these, and it must be unlocked + * once the caller is done accessing the rules. '*aprp' is equal to 'pr' if and + * only if the current jail has its own set of rules. + */ +static struct rules * +find_rules(struct prison *const pr, struct prison **const aprp) +{ + struct prison *cpr, *ppr; + struct rules *rules; + + cpr = pr; + for (;;) { + prison_lock(cpr); + rules = osd_jail_get(cpr, osd_jail_slot); + if (rules != NULL) + break; + prison_unlock(cpr); + + ppr = cpr->pr_parent; + MPASS(ppr != NULL); /* prison0 always has rules. */ + cpr = ppr; + } + + *aprp = cpr; + return (rules); +} + +static void +hold_rules(struct rules *const rules) +{ + refcount_acquire(&rules->use_count); +} + +static void +drop_rules(struct rules *const rules) +{ + if (refcount_release(&rules->use_count)) + toast_rules(rules); +} + +#ifdef INVARIANTS +static void +check_rules_use_count(const struct rules *const rules, u_int expected) +{ + const u_int use_count = refcount_load(&rules->use_count); + + if (use_count != expected) + panic("MAC/do: Rules at %p: Use count is %u, expected %u", + rules, use_count, expected); +} +#else +#define check_rules_use_count(...) +#endif /* INVARIANTS */ + +/* + * OSD destructor for slot 'osd_jail_slot'. + * + * Called with 'value' not NULL. We have arranged that it is only ever called + * when the corresponding jail goes down or at module unload. + */ +static void +dealloc_jail_osd(void *const value) +{ + struct rules *const rules = value; + + /* + * If called because the "holding" jail goes down, no one should be + * using the rules but us at this point because no threads of that jail + * (or its sub-jails) should currently be executing (in particular, + * currently executing setcred()). The case of module unload is more + * complex. Although the MAC framework takes care that no hook is + * called while a module is unloading, the unload could happen between + * two calls to MAC hooks in the course of, e.g., executing setcred(), + * where the rules' reference count has been bumped to keep them alive + * even if the rules on the "holding" jail has been concurrently + * changed. These other references are held in our thread OSD slot, so + * we ensure that all thread's slots are freed first in mac_do_destroy() + * to be able to check that only one reference remains. + */ + check_rules_use_count(rules, 1); + toast_rules(rules); +} + +/* + * Remove the rules specifically associated to a prison. + * + * In practice, this means that the rules become inherited (from the closest + * ascendant that has some). + * + * Destroys the 'osd_jail_slot' slot of the passed jail. + */ +static void +remove_rules(struct prison *const pr) +{ + struct rules *old_rules; + int error __unused; + + prison_lock(pr); + /* + * We go to the burden of extracting rules first instead of just letting + * osd_jail_del() calling dealloc_jail_osd() as we want to decrement + * their use count, and possibly free them, outside of the prison lock. + */ + old_rules = osd_jail_get(pr, osd_jail_slot); + error = osd_jail_set(pr, osd_jail_slot, NULL); + /* osd_set() never fails nor allocate memory when 'value' is NULL. */ + MPASS(error == 0); + /* + * This completely frees the OSD slot, but doesn't call the destructor + * since we've just put NULL in the slot. + */ + osd_jail_del(pr, osd_jail_slot); + prison_unlock(pr); + + if (old_rules != NULL) + drop_rules(old_rules); +} + +/* + * Assign already built rules to a jail. + */ +static void +set_rules(struct prison *const pr, struct rules *const rules) +{ + struct rules *old_rules; + void **rsv; + + check_rules_use_count(rules, 0); + hold_rules(rules); + rsv = osd_reserve(osd_jail_slot); + + prison_lock(pr); + old_rules = osd_jail_get(pr, osd_jail_slot); + osd_jail_set_reserved(pr, osd_jail_slot, rsv, rules); + prison_unlock(pr); + if (old_rules != NULL) + drop_rules(old_rules); +} + +/* + * Assigns empty rules to a jail. + */ +static void +set_empty_rules(struct prison *const pr) +{ + struct rules *const rules = alloc_rules(); + + set_rules(pr, rules); +} + +/* + * Parse a rules specification and assign them to a jail. + * + * Returns the same error code as parse_rules() (which see). + */ +static int +parse_and_set_rules(struct prison *const pr, const char *rules_string, + struct parse_error **const parse_error) +{ + struct rules *rules; + int error; + + error = parse_rules(rules_string, &rules, parse_error); + if (error != 0) + return (error); + set_rules(pr, rules); + return (0); +} + +static int +mac_do_sysctl_rules(SYSCTL_HANDLER_ARGS) +{ + char *const buf = malloc(MAC_RULE_STRING_LEN, M_DO, M_WAITOK); + struct prison *const td_pr = req->td->td_ucred->cr_prison; + struct prison *pr; + struct rules *rules; + struct parse_error *parse_error; + int error; + + rules = find_rules(td_pr, &pr); + strlcpy(buf, rules->string, MAC_RULE_STRING_LEN); + prison_unlock(pr); + + error = sysctl_handle_string(oidp, buf, MAC_RULE_STRING_LEN, req); + if (error != 0 || req->newptr == NULL) + goto out; + + /* Set our prison's rules, not that of the jail we inherited from. */ + error = parse_and_set_rules(td_pr, buf, &parse_error); + if (error != 0) { + if (print_parse_error) + printf("MAC/do: Parse error at index %zu: %s\n", + parse_error->pos, parse_error->msg); + free_parse_error(parse_error); + } +out: + free(buf, M_DO); return (error); } SYSCTL_PROC(_security_mac_do, OID_AUTO, rules, - CTLTYPE_STRING|CTLFLAG_RW|CTLFLAG_MPSAFE, - 0, 0, sysctl_rules, "A", + CTLTYPE_STRING|CTLFLAG_RW|CTLFLAG_PRISON|CTLFLAG_MPSAFE, + 0, 0, mac_do_sysctl_rules, "A", "Rules"); -static void -destroy(struct mac_policy_conf *mpc) -{ - osd_jail_deregister(mac_do_osd_jail_slot); - toast_rules(&rules0.head); -} -static void -mac_do_alloc_prison(struct prison *pr, struct mac_do_rule **lrp) -{ - struct prison *ppr; - struct mac_do_rule *rules, *new_rules; - void **rsv; - - rules = mac_do_rule_find(pr, &ppr); - if (ppr == pr) - goto done; - - mtx_unlock(&ppr->pr_mtx); - new_rules = malloc(sizeof(*new_rules), M_PRISON, M_WAITOK|M_ZERO); - rsv = osd_reserve(mac_do_osd_jail_slot); - rules = mac_do_rule_find(pr, &ppr); - if (ppr == pr) { - free(new_rules, M_PRISON); - osd_free_reserved(rsv); - goto done; - } - mtx_lock(&pr->pr_mtx); - osd_jail_set_reserved(pr, mac_do_osd_jail_slot, rsv, new_rules); - TAILQ_INIT(&new_rules->head); -done: - if (lrp != NULL) - *lrp = rules; - mtx_unlock(&pr->pr_mtx); - mtx_unlock(&ppr->pr_mtx); -} - -static void -mac_do_dealloc_prison(void *data) -{ - struct mac_do_rule *r = data; - - toast_rules(&r->head); -} - -static int -mac_do_prison_set(void *obj, void *data) -{ - struct prison *pr = obj; - struct vfsoptlist *opts = data; - struct rulehead head, saved_head; - struct mac_do_rule *rules; - char *rules_string, *copy_string; - int error, jsys, len; - - error = vfs_copyopt(opts, "mdo", &jsys, sizeof(jsys)); - if (error == ENOENT) - jsys = -1; - error = vfs_getopt(opts, "mdo.rules", (void **)&rules_string, &len); - if (error == ENOENT) - rules = NULL; - else - jsys = JAIL_SYS_NEW; - switch (jsys) { - case JAIL_SYS_INHERIT: - mtx_lock(&pr->pr_mtx); - osd_jail_del(pr, mac_do_osd_jail_slot); - mtx_unlock(&pr->pr_mtx); - break; - case JAIL_SYS_NEW: - mac_do_alloc_prison(pr, &rules); - if (rules_string == NULL) - break; - copy_string = strdup(rules_string, M_DO); - TAILQ_INIT(&head); - error = parse_rules(copy_string, &head); - free(copy_string, M_DO); - if (error) - return (1); - TAILQ_INIT(&saved_head); - mtx_lock(&pr->pr_mtx); - TAILQ_CONCAT(&saved_head, &rules->head, r_entries); - TAILQ_CONCAT(&rules->head, &head, r_entries); - strlcpy(rules->string, rules_string, MAC_RULE_STRING_LEN); - mtx_unlock(&pr->pr_mtx); - toast_rules(&saved_head); - break; - } - return (0); -} - -SYSCTL_JAIL_PARAM_SYS_NODE(mdo, CTLFLAG_RW, "Jail MAC/do parameters"); -SYSCTL_JAIL_PARAM_STRING(_mdo, rules, CTLFLAG_RW, MAC_RULE_STRING_LEN, +SYSCTL_JAIL_PARAM_SYS_SUBNODE(mac, do, CTLFLAG_RW, "Jail MAC/do parameters"); +SYSCTL_JAIL_PARAM_STRING(_mac_do, rules, CTLFLAG_RW, MAC_RULE_STRING_LEN, "Jail MAC/do rules"); + static int -mac_do_prison_get(void *obj, void *data) +mac_do_jail_create(void *obj, void *data __unused) { - struct prison *ppr, *pr = obj; - struct vfsoptlist *opts = data; - struct mac_do_rule *rules; + struct prison *const pr = obj; + + set_empty_rules(pr); + return (0); +} + +static int +mac_do_jail_get(void *obj, void *data) +{ + struct prison *ppr, *const pr = obj; + struct vfsoptlist *const opts = data; + struct rules *rules; int jsys, error; - rules = mac_do_rule_find(pr, &ppr); - error = vfs_setopt(opts, "mdo", &jsys, sizeof(jsys)); + rules = find_rules(pr, &ppr); + + jsys = pr == ppr ? + (STAILQ_EMPTY(&rules->head) ? JAIL_SYS_DISABLE : JAIL_SYS_NEW) : + JAIL_SYS_INHERIT; + error = vfs_setopt(opts, "mac.do", &jsys, sizeof(jsys)); if (error != 0 && error != ENOENT) goto done; - error = vfs_setopts(opts, "mdo.rules", rules->string); + + error = vfs_setopts(opts, "mac.do.rules", rules->string); if (error != 0 && error != ENOENT) goto done; - mtx_unlock(&ppr->pr_mtx); + error = 0; done: - return (0); -} - -static int -mac_do_prison_create(void *obj, void *data __unused) -{ - struct prison *pr = obj; - - mac_do_alloc_prison(pr, NULL); - return (0); -} - -static int -mac_do_prison_remove(void *obj, void *data __unused) -{ - struct prison *pr = obj; - struct mac_do_rule *r; - - mtx_lock(&pr->pr_mtx); - r = osd_jail_get(pr, mac_do_osd_jail_slot); - mtx_unlock(&pr->pr_mtx); - toast_rules(&r->head); - return (0); -} - -static int -mac_do_prison_check(void *obj, void *data) -{ - struct vfsoptlist *opts = data; - char *rules_string; - int error, jsys, len; - - error = vfs_copyopt(opts, "mdo", &jsys, sizeof(jsys)); - if (error != ENOENT) { - if (error != 0) - return (error); - if (jsys != JAIL_SYS_NEW && jsys != JAIL_SYS_INHERIT) - return (EINVAL); - } - error = vfs_getopt(opts, "mdo.rules", (void **)&rules_string, &len); - if (error != ENOENT) { - if (error != 0) - return (error); - if (len > MAC_RULE_STRING_LEN) { - vfs_opterror(opts, "mdo.rules too long"); - return (ENAMETOOLONG); - } - } - if (error == ENOENT) - error = 0; + prison_unlock(ppr); return (error); } -static void -init(struct mac_policy_conf *mpc) +/* + * We perform only cheap checks here, i.e., we do not really parse the rules + * specification string, if any. + */ +static int +mac_do_jail_check(void *obj, void *data) { - static osd_method_t methods[PR_MAXMETHOD] = { - [PR_METHOD_CREATE] = mac_do_prison_create, - [PR_METHOD_GET] = mac_do_prison_get, - [PR_METHOD_SET] = mac_do_prison_set, - [PR_METHOD_CHECK] = mac_do_prison_check, - [PR_METHOD_REMOVE] = mac_do_prison_remove, - }; - struct prison *pr; + struct vfsoptlist *opts = data; + char *rules_string; + int error, jsys, size; - mac_do_osd_jail_slot = osd_jail_register(mac_do_dealloc_prison, methods); - TAILQ_INIT(&rules0.head); - sx_slock(&allprison_lock); - TAILQ_FOREACH(pr, &allprison, pr_list) - mac_do_alloc_prison(pr, NULL); - sx_sunlock(&allprison_lock); + error = vfs_copyopt(opts, "mac.do", &jsys, sizeof(jsys)); + if (error == ENOENT) + jsys = -1; + else { + if (error != 0) + return (error); + if (jsys != JAIL_SYS_DISABLE && jsys != JAIL_SYS_NEW && + jsys != JAIL_SYS_INHERIT) + return (EINVAL); + } + + /* + * We use vfs_getopt() here instead of vfs_getopts() to get the length. + * We perform the additional checks done by the latter here, even if + * jail_set() calls vfs_getopts() itself later (they becoming + * inconsistent wouldn't cause any security problem). + */ + error = vfs_getopt(opts, "mac.do.rules", (void**)&rules_string, &size); + if (error == ENOENT) { + /* + * Default (in absence of "mac.do.rules") is to disable (and, in + * particular, not inherit). + */ + if (jsys == -1) + jsys = JAIL_SYS_DISABLE; + + if (jsys == JAIL_SYS_NEW) { + vfs_opterror(opts, "'mac.do.rules' must be specified " + "given 'mac.do''s value"); + return (EINVAL); + } + + /* Absence of "mac.do.rules" at this point is OK. */ + error = 0; + } else { + if (error != 0) + return (error); + + /* Not a proper string. */ + if (size == 0 || rules_string[size - 1] != '\0') { + vfs_opterror(opts, "'mac.do.rules' not a proper string"); + return (EINVAL); + } + + if (size > MAC_RULE_STRING_LEN) { + vfs_opterror(opts, "'mdo.rules' too long"); + return (ENAMETOOLONG); + } + + if (jsys == -1) + /* Default (if "mac.do.rules" is present). */ + jsys = rules_string[0] == '\0' ? JAIL_SYS_DISABLE : + JAIL_SYS_NEW; + + /* + * Be liberal and accept JAIL_SYS_DISABLE and JAIL_SYS_INHERIT + * with an explicit empty rules specification. + */ + switch (jsys) { + case JAIL_SYS_DISABLE: + case JAIL_SYS_INHERIT: + if (rules_string[0] != '\0') { + vfs_opterror(opts, "'mac.do.rules' specified " + "but should not given 'mac.do''s value"); + return (EINVAL); + } + break; + } + } + + return (error); +} + +static int +mac_do_jail_set(void *obj, void *data) +{ + struct prison *pr = obj; + struct vfsoptlist *opts = data; + char *rules_string; + struct parse_error *parse_error; + int error, jsys; + + /* + * The invariants checks used below correspond to what has already been + * checked in jail_check() above. + */ + + error = vfs_copyopt(opts, "mac.do", &jsys, sizeof(jsys)); + MPASS(error == 0 || error == ENOENT); + if (error != 0) + jsys = -1; /* Mark unfilled. */ + + rules_string = vfs_getopts(opts, "mac.do.rules", &error); + MPASS(error == 0 || error == ENOENT); + if (error == 0) { + MPASS(strlen(rules_string) < MAC_RULE_STRING_LEN); + if (jsys == -1) + /* Default (if "mac.do.rules" is present). */ + jsys = rules_string[0] == '\0' ? JAIL_SYS_DISABLE : + JAIL_SYS_NEW; + else + MPASS(jsys == JAIL_SYS_NEW || + ((jsys == JAIL_SYS_DISABLE || + jsys == JAIL_SYS_INHERIT) && + rules_string[0] == '\0')); + } else { + MPASS(jsys != JAIL_SYS_NEW); + if (jsys == -1) + /* + * Default (in absence of "mac.do.rules") is to disable + * (and, in particular, not inherit). + */ + jsys = JAIL_SYS_DISABLE; + /* If disabled, we'll store an empty rule specification. */ + if (jsys == JAIL_SYS_DISABLE) + rules_string = ""; + } + + switch (jsys) { + case JAIL_SYS_INHERIT: + remove_rules(pr); + error = 0; + break; + case JAIL_SYS_DISABLE: + case JAIL_SYS_NEW: + error = parse_and_set_rules(pr, rules_string, &parse_error); + if (error != 0) { + vfs_opterror(opts, + "MAC/do: Parse error at index %zu: %s\n", + parse_error->pos, parse_error->msg); + free_parse_error(parse_error); + } + break; + default: + __assert_unreachable(); + } + return (error); +} + +/* + * OSD jail methods. + * + * There is no PR_METHOD_REMOVE, as OSD storage is destroyed by the common jail + * code (see prison_cleanup()), which triggers a run of our dealloc_jail_osd() + * destructor. + */ +static const osd_method_t osd_methods[PR_MAXMETHOD] = { + [PR_METHOD_CREATE] = mac_do_jail_create, + [PR_METHOD_GET] = mac_do_jail_get, + [PR_METHOD_CHECK] = mac_do_jail_check, + [PR_METHOD_SET] = mac_do_jail_set, +}; + + +/* + * Common header structure. + * + * Each structure that is used to pass information between some MAC check + * function and priv_grant() must start with this header. + */ +struct mac_do_data_header { + /* Size of the allocated buffer holding the containing structure. */ + size_t allocated_size; + /* Full size of the containing structure. */ + size_t size; + /* + * For convenience, we use privilege numbers as an identifier for the + * containing structure's type, since there is one distinct privilege + * for each privilege changing function we are supporting. 0 in 'priv' + * indicates this header is uninitialized. + */ + int priv; + /* Rules to apply. */ + struct rules *rules; +}; + +/* + * The case of unusable or absent per-thread data can actually happen as nothing + * prevents, e.g., priv_check*() with privilege 'priv' to be called standalone, + * as it is currently by, e.g., the Linux emulator for PRIV_CRED_SETUID. We + * interpret such calls to priv_check*() as full, unrestricted requests for + * 'priv', contrary to what we're doing here for selected operations, and + * consequently will not grant the requested privilege. + * + * Also, we protect ourselves from a concurrent change of 'do_enabled' while + * a call to setcred() is in progress by storing the rules per-thread + * which is then consulted by each successive hook so that they all have + * a coherent view of the specifications, and we empty the slot (actually, mark + * it as empty) when MAC/do is disabled. + */ +static int +check_data_usable(const void *const data, const size_t size, const int priv) +{ + const struct mac_do_data_header *const hdr = data; + + if (hdr == NULL || hdr->priv == 0) + return (ENOENT); + /* + * Impacting changes in the protocols we are based on... Don't crash in + * production. + */ + if (hdr->priv != priv) { + MPASS(hdr->priv == priv); + return (EBUSY); + } + MPASS(hdr->size == size); + MPASS(hdr->size <= hdr->allocated_size); + return (0); +} + +static void +clear_data(void *const data) +{ + struct mac_do_data_header *const hdr = data; + + if (hdr != NULL) { + drop_rules(hdr->rules); + /* We don't deallocate so as to save time on next access. */ + hdr->priv = 0; + } +} + +static void * +fetch_data(void) +{ + return (osd_thread_get_unlocked(curthread, osd_thread_slot)); } static bool -rule_is_valid(struct ucred *cred, struct rule *r) +is_data_reusable(const void *const data, const size_t size) { - if (r->from_type == RULE_UID && r->f_uid == cred->cr_uid) + const struct mac_do_data_header *const hdr = data; + + return (hdr != NULL && size <= hdr->allocated_size); +} + +static void +set_data_header(void *const data, const size_t size, const int priv, + struct rules *const rules) +{ + struct mac_do_data_header *const hdr = data; + + MPASS(hdr->priv == 0); + MPASS(priv != 0); + MPASS(size <= hdr->allocated_size); + hdr->size = size; + hdr->priv = priv; + hdr->rules = rules; +} + +/* The proc lock (and any other non-sleepable lock) must not be held. */ +static void * +alloc_data(void *const data, const size_t size) +{ + struct mac_do_data_header *const hdr = realloc(data, size, M_DO, + M_WAITOK); + + MPASS(size >= sizeof(struct mac_do_data_header)); + hdr->allocated_size = size; + hdr->priv = 0; + if (hdr != data) { + /* + * This call either reuses the existing memory allocated for the + * slot or tries to allocate some without blocking. + */ + int error = osd_thread_set(curthread, osd_thread_slot, hdr); + + if (error != 0) { + /* Going to make a M_WAITOK allocation. */ + void **const rsv = osd_reserve(osd_thread_slot); + + error = osd_thread_set_reserved(curthread, + osd_thread_slot, rsv, hdr); + MPASS(error == 0); + } + } + return (hdr); +} + +/* Destructor for 'osd_thread_slot'. */ +static void +dealloc_thread_osd(void *const value) +{ + free(value, M_DO); +} + +/* + * Whether to grant access to some primary group according to flags. + * + * The passed 'flags' must be those of a rule's matching GID, or the IT_GID type + * flags when MDF_CURRENT has been matched. + * + * Return values: + * - 0: Access granted. + * - EJUSTRETURN: Flags are agnostic. + */ +static int +grant_primary_group_from_flags(const flags_t flags) +{ + return (flags & MDF_PRIMARY ? 0 : EJUSTRETURN); +} + +/* + * Same as grant_primary_group_from_flags(), but for supplementary groups. + * + * Return values: + * - 0: Access granted. + * - EJUSTRETURN: Flags are agnostic. + * - EPERM: Access denied. + */ +static int +grant_supplementary_group_from_flags(const flags_t flags) +{ + if (flags & MDF_SUPP_MASK) + return (flags & MDF_SUPP_DONT ? EPERM : 0); + + return (EJUSTRETURN); +} + +/* + * Check whether to grant access to supplementary groups. + */ +static int +rule_grant_supplementary_groups(const struct rule *const rule, + const struct ucred *const old_cred, const struct ucred *const new_cred) +{ + const int nb_groups = new_cred->cr_ngroups - 1; + const gid_t *const groups = new_cred->cr_groups + 1; + int error; + id_nb_t r_idx = 0; + + if (rule->gid_flags & MDF_ANY_SUPP) + /* + * All supplementary groups will be accepted, no need to loop + * over them. + */ + return (0); + + for (int i = 0; i < nb_groups; ++i) { + const int gid = groups[i]; + + /* Was MDF_CURRENT specified, and is 'gid' a current GID? */ + if ((rule->gid_flags & MDF_CURRENT) && + group_is_supplementary(gid, old_cred)) { + error = grant_supplementary_group_from_flags + (rule->gid_flags); + if (error == 0) + continue; + /* + * MDF_SUPP_DONT must have been handled in the relevant + * check_*() function (or these functions weren't + * executed at all (e.g., Linux emulation), and we + * shouldn't get this deep). + */ + MPASS(error == EJUSTRETURN); + } + + /* + * Search by GID for a corresponding 'struct id_spec'. + * + * As both the new credentials and the groups in the rule are + * sorted, we just need to browse the rules' groups only once + * for each one. + */ + for (; r_idx < rule->gids_nb; ++r_idx) { + const int r_gid = rule->gids[r_idx].id; + + if (gid == r_gid) { + error = grant_supplementary_group_from_flags + (rule->gids[r_idx].flags); + if (error == 0) + goto next_group; + /* Same as above. */ + MPASS(error == EJUSTRETURN); + break; + } else if (gid < r_gid) + break; + } + + /* 'gid' wasn't accepted. */ + return (EPERM); +next_group: + ; + } + + return (0); +} + +static int +rule_grant_primary_group(const struct rule *const rule, + const struct ucred *const old_cred, const gid_t gid) +{ + struct id_spec gid_is = {}; + const struct id_spec *found_is; + int error; + + if (rule->gid_flags & MDF_ANY) + return (0); + + /* Was MDF_CURRENT specified, and is 'gid' a current GID? */ + if ((rule->gid_flags & MDF_CURRENT) && + group_is_primary(gid, old_cred)) { + error = grant_primary_group_from_flags(rule->gid_flags); + if (error == 0) + return (0); + } + + /* Search by GID for a corresponding 'struct id_spec'. */ + gid_is.id = gid; + found_is = bsearch(&gid_is, rule->gids, rule->gids_nb, + sizeof(*rule->gids), id_spec_cmp); + + if (found_is != NULL) { + error = grant_primary_group_from_flags(found_is->flags); + if (error == 0) + return (0); + } + + return (EPERM); +} + +static int +rule_grant_primary_groups(const struct rule *const rule, + const struct ucred *const old_cred, const struct ucred *const new_cred) +{ + int error; + + /* Shortcut. */ + if (rule->gid_flags & MDF_ANY) + return (0); + + error = rule_grant_primary_group(rule, old_cred, new_cred->cr_gid); + if (error != 0) + return (error); + error = rule_grant_primary_group(rule, old_cred, new_cred->cr_rgid); + if (error != 0) + return (error); + error = rule_grant_primary_group(rule, old_cred, new_cred->cr_svgid); + if (error != 0) + return (error); + return (0); +} + +static bool +user_is_current(const uid_t uid, const struct ucred *const old_cred) +{ + return (uid == old_cred->cr_uid || uid == old_cred->cr_ruid || + uid == old_cred->cr_svuid); +} + +static int +rule_grant_user(const struct rule *const rule, + const struct ucred *const old_cred, const uid_t uid) +{ + struct id_spec uid_is = {}; + const struct id_spec *found_is; + + if (rule->uid_flags & MDF_ANY) + return (0); + + /* Was MDF_CURRENT specified, and is 'uid' a current UID? */ + if ((rule->uid_flags & MDF_CURRENT) && + user_is_current(uid, old_cred)) + return (0); + + /* Search by UID for a corresponding 'struct id_spec'. */ + uid_is.id = uid; + found_is = bsearch(&uid_is, rule->uids, rule->uids_nb, + sizeof(*rule->uids), id_spec_cmp); + + if (found_is != NULL) + return (0); + + return (EPERM); +} + +static int +rule_grant_users(const struct rule *const rule, + const struct ucred *const old_cred, const struct ucred *const new_cred) +{ + int error; + + /* Shortcut. */ + if (rule->uid_flags & MDF_ANY) + return (0); + + error = rule_grant_user(rule, old_cred, new_cred->cr_uid); + if (error != 0) + return (error); + error = rule_grant_user(rule, old_cred, new_cred->cr_ruid); + if (error != 0) + return (error); + error = rule_grant_user(rule, old_cred, new_cred->cr_svuid); + if (error != 0) + return (error); + + return (0); +} + +static int +rule_grant_setcred(const struct rule *const rule, + const struct ucred *const old_cred, const struct ucred *const new_cred) +{ + int error; + + error = rule_grant_users(rule, old_cred, new_cred); + if (error != 0) + return (error); + error = rule_grant_primary_groups(rule, old_cred, new_cred); + if (error != 0) + return (error); + error = rule_grant_supplementary_groups(rule, old_cred, new_cred); + if (error != 0) + return (error); + + return (0); +} + +static bool +rule_applies(const struct rule *const rule, const struct ucred *const cred) +{ + if (rule->from_type == IT_UID && rule->from_id == cred->cr_uid) return (true); - if (r->from_type == RULE_GID && groupmember(r->f_gid, cred)) + if (rule->from_type == IT_GID && groupmember(rule->from_id, cred)) return (true); return (false); } -static int -priv_grant(struct ucred *cred, int priv) -{ - struct rule *r; - struct prison *pr; - struct mac_do_rule *rule; - - if (do_enabled == 0) - return (EPERM); - - rule = mac_do_rule_find(cred->cr_prison, &pr); - TAILQ_FOREACH(r, &rule->head, r_entries) { - if (rule_is_valid(cred, r)) { - switch (priv) { - case PRIV_CRED_SETGROUPS: - case PRIV_CRED_SETUID: - mtx_unlock(&pr->pr_mtx); - return (0); - default: - break; - } - } - } - mtx_unlock(&pr->pr_mtx); - return (EPERM); -} +/* + * To pass data between check_setcred() and priv_grant() (on PRIV_CRED_SETCRED). + */ +struct mac_do_setcred_data { + struct mac_do_data_header hdr; + const struct ucred *new_cred; + u_int setcred_flags; +}; static int -check_setgroups(struct ucred *cred, int ngrp, gid_t *groups) +mac_do_priv_grant(struct ucred *cred, int priv) { - struct rule *r; - char *fullpath = NULL; - char *freebuf = NULL; - struct prison *pr; - struct mac_do_rule *rule; - - if (do_enabled == 0) - return (0); - if (cred->cr_uid == 0) - return (0); - - if (vn_fullpath(curproc->p_textvp, &fullpath, &freebuf) != 0) - return (EPERM); - if (strcmp(fullpath, "/usr/bin/mdo") != 0) { - free(freebuf, M_TEMP); - return (EPERM); - } - free(freebuf, M_TEMP); - - rule = mac_do_rule_find(cred->cr_prison, &pr); - TAILQ_FOREACH(r, &rule->head, r_entries) { - if (rule_is_valid(cred, r)) { - mtx_unlock(&pr->pr_mtx); - return (0); - } - } - mtx_unlock(&pr->pr_mtx); - - return (EPERM); -} - -static int -check_setuid(struct ucred *cred, uid_t uid) -{ - struct rule *r; + struct mac_do_setcred_data *const data = fetch_data(); + const struct rules *rules; + const struct ucred *new_cred; + const struct rule *rule; + u_int setcred_flags; int error; - char *fullpath = NULL; - char *freebuf = NULL; - struct prison *pr; - struct mac_do_rule *rule; - if (do_enabled == 0) - return (0); - if (cred->cr_uid == uid || cred->cr_uid == 0 || cred->cr_ruid == 0) - return (0); - - if (vn_fullpath(curproc->p_textvp, &fullpath, &freebuf) != 0) + /* Bail out fast if we aren't concerned. */ + if (priv != PRIV_CRED_SETCRED) return (EPERM); - if (strcmp(fullpath, "/usr/bin/mdo") != 0) { - free(freebuf, M_TEMP); - return (EPERM); - } - free(freebuf, M_TEMP); + /* + * Do we have to do something? + */ + if (check_data_usable(data, sizeof(*data), priv) != 0) + /* No. */ + return (EPERM); + + rules = data->hdr.rules; + new_cred = data->new_cred; + KASSERT(new_cred != NULL, + ("priv_check*() called before mac_cred_check_setcred()")); + setcred_flags = data->setcred_flags; + + /* + * Rule out new versions of setcred() (in case, e.g., they introduce new + * flags), as well as MAC label changes as we do not (currently) support + * validating such changes. This may be improved by actually checking + * whether the requested label and the current one would differ. For + * now, we just check for SETCREDF_MAC_LABEL's presence. + */ + if (SETCREDF_TO_VERSION(setcred_flags) != 0 || + (setcred_flags & SETCREDF_MAC_LABEL) != 0) + return (EPERM); + + /* + * Browse rules, and for those that match the requestor, call specific + * privilege granting functions interpreting the "to"/"target" part. + */ error = EPERM; - rule = mac_do_rule_find(cred->cr_prison, &pr); - TAILQ_FOREACH(r, &rule->head, r_entries) { - if (r->from_type == RULE_UID) { - if (cred->cr_uid != r->f_uid) - continue; - if (r->to_type == RULE_ANY) { - error = 0; - break; - } - if (r->to_type == RULE_UID && uid == r->t_uid) { - error = 0; - break; - } - } - if (r->from_type == RULE_GID) { - if (!groupmember(r->f_gid, cred)) - continue; - if (r->to_type == RULE_ANY) { - error = 0; - break; - } - if (r->to_type == RULE_UID && uid == r->t_uid) { - error = 0; - break; - } - } - } - mtx_unlock(&pr->pr_mtx); + STAILQ_FOREACH(rule, &rules->head, r_entries) + if (rule_applies(rule, cred)) { + error = rule_grant_setcred(rule, cred, new_cred); + if (error != EPERM) + break; + } + return (error); } +static int +rule_check_set_supplementary_groups(const struct rule *const rule, + const struct ucred *const old_cred, const struct ucred *const new_cred) +{ + const flags_t gid_flags = rule->gid_flags; + + if (gid_flags & MDF_MAY_REJ_SUPP) { + const bool has_current_rej = (gid_flags & MDF_CURRENT) && + ((gid_flags & MDF_SUPP_MUST) || (gid_flags & MDF_SUPP_DONT)); + int o_idx, n_idx, r_idx; + + o_idx = r_idx = 1; + for (n_idx = 1; n_idx < new_cred->cr_ngroups; ++n_idx) { + const int gid = new_cred->cr_groups[n_idx]; + + if (has_current_rej) { + /* + * Linear search, as both supplementary groups + * arrays are sorted. + */ + for (; o_idx < old_cred->cr_ngroups; ++o_idx) { + const gid_t o_gid = + old_cred->cr_groups[o_idx]; + + if (o_gid < gid) { + if (gid_flags & MDF_SUPP_MUST) + return (EPERM); + } else if (o_gid == gid) { + if (gid_flags & MDF_SUPP_DONT) + return (EPERM); + } + else + break; + } + } + + /* + * Linear search, as both the supplementary groups + * array and the GIDs in the rule are sorted. + */ + for (; r_idx < rule->gids_nb; ++r_idx) { + const struct id_spec *const is = + &rule->gids[r_idx]; + + if (is->id < gid) { + if (is->flags & MDF_SUPP_MUST) + /* Not present but mandatory. */ + return (EPERM); + } else if (is->id == gid) { + if (is->flags & MDF_SUPP_DONT) + /* Present but forbidden. */ + return (EPERM); + } else + break; + } + } + + /* + * If we must have all current groups and we didn't browse all + * of them at this point (because the remaining ones have GIDs + * greater than the last requested group), we are simply missing + * them. + */ + if ((gid_flags & MDF_CURRENT) && (gid_flags & MDF_SUPP_MUST) && + o_idx < old_cred->cr_ngroups) + return (EPERM); + /* + * Similarly, we have to finish browsing all GIDs from the rule + * in case some are marked mandatory. + */ + for (; r_idx < rule->gids_nb; ++r_idx) { + const struct id_spec *const is = &rule->gids[r_idx]; + + if (is->flags & MDF_SUPP_MUST) + return (EPERM); + } + } + + return (0); +} + +static int +check_proc(void) +{ + char *path, *to_free; + int error; + + /* + * Only grant privileges if requested by the right executable. + * + * XXXOC: We may want to base this check on a tunable path and/or + * a specific MAC label. Going even further, e.g., envisioning to + * completely replace the path check with the latter, we would need to + * install FreeBSD on a FS with multilabel enabled by default, which in + * practice entails adding an option to ZFS to set MNT_MULTILABEL + * automatically on mounts, ensuring that root (and more if using + * different partitions) ZFS or UFS filesystems are created with + * multilabel turned on, and having the installation procedure support + * setting a MAC label per file (perhaps via additions to mtree(1)). So + * this probably isn't going to happen overnight, if ever. + */ + if (vn_fullpath(curproc->p_textvp, &path, &to_free) != 0) + return (EPERM); + error = strcmp(path, "/usr/bin/mdo") == 0 ? 0 : EPERM; + free(to_free, M_TEMP); + return (error); +} + +static void +mac_do_setcred_enter(void) +{ + struct rules *rules; + struct prison *pr; + struct mac_do_setcred_data * data; + int error; + + /* + * If not enabled, don't prepare data. Other hooks will check for that + * to know if they have to do something. + */ + if (do_enabled == 0) + return; + + /* + * MAC/do only applies to a process launched from a given executable. + * For other processes, we just won't intervene (we don't deny requests, + * nor do we grant privileges to them). + */ + error = check_proc(); + if (error != 0) + return; + + /* + * Find the currently applicable rules. + */ + rules = find_rules(curproc->p_ucred->cr_prison, &pr); + hold_rules(rules); + prison_unlock(pr); + + /* + * Setup thread data to be used by other hooks. + */ + data = fetch_data(); + if (!is_data_reusable(data, sizeof(*data))) + data = alloc_data(data, sizeof(*data)); + set_data_header(data, sizeof(*data), PRIV_CRED_SETCRED, rules); + /* Not really necessary, but helps to catch programming errors. */ + data->new_cred = NULL; + data->setcred_flags = 0; +} + +static int +mac_do_check_setcred(u_int flags, const struct ucred *const old_cred, + struct ucred *const new_cred) +{ + struct mac_do_setcred_data *const data = fetch_data(); + const struct rule *rule; + + /* + * Do we have to do something? + */ + if (check_data_usable(data, sizeof(*data), PRIV_CRED_SETCRED) != 0) + /* No. */ + return (0); + + /* + * Check for (supplementary) mandatory and forbidden groups. + */ + STAILQ_FOREACH(rule, &data->hdr.rules->head, r_entries) { + if (rule_applies(rule, old_cred)) { + int error = rule_check_set_supplementary_groups(rule, + old_cred, new_cred); + + if (error != 0) + return (error); + } + } + + /* + * Keep track of the setcred() flags and the new credentials for + * priv_check*(). + */ + data->new_cred = new_cred; + data->setcred_flags = flags; + + return (0); +} + +static void +mac_do_setcred_exit(void) +{ + struct mac_do_setcred_data *const data = fetch_data(); + + if (check_data_usable(data, sizeof(*data), PRIV_CRED_SETCRED) == 0) + /* + * This doesn't deallocate the small per-thread data storage, + * which can be reused on subsequent calls. (That data is of + * course deallocated as the current thread dies or this module + * is unloaded.) + */ + clear_data(data); +} + +static void +mac_do_init(struct mac_policy_conf *mpc) +{ + struct prison *pr; + + osd_jail_slot = osd_jail_register(dealloc_jail_osd, osd_methods); + set_empty_rules(&prison0); + sx_slock(&allprison_lock); + TAILQ_FOREACH(pr, &allprison, pr_list) + set_empty_rules(pr); + sx_sunlock(&allprison_lock); + + osd_thread_slot = osd_thread_register(dealloc_thread_osd); +} + +static void +mac_do_destroy(struct mac_policy_conf *mpc) +{ + /* + * osd_thread_deregister() must be called before osd_jail_deregister(), + * for the reason explained in dealloc_jail_osd(). + */ + osd_thread_deregister(osd_thread_slot); + osd_jail_deregister(osd_jail_slot); +} + static struct mac_policy_ops do_ops = { - .mpo_destroy = destroy, - .mpo_init = init, - .mpo_cred_check_setuid = check_setuid, - .mpo_cred_check_setgroups = check_setgroups, - .mpo_priv_grant = priv_grant, + .mpo_init = mac_do_init, + .mpo_destroy = mac_do_destroy, + .mpo_cred_setcred_enter = mac_do_setcred_enter, + .mpo_cred_check_setcred = mac_do_check_setcred, + .mpo_cred_setcred_exit = mac_do_setcred_exit, + .mpo_priv_grant = mac_do_priv_grant, }; -MAC_POLICY_SET(&do_ops, mac_do, "MAC/do", - MPC_LOADTIME_FLAG_UNLOADOK, NULL); +MAC_POLICY_SET(&do_ops, mac_do, "MAC/do", MPC_LOADTIME_FLAG_UNLOADOK, NULL); MODULE_VERSION(mac_do, 1); diff --git a/sys/security/mac_stub/mac_stub.c b/sys/security/mac_stub/mac_stub.c --- a/sys/security/mac_stub/mac_stub.c +++ b/sys/security/mac_stub/mac_stub.c @@ -222,6 +222,26 @@ return (0); } +static void +stub_cred_setcred_enter(void) +{ + +} + +static int +stub_cred_check_setcred(u_int flags, const struct ucred *old_cred, + struct ucred *new_cred) +{ + + return (0); +} + +static void +stub_cred_setcred_exit(void) +{ + +} + static int stub_cred_check_setegid(struct ucred *cred, gid_t egid) { @@ -1688,6 +1708,9 @@ .mpo_cred_check_setaudit = stub_cred_check_setaudit, .mpo_cred_check_setaudit_addr = stub_cred_check_setaudit_addr, .mpo_cred_check_setauid = stub_cred_check_setauid, + .mpo_cred_setcred_enter = stub_cred_setcred_enter, + .mpo_cred_check_setcred = stub_cred_check_setcred, + .mpo_cred_setcred_exit = stub_cred_setcred_exit, .mpo_cred_check_setegid = stub_cred_check_setegid, .mpo_cred_check_seteuid = stub_cred_check_seteuid, .mpo_cred_check_setgid = stub_cred_check_setgid, diff --git a/sys/security/mac_test/mac_test.c b/sys/security/mac_test/mac_test.c --- a/sys/security/mac_test/mac_test.c +++ b/sys/security/mac_test/mac_test.c @@ -257,6 +257,35 @@ return (0); } +COUNTER_DECL(cred_setcred_enter); +static void +test_cred_setcred_enter(void) +{ + + COUNTER_INC(cred_setcred_enter); +} + +COUNTER_DECL(cred_check_setcred); +static int +test_cred_check_setcred(u_int flags, const struct ucred *old_cred, + struct ucred *new_cred) +{ + + LABEL_CHECK(old_cred->cr_label, MAGIC_CRED); + LABEL_CHECK(new_cred->cr_label, MAGIC_CRED); + COUNTER_INC(cred_check_setcred); + + return (0); +} + +COUNTER_DECL(cred_setcred_exit); +static void +test_cred_setcred_exit(void) +{ + + COUNTER_INC(cred_setcred_exit); +} + COUNTER_DECL(cred_check_setegid); static int test_cred_check_setegid(struct ucred *cred, gid_t egid) @@ -3033,6 +3062,9 @@ .mpo_cred_check_setaudit = test_cred_check_setaudit, .mpo_cred_check_setaudit_addr = test_cred_check_setaudit_addr, .mpo_cred_check_setauid = test_cred_check_setauid, + .mpo_cred_setcred_enter = test_cred_setcred_enter, + .mpo_cred_check_setcred = test_cred_check_setcred, + .mpo_cred_setcred_exit = test_cred_setcred_exit, .mpo_cred_check_seteuid = test_cred_check_seteuid, .mpo_cred_check_setegid = test_cred_check_setegid, .mpo_cred_check_setgid = test_cred_check_setgid, diff --git a/sys/sys/priv.h b/sys/sys/priv.h --- a/sys/sys/priv.h +++ b/sys/sys/priv.h @@ -105,7 +105,8 @@ #define PRIV_CRED_SETRESGID 58 /* setresgid. */ #define PRIV_SEEOTHERGIDS 59 /* Exempt bsd.seeothergids. */ #define PRIV_SEEOTHERUIDS 60 /* Exempt bsd.seeotheruids. */ -#define PRIV_SEEJAILPROC 61 /* Exempt from bsd.see_jail_proc. */ +#define PRIV_SEEJAILPROC 61 /* Exempt from bsd.see_jail_proc. */ +#define PRIV_CRED_SETCRED 62 /* setcred. */ /* * Debugging privileges. diff --git a/sys/sys/syscall.h b/sys/sys/syscall.h --- a/sys/sys/syscall.h +++ b/sys/sys/syscall.h @@ -526,4 +526,5 @@ #define SYS_timerfd_settime 587 #define SYS_kcmp 588 #define SYS_getrlimitusage 589 -#define SYS_MAXSYSCALL 590 +#define SYS_setcred 590 +#define SYS_MAXSYSCALL 591 diff --git a/sys/sys/syscall.mk b/sys/sys/syscall.mk --- a/sys/sys/syscall.mk +++ b/sys/sys/syscall.mk @@ -432,4 +432,5 @@ timerfd_gettime.o \ timerfd_settime.o \ kcmp.o \ - getrlimitusage.o + getrlimitusage.o \ + setcred.o diff --git a/sys/sys/syscallsubr.h b/sys/sys/syscallsubr.h --- a/sys/sys/syscallsubr.h +++ b/sys/sys/syscallsubr.h @@ -320,6 +320,8 @@ fd_set *fd_ex, struct timeval *tvp, int abi_nfdbits); int kern_sendit(struct thread *td, int s, struct msghdr *mp, int flags, struct mbuf *control, enum uio_seg segflg); +int kern_setcred_v0(struct thread *const td, const u_int flags, + struct setcred_v0 *const wcred, gid_t *preallocated_groups); int kern_setgroups(struct thread *td, int *ngrpp, gid_t *groups); int kern_setitimer(struct thread *, u_int, struct itimerval *, struct itimerval *); diff --git a/sys/sys/sysproto.h b/sys/sys/sysproto.h --- a/sys/sys/sysproto.h +++ b/sys/sys/sysproto.h @@ -1882,6 +1882,11 @@ char flags_l_[PADL_(int)]; int flags; char flags_r_[PADR_(int)]; char res_l_[PADL_(rlim_t *)]; rlim_t * res; char res_r_[PADR_(rlim_t *)]; }; +struct setcred_args { + char flags_l_[PADL_(u_int)]; u_int flags; char flags_r_[PADR_(u_int)]; + char wcred_l_[PADL_(const void *)]; const void * wcred; char wcred_r_[PADR_(const void *)]; + char size_l_[PADL_(size_t)]; size_t size; char size_r_[PADR_(size_t)]; +}; int sys_exit(struct thread *, struct exit_args *); int sys_fork(struct thread *, struct fork_args *); int sys_read(struct thread *, struct read_args *); @@ -2282,6 +2287,7 @@ int sys_timerfd_settime(struct thread *, struct timerfd_settime_args *); int sys_kcmp(struct thread *, struct kcmp_args *); int sys_getrlimitusage(struct thread *, struct getrlimitusage_args *); +int sys_setcred(struct thread *, struct setcred_args *); #ifdef COMPAT_43 @@ -3262,6 +3268,7 @@ #define SYS_AUE_timerfd_settime AUE_TIMERFD #define SYS_AUE_kcmp AUE_NULL #define SYS_AUE_getrlimitusage AUE_NULL +#define SYS_AUE_setcred AUE_SETCRED #undef PAD_ #undef PADL_ diff --git a/sys/sys/ucred.h b/sys/sys/ucred.h --- a/sys/sys/ucred.h +++ b/sys/sys/ucred.h @@ -32,6 +32,7 @@ #ifndef _SYS_UCRED_H_ #define _SYS_UCRED_H_ +#include #if defined(_KERNEL) || defined(_WANT_UCRED) #include #include @@ -39,8 +40,6 @@ #include #if defined(_KERNEL) || defined(_WANT_UCRED) -struct loginclass; - /* * Flags for cr_flags. */ @@ -53,6 +52,11 @@ */ #define CRED_SMALLGROUPS_NB 16 +struct label; +struct loginclass; +struct prison; +struct uidinfo; + /* * Credentials. * @@ -119,7 +123,50 @@ /* This can be used for both ucred and xucred structures. */ #define cr_gid cr_groups[0] +struct mac; +/* + * Structure to pass as an argument to the setcred() system call. + */ +struct setcred_v0 { + uid_t sc_uid; /* effective user id */ + uid_t sc_ruid; /* real user id */ + uid_t sc_svuid; /* saved user id */ + gid_t sc_gid; /* effective group id */ + gid_t sc_rgid; /* real group id */ + gid_t sc_svgid; /* saved group id */ + int sc_supp_groups_nb; /* number of supplementary groups */ + gid_t *sc_supp_groups; /* supplementary groups */ + struct mac *sc_label; /* MAC label */ +}; + +/* + * Flags to setcred(). + * + * Descending order to leave room for more version bits (if ever needed). + */ +#define SETCREDF_UID (1u << 31) +#define SETCREDF_RUID (1u << 30) +#define SETCREDF_SVUID (1u << 29) +#define SETCREDF_GID (1u << 28) +#define SETCREDF_RGID (1u << 27) +#define SETCREDF_SVGID (1u << 26) +#define SETCREDF_SUPP_GROUPS (1u << 25) +#define SETCREDF_MAC_LABEL (1u << 24) + +#define SETCREDF_FROM_VERSION(version) (((u_int)version) & 0xFF) +#define SETCREDF_TO_VERSION(flags) ((flags) & 0xFF) + #ifdef _KERNEL +/* + * Masks of the currently valid flags to setcred() (v0). As new versions are + * added, they may or may not use the same flags. + */ +#define SETCREDF_VERSION_BITS (0xFF) +#define SETCREDF_SET_MASK (SETCREDF_UID | SETCREDF_RUID | SETCREDF_SVUID | \ + SETCREDF_GID | SETCREDF_RGID | SETCREDF_SVGID | SETCREDF_SUPP_GROUPS | \ + SETCREDF_MAC_LABEL) +#define SETCREDF_MASK (SETCREDF_SET_MASK | SETCREDF_VERSION_BITS) + struct proc; struct thread; @@ -183,6 +230,13 @@ bool group_is_supplementary(const gid_t gid, const struct ucred *const cred); bool groupmember(gid_t gid, const struct ucred *cred); bool realgroupmember(gid_t gid, const struct ucred *cred); + +#else /* !_KERNEL */ + +__BEGIN_DECLS +int setcred(u_int flags, const void *wcred, size_t size); +__END_DECLS + #endif /* _KERNEL */ #endif /* !_SYS_UCRED_H_ */ diff --git a/usr.bin/mdo/mdo.c b/usr.bin/mdo/mdo.c --- a/usr.bin/mdo/mdo.c +++ b/usr.bin/mdo/mdo.c @@ -5,6 +5,7 @@ */ #include +#include #include #include @@ -27,6 +28,8 @@ { struct passwd *pw; const char *username = "root"; + struct setcred_v0 wcred = {}; + u_int setcred_flags = SETCREDF_FROM_VERSION(0); bool uidonly = false; int ch; @@ -50,20 +53,45 @@ const char *errp = NULL; uid_t uid = strtonum(username, 0, UID_MAX, &errp); if (errp != NULL) - err(EXIT_FAILURE, "%s", errp); + err(EXIT_FAILURE, "invalid user ID '%s'", + username); pw = getpwuid(uid); } if (pw == NULL) err(EXIT_FAILURE, "invalid username '%s'", username); } + + wcred.sc_uid = wcred.sc_ruid = wcred.sc_svuid = pw->pw_uid; + setcred_flags |= SETCREDF_UID | SETCREDF_RUID | SETCREDF_SVUID; + if (!uidonly) { - if (initgroups(pw->pw_name, pw->pw_gid) == -1) - err(EXIT_FAILURE, "failed to call initgroups"); - if (setgid(pw->pw_gid) == -1) - err(EXIT_FAILURE, "failed to call setgid"); + /* + * If there are too many groups specified for some UID, setting + * the groups will fail. We preserve this condition by + * allocating one more group slot than allowed, as + * getgrouplist() itself is just some getter function and thus + * doesn't (and shouldn't) check the limit, and to allow + * setcred() to actually check for overflow. + */ + const long ngroups_alloc = sysconf(_SC_NGROUPS_MAX) + 2; + gid_t *const groups = malloc(sizeof(*groups) * ngroups_alloc); + int ngroups = ngroups_alloc; + + if (groups == NULL) + err(EXIT_FAILURE, "cannot allocate memory for groups"); + + getgrouplist(pw->pw_name, pw->pw_gid, groups, &ngroups); + + wcred.sc_gid = wcred.sc_rgid = wcred.sc_svgid = pw->pw_gid; + wcred.sc_supp_groups = groups + 1; + wcred.sc_supp_groups_nb = ngroups - 1; + setcred_flags |= SETCREDF_GID | SETCREDF_RGID | SETCREDF_SVGID | + SETCREDF_SUPP_GROUPS; } - if (setuid(pw->pw_uid) == -1) - err(EXIT_FAILURE, "failed to call setuid"); + + if (setcred(setcred_flags, &wcred, sizeof(wcred)) != 0) + err(EXIT_FAILURE, "calling setcred() failed"); + if (*argv == NULL) { const char *sh = getenv("SHELL"); if (sh == NULL)