By Luca Ginex
Overview
This blog post discusses a use-after-free vulnerability that we found in VirtualBox in 2025. This vulnerability was patched on Oracle Critical Patch Update – January 2026. The vulnerability was also presented, along with others, at OffensiveCon 2026. This post describes the exploitation process for the vulnerability on a Linux system.
First, a general overview of the SVGA device is given. Next, an analysis of the vulnerability is provided. Finally, the exploitation strategy is presented.
Preliminaries
In this section we give a general overview of the SVGA device and describe the relevant structures involved in SVGA operations.
Super Video Graphics Array (SVGA)
The Super Video Graphics Array (SVGA) is a display standard that extends the capabilities of the original Video Graphics Array (VGA) standard introduced by IBM in 1987. SVGA provides support for a wide range of screen resolutions and color depths, surpassing the limits of VGA.
Common SVGA resolutions include: 800×600 and 1024×768, with modern implementations supporting much higher resolutions dependent on the graphics card and monitor capabilities.
VMware SVGA-II
VMware SVGA-II is an SVGA-compatible virtual graphics adapter developed by VMware for use in its virtual machines (VMs). It is a software-defined device that provides guest operating systems with enhanced graphics capabilities, including 3D acceleration. This virtual graphics device is a standard component in VMware products like Workstation, Fusion, and ESXi, and is also supported by other hypervisors, including Oracle VirtualBox.
The VMware SVGA-II virtual device is exposed to the guest VM through the PCI bus, presenting both I/O ports and a Memory-Mapped I/O (MMIO) region. The base address for the I/O ports (SVGA_PORT_BASE) is dynamically assigned during PCI bus enumeration.
The following table describes the primary I/O port offsets exposed by the virtual device:
Name Index Description --------------- --------- ---------------------------------------------------------------------- SVGA_INDEX 0x00 Index of the SVGA register to read from or write to. SVGA_VALUE 0x01 Value read or to write to the register specified in the SVGA_INDEX offset. SVGA_BIOS 0x02 Unknown. SVGA_IRQSTATUS 0x03 Unknown.
The guest VM can use these I/O ports to read from and write to the internal registers of the SVGA-II device. To access a register, the guest driver writes the register’s index to the SVGA_INDEX port (SVGA_PORT_BASE + SVGA_INDEX). It can then read the register’s value from the SVGA_VALUE port (SVGA_PORT_BASE + SVGA_VALUE) or write a new value to it through the same port.
The following table shows the defined registers by the SVGA-II specification:
Register Name Index Description
---------------------------- ----- ----------------------------------------------------------
SVGA_REG_ID 0x00 SVGA device ID. Guest writes expected ID; host responds if
supported.
SVGA_REG_ENABLE 0x01 Enables or disables the SVGA device.
SVGA_REG_WIDTH 0x02 Framebuffer width (in pixels).
SVGA_REG_HEIGHT 0x03 Framebuffer height (in pixels).
SVGA_REG_MAX_WIDTH 0x04 Maximum width supported by the device.
SVGA_REG_MAX_HEIGHT 0x05 Maximum height supported by the device.
SVGA_REG_DEPTH 0x06 Bits per pixel (usually 24 or 32).
SVGA_REG_BITS_PER_PIXEL 0x07 Guest-visible bits per pixel (may differ from DEPTH).
SVGA_REG_PSEUDOCOLOR 0x08 Boolean; set if using pseudocolor mode.
SVGA_REG_RED_MASK 0x09 Red mask (bitfield).
SVGA_REG_GREEN_MASK 0x0A Green mask.
SVGA_REG_BLUE_MASK 0x0B Blue mask.
SVGA_REG_BYTES_PER_LINE 0x0C Pitch: bytes per scanline.
SVGA_REG_FB_START 0x0D Framebuffer physical address (read-only).
SVGA_REG_FB_OFFSET 0x0E Offset to start of visible framebuffer.
SVGA_REG_VRAM_SIZE 0x0F Size of video RAM.
SVGA_REG_FB_SIZE 0x10 Size of framebuffer.
SVGA_REG_CAPABILITIES 0x11 Bitmask of supported features.
SVGA_REG_MEM_START 0x12 MMIO (memory-mapped I/O) region base address.
SVGA_REG_MEM_SIZE 0x13 Size of MMIO region.
SVGA_REG_CONFIG_DONE 0x14 Write 1 when configuration is done.
SVGA_REG_SYNC 0x15 Write 1 to sync guest/host framebuffer.
SVGA_REG_BUSY 0x16 Read 1 if device is busy processing commands.
SVGA_REG_GUEST_ID 0x17 Guest OS identifier.
SVGA_REG_CURSOR_ID 0x18 Cursor identifier.
SVGA_REG_CURSOR_X 0x19 Cursor X position.
SVGA_REG_CURSOR_Y 0x1A Cursor Y position.
SVGA_REG_CURSOR_ON 0x1B Show/hide cursor.
SVGA_REG_HOST_BITS_PER_PIXEL 0x1C Bits per pixel on host display (read-only).
SVGA_REG_SCRATCH_SIZE 0x1D Number of scratch registers available.
SVGA_REG_MEM_REGS 0x1E Number of FIFO registers.
SVGA_REG_NUM_DISPLAYS 0x1F Number of virtual displays supported.
SVGA_REG_PITCHLOCK 0x20 Lock bytes-per-line (pitch).
SVGA_REG_IRQMASK 0x21 Interrupt bitmask.
SVGA_REG_NUM_GUEST_DISPLAYS 0x22 Guest-specified number of displays.
SVGA_REG_DISPLAY_ID 0x23 ID of current display being modified.
SVGA_REG_DISPLAY_IS_PRIMARY 0x24 Boolean; is this display the primary one?
SVGA_REG_DISPLAY_POSITION_X 0x25 Display position X (for multi-monitor layout).
SVGA_REG_DISPLAY_POSITION_Y 0x26 Display position Y.
SVGA_REG_DISPLAY_WIDTH 0x27 Width of a specific display.
SVGA_REG_DISPLAY_HEIGHT 0x28 Height of a specific display.
SVGA_REG_GMR_ID 0x29 ID of the Guest Memory Region
SVGA_REG_GMR_DESCRIPTOR 0x2A Unknown
SVGA_REG_GMR_MAX_IDS 0x2B Unknown
SVGA_REG_GMR_MAX_DESCRIPTOR_LENGTH 0x2C Unknown
SVGA_REG_TRACES 0x2D Unknown
SVGA_REG_GMRS_MAX_PAGES 0x2E Unknown
SVGA_REG_MEMORY_SIZE 0x2F Total dedicated device memory size excluding FIFO
SVGA_REG_COMMAND_LOW 0x30 Lower 32 bits of the command buffer physical address
SVGA_REG_COMMAND_HIGH 0x31 Upper 32 bits of command buffer physical address
The SVGA-II driver uses a FIFO (First In, First Out) queue, which is a region of memory shared between the guest and the host, to process commands. The guest driver writes command sequences into this FIFO buffer and updates registers, such as SVGA_FIFO_NEXT_CMD, to notify the host that new commands are available for execution. Another way to submit commands is by registering a command buffer: the guest sets the SVGA_REG_COMMAND_HIGH and the SVGA_REG_COMMAND_LOW registers with the physical address of a buffer that contains an SVGACBHeader structure. Its definition is shown in the following code listing.
typedef
#include "vmware_pack_begin.h"
struct {
volatile SVGACBStatus status; /* Modified by device. */
volatile uint32 errorOffset; /* Modified by device. */
uint64 id;
SVGACBFlags flags;
uint32 length;
union {
PA pa;
struct {
SVGAMobId mobid;
uint32 mobOffset;
} mob;
} ptr;
uint32 offset; /* Valid if CMD_BUFFERS_2 cap set, must be zero otherwise,
* modified by device.
*/
uint32 dxContext; /* Valid if DX_CONTEXT flag set, must be zero otherwise */
uint32 mustBeZero[6];
}
#include "vmware_pack_end.h"
SVGACBHeader;
The ptr.pa field contains the physical address of the command buffer. The length field contains the size of the command buffer. The maximum size is 512KB.
SVGA Memory Objects
The VirtualBox SVGA implementation uses the generic VMSVGAMOB structure to keep track of several types of memory objects. The following listing shows the definition of the VMSVGAMOBstructure.
/* MOB is also a GBO.
*/
typedef struct VMSVGAMOB
{
AVLU32NODECORE Core; /* Key is the mobid. */
RTLISTNODE nodeLRU;
[1]
VMSVGAGBO Gbo;
} VMSVGAMOB, *PVMSVGAMOB;
VirtualBox uses this structure on the host while object data can be stored on guest memory. In this scenario the object is a Guest-Backed Object (GBO). In order for the host to access object data it must keep track of where the data is stored on the guest side. In order to do that the VMSVGAGBO structure is used. Note that the VMSVGAMOB structure contains a VMSVGAGBOstructure, at [1].
The following listing shows the VMSVGAGBO structure definition.
/* GBO.
*/
typedef struct VMSVGAGBO
{
uint32_t fGboFlags;
uint32_t cTotalPages;
uint32_t cbTotal;
#ifndef VMSVGA_WITH_PGM_LOCKING
uint32_t cDescriptors;
PVMSVGAGBODESCRIPTOR paDescriptors;
#else
uint32_t cSegsUsed; /**< Number of segments used in VMSVGAGBO::paSegs. */
void *pvDescriptors; /**< Pointer to the memory for holding all the parallel arrays. */
RTGCPHYS *paGCPhysPages; /**< Pointer to the array of guest physical address for the pages. */
PPGMPAGEMAPLOCK paPageLocks; /**< Pointer to the array of PGM page map locks. */
void **papvPages; /**< Pointer to the host adresses of mapped pages. */
PRTSGSEG paSegs; /**< Pointer to an array of segments. */
#endif
[2]
void *pvHost; /* Pointer to cbTotal bytes on the host if VMSVGAGBO_F_HOST_BACKED is set. */
} VMSVGAGBO, *PVMSVGAGBO;
typedef VMSVGAGBO const *PCVMSVGAGBO;
The pvHost field, at [2], contains a pointer to a memory region where the host keeps a copy of guest object data.
The DX Context
The VirtualBox SVGA implementation uses the VMSVGA3DDXCONTEXT structure to keep track of different DirectX contexts. Each context contains all the rendering parameters and resources used by the GPU (shaders, textures). The following listing shows the VMSVGA3DDXCONTEXTstructure definition.
/**
* VMSVGA3D DX context (VGPU10+). DX contexts ids are a separate namespace from legacy context ids.
*/
typedef struct VMSVGA3DDXCONTEXT
{
[3]
/** The DX context id. */
uint32_t cid;
/** . */
uint32_t u32Reserved;
/** . */
uint32_t cRenderTargets;
/** Backend specific data. */
PVMSVGA3DBACKENDDXCONTEXT pBackendDXContext;
/** Copy of the guest memory for this context. The guest will be updated on unbind. */
SVGADXContextMobFormat svgaDXContext;
[4]
/* Context-Object Tables bound to this context. */
PVMSVGAMOB aCOTMobs[VBSVGA_NUM_COTABLES];
struct
{
SVGACOTableDXRTViewEntry *paRTView;
SVGACOTableDXDSViewEntry *paDSView;
SVGACOTableDXSRViewEntry *paSRView;
SVGACOTableDXElementLayoutEntry *paElementLayout;
SVGACOTableDXBlendStateEntry *paBlendState;
SVGACOTableDXDepthStencilEntry *paDepthStencil;
SVGACOTableDXRasterizerStateEntry *paRasterizerState;
SVGACOTableDXSamplerEntry *paSampler;
SVGACOTableDXStreamOutputEntry *paStreamOutput;
SVGACOTableDXQueryEntry *paQuery;
SVGACOTableDXShaderEntry *paShader;
SVGACOTableDXUAViewEntry *paUAView;
uint32_t cRTView;
uint32_t cDSView;
uint32_t cSRView;
uint32_t cElementLayout;
uint32_t cBlendState;
uint32_t cDepthStencil;
uint32_t cRasterizerState;
uint32_t cSampler;
uint32_t cStreamOutput;
uint32_t cQuery;
uint32_t cShader;
uint32_t cUAView;
VBSVGACOTableDXVideoProcessorEntry *paVideoProcessor;
VBSVGACOTableDXVideoDecoderOutputViewEntry *paVideoDecoderOutputView;
VBSVGACOTableDXVideoDecoderEntry *paVideoDecoder;
VBSVGACOTableDXVideoProcessorInputViewEntry *paVideoProcessorInputView;
VBSVGACOTableDXVideoProcessorOutputViewEntry *paVideoProcessorOutputView;
uint32_t cVideoProcessor;
uint32_t cVideoDecoderOutputView;
uint32_t cVideoDecoder;
uint32_t cVideoProcessorInputView;
uint32_t cVideoProcessorOutputView;
} cot;
} VMSVGA3DDXCONTEXT;
Each structure has a unique context ID saved in the cid field, at [3]. Each context also keeps track of all the memory objects (MOBs) used in that specific context. MOBs are stored into the Context-Object Table (COTable) [4]. The table contains one entry for each type of MOB supported. At any given time only one MOB of a specific type can be stored into the COTable.
Vulnerability
The vulnerability is caused by how guest-backed memory objects (MOBs) are handled. By issuing an SVGA_3D_CMD_DX_SET_COTABLE SVGA command it is possible to bind a MOB to a context-object table (COTable). The COTable holds a pointer to the bound MOB. Later it is possible to issue an SVGA_3D_CMD_DESTROY_GB_MOB SVGA command to destroy the same MOB, while the COTable holds a dangling pointer to the freed MOB. The destroy handler does not verify if the requested MOB is bound to a COTable before freeing it. By manipulating the heap memory it is possible to reuse the freed MOB memory to corrupt memory and eventually achieve guest-to-host escape and execute code in the context of the hypervisor process.
COTable Initialization
When an SVGA_3D_CMD_DX_SET_COTABLE SVGA command is sent from a guest VM to the host the vmsvga3dDXSetCOTable() function is called to handle the command. The following code listing shows the vmsvga3dDXSetCOTable() function.
// File: src/VBox/Devices/Graphics/DevVGA-SVGA3d-dx.cpp
[Truncated]
int vmsvga3dDXSetCOTable(PVGASTATECC pThisCC, SVGA3dCmdDXSetCOTable const *pCmd, PVMSVGAMOB pMob)
{
int rc;
PVMSVGAR3STATE const pSvgaR3State = pThisCC->svga.pSvgaR3State;
AssertReturn(pSvgaR3State->pFuncsDX && pSvgaR3State->pFuncsDX->pfnDXSetCOTable, VERR_INVALID_STATE);
PVMSVGA3DSTATE p3dState = pThisCC->svga.p3dState;
AssertReturn(p3dState, VERR_INVALID_STATE);
PVMSVGA3DDXCONTEXT pDXContext;
rc = vmsvga3dDXContextFromCid(p3dState, pCmd->cid, &pDXContext);
AssertRCReturn(rc, rc);
RT_UNTRUSTED_VALIDATED_FENCE();
[1]
return dxSetOrGrowCOTable(pThisCC, pDXContext, pMob, pCmd->type, pCmd->validSizeInBytes, false);
}
[Truncated]
static int dxSetOrGrowCOTable(PVGASTATECC pThisCC, PVMSVGA3DDXCONTEXT pDXContext, PVMSVGAMOB pMob,
SVGACOTableType enmType, uint32_t validSizeInBytes, bool fGrow)
{
PVMSVGAR3STATE const pSvgaR3State = pThisCC->svga.pSvgaR3State;
int rc = VINF_SUCCESS;
uint32_t idxCOTable;
if (enmType < SVGA_COTABLE_MAX)
[2]
idxCOTable = enmType;
else if (enmType >= VBSVGA_COTABLE_MIN && enmType < VBSVGA_COTABLE_MAX)
idxCOTable = SVGA_COTABLE_MAX + (enmType - VBSVGA_COTABLE_MIN);
else
ASSERT_GUEST_FAILED_RETURN(VERR_INVALID_PARAMETER);
RT_UNTRUSTED_VALIDATED_FENCE();
uint32_t cbCOT;
if (pMob)
{
[3]
/* Bind a new mob to the COTable. */
cbCOT = vmsvgaR3MobSize(pMob);
[4]
ASSERT_GUEST_RETURN(validSizeInBytes <= cbCOT, VERR_INVALID_PARAMETER);
RT_UNTRUSTED_VALIDATED_FENCE();
/* When growing a COTable, the valid size can't be greater than the old COTable size. */
if (fGrow)
validSizeInBytes = RT_MIN(validSizeInBytes, vmsvgaR3MobSize(pDXContext->aCOTMobs[idxCOTable]));
[5]
/* Create a memory pointer, which is accessible by host. */
rc = vmsvgaR3MobBackingStoreCreate(pSvgaR3State, pMob, fGrow ? 0 : validSizeInBytes);
}
else
{
/* Unbind. */
validSizeInBytes = 0;
cbCOT = 0;
vmsvgaR3MobBackingStoreDelete(pSvgaR3State, pDXContext->aCOTMobs[idxCOTable]);
}
[Truncated]
if (RT_SUCCESS(rc))
{
[6]
pDXContext->aCOTMobs[idxCOTable] = pMob;
[Truncated]
return rc;
}
The pCmd parameter points to a structure that wraps all the guest-submitted SVGA command parameters. The command requires the following parameters:
cid: DX context ID.mobid: ID of the MOB to bind.type: Type of the MOB to bind.validSizeInBytes: Size of the MOB to bind.
The pMob parameter holds a pointer to the target MOB structure that the guest VM requested to bind.
The vmsvga3dDXSetCOTable() function, at [1], calls the dxSetOrGrowCOTable() function. The dxSetOrGrowCOTable() function, at [2], validates the guest-supplied type for the MOB. If it is a valid type the value is stored into the idxCOTable variable. The code at [3] calls the vmsvgaR3MobSize() function on the submitted MOB to get the size of the MOB. At [4] the code validates that the validSizeInBytes value is not bigger than the size of the submitted MOB.
The code at [5] calls the vmsvgaR3MobBackingStoreCreate() function to allocate a memory region to store the content of the guest-backed object into the host memory. This function populates the Gbo.pvHost field of the submitted MOB with a pointer to the allocated memory region. Note that this function performs the copy of the content of guest memory into the allocated memory region. In order to complete the binding process, at [6], the function sets the aCOTMobs[idxCOTable] array entry to a pointer to the requested MOB. This completes the binding operation of a MOB to the COTable.
MOB Destruction
Later a guest VM can issue an SVGA_3D_CMD_DESTROY_GB_MOB SVGA command. The handler for this command is the vmsvgaR3MobDestroy() function. The guest must supply a valid MOB ID to the function. The following code listing shows the vmsvgaR3MobDestroy() function.
// File: src/VBox/Devices/Graphics/DevVGA-SVGA-cmd.cpp
[Truncated]
static void vmsvgaR3MobFree(PVMSVGAR3STATE pSvgaR3State, PVMSVGAMOB pMob)
{
vmsvgaR3GboDestroy(pSvgaR3State, &pMob->Gbo);
[7]
RTMemFree(pMob);
}
static int vmsvgaR3MobDestroy(PVMSVGAR3STATE pSvgaR3State, SVGAMobId mobid)
{
/* Update the entry in the pSvgaR3State->pGboOTableMob. */
SVGAOTableMobEntry entry;
RT_ZERO(entry);
vmsvgaR3OTableWrite(pSvgaR3State, &pSvgaR3State->aGboOTables[SVGA_OTABLE_MOB],
mobid, SVGA3D_OTABLE_MOB_ENTRY_SIZE, &entry, sizeof(entry));
[8]
PVMSVGAMOB pMob = (PVMSVGAMOB)RTAvlU32Remove(&pSvgaR3State->MOBTree, mobid);
if (pMob)
{
RTListNodeRemove(&pMob->nodeLRU);
[9]
vmsvgaR3MobFree(pSvgaR3State, pMob);
return VINF_SUCCESS;
}
return VERR_INVALID_PARAMETER;
}
The function at [8] removes the MOB entry corresponding to the MOB ID provided by the guest (if it exists) from a tree data structure. At [9], the vmsvgaR3MobFree() function is called. At [7] the RTMemFree() function is called on the pointer to the MOB structure. This terminates the deleting process for a MOB. The code however does not check if the target MOB is bound to a COTable before freeing it. By exploiting this vulnerability it is possible to have a COTable entry that still points to a freed MOB. By manipulating the heap memory it is possible to reuse the freed MOB memory to corrupt memory and eventually achieve guest-to-host escape and execute code in the context of the hypervisor process.
Exploitation
Exploitation Steps
Exploiting this vulnerability involves the following steps:
- Defining a MOB. This causes the allocation of a MOB structure with size 104.
- Binding a MOB to a COTable. The included exploit uses the context ID 2 in this step. Note that any type in the COTable can be used. The included exploit uses the type 7. The
validSizeInBytesparameter must be set to the size of theVMSVGA3DDXCONTEXTstructure (0x2170) that the exploit will later use. This causes an allocation of a buffer with size 0x2170. In the following discussion this MOB will be referred as MOB1. - Destroying MOB1. In the process, both the MOB structure (size 104) and the buffer to hold guest data (size 0x2170) are freed. This causes
COTable[7]to still hold a pointer to MOB1. - Reclaiming MOB1 freed memory by binding another MOB to the COTable. The
validSizeInBytesparameter must be set to 104. This will likely cause the memory associated with MOB1 to be used as the buffer to hold guest data of MOB2. - Trigger allocation of a new
VMSVGA3DDXCONTEXTstructure. This will likely cause the structure to be placed in the same memory used for MOB1 guest buffer.
The following sections further explore the above steps and explain how to achieve guest-to-host escape.
Binding Process
When a MOB structure is allocated it does not have an associated buffer to store guest-backed data. The buffer is allocated when the MOB is bound to a COTable. The following code listing shows the vmsvgaR3GboBackingStoreCreate() function.
// File: src/VBox/Devices/Graphics/DevVGA-SVGA-cmd.cpp
static int vmsvgaR3GboBackingStoreCreate(PVMSVGAR3STATE pSvgaR3State, PVMSVGAGBO pGbo, uint32_t cbValid)
{
int rc;
/* Just reread the data if pvHost has been allocated already. */
if (!(pGbo->fGboFlags & VMSVGAGBO_F_HOST_BACKED))
[1]
pGbo->pvHost = RTMemAllocZ(pGbo->cbTotal);
if (pGbo->pvHost)
{
cbValid = RT_MIN(cbValid, pGbo->cbTotal);
[2]
rc = vmsvgaR3GboRead(pSvgaR3State, pGbo, 0, pGbo->pvHost, cbValid);
}
else
rc = VERR_NO_MEMORY;
if (RT_SUCCESS(rc))
pGbo->fGboFlags |= VMSVGAGBO_F_HOST_BACKED;
else
{
RTMemFree(pGbo->pvHost);
pGbo->pvHost = NULL;
}
return rc;
}
In case of a just-allocated MOB, code at [1] is executed. This causes the allocation of a buffer with an arbitrary size that the guest controls. Note how the allocated memory is also zeroed out by the usage of the RTMemAllocZ() function. The Gbo.pvHost field contains the pointer to the memory region. Note at [2] that the function also copies content from the guest-controlled memory into the newly-allocated buffer.
It is important to note that a pointer to the memory region is also stored in the cot structure of the VMSVGA3DDXCONTEXT structure during the binding process. Specifically the pointer associated with the type specified during the binding process is populated. For example binding a MOB with type 7, which corresponds to the SVGA_COTABLE_SAMPLER type, causes the paSampler pointer to be populated with the pvHost value of the MOB.
After the binding process, the COTable state is the following.

Destruction Process And Reallocation
By issuing an SVGA_3D_CMD_DESTROY_GB_MOB SVGA command, MOB1 memory and MOB1’s pvHost buffer are freed. The freed memory is also zeroed out.

After this step, the exploit reclaims the two freed memory chunks. MOB1 memory can be reclaimed by binding another MOB to the COTable with a guest buffer size of 104. This causes MOB2’s pvHost pointer to point to MOB1. Since the goal is to gain control over a VMSVGA3DDXCONTEXT structure, MOB1’s pvHost buffer must have the size of that structure, which is 0x2170. In order to trigger allocation of a DX context, DX contexts management must be analyzed.
The following code listing shows the vmsvga3dDXDefineContext() function.
// File: src/VBox/Devices/Graphics/DevVGA-SVGA3d-dx.cpp
/**
* Create a new 3D DX context.
*
* @returns VBox status code.
* @param pThisCC The VGA/VMSVGA state for ring-3.
* @param cid Context id to be created.
*/
int vmsvga3dDXDefineContext(PVGASTATECC pThisCC, uint32_t cid)
{
int rc;
PVMSVGAR3STATE const pSvgaR3State = pThisCC->svga.pSvgaR3State;
AssertReturn(pSvgaR3State->pFuncsDX && pSvgaR3State->pFuncsDX->pfnDXDefineContext, VERR_INVALID_STATE);
PVMSVGA3DSTATE p3dState = pThisCC->svga.p3dState;
AssertReturn(p3dState, VERR_INVALID_STATE);
PVMSVGA3DDXCONTEXT pDXContext;
LogFunc(("cid %d\n", cid));
AssertReturn(cid < SVGA3D_MAX_CONTEXT_IDS, VERR_INVALID_PARAMETER);
[3]
if (cid >= p3dState->cDXContexts)
{
/* Grow the array. */
[4]
uint32_t cNew = RT_ALIGN(cid + 15, 16);
void *pvNew = RTMemRealloc(p3dState->papDXContexts, sizeof(p3dState->papDXContexts[0]) * cNew);
AssertReturn(pvNew, VERR_NO_MEMORY);
p3dState->papDXContexts = (PVMSVGA3DDXCONTEXT *)pvNew;
while (p3dState->cDXContexts < cNew)
{
[5]
pDXContext = (PVMSVGA3DDXCONTEXT)RTMemAllocZ(sizeof(*pDXContext));
AssertReturn(pDXContext, VERR_NO_MEMORY);
pDXContext->cid = SVGA3D_INVALID_ID;
p3dState->papDXContexts[p3dState->cDXContexts++] = pDXContext;
}
}
/* If one already exists with this id, then destroy it now. */
if (p3dState->papDXContexts[cid]->cid != SVGA3D_INVALID_ID)
vmsvga3dDXDestroyContext(pThisCC, cid);
pDXContext = p3dState->papDXContexts[cid];
memset(pDXContext, 0, sizeof(*pDXContext));
[Truncated]
return rc;
}
When the exploit defines the first context it uses (context ID 2), at [3], the function first checks if the requested context ID is greater than the max ID currently allocated. If that’s the case, the function, at [4], rounds the number of context IDs to allocate by adding 15 to the requested context ID and aligning that to 16. By submitting a context ID value of 2, context IDs 0-31 are allocated. So if the exploit uses a new context ID inside the range 0-31, no actual allocation is performed.
In order to force a call to the RTMemAllocZ() function, at [5], the exploit must define a context with a context ID greater than 31. This causes the context ID 32 (the first one to be allocated) to use MOB1’s pvHost freed buffer for the VMSVGA3DDXCONTEXT structure.
If the exploit is successful, the COTable status is the following.

With this setup, the exploit has control over a VMSVGA3DDXCONTEXT structure through the pDXContext->cot field of context ID 2. An interesting feature of the cot structure is that it is possible to arbitrarily set the content of the buffers that the pointers in the cot structure point to. For each COT type a function setter is defined that can be used to populate the corresponding buffer. In the exploit case the cot.paSampler contains the address of the VMSVGA3DDXCONTEXT structure. By using the SVGA_3D_CMD_DX_DEFINE_SAMPLER_STATE SVGA command it is possible to insert arbitrary data into the cot.paSampler buffer. The following code listing shows the vmsvga3dDXDefineSamplerState() function, which is the handler for the aforementioned SVGA command.
// File: src/VBox/Devices/Graphics/DevVGA-SVGA3d-dx.cpp
int vmsvga3dDXDefineSamplerState(PVGASTATECC pThisCC, uint32_t idDXContext, SVGA3dCmdDXDefineSamplerState const *pCmd)
{
int rc;
PVMSVGAR3STATE const pSvgaR3State = pThisCC->svga.pSvgaR3State;
AssertReturn(pSvgaR3State->pFuncsDX && pSvgaR3State->pFuncsDX->pfnDXDefineSamplerState, VERR_INVALID_STATE);
PVMSVGA3DSTATE p3dState = pThisCC->svga.p3dState;
AssertReturn(p3dState, VERR_INVALID_STATE);
PVMSVGA3DDXCONTEXT pDXContext;
rc = vmsvga3dDXContextFromCid(p3dState, idDXContext, &pDXContext);
AssertRCReturn(rc, rc);
[6]
SVGA3dSamplerId const samplerId = pCmd->samplerId;
[Truncated]
[7]
SVGACOTableDXSamplerEntry *pEntry = &pDXContext->cot.paSampler[samplerId];
pEntry->filter = pCmd->filter;
pEntry->addressU = pCmd->addressU;
pEntry->addressV = pCmd->addressV;
pEntry->addressW = pCmd->addressW;
pEntry->mipLODBias = pCmd->mipLODBias;
pEntry->maxAnisotropy = pCmd->maxAnisotropy;
pEntry->comparisonFunc = pCmd->comparisonFunc;
pEntry->borderColor = pCmd->borderColor;
pEntry->minLOD = pCmd->minLOD;
pEntry->maxLOD = pCmd->maxLOD;
[Truncated]
return rc;
}
At [6], the function extracts the samplerId from the guest-provided command parameters. This ID is used as an offset into the cot.paSampler buffer. The chosen entry is populated with guest-provided data [7]. This allows the exploit to overwrite certain fields of the VMSVGA3DDXCONTEXT structure. This process can also be used to create an arbitrary writeprimitive: by modifying the paSamplerState pointer it is possible to write to any arbitrary memory location.
Information Disclosure
The exploit leverages the VMSVGA3DDXCONTEXT structure overwrite to achieve information disclosure. This allows the exploit to know where the VBoxDD.so library is loaded into the hypervisor process and to further exploit the target.

The exploit performs the following steps:
- Allocate 6 MOBs. This will likely result in the six MOBs to be allocated one after another in memory.
- Free MOBs number 2, 4, and 6.
- Bind the remaining 1, 3, and 5 MOBs to the COTable with context ID 32. The size of the guest buffer must be 104 (0x68).
This, with a high chance, creates the layout shown above.
The exploit then modifies the counter values of all the entries in the cot structure of the context ID 32 by leveraging the VMSVGA3DDXCONTEXT overwrite primitive shown before. This keeps the pointers in the cot structure intact. The modified 0xffffffff counter instead allows the exploit to perform an out-of-bounds write into adjacent memory of the COT pointers. By modifying the cot.cSampler value it is possible to overwrite memory past the size of the paSampler buffer. If the heap grooming process is successful, this allows the exploit to modify the pMob->Gbo.cbTotal field of an adjacent MOB.
In the shown example, the exploit can modify the MOB bound in entry #4. Note that this part of the exploit is not 100% reliable as the heap layout for the six allocated MOBs depends on memory pressure on the hypervisor side. Testing shows the exploit to be reliable with a probability of 70%.
By issuing an SVGA_3D_CMD_DX_GROW_COTABLE command it is possible to read data from the pvHost pointer with the modified Gbo.cbTotal field of entry 8, leaking the adjacent MOB structure. From this leak it is possible to recover the base address of the VBoxDD.so library in memory and the address in memory of the VMSVGAR3STATE structure. The following code listing shows its definition.
/**
* Internal SVGA ring-3 only state.
*/
typedef struct VMSVGAR3STATE
{
PPDMDEVINS pDevIns; /* Stored here to use with PDMDevHlp* */
GMR *paGMR; // [VMSVGAState::cGMR]
struct
{
SVGAGuestPtr RT_UNTRUSTED_GUEST ptr;
uint32_t RT_UNTRUSTED_GUEST bytesPerLine;
SVGAGMRImageFormat RT_UNTRUSTED_GUEST format;
} GMRFB;
struct
{
bool fActive;
uint32_t xHotspot;
uint32_t yHotspot;
uint32_t width;
uint32_t height;
uint32_t cbData;
void *pData;
} Cursor;
SVGAColorBGRX colorAnnotation;
# ifdef VMSVGA_USE_EMT_HALT_CODE
/** Number of EMTs in BusyDelayedEmts (quicker than scanning the set). */
uint32_t volatile cBusyDelayedEmts;
/** Set of EMTs that are */
VMCPUSET BusyDelayedEmts;
# else
/** Number of EMTs waiting on hBusyDelayedEmts. */
uint32_t volatile cBusyDelayedEmts;
/** Semaphore that EMTs wait on when reading SVGA_REG_BUSY and the FIFO is
* busy (ugly). */
RTSEMEVENTMULTI hBusyDelayedEmts;
# endif
/** Information about screens. */
VMSVGASCREENOBJECT aScreens[64];
/** Command buffer contexts. */
PVMSVGACMDBUFCTX apCmdBufCtxs[SVGA_CB_CONTEXT_MAX];
/** The special Device Context for synchronous commands. */
VMSVGACMDBUFCTX CmdBufCtxDC;
/** Flag which indicates that there are buffers to be processed. */
uint32_t volatile fCmdBuf;
/** Critical section for accessing the command buffer data. */
RTCRITSECT CritSectCmdBuf;
/** Object Tables: MOBs, etc. see SVGA_OTABLE_* */
VMSVGAGBO aGboOTables[SVGA_OTABLE_MAX];
/** Tree of guest's Memory OBjects. Key is mobid. */
AVLU32TREE MOBTree;
/** Least Recently Used list of MOBs.
* To unmap older MOBs when the guest exceeds SVGA_REG_SUGGESTED_GBOBJECT_MEM_SIZE_KB (SVGA_REG_GBOBJECT_MEM_SIZE_KB) value. */
RTLISTANCHOR MOBLRUList;
# ifdef VBOX_WITH_VMSVGA3D
# ifdef VMSVGA3D_DX
/** DX context of the currently processed command buffer */
uint32_t idDXContextCurrent;
uint32_t u32Reserved;
# endif
[8]
VMSVGA3DBACKENDFUNCS3D *pFuncs3D;
VMSVGA3DBACKENDFUNCSVGPU9 *pFuncsVGPU9;
VMSVGA3DBACKENDFUNCSMAP *pFuncsMap;
VMSVGA3DBACKENDFUNCSGBO *pFuncsGBO;
VMSVGA3DBACKENDFUNCSDX *pFuncsDX;
VMSVGA3DBACKENDFUNCSDXVIDEO *pFuncsDXVideo;
# endif
[Truncated]
} VMSVGAR3STATE, *PVMSVGAR3STATE;
It is important to note at [8] how this structure contains some function pointers.
Arbitrary Read
The arbitrary read primitive is achieved in the following manner:
- Define a DX Context.
- Define two MOBs.
- Bind one MOB to the COTable of the newly-created context.
- Destroy the MOB.
- Reclaim the MOB’s memory as shown earlier. This allows the guest to overwrite MOB’s structure with arbitrary data. Set
Gbo.pvHostpointer to point to where to read. - Issue an
SVGA_3D_CMD_DX_GROW_COTABLEto read back the content from MOB’sGbo.pvHostpointer.
ROP Chain
With the arbitrary read/write primitives and the information leak, it is possible to overwrite a function pointer in the VMSVGAR3STATE structure with the address of the first gadget of a ROP chain. The exploit uses the pFuncsVGPU9->pfnCommandClear function pointer. It is possible to invoke the function pointer by issuing the SVGA_3D_CMD_CLEAR SVGA command. This command is very helpful as it allows to provide a variable number of parameters that can be used as places to put ROP gadgets. The included ROP chain performs the following operations:
- Performs stack pivoting into the buffer that contains guest-provided command parameters.
- Calls RTMemProtect() to set one of the MOB’s buffer with RWX privileges.
- Jumps into the buffer.
Since the content of the buffer can be controlled by the guest, the guest can place shellcode there and control execution of the hypervisor process.
Process Continuation
The exploit during the ROP chain loses any reference to the stack. It is possible however to create a fake stack that redirects execution to the vmsvgaR3FifoLoop() function, which is the main loop of the SVGA thread. By doing so, the guest VM doesn’t crash and it continues its execution.
Conclusion
In this blogpost we presented a use-after-free vulnerability in the SVGA subsystem in Oracle VirtualBox and how we exploited it to achieve code execution on the host Linux machine while keeping the guest VM alive. In the tested setup the exploit reliability is around 70%.
Demo
About Exodus Intelligence
Our world class team of vulnerability researchers discover hundreds of exclusive Zero-Day vulnerabilities, providing our clients with proprietary knowledge before the adversaries find them. We also conduct N-Day research, where we select critical N-Day vulnerabilities and complete research to prove whether these vulnerabilities are truly exploitable in the wild. Our researchers create and use in-house agentic AI tooling to supplement parts of their vulnerability research and exploit development workflow. In addition to efficiency gains, we’re able to ensure AI-enabled research output maintains the same standards of quality as traditional research.
For more information on our products and how we can help your vulnerability efforts, visit www.exodusintel.com or contact info@exodusintel.com for further discussion.