[clang] [CIR] Always set base subobject type for C++ records (PR #194504)

Bruno Cardoso Lopes via cfe-commits cfe-commits at lists.llvm.org
Mon Apr 27 18:08:45 PDT 2026


https://github.com/bcardosolopes created https://github.com/llvm/llvm-project/pull/194504

For unions and final C++ classes, computeRecordLayout previously left the base-subobject type as a default-constructed (null) cir::RecordType. The non-virtual size always equals the size for those kinds of records, so no separate ".base" variant is needed, but `BaseSubobjectType` itself was never set to the complete type either.

That null leaked through getCIRGenRecordLayout(RD).getBaseSubobjectCIRType(), which is called by getStorageType(const CXXRecordDecl *) when laying out a [[no_unique_address]] / potentially-overlapping field. The null mlir::Type was then stored as MemberInfo::data and tripped a SmallVector::back() !empty() assertion in CIRRecordLowering::fillOutputFields, because a member with null data is interpreted as a bitfield placeholder and reads fieldTypes.back() that was never pushed.

Match classic CodeGen (CGRecordLayoutBuilder.cpp) and unconditionally set baseTy = *ty for any CXXRecordDecl, only constructing a separate ".base" record when nv-size != size (which excludes unions and final classes by construction).

Reproducer:
```
  union U { int a; long b; };
  struct S {
    [[no_unique_address]] U u;
    int x;
  } s;
```

This comes from a crash on the libc++ test-suite (~1,500 tests affected, all crashing inside CIRRecordLowering::lower while laying out __compressed_pair_padding-style structs in <string>).

>From 069014416f709978c2091d286d67676f2c5bb2b1 Mon Sep 17 00:00:00 2001
From: Bruno Cardoso Lopes <bruno.cardoso at gmail.com>
Date: Mon, 27 Apr 2026 17:28:51 -0700
Subject: [PATCH] [CIR] Always set base subobject type for C++ records

For unions and final C++ classes, computeRecordLayout previously left the
base-subobject type as a default-constructed (null) cir::RecordType. The
non-virtual size always equals the size for those kinds of records, so no
separate ".base" variant is needed, but `BaseSubobjectType` itself was
never set to the complete type either.

That null leaked through getCIRGenRecordLayout(RD).getBaseSubobjectCIRType(),
which is called by getStorageType(const CXXRecordDecl *) when laying out a
[[no_unique_address]] / potentially-overlapping field. The null mlir::Type
was then stored as MemberInfo::data and tripped a SmallVector::back()
!empty() assertion in CIRRecordLowering::fillOutputFields, because a member
with null data is interpreted as a bitfield placeholder and reads
fieldTypes.back() that was never pushed.

Match classic CodeGen (CGRecordLayoutBuilder.cpp) and unconditionally set
baseTy = *ty for any CXXRecordDecl, only constructing a separate ".base"
record when nv-size != size (which excludes unions and final classes by
construction).

Reproducer:
  union U { int a; long b; };
  struct S {
    [[no_unique_address]] U u;
    int x;
  } s;

This was the #2 libcxx crash on the libc++ test-suite (~1,500 tests
affected, all crashing inside CIRRecordLowering::lower while laying out
__compressed_pair_padding-style structs in <string>).
---
 .../CIR/CodeGen/CIRGenRecordLayoutBuilder.cpp | 18 ++++--
 clang/test/CIR/CodeGen/no-unique-address.cpp  | 56 +++++++++++++++++++
 2 files changed, 69 insertions(+), 5 deletions(-)

diff --git a/clang/lib/CIR/CodeGen/CIRGenRecordLayoutBuilder.cpp b/clang/lib/CIR/CodeGen/CIRGenRecordLayoutBuilder.cpp
index 6947fd257f47f..8399d6a333006 100644
--- a/clang/lib/CIR/CodeGen/CIRGenRecordLayoutBuilder.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenRecordLayoutBuilder.cpp
@@ -685,13 +685,21 @@ CIRGenTypes::computeRecordLayout(const RecordDecl *rd, cir::RecordType *ty) {
   assert(ty->isIncomplete() && "recomputing record layout?");
   lowering.lower(/*nonVirtualBaseType=*/false);
 
-  // If we're in C++, compute the base subobject type.
+  // If we're in C++, compute the base subobject type. For C++ records the base
+  // subobject type is always set (matching classic CodeGen). For unions and
+  // final classes the base subobject and complete object types are identical
+  // (no tail padding can be reused), so baseTy points at the same record as
+  // ty. We must still populate baseTy in those cases because callers such as
+  // getStorageType(const CXXRecordDecl *) used to lay out potentially-
+  // overlapping ([[no_unique_address]]) fields read it unconditionally; a
+  // null baseTy would otherwise propagate as a null mlir::Type into the
+  // members vector and trip the !empty() assertion in fillOutputFields.
   cir::RecordType baseTy;
-  if (llvm::isa<CXXRecordDecl>(rd) && !rd->isUnion() &&
-      !rd->hasAttr<FinalAttr>()) {
+  if (llvm::isa<CXXRecordDecl>(rd)) {
     baseTy = *ty;
-    if (lowering.astRecordLayout.getNonVirtualSize() !=
-        lowering.astRecordLayout.getSize()) {
+    if (!rd->isUnion() && !rd->hasAttr<FinalAttr>() &&
+        lowering.astRecordLayout.getNonVirtualSize() !=
+            lowering.astRecordLayout.getSize()) {
       CIRRecordLowering baseLowering(*this, rd, /*Packed=*/lowering.packed);
       baseLowering.lower(/*NonVirtualBaseType=*/true);
       std::string baseIdentifier = getRecordTypeName(rd, ".base");
diff --git a/clang/test/CIR/CodeGen/no-unique-address.cpp b/clang/test/CIR/CodeGen/no-unique-address.cpp
index da64b5078ba53..82eef4d3a0838 100644
--- a/clang/test/CIR/CodeGen/no-unique-address.cpp
+++ b/clang/test/CIR/CodeGen/no-unique-address.cpp
@@ -1,6 +1,7 @@
 // RUN: %clang_cc1 -std=c++20 -triple x86_64-unknown-linux-gnu \
 // RUN:   -fclangir -emit-cir %s -o %t.cir
 // RUN: FileCheck --check-prefix=CIR --input-file=%t.cir %s
+// RUN: FileCheck --check-prefix=CIR-NUA --input-file=%t.cir %s
 // RUN: %clang_cc1 -std=c++20 -triple x86_64-unknown-linux-gnu \
 // RUN:   -fclangir -emit-llvm %s -o %t.ll
 // RUN: FileCheck --check-prefix=LLVM --input-file=%t.ll %s
@@ -38,6 +39,22 @@ struct Outer {
 // CIR:         cir.copy %{{.+}} to %[[M_COMPLETE]] skip_tail_padding : !cir.ptr<!rec_Middle>
 // CIR:         %[[EXTRA:.*]] = cir.get_member %[[THIS]][1] {name = "extra"} : !cir.ptr<!rec_Outer> -> !cir.ptr<!s8i>
 
+// Globals for the union/final NUA cases below (placed before LLVM-LABEL so
+// these DAG checks anchor to the top of the .ll file rather than to the
+// function body).
+// LLVM-DAG: %struct.OuterUnion = type { %union.UnionForNUA, i32 }
+// LLVM-DAG: %union.UnionForNUA = type { i64 }
+// LLVM-DAG: %struct.OuterFinal = type { %struct.FinalForNUA, i8 }
+// LLVM-DAG: %struct.FinalForNUA = type { i32, i8 }
+// LLVM-DAG: @ou = {{(dso_local )?}}global %struct.OuterUnion zeroinitializer, align 8
+// LLVM-DAG: @of = {{(dso_local )?}}global %struct.OuterFinal zeroinitializer, align 4
+// OGCG-DAG: %struct.OuterUnion = type { %union.UnionForNUA, i32 }
+// OGCG-DAG: %union.UnionForNUA = type { i64 }
+// OGCG-DAG: %struct.OuterFinal = type { %struct.FinalForNUA, i8 }
+// OGCG-DAG: %struct.FinalForNUA = type { i32, i8 }
+// OGCG-DAG: @ou = {{(dso_local )?}}global %struct.OuterUnion zeroinitializer, align 8
+// OGCG-DAG: @of = {{(dso_local )?}}global %struct.OuterFinal zeroinitializer, align 4
+
 // LLVM-LABEL: define {{.*}} void @_ZN5OuterC2ERK6Middlec(
 // LLVM:         %[[GEP:.*]] = getelementptr inbounds nuw %struct.Outer, ptr %{{.+}}, i32 0, i32 0
 // LLVM:         call void @llvm.memcpy.p0.p0.i64(ptr %[[GEP]], ptr %{{.+}}, i64 5, i1 false)
@@ -49,3 +66,42 @@ struct Outer {
 void test(const Middle &m) {
   Outer o(m, 'x');
 }
+
+// Regression test: a [[no_unique_address]] field whose type is a union (or a
+// final class) used to crash CIRRecordLowering with an empty SmallVector::back()
+// because computeRecordLayout left the base-subobject type unset for those
+// kinds of records, and getStorageType(const CXXRecordDecl *) propagated the
+// resulting null mlir::Type into the members vector. We now set baseTy = ty
+// for all C++ records, so these layouts succeed.
+
+union UnionForNUA {
+  int i;
+  long l;
+};
+
+struct OuterUnion {
+  [[no_unique_address]] UnionForNUA u;
+  int x;
+};
+
+OuterUnion ou;
+
+struct FinalForNUA final {
+  int a;
+  char b;
+};
+
+struct OuterFinal {
+  [[no_unique_address]] FinalForNUA f;
+  char tail;
+};
+
+OuterFinal of;
+
+// CIR-NUA-DAG: !rec_FinalForNUA = !cir.record<struct "FinalForNUA" {!s32i, !s8i}>
+// CIR-NUA-DAG: !rec_UnionForNUA = !cir.record<union "UnionForNUA" {!s32i, !s64i}>
+// CIR-NUA-DAG: !rec_OuterFinal = !cir.record<struct "OuterFinal" {!rec_FinalForNUA, !s8i}>
+// CIR-NUA-DAG: !rec_OuterUnion = !cir.record<struct "OuterUnion" {!rec_UnionForNUA, !s32i}>
+// CIR-NUA-DAG: cir.global external @ou = #cir.zero : !rec_OuterUnion
+// CIR-NUA-DAG: cir.global external @of = #cir.zero : !rec_OuterFinal
+



More information about the cfe-commits mailing list