CVE: CVE-2019-2984

Tested Versions: Oracle VirtualBox 5.2.18 revision r123745

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.

There are several interfaces for the guest to communicate with the host in VirtualBox, one of them is Host-Guest Shared Memory Interface (HGSMI) services. These vulnerabilities occur in the VirtualBox Video Acceleration (VBVA) channel, which works on top of HGSMI.

Vulnerability

When the guest submits a vbva command buffer (hit VGA_PORT_HGSMI_GUEST), this buffer will be passed to a function based on the command info. The pvBuffer buffer and its size cbBuffer are guest controlled:

/*
 * @return VBox status code.
 * @param pvHandler      The VBVA channel context.
 * @param u16ChannelInfo Command code.
 * @param pvBuffer       HGSMI buffer with command data.  Considered volatile!
 * @param cbBuffer       Size of command data.
 */
static DECLCALLBACK(int) vbvaChannelHandler(void *pvHandler, uint16_t u16ChannelInfo,
                                            void RT_UNTRUSTED_VOLATILE_GUEST *pvBuffer, HGSMISIZE cbBuffer)
{
    int rc = VINF_SUCCESS;

    LogFlowFunc(("pvHandler %p, u16ChannelInfo %d, pvBuffer %p, cbBuffer %u\n", 
                    pvHandler, u16ChannelInfo, pvBuffer, cbBuffer));

    PVGASTATE       pVGAState = (PVGASTATE)pvHandler;
    PHGSMIINSTANCE  pIns      = pVGAState->pHGSMI;
    VBVACONTEXT    *pCtx      = (VBVACONTEXT *)HGSMIContext(pIns);

    switch (u16ChannelInfo)
    {
...

#ifdef VBOX_WITH_VIDEOHWACCEL
        case VBVA_VHWA_CMD:
            if (cbBuffer >= VBOXVHWACMD_HEADSIZE())
            {
                vbvaVHWAHandleCommand(pVGAState, (VBOXVHWACMD RT_UNTRUSTED_VOLATILE_GUEST *)pvBuffer);
                rc = VINF_SUCCESS;
            }
            else
                rc = VERR_INVALID_PARAMETER;
            break;
#endif
...

In case VBOX_WITH_VIDEOHWACCEL, function vbvaVHWAHandleCommand will process the buffer, through several wrapper functions, the buffer come to the function vbvaVHWACommandSubmitInner

static void vbvaVHWAHandleCommand(PVGASTATE pVGAState, VBOXVHWACMD RT_UNTRUSTED_VOLATILE_GUEST *pCmd)
{
    if (vbvaVHWACheckPendingCommands(pVGAState))
    {
        if (vbvaVHWACommandSubmit(pVGAState, pCmd, false))
            return;
    }

    vbvaVHWACommandPend(pVGAState, pCmd);
}
static bool vbvaVHWACommandSubmit(PVGASTATE pVGAState, VBOXVHWACMD RT_UNTRUSTED_VOLATILE_GUEST *pCommand, 
                                       bool fAsyncCommand)
{
    bool fPending = false;
    bool fRet = vbvaVHWACommandSubmitInner(pVGAState, pCommand, &fPending);
    if (!fPending)
        vbvaVHWACommandComplete(pVGAState, pCommand, fAsyncCommand);
    return fRet;
}
static bool vbvaVHWACommandSubmitInner(PVGASTATE pVGAState, VBOXVHWACMD RT_UNTRUSTED_VOLATILE_GUEST *pCommand,
                                             bool *pfPending)
{
    *pfPending = false;

...

    ASSERT_GUEST_STMT_RETURN(pVGAState->pDrv->pfnVHWACommandProcess, pCommand->rc = VERR_INVALID_STATE, true);
    RT_UNTRUSTED_VALIDATED_FENCE();
    /*
     * Call the driver to process the command.
     */
    Log(("VGA Command >>> %#p, %d\n", pCommand, enmCmd));
    int rc = pVGAState->pDrv->pfnVHWACommandProcess(pVGAState->pDrv, enmCmd, fGuestCmd, pCommand);
    if (rc == VINF_CALLBACK_RETURN)
    {
        Log(("VGA Command --- Going Async %#p, %d\n", pCommand, enmCmd));
        *pfPending = true;
        return true; /* Command will be completed asynchronously by the driver and need not be put in the pending list. */
    }

...
    
    return true;
}

One thing must be noted is the vtable pVGAState->pDrv only be initialized when the 2D Video Acceleration is enabled in guest display setting.

The vtable was initialized in Display::i_drvConstruct function, then we follow the command buffer:

DECLCALLBACK(int) Display::i_drvConstruct(PPDMDRVINS pDrvIns, PCFGMNODE pCfg, uint32_t fFlags)
{
...
    
#ifdef VBOX_WITH_VIDEOHWACCEL
    pThis->IConnector.pfnVHWACommandProcess    = Display::i_displayVHWACommandProcess;
#endif
    
...
DECLCALLBACK(int) Display::i_displayVHWACommandProcess(PPDMIDISPLAYCONNECTOR pInterface, int enmCmd, bool fGuestCmd,
                                                       VBOXVHWACMD RT_UNTRUSTED_VOLATILE_GUEST *pCommand)
{
    PDRVMAINDISPLAY pDrv = PDMIDISPLAYCONNECTOR_2_MAINDISPLAY(pInterface);
    return pDrv->pDisplay->i_handleVHWACommandProcess(enmCmd, fGuestCmd, pCommand);
}
int Display::i_handleVHWACommandProcess(int enmCmd, bool fGuestCmd, VBOXVHWACMD RT_UNTRUSTED_VOLATILE_GUEST *pCommand)
{
...

    HRESULT hr = pFramebuffer->ProcessVHWACommand((BYTE *)pCommand, enmCmd, fGuestCmd);
...
    
}
class VBoxOverlayFrameBuffer : public UIFrameBufferPrivate
{
public:

    STDMETHOD(ProcessVHWACommand)(BYTE *pCommand, LONG enmCmd, BOOL fGuestCmd)
    {
...
      int rc = mOverlay.onVHWACommand((struct VBOXVHWACMD RT_UNTRUSTED_VOLATILE_GUEST *)pCommand, enmCmd, fGuestCmd != FALSE);
      UIFrameBufferPrivate::unlock();
...
    }
...
int VBoxQGLOverlay::onVHWACommand(struct VBOXVHWACMD RT_UNTRUSTED_VOLATILE_GUEST *pCmd,
                                  int /*VBOXVHWACMD_TYPE*/ enmCmdInt, bool fGuestCmd)
{
...
    mCmdPipe.postCmd(VBOXVHWA_PIPECMD_VHWA, (void *)pCmd, enmCmd, fGuestCmd);
    return VINF_CALLBACK_RETURN;
}
void VBoxVHWACommandElementProcessor::postCmd(VBOXVHWA_PIPECMD_TYPE aType, void *pvData,
                                              int /*VBOXVHWACMD_TYPE*/ enmCmd, bool fGuestCmd)
{
...    
    
    pCmd->setData(aType, pvData, enmCmd, fGuestCmd);
...

    RTListAppend(&mCommandList, &pCmd->ListNode);

    RTCritSectLeave(&mCritSect);

    if (pNotifyObject)
    {
        VBoxVHWACommandProcessEvent *pCurrentEvent = new VBoxVHWACommandProcessEvent();
        QApplication::postEvent(pNotifyObject, pCurrentEvent);
        m_NotifyObjectRefs.dec();
    }
}
void VBoxQGLOverlay::onVHWACommandEvent(QEvent *pEvent)
{
...    
    
    VBoxVHWACommandElement *pCmd = mCmdPipe.getCmd();
    if (pCmd)
    {
        processCmd(pCmd);
        mCmdPipe.doneCmd();
    }
...
}
void VBoxQGLOverlay::processCmd(VBoxVHWACommandElement * pCmd)
{
...
        case VBOXVHWA_PIPECMD_VHWA:
            vboxDoVHWACmd(pCmd->vhwaCmdPtr(), pCmd->vhwaCmdType(), pCmd->vhwaIsGuestCmd());
            break;
...
}
void VBoxQGLOverlay::vboxDoVHWACmd(void RT_UNTRUSTED_VOLATILE_GUEST *pvCmd, int /*VBOXVHWACMD_TYPE*/ enmCmd, bool fGuestCmd)
{
    vboxDoVHWACmdExec(pvCmd, enmCmd, fGuestCmd);
...
}
void VBoxQGLOverlay::vboxDoVHWACmdExec(void RT_UNTRUSTED_VOLATILE_GUEST *pvCmd, int /*VBOXVHWACMD_TYPE*/ enmCmdInt, bool fGuestCmd)
{
    struct VBOXVHWACMD RT_UNTRUSTED_VOLATILE_GUEST *pCmd = (struct VBOXVHWACMD RT_UNTRUSTED_VOLATILE_GUEST *)pvCmd;
    VBOXVHWACMD_TYPE enmCmd = (VBOXVHWACMD_TYPE)enmCmdInt;

    switch (enmCmd)
    {
       ...
        case VBOXVHWACMD_TYPE_SURF_COLORKEY_SET:
        {
            VBOXVHWACMD_SURF_COLORKEY_SET RT_UNTRUSTED_VOLATILE_GUEST *pBody = VBOXVHWACMD_BODY(pCmd, VBOXVHWACMD_SURF_COLORKEY_SET);
            Assert(!mGlOn == !mOverlayImage.hasSurfaces());
            initGl();
            makeCurrent();
            pCmd->rc = mOverlayImage.vhwaSurfaceColorkeySet(pBody);
            /* this is here to ensure we have color key changes picked up */
            vboxDoCheckUpdateViewport();
            mNeedOverlayRepaint = true;
            Assert(!mGlOn == !mOverlayImage.hasSurfaces());
            break;
        }
...            

In case VBOXVHWACMD_TYPE_SURF_COLORKEY_SET, the pBody is still guest controlled buffer and will be passed to mOverlayImage.vhwaSurfaceColorkeySet() function:

int VBoxVHWAImage::vhwaSurfaceColorkeySet(struct VBOXVHWACMD_SURF_COLORKEY_SET RT_UNTRUSTED_VOLATILE_GUEST *pCmd)
{
    VBoxVHWASurfaceBase *pSurf = handle2Surface(pCmd->u.in.hSurf);
    VBOXQGLLOG_ENTER(("pSurf (0x%x)\n", pSurf));

    vboxCheckUpdateAddress (pSurf, pCmd->u.in.offSurface);
    VBoxVHWASurfaceBase *pSurf = handle2Surface(pCmd->u.in.hSurf);

    VBoxVHWASurfList *pList = pSurf->getComplexList();
    Assert(pSurf->handle() != VBOXVHWA_SURFHANDLE_INVALID);
 ...
}
int VBoxVHWAImage::vhwaSurfaceLock(struct VBOXVHWACMD_SURF_LOCK RT_UNTRUSTED_VOLATILE_GUEST *pCmd)
{
    VBoxVHWASurfaceBase *pSurf = handle2Surface(pCmd->u.in.hSurf);
    VBOXQGLLOG_ENTER(("pSurf (0x%x)\n",pSurf));
    vboxCheckUpdateAddress (pSurf, pCmd->u.in.offSurface);
...
}
int VBoxVHWAImage::vhwaSurfaceUnlock(struct VBOXVHWACMD_SURF_UNLOCK RT_UNTRUSTED_VOLATILE_GUEST *pCmd)
{
    VBoxVHWASurfaceBase *pSurf = handle2Surface(pCmd->u.in.hSurf);

    VBOXQGLLOG_ENTER(("pSurf (0x%x)\n",pSurf));
    if (pCmd->u.in.xUpdatedMemValid)
    {
        QRect r = VBOXVHWA_CONSTRUCT_QRECT_FROM_RECTL_WH(&pCmd->u.in.xUpdatedMemRect);
        pSurf->updatedMem(&r);
    }

    return pSurf->unlock();
}
int VBoxVHWAImage::vhwaSurfaceFlip(struct VBOXVHWACMD_SURF_FLIP RT_UNTRUSTED_VOLATILE_GUEST *pCmd)
{
    VBoxVHWASurfaceBase *pTargSurf = handle2Surface(pCmd->u.in.hTargSurf);
    VBoxVHWASurfaceBase *pCurrSurf = handle2Surface(pCmd->u.in.hCurrSurf);
    VBOXQGLLOG_ENTER(("pTargSurf (0x%x), pCurrSurf (0x%x)\n",pTargSurf,pCurrSurf));
    vboxCheckUpdateAddress (pCurrSurf, pCmd->u.in.offCurrSurface);
    vboxCheckUpdateAddress (pTargSurf, pCmd->u.in.offTargSurface);
...
}
int VBoxVHWAImage::vhwaSurfaceOverlayUpdate(struct VBOXVHWACMD_SURF_OVERLAY_UPDATE RT_UNTRUSTED_VOLATILE_GUEST *pCmd)
{
    VBoxVHWASurfaceBase *pSrcSurf = handle2Surface(pCmd->u.in.hSrcSurf);
    VBoxVHWASurfList *pList = pSrcSurf->getComplexList();
    vboxCheckUpdateAddress (pSrcSurf, pCmd->u.in.offSrcSurface);
...
}
int VBoxVHWAImage::vhwaSurfaceOverlaySetPosition(struct VBOXVHWACMD_SURF_OVERLAY_SETPOSITION RT_UNTRUSTED_VOLATILE_GUEST *pCmd)
{
    VBoxVHWASurfaceBase *pDstSurf = handle2Surface(pCmd->u.in.hDstSurf);
    VBoxVHWASurfaceBase *pSrcSurf = handle2Surface(pCmd->u.in.hSrcSurf);

    VBOXQGLLOG_ENTER(("pDstSurf (0x%x), pSrcSurf (0x%x)\n",pDstSurf,pSrcSurf));

    vboxCheckUpdateAddress (pSrcSurf, pCmd->u.in.offSrcSurface);
    vboxCheckUpdateAddress (pDstSurf, pCmd->u.in.offDstSurface);
...
}

There is another NULL pointer dereference in VBoxQGLOverlay::vhwaSurfaceUnlock:

VBoxVHWASurfaceBase * getVGA() const { return mSurfVGA; }

VBoxVHWASurfaceBase * vgaSurface() { return mDisplay.getVGA(); }

int VBoxQGLOverlay::vhwaSurfaceUnlock(struct VBOXVHWACMD_SURF_UNLOCK RT_UNTRUSTED_VOLATILE_GUEST *pCmd)
{
    int rc = mOverlayImage.vhwaSurfaceUnlock(pCmd);
    VBoxVHWASurfaceBase *pVGA = mOverlayImage.vgaSurface();
    const VBoxVHWADirtyRect &rect = pVGA->getDirtyRect();
    mNeedOverlayRepaint = true;
    if (!rect.isClear())
        mMainDirtyRect.add(rect);
    return rc;
}

The mSurfVGA object needs to be initialized but if we send a VBOXVHWACMD_TYPE_SURF_UNLOCK request before initializing it, the pointer pVGA will be NULL and is used right after that, causing the VM to crash.

To trigger the bug,

  • Attacker must first obtain the ability to execute high-privileged code on the target guest system
  • 2D acceleration on the guest VM setting must be enabled

Sending a request with any of following types without first creating a surface will result in a VM crash:

  • VBOXVHWACMD_TYPE_SURF_DESTROY
  • VBOXVHWACMD_TYPE_SURF_LOCK
  • VBOXVHWACMD_TYPE_SURF_UNLOCK
  • VBOXVHWACMD_TYPE_SURF_FLIP
  • VBOXVHWACMD_TYPE_SURF_OVERLAY_UPDATE
  • VBOXVHWACMD_TYPE_SURF_OVERLAY_SETPOSITION
  • VBOXVHWACMD_TYPE_SURF_COLORKEY_SET

Timeline

  • 2019-09-11 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.