diff options
Diffstat (limited to 'sys/kern/kern_unveil.c')
-rw-r--r-- | sys/kern/kern_unveil.c | 727 |
1 files changed, 727 insertions, 0 deletions
diff --git a/sys/kern/kern_unveil.c b/sys/kern/kern_unveil.c new file mode 100644 index 00000000000..9a1f293b6a6 --- /dev/null +++ b/sys/kern/kern_unveil.c @@ -0,0 +1,727 @@ +/* $OpenBSD: kern_unveil.c,v 1.1 2018/07/13 09:25:23 beck Exp $ */ + +/* + * Copyright (c) 2017-2018 Bob Beck <beck@openbsd.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/param.h> + +#include <sys/mount.h> +#include <sys/proc.h> +#include <sys/namei.h> +#include <sys/pool.h> +#include <sys/vnode.h> +#include <sys/ktrace.h> +#include <sys/types.h> +#include <sys/malloc.h> +#include <sys/tree.h> + +#include <sys/conf.h> +#include <sys/syscall.h> +#include <sys/syscallargs.h> +#include <sys/systm.h> + +#define PLEDGENAMES +#include <sys/pledge.h> + +/* #define DEBUG_UNVEIL */ + +#define UNVEIL_MAX_VNODES 128 +#define UNVEIL_MAX_NAMES 128 + +static inline int +unvname_compare(const struct unvname *n1, const struct unvname *n2) +{ + if (n1->un_namesize == n2->un_namesize) + return (memcmp(n1->un_name, n2->un_name, n1->un_namesize)); + else + return (n1->un_namesize - n2->un_namesize); +} + +struct unvname * +unvname_new(const char *name, size_t size, int flags) +{ + struct unvname *ret = malloc(sizeof(struct unvname), M_PROC, M_WAITOK); + ret->un_name = malloc(size, M_PROC, M_WAITOK); + memcpy(ret->un_name, name, size); + ret->un_namesize = size; + ret->un_flags = flags; + return ret; +} + +void +unveil_free_traversed_vnodes(struct nameidata *ndp) { + if (ndp->ni_tvpsize) { + size_t i; + for (i = 0; i < ndp->ni_tvpend; i++) + vrele(ndp->ni_tvp[i]); /* ref for being in list */ + free(ndp->ni_tvp, M_PROC, ndp->ni_tvpsize * sizeof(struct vnode *)); + ndp->ni_tvpsize = 0; + ndp->ni_tvpend = 0; + } +} + +void +unveil_save_traversed_vnode(struct nameidata *ndp, struct vnode *vp) { + if (ndp->ni_tvpsize == 0) { + ndp->ni_tvp = mallocarray(MAXPATHLEN, sizeof(struct vnode *), + M_PROC, M_WAITOK); + ndp->ni_tvpsize = MAXPATHLEN; + } + /* This should be limited by MAXPATHLEN on a single lookup */ + KASSERT(ndp->ni_tvpsize > ndp->ni_tvpend); + vref(vp); /* ref for being in the list */ + ndp->ni_tvp[ndp->ni_tvpend++] = vp; +} + +void +unvname_delete(struct unvname *name) +{ + free(name->un_name, M_PROC, name->un_namesize);; + free(name, M_PROC, sizeof(struct unvname)); +} + +RBT_PROTOTYPE(unvname_rbt, unvname, un_rbt, unvname_compare); +RBT_GENERATE(unvname_rbt, unvname, un_rbt, unvname_compare); + +int +unveil_delete_names(struct unveil *uv) +{ + struct unvname *unvn, *next; + int ret = 0; + + rw_enter_write(&uv->uv_lock); + RBT_FOREACH_SAFE(unvn, unvname_rbt, &uv->uv_names, next) { + RBT_REMOVE(unvname_rbt, &uv->uv_names, unvn); + unvname_delete(unvn); + ret++; + } + rw_exit_write(&uv->uv_lock); + return ret; +} + +void +unveil_add_name(struct unveil *uv, char *name, uint64_t flags) +{ + struct unvname *unvn; + + rw_enter_write(&uv->uv_lock); + unvn = unvname_new(name, strlen(name) + 1, flags); + RBT_INSERT(unvname_rbt, &uv->uv_names, unvn); + rw_exit_write(&uv->uv_lock); +#ifdef DEBUG_UNVEIL + printf("added name %s\n", name); +#endif +} + +struct unvname * +unveil_namelookup(struct unveil *uv, char *name) +{ + struct unvname n, *ret = NULL; + + rw_enter_read(&uv->uv_lock); + +#ifdef DEBUG_UNVEIL + printf("unveil_namelookup: looking up name %s (%p) in vnode %p\n", + name, name, uv->uv_vp); +#endif + + KASSERT(uv->uv_vp != NULL); + + n.un_name = name; + n.un_namesize = strlen(name) + 1; + + ret = RBT_FIND(unvname_rbt, &uv->uv_names, &n); + + rw_exit_read(&uv->uv_lock); + +#ifdef DEBUG_UNVEIL + if (ret == NULL) + printf("unveil_namelookup: no match for name %s in vnode %p\n", + name, uv->uv_vp); + else + printf("unveil_namelookup: matched name %s in vnode %p\n", + name, uv->uv_vp); +#endif + return ret; +} + +void +unveil_destroy(struct process *ps) +{ + size_t i; + + for (i = 0; ps->ps_uvpaths != NULL && i < ps->ps_uvvcount; i++) { + struct unveil *uv = ps->ps_uvpaths + i; + + struct vnode *vp = uv->uv_vp; + /* skip any vnodes zapped by unveil_removevnode */ + if (vp != NULL) { + vp->v_uvcount--; +#ifdef DEBUG_UNVEIL + printf("unveil: %s(%d): removing vnode %p uvcount %d " + "in position %ld\n", + ps->ps_comm, ps->ps_pid, vp, vp->v_uvcount, i); +#endif + vrele(vp); + } + ps->ps_uvncount -= unveil_delete_names(uv); + uv->uv_vp = NULL; + uv->uv_flags = 0; + } + + KASSERT(ps->ps_uvncount == 0); + free(ps->ps_uvpaths, M_PROC, UNVEIL_MAX_VNODES * + sizeof(struct unveil)); + ps->ps_uvvcount = 0; + ps->ps_uvpaths = NULL; +} + +struct unveil * +unveil_copy(struct process *ps, size_t *count) +{ + struct unveil *ret; + size_t i; + + ret = mallocarray(UNVEIL_MAX_VNODES, sizeof(struct unveil), + M_PROC, M_WAITOK|M_ZERO); + + *count = 0; + for (i = 0; ps->ps_uvpaths != NULL && i < ps->ps_uvvcount; i++) { + struct unveil *uv = ps->ps_uvpaths + i; + struct unvname *unvn, *next; + + ret[i].uv_vp = uv->uv_vp; + if (ret[i].uv_vp != NULL) { + vref(ret[i].uv_vp); + ret[i].uv_vp->v_uvcount++; + } + rw_init(&ret[i].uv_lock, "unveil"); + RBT_INIT(unvname_rbt, &ret[i].uv_names); + rw_enter_read(&uv->uv_lock); + RBT_FOREACH_SAFE(unvn, unvname_rbt, &uv->uv_names, next) { + unveil_add_name(&ret[i], unvn->un_name, unvn->un_flags); + (*count)++; + } + printf("count now %ld\n", *count); + rw_exit_read(&uv->uv_lock); + ret[i].uv_flags = uv->uv_flags; + } + return(ret); +} + + +struct unveil * +unveil_lookup(struct vnode *vp, struct proc *p) +{ + struct process *pr = p->p_p; + struct unveil *uv = pr->ps_uvpaths; + ssize_t l, r; + + if (vp->v_uvcount == 0) + return NULL; + + /* + * shrink if told to do so to remove dead vnodes. + */ + if (pr->ps_uvshrink) { + size_t i = 0, j; + while (i < pr->ps_uvvcount) { + if (uv[i].uv_vp == NULL) { + pr->ps_uvncount -= unveil_delete_names(&uv[i]); + for (j = i + 1; j < pr->ps_uvvcount; j++) + uv[j - 1] = uv[j]; + pr->ps_uvvcount--; + } + i++; + } + pr->ps_uvshrink = 0; + } + + if (pr->ps_uvvcount == 0) + return NULL; + + /* clear the cwd unveil when we .. past it */ + if (pr->ps_uvpcwd && (vp == pr->ps_uvpcwd->uv_vp)) { +#ifdef DEBUG_UNVEIL + printf("unveil: %s(%d): nuking cwd traversing vnode %p\n", + p->p_p->ps_comm, p->p_p->ps_pid, vp); +#endif + p->p_p->ps_uvpcwd = NULL; + p->p_p->ps_uvpcwdgone = 0; + } +#ifdef DEBUG_UNVEIL + else { + if (pr->ps_uvpcwd) { + printf("unveil: %s(%d): did not nuke cwd because %p != %p\n", + p->p_p->ps_comm, p->p_p->ps_pid, vp, pr->ps_uvpcwd->uv_vp); + } + else + printf("unveil: %s(%d): cwd is null\n", + p->p_p->ps_comm, p->p_p->ps_pid); + + } +#endif + + l = 0; + r = pr->ps_uvvcount - 1; + while (l <= r) { + size_t m = l + (r - l)/2; +#ifdef DEBUG_UNVEIL + printf("unveil: checking vnode %p vs. unveil vnode %p\n", + vp, uv[m].uv_vp); +#endif + if (vp == uv[m].uv_vp) { + KASSERT(uv[m].uv_vp->v_uvcount > 0); + KASSERT(uv[m].uv_vp->v_usecount > 0); + return &uv[m]; + } + if (vp > uv[m].uv_vp) + l = m + 1; + else + r = m - 1; + } + return NULL; +} + +int +unveil_parseflags(const char *cflags, uint64_t *flags) +{ + size_t i = 0; + char c; + + *flags = 0; + while ((c = cflags[i++]) != '\0') { + switch (c) { + case 'r': + *flags |= PLEDGE_RPATH; + break; + case 'w': + *flags |= PLEDGE_WPATH; + break; + case 'x': + *flags |= PLEDGE_EXEC; + break; + case 'c': + *flags |= PLEDGE_CPATH; + break; + default: + return -1; + } + } + return 0; +} + +int +unveil_setflags(uint64_t *flags, uint64_t nflags) +{ +#if 0 + if (((~(*flags)) & nflags) != 0) { +#ifdef DEBUG_UNVEIL + printf("Flags escalation %llX -> %llX\n", *flags, nflags); +#endif + return 1; + } +#endif + *flags = nflags; + return 1; +} + +struct unveil * +unveil_add_vnode(struct process *pr, struct vnode *vp) +{ + struct unveil *uv = NULL; + ssize_t i; + for (i = pr->ps_uvvcount; + i > 0 && pr->ps_uvpaths[i - 1].uv_vp > vp; + i--) + pr->ps_uvpaths[i] = pr->ps_uvpaths[i - 1]; + + uv = &pr->ps_uvpaths[i]; + rw_init(&uv->uv_lock, "unveil"); + RBT_INIT(unvname_rbt, &uv->uv_names); + uv->uv_vp = vp; + uv->uv_flags = 0; + pr->ps_uvvcount++; + return (uv); +} + +void +unveil_add_traversed_vnodes(struct proc *p, struct nameidata *ndp) +{ + /* + * add the traversed vnodes with 0 flags if they + * are not already present. + */ + if (ndp->ni_tvpsize) { + size_t i; + for (i = 0; i < ndp->ni_tvpend; i++) { + struct vnode *vp = ndp->ni_tvp[i]; + if (unveil_lookup(vp, p) == NULL) { + vref(vp); + vp->v_uvcount++; + unveil_add_vnode(p->p_p, vp); + } + } + } +} + +int +unveil_add(struct proc *p, struct nameidata *ndp, const char *cflags) +{ + struct process *pr = p->p_p; + struct vnode *vp; + struct unveil *uv; + int directory_add; + int ret = EINVAL; + u_int64_t flags; + + KASSERT(ISSET(ndp->ni_cnd.cn_flags, HASBUF)); /* must have SAVENAME */ + + if (unveil_parseflags(cflags, &flags) == -1) + goto done; + + if (pr->ps_uvpaths == NULL) { + pr->ps_uvpaths = mallocarray(UNVEIL_MAX_VNODES, + sizeof(struct unveil), M_PROC, M_WAITOK|M_ZERO); + } + + if (pr->ps_uvvcount >= UNVEIL_MAX_VNODES || + pr->ps_uvncount >= UNVEIL_MAX_NAMES) { + ret = E2BIG; + goto done; + } + + /* Are we a directory? or something else */ + directory_add = ndp->ni_vp != NULL && ndp->ni_vp->v_type == VDIR; + + if (directory_add) + vp=ndp->ni_vp; + else + vp=ndp->ni_dvp; + + KASSERT(vp->v_type == VDIR); + vref(vp); + vp->v_uvcount++; + if ((uv = unveil_lookup(vp, p)) != NULL) { + /* + * We already have unveiled this directory + * vnode + */ + vp->v_uvcount--; + vrele(vp); + + /* + * If we are adding a directory which was already + * unveiled containing only specific terminals, + * unrestrict it. + */ + if (directory_add) { +#ifdef DEBUG_UNVEIL + printf("unveil: %s(%d): updating directory vnode %p" + " to unrestricted uvcount %d\n", + pr->ps_comm, pr->ps_pid, vp, vp->v_uvcount); +#endif + if (!unveil_setflags(&uv->uv_flags, flags)) + ret = EPERM; + else + ret = 0; + goto done; + } + + /* + * If we are adding a terminal that is already unveiled, just + * replace the flags and we are done + */ + if (!directory_add) { + struct unvname *tname; + if ((tname = unveil_namelookup(uv, + ndp->ni_cnd.cn_nameptr)) != NULL) { +#ifdef DEBUG_UNVEIL + printf("unveil: %s(%d): changing flags for %s" + "in vnode %p, uvcount %d\n", + pr->ps_comm, pr->ps_pid, tname->un_name, vp, + vp->v_uvcount); +#endif + if (!unveil_setflags(&tname->un_flags, flags)) + ret = EPERM; + else + ret = 0; + goto done; + } + } + + } else { + /* + * New unveil involving this directory vnode. + */ + uv = unveil_add_vnode(pr, vp); + } + + /* + * At this stage with have a unveil in uv with a vnode for a + * directory. If the component we are adding is a directory, + * we are done. Otherwise, we add the component name the name + * list in uv. + */ + + if (directory_add) { + uv->uv_flags = flags; + ret = 0; +#ifdef DEBUG_UNVEIL + printf("unveil: %s(%d): added unrestricted directory vnode %p" + ", uvcount %d\n", + pr->ps_comm, pr->ps_pid, vp, vp->v_uvcount); +#endif + goto done; + } + + unveil_add_name(uv, ndp->ni_cnd.cn_nameptr, flags); + pr->ps_uvncount++; + ret = 0; + +#ifdef DEBUG_UNVEIL + printf("unveil: %s(%d): added name %s beneath %s vnode %p," + " uvcount %d\n", + pr->ps_comm, pr->ps_pid, ndp->ni_cnd.cn_nameptr, + uv->uv_flags ? "unrestricted" : "restricted", + vp, vp->v_uvcount); +#endif + + done: + if (ret == 0) + unveil_add_traversed_vnodes(p, ndp); + unveil_free_traversed_vnodes(ndp); + pool_put(&namei_pool, ndp->ni_cnd.cn_pnbuf); + return ret; +} + +/* + * XXX this will probably change. + * XXX collapse down later once debug surely unneded + */ +int +unveil_flagmatch(struct nameidata *ni, uint64_t flags) +{ + if (flags == 0) { + if (ni->ni_pledge & PLEDGE_STAT) { +#ifdef DEBUG_UNVEIL + printf("allowing stat/accesss for 0 flags"); +#endif + SET(ni->ni_pledge, PLEDGE_STATLIE); + return 1; + } +#ifdef DEBUG_UNVEIL + printf("All operations forbidden for 0 flags\n"); +#endif + return 0; + } + if (ni->ni_pledge & PLEDGE_STAT) { +#ifdef DEBUG_UNVEIL + printf("Allowing stat for nonzero flags\n"); +#endif + CLR(ni->ni_pledge, PLEDGE_STATLIE); + return 1; + } + if (ni->ni_pledge & PLEDGE_RPATH) { + if ((flags & PLEDGE_RPATH) == 0) { +#ifdef DEBUG_UNVEIL + printf("Pledge wants read but disallowed\n"); +#endif + return 0; + } + } + if (ni->ni_pledge & PLEDGE_WPATH) { + if ((flags & PLEDGE_WPATH) == 0) { +#ifdef DEBUG_UNVEIL + printf("Pledge wants write but disallowed\n"); +#endif + return 0; + } + } + if (ni->ni_pledge & PLEDGE_EXEC) { + if ((flags & PLEDGE_EXEC) == 0) { +#ifdef DEBUG_UNVEIL + printf("Pledge wants exec but disallowed\n"); +#endif + return 0; + } + } + if (ni->ni_pledge & PLEDGE_CPATH) { + if ((flags & PLEDGE_CPATH) == 0) { +#ifdef DEBUG_UNVEIL + printf("Pledge wants cpath but disallowed\n"); +#endif + return 0; + } + } + return 1; +} + +/* + * unveil checking - for component directories in a namei lookup. + */ +void +unveil_check_component(struct proc *p, struct nameidata *ni, struct vnode *dp ) +{ + struct unveil *uv = NULL; + + if (ni->ni_pledge != PLEDGE_UNVEIL) { + if ((ni->ni_cnd.cn_flags & BYPASSUNVEIL) == 0 && + (uv = unveil_lookup(dp, p)) != NULL) { + /* if directory flags match, it's a match */ + if (unveil_flagmatch(ni, uv->uv_flags)) { + if (uv->uv_flags) { + ni->ni_unveil_match = uv; +#ifdef DEBUG_UNVEIL + printf("unveil: %s(%d): component directory match" + " for vnode %p\n", + p->p_p->ps_comm, p->p_p->ps_pid, dp); + +#endif + } + } + } + } + else + unveil_save_traversed_vnode(ni, dp); +} + +/* + * unveil checking - only done after namei lookup has succeeded on + * the last compoent of a namei lookup. + */ +int +unveil_check_final(struct proc *p, struct nameidata *ni) +{ + struct unveil *uv; + struct unvname *tname = NULL; + + if (ni->ni_pledge == PLEDGE_UNVEIL || + p->p_p->ps_uvpaths == NULL) + return (0); + + if (ni->ni_cnd.cn_flags & BYPASSUNVEIL) { +#ifdef DEBUG_UNVEIL + printf("unveil: %s(%d): BYPASSUNVEIL.\n", + p->p_p->ps_comm, p->p_p->ps_pid); +#endif + CLR(ni->ni_pledge, PLEDGE_STATLIE); + return (0); + } + if (ni->ni_vp != NULL && ni->ni_vp->v_type == VDIR) { + uv = unveil_lookup(ni->ni_vp, p); + if (uv == NULL) { +#ifdef DEBUG_UNVEIL + printf("unveil: %s(%d) no match for vnode %p\n", + p->p_p->ps_comm, p->p_p->ps_pid, ni->ni_vp); +#endif + goto done; + } + if (!unveil_flagmatch(ni, uv->uv_flags)) { +#ifdef DEBUG_UNVEIL + printf("unveil: %s(%d) flag mismatch for directory" + " vnode %p\n", + p->p_p->ps_comm, p->p_p->ps_pid, ni->ni_vp); +#endif + return EACCES; + } + } else { + uv = unveil_lookup(ni->ni_dvp, p); + if (uv == NULL) { +#ifdef DEBUG_UNVEIL + printf("unveil: %s(%d) no match for directory" + " vnode %p\n", + p->p_p->ps_comm, p->p_p->ps_pid, ni->ni_dvp); +#endif + goto done; + } + if ((tname = unveil_namelookup(uv, ni->ni_cnd.cn_nameptr)) + == NULL) { +#ifdef DEBUG_UNVEIL + printf("unveil: %s(%d) no match for terminal '%s' in " + "directory vnode %p\n", + p->p_p->ps_comm, p->p_p->ps_pid, + ni->ni_cnd.cn_nameptr, ni->ni_dvp); +#endif + uv = NULL; + goto done; + } + if (!unveil_flagmatch(ni, tname->un_flags)) { +#ifdef DEBUG_UNVEIL + printf("unveil: %s(%d) flag mismatch for terminal '%s'\n", + p->p_p->ps_comm, p->p_p->ps_pid, tname->un_name); +#endif + return EACCES; + } + } + ni->ni_unveil_match = uv; +done: + if (ni->ni_unveil_match) { +#ifdef DEBUG_UNVEIL + printf("unveil: %s(%d): matched \"%s\" underneath/at vnode %p\n", + p->p_p->ps_comm, p->p_p->ps_pid, ni->ni_cnd.cn_nameptr, + ni->ni_unveil_match->uv_vp); +#endif + return (0); + } + else if (p->p_p->ps_uvpcwd) { + ni->ni_unveil_match = p->p_p->ps_uvpcwd; +#ifdef DEBUG_UNVEIL + printf("unveil: %s(%d): used cwd unveil vnode from vnode %p\n", + p->p_p->ps_comm, p->p_p->ps_pid, ni->ni_unveil_match->uv_vp); +#endif + return (0); + } else if (p->p_p->ps_uvpcwdgone) { + printf("Corner cases make Bob cry in a corner\n"); + } + return ENOENT; +} + +/* + * Scan all active processes to see if any of them have a unveil + * to this vnode. If so, NULL the vnode in their unveil list, + * vrele, drop the reference, and mark their unveil list + * as needing to have the hole shrunk the next time the process + * uses it for lookup. + */ +void +unveil_removevnode(struct vnode *vp) +{ + struct process *pr; + int count = 0; + + if (vp->v_uvcount == 0) + return; +#ifdef DEBUG_UNVEIL + printf("unveil_removevnode found vnode %p with count %d", vp, vp->v_uvcount); +#endif + LIST_FOREACH(pr, &allprocess, ps_list) { + struct unveil * uv; + if ((uv = unveil_lookup(vp, pr->ps_mainproc)) != NULL) { + uv->uv_vp = NULL; + uv->uv_flags = 0; +#ifdef DEBUG_UNVEIL + printf("unveil_removevnode vnode %p now count %d", vp, vp->v_uvcount); +#endif + pr->ps_uvshrink = 1; + count++; + } + } + KASSERT(vp->v_uvcount == count); + + while (vp->v_uvcount--) + vrele(vp); +} |