CVE-2026-87528
Overview
Files Changed
build/rust/gni_impl/cpp_api_from_rust.gni
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.