Medium chrome Type Confusion 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactType confusion in Rust
DescriptionType confusion in Rust
ComponentRust
Bug ClassType Confusion
Tracker539569491
Fix commit0eaa812cde99 (chromium/src) +5/-0
CISA KEVNot listed
Creditedmarcobartoli
Disclosed2026-09-08

Files Changed

  • build/rust/gni_impl/cpp_api_from_rust.gni
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,
 
Loading diff…

Original Bug Report

reported by ma...@microsoft.com

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++ declaration
  • generate_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.

View on issue tracker