Overview

Medium
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactType confusion in Rust
DescriptionType confusion in Rust
ComponentRust
Sub-componentCrubit
Bug ClassType Confusion
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker539569491
Fix commit0eaa812cde99 (chromium/src) +5/-0
CWECWE-843 (Type confusion)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
Creditedmarcobartoli
Reported2026-07-27 (per vendor advisory)
Fix landed2026-08-04 (public git)
Disclosed2026-09-08
Patch gap35 days — fix public in source before the advisory

Background

Crubit
Chromium’s Rust/C++ interop tool that auto-generates C++ bindings for Rust code, invoked here through the cpp_api_from_rust.gni GN 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.

Key insight
The single mistake was building Chromium’s Crubit-generated Rust/C++ bindings without the corrected ABI mode, so on Windows/Android the generated interop code could pass or interpret values under a mismatched ABI (a type confusion at the FFI boundary); the fix simply opts in via the --portable-abi-compatible flag so Crubit emits ABI-correct bindings (including the extra thunks it now requires).

Attack Path

  1. Mismatched bindings are generated Without --portable-abi-compatible, Crubit generates C++↔Rust interop bindings from cpp_api_from_rust.gni using its old ABI handling, which is incorrect on the Windows/Android ABIs.
  2. 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)
  3. 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)
  4. 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

The advisory classifies this as a type confusion in Rust interop; an attacker who reaches an affected Rust↔C++ binding on the Windows or Android ABI could trigger a value being treated as the wrong type at the boundary. Whether this yields a usable read/write primitive or further escalation (memory corruption, code execution) is not demonstrated by the diff and is (inferred). The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 9.6. The precondition — presence of a Chromium build compiled without the fix on Windows/Android — is a build-configuration property rather than something shown in the changed code.

Files Changed

  • build/rust/gni_impl/cpp_api_from_rust.gni

Audit Directions

  • Toolchain/codegen flags
    Audit 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 boundaries
    Review 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 divergence
    Look 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.
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