diff mbox

[13/29] 9pfs: local: remove: don't follow symlinks

Message ID 148760166051.31154.9036980312875397552.stgit@bahia.lan
State New
Headers show

Commit Message

Greg Kurz Feb. 20, 2017, 2:41 p.m. UTC
The local_remove() callback is vulnerable to symlink attacks because it
calls:

(1) lstat() which follows symbolic links in all path elements but the
    rightmost one
(2) remove() which follows symbolic links in all path elements but the
    rightmost one

This patch converts local_remove() to rely on opendir_nofollow(),
fstatat(AT_SYMLINK_NOFOLLOW) to fix (1) and unlinkat() to fix (2).

This partly fixes CVE-2016-9602.

Signed-off-by: Greg Kurz <groug@kaod.org>
---
 hw/9pfs/9p-local.c |   64 +++++++++++++++++-----------------------------------
 1 file changed, 21 insertions(+), 43 deletions(-)

Comments

Stefan Hajnoczi Feb. 23, 2017, 2:23 p.m. UTC | #1
On Mon, Feb 20, 2017 at 03:41:00PM +0100, Greg Kurz wrote:
> +    dirfd = local_opendir_nofollow(ctx, dirpath);
> +    if (dirfd) {
> +        goto out;
>      }
>  
> -    buffer = rpath(ctx, path);
> -    err = remove(buffer);
> -    g_free(buffer);
> +    if (fstatat(dirfd, path, &stbuf, AT_SYMLINK_NOFOLLOW) < 0) {
> +        goto err_out;
> +    }
> +
> +    if (S_ISDIR(stbuf.st_mode)) {
> +        flags |= AT_REMOVEDIR;
> +    }
> +
> +    err = local_unlinkat_common(ctx, dirfd, name, flags);

The alternative is optimistically skip fstat but then do:

  if (err == EISDIR) {
      err = local_unlinkat_common(ctx, dirfd, name, flags | AT_REMOVEDIR);
  }

It might be faster.

Reviewed-by: Stefan Hajnoczi <stefanha@redhat.com>
Greg Kurz Feb. 24, 2017, 12:21 a.m. UTC | #2
On Thu, 23 Feb 2017 14:23:15 +0000
Stefan Hajnoczi <stefanha@gmail.com> wrote:

> On Mon, Feb 20, 2017 at 03:41:00PM +0100, Greg Kurz wrote:
> > +    dirfd = local_opendir_nofollow(ctx, dirpath);
> > +    if (dirfd) {
> > +        goto out;
> >      }
> >  
> > -    buffer = rpath(ctx, path);
> > -    err = remove(buffer);
> > -    g_free(buffer);
> > +    if (fstatat(dirfd, path, &stbuf, AT_SYMLINK_NOFOLLOW) < 0) {
> > +        goto err_out;
> > +    }
> > +
> > +    if (S_ISDIR(stbuf.st_mode)) {
> > +        flags |= AT_REMOVEDIR;
> > +    }
> > +
> > +    err = local_unlinkat_common(ctx, dirfd, name, flags);  
> 
> The alternative is optimistically skip fstat but then do:
> 
>   if (err == EISDIR) {

It would be err == -1 && errno == EISDIR actually.

>       err = local_unlinkat_common(ctx, dirfd, name, flags | AT_REMOVEDIR);
>   }
> 
> It might be faster.
> 

This would work for passthrough and mapped modes indeed. For mapped-file
mode, things are more complicated though. If path points to a directory
and we call local_unlinkat_common() without AT_REMOVEDIR, it won't unlink
the metadata directory and unlinkat() will fail with ENOENT because
the directory isn't empty... I'd rather try to optimize in a followup
patch later to avoid the extra complexity.

> Reviewed-by: Stefan Hajnoczi <stefanha@redhat.com>
diff mbox

Patch

diff --git a/hw/9pfs/9p-local.c b/hw/9pfs/9p-local.c
index 21522ca7de43..c6f4c8d95442 100644
--- a/hw/9pfs/9p-local.c
+++ b/hw/9pfs/9p-local.c
@@ -1016,54 +1016,32 @@  err_out:
 
 static int local_remove(FsContext *ctx, const char *path)
 {
-    int err;
     struct stat stbuf;
-    char *buffer;
+    char *dirpath = g_path_get_dirname(path);
+    char *name = g_path_get_basename(path);
+    int flags = 0;
+    int dirfd;
+    int err = -1;
 
-    if (ctx->export_flags & V9FS_SM_MAPPED_FILE) {
-        buffer = rpath(ctx, path);
-        err =  lstat(buffer, &stbuf);
-        g_free(buffer);
-        if (err) {
-            goto err_out;
-        }
-        /*
-         * If directory remove .virtfs_metadata contained in the
-         * directory
-         */
-        if (S_ISDIR(stbuf.st_mode)) {
-            buffer = g_strdup_printf("%s/%s/%s", ctx->fs_root,
-                                     path, VIRTFS_META_DIR);
-            err = remove(buffer);
-            g_free(buffer);
-            if (err < 0 && errno != ENOENT) {
-                /*
-                 * We didn't had the .virtfs_metadata file. May be file created
-                 * in non-mapped mode ?. Ignore ENOENT.
-                 */
-                goto err_out;
-            }
-        }
-        /*
-         * Now remove the name from parent directory
-         * .virtfs_metadata directory
-         */
-        buffer = local_mapped_attr_path(ctx, path);
-        err = remove(buffer);
-        g_free(buffer);
-        if (err < 0 && errno != ENOENT) {
-            /*
-             * We didn't had the .virtfs_metadata file. May be file created
-             * in non-mapped mode ?. Ignore ENOENT.
-             */
-            goto err_out;
-        }
+    dirfd = local_opendir_nofollow(ctx, dirpath);
+    if (dirfd) {
+        goto out;
     }
 
-    buffer = rpath(ctx, path);
-    err = remove(buffer);
-    g_free(buffer);
+    if (fstatat(dirfd, path, &stbuf, AT_SYMLINK_NOFOLLOW) < 0) {
+        goto err_out;
+    }
+
+    if (S_ISDIR(stbuf.st_mode)) {
+        flags |= AT_REMOVEDIR;
+    }
+
+    err = local_unlinkat_common(ctx, dirfd, name, flags);
 err_out:
+    close_preserve_errno(dirfd);
+out:
+    g_free(name);
+    g_free(dirpath);
     return err;
 }