From b070dbca7ca4aaf5e8cc03ec844980fd5296aa7f Mon Sep 17 00:00:00 2001 From: Jan Bramkamp Date: Mon, 14 Apr 2025 18:01:47 +0200 Subject: [PATCH] Fix PR #262179 [1] by blocking unsafe directory filedescriptor passing. Prohibit jailed processes from receiving directory file descriptors to directories outside the receiver's jail root since those would provide an jail escape via fchdir(2) an openat(2). - [1]: https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=262179 --- sys/kern/kern_jail.c | 94 ++++++++++++++++++++++++++++++++++++++++++ sys/kern/uipc_usrreq.c | 6 +++ sys/sys/jail.h | 3 ++ 3 files changed, 103 insertions(+) diff --git a/sys/kern/kern_jail.c b/sys/kern/kern_jail.c index 5dd07fbf77d1..bb332253b94c 100644 --- a/sys/kern/kern_jail.c +++ b/sys/kern/kern_jail.c @@ -65,6 +65,7 @@ #include #include #include +#include #include #include @@ -5015,6 +5016,99 @@ prison_racct_detach(struct prison *pr) } #endif /* RACCT */ +/* + * Return 0 if the jail contains the vnode, + * return EPERM if the jail does not contain the vnode, + * return early on errors (VOP_LOCK(9), namei(9)). + */ +int +prison_contains(struct prison *const pr, struct vnode *const node) +{ + // Quick check for a trivial case. + struct vnode *const root = pr->pr_root; + if (node == root) + return 0; + + // Lock node and retain additional reference. + int error = VOP_LOCK(node, LK_SHARED); + if (error) + return error; + vref(node); + + // Growable stack of vnodes starting with a fixed sized array. + struct vnode *fixed[32], **stack = fixed; + size_t used = 0, size = nitems(fixed); + + // Push the starting node on the stack. + stack[used++] = node; + struct vnode *top_of_stack = node, *next_of_stack = NULL; + + // Search for the root among the current node's ancestors, + // terminate early when hitting an other root. + while (top_of_stack != root && top_of_stack != next_of_stack) { + // Attempt to lookup, retain and lock the node's parent. + struct nameidata nd[1]; + NDINIT_ALL(nd, LOOKUP, LOCKLEAF | LOCKSHARED, UIO_SYSSPACE, "..", AT_FDCWD, top_of_stack, &cap_no_rights); + error = namei(nd); + NDFREE_PNBUF(nd); + + // Path traversal errors abort the search early. + if (error) + break; + + // Grow the vnode stack as needed. + if (used >= size) { + size *= 2; + if (stack == fixed) // Move the vnode stack from the kernel stack to the kernel heap to grow. + stack = memcpy(mallocarray(size, sizeof(*stack), M_TEMP, M_WAITOK), fixed, sizeof(fixed)); + else // Grow the vnode stack on the kernel heap. + stack = realloc(stack, sizeof(*stack) * size, M_TEMP, M_WAITOK); + } + + // Push the parent vnode on the stack. + next_of_stack = top_of_stack; + top_of_stack = nd->ni_vp; + stack[used++] = top_of_stack; + } + + // Report permission error unless the root has been found among the nodes ancestors. + if (!error && top_of_stack != root) + error = EPERM; + + // Unlock and release the vnodes. + while (used > 0) + vput(stack[--used]); + + // Free the stack if it was moved on the heap. + if (stack != fixed) + free(stack, M_TEMP); + + return error; +} + +/* + * Return 0 iif jail can safely contain the file. + * + * A jail can contain a file if it's either already contained by the jail, + * is anything but a directory or is a directory inside the jail root. + */ +int +prison_cancontain(struct prison *const pr, struct file *const file) +{ + // Allow if the file was opened inside the jail. + if (pr == file->f_cred->cr_prison) + return 0; + + // Allow unless the file is a directory. + struct vnode *const node = file->f_vnode; + if (node == NULL || node->v_type != VDIR) + return 0; + + // Require the file to be a descendant of the jail root. + return prison_contains(pr, node); + +} + #ifdef DDB static void diff --git a/sys/kern/uipc_usrreq.c b/sys/kern/uipc_usrreq.c index a67e105a1447..49477d7981d7 100644 --- a/sys/kern/uipc_usrreq.c +++ b/sys/kern/uipc_usrreq.c @@ -89,6 +89,7 @@ #include #include #include +#include #include @@ -2464,6 +2465,11 @@ unp_externalize(struct mbuf *control, struct mbuf **controlp, int flags) goto next; fdep = data; + /* Refuse to deliver unsafe directory file descriptors to jailed receivers. */ + if (jailed(td->td_ucred)) + for (i = 0; error == 0 && i < newfds; i++) + error = prison_cancontain(td->td_ucred->cr_prison, fdep[i]->fde_file); + /* If we're not outputting the descriptors free them. */ if (error || controlp == NULL) { unp_freerights(fdep, newfds); diff --git a/sys/sys/jail.h b/sys/sys/jail.h index 90fcf8cd5a47..64d820dccb89 100644 --- a/sys/sys/jail.h +++ b/sys/sys/jail.h @@ -418,6 +418,7 @@ struct mount; struct sockaddr; struct statfs; struct vfsconf; +struct file; /* * Return 1 if the passed credential is in a jail, otherwise 0. @@ -496,6 +497,8 @@ void prison_racct_foreach(void (*callback)(struct racct *racct, struct prison_racct *prison_racct_find(const char *name); void prison_racct_hold(struct prison_racct *prr); void prison_racct_free(struct prison_racct *prr); +int prison_contains(struct prison *, struct vnode *); +int prison_cancontain(struct prison *, struct file *); #endif /* _KERNEL */ #endif /* !_SYS_JAIL_H_ */ -- 2.49.0