CVE: CVE-2020-2682

Tested Versions:

  • Oracle VirtualBox 5.2.18 revision r123745

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.

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. The vulnerability occurs in the VirtualBox Video Acceleration (VBVA) channel which works on top of HGSMI.

vbvaChannelHandler() function

The guest submitted a vbva command buffer (hit VGA_PORT_HGSMI_GUEST), this buffer will be passed to a correlative function base 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 is 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
{
    Q_OBJECT;

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));
	...
}

The handle2Surface function:

VBoxVHWASurfaceBase* handle2Surface(uint32_t h)
{
    VBoxVHWASurfaceBase* pSurf = (VBoxVHWASurfaceBase*)mSurfHandleTable.get(h);
    Assert(pSurf);
    return pSurf;
}

The method get of mSurfHandleTable is declared as:

void* VBoxVHWAHandleTable::get(uint32_t h)
{
    Assert(h < mcSize);
    Assert(h > 0);
    return mTable[h];
}

Since the index h is guest controlled and the Assert macro will be disabled in the release build, this obviously an Out-Of-Bound access vulnerability.

Later in 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);

    mRepaintNeeded = true;

    if (pCmd->u.in.flags & VBOXVHWA_CKEY_DESTBLT)
    {
        VBoxVHWAColorKey ckey(pCmd->u.in.CKey.high, pCmd->u.in.CKey.low);
        pSurf->setDstBltCKey(&ckey);
    }
	...

    return VINF_SUCCESS;
}

pCmd is guest controlled, so the value of ckey is controlled, after that we can control the value which will be written to the mSrcBltCKey field of VBoxVHWASurfaceBase class object pSurf:

class VBoxVHWASurfaceBase
{
public:
    VBoxVHWASurfaceBase (class VBoxVHWAImage *pImage,
            const QSize & aSize,
            const QRect & aTargRect,
            const QRect & aSrcRect,
            const QRect & aVisTargRect,
            VBoxVHWAColorFormat & aColorFormat,
            VBoxVHWAColorKey * pSrcBltCKey, VBoxVHWAColorKey * pDstBltCKey,
            VBoxVHWAColorKey * pSrcOverlayCKey, VBoxVHWAColorKey * pDstOverlayCKey,
            VBOXVHWAIMG_TYPE aImgFlags);
            
	...

    void setSrcBltCKey (const VBoxVHWAColorKey * ckey)
    {
        if(ckey)
        {
            mSrcBltCKey = *ckey;
            mpSrcBltCKey = &mSrcBltCKey;
        }
        else
        {
            mpSrcBltCKey = NULL;
        }
    }
	...    

Timeline:

  • 2019-09-09 Disclosed to ZDI
  • 2019-10-31 Verified and reported to vendor
  • 2020-01-15 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/cpujan2020.html.