CVE: CVE-2019-3026

Tested Versions:

  • Oracle VirtualBox 6.0.4 revision r128413

Product URL(s): https://virtualbox.org

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.

Vulnerability

VboxSVGA is the default Video Adapter for Windows guests. The vulnerability occurs while processing an SVGA command which the guest send to the host.

The SVGA FIFO

The SVGA FIFO is a memory buffer which is shared between host and guest, both sides can read and write to this buffer. When the guest wants to send an SVGA command to the host, it forms a command inside the FIFO buffer and notifies the host that the FIFO buffer now contains data to be processed, then the host uses some SVGA registers to locate the command and processes it until the FIFO is empty.

vmsvgaFIFOLoop()

The vmsvgaFIFOLoop function runs a loop to wait for the guest to write the command to the FIFO buffer and process them. When the guest has written the command data, we can enable the following flags to start the process:

static DECLCALLBACK(int) vmsvgaFIFOLoop(PPDMDEVINS pDevIns, PPDMTHREAD pThread)
{
    ...    
    
    while (pThread->enmState == PDMTHREADSTATE_RUNNING)
    {
        ...        
        
        if (   !pThis->svga.fEnabled
            || !pThis->svga.fConfigured)
        {
            vmsvgaFifoSetNotBusy(pThis, pSVGAState, pFIFO[SVGA_FIFO_MIN]);
            fBadOrDisabledFifo = true;
            continue;
        }
    ...        

When svga.fEnabled and svga.fConfigured are enabled, the host locates the command in FIFO buffer by the following code snippet:

uint32_t const offFifoMin    = pFIFO[SVGA_FIFO_MIN];
uint32_t const offFifoMax    = pFIFO[SVGA_FIFO_MAX];
uint32_t       offCurrentCmd = pFIFO[SVGA_FIFO_STOP];

RT_UNTRUSTED_NONVOLATILE_COPY_FENCE();
if (RT_UNLIKELY(   !VMSVGA_IS_VALID_FIFO_REG(SVGA_FIFO_STOP, offFifoMin)
                || offFifoMax <= offFifoMin
                || offFifoMax > pThis->svga.cbFIFO
                || (offFifoMax & 3) != 0
                || (offFifoMin & 3) != 0
                || offCurrentCmd < offFifoMin
                || offCurrentCmd > offFifoMax))
{
     ...
}
RT_UNTRUSTED_VALIDATED_FENCE();
if (RT_UNLIKELY(offCurrentCmd & 3))
{
    STAM_REL_COUNTER_INC(&pSVGAState->StatFifoErrors);
    LogRelMax(8, ("vmsvgaFIFOLoop: Misaligned offCurrentCmd=%#x?\n", offCurrentCmd));
    offCurrentCmd = ~UINT32_C(3);
}

If 3D acceleration is enabled:

if (RT_LIKELY(pThis->svga.f3DEnabled))
{ /* likely */ }
else
{
    LogRelMax(8, ("VMSVGA3d: 3D disabled, command %d skipped\n", enmCmdId));
    break;
}

...

#define VMSVGAFIFO_CHECK_3D_CMD_MIN_SIZE_BREAK(a_cbMin) \
do { AssertMsgBreak(pHdr->size >= (a_cbMin), ("size=%#x a_cbMin=%#zx\n", pHdr->size, (size_t)(a_cbMin))); \
          RT_UNTRUSTED_VALIDATED_FENCE(); \
     } while (0)
    switch ((int)enmCmdId)
    {
        case SVGA_3D_CMD_SURFACE_DEFINE:

...            

If 3D acceleration is enabled, 3D SVGA command can be processed, there is a header size check macro named VMSVGAFIFO_CHECK_3D_CMD_MIN_SIZE_BREAK:

#define VMSVGAFIFO_CHECK_3D_CMD_MIN_SIZE_BREAK(a_cbMin) \
     do { AssertMsgBreak(pHdr->size >= (a_cbMin), ("size=%#x a_cbMin=%#zx\n", pHdr->size, (size_t)(a_cbMin))); \
          RT_UNTRUSTED_VALIDATED_FENCE(); \
     } while (0)

The definition of AssertMsgBreak(expr, a) macro:

#ifdef RT_STRICT
# define AssertMsgBreak(expr, a) \
    if (RT_LIKELY(!!(expr))) \
    { /* likely */ } \
    else if (1) \
    { \
        RTAssertMsg1Weak(#expr, __LINE__, __FILE__, RT_GCC_EXTENSION __PRETTY_FUNCTION__); \
        RTAssertMsg2Weak a; \
        RTAssertPanic(); \
        break; \
    } else \
        break
#else
# define AssertMsgBreak(expr, a) \
    if (RT_LIKELY(!!(expr))) \
    { /* likely */ } \
    else \
        break
#endif

So if the expression passed to AssertMsgBreak(expr, a) returns false, the execution of the current scope will be break, but the macro VMSVGAFIFO_CHECK_3D_CMD_MIN_SIZE_BREAK puts the AssertMsgBreak macro inside a do {} while(0) loop, so this breaks the loop instead of the intended switch-case block. This causes processing to continue with any expr result.

Most of the 3D SVGA commands use this macro to validate the size of the command header. For example:

case SVGA_3D_CMD_SET_SHADER_CONST:
{
    SVGA3dCmdSetShaderConst *pCmd = (SVGA3dCmdSetShaderConst *)(pHdr + 1);
    VMSVGAFIFO_CHECK_3D_CMD_MIN_SIZE_BREAK(sizeof(*pCmd));
    STAM_REL_COUNTER_INC(&pSVGAState->StatR3Cmd3dSetShaderConst);

    uint32_t cRegisters = (pHdr->size - sizeof(*pCmd)) / sizeof(pCmd->values) + 1;
    rc = vmsvga3dShaderSetConst(pThis, pCmd->cid, pCmd->reg, pCmd->type, pCmd->ctype, cRegisters, pCmd->values);
    break;
}

Because VMSVGAFIFO_CHECK_3D_CMD_MIN_SIZE_BREAK does not work as intended, we can set the value of pHdr->size smaller than sizeof(*pCmd) and cause an Integer Underflow to make cRegisters to be a big number, this lead to Out-of-Bound Read in vmsvga3dShaderSetConst() function:

int vmsvga3dShaderSetConst(PVGASTATE pThis, uint32_t cid, uint32_t reg, SVGA3dShaderType type, 
              SVGA3dShaderConstType ctype, uint32_t cRegisters, uint32_t *pValues)
{
...    

    for (uint32_t i = 0; i < cRegisters; i++)
    {
       ...
        
        vmsvga3dSaveShaderConst(pContext, reg + i, type, ctype, 
             pValues[i*4 + 0], pValues[i*4 + 1], 
             pValues[i*4 + 2], pValues[i*4 + 3]);
    }

To trigger the bug:

  • The attacker must first obtain the ability to execute high-privileged code on the target guest system.
  • The guest VM setting must enable the 3D acceleration.

Timeline

  • 2019-08-13 Reported to vendor
  • 2019-10-20 Vendor patched

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/cpuoct2019.html.