CVE: CVE-2020-2748

Tested Versions:

  • Oracle VirtualBox 6.1.0 r135406

Product URL(s):

Description of the vulnerability

VirtualBox is a x86 and AMD64/Intel64 virtualization product for enterprise as well as home use. It is a solution commercially supported by Oracle, in addition to being made available as open source software. It runs on various host platforms like Windows, Linux, Mac and Solaris and also supports a large number of guest operating systems.

VboxSVGA is the default Video Adapter for Windows guests. The vulnerability occurs while processing cursor updates from the SVGA FIFO buffer, which the guest can write to.

This flaw lies within in the SVGA FIFO code. The FIFO buffer can be mapped and controlled by a guest with high-privileged code.

When certain conditions are fulfilled (fEnabled and fConfigured flags enabled):

// src/VBox/Devices/Graphics/DevVGA-SVGA.cpp:3580
/*
 * The device must be enabled and configured.
 */
if (   !pThis->svga.fEnabled
    || !pThis->svga.fConfigured)
{
    vmsvgaR3FifoSetNotBusy(pDevIns, pThis, pThisCC, pSVGAState, pFIFO[SVGA_FIFO_MIN]);
    fBadOrDisabledFifo = true;
    cMsSleep           = cMsMaxSleep; /* cheat */
    continue;
}

eventually the following code will be run:

// src/VBox/Devices/Graphics/DevVGA-SVGA.cpp:3624
/*
 * Update the cursor position before we start on the FIFO commands.
 */
/** @todo do we need to check whether the guest disabled the SVGA_FIFO_CAP_CURSOR_BYPASS_3 capability here? */
if (VMSVGA_IS_VALID_FIFO_REG(SVGA_FIFO_CURSOR_LAST_UPDATED, offFifoMin))
{
    uint32_t const uCursorUpdateCount = pFIFO[SVGA_FIFO_CURSOR_COUNT];
    if (uCursorUpdateCount == pThis->svga.uLastCursorUpdateCount)
    { /* halfways likely */ }
    else
    {
        uint32_t const uNewCount = vmsvgaR3FifoUpdateCursor(pThisCC, pSVGAState, pFIFO, offFifoMin, uCursorUpdateCount,
                                                            &xLastCursor, &yLastCursor, &fLastCursorVisible);
        ASMAtomicWriteU32(&pThis->svga.uLastCursorUpdateCount, uNewCount);
    }
}

By setting pFIFO[SVGA_FIFO_CURSOR_COUNT] to a value different from pThis->svga.uLastCursorUpdateCount, we can reach the code path to call vmsvgaR3FifoUpdateCursor.

// src/VBox/Devices/Graphics/DevVGA-SVGA.cpp:3273
/**
 * Sends cursor position and visibility information from the FIFO to the front-end.
 * @returns SVGA_FIFO_CURSOR_COUNT value used.
 */
static uint32_t
vmsvgaR3FifoUpdateCursor(PVGASTATECC pThisCC, PVMSVGAR3STATE  pSVGAState, uint32_t RT_UNTRUSTED_VOLATILE_GUEST *pFIFO,
                         uint32_t offFifoMin,  uint32_t uCursorUpdateCount,
                         uint32_t *pxLast, uint32_t *pyLast, uint32_t *pfLastVisible)
{
    ...
    for (uint32_t i = 0; ; i++)
    {
        x        = pFIFO[SVGA_FIFO_CURSOR_X];
        y        = pFIFO[SVGA_FIFO_CURSOR_Y];
        fVisible = pFIFO[SVGA_FIFO_CURSOR_ON];
        idScreen = VMSVGA_IS_VALID_FIFO_REG(SVGA_FIFO_CURSOR_SCREEN_ID, offFifoMin)
                 ? pFIFO[SVGA_FIFO_CURSOR_SCREEN_ID] : SVGA_ID_INVALID;
        /* 
         * pFIFO[SVGA_FIFO_CURSOR_SCREEN_ID] can be set by guest
         * to any value other than SVGA_ID_INVALID (0xFFFFFFFF)
         */
        ...
    }

    ...
    if (   *pxLast == x
        && *pyLast == y
        && (idScreen != SVGA_ID_INVALID || *pfLastVisible == fVisible))
        STAM_REL_COUNTER_INC(&pSVGAState->StatFifoCursorNoChange);
    else
    {
        ...
        if (idScreen != SVGA_ID_INVALID)
            fFlags |= VBVA_CURSOR_SCREEN_RELATIVE;
        else if (*pfLastVisible != fVisible)
        {
            LogRel2(("vmsvgaR3FifoUpdateCursor: fVisible %d fLastVisible %d (%d,%d)\n", fVisible, *pfLastVisible, x, y));
            *pfLastVisible = fVisible;
            pThisCC->pDrv->pfnVBVAMousePointerShape(pThisCC->pDrv, RT_BOOL(fVisible), false, 0, 0, 0, 0, NULL);
            STAM_REL_COUNTER_INC(&pSVGAState->StatFifoCursorVisiblity);
        }
        pThisCC->pDrv->pfnVBVAReportCursorPosition(pThisCC->pDrv, fFlags, idScreen, x, y); /* idScreen controlled */
        STAM_REL_COUNTER_INC(&pSVGAState->StatFifoCursorPosition);
    }

    ...

    return uCursorUpdateCount;
}

Within vmsvgaR3FifoUpdateCursor, the value of idScreen comes from the FIFO buffer which the guest has full control of. By updating pFIFO[SVGA_FIFO_CURSOR_X] or pFIFO[SVGA_FIFO_CURSOR_Y] to a new value, we can eventually reach the vulnerable call to pThisCC->pDrv->pfnVBVAReportCursorPosition:

pThisCC->pDrv->pfnVBVAReportCursorPosition(pThisCC->pDrv, fFlags, idScreen, x, y); /* idScreen controlled */

pfnVBVAReportCursorPosition is a function pointer that points to Display::i_displayVBVAReportCursorPosition.

// src/VBox/Main/src-client/DisplayImpl.cpp:3538
DECLCALLBACK(void) Display::i_displayVBVAReportCursorPosition(PPDMIDISPLAYCONNECTOR pInterface, uint32_t fFlags, uint32_t aScreenId, uint32_t x, uint32_t y)
{
    LogFlowFunc(("\n"));
    LogRel2(("%s: fFlags=%RU32, aScreenId=%RU32, x=%RU32, y=%RU32\n",
             __PRETTY_FUNCTION__, fFlags, aScreenId, x, y));

    PDRVMAINDISPLAY pDrv = PDMIDISPLAYCONNECTOR_2_MAINDISPLAY(pInterface);
    Display *pThis = pDrv->pDisplay;

    if (fFlags & VBVA_CURSOR_SCREEN_RELATIVE)
    {
        x += pThis->maFramebuffers[aScreenId].xOrigin; /* OOB */
        y += pThis->maFramebuffers[aScreenId].yOrigin; /* OOB */
    }
    fireCursorPositionChangedEvent(pThis->mParent->i_getEventSource(), RT_BOOL(fFlags & VBVA_CURSOR_VALID_DATA), x, y);
}

Given that aScreenId is controlled fully by the guest, this would allow for out-of-bound access of the maFramebuffers:

x += pThis->maFramebuffers[aScreenId].xOrigin; /* OOB */
y += pThis->maFramebuffers[aScreenId].yOrigin; /* OOB */

Timeline:

  • 2020-03-03 Vendor disclosure via ZDI
  • 2020-04-30 Coordinated public disclosure

Vendor Response

The vendor has acknowledged the issue and released an update to address it.

The vendor advisory can be found here: https://www.oracle.com/security-alerts/cpuapr2020.html.