diff mbox series

[RFC,PATCH-for-7.2,4/5] hw/display/qxl: Avoid buffer overrun in qxl_phys2virt (CVE-2022-4144)

Message ID 20221128134832.84867-5-philmd@linaro.org
State New
Headers show
Series hw/display/qxl: Avoid buffer overrun in qxl_phys2virt() | expand

Commit Message

Philippe Mathieu-Daudé Nov. 28, 2022, 1:48 p.m. UTC
Have qxl_get_check_slot_offset() return false if the requested
buffer size does not fit within the slot memory region.

Similarly qxl_phys2virt() now returns NULL in such case, and
qxl_dirty_one_surface() aborts.

This avoids buffer overrun in the host pointer returned by
memory_region_get_ram_ptr().

Fixes: CVE-2022-4144 (out-of-bounds read)
Reported-by: Wenxu Yin (@awxylitol)
Resolves: https://gitlab.com/qemu-project/qemu/-/issues/1336
Signed-off-by: Philippe Mathieu-Daudé <philmd@linaro.org>
---
 hw/display/qxl.c | 22 ++++++++++++++++++----
 hw/display/qxl.h |  2 +-
 2 files changed, 19 insertions(+), 5 deletions(-)

Comments

Stefan Hajnoczi Nov. 28, 2022, 3:16 p.m. UTC | #1
On Mon, 28 Nov 2022 at 08:53, Philippe Mathieu-Daudé <philmd@linaro.org> wrote:
>
> Have qxl_get_check_slot_offset() return false if the requested
> buffer size does not fit within the slot memory region.
>
> Similarly qxl_phys2virt() now returns NULL in such case, and
> qxl_dirty_one_surface() aborts.
>
> This avoids buffer overrun in the host pointer returned by
> memory_region_get_ram_ptr().
>
> Fixes: CVE-2022-4144 (out-of-bounds read)
> Reported-by: Wenxu Yin (@awxylitol)
> Resolves: https://gitlab.com/qemu-project/qemu/-/issues/1336
> Signed-off-by: Philippe Mathieu-Daudé <philmd@linaro.org>
> ---
>  hw/display/qxl.c | 22 ++++++++++++++++++----
>  hw/display/qxl.h |  2 +-
>  2 files changed, 19 insertions(+), 5 deletions(-)
>
> diff --git a/hw/display/qxl.c b/hw/display/qxl.c
> index 231d733250..afa157d327 100644
> --- a/hw/display/qxl.c
> +++ b/hw/display/qxl.c
> @@ -1424,11 +1424,13 @@ static void qxl_reset_surfaces(PCIQXLDevice *d)
>
>  /* can be also called from spice server thread context */
>  static bool qxl_get_check_slot_offset(PCIQXLDevice *qxl, QXLPHYSICAL pqxl,
> -                                      uint32_t *s, uint64_t *o)
> +                                      uint32_t *s, uint64_t *o,
> +                                      size_t size_requested)
>  {
>      uint64_t phys   = le64_to_cpu(pqxl);
>      uint32_t slot   = (phys >> (64 -  8)) & 0xff;
>      uint64_t offset = phys & 0xffffffffffff;
> +    uint64_t size_available;
>
>      if (slot >= NUM_MEMSLOTS) {
>          qxl_set_guest_bug(qxl, "slot too large %d >= %d", slot,
> @@ -1453,6 +1455,18 @@ static bool qxl_get_check_slot_offset(PCIQXLDevice *qxl, QXLPHYSICAL pqxl,
>          return false;
>      }
>
> +    size_available = memory_region_size(qxl->guest_slots[slot].mr);
> +    assert(qxl->guest_slots[slot].offset + offset < size_available);

Can this assertion be triggered by the guest (via an invalid pqxl
value)? I think the answer is no, but I don't know the the qxl code
well enough to be sure.

> +    size_available -= qxl->guest_slots[slot].offset + offset;
> +    if (size_requested > size_available) {
> +        qxl_set_guest_bug(qxl,
> +                          "slot %d offset %"PRIu64" size %zu: "
> +                          "overrun by %"PRIu64" bytes\n",
> +                          slot, offset, size_requested,
> +                          size_requested - size_available);
> +        return false;
> +    }
> +
>      *s = slot;
>      *o = offset;
>      return true;
> @@ -1471,7 +1485,7 @@ void *qxl_phys2virt(PCIQXLDevice *qxl, QXLPHYSICAL pqxl, int group_id,
>          offset = le64_to_cpu(pqxl) & 0xffffffffffff;
>          return (void *)(intptr_t)offset;
>      case MEMSLOT_GROUP_GUEST:
> -        if (!qxl_get_check_slot_offset(qxl, pqxl, &slot, &offset)) {
> +        if (!qxl_get_check_slot_offset(qxl, pqxl, &slot, &offset, size)) {
>              return NULL;
>          }
>          ptr = memory_region_get_ram_ptr(qxl->guest_slots[slot].mr);
> @@ -1937,9 +1951,9 @@ static void qxl_dirty_one_surface(PCIQXLDevice *qxl, QXLPHYSICAL pqxl,
>      uint32_t slot;
>      bool rc;
>
> -    rc = qxl_get_check_slot_offset(qxl, pqxl, &slot, &offset);
> -    assert(rc == true);
>      size = (uint64_t)height * abs(stride);
> +    rc = qxl_get_check_slot_offset(qxl, pqxl, &slot, &offset, size);
> +    assert(rc == true);
>      trace_qxl_surfaces_dirty(qxl->id, offset, size);
>      qxl_set_dirty(qxl->guest_slots[slot].mr,
>                    qxl->guest_slots[slot].offset + offset,
> diff --git a/hw/display/qxl.h b/hw/display/qxl.h
> index bf03138ab4..7894bd5134 100644
> --- a/hw/display/qxl.h
> +++ b/hw/display/qxl.h
> @@ -157,7 +157,7 @@ OBJECT_DECLARE_SIMPLE_TYPE(PCIQXLDevice, PCI_QXL)
>   *
>   * Returns a host pointer to a buffer placed at offset @phys within the
>   * active slot @group_id of the PCI VGA RAM memory region associated with
> - * the @qxl device. If the slot is inactive, or the offset is out
> + * the @qxl device. If the slot is inactive, or the offset + size are out
>   * of the memory region, returns NULL.
>   *
>   * Use with care; by the time this function returns, the returned pointer is
> --
> 2.38.1
>
>
Philippe Mathieu-Daudé Nov. 28, 2022, 3:25 p.m. UTC | #2
On 28/11/22 16:16, Stefan Hajnoczi wrote:
> On Mon, 28 Nov 2022 at 08:53, Philippe Mathieu-Daudé <philmd@linaro.org> wrote:
>>
>> Have qxl_get_check_slot_offset() return false if the requested
>> buffer size does not fit within the slot memory region.
>>
>> Similarly qxl_phys2virt() now returns NULL in such case, and
>> qxl_dirty_one_surface() aborts.
>>
>> This avoids buffer overrun in the host pointer returned by
>> memory_region_get_ram_ptr().
>>
>> Fixes: CVE-2022-4144 (out-of-bounds read)
>> Reported-by: Wenxu Yin (@awxylitol)
>> Resolves: https://gitlab.com/qemu-project/qemu/-/issues/1336
>> Signed-off-by: Philippe Mathieu-Daudé <philmd@linaro.org>
>> ---
>>   hw/display/qxl.c | 22 ++++++++++++++++++----
>>   hw/display/qxl.h |  2 +-
>>   2 files changed, 19 insertions(+), 5 deletions(-)
>>
>> diff --git a/hw/display/qxl.c b/hw/display/qxl.c
>> index 231d733250..afa157d327 100644
>> --- a/hw/display/qxl.c
>> +++ b/hw/display/qxl.c
>> @@ -1424,11 +1424,13 @@ static void qxl_reset_surfaces(PCIQXLDevice *d)
>>
>>   /* can be also called from spice server thread context */
>>   static bool qxl_get_check_slot_offset(PCIQXLDevice *qxl, QXLPHYSICAL pqxl,
>> -                                      uint32_t *s, uint64_t *o)
>> +                                      uint32_t *s, uint64_t *o,
>> +                                      size_t size_requested)
>>   {
>>       uint64_t phys   = le64_to_cpu(pqxl);
>>       uint32_t slot   = (phys >> (64 -  8)) & 0xff;
>>       uint64_t offset = phys & 0xffffffffffff;
>> +    uint64_t size_available;
>>
>>       if (slot >= NUM_MEMSLOTS) {
>>           qxl_set_guest_bug(qxl, "slot too large %d >= %d", slot,
>> @@ -1453,6 +1455,18 @@ static bool qxl_get_check_slot_offset(PCIQXLDevice *qxl, QXLPHYSICAL pqxl,
>>           return false;
>>       }
>>
>> +    size_available = memory_region_size(qxl->guest_slots[slot].mr);
>> +    assert(qxl->guest_slots[slot].offset + offset < size_available);
> 
> Can this assertion be triggered by the guest (via an invalid pqxl
> value)? I think the answer is no, but I don't know the the qxl code
> well enough to be sure.

'qxl->guest_slots[slot].offset' is initialized in qxl_add_memslot()
(host); 'size_available' also comes from the host, but 'offset'
comes from the guest via 'QXLPHYSICAL pqxl' IIUC.

I added this check to avoid overflow, but it can be changed to return
an error.

Thanks,

Phil.
Stefan Hajnoczi Nov. 28, 2022, 3:32 p.m. UTC | #3
On Mon, 28 Nov 2022 at 10:25, Philippe Mathieu-Daudé <philmd@linaro.org> wrote:
>
> On 28/11/22 16:16, Stefan Hajnoczi wrote:
> > On Mon, 28 Nov 2022 at 08:53, Philippe Mathieu-Daudé <philmd@linaro.org> wrote:
> >>
> >> Have qxl_get_check_slot_offset() return false if the requested
> >> buffer size does not fit within the slot memory region.
> >>
> >> Similarly qxl_phys2virt() now returns NULL in such case, and
> >> qxl_dirty_one_surface() aborts.
> >>
> >> This avoids buffer overrun in the host pointer returned by
> >> memory_region_get_ram_ptr().
> >>
> >> Fixes: CVE-2022-4144 (out-of-bounds read)
> >> Reported-by: Wenxu Yin (@awxylitol)
> >> Resolves: https://gitlab.com/qemu-project/qemu/-/issues/1336
> >> Signed-off-by: Philippe Mathieu-Daudé <philmd@linaro.org>
> >> ---
> >>   hw/display/qxl.c | 22 ++++++++++++++++++----
> >>   hw/display/qxl.h |  2 +-
> >>   2 files changed, 19 insertions(+), 5 deletions(-)
> >>
> >> diff --git a/hw/display/qxl.c b/hw/display/qxl.c
> >> index 231d733250..afa157d327 100644
> >> --- a/hw/display/qxl.c
> >> +++ b/hw/display/qxl.c
> >> @@ -1424,11 +1424,13 @@ static void qxl_reset_surfaces(PCIQXLDevice *d)
> >>
> >>   /* can be also called from spice server thread context */
> >>   static bool qxl_get_check_slot_offset(PCIQXLDevice *qxl, QXLPHYSICAL pqxl,
> >> -                                      uint32_t *s, uint64_t *o)
> >> +                                      uint32_t *s, uint64_t *o,
> >> +                                      size_t size_requested)
> >>   {
> >>       uint64_t phys   = le64_to_cpu(pqxl);
> >>       uint32_t slot   = (phys >> (64 -  8)) & 0xff;
> >>       uint64_t offset = phys & 0xffffffffffff;
> >> +    uint64_t size_available;
> >>
> >>       if (slot >= NUM_MEMSLOTS) {
> >>           qxl_set_guest_bug(qxl, "slot too large %d >= %d", slot,
> >> @@ -1453,6 +1455,18 @@ static bool qxl_get_check_slot_offset(PCIQXLDevice *qxl, QXLPHYSICAL pqxl,
> >>           return false;
> >>       }
> >>
> >> +    size_available = memory_region_size(qxl->guest_slots[slot].mr);
> >> +    assert(qxl->guest_slots[slot].offset + offset < size_available);
> >
> > Can this assertion be triggered by the guest (via an invalid pqxl
> > value)? I think the answer is no, but I don't know the the qxl code
> > well enough to be sure.
>
> 'qxl->guest_slots[slot].offset' is initialized in qxl_add_memslot()
> (host); 'size_available' also comes from the host, but 'offset'
> comes from the guest via 'QXLPHYSICAL pqxl' IIUC.
>
> I added this check to avoid overflow, but it can be changed to return
> an error.

Yes, please.

Aside from concerns about -DNDEBUG, which builds without assertions,
there is also a DoS issue with nested virt where an L2 guest shouldn't
be able to abort the L1 guest's QEMU by triggering an assertion in a
pass through device.

Guest input validation should use explicit error checking code instead
of assert(3).

Thanks,
Stefan
Philippe Mathieu-Daudé Nov. 28, 2022, 3:46 p.m. UTC | #4
On 28/11/22 16:32, Stefan Hajnoczi wrote:
> On Mon, 28 Nov 2022 at 10:25, Philippe Mathieu-Daudé <philmd@linaro.org> wrote:
>>
>> On 28/11/22 16:16, Stefan Hajnoczi wrote:
>>> On Mon, 28 Nov 2022 at 08:53, Philippe Mathieu-Daudé <philmd@linaro.org> wrote:
>>>>
>>>> Have qxl_get_check_slot_offset() return false if the requested
>>>> buffer size does not fit within the slot memory region.
>>>>
>>>> Similarly qxl_phys2virt() now returns NULL in such case, and
>>>> qxl_dirty_one_surface() aborts.
>>>>
>>>> This avoids buffer overrun in the host pointer returned by
>>>> memory_region_get_ram_ptr().
>>>>
>>>> Fixes: CVE-2022-4144 (out-of-bounds read)
>>>> Reported-by: Wenxu Yin (@awxylitol)
>>>> Resolves: https://gitlab.com/qemu-project/qemu/-/issues/1336
>>>> Signed-off-by: Philippe Mathieu-Daudé <philmd@linaro.org>
>>>> ---
>>>>    hw/display/qxl.c | 22 ++++++++++++++++++----
>>>>    hw/display/qxl.h |  2 +-
>>>>    2 files changed, 19 insertions(+), 5 deletions(-)
>>>>
>>>> diff --git a/hw/display/qxl.c b/hw/display/qxl.c
>>>> index 231d733250..afa157d327 100644
>>>> --- a/hw/display/qxl.c
>>>> +++ b/hw/display/qxl.c
>>>> @@ -1424,11 +1424,13 @@ static void qxl_reset_surfaces(PCIQXLDevice *d)
>>>>
>>>>    /* can be also called from spice server thread context */
>>>>    static bool qxl_get_check_slot_offset(PCIQXLDevice *qxl, QXLPHYSICAL pqxl,
>>>> -                                      uint32_t *s, uint64_t *o)
>>>> +                                      uint32_t *s, uint64_t *o,
>>>> +                                      size_t size_requested)
>>>>    {
>>>>        uint64_t phys   = le64_to_cpu(pqxl);
>>>>        uint32_t slot   = (phys >> (64 -  8)) & 0xff;
>>>>        uint64_t offset = phys & 0xffffffffffff;
>>>> +    uint64_t size_available;
>>>>
>>>>        if (slot >= NUM_MEMSLOTS) {
>>>>            qxl_set_guest_bug(qxl, "slot too large %d >= %d", slot,
>>>> @@ -1453,6 +1455,18 @@ static bool qxl_get_check_slot_offset(PCIQXLDevice *qxl, QXLPHYSICAL pqxl,
>>>>            return false;
>>>>        }
>>>>
>>>> +    size_available = memory_region_size(qxl->guest_slots[slot].mr);
>>>> +    assert(qxl->guest_slots[slot].offset + offset < size_available);
>>>
>>> Can this assertion be triggered by the guest (via an invalid pqxl
>>> value)? I think the answer is no, but I don't know the the qxl code
>>> well enough to be sure.
>>
>> 'qxl->guest_slots[slot].offset' is initialized in qxl_add_memslot()
>> (host); 'size_available' also comes from the host, but 'offset'
>> comes from the guest via 'QXLPHYSICAL pqxl' IIUC.
>>
>> I added this check to avoid overflow, but it can be changed to return
>> an error.
> 
> Yes, please.

Or I could use Int128 to do arithmetic, but various other places do it
this way without checking overflow with memory_region_size(). Such API
change should be global and is out of the scope of this CVE fix IMO.

> Aside from concerns about -DNDEBUG, which builds without assertions,

This isn't an issue anymore since 262a69f428 ("osdep.h: Prohibit 
disabling assert() in supported builds").

> there is also a DoS issue with nested virt where an L2 guest shouldn't
> be able to abort the L1 guest's QEMU by triggering an assertion in a
> pass through device.
> 
> Guest input validation should use explicit error checking code instead
> of assert(3).

Certainly.
Stefan Hajnoczi Nov. 28, 2022, 3:48 p.m. UTC | #5
On Mon, 28 Nov 2022 at 10:46, Philippe Mathieu-Daudé <philmd@linaro.org> wrote:
>
> On 28/11/22 16:32, Stefan Hajnoczi wrote:
> > On Mon, 28 Nov 2022 at 10:25, Philippe Mathieu-Daudé <philmd@linaro.org> wrote:
> >>
> >> On 28/11/22 16:16, Stefan Hajnoczi wrote:
> >>> On Mon, 28 Nov 2022 at 08:53, Philippe Mathieu-Daudé <philmd@linaro.org> wrote:
> >>>>
> >>>> Have qxl_get_check_slot_offset() return false if the requested
> >>>> buffer size does not fit within the slot memory region.
> >>>>
> >>>> Similarly qxl_phys2virt() now returns NULL in such case, and
> >>>> qxl_dirty_one_surface() aborts.
> >>>>
> >>>> This avoids buffer overrun in the host pointer returned by
> >>>> memory_region_get_ram_ptr().
> >>>>
> >>>> Fixes: CVE-2022-4144 (out-of-bounds read)
> >>>> Reported-by: Wenxu Yin (@awxylitol)
> >>>> Resolves: https://gitlab.com/qemu-project/qemu/-/issues/1336
> >>>> Signed-off-by: Philippe Mathieu-Daudé <philmd@linaro.org>
> >>>> ---
> >>>>    hw/display/qxl.c | 22 ++++++++++++++++++----
> >>>>    hw/display/qxl.h |  2 +-
> >>>>    2 files changed, 19 insertions(+), 5 deletions(-)
> >>>>
> >>>> diff --git a/hw/display/qxl.c b/hw/display/qxl.c
> >>>> index 231d733250..afa157d327 100644
> >>>> --- a/hw/display/qxl.c
> >>>> +++ b/hw/display/qxl.c
> >>>> @@ -1424,11 +1424,13 @@ static void qxl_reset_surfaces(PCIQXLDevice *d)
> >>>>
> >>>>    /* can be also called from spice server thread context */
> >>>>    static bool qxl_get_check_slot_offset(PCIQXLDevice *qxl, QXLPHYSICAL pqxl,
> >>>> -                                      uint32_t *s, uint64_t *o)
> >>>> +                                      uint32_t *s, uint64_t *o,
> >>>> +                                      size_t size_requested)
> >>>>    {
> >>>>        uint64_t phys   = le64_to_cpu(pqxl);
> >>>>        uint32_t slot   = (phys >> (64 -  8)) & 0xff;
> >>>>        uint64_t offset = phys & 0xffffffffffff;
> >>>> +    uint64_t size_available;
> >>>>
> >>>>        if (slot >= NUM_MEMSLOTS) {
> >>>>            qxl_set_guest_bug(qxl, "slot too large %d >= %d", slot,
> >>>> @@ -1453,6 +1455,18 @@ static bool qxl_get_check_slot_offset(PCIQXLDevice *qxl, QXLPHYSICAL pqxl,
> >>>>            return false;
> >>>>        }
> >>>>
> >>>> +    size_available = memory_region_size(qxl->guest_slots[slot].mr);
> >>>> +    assert(qxl->guest_slots[slot].offset + offset < size_available);
> >>>
> >>> Can this assertion be triggered by the guest (via an invalid pqxl
> >>> value)? I think the answer is no, but I don't know the the qxl code
> >>> well enough to be sure.
> >>
> >> 'qxl->guest_slots[slot].offset' is initialized in qxl_add_memslot()
> >> (host); 'size_available' also comes from the host, but 'offset'
> >> comes from the guest via 'QXLPHYSICAL pqxl' IIUC.
> >>
> >> I added this check to avoid overflow, but it can be changed to return
> >> an error.
> >
> > Yes, please.
>
> Or I could use Int128 to do arithmetic, but various other places do it
> this way without checking overflow with memory_region_size(). Such API
> change should be global and is out of the scope of this CVE fix IMO.
>
> > Aside from concerns about -DNDEBUG, which builds without assertions,
>
> This isn't an issue anymore since 262a69f428 ("osdep.h: Prohibit
> disabling assert() in supported builds").

I didn't know about that. Thanks!

Stefan
diff mbox series

Patch

diff --git a/hw/display/qxl.c b/hw/display/qxl.c
index 231d733250..afa157d327 100644
--- a/hw/display/qxl.c
+++ b/hw/display/qxl.c
@@ -1424,11 +1424,13 @@  static void qxl_reset_surfaces(PCIQXLDevice *d)
 
 /* can be also called from spice server thread context */
 static bool qxl_get_check_slot_offset(PCIQXLDevice *qxl, QXLPHYSICAL pqxl,
-                                      uint32_t *s, uint64_t *o)
+                                      uint32_t *s, uint64_t *o,
+                                      size_t size_requested)
 {
     uint64_t phys   = le64_to_cpu(pqxl);
     uint32_t slot   = (phys >> (64 -  8)) & 0xff;
     uint64_t offset = phys & 0xffffffffffff;
+    uint64_t size_available;
 
     if (slot >= NUM_MEMSLOTS) {
         qxl_set_guest_bug(qxl, "slot too large %d >= %d", slot,
@@ -1453,6 +1455,18 @@  static bool qxl_get_check_slot_offset(PCIQXLDevice *qxl, QXLPHYSICAL pqxl,
         return false;
     }
 
+    size_available = memory_region_size(qxl->guest_slots[slot].mr);
+    assert(qxl->guest_slots[slot].offset + offset < size_available);
+    size_available -= qxl->guest_slots[slot].offset + offset;
+    if (size_requested > size_available) {
+        qxl_set_guest_bug(qxl,
+                          "slot %d offset %"PRIu64" size %zu: "
+                          "overrun by %"PRIu64" bytes\n",
+                          slot, offset, size_requested,
+                          size_requested - size_available);
+        return false;
+    }
+
     *s = slot;
     *o = offset;
     return true;
@@ -1471,7 +1485,7 @@  void *qxl_phys2virt(PCIQXLDevice *qxl, QXLPHYSICAL pqxl, int group_id,
         offset = le64_to_cpu(pqxl) & 0xffffffffffff;
         return (void *)(intptr_t)offset;
     case MEMSLOT_GROUP_GUEST:
-        if (!qxl_get_check_slot_offset(qxl, pqxl, &slot, &offset)) {
+        if (!qxl_get_check_slot_offset(qxl, pqxl, &slot, &offset, size)) {
             return NULL;
         }
         ptr = memory_region_get_ram_ptr(qxl->guest_slots[slot].mr);
@@ -1937,9 +1951,9 @@  static void qxl_dirty_one_surface(PCIQXLDevice *qxl, QXLPHYSICAL pqxl,
     uint32_t slot;
     bool rc;
 
-    rc = qxl_get_check_slot_offset(qxl, pqxl, &slot, &offset);
-    assert(rc == true);
     size = (uint64_t)height * abs(stride);
+    rc = qxl_get_check_slot_offset(qxl, pqxl, &slot, &offset, size);
+    assert(rc == true);
     trace_qxl_surfaces_dirty(qxl->id, offset, size);
     qxl_set_dirty(qxl->guest_slots[slot].mr,
                   qxl->guest_slots[slot].offset + offset,
diff --git a/hw/display/qxl.h b/hw/display/qxl.h
index bf03138ab4..7894bd5134 100644
--- a/hw/display/qxl.h
+++ b/hw/display/qxl.h
@@ -157,7 +157,7 @@  OBJECT_DECLARE_SIMPLE_TYPE(PCIQXLDevice, PCI_QXL)
  *
  * Returns a host pointer to a buffer placed at offset @phys within the
  * active slot @group_id of the PCI VGA RAM memory region associated with
- * the @qxl device. If the slot is inactive, or the offset is out
+ * the @qxl device. If the slot is inactive, or the offset + size are out
  * of the memory region, returns NULL.
  *
  * Use with care; by the time this function returns, the returned pointer is