Commit message: 9pfs: fix memory leak in v9fs_write

If an error occurs when marshalling the transfer length to the guest, the
v9fs_write() function doesn't free an IO vector, thus leading to a memory
leak. This patch fixes the issue.

Signed-off-by: Li Qiang <liqiang6-s@360.cn>
Reviewed-by: Greg Kurz <groug@kaod.org>
[groug, rephrased the changelog]
Signed-off-by: Greg Kurz <groug@kaod.org>
CVE: CVE-2016-9106
CWE: CWE-399
Vulnerability description: Memory leak in the v9fs_write function in hw/9pfs/9p.c in QEMU (aka Quick Emulator) allows local guest OS administrators to cause a denial of service (memory consumption) by leveraging failure to free an IO vector.

Project: qemu
File: 9p.c

static int coroutine_fn v9fs_complete_rename(V9fsPDU *pdu, V9fsFidState *fidp,
                                             int32_t newdirfid,
                                             V9fsString *name)
{
    char *end;
    int err = 0;
    V9fsPath new_path;
    V9fsFidState *tfidp;
    V9fsState *s = pdu->s;
    V9fsFidState *dirfidp = NULL;
    char *old_name, *new_name;

    v9fs_path_init(&new_path);
    if (newdirfid != -1) {
        dirfidp = get_fid(pdu, newdirfid);
        if (dirfidp == NULL) {
            err = -ENOENT;
            goto out_nofid;
        }
        BUG_ON(dirfidp->fid_type != P9_FID_NONE);
        v9fs_co_name_to_path(pdu, &dirfidp->path, name->data, &new_path);
    } else {
        old_name = fidp->path.data;
        end = strrchr(old_name, '/');
        if (end) {
            end++;
        } else {
            end = old_name;
        }
        new_name = g_malloc0(end - old_name + name->size + 1);
        strncat(new_name, old_name, end - old_name);
        strncat(new_name + (end - old_name), name->data, name->size);
        v9fs_co_name_to_path(pdu, NULL, new_name, &new_path);
        g_free(new_name);
    }
    err = v9fs_co_rename(pdu, &fidp->path, &new_path);
    if (err < 0) {
        goto out;
    }
    /*
     * Fixup fid's pointing to the old name to
     * start pointing to the new name
     */
    for (tfidp = s->fid_list; tfidp; tfidp = tfidp->next) {
        if (v9fs_path_is_ancestor(&fidp->path, &tfidp->path)) {
            /* replace the name */
            v9fs_fix_path(&tfidp->path, &new_path, strlen(fidp->path.data));
        }
    }
out:
    if (dirfidp) {
        put_fid(pdu, dirfidp);
    }
    v9fs_path_free(&new_path);
out_nofid:
    return err;
}

Project: qemu
File: 9p.c

static void coroutine_fn v9fs_open(void *opaque)
{
    int flags;
    int32_t fid;
    int32_t mode;
    V9fsQID qid;
    int iounit = 0;
    ssize_t err = 0;
    size_t offset = 7;
    struct stat stbuf;
    V9fsFidState *fidp;
    V9fsPDU *pdu = opaque;
    V9fsState *s = pdu->s;

    if (s->proto_version == V9FS_PROTO_2000L) {
        err = pdu_unmarshal(pdu, offset, "dd", &fid, &mode);
    } else {
        uint8_t modebyte;
        err = pdu_unmarshal(pdu, offset, "db", &fid, &modebyte);
        mode = modebyte;
    }
    if (err < 0) {
        goto out_nofid;
    }
    trace_v9fs_open(pdu->tag, pdu->id, fid, mode);

    fidp = get_fid(pdu, fid);
    if (fidp == NULL) {
        err = -ENOENT;
        goto out_nofid;
    }
    BUG_ON(fidp->fid_type != P9_FID_NONE);

    err = v9fs_co_lstat(pdu, &fidp->path, &stbuf);
    if (err < 0) {
        goto out;
    }
    stat_to_qid(&stbuf, &qid);
    if (S_ISDIR(stbuf.st_mode)) {
        err = v9fs_co_opendir(pdu, fidp);
        if (err < 0) {
            goto out;
        }
        fidp->fid_type = P9_FID_DIR;
        err = pdu_marshal(pdu, offset, "Qd", &qid, 0);
        if (err < 0) {
            goto out;
        }
        err += offset;
    } else {
        if (s->proto_version == V9FS_PROTO_2000L) {
            flags = get_dotl_openflags(s, mode);
        } else {
            flags = omode_to_uflags(mode);
        }
        if (is_ro_export(&s->ctx)) {
            if (mode & O_WRONLY || mode & O_RDWR ||
                mode & O_APPEND || mode & O_TRUNC) {
                err = -EROFS;
                goto out;
            }
        }
        err = v9fs_co_open(pdu, fidp, flags);
        if (err < 0) {
            goto out;
        }
        fidp->fid_type = P9_FID_FILE;
        fidp->open_flags = flags;
        if (flags & O_EXCL) {
            /*
             * We let the host file system do O_EXCL check
             * We should not reclaim such fd
             */
            fidp->flags |= FID_NON_RECLAIMABLE;
        }
        iounit = get_iounit(pdu, &fidp->path);
        err = pdu_marshal(pdu, offset, "Qd", &qid, iounit);
        if (err < 0) {
            goto out;
        }
        err += offset;
    }
    trace_v9fs_open_return(pdu->tag, pdu->id,
                           qid.type, qid.version, qid.path, iounit);
out:
    put_fid(pdu, fidp);
out_nofid:
    pdu_complete(pdu, err);
}

Project: qemu
File: 9p.c

static void coroutine_fn v9fs_rename(void *opaque)
{
    int32_t fid;
    ssize_t err = 0;
    size_t offset = 7;
    V9fsString name;
    int32_t newdirfid;
    V9fsFidState *fidp;
    V9fsPDU *pdu = opaque;
    V9fsState *s = pdu->s;

    v9fs_string_init(&name);
    err = pdu_unmarshal(pdu, offset, "dds", &fid, &newdirfid, &name);
    if (err < 0) {
        goto out_nofid;
    }

    if (name_is_illegal(name.data)) {
        err = -ENOENT;
        goto out_nofid;
    }

    if (!strcmp(".", name.data) || !strcmp("..", name.data)) {
        err = -EISDIR;
        goto out_nofid;
    }

    fidp = get_fid(pdu, fid);
    if (fidp == NULL) {
        err = -ENOENT;
        goto out_nofid;
    }
    BUG_ON(fidp->fid_type != P9_FID_NONE);
    /* if fs driver is not path based, return EOPNOTSUPP */
    if (!(pdu->s->ctx.export_flags & V9FS_PATHNAME_FSCONTEXT)) {
        err = -EOPNOTSUPP;
        goto out;
    }
    v9fs_path_write_lock(s);
    err = v9fs_complete_rename(pdu, fidp, newdirfid, &name);
    v9fs_path_unlock(s);
    if (!err) {
        err = offset;
    }
out:
    put_fid(pdu, fidp);
out_nofid:
    pdu_complete(pdu, err);
    v9fs_string_free(&name);
}

Project: qemu
File: 9p.c

static void coroutine_fn v9fs_walk(void *opaque)
{
    int name_idx;
    V9fsQID *qids = NULL;
    int i, err = 0;
    V9fsPath dpath, path;
    uint16_t nwnames;
    struct stat stbuf;
    size_t offset = 7;
    int32_t fid, newfid;
    V9fsString *wnames = NULL;
    V9fsFidState *fidp;
    V9fsFidState *newfidp = NULL;
    V9fsPDU *pdu = opaque;
    V9fsState *s = pdu->s;
    V9fsQID qid;

    err = pdu_unmarshal(pdu, offset, "ddw", &fid, &newfid, &nwnames);
    if (err < 0) {
        pdu_complete(pdu, err);
        return ;
    }
    offset += err;

    trace_v9fs_walk(pdu->tag, pdu->id, fid, newfid, nwnames);

    if (nwnames && nwnames <= P9_MAXWELEM) {
        wnames = g_malloc0(sizeof(wnames[0]) * nwnames);
        qids   = g_malloc0(sizeof(qids[0]) * nwnames);
        for (i = 0; i < nwnames; i++) {
            err = pdu_unmarshal(pdu, offset, "s", &wnames[i]);
            if (err < 0) {
                goto out_nofid;
            }
            if (name_is_illegal(wnames[i].data)) {
                err = -ENOENT;
                goto out_nofid;
            }
            offset += err;
        }
    } else if (nwnames > P9_MAXWELEM) {
        err = -EINVAL;
        goto out_nofid;
    }
    fidp = get_fid(pdu, fid);
    if (fidp == NULL) {
        err = -ENOENT;
        goto out_nofid;
    }

    v9fs_path_init(&dpath);
    v9fs_path_init(&path);

    err = fid_to_qid(pdu, fidp, &qid);
    if (err < 0) {
        goto out;
    }

    /*
     * Both dpath and path initially poin to fidp.
     * Needed to handle request with nwnames == 0
     */
    v9fs_path_copy(&dpath, &fidp->path);
    v9fs_path_copy(&path, &fidp->path);
    for (name_idx = 0; name_idx < nwnames; name_idx++) {
        if (not_same_qid(&pdu->s->root_qid, &qid) ||
            strcmp("..", wnames[name_idx].data)) {
            err = v9fs_co_name_to_path(pdu, &dpath, wnames[name_idx].data,
                                       &path);
            if (err < 0) {
                goto out;
            }

            err = v9fs_co_lstat(pdu, &path, &stbuf);
            if (err < 0) {
                goto out;
            }
            stat_to_qid(&stbuf, &qid);
            v9fs_path_copy(&dpath, &path);
        }
        memcpy(&qids[name_idx], &qid, sizeof(qid));
    }
    if (fid == newfid) {
        BUG_ON(fidp->fid_type != P9_FID_NONE);
        v9fs_path_copy(&fidp->path, &path);
    } else {
        newfidp = alloc_fid(s, newfid);
        if (newfidp == NULL) {
            err = -EINVAL;
            goto out;
        }
        newfidp->uid = fidp->uid;
        v9fs_path_copy(&newfidp->path, &path);
    }
    err = v9fs_walk_marshal(pdu, nwnames, qids);
    trace_v9fs_walk_return(pdu->tag, pdu->id, nwnames, qids);
out:
    put_fid(pdu, fidp);
    if (newfidp) {
        put_fid(pdu, newfidp);
    }
    v9fs_path_free(&dpath);
    v9fs_path_free(&path);
out_nofid:
    pdu_complete(pdu, err);
    if (nwnames && nwnames <= P9_MAXWELEM) {
        for (name_idx = 0; name_idx < nwnames; name_idx++) {
            v9fs_string_free(&wnames[name_idx]);
        }
        g_free(wnames);
        g_free(qids);
    }
}

Project: qemu
File: 9p.c

static int coroutine_fn v9fs_xattr_fid_clunk(V9fsPDU *pdu, V9fsFidState *fidp)
{
    int retval = 0;

    if (fidp->fs.xattr.copied_len == -1) {
        /* getxattr/listxattr fid */
        goto free_value;
    }
    /*
     * if this is fid for setxattr. clunk should
     * result in setxattr localcall
     */
    if (fidp->fs.xattr.len != fidp->fs.xattr.copied_len) {
        /* clunk after partial write */
        retval = -EINVAL;
        goto free_out;
    }
    if (fidp->fs.xattr.len) {
        retval = v9fs_co_lsetxattr(pdu, &fidp->path, &fidp->fs.xattr.name,
                                   fidp->fs.xattr.value,
                                   fidp->fs.xattr.len,
                                   fidp->fs.xattr.flags);
    } else {
        retval = v9fs_co_lremovexattr(pdu, &fidp->path, &fidp->fs.xattr.name);
    }
free_out:
    v9fs_string_free(&fidp->fs.xattr.name);
free_value:
    g_free(fidp->fs.xattr.value);
    return retval;
}

Project: qemu
File: 9p.c

static int v9fs_xattr_read(V9fsState *s, V9fsPDU *pdu, V9fsFidState *fidp,
                           uint64_t off, uint32_t max_count)
{
    ssize_t err;
    size_t offset = 7;
    int read_count;
    int64_t xattr_len;
    V9fsVirtioState *v = container_of(s, V9fsVirtioState, state);
    VirtQueueElement *elem = v->elems[pdu->idx];

    xattr_len = fidp->fs.xattr.len;
    read_count = xattr_len - off;
    if (read_count > max_count) {
        read_count = max_count;
    } else if (read_count < 0) {
        /*
         * read beyond XATTR value
         */
        read_count = 0;
    }
    err = pdu_marshal(pdu, offset, "d", read_count);
    if (err < 0) {
        return err;
    }
    offset += err;

    err = v9fs_pack(elem->in_sg, elem->in_num, offset,
                    ((char *)fidp->fs.xattr.value) + off,
                    read_count);
    if (err < 0) {
        return err;
    }
    offset += err;
    return offset;
}

Project: qemu
File: 9p.c

static void coroutine_fn v9fs_xattrcreate(void *opaque)
{
    int flags;
    int32_t fid;
    int64_t size;
    ssize_t err = 0;
    V9fsString name;
    size_t offset = 7;
    V9fsFidState *file_fidp;
    V9fsFidState *xattr_fidp;
    V9fsPDU *pdu = opaque;

    v9fs_string_init(&name);
    err = pdu_unmarshal(pdu, offset, "dsqd", &fid, &name, &size, &flags);
    if (err < 0) {
        goto out_nofid;
    }
    trace_v9fs_xattrcreate(pdu->tag, pdu->id, fid, name.data, size, flags);

    file_fidp = get_fid(pdu, fid);
    if (file_fidp == NULL) {
        err = -EINVAL;
        goto out_nofid;
    }
    /* Make the file fid point to xattr */
    xattr_fidp = file_fidp;
    xattr_fidp->fid_type = P9_FID_XATTR;
    xattr_fidp->fs.xattr.copied_len = 0;
    xattr_fidp->fs.xattr.len = size;
    xattr_fidp->fs.xattr.flags = flags;
    v9fs_string_init(&xattr_fidp->fs.xattr.name);
    v9fs_string_copy(&xattr_fidp->fs.xattr.name, &name);
    g_free(xattr_fidp->fs.xattr.value);
    xattr_fidp->fs.xattr.value = g_malloc0(size);
    err = offset;
    put_fid(pdu, file_fidp);
out_nofid:
    pdu_complete(pdu, err);
    v9fs_string_free(&name);
}

Project: qemu
File: 9p.c

static void coroutine_fn v9fs_xattrwalk(void *opaque)
{
    int64_t size;
    V9fsString name;
    ssize_t err = 0;
    size_t offset = 7;
    int32_t fid, newfid;
    V9fsFidState *file_fidp;
    V9fsFidState *xattr_fidp = NULL;
    V9fsPDU *pdu = opaque;
    V9fsState *s = pdu->s;

    v9fs_string_init(&name);
    err = pdu_unmarshal(pdu, offset, "dds", &fid, &newfid, &name);
    if (err < 0) {
        goto out_nofid;
    }
    trace_v9fs_xattrwalk(pdu->tag, pdu->id, fid, newfid, name.data);

    file_fidp = get_fid(pdu, fid);
    if (file_fidp == NULL) {
        err = -ENOENT;
        goto out_nofid;
    }
    xattr_fidp = alloc_fid(s, newfid);
    if (xattr_fidp == NULL) {
        err = -EINVAL;
        goto out;
    }
    v9fs_path_copy(&xattr_fidp->path, &file_fidp->path);
    if (!v9fs_string_size(&name)) {
        /*
         * listxattr request. Get the size first
         */
        size = v9fs_co_llistxattr(pdu, &xattr_fidp->path, NULL, 0);
        if (size < 0) {
            err = size;
            clunk_fid(s, xattr_fidp->fid);
            goto out;
        }
        /*
         * Read the xattr value
         */
        xattr_fidp->fs.xattr.len = size;
        xattr_fidp->fid_type = P9_FID_XATTR;
        xattr_fidp->fs.xattr.copied_len = -1;
        if (size) {
            xattr_fidp->fs.xattr.value = g_malloc(size);
            err = v9fs_co_llistxattr(pdu, &xattr_fidp->path,
                                     xattr_fidp->fs.xattr.value,
                                     xattr_fidp->fs.xattr.len);
            if (err < 0) {
                clunk_fid(s, xattr_fidp->fid);
                goto out;
            }
        }
        err = pdu_marshal(pdu, offset, "q", size);
        if (err < 0) {
            goto out;
        }
        err += offset;
    } else {
        /*
         * specific xattr fid. We check for xattr
         * presence also collect the xattr size
         */
        size = v9fs_co_lgetxattr(pdu, &xattr_fidp->path,
                                 &name, NULL, 0);
        if (size < 0) {
            err = size;
            clunk_fid(s, xattr_fidp->fid);
            goto out;
        }
        /*
         * Read the xattr value
         */
        xattr_fidp->fs.xattr.len = size;
        xattr_fidp->fid_type = P9_FID_XATTR;
        xattr_fidp->fs.xattr.copied_len = -1;
        if (size) {
            xattr_fidp->fs.xattr.value = g_malloc(size);
            err = v9fs_co_lgetxattr(pdu, &xattr_fidp->path,
                                    &name, xattr_fidp->fs.xattr.value,
                                    xattr_fidp->fs.xattr.len);
            if (err < 0) {
                clunk_fid(s, xattr_fidp->fid);
                goto out;
            }
        }
        err = pdu_marshal(pdu, offset, "q", size);
        if (err < 0) {
            goto out;
        }
        err += offset;
    }
    trace_v9fs_xattrwalk_return(pdu->tag, pdu->id, size);
out:
    put_fid(pdu, file_fidp);
    if (xattr_fidp) {
        put_fid(pdu, xattr_fidp);
    }
out_nofid:
    pdu_complete(pdu, err);
    v9fs_string_free(&name);
}

Project: qemu
File: 9p.c

static void coroutine_fn virtfs_reset(V9fsPDU *pdu)
{
    V9fsState *s = pdu->s;
    V9fsFidState *fidp = NULL;

    /* Free all fids */
    while (s->fid_list) {
        fidp = s->fid_list;
        s->fid_list = fidp->next;

        if (fidp->ref) {
            fidp->clunked = 1;
        } else {
            free_fid(pdu, fidp);
        }
    }
    if (fidp) {
        /* One or more unclunked fids found... */
        error_report("9pfs:%s: One or more uncluncked fids "
                     "found during reset", __func__);
    }
}