I wrote some patches to allow pledging across execs. Currently, the exec pledge passes down the process tree.
The initial version simply inherited the current pledge when execing with the `pledge("rexec")` promise, but after discussing with Theo at EuroBSD, a better design was suggested. Because directory pledges are going to be their own system call, we can repurpose the second argument of pledge as the "exec pledge". My use is for sandboxing a bad idea, where arbitrary users can submit code to a sandbox for my pet language, sitting on my website, which gets compiled and run. In the base system, it seems like many programs with pledge("exec") invoke arbitrary binaries on behalf of the user, but there are others (eg, pax) that expect specfic behavior from what they exec. This also could have broader uses -- for example, pledging entire shell scripts. It also helps mitigate the case where an attacker might fool us into exec()ing the wrong binary. This is split into four patches: - Adding kernel+libc support and docs. This changes the signature of pledge, but because everything calls plege("promises", NULL), things keep working. - Add a pledge(1) binary + docs. This allows enforcing arbitrary pledges for a process tree. The minimal pledge for dynamically linked executables is a bit broad ("stdio rpath prot_exec"), due to ld.so, but this is still usefully restrictive. For statically linked executables, we do even better. 'pledge stdio echo hi' works just fine. - The third patch adds the ability to pledge programs running under slowcgi. - The fourth patch exec-pledges pax. I'm not sure I got the pledges right, so more careful review would be appreciated. I grabbed the pledges from compress, which seem to be a superset of the bzip2 pledges. Regress tests would be good to add. I haven't done the work yet, but it's mostly a matter of figuring out how to arrange the makefiles to produce a binary that can be exec'ed. The examples I see all seem to just produce one regress binary. In an appropriate turn of events, this patch was written while sitting in a capsicum talk at EuroBSD. ============ diff --git include/unistd.h include/unistd.h index ffec1538f44..1faf2543f3d 100644 --- include/unistd.h +++ include/unistd.h @@ -522,7 +522,7 @@ int strtofflags(char **, u_int32_t *, u_int32_t *); int swapctl(int cmd, const void *arg, int misc); int syscall(int, ...); int getentropy(void *, size_t); -int pledge(const char *, const char **); +int pledge(const char *, const char *); pid_t __tfork_thread(const struct __tfork *, size_t, void (*)(void *), void *); #endif /* __BSD_VISIBLE */ diff --git lib/libc/sys/pledge.2 lib/libc/sys/pledge.2 index 89884352500..9bfedc3d14b 100644 --- lib/libc/sys/pledge.2 +++ lib/libc/sys/pledge.2 @@ -23,7 +23,7 @@ .Sh SYNOPSIS .In unistd.h .Ft int -.Fn pledge "const char *promises" "const char *paths[]" +.Fn pledge "const char *promises" "const char *execpromises" .Sh DESCRIPTION The current process is forced into a restricted-service operating mode. A few subsets are available, roughly described as computation, memory @@ -31,9 +31,16 @@ management, read-write operations on file descriptors, opening of files, networking. In general, these modes were selected by studying the operation of many programs using libc and other such interfaces, and setting -.Ar promises -or -.Ar paths . +.Ar promises. +.Pp +Specifying +.Ar execpromises +allow the programmer to set the default pledge set for all future +processes process that are execed. This is allows for inheriting +.Fn fork +and +.Fn exec +calls. .Pp Use of .Fn pledge @@ -70,7 +77,7 @@ Passing to .Fa promises or -.Fa paths +.Fa execpromises specifies to not change the current value. .Pp Some system calls, when allowed, have restrictions applied to them: @@ -143,9 +150,7 @@ support: system sensor readings. .Pp .It Fn pledge -Can only reduce permissions; can only set a list of -.Pa paths -once. +Can only reduce permissions. .El .Pp The @@ -467,8 +472,8 @@ Allows a process to call Coupled with the .Va proc promise, this allows a process to fork and execute another program. -The new program starts running without pledge active and hopefully -makes a new +The new program starts running with the requested exec pledges active +and hopefully makes a new .Fn pledge . .It Va prot_exec Allows the use of @@ -556,10 +561,6 @@ Allow operation for statistics collection from a bpf device. .El .Pp -A whitelist of permitted paths may be provided in -.Ar paths . -All other paths will return -.Er ENOENT . At least one promise is required to be pledged in order to activate a whitelist. .Sh RETURN VALUES .Rv -std diff --git regress/sys/kern/pledge/generic/manager.c regress/sys/kern/pledge/generic/manager.c index 451a3ecc088..e6af6b3d69e 100644 --- regress/sys/kern/pledge/generic/manager.c +++ regress/sys/kern/pledge/generic/manager.c @@ -175,7 +175,7 @@ drainfd(int rfd, int wfd) void _start_test(int *ret, const char *test_name, const char *request, - const char *paths[], void (*test_func)(void)) + const char *execrequest, const char *paths[], void (*test_func)(void)) { int fildes[2]; pid_t pid; @@ -235,7 +235,7 @@ _start_test(int *ret, const char *test_name, const char *request, setsid(); /* set pledge policy */ - if (request && pledge(request, paths) != 0) + if (request && pledge(request, execrequest) != 0) err(errno, "pledge"); /* reset errno and launch test */ diff --git regress/sys/kern/pledge/generic/manager.h regress/sys/kern/pledge/generic/manager.h index 13c52eea75a..072406ed2e4 100644 --- regress/sys/kern/pledge/generic/manager.h +++ regress/sys/kern/pledge/generic/manager.h @@ -18,10 +18,10 @@ #define _MANAGER_H_ void _start_test(int *ret, const char *test_name, const char *request, - const char *paths[], void (*test_func)(void)); + const char *execrequest, const char *paths[], void (*test_func)(void)); -#define start_test(ret,req,paths,func) \ - _start_test(ret,#func,req,paths,func) +#define start_test(ret,req, paths,func) \ + _start_test(ret,#func,req,NULL,paths,func) #define start_test1(ret,req,path,func) \ do { \ diff --git sys/kern/init_sysent.c sys/kern/init_sysent.c index 2b46cda280b..1cbeb93edf8 100644 --- sys/kern/init_sysent.c +++ sys/kern/init_sysent.c @@ -1,10 +1,10 @@ -/* $OpenBSD: init_sysent.c,v 1.189 2017/08/12 00:04:06 tedu Exp $ */ +/* $OpenBSD$ */ /* * System call switch table. * * DO NOT EDIT-- this file is automatically generated. - * created from; OpenBSD: syscalls.master,v 1.177 2017/08/12 00:03:10 tedu Exp + * created from; OpenBSD: syscalls.master,v 1.178 2017/09/25 23:00:33 espie Exp */ #include <sys/param.h> diff --git sys/kern/kern_exec.c sys/kern/kern_exec.c index 3c65a3a9a44..056c346e22e 100644 --- sys/kern/kern_exec.c +++ sys/kern/kern_exec.c @@ -520,7 +520,20 @@ sys_execve(struct proc *p, void *v, register_t *retval) else atomic_clearbits_int(&pr->ps_flags, PS_SUGIDEXEC); - atomic_clearbits_int(&pr->ps_flags, PS_PLEDGE); + /* + * If the process has a child pledge, this pledge + * is copied over. It is not cleared, so the pledge + * applies to all descendants, forever. + * + * Because it's possible to call pledge(NULL, "child"), + * which leaves pledge unset in the parent, the pledge bit + * needs to be set explicitly here + */ + if (pr->ps_flags & PS_EPLEDGE) { + pr->ps_pledge = pr->ps_execpledge; + atomic_setbits_int(&pr->ps_flags, PS_PLEDGE); + } else + atomic_clearbits_int(&pr->ps_flags, PS_PLEDGE); /* * deal with set[ug]id. diff --git sys/kern/kern_pledge.c sys/kern/kern_pledge.c index 7cf81613b47..2ffe2dfb495 100644 --- sys/kern/kern_pledge.c +++ sys/kern/kern_pledge.c @@ -84,6 +84,7 @@ #endif uint64_t pledgereq_flags(const char *req); +int pledgereq_parse(struct proc *p, const char *user_flags, uint64_t *flagp); int canonpath(const char *input, char *buf, size_t bufsize); /* #define DEBUG_PLEDGE */ @@ -398,44 +399,16 @@ sys_pledge(struct proc *p, void *v, register_t *retval) { struct sys_pledge_args /* { syscallarg(const char *)request; - syscallarg(const char **)paths; + syscallarg(const char *)execrequest; } */ *uap = v; struct process *pr = p->p_p; - uint64_t flags = 0; + uint64_t flags = 0, execflags = 0; int error; if (SCARG(uap, request)) { - size_t rbuflen; - char *rbuf, *rp, *pn; - uint64_t f; - - rbuf = malloc(MAXPATHLEN, M_TEMP, M_WAITOK); - error = copyinstr(SCARG(uap, request), rbuf, MAXPATHLEN, - &rbuflen); - if (error) { - free(rbuf, M_TEMP, MAXPATHLEN); + error = pledgereq_parse(p, SCARG(uap, request), &flags); + if (error) return (error); - } -#ifdef KTRACE - if (KTRPOINT(p, KTR_STRUCT)) - ktrstruct(p, "pledgereq", rbuf, rbuflen-1); -#endif - - for (rp = rbuf; rp && *rp && error == 0; rp = pn) { - pn = strchr(rp, ' '); /* find terminator */ - if (pn) { - while (*pn == ' ') - *pn++ = '\0'; - } - - if ((f = pledgereq_flags(rp)) == 0) { - free(rbuf, M_TEMP, MAXPATHLEN); - return (EINVAL); - } - flags |= f; - } - free(rbuf, M_TEMP, MAXPATHLEN); - /* * if we are already pledged, allow only promises reductions. * flags doesn't contain flags outside _USERSET: they will be @@ -446,13 +419,29 @@ sys_pledge(struct proc *p, void *v, register_t *retval) return (EPERM); } - if (SCARG(uap, paths)) - return (EINVAL); + if (SCARG(uap, execrequest)) { + error = pledgereq_parse(p, SCARG(uap, execrequest), &execflags); + if (error) + return (error); + /* Same as above; Only allow reductions in promises. */ + if (ISSET(pr->ps_flags, PS_PLEDGE) && + (((flags | pr->ps_execpledge) != pr->ps_execpledge))) + return (EPERM); + } if (SCARG(uap, request)) { pr->ps_pledge = flags; pr->ps_flags |= PS_PLEDGE; } + /* + * if we call pledge(NULL, "childreq"), we do not want to + * set the pledge flag on the parent, so this needs to be + * set separately. + */ + if (SCARG(uap, execrequest)) { + pr->ps_execpledge = execflags; + pr->ps_flags |= PS_EPLEDGE; + } return (0); } @@ -1350,6 +1339,43 @@ pledge_swapctl(struct proc *p) return (EPERM); } +int +pledgereq_parse(struct proc *p, const char *user_req, uint64_t *flagp) +{ + size_t rbuflen; + char *rbuf, *rp, *pn; + uint64_t flags = 0; + uint64_t f; + int error; + + rbuf = malloc(MAXPATHLEN, M_TEMP, M_WAITOK); + error = copyinstr(user_req, rbuf, MAXPATHLEN, &rbuflen); + if (error) { + free(rbuf, M_TEMP, MAXPATHLEN); + return (error); + } +#ifdef KTRACE + if (KTRPOINT(p, KTR_STRUCT)) + ktrstruct(p, "pledgereq", rbuf, rbuflen-1); +#endif + + for (rp = rbuf; rp && *rp && error == 0; rp = pn) { + pn = strchr(rp, ' '); /* find terminator */ + if (pn) { + while (*pn == ' ') + *pn++ = '\0'; + } + + if ((f = pledgereq_flags(rp)) == 0) { + free(rbuf, M_TEMP, MAXPATHLEN); + return (EINVAL); + } + flags |= f; + } + free(rbuf, M_TEMP, MAXPATHLEN); + *flagp = flags; + return (0); +} /* bsearch over pledgereq. return flags value if found, 0 else */ uint64_t pledgereq_flags(const char *req_name) diff --git sys/kern/syscalls.c sys/kern/syscalls.c index 0bd41a89b71..4786db3404e 100644 --- sys/kern/syscalls.c +++ sys/kern/syscalls.c @@ -1,10 +1,10 @@ -/* $OpenBSD: syscalls.c,v 1.188 2017/08/12 00:04:06 tedu Exp $ */ +/* $OpenBSD$ */ /* * System call names. * * DO NOT EDIT-- this file is automatically generated. - * created from; OpenBSD: syscalls.master,v 1.177 2017/08/12 00:03:10 tedu Exp + * created from; OpenBSD: syscalls.master,v 1.178 2017/09/25 23:00:33 espie Exp */ char *syscallnames[] = { diff --git sys/kern/syscalls.master sys/kern/syscalls.master index 9708ae86afb..44436aba279 100644 --- sys/kern/syscalls.master +++ sys/kern/syscalls.master @@ -227,7 +227,8 @@ 106 STD { int sys_listen(int s, int backlog); } 107 STD { int sys_chflagsat(int fd, const char *path, \ u_int flags, int atflags); } -108 STD { int sys_pledge(const char *request, const char **paths); } +108 STD { int sys_pledge(const char *request, \ + const char *execrequest); } 109 STD { int sys_ppoll(struct pollfd *fds, \ u_int nfds, const struct timespec *ts, \ const sigset_t *mask); } diff --git sys/sys/proc.h sys/sys/proc.h index d8861f1d896..761479f9fbc 100644 --- sys/sys/proc.h +++ sys/sys/proc.h @@ -220,6 +220,7 @@ struct process { u_short ps_acflag; /* Accounting flags. */ uint64_t ps_pledge; + uint64_t ps_execpledge; int64_t ps_kbind_cookie; u_long ps_kbind_addr; @@ -262,6 +263,7 @@ struct process { #define PS_NOBROADCASTKILL 0x00080000 /* Process excluded from kill -1. */ #define PS_PLEDGE 0x00100000 /* Has called pledge(2) */ #define PS_WXNEEDED 0x00200000 /* Process may violate W^X */ +#define PS_EPLEDGE 0x00400000 /* Has called pledge(2) */ #define PS_BITS \ ("\20" "\01CONTROLT" "\02EXEC" "\03INEXEC" "\04EXITING" "\05SUGID" \ diff --git sys/sys/syscall.h sys/sys/syscall.h index efdb1c2d6f7..daacd2f9fc7 100644 --- sys/sys/syscall.h +++ sys/sys/syscall.h @@ -1,10 +1,10 @@ -/* $OpenBSD: syscall.h,v 1.188 2017/09/25 23:00:33 espie Exp $ */ +/* $OpenBSD$ */ /* * System call numbers. * * DO NOT EDIT-- this file is automatically generated. - * created from; OpenBSD: syscalls.master,v 1.177 2017/08/12 00:03:10 tedu Exp + * created from; OpenBSD: syscalls.master,v 1.178 2017/09/25 23:00:33 espie Exp */ /* syscall: "syscall" ret: "int" args: "int" "..." */ @@ -327,7 +327,7 @@ /* syscall: "chflagsat" ret: "int" args: "int" "const char *" "u_int" "int" */ #define SYS_chflagsat 107 -/* syscall: "pledge" ret: "int" args: "const char *" "const char **" */ +/* syscall: "pledge" ret: "int" args: "const char *" "const char *" */ #define SYS_pledge 108 /* syscall: "ppoll" ret: "int" args: "struct pollfd *" "u_int" "const struct timespec *" "const sigset_t *" */ diff --git sys/sys/syscallargs.h sys/sys/syscallargs.h index 2af8ebccc9b..91ffe411cf2 100644 --- sys/sys/syscallargs.h +++ sys/sys/syscallargs.h @@ -1,10 +1,10 @@ -/* $OpenBSD: syscallargs.h,v 1.191 2017/09/25 23:00:33 espie Exp $ */ +/* $OpenBSD$ */ /* * System call argument lists. * * DO NOT EDIT-- this file is automatically generated. - * created from; OpenBSD: syscalls.master,v 1.177 2017/08/12 00:03:10 tedu Exp + * created from; OpenBSD: syscalls.master,v 1.178 2017/09/25 23:00:33 espie Exp */ #ifdef syscallarg @@ -539,7 +539,7 @@ struct sys_chflagsat_args { struct sys_pledge_args { syscallarg(const char *) request; - syscallarg(const char **) paths; + syscallarg(const char *) execrequest; }; struct sys_ppoll_args {