win32kfull.sys xxxGetDummyPalette trusted palette count OOB

xxxGetDummyPalette builds the dummy palette the clipboard render path uses alongside the dummy DIB. The colour-table layout of a BITMAPINFOHEADER bitmap is biClrUsed entries of RGBQUAD following the header proper. The descriptor returned for the DIB handle inlines the BITMAPINFOHEADER from +0x14, so biClrUsed lives at descriptor +0x34 — and the helper reads it from there and hands it straight to CreateDIBPalette:

/* xxxGetDummyPalette — vulnerable build. CF_DIBV5 (0x11) is tried first; if its
   clip entry is present with hData already 2, the function falls through to
   CF_DIB (0x8). */
ULONG   fmt  = CF_DIBV5;                                       /* 0x11, tried first */
tagCLIP *clip = FindClipFormat(pwinsta, fmt, 1);
if (clip != NULL && clip->hData /* +0x8 */ == (HANDLE)2) {     /* DIBV5 slot busy — try DIB */
    fmt  = CF_DIB;                                             /* 0x8, fallback */
    clip = FindClipFormat(pwinsta, fmt, 1);
    if (clip == NULL || clip->hData /* +0x8 */ == (HANDLE)2)
        return NULL;                                           /* neither slot usable */
}

HANDLE hmem = xxxGetClipboardData(pwinsta, fmt, pgcbd);
if (hmem == NULL)
    return NULL;

/* HMValidateHandleWithDescriptor returns the kernel clipboard descriptor
   (tagDIBDATA) that wraps the DIB payload: cbData at +0x10 is the recorded
   payload byte count, bmiHeader at +0x14 inlines the BITMAPINFOHEADER (so
   biClrUsed sits at +0x34). */
tagDIBDATA *desc = HMValidateHandleWithDescriptor(hmem, TYPE_DIB /* 6 */);
if (desc == NULL)
    return NULL;

tagCLIP *palclip = FindClipFormat(pwinsta, CF_PALETTE /* 9 */, 1);
if (palclip == NULL)
    return NULL;

BITMAPINFOHEADER *bih   = &desc->bmiHeader;                    /* +0x14 */
ULONG             count = bih->biClrUsed;                      /* +0x34 — taken on trust */
HPALETTE          hpal  = CreateDIBPalette(bih, count);         /* walks `count` RGBQUADs past bih */

if (hpal != NULL) {
    UT_FreeCBFormat(palclip);
    palclip->hData /* +0x8 */ = (PVOID)hpal;
    GreSetPaletteOwner(hpal, 0);                               /* hand the palette to the kernel */
    return hpal;
}
return NULL;

Nothing compares count against the descriptor’s recorded payload size, so a forged biClrUsed that overshoots the colour-table region sends CreateDIBPalette’s read past the descriptor allocation into neighbouring session pool. One detail of the consumer matters for forging the value: CreateDIBPalette takes the entry count as a 16-bit word — it reads its second argument with a word-width load and then re-reads biClrUsed from the header the same way — so a declared 0x40000000 truncates to zero and the walk never runs; the forged count has to live in the low sixteen bits (0xFFFF being the effective maximum). The leaked bytes come back as palette entries, which the renderer can observe — the same session-pool info-leak shape as the rest of the clipboard render path, reached by trusting a header count instead of bounding it.

Reaching the helper has one precondition worth naming because it decides whether the path exists at all on a given machine. The dispatch into the dummy-render family happens when the requested format’s clip entry carries the synthetic marker hData == 2, and a CF_PALETTE slot with that marker is only created by the clipboard’s companion synthesis at close time when the session runs in palette display mode — a session-state bit that every depth-32 desktop leaves clear. A SetClipboardData(CF_PALETTE, NULL) does not substitute: it registers a delayed-render slot (hData == 0), which the getter routes to the owner-render path, not to the dummy family. On a palette-mode session, placing a forged CF_DIB or CF_DIBV5 on the clipboard and asking for CF_PALETTE walks straight in.

The patch

The fix bounds every term against the descriptor’s recorded payload size before CreateDIBPalette is reached. The payload must be at least four bytes (else biSize is not even safe to read); a 0x0c BITMAPCOREHEADER is treated as declaring zero colours, since the core header ends before a biClrUsed field exists; any other biSize must be a standard-or-larger header (>= 0x28) that the payload actually contains. Only then is biClrUsed read, multiplied by sizeof(RGBQUAD) wide, and the sum checked for both wrap and buffer-fit:

/* xxxGetDummyPalette — patched build. Same format selection and descriptor
   lookup as above; the new work is the bounds check before CreateDIBPalette. */
ULONG cbData = desc->cbData;                                   /* +0x10 — descriptor's payload byte count */
if (cbData < 4)
    return NULL;                                               /* too small to read biSize safely */

BITMAPINFOHEADER *bih    = &desc->bmiHeader;                   /* +0x14 */
ULONG             biSize = bih->biSize;                        /* +0x14 — first DWORD of the header */

ULONG count;
if (biSize == 0x0c) {                                          /* BITMAPCOREHEADER — no biClrUsed field */
    count = 0;
} else if (biSize >= 0x28 &&                                  /* standard or larger header */
           cbData >= 0x28 && cbData >= biSize) {               /* payload actually holds the header */
    count = bih->biClrUsed;                                    /* +0x34 */
    UINT64 table_bytes = (UINT64)count << 2;                   /* count * sizeof(RGBQUAD), done wide */
    if (table_bytes > 0xffffffff)
        return NULL;                                           /* colour-table size overflows 32 bits */
    UINT64 need = (UINT64)biSize + (UINT32)table_bytes;        /* header plus colour table */
    if (need < biSize || cbData < need)
        return NULL;                                           /* addition wrapped, or buffer too small */
} else {
    return NULL;                                               /* biSize in the unsupported 0x0d..0x27 gap */
}

HPALETTE hpal = CreateDIBPalette(bih, count);                  /* count is now provably in-buffer */
if (hpal != NULL) {
    UT_FreeCBFormat(palclip);
    palclip->hData /* +0x8 */ = (PVOID)hpal;
    GreSetPaletteOwner(hpal, 0);
    return hpal;
}
return NULL;

The 0x0c short-circuit exists because a BITMAPCOREHEADER ends at byte twelve — there is no biClrUsed field at offset +0x20 to read, so the count has to default to zero rather than be trusted. The count << 2 is the multiplication the old code never did safely; the need < biSize test catches the addition wrapping; the cbData < need test is the actual freshness check that the buffer carries what the header claims. Together they enforce the rule that a count declared in a header is a claim about variable trailing data, and the claim has to be bounded by the buffer that carries it before it can be used as a loop or allocation bound.

Attack path

flowchart TD
    A["attacker places a forged CF_DIB or CF_DIBV5 on the clipboard with biClrUsed oversized past the colour table"] --> B["xxxGetDummyPalette reads biClrUsed, no bound against descriptor payload size"]
    B --> C["CreateDIBPalette iterates that many RGBQUADs"]
    C --> D["read walks off the descriptor allocation into neighbouring session pool"]
    D --> E["leaked bytes returned as palette entries, observable by the renderer, session-space info-leak"]

The wider pattern in this month’s clipboard render patch is that every helper that took a size or count from a header field — biSize in xxxDIBtoBMP, the hand-rolled width × bpp × height in xxxBMPtoDIB and xxxGetDummyDib, the palette count here — had the same shape of bug and got the same shape of fix. A sibling on the UMPD bitmap path, UMPDOBJ::bThunkLargeBitmap, was fixed in the same patch for the complementary mistake: it wrote the caller’s out-pointers (*arg3, *arg4, *arg5) at the top of the function and only then ran the size checks that could make it return 0, leaving the caller with references into a surface that was never prepared. The fix there is the standard “cache locally in tmp3/tmp4/tmp5, commit only on success” — out- parameters are commitments, and the right order is to run every check before touching them. The through-line for the whole corner is that a header field that names a size or count is a claim about variable trailing data; the render helper has to bound the claim against the buffer before it acts on it, and call the central validator instead of redoing the arithmetic.