CVE-2026-6311
Overview
Background
- IAccessible2 table API
- the Windows accessibility interface through which assistive-technology clients query a table’s header cells via methods like
get_columnHeaderCellsandget_rowHeaderCells. - `CoTaskMemAlloc`
- the COM allocator used to hand an array of
IUnknown*pointers back to an out-of-process client; the callee reports both the buffer and an element count. - COM marshaller
- the RPC layer that, given a pointer array and a count,
AddRefs and serializes each element across the process boundary, dereferencing every slot the count claims is valid. - Populated-count invariant
- the reported element count must equal the number of slots actually written, never the number allocated.
Root Cause Analysis
get_columnHeaderCells and get_rowHeaderCells allocate an output array with CoTaskMemAlloc sized to column_header_ids.size() (respectively row_header_ids.size()), then loop over the header ids resolving each to an accessibility node and writing the corresponding IAccessible2* into the array, incrementing a local index only on success. When a node lookup fails partway through, fewer than size() slots are written, but the code set *n_column_header_cells = static_cast<LONG>(column_header_ids.size()) — the requested size, not the populated index. The trailing, never-written slots hold whatever uninitialized heap bytes CoTaskMemAlloc returned, and the COM marshaller treats each as a valid IUnknown*, calling AddRef and dereferencing it.
The fix reports static_cast<LONG>(index) from both methods, so the marshaller only ever touches slots that were actually populated — the same pattern already used by get_targets.
index bounds the count to the elements that were actually written.Attack Path
- Enable accessibility
a client (screen reader, UIA/IA2 bridge, or a page that turns on accessibility) causes Chromium’s browser process to service
IAccessibleTableheader-cell queries. - Craft a table with unresolvable headers
the attacker page builds an accessible table whose header-id list contains entries that fail node lookup mid-iteration, so
index < size(). - Invoke the header query
the client calls
get_columnHeaderCells/get_rowHeaderCells; the method returns the oversized count with uninitialized trailing slots. - Marshaller dereferences garbage
the COM marshaller
AddRefs each claimed element, dereferencing uninitialized heap memory as anIUnknown*. - Corruption or disclosure depending on the stale bytes, this yields a crash, an information leak, or a controlled pointer dereference in the browser process.
Impact Assessment
IUnknown* dereferences that can crash, leak memory, or be steered toward a controlled call. Preconditions are that accessibility is active and the page can present a table whose header lookups fail mid-iteration; no renderer compromise is required, but the payoff depends on grooming the freshly allocated heap slots.Files Changed
ui/accessibility/platform/ax_platform_node_win.cc
Audit Directions
- Allocated-size vs written-countaudit every COM/array-returning method that allocates to a collection’s
.size()but fills conditionally in a loop; the reported count must be the write index. - Failible lookups inside fill loopsflag loops that skip elements on lookup failure yet size the output to the input list, leaving uninitialized tail slots.
- `CoTaskMemAlloc` out-paramsreview all accessibility interfaces returning
IUnknown*arrays (headers, relations, selections) against theget_targetspopulated-count pattern.
Patch
From db2a3f8ac9a6c1fa8e8a26c5d34072814f14c6a0 Mon Sep 17 00:00:00 2001 From: Benjamin Beaudry <[email protected]> Date: Fri, 03 Apr 2026 08:57:27 -0700 Subject: [PATCH] [M147][a11y] Fix uninitialized memory in get_columnHeaderCells/get_rowHeaderCells Both get_columnHeaderCells and get_rowHeaderCells allocate a COM array via CoTaskMemAlloc, but report the requested array size (column_header_ids.size()) to the caller instead of the number of successfully populated elements (index). If a node lookup fails mid-iteration, the trailing array slots contain uninitialized memory that the COM marshaller will treat as valid IUnknown* pointers. This CL fixes both methods to return the actual populated count (index), matching the pattern already used by get_targets. (cherry picked from commit c81f01b469c4eae60e312ebe8b1b691d454609e9) Fixed: 498201025 Change-Id: I596745388199d61eef8261fe0ae6e1d3e773f240 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7717598 Reviewed-by: Kevin Babbitt <[email protected]> Commit-Queue: Kevin Babbitt <[email protected]> Auto-Submit: Benjamin Beaudry <[email protected]> Commit-Queue: Benjamin Beaudry <[email protected]> Cr-Original-Commit-Position: refs/heads/main@{#1607938} Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7729125 Cr-Commit-Position: refs/branch-heads/7727@{#2183} Cr-Branched-From: ce01102937348db7b88c8a4257ee4b3ac702eb1a-refs/heads/main@{#1596535} --- diff --git a/ui/accessibility/platform/ax_platform_node_win.cc b/ui/accessibility/platform/ax_platform_node_win.cc index e8c6a971..3685384d 100644 --- a/ui/accessibility/platform/ax_platform_node_win.cc +++ b/ui/accessibility/platform/ax_platform_node_win.cc @@ -4274,7 +4274,7 @@ } } - *n_column_header_cells = static_cast<LONG>(column_header_ids.size()); + *n_column_header_cells = static_cast<LONG>(index); return S_OK; } @@ -4330,7 +4330,7 @@ } } - *n_row_header_cells = static_cast<LONG>(row_header_ids.size()); + *n_row_header_cells = static_cast<LONG>(index); return S_OK; }
Original Bug Report
Potential uninitialized memory use in AXPlatformNodeWin header cell getters
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: The AXPlatformNodeWin::get_columnHeaderCells and get_rowHeaderCells methods allocate an array via CoTaskMemAlloc but may leave elements uninitialized if node lookups fail. The methods incorrectly return the requested array size rather than the successfully populated count, causing the COM marshaller to process uninitialized memory. A compromised renderer could potentially exploit this to escape the sandbox by grooming the OLE heap.
Affected files:
ui/accessibility/platform/ax_platform_node_win.cc
Estimated timestamp from git blame: 2022-02-08
Summary
A potential uninitialized memory vulnerability exists in ui/accessibility/platform/ax_platform_node_win.cc within the AXPlatformNodeWin::get_columnHeaderCells and AXPlatformNodeWin::get_rowHeaderCells methods. If an accessibility node lookup fails during array population, the uninitialized memory allocated by ::CoTaskMemAlloc is returned to the COM caller because the methods return the requested array size instead of the number of successfully populated elements.
Technical Details
Both vulnerable methods follow the exact same pattern. For instance, in get_columnHeaderCells:
- The method retrieves a list of node IDs (
column_header_ids). - It allocates memory using
::CoTaskMemAlloc(column_header_ids.size() * sizeof(IUnknown*)). Crucially,CoTaskMemAllocdoes not zero-initialize the memory. - It iterates over the IDs, looks up the corresponding wrapper, and if successful, calls
QueryInterfaceand increments anindexcounter. - If
GetDelegate()->GetFromNodeID(node_id)returnsnullptr(e.g., due to a missing wrapper from reparenting edge cases), the loop skips the element and does not incrementindex. - The method concludes by setting
*n_column_header_cells = static_cast<LONG>(column_header_ids.size());instead of using theindexvariable.
If any node lookup fails, the resulting array will contain uninitialized bytes at the end, but the output count (*n_column_header_cells) tells the COM marshaller that the entire array is populated with valid IUnknown* pointers.
Potential Exploitation Steps
Note: These are suggested steps based on static analysis. Our tooling agent does not currently have the capability to run code to produce a working Proof of Concept.
An attacker with a compromised renderer process could potentially achieve a sandbox escape (RCE in the browser process) through the following sequence:
- Heap Grooming: The attacker sends
AXTreeUpdateIPC messages containing many nodes with large string attributes (likekNameorkDescription). These are allocated asBSTRobjects in the browser process, which use the same Windows OLE/COM heap asCoTaskMemAlloc. This allows the attacker to fill the heap with fake COM object pointers and fake vtables. - State Corruption: The attacker sends a crafted
AXTreeUpdatedesigned to trigger a reparenting edge case (e.g., exploiting crbug.com/40833630). This creates a state where anAXNodeexists logically in the tree but lacks a corresponding platform wrapper in theid_wrapper_map_. - Table Construction: The attacker constructs a table where the column headers reference the ID of the corrupted node alongside valid node IDs.
- Trigger: An external out-of-process accessibility client (like a screen reader) is induced to query
get_columnHeaderCellson the target table. - Vtable Dispatch: The browser returns the array containing uninitialized, attacker-controlled OLE heap memory. The Windows NDR (Network Data Representation) marshaller iterates over the array based on the inflated count and calls
QueryInterfaceandReleaseon the garbage pointers. This indirect call dereferences the attacker’s fake vtable, hijacking control flow.
Note that MiraclePtr does not protect against this issue, as the uninitialized bytes are processed directly by the OS-level COM marshaller, bypassing PartitionAlloc.
Suggested Fix
Update both get_columnHeaderCells and get_rowHeaderCells to set the output count to the number of successfully processed nodes (index) rather than the requested size.
// ui/accessibility/platform/ax_platform_node_win.cc
// In AXPlatformNodeWin::get_columnHeaderCells (approx line 4288)
- *n_column_header_cells = static_cast<LONG>(column_header_ids.size());
+ *n_column_header_cells = static_cast<LONG>(index);
// In AXPlatformNodeWin::get_rowHeaderCells (approx line 4344)
- *n_row_header_cells = static_cast<LONG>(row_header_ids.size());
+ *n_row_header_cells = static_cast<LONG>(index);
This correctly handles cases where nodes fail to resolve, identical to the pattern already safely used in AXPlatformNodeWin::get_targets.
Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33
Results from so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.