| From 14588dfe2e411056df5ba85ef88ad51730a2fa0a Mon Sep 17 00:00:00 2001 |
| From: "Eric W. Biederman" <ebiederm@xmission.com> |
| Date: Sat, 15 Aug 2015 20:27:13 -0500 |
| Subject: [PATCH 2/2] vfs: Test for and handle paths that are unreachable from |
| their mnt_root |
| |
| commit 397d425dc26da728396e66d392d5dcb8dac30c37 upstream. |
| |
| In rare cases a directory can be renamed out from under a bind mount. |
| In those cases without special handling it becomes possible to walk up |
| the directory tree to the root dentry of the filesystem and down |
| from the root dentry to every other file or directory on the filesystem. |
| |
| Like division by zero .. from an unconnected path can not be given |
| a useful semantic as there is no predicting at which path component |
| the code will realize it is unconnected. We certainly can not match |
| the current behavior as the current behavior is a security hole. |
| |
| Therefore when encounting .. when following an unconnected path |
| return -ENOENT. |
| |
| - Add a function path_connected to verify path->dentry is reachable |
| from path->mnt.mnt_root. AKA to validate that rename did not do |
| something nasty to the bind mount. |
| |
| To avoid races path_connected must be called after following a path |
| component to it's next path component. |
| |
| Signed-off-by: "Eric W. Biederman" <ebiederm@xmission.com> |
| Signed-off-by: Al Viro <viro@zeniv.linux.org.uk> |
| |
| fs/namei.c | 27 +++++++++++++++++++++++++-- |
| 1 file changed, 25 insertions(+), 2 deletions(-) |
| |
| diff --git a/fs/namei.c b/fs/namei.c |
| index 1c2105ed20c5..29b927938b8c 100644 |
| |
| |
| @@ -560,6 +560,24 @@ static int __nd_alloc_stack(struct nameidata *nd) |
| return 0; |
| } |
| |
| +/** |
| + * path_connected - Verify that a path->dentry is below path->mnt.mnt_root |
| + * @path: nameidate to verify |
| + * |
| + * Rename can sometimes move a file or directory outside of a bind |
| + * mount, path_connected allows those cases to be detected. |
| + */ |
| +static bool path_connected(const struct path *path) |
| +{ |
| + struct vfsmount *mnt = path->mnt; |
| + |
| + /* Only bind mounts can have disconnected paths */ |
| + if (mnt->mnt_root == mnt->mnt_sb->s_root) |
| + return true; |
| + |
| + return is_subdir(path->dentry, mnt->mnt_root); |
| +} |
| + |
| static inline int nd_alloc_stack(struct nameidata *nd) |
| { |
| if (likely(nd->depth != EMBEDDED_LEVELS)) |
| @@ -1296,6 +1314,8 @@ static int follow_dotdot_rcu(struct nameidata *nd) |
| return -ECHILD; |
| nd->path.dentry = parent; |
| nd->seq = seq; |
| + if (unlikely(!path_connected(&nd->path))) |
| + return -ENOENT; |
| break; |
| } else { |
| struct mount *mnt = real_mount(nd->path.mnt); |
| @@ -1396,7 +1416,7 @@ static void follow_mount(struct path *path) |
| } |
| } |
| |
| -static void follow_dotdot(struct nameidata *nd) |
| +static int follow_dotdot(struct nameidata *nd) |
| { |
| if (!nd->root.mnt) |
| set_root(nd); |
| @@ -1412,6 +1432,8 @@ static void follow_dotdot(struct nameidata *nd) |
| /* rare case of legitimate dget_parent()... */ |
| nd->path.dentry = dget_parent(nd->path.dentry); |
| dput(old); |
| + if (unlikely(!path_connected(&nd->path))) |
| + return -ENOENT; |
| break; |
| } |
| if (!follow_up(&nd->path)) |
| @@ -1419,6 +1441,7 @@ static void follow_dotdot(struct nameidata *nd) |
| } |
| follow_mount(&nd->path); |
| nd->inode = nd->path.dentry->d_inode; |
| + return 0; |
| } |
| |
| /* |
| @@ -1634,7 +1657,7 @@ static inline int handle_dots(struct nameidata *nd, int type) |
| if (nd->flags & LOOKUP_RCU) { |
| return follow_dotdot_rcu(nd); |
| } else |
| - follow_dotdot(nd); |
| + return follow_dotdot(nd); |
| } |
| return 0; |
| } |
| -- |
| 2.4.3 |
| |