CVE-2026-87528
Overview
Background
- Crubit
- Chromium’s Rust/C++ interop tool that auto-generates C++ bindings for Rust code, invoked here through the
cpp_api_from_rust.gniGN build template. - ABI (Application Binary Interface)
- the low-level, compiler-and-platform-specific contract for how types are laid out and how function arguments/return values are passed, which must match on both sides of a Rust↔C++ call or the two sides disagree about what the bytes mean.
- Thunk
- a small generated wrapper function that adapts a call from one calling convention/ABI to another; the patch comment notes that the fix “means that thunks are required in additional scenarios.”
- `--portable-abi-compatible`
- the Crubit command-line flag this patch adds, opting Chromium into Crubit’s corrected (portable) ABI handling for the generated bindings.
Root Cause Analysis
The diff adds a single build-time flag, --portable-abi-compatible, to the Crubit invocation constructed in build/rust/gni_impl/cpp_api_from_rust.gni, inserted alongside the existing --rs-out and --crubit-support-path-format arguments. Per the commit message, the pre-patch build did not pass this flag, so Crubit emitted C++/Rust interop bindings using its old ABI handling, which was incorrect on the Windows and Android ABIs; the added comment and message state that the corrected behavior “means that thunks are required in additional scenarios, which in turn means that some function pointers may not work anymore.” An ABI mismatch across the Rust↔C++ boundary means one side lays out or passes a value differently than the other side reads it — consistent with the advisory’s TypeConfusion / “Type confusion in Rust” classification, where a value is interpreted as the wrong type at the interop seam.
The fix works by opting into Crubit’s --portable-abi-compatible code generation so the generated thunks/bindings agree with the platform ABI on Windows/Android. The change is purely to the build configuration (the flag list passed to Crubit); the diff shows only an added argument, and contains no application-logic check, guard, or code path being tightened or relaxed. Note the diff itself does not show the incorrect generated code or the type-confusion sink — it shows only that the corrected code-generation mode is now enabled.
--portable-abi-compatible flag so Crubit emits ABI-correct bindings (including the extra thunks it now requires).Attack Path
- Mismatched bindings are generated
Without
--portable-abi-compatible, Crubit generates C++↔Rust interop bindings fromcpp_api_from_rust.gniusing its old ABI handling, which is incorrect on the Windows/Android ABIs. - Reach an affected interop call Code exercises a Rust↔C++ boundary whose generated binding relies on the affected ABI scenario (the commit notes thunks are needed in additional scenarios and that “some function pointers may not work anymore”); the specific vulnerable interop function is not identified in the diff. (inferred)
- Type confusion occurs at the boundary Because the two sides disagree on argument/return layout under the mismatched ABI, a value is passed or interpreted as the wrong type, matching the advisory’s “Type confusion in Rust” classification. (inferred — the diff does not show the confused types or the exact call site)
- Downstream memory corruption A type confusion of this kind can be leveraged into memory corruption or control-flow hijack, but the diff shows no specific out-of-bounds access, primitive, or sink. (inferred)
Impact Assessment
Files Changed
build/rust/gni_impl/cpp_api_from_rust.gni
Audit Directions
- Toolchain/codegen flagsAudit other build-time invocations of interop and code-generation tools (Crubit, bindgen, protobuf/mojo generators) for missing correctness or ABI-compatibility opt-in flags, since a wrong flag can silently produce unsafe generated code without any visible source-level defect.
- FFI ABI boundariesReview Rust↔C++ boundaries that pass function pointers, structs by value, or non-trivial return types, especially on Windows and Android, where the ABI scenarios that previously required (and lacked) thunks are most likely to surface as type confusion.
- Cross-platform ABI divergenceLook for generated or hand-written interop that behaves differently per target ABI; a binding that is correct on Linux but relies on platform-specific calling conventions is a candidate for the same class of layout/type mismatch.
Patch
From 0eaa812cde997b2bede5a6c1907f209e8345f430 Mon Sep 17 00:00:00 2001
From: Lukasz Anforowicz <lukasza@chromium.org>
Date: Tue, 04 Aug 2026 07:04:30 -0700
Subject: [PATCH] [crubit] Opt into Windows/Android ABI fix.
Crubit requires passing an opt-in command-line flag, because the fix
for the Windows/Android ABI issues is a breaking change (it means that
thunks are required in additional scenarios, which in turn means that
some function pointers may not work anymore). This CL adds the opt-in
`--portable-abi-compatible` flag to Chromium / Crubit integration.
Tested by running CQ dry run in https://crrev.com/c/8067856/12 - it
used to fail in earlier patchsets because of the Windows / Android ABI
issues - see https://crrev.com/c/8067856?checksPatchset=5&tab=checks
Bug: 539569491
Change-Id: Iaa6a462c2c4e7a5ca03e0367942ff0100ce096be
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8190935
Auto-Submit: Łukasz Anforowicz <lukasza@chromium.org>
Reviewed-by: Devon Loehr <dloehr@google.com>
Commit-Queue: Devon Loehr <dloehr@google.com>
Cr-Commit-Position: refs/heads/main@{#1673304}
---
diff --git a/build/rust/gni_impl/cpp_api_from_rust.gni b/build/rust/gni_impl/cpp_api_from_rust.gni
index 739922f4..65cfea7 100644
--- a/build/rust/gni_impl/cpp_api_from_rust.gni
+++ b/build/rust/gni_impl/cpp_api_from_rust.gni
@@ -169,6 +169,11 @@
"--rs-out",
rebase_path(_rs_out, root_build_dir),
+ # TODO(https://crbug.com/539569491): Remove this flag once/when it no
+ # longer works. (Crubit will remove this flag once its clients no longer
+ # depend on the old behavior.)
+ "--portable-abi-compatible",
+
"--crubit-support-path-format",
_crubit_support_path_format,
Original Bug Report
Crubit: Broken ABI on `*-pc-windows-msvc` thunks returning small types in wrong registers
Summary
cc_bindings_from_rs declares thunks that return Crubit C++ class types directly, e.g.:
extern "C" rs_std::StrRef __crubit_thunk_foo_uas_ustr();
Under the Microsoft C++ ABI, the one used by clang-cl for Chromium on Windows, any class with a user-provided constructor is returned through a
hidden out-pointer (x8 on AArch64, rcx on x86-64), even when it is trivially copyable and of
register size.
[[clang::trivial_abi]] is ignored when Microsoft C++ ABI is used, as it is only used in case of Itanium ABI.
Rust’s extern "C" returns those same values in registers. The result is a silent ABI mismatch: the
C++ caller passes a buffer pointer and reads back uninitialized stack memory while Rust writes the
result registers. No diagnostic fires, because generated headers
#pragma clang diagnostic ignored "-Wreturn-type-c-linkage".
Every rs_std vocabulary type and every Crubit-generated class has user-provided constructors, so
this is not a corner case.
Impact
Affects all *-pc-windows-msvc targets. The size of the value determines which ones diverge, which
is why this typically surfaces on ARM64 first:
| Return type | Size | rustc | C++ (Crubit class) | x86_64-msvc | aarch64-msvc |
|---|---|---|---|---|---|
&str, &[T] |
16 | registers | indirect | OK by coincidence (both indirect) | BROKEN |
char |
4 | i32 |
indirect | BROKEN | BROKEN |
#[repr(transparent)] ADT |
4/8/16 | registers | indirect | BROKEN | BROKEN |
Non-MSVC targets (aarch64-unknown-linux-gnu, aarch64-apple-darwin, x86_64-unknown-linux-gnu)
are unaffected, both sides return in registers.
Note that x86_64-pc-windows-msvc only appears healthy for &str: both sides independently choose
sret for 16 bytes. Smaller values are broken there too.
Arguments are unaffected on every target. This is strictly a return-position bug.
Root cause
1. What actually triggers indirect return
Classification is by user-provided constructor, not by triviality or size. = default on first
declaration is fine:
| Class shape (identical 16-byte layout) | aarch64-msvc |
|---|---|
| pure aggregate | [2 x i64] (registers) |
A() = default; / A(const A&) = default; / ~A() = default; |
[2 x i64] |
user-provided ctor (A() noexcept : ... {}) |
sret |
user-provided ctor + [[clang::trivial_abi]] |
sret |
rs_std::StrRef, rs_std::SliceRef<T> and rs_std::char_ all have user-provided constructors (the
consteval validating conversions, the UnsafePromiseUtf8 constructor, the dangling-pointer default
constructor). Crubit-generated classes have them too (T(), T(UnsafeRelocateTag, T&&)).
2. Where the two sides diverge
is_c_abi_compatible_by_value (cc_bindings_from_rs/generate_bindings/query_compiler.rs:41) returns
true for TyKind::Ref, TyKind::RawPtr, TyKind::Char, and #[repr(transparent)] TyKind::Adt.
It is then used to pick both sides of the thunk signature:
generate_function_thunk.rs:211: the C++ declarationgenerate_function_thunk.rs:620: the Rust implementation
The predicate is correct about value ABI compatibility, but it says nothing about whether the C++ type Crubit maps the value to uses the C return convention. On MSVC it does not.
Incidentally, the arm that looks like it should have caught this is unreachable:
// Slice references (`&[T]`, `&str`) are not guaranteed to be ABI-compatible when passed
// by-value.
ty::TyKind::Slice { .. } | ty::TyKind::Str => false,
&str is Ref(_, str, _), so it is matched by the Ref arm above and returns true. The intent to
exclude fat references was already there.
Reproduction
No Windows machine or MSVC toolchain required, clang cross-targets and both compilers can emit LLVM IR.
// t.cpp
using size_t = unsigned long long;
struct Pod { const char* ptr; size_t size; };
struct [[clang::trivial_abi]] StrRefLike { // shape of rs_std::StrRef
StrRefLike() noexcept : ptr(nullptr), size(0) {} // user-provided
StrRefLike(const StrRefLike&) = default;
~StrRefLike() = default;
const char* ptr; size_t size;
};
extern "C" Pod r_pod();
extern "C" StrRefLike r_strref();
Pod a() { return r_pod(); } StrRefLike b() { return r_strref(); }
$ clang++ --target=aarch64-pc-windows-msvc -std=c++20 -O1 -S -emit-llvm -o - t.cpp | grep '^declare'
declare dso_local [2 x i64] @r_pod()
declare dso_local void @r_strref(ptr inreg sret(%struct.StrRefLike) align 8)
// s.rs
#![crate_type = "lib"]
#[unsafe(no_mangle)] pub extern "C" fn t_str() -> &'static str { "foo" }
#[unsafe(no_mangle)] pub extern "C" fn t_char() -> char { 'a' }
$ rustup target add aarch64-pc-windows-msvc
$ rustc --target=aarch64-pc-windows-msvc --emit=llvm-ir -O --crate-type=lib -o out.ll s.rs
$ grep '^define' out.ll
define [2 x i64] @t_str()
define noundef range(i32 0, 1114112) i32 @t_char()
Rust returns [2 x i64]; C++ expects sret. Swapping the triple for aarch64-unknown-linux-gnu
makes both [2 x i64], which is why this is invisible on Linux CI.
To confirm against real generated bindings, run cc_bindings_from_rs over a crate exposing
pub fn f() -> &'static str and compile the emitted header with --target=aarch64-pc-windows-msvc.