Commit message: memslot: Fix off-by-one error in group/slot boundary check

RedMemSlotInfo keeps an array of groups, and each group contains an
array of slots. Unfortunately, these checks are off by 1, they check
that the index is greater or equal to the number of elements in the
array, while these arrays are 0 based. The check should only check for
strictly greater than the number of elements.

For the group array, this is not a big issue, as these memslot groups
are created by spice-server users (eg QEMU), and the group ids used to
index that array are also generated by the spice-server user, so it
should not be possible for the guest to set them to arbitrary values.

The slot id is more problematic, as it's calculated from a QXLPHYSICAL
address, and such addresses are usually set by the guest QXL driver, so
the guest can set these to arbitrary values, including malicious values,
which are probably easy to build from the guest PCI configuration.

This patch fixes the arrays bound check, and adds a test case for this.
This fixes CVE-2019-3813.

Signed-off-by: Christophe Fergeau <cfergeau@redhat.com>
Acked-by: Frediano Ziglio <fziglio@redhat.com>
CVE: CVE-2019-3813
CWE: CWE-193
Vulnerability description: Spice, versions 0.5.2 through 0.14.1, are vulnerable to an out-of-bounds read due to an off-by-one error in memslot_get_virt. This may lead to a denial of service, or, in the worst case, code-execution by unauthenticated attackers.

Project: spice
File: memslot.c

void memslot_info_reset(RedMemSlotInfo *info)
{
        uint32_t i;
        for (i = 0; i < info->num_memslots_groups; ++i) {
            memset(info->mem_slots[i], 0, sizeof(MemSlot) * info->num_memslots);
        }
}

Project: spice
File: test-qxl-parsing.c

static void test_circular_small_chunks(void)
{
    RedMemSlotInfo mem_info;
    RedCursorCmd *red_cursor_cmd;
    QXLCursorCmd cursor_cmd;
    QXLCursor *cursor;
    QXLDataChunk *chunks[2];

    init_meminfo(&mem_info);
    g_test_expect_message(G_LOG_DOMAIN, G_LOG_LEVEL_WARNING,
                          "*red_get_data_chunks_ptr: data split in too many chunks, avoiding DoS*");

    /* a circular list of small chunks should not be a problems */
    memset(&cursor_cmd, 0, sizeof(cursor_cmd));
    cursor_cmd.type = QXL_CURSOR_SET;

    cursor = create_chunk(SPICE_OFFSETOF(QXLCursor, chunk), 1, NULL, 0xaa);
    cursor->header.unique = 1;
    cursor->header.width = 128;
    cursor->header.height = 128;
    cursor->data_size = 128 * 128 * 4;

    chunks[0] = create_chunk(0, 1, &cursor->chunk, 0xaa);
    chunks[0]->next_chunk = to_physical(&cursor->chunk);

    cursor_cmd.u.set.shape = to_physical(cursor);

    red_cursor_cmd = red_cursor_cmd_new(NULL, &mem_info, 0, to_physical(&cursor_cmd));
    if (red_cursor_cmd != NULL) {
        /* function does not return errors so there should be no data */
        g_assert_cmpuint(red_cursor_cmd->type, ==, QXL_CURSOR_SET);
        g_assert_cmpuint(red_cursor_cmd->u.set.position.x, ==, 0);
        g_assert_cmpuint(red_cursor_cmd->u.set.position.y, ==, 0);
        g_assert_cmpuint(red_cursor_cmd->u.set.shape.data_size, ==, 0);
        red_cursor_cmd_unref(red_cursor_cmd);
    }
    g_test_assert_expected_messages();

    g_free(cursor);
    g_free(chunks[0]);
    memslot_info_destroy(&mem_info);
}

Project: spice
File: test-qxl-parsing.c

static void deinit_qxl_surface(QXLSurfaceCmd *qxl)
{
    g_free(from_physical(qxl->u.surface_create.data));
}

Project: spice
File: memslot.c

void memslot_info_del_slot(RedMemSlotInfo *info, uint32_t slot_group_id, uint32_t slot_id)
{
    spice_return_if_fail(info->num_memslots_groups > slot_group_id);
    spice_return_if_fail(info->num_memslots > slot_id);

    info->mem_slots[slot_group_id][slot_id].virt_start_addr = 0;
    info->mem_slots[slot_group_id][slot_id].virt_end_addr = 0;
}

Project: spice
File: test-qxl-parsing.c

from_physical(QXLPHYSICAL physical)
{
    return (void *)(uintptr_t) physical;
}

Project: spice
File: memslot.c

void memslot_info_destroy(RedMemSlotInfo *info)
{
    uint32_t i;

    for (i = 0; i < info->num_memslots_groups; ++i) {
        g_free(info->mem_slots[i]);
    }
    g_free(info->mem_slots);
}

Project: spice
File: test-qxl-parsing.c

static void test_memslot_invalid_slot_id(void)
{
    RedMemSlotInfo mem_info;
    init_meminfo(&mem_info);

    memslot_get_virt(&mem_info, 1 << mem_info.memslot_id_shift, 16, 0);
}

Project: spice
File: test-qxl-parsing.c

static void test_memslot_invalid_group_id(void)
{
    RedMemSlotInfo mem_info;
    init_meminfo(&mem_info);

    memslot_get_virt(&mem_info, 0, 16, 1);
}

Project: spice
File: memslot.c

static unsigned long __get_clean_virt(RedMemSlotInfo *info, QXLPHYSICAL addr)
{
    return addr & info->memslot_clean_virt_mask;
}

Project: spice
File: memslot.c

void memslot_info_init(RedMemSlotInfo *info,
                       uint32_t num_groups, uint32_t num_slots,
                       uint8_t generation_bits,
                       uint8_t id_bits,
                       uint8_t internal_groupslot_id)
{
    uint32_t i;

    spice_assert(num_slots > 0);
    spice_assert(num_groups > 0);

    info->num_memslots_groups = num_groups;
    info->num_memslots = num_slots;
    info->generation_bits = generation_bits;
    info->mem_slot_bits = id_bits;
    info->internal_groupslot_id = internal_groupslot_id;

    info->mem_slots = g_new(MemSlot *, num_groups);

    for (i = 0; i < num_groups; ++i) {
        info->mem_slots[i] = g_new0(MemSlot, num_slots);
    }

    /* TODO: use QXLPHYSICAL_BITS */
    info->memslot_id_shift = 64 - info->mem_slot_bits;
    info->memslot_gen_shift = 64 - (info->mem_slot_bits + info->generation_bits);
    info->memslot_gen_mask = ~((QXLPHYSICAL)-1 << info->generation_bits);
    info->memslot_clean_virt_mask = (((QXLPHYSICAL)(-1)) >>
                                       (info->mem_slot_bits + info->generation_bits));
}

Project: spice
File: test-qxl-parsing.c

create_chunk(size_t prefix, uint32_t size, QXLDataChunk* prev, int fill)
{
    uint8_t *ptr = g_malloc0(prefix + sizeof(QXLDataChunk) + size);
    QXLDataChunk *qxl = (QXLDataChunk *) (ptr + prefix);
    memset(&qxl->data[0], fill, size);
    qxl->data_size = size;
    qxl->prev_chunk = to_physical(prev);
    if (prev)
        prev->next_chunk = to_physical(qxl);
    return ptr;
}

Project: spice
File: test-qxl-parsing.c

static void test_circular_empty_chunks(void)
{
    RedMemSlotInfo mem_info;
    RedCursorCmd *red_cursor_cmd;
    QXLCursorCmd cursor_cmd;
    QXLCursor *cursor;
    QXLDataChunk *chunks[2];

    init_meminfo(&mem_info);
    g_test_expect_message(G_LOG_DOMAIN, G_LOG_LEVEL_WARNING,
                          "*red_get_data_chunks_ptr: data split in too many chunks, avoiding DoS*");

    /* a circular list of empty chunks should not be a problems */
    memset(&cursor_cmd, 0, sizeof(cursor_cmd));
    cursor_cmd.type = QXL_CURSOR_SET;

    cursor = create_chunk(SPICE_OFFSETOF(QXLCursor, chunk), 0, NULL, 0xaa);
    cursor->header.unique = 1;
    cursor->header.width = 128;
    cursor->header.height = 128;
    cursor->data_size = 128 * 128 * 4;

    chunks[0] = create_chunk(0, 0, &cursor->chunk, 0xaa);
    chunks[0]->next_chunk = to_physical(&cursor->chunk);

    cursor_cmd.u.set.shape = to_physical(cursor);

    red_cursor_cmd = red_cursor_cmd_new(NULL, &mem_info, 0, to_physical(&cursor_cmd));
    if (red_cursor_cmd != NULL) {
        /* function does not return errors so there should be no data */
        g_assert_cmpuint(red_cursor_cmd->type, ==, QXL_CURSOR_SET);
        g_assert_cmpuint(red_cursor_cmd->u.set.position.x, ==, 0);
        g_assert_cmpuint(red_cursor_cmd->u.set.position.y, ==, 0);
        g_assert_cmpuint(red_cursor_cmd->u.set.shape.data_size, ==, 0);
        red_cursor_cmd_unref(red_cursor_cmd);
    }
    g_test_assert_expected_messages();

    g_free(cursor);
    g_free(chunks[0]);
    memslot_info_destroy(&mem_info);
}

Project: spice
File: test-qxl-parsing.c

static void test_no_issues(void)
{
    RedMemSlotInfo mem_info;
    RedSurfaceCmd *cmd;
    QXLSurfaceCmd qxl;

    init_meminfo(&mem_info);
    init_qxl_surface(&qxl);

    /* try to create a surface with no issues, should succeed */
    cmd = red_surface_cmd_new(NULL, &mem_info, 0, to_physical(&qxl));
    g_assert_nonnull(cmd);
    red_surface_cmd_unref(cmd);

    deinit_qxl_surface(&qxl);
    memslot_info_destroy(&mem_info);
}

Project: spice
File: test-qxl-parsing.c

to_physical(const void *ptr)
{
    return (uintptr_t) ptr;
}

Project: spice
File: test-qxl-parsing.c

int main(int argc, char *argv[])
{
    g_test_init(&argc, &argv, NULL);

    /* try to use invalid memslot group/slot */
    g_test_add_func("/server/memslot-invalid-addresses", test_memslot_invalid_addresses);
    g_test_add_func("/server/memslot-invalid-addresses/subprocess/group_id", test_memslot_invalid_group_id);
    g_test_add_func("/server/memslot-invalid-addresses/subprocess/slot_id", test_memslot_invalid_slot_id);

    /* try to create a surface with no issues, should succeed */
    g_test_add_func("/server/qxl-parsing-no-issues", test_no_issues);

    /* try to create a surface with a stride too small to fit
     * the entire width.
     * This can be used to cause buffer overflows so refuse it.
     */
    g_test_add_func("/server/qxl-parsing/stride-too-small", test_stride_too_small);

    /* try to create a surface quite large.
     * The sizes (width and height) were chosen so the multiplication
     * using 32 bit values gives a very small value.
     * These kind of values should be refused as they will cause
     * overflows. Also the total memory for the card is not enough to
     * hold the surface so surely can't be accepted.
     */
    g_test_add_func("/server/qxl-parsing/too-big-image", test_too_big_image);

    /* test base cursor with no problems */
    g_test_add_func("/server/qxl-parsing/base-cursor-command", test_cursor_command);

    /* a circular list of empty chunks should not be a problems */
    g_test_add_func("/server/qxl-parsing/circular-empty-chunks", test_circular_empty_chunks);

    /* a circular list of small chunks should not be a problems */
    g_test_add_func("/server/qxl-parsing/circular-small-chunks", test_circular_small_chunks);

    return g_test_run();
}

Project: spice
File: test-qxl-parsing.c

static void test_memslot_invalid_addresses(void)
{
    g_test_trap_subprocess("/server/memslot-invalid-addresses/subprocess/group_id", 0, 0);
    g_test_trap_assert_stderr("*group_id too big*");

    g_test_trap_subprocess("/server/memslot-invalid-addresses/subprocess/slot_id", 0, 0);
    g_test_trap_assert_stderr("*slot_id 1 too big*");
}

Project: spice
File: memslot.c

int memslot_validate_virt(RedMemSlotInfo *info, unsigned long virt, int slot_id,
                          uint32_t add_size, uint32_t group_id)
{
    MemSlot *slot;

    slot = &info->mem_slots[group_id][slot_id];
    if ((virt + add_size) < virt) {
        spice_critical("virtual address overlap");
        return 0;
    }

    if (virt < slot->virt_start_addr || (virt + add_size) > slot->virt_end_addr) {
        print_memslots(info);
        spice_warning("virtual address out of range"
              "    virt=0x%lx+0x%x slot_id=%d group_id=%d\n"
              "    slot=0x%lx-0x%lx delta=0x%lx",
              virt, add_size, slot_id, group_id,
              slot->virt_start_addr, slot->virt_end_addr, slot->address_delta);
        return 0;
    }
    return 1;
}

Project: spice
File: memslot.c

unsigned long memslot_max_size_virt(RedMemSlotInfo *info,
                                    unsigned long virt, int slot_id,
                                    uint32_t group_id)
{
    MemSlot *slot;

    slot = &info->mem_slots[group_id][slot_id];

    if (virt < slot->virt_start_addr || virt > slot->virt_end_addr) {
        return 0;
    }
    return slot->virt_end_addr - virt;
}

Project: spice
File: memslot.c

static void print_memslots(RedMemSlotInfo *info)
{
    int i;
    int x;

    for (i = 0; i < info->num_memslots_groups; ++i) {
        for (x = 0; x < info->num_memslots; ++x) {
            if (!info->mem_slots[i][x].virt_start_addr &&
                !info->mem_slots[i][x].virt_end_addr) {
                continue;
            }
            printf("id %d, group %d, virt start %lx, virt end %lx, generation %u, delta %lx\n",
                   x, i, info->mem_slots[i][x].virt_start_addr,
                   info->mem_slots[i][x].virt_end_addr, info->mem_slots[i][x].generation,
                   info->mem_slots[i][x].address_delta);
            }
    }
}

Project: spice
File: test-qxl-parsing.c

static void test_cursor_command(void)
{
    RedMemSlotInfo mem_info;
    RedCursorCmd *red_cursor_cmd;
    QXLCursorCmd cursor_cmd;
    QXLCursor *cursor;

    init_meminfo(&mem_info);

    /* test base cursor with no problems */
    memset(&cursor_cmd, 0, sizeof(cursor_cmd));
    cursor_cmd.type = QXL_CURSOR_SET;

    cursor = create_chunk(SPICE_OFFSETOF(QXLCursor, chunk), 128 * 128 * 4, NULL, 0xaa);
    cursor->header.unique = 1;
    cursor->header.width = 128;
    cursor->header.height = 128;
    cursor->data_size = 128 * 128 * 4;

    cursor_cmd.u.set.shape = to_physical(cursor);

    red_cursor_cmd = red_cursor_cmd_new(NULL, &mem_info, 0, to_physical(&cursor_cmd));
    g_assert_nonnull(red_cursor_cmd);
    red_cursor_cmd_unref(red_cursor_cmd);
    g_free(cursor);
    memslot_info_destroy(&mem_info);
}

Project: spice
File: test-qxl-parsing.c

static void init_qxl_surface(QXLSurfaceCmd *qxl)
{
    void *surface_mem;

    memset(qxl, 0, sizeof(*qxl));

    qxl->surface_id = 123;

    qxl->u.surface_create.format = SPICE_SURFACE_FMT_32_xRGB;
    qxl->u.surface_create.width = 128;
    qxl->u.surface_create.stride = 512;
    qxl->u.surface_create.height = 128;
    surface_mem = g_malloc(0x10000);
    qxl->u.surface_create.data = to_physical(surface_mem);
}

Project: spice
File: test-qxl-parsing.c

static void test_too_big_image(void)
{
    RedMemSlotInfo mem_info;
    RedSurfaceCmd *cmd;
    QXLSurfaceCmd qxl;

    init_meminfo(&mem_info);
    init_qxl_surface(&qxl);

    /* try to create a surface quite large.
     * The sizes (width and height) were chosen so the multiplication
     * using 32 bit values gives a very small value.
     * These kind of values should be refused as they will cause
     * overflows. Also the total memory for the card is not enough to
     * hold the surface so surely can't be accepted.
     */
    qxl.u.surface_create.stride = 0x08000004 * 4;
    qxl.u.surface_create.width = 0x08000004;
    qxl.u.surface_create.height = 0x40000020;
    cmd = red_surface_cmd_new(NULL, &mem_info, 0, to_physical(&qxl));
    g_assert_null(cmd);

    deinit_qxl_surface(&qxl);
    memslot_info_destroy(&mem_info);
}

Project: spice
File: test-qxl-parsing.c

static void init_meminfo(RedMemSlotInfo *mem_info)
{
    memslot_info_init(mem_info, 1 /* groups */, 1 /* slots */, 1, 1, 0);
    memslot_info_add_slot(mem_info, 0, 0, 0 /* delta */, 0 /* start */, ~0ul /* end */, 0 /* generation */);
}

Project: spice
File: memslot.c

void *memslot_get_virt(RedMemSlotInfo *info, QXLPHYSICAL addr, uint32_t add_size,
                       int group_id)
{
    int slot_id;
    int generation;
    unsigned long h_virt;

    MemSlot *slot;

    if (group_id >= info->num_memslots_groups) {
        spice_critical("group_id too big");
        return NULL;
    }

    slot_id = memslot_get_id(info, addr);
    if (slot_id >= info->num_memslots) {
        print_memslots(info);
        spice_critical("slot_id %d too big, addr=%" PRIx64, slot_id, addr);
        return NULL;
    }

    slot = &info->mem_slots[group_id][slot_id];

    generation = memslot_get_generation(info, addr);
    if (generation != slot->generation) {
        print_memslots(info);
        spice_critical("address generation is not valid, group_id %d, slot_id %d, "
                       "gen %d, slot_gen %d",
                       group_id, slot_id,
                       generation, slot->generation);
        return NULL;
    }

    h_virt = __get_clean_virt(info, addr);
    h_virt += slot->address_delta;

    if (!memslot_validate_virt(info, h_virt, slot_id, add_size, group_id)) {
        return NULL;
    }

    return (void*)(uintptr_t)h_virt;
}

Project: spice
File: memslot.c

void memslot_info_add_slot(RedMemSlotInfo *info, uint32_t slot_group_id, uint32_t slot_id,
                           uint64_t addr_delta, unsigned long virt_start, unsigned long virt_end,
                           uint32_t generation)
{
    spice_assert(info->num_memslots_groups > slot_group_id);
    spice_assert(info->num_memslots > slot_id);

    info->mem_slots[slot_group_id][slot_id].address_delta = addr_delta;
    info->mem_slots[slot_group_id][slot_id].virt_start_addr = virt_start;
    info->mem_slots[slot_group_id][slot_id].virt_end_addr = virt_end;
    info->mem_slots[slot_group_id][slot_id].generation = generation;
}

Project: spice
File: test-qxl-parsing.c

static void test_stride_too_small(void)
{
    RedMemSlotInfo mem_info;
    RedSurfaceCmd *cmd;
    QXLSurfaceCmd qxl;

    init_meminfo(&mem_info);
    init_qxl_surface(&qxl);

    /* try to create a surface with a stride too small to fit
     * the entire width.
     * This can be used to cause buffer overflows so refuse it.
     */
    qxl.u.surface_create.stride = 256;
    cmd = red_surface_cmd_new(NULL, &mem_info, 0, to_physical(&qxl));
    g_assert_null(cmd);

    deinit_qxl_surface(&qxl);
    memslot_info_destroy(&mem_info);
}