[llvm] [BPF] Handle C++ record elements in BTF and CO-RE (PR #226717)

Maxim Skokov via llvm-commits llvm-commits at lists.llvm.org
Sun Sep 27 12:01:28 PDT 2026


https://github.com/maxskokov updated https://github.com/llvm/llvm-project/pull/226717

>From 5bd8f90035280c008a648bfe4264e91de7fb9034 Mon Sep 17 00:00:00 2001
From: Maxim Skokov <skokovmaksimevg at gmail.com>
Date: Sat, 26 Sep 2026 14:59:38 +0300
Subject: [PATCH 1/2] [BPF] Handle C++ record elements in BTF and CO-RE

A C++ record lists base classes, static data members and methods among
its debug info elements. Since #155783, BTF emission hits
llvm_unreachable() on any record element other than a member or a
variant part, and #205396 moved the crash into the offset sort; the
CO-RE pass and visitMapDefType() crash on them too.

BTF: emit a non-virtual base as an anonymous member of the base type at
its offset, as pahole does, and skip the other elements. Virtual bases
are skipped, as their offset is only known at run time.

CO-RE: fixing BTF alone would turn the crash into a silent miscompile,
since clang numbers the fields of a record while the backend indexed its
elements: for "struct S : Base { int x, y; }", &s->y got the offset of
x. Map the access index to the field and take the access string index
from the same member list BTF is built from. For an inherited field only
its position within the base is relocated; the offset of the base stays
a constant, or is loaded from the vtable for a virtual base. Report an
access into a record the debug info only declares as an error at the
access, and replace a field info call on it with 0.

Assisted-by: Claude Code (Claude Opus 5.5)
---
 .../Target/BPF/BPFAbstractMemberAccess.cpp    |  58 ++++--
 llvm/lib/Target/BPF/BPFCORE.h                 |  54 ++++++
 llvm/lib/Target/BPF/BTFDebug.cpp              |  29 ++-
 .../CodeGen/BPF/BTF/struct-cxx-elements.ll    | 142 +++++++++++++++
 .../CodeGen/BPF/CORE/struct-cxx-fields.ll     | 169 ++++++++++++++++++
 .../CodeGen/BPF/CORE/struct-cxx-fwd-decl.ll   |  59 ++++++
 6 files changed, 478 insertions(+), 33 deletions(-)
 create mode 100644 llvm/test/CodeGen/BPF/BTF/struct-cxx-elements.ll
 create mode 100644 llvm/test/CodeGen/BPF/CORE/struct-cxx-fields.ll
 create mode 100644 llvm/test/CodeGen/BPF/CORE/struct-cxx-fwd-decl.ll

diff --git a/llvm/lib/Target/BPF/BPFAbstractMemberAccess.cpp b/llvm/lib/Target/BPF/BPFAbstractMemberAccess.cpp
index f5410820bfb49..003459f56a1f6 100644
--- a/llvm/lib/Target/BPF/BPFAbstractMemberAccess.cpp
+++ b/llvm/lib/Target/BPF/BPFAbstractMemberAccess.cpp
@@ -82,6 +82,7 @@
 #include "llvm/BinaryFormat/Dwarf.h"
 #include "llvm/DebugInfo/BTF/BTF.h"
 #include "llvm/IR/DebugInfoMetadata.h"
+#include "llvm/IR/DiagnosticInfo.h"
 #include "llvm/IR/GlobalVariable.h"
 #include "llvm/IR/Instruction.h"
 #include "llvm/IR/Instructions.h"
@@ -298,6 +299,13 @@ static const DIType * stripQualifiers(const DIType *Ty) {
   return Ty;
 }
 
+/// Return the position of Field among the BTF members of record CTy.
+static uint64_t getBTFMemberIndex(const DICompositeType *CTy,
+                                  const DINode *Field) {
+  SmallVector<const DINode *, 8> Elements = getBTFRecordElements(CTy);
+  return llvm::find(Elements, Field) - Elements.begin();
+}
+
 static uint32_t calcArraySize(const DICompositeType *CTy, uint32_t StartDim) {
   DINodeArray Elements = CTy->getElements();
   uint32_t DimSize = 1;
@@ -528,7 +536,9 @@ bool BPFAbstractMemberAccess::IsValidAIChain(const MDNode *ParentType,
   if (PTyTag == dwarf::DW_TAG_array_type)
     Ty = PTy->getBaseType();
   else
-    Ty = dyn_cast<DIType>(PTy->getElements()[ParentAI]);
+    Ty = dyn_cast_or_null<DIType>(getDIRecordField(PTy, ParentAI));
+  if (!Ty)
+    return false;
 
   return dyn_cast<DICompositeType>(stripQualifiers(Ty)) == CTy;
 }
@@ -685,7 +695,7 @@ uint32_t BPFAbstractMemberAccess::GetFieldInfo(uint32_t InfoKind,
       PatchImm += AccessIndex * calcArraySize(CTy, 1) *
                   (EltTy->getSizeInBits() >> 3);
     } else if (Tag == dwarf::DW_TAG_structure_type) {
-      auto *MemberTy = cast<DIDerivedType>(CTy->getElements()[AccessIndex]);
+      auto *MemberTy = cast<DIDerivedType>(getDIRecordField(CTy, AccessIndex));
       if (!MemberTy->isBitField()) {
         PatchImm += MemberTy->getOffsetInBits() >> 3;
       } else {
@@ -703,7 +713,7 @@ uint32_t BPFAbstractMemberAccess::GetFieldInfo(uint32_t InfoKind,
       auto *EltTy = stripQualifiers(CTy->getBaseType());
       return calcArraySize(CTy, 1) * (EltTy->getSizeInBits() >> 3);
     } else {
-      auto *MemberTy = cast<DIDerivedType>(CTy->getElements()[AccessIndex]);
+      auto *MemberTy = cast<DIDerivedType>(getDIRecordField(CTy, AccessIndex));
       uint32_t SizeInBits = MemberTy->getSizeInBits();
       if (!MemberTy->isBitField())
         return SizeInBits >> 3;
@@ -726,7 +736,7 @@ uint32_t BPFAbstractMemberAccess::GetFieldInfo(uint32_t InfoKind,
         report_fatal_error("Invalid array expression for llvm.bpf.preserve.field.info");
       BaseTy = stripQualifiers(CTy->getBaseType());
     } else {
-      auto *MemberTy = cast<DIDerivedType>(CTy->getElements()[AccessIndex]);
+      auto *MemberTy = cast<DIDerivedType>(getDIRecordField(CTy, AccessIndex));
       BaseTy = stripQualifiers(MemberTy->getBaseType());
     }
 
@@ -758,7 +768,7 @@ uint32_t BPFAbstractMemberAccess::GetFieldInfo(uint32_t InfoKind,
       auto *EltTy = stripQualifiers(CTy->getBaseType());
       SizeInBits = calcArraySize(CTy, 1) * EltTy->getSizeInBits();
     } else {
-      MemberTy = cast<DIDerivedType>(CTy->getElements()[AccessIndex]);
+      MemberTy = cast<DIDerivedType>(getDIRecordField(CTy, AccessIndex));
       SizeInBits = MemberTy->getSizeInBits();
       IsBitField = MemberTy->isBitField();
     }
@@ -789,7 +799,7 @@ uint32_t BPFAbstractMemberAccess::GetFieldInfo(uint32_t InfoKind,
       auto *EltTy = stripQualifiers(CTy->getBaseType());
       SizeInBits = calcArraySize(CTy, 1) * EltTy->getSizeInBits();
     } else {
-      MemberTy = cast<DIDerivedType>(CTy->getElements()[AccessIndex]);
+      MemberTy = cast<DIDerivedType>(getDIRecordField(CTy, AccessIndex));
       SizeInBits = MemberTy->getSizeInBits();
       IsBitField = MemberTy->isBitField();
     }
@@ -965,19 +975,23 @@ Value *BPFAbstractMemberAccess::computeBaseAndAccessKey(CallInst *Call,
     // At this stage, it cannot be pointer type.
     auto *CTy = cast<DICompositeType>(stripQualifiers(cast<DIType>(MDN)));
 
+    // For a record, use the position of the field among its BTF members, and
+    // use an array index as is.
     uint64_t BTFIndex = AccessIndex;
-    if (CTy->getTag() == dwarf::DW_TAG_structure_type) {
-      DINodeArray Elements = CTy->getElements();
-      uint64_t Offset = getBTFRecordElementOffset(Elements[AccessIndex]);
-      // Find this element's position in the stable offset order without
-      // sorting the whole record for every CO-RE access.
-      BTFIndex = 0;
-      for (unsigned I = 0; I < Elements.size(); ++I) {
-        uint64_t ElementOffset = getBTFRecordElementOffset(Elements[I]);
-        if (ElementOffset < Offset ||
-            (ElementOffset == Offset && I < AccessIndex))
-          ++BTFIndex;
+    if (CTy->getTag() == dwarf::DW_TAG_structure_type ||
+        CTy->getTag() == dwarf::DW_TAG_union_type) {
+      const DINode *Field = getDIRecordField(CTy, AccessIndex);
+      if (!Field) {
+        Call->getContext().diagnose(DiagnosticInfoUnsupported(
+            *Call->getFunction(),
+            "CO-RE access to a field of '" + CTy->getName() +
+                "', which the debug info does not describe; the type may only "
+                "be declared there (e.g. clang's -fstandalone-debug emits it "
+                "in full)",
+            Call->getDebugLoc()));
+        return nullptr;
       }
+      BTFIndex = getBTFMemberIndex(CTy, Field);
     }
     AccessKey += ":" + std::to_string(BTFIndex);
 
@@ -1074,8 +1088,16 @@ bool BPFAbstractMemberAccess::transformGEPChain(CallInst *Call,
     TypeMeta = computeAccessKey(Call, CInfo, AccessKey, IsInt32Ret);
   } else {
     Base = computeBaseAndAccessKey(Call, CInfo, AccessKey, TypeMeta);
-    if (!Base)
+    if (!Base) {
+      // For a field info call, computeBaseAndAccessKey() only gives up after
+      // reporting an error, so the value of the call does not matter.
+      if (IsInt32Ret) {
+        Call->replaceAllUsesWith(ConstantInt::get(Call->getType(), 0));
+        Call->eraseFromParent();
+        return true;
+      }
       return false;
+    }
   }
 
   BasicBlock *BB = Call->getParent();
diff --git a/llvm/lib/Target/BPF/BPFCORE.h b/llvm/lib/Target/BPF/BPFCORE.h
index 5aa7695c0ecad..1a4d65b98e70b 100644
--- a/llvm/lib/Target/BPF/BPFCORE.h
+++ b/llvm/lib/Target/BPF/BPFCORE.h
@@ -9,6 +9,8 @@
 #ifndef LLVM_LIB_TARGET_BPF_BPFCORE_H
 #define LLVM_LIB_TARGET_BPF_BPFCORE_H
 
+#include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/SmallVector.h"
 #include "llvm/ADT/StringRef.h"
 #include "llvm/BinaryFormat/Dwarf.h"
 #include "llvm/IR/DebugInfoMetadata.h"
@@ -21,10 +23,47 @@ class BasicBlock;
 class Instruction;
 class Module;
 
+/// Whether a record element becomes a BTF member: data members, variant parts
+/// and non-virtual C++ bases, the latter as anonymous members. Virtual bases
+/// have no fixed offset.
+inline bool isBTFRecordElement(const DINode *Element) {
+  switch (Element->getTag()) {
+  case dwarf::DW_TAG_member:
+    return !cast<DIDerivedType>(Element)->isStaticMember();
+  case dwarf::DW_TAG_inheritance:
+    return !cast<DIDerivedType>(Element)->isVirtual();
+  case dwarf::DW_TAG_variant_part:
+    return true;
+  default:
+    return false;
+  }
+}
+
+/// Whether a record element is a field that a CO-RE access index counts.
+/// Clang does not count bases or its vtable pointer member ("_vptr$<class>").
+inline bool isDIRecordField(const DINode *Element) {
+  if (Element->getTag() == dwarf::DW_TAG_inheritance ||
+      !isBTFRecordElement(Element))
+    return false;
+  const auto *DTy = dyn_cast<DIDerivedType>(Element);
+  return !DTy || !(DTy->isArtificial() && DTy->getName().starts_with("_vptr$"));
+}
+
+/// Return the field a CO-RE access index refers to, or null if the debug info
+/// does not describe the record's fields.
+inline DINode *getDIRecordField(const DICompositeType *CTy,
+                                uint64_t AccessIndex) {
+  for (DINode *Element : CTy->getElements())
+    if (isDIRecordField(Element) && AccessIndex-- == 0)
+      return Element;
+  return nullptr;
+}
+
 /// Return the bit offset used to order an element of a BTF structure record.
 inline uint64_t getBTFRecordElementOffset(const DINode *Element) {
   switch (Element->getTag()) {
   case dwarf::DW_TAG_member:
+  case dwarf::DW_TAG_inheritance:
     return cast<DIDerivedType>(Element)->getOffsetInBits();
   case dwarf::DW_TAG_variant_part:
     return cast<DICompositeType>(Element)->getOffsetInBits();
@@ -33,6 +72,21 @@ inline uint64_t getBTFRecordElementOffset(const DINode *Element) {
   }
 }
 
+/// Return the BTF members of a record in BTF order: structure members by
+/// offset (BTF requires nondecreasing offsets), stable for equal offsets.
+inline SmallVector<const DINode *, 8>
+getBTFRecordElements(const DICompositeType *CTy) {
+  SmallVector<const DINode *, 8> Elements;
+  for (const DINode *Element : CTy->getElements())
+    if (isBTFRecordElement(Element))
+      Elements.push_back(Element);
+  if (CTy->getTag() == dwarf::DW_TAG_structure_type)
+    llvm::stable_sort(Elements, [](const DINode *LHS, const DINode *RHS) {
+      return getBTFRecordElementOffset(LHS) < getBTFRecordElementOffset(RHS);
+    });
+  return Elements;
+}
+
 class BPFCoreSharedInfo {
 public:
   enum BTFTypeIdFlag : uint32_t {
diff --git a/llvm/lib/Target/BPF/BTFDebug.cpp b/llvm/lib/Target/BPF/BTFDebug.cpp
index 7ddeedb838e87..303196db64496 100644
--- a/llvm/lib/Target/BPF/BTFDebug.cpp
+++ b/llvm/lib/Target/BPF/BTFDebug.cpp
@@ -566,7 +566,8 @@ void BTFTypeStruct::completeType(BTFDebug &BDebug) {
     struct BTF::BTFMember BTFMember;
 
     switch (Element->getTag()) {
-    case dwarf::DW_TAG_member: {
+    case dwarf::DW_TAG_member:
+    case dwarf::DW_TAG_inheritance: {
       const auto *DDTy = cast<DIDerivedType>(Element);
 
       BTFMember.NameOff = BDebug.addString(DDTy->getName());
@@ -976,14 +977,7 @@ int BTFDebug::genBTFTypeTags(const DIDerivedType *DTy, int BaseTypeId) {
 /// Handle structure/union types.
 void BTFDebug::visitStructType(const DICompositeType *CTy, bool IsStruct,
                                uint32_t &TypeId) {
-  DINodeArray DIElements = CTy->getElements();
-  SmallVector<const DINode *, 8> Elements(DIElements.begin(), DIElements.end());
-  // Structure elements must have nondecreasing offsets in BTF. Preserve DI
-  // order for union and variant-part records.
-  if (CTy->getTag() == dwarf::DW_TAG_structure_type)
-    llvm::stable_sort(Elements, [](const DINode *LHS, const DINode *RHS) {
-      return getBTFRecordElementOffset(LHS) < getBTFRecordElementOffset(RHS);
-    });
+  SmallVector<const DINode *, 8> Elements = getBTFRecordElements(CTy);
   uint32_t VLen = Elements.size();
   // Variant parts might have a discriminator. LLVM DI doesn't consider it as
   // an element and instead keeps it as a separate reference. But we represent
@@ -1022,7 +1016,8 @@ void BTFDebug::visitStructType(const DICompositeType *CTy, bool IsStruct,
   int FieldNo = 0;
   for (const auto *Element : Elements) {
     switch (Element->getTag()) {
-    case dwarf::DW_TAG_member: {
+    case dwarf::DW_TAG_member:
+    case dwarf::DW_TAG_inheritance: {
       const auto Elem = cast<DIDerivedType>(Element);
       visitTypeEntry(Elem);
       processDeclAnnotations(Elem->getAnnotations(), TypeId, FieldNo);
@@ -1205,14 +1200,14 @@ void BTFDebug::visitDerivedType(const DIDerivedType *DTy, uint32_t &TypeId,
              Tag == dwarf::DW_TAG_restrict_type) {
     auto TypeEntry = std::make_unique<BTFTypeDerived>(DTy, Tag, false);
     TypeId = addType(std::move(TypeEntry), DTy);
-  } else if (Tag != dwarf::DW_TAG_member) {
+  } else if (Tag != dwarf::DW_TAG_member && Tag != dwarf::DW_TAG_inheritance) {
     return;
   }
 
-  // Visit base type of pointer, typedef, const, volatile, restrict or
-  // struct/union member.
+  // Visit base type of pointer, typedef, const, volatile, restrict, or
+  // struct/union member or base class.
   uint32_t TempTypeId = 0;
-  if (Tag == dwarf::DW_TAG_member)
+  if (Tag == dwarf::DW_TAG_member || Tag == dwarf::DW_TAG_inheritance)
     visitTypeEntry(DTy->getBaseType(), TempTypeId, true, false);
   else
     visitTypeEntry(DTy->getBaseType(), TempTypeId, CheckPointer, SeenPointer);
@@ -1328,7 +1323,11 @@ void BTFDebug::visitMapDefType(const DIType *Ty, uint32_t &TypeId) {
     const auto *CTy = cast<DICompositeType>(Ty);
     const DINodeArray Elements = CTy->getElements();
     for (const auto *Element : Elements) {
-      const auto *MemberType = cast<DIDerivedType>(Element);
+      // Static data members, methods and other elements of a C++ record are
+      // not part of the map definition.
+      const auto *MemberType = dyn_cast<DIDerivedType>(Element);
+      if (!MemberType || !isBTFRecordElement(Element))
+        continue;
       const DIType *MemberBaseType = MemberType->getBaseType();
       // If the member is a composite type, that may indicate the currently
       // visited composite type is a wrapper, and the member represents the
diff --git a/llvm/test/CodeGen/BPF/BTF/struct-cxx-elements.ll b/llvm/test/CodeGen/BPF/BTF/struct-cxx-elements.ll
new file mode 100644
index 0000000000000..9f67dd3575ccf
--- /dev/null
+++ b/llvm/test/CodeGen/BPF/BTF/struct-cxx-elements.ll
@@ -0,0 +1,142 @@
+; RUN: llc -mtriple=bpfel -filetype=obj -o %t1 %s
+; RUN: llvm-objcopy --dump-section='.BTF'=%t2 %t1
+; RUN: %python %p/print_btf.py %t2 | FileCheck %s
+; RUN: llc -mtriple=bpfeb -filetype=obj -o %t1 %s
+; RUN: llvm-objcopy --dump-section='.BTF'=%t2 %t1
+; RUN: %python %p/print_btf.py %t2 | FileCheck %s
+
+; C++ records list base classes, static data members and methods among their
+; elements. A non-virtual base becomes an anonymous member of the base type at
+; its offset, so the layout stays complete. Static data members and methods
+; are dropped, and so is a virtual base: its offset is only known at run time,
+; and the DI offset of a virtual base is the position of its offset in the
+; vtable. The declaration tag index counts the emitted members. An empty base
+; shares the offset of the first field and comes first, a base before
+; bit-fields gets bitfield_size=0, and map definitions skip the same elements.
+;
+; struct Base { int b; };
+; struct Base2 { int c; };
+; struct S : Base, Base2 {
+;   static int s1;       // DW_TAG_variable
+;   int x;
+;   void m();
+;   static int s2;       // DW_TAG_member with DIFlagStaticMember
+;   int y __attribute__((btf_decl_tag("y_tag")));
+; };
+; struct V : virtual Base { int v; };   // vptr at 0, v at 8, Base at 12
+; struct Empty {};
+; struct E : Empty { int e; };          // Empty and e both at offset 0
+; struct BF : Base { int x : 3; int y : 5; };
+; struct Map { int *key; void lookup(); static int instances; int *value; };
+; S value;
+; V vvalue;
+; E evalue;
+; BF bfvalue;
+; Map map __attribute__((section(".maps")));
+;
+; The vptr member of V ('_vptr$V') is left out of the debug info below on
+; purpose: its name is not valid in BTF, which is dealt with separately.
+
+; CHECK:      [1] PTR '(anon)' type_id=2
+; CHECK-NEXT: [2] INT 'int' size=4 bits_offset=0 nr_bits=32 encoding=SIGNED
+; CHECK-NEXT: [3] STRUCT 'Map' size=16 vlen=2
+; CHECK-NEXT:         'key' type_id=1 bits_offset=0
+; CHECK-NEXT:         'value' type_id=1 bits_offset=64
+; CHECK-NEXT: [4] VAR 'map' type_id=3, linkage=global
+; CHECK-NEXT: [5] STRUCT 'S' size=16 vlen=4
+; CHECK-NEXT:         '(anon)' type_id=6 bits_offset=0
+; CHECK-NEXT:         '(anon)' type_id=7 bits_offset=32
+; CHECK-NEXT:         'x' type_id=2 bits_offset=64
+; CHECK-NEXT:         'y' type_id=2 bits_offset=96
+; CHECK-NEXT: [6] STRUCT 'Base' size=4 vlen=1
+; CHECK-NEXT:         'b' type_id=2 bits_offset=0
+; CHECK-NEXT: [7] STRUCT 'Base2' size=4 vlen=1
+; CHECK-NEXT:         'c' type_id=2 bits_offset=0
+; CHECK-NEXT: [8] DECL_TAG 'y_tag' type_id=5 component_idx=3
+; CHECK-NEXT: [9] VAR 'value' type_id=5, linkage=global
+; CHECK-NEXT: [10] STRUCT 'V' size=16 vlen=1
+; CHECK-NEXT:         'v' type_id=2 bits_offset=64
+; CHECK-NEXT: [11] VAR 'vvalue' type_id=10, linkage=global
+; CHECK-NEXT: [12] STRUCT 'E' size=4 vlen=2
+; CHECK-NEXT:         '(anon)' type_id=13 bits_offset=0
+; CHECK-NEXT:         'e' type_id=2 bits_offset=0
+; CHECK-NEXT: [13] STRUCT 'Empty' size=1 vlen=0
+; CHECK-NEXT: [14] VAR 'evalue' type_id=12, linkage=global
+; CHECK-NEXT: [15] STRUCT 'BF' size=8 vlen=3
+; CHECK-NEXT:         '(anon)' type_id=6 bits_offset=0 bitfield_size=0
+; CHECK-NEXT:         'x' type_id=2 bits_offset=32 bitfield_size=3
+; CHECK-NEXT:         'y' type_id=2 bits_offset=35 bitfield_size=5
+; CHECK-NEXT: [16] VAR 'bfvalue' type_id=15, linkage=global
+
+%struct.S = type { i32, i32, i32, i32 }
+%struct.V = type { i32, i32, i32, i32 }
+
+ at value = global %struct.S zeroinitializer, align 4, !dbg !0
+ at vvalue = global %struct.V zeroinitializer, align 4, !dbg !40
+ at evalue = global i32 0, align 4, !dbg !100
+ at bfvalue = global i64 0, align 4, !dbg !110
+ at map = global [2 x ptr] zeroinitializer, section ".maps", align 8, !dbg !120
+
+!llvm.dbg.cu = !{!2}
+!llvm.module.flags = !{!30, !31}
+
+!0 = !DIGlobalVariableExpression(var: !1, expr: !DIExpression())
+!1 = distinct !DIGlobalVariable(name: "value", scope: !2, file: !3, line: 12, type: !5, isLocal: false, isDefinition: true)
+!2 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus_14, file: !3, producer: "clang", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug, globals: !4)
+!3 = !DIFile(filename: "test.cpp", directory: "/")
+!4 = !{!0, !40, !100, !110, !120}
+!5 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "S", file: !3, line: 3, size: 128, elements: !6)
+!6 = !{!7, !23, !11, !12, !13, !17, !18}
+!7 = !DIDerivedType(tag: DW_TAG_inheritance, scope: !5, baseType: !8, extraData: i32 0)
+!8 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "Base", file: !3, line: 1, size: 32, elements: !9)
+!9 = !{!10}
+!10 = !DIDerivedType(tag: DW_TAG_member, name: "b", scope: !8, file: !3, line: 1, baseType: !20, size: 32)
+!11 = !DIDerivedType(tag: DW_TAG_variable, name: "s1", scope: !5, file: !3, line: 4, baseType: !20, flags: DIFlagStaticMember)
+!12 = !DIDerivedType(tag: DW_TAG_member, name: "x", scope: !5, file: !3, line: 5, baseType: !20, size: 32, offset: 64)
+!13 = !DISubprogram(name: "m", linkageName: "_ZN1S1mEv", scope: !5, file: !3, line: 6, type: !14, scopeLine: 6, flags: DIFlagPrototyped, spFlags: 0)
+!14 = !DISubroutineType(types: !15)
+!15 = !{null, !16}
+!16 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !5, size: 64, flags: DIFlagArtificial | DIFlagObjectPointer)
+!17 = !DIDerivedType(tag: DW_TAG_member, name: "s2", scope: !5, file: !3, line: 7, baseType: !20, flags: DIFlagStaticMember)
+!18 = !DIDerivedType(tag: DW_TAG_member, name: "y", scope: !5, file: !3, line: 8, baseType: !20, size: 32, offset: 96, annotations: !21)
+!20 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!21 = !{!22}
+!22 = !{!"btf_decl_tag", !"y_tag"}
+!23 = !DIDerivedType(tag: DW_TAG_inheritance, scope: !5, baseType: !24, offset: 32, extraData: i32 0)
+!24 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "Base2", file: !3, line: 2, size: 32, elements: !25)
+!25 = !{!26}
+!26 = !DIDerivedType(tag: DW_TAG_member, name: "c", scope: !24, file: !3, line: 2, baseType: !20, size: 32)
+!30 = !{i32 2, !"Debug Info Version", i32 3}
+!31 = !{i32 2, !"Dwarf Version", i32 5}
+!40 = !DIGlobalVariableExpression(var: !41, expr: !DIExpression())
+!41 = distinct !DIGlobalVariable(name: "vvalue", scope: !2, file: !3, line: 13, type: !42, isLocal: false, isDefinition: true)
+!42 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "V", file: !3, line: 10, size: 128, elements: !43)
+!43 = !{!44, !45}
+!44 = !DIDerivedType(tag: DW_TAG_inheritance, scope: !42, baseType: !8, offset: 24, flags: DIFlagVirtual, extraData: i32 0)
+!45 = !DIDerivedType(tag: DW_TAG_member, name: "v", scope: !42, file: !3, line: 10, baseType: !20, size: 32, offset: 64)
+!100 = !DIGlobalVariableExpression(var: !101, expr: !DIExpression())
+!101 = distinct !DIGlobalVariable(name: "evalue", scope: !2, file: !3, line: 16, type: !102, isLocal: false, isDefinition: true)
+!102 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "E", file: !3, line: 15, size: 32, elements: !103)
+!103 = !{!104, !106}
+!104 = !DIDerivedType(tag: DW_TAG_inheritance, scope: !102, baseType: !105, extraData: i32 0)
+!105 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "Empty", file: !3, line: 14, size: 8, elements: !107)
+!106 = !DIDerivedType(tag: DW_TAG_member, name: "e", scope: !102, file: !3, line: 15, baseType: !20, size: 32)
+!107 = !{}
+!110 = !DIGlobalVariableExpression(var: !111, expr: !DIExpression())
+!111 = distinct !DIGlobalVariable(name: "bfvalue", scope: !2, file: !3, line: 18, type: !112, isLocal: false, isDefinition: true)
+!112 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "BF", file: !3, line: 17, size: 64, elements: !113)
+!113 = !{!114, !115, !116}
+!114 = !DIDerivedType(tag: DW_TAG_inheritance, scope: !112, baseType: !8, extraData: i32 0)
+!115 = !DIDerivedType(tag: DW_TAG_member, name: "x", scope: !112, file: !3, line: 17, baseType: !20, size: 3, offset: 32, flags: DIFlagBitField, extraData: i64 32)
+!116 = !DIDerivedType(tag: DW_TAG_member, name: "y", scope: !112, file: !3, line: 17, baseType: !20, size: 5, offset: 35, flags: DIFlagBitField, extraData: i64 32)
+!120 = !DIGlobalVariableExpression(var: !121, expr: !DIExpression())
+!121 = distinct !DIGlobalVariable(name: "map", scope: !2, file: !3, line: 20, type: !122, isLocal: false, isDefinition: true)
+!122 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "Map", file: !3, line: 19, size: 128, elements: !123)
+!123 = !{!124, !125, !126, !127}
+!124 = !DIDerivedType(tag: DW_TAG_member, name: "key", scope: !122, file: !3, line: 19, baseType: !128, size: 64)
+!125 = !DISubprogram(name: "lookup", scope: !122, file: !3, line: 19, type: !129, spFlags: 0)
+!126 = !DIDerivedType(tag: DW_TAG_variable, name: "instances", scope: !122, file: !3, line: 19, baseType: !20, flags: DIFlagStaticMember)
+!127 = !DIDerivedType(tag: DW_TAG_member, name: "value", scope: !122, file: !3, line: 19, baseType: !128, size: 64, offset: 64)
+!128 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !20, size: 64)
+!129 = !DISubroutineType(types: !130)
+!130 = !{null}
diff --git a/llvm/test/CodeGen/BPF/CORE/struct-cxx-fields.ll b/llvm/test/CodeGen/BPF/CORE/struct-cxx-fields.ll
new file mode 100644
index 0000000000000..de26896113b10
--- /dev/null
+++ b/llvm/test/CodeGen/BPF/CORE/struct-cxx-fields.ll
@@ -0,0 +1,169 @@
+; RUN: opt -O2 %s -S | FileCheck %s
+; RUN: opt -O2 %s | llc -mtriple=bpfel -filetype=asm -o - | FileCheck %s --check-prefix=BTF
+
+; Clang numbers the fields of a record for a CO-RE access, while the debug
+; info of a C++ record also lists base classes, static data members, methods
+; and the vtable pointer among its elements. The access index is mapped to the
+; field it counts, and the access string index is the position of that field
+; among the BTF members, where non-virtual bases come first as anonymous
+; members and the vtable pointer is a member too. The name of each relocation
+; global encodes <kind>:<patched value>$<access string>.
+;
+; struct Base { int b; };
+; struct Base2 { int c; };
+; struct S : Base, Base2 {
+;   static int s1; int x; void m(); static int s2; int y;
+; };
+; struct V : virtual Base { int v; };   // vptr at 0, v at 8, Base at 12
+; struct Empty {};
+; struct E : Empty { int e; };          // Empty and e both at offset 0
+; union U { int a; static int s; long b; void m(); };
+; struct BF : Base {
+;   void m(); unsigned x : 3; int y : 5; static int s; int z;
+; };
+;
+; &s->x, &s->y, &v->v, &e->e, &u->b, and field info of bf->x, bf->y, bf->z
+
+; CHECK-DAG: @"llvm.S:0:8$0:2" =
+; CHECK-DAG: @"llvm.S:0:12$0:3" =
+; CHECK-DAG: @"llvm.V:0:8$0:1" =
+; CHECK-DAG: @"llvm.E:0:0$0:1" =
+; CHECK-DAG: @"llvm.U:0:0$0:1" =
+; CHECK-DAG: @"llvm.BF:1:4$0:2" =
+; CHECK-DAG: @"llvm.BF:3:1$0:2" =
+; CHECK-DAG: @"llvm.BF:3:0$0:1" =
+; CHECK-DAG: @"llvm.BF:4:56$0:2" =
+; CHECK-DAG: @"llvm.BF:0:8$0:3" =
+
+; The relocation for &s->y names member 3 of the BTF struct S, which is y.
+; S is the struct whose name is at string offset 1, and 83 is 'S'.
+; BTF:      .long   1                               # BTF_KIND_STRUCT(id = [[S_ID:[0-9]+]])
+; BTF:      .byte   83                              # string offset=1
+; BTF:      .ascii  "0:3"                           # string offset=[[Y_ACCESS:[0-9]+]]
+; BTF:      .section        .BTF.ext
+; BTF:      .long   [[S_ID]]{{\n}}{{[[:space:]]+}}.long   [[Y_ACCESS]]{{\n}}{{[[:space:]]+}}.long   0
+
+target triple = "bpf"
+
+%struct.Base = type { i32 }
+%struct.Base2 = type { i32 }
+%struct.S = type { %struct.Base, %struct.Base2, i32, i32 }
+%struct.V = type <{ ptr, i32, %struct.Base, [4 x i8] }>
+%struct.E = type { i32 }
+%struct.BF = type { %struct.Base, i8, i32 }
+
+define ptr @s_x(ptr %p) {
+  %r = call ptr @llvm.preserve.struct.access.index.p0.p0(ptr elementtype(%struct.S) %p, i32 2, i32 0), !llvm.preserve.access.index !10
+  ret ptr %r
+}
+
+define ptr @s_y(ptr %p) !dbg !61 {
+  %r = call ptr @llvm.preserve.struct.access.index.p0.p0(ptr elementtype(%struct.S) %p, i32 3, i32 1), !dbg !64, !llvm.preserve.access.index !10
+  ret ptr %r
+}
+
+define ptr @v_v(ptr %p) {
+  %r = call ptr @llvm.preserve.struct.access.index.p0.p0(ptr elementtype(%struct.V) %p, i32 1, i32 0), !llvm.preserve.access.index !20
+  ret ptr %r
+}
+
+define ptr @e_e(ptr %p) {
+  %r = call ptr @llvm.preserve.struct.access.index.p0.p0(ptr elementtype(%struct.E) %p, i32 0, i32 0), !llvm.preserve.access.index !30
+  ret ptr %r
+}
+
+define ptr @u_b(ptr %p) {
+  %r = call ptr @llvm.preserve.union.access.index.p0.p0(ptr %p, i32 1), !llvm.preserve.access.index !40
+  ret ptr %r
+}
+
+define i32 @bf_y_size(ptr %p) {
+  %f = call ptr @llvm.preserve.struct.access.index.p0.p0(ptr elementtype(%struct.BF) %p, i32 1, i32 1), !llvm.preserve.access.index !50
+  %r = call i32 @llvm.bpf.preserve.field.info.p0(ptr %f, i64 1)
+  ret i32 %r
+}
+
+define i32 @bf_y_signed(ptr %p) {
+  %f = call ptr @llvm.preserve.struct.access.index.p0.p0(ptr elementtype(%struct.BF) %p, i32 1, i32 1), !llvm.preserve.access.index !50
+  %r = call i32 @llvm.bpf.preserve.field.info.p0(ptr %f, i64 3)
+  ret i32 %r
+}
+
+define i32 @bf_x_signed(ptr %p) {
+  %f = call ptr @llvm.preserve.struct.access.index.p0.p0(ptr elementtype(%struct.BF) %p, i32 1, i32 0), !llvm.preserve.access.index !50
+  %r = call i32 @llvm.bpf.preserve.field.info.p0(ptr %f, i64 3)
+  ret i32 %r
+}
+
+define i32 @bf_y_lshift(ptr %p) {
+  %f = call ptr @llvm.preserve.struct.access.index.p0.p0(ptr elementtype(%struct.BF) %p, i32 1, i32 1), !llvm.preserve.access.index !50
+  %r = call i32 @llvm.bpf.preserve.field.info.p0(ptr %f, i64 4)
+  ret i32 %r
+}
+
+define i32 @bf_z_offset(ptr %p) {
+  %f = call ptr @llvm.preserve.struct.access.index.p0.p0(ptr elementtype(%struct.BF) %p, i32 2, i32 2), !llvm.preserve.access.index !50
+  %r = call i32 @llvm.bpf.preserve.field.info.p0(ptr %f, i64 0)
+  ret i32 %r
+}
+
+declare ptr @llvm.preserve.struct.access.index.p0.p0(ptr, i32 immarg, i32 immarg)
+declare ptr @llvm.preserve.union.access.index.p0.p0(ptr, i32 immarg)
+declare i32 @llvm.bpf.preserve.field.info.p0(ptr, i64 immarg)
+
+!llvm.dbg.cu = !{!60}
+!llvm.module.flags = !{!0}
+
+!0 = !{i32 2, !"Debug Info Version", i32 3}
+!1 = !DIFile(filename: "test.cpp", directory: "/")
+!2 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!3 = !DIBasicType(name: "unsigned int", size: 32, encoding: DW_ATE_unsigned)
+!4 = !DIBasicType(name: "long", size: 64, encoding: DW_ATE_signed)
+!5 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "Base", file: !1, size: 32, elements: !6)
+!6 = !{!7}
+!7 = !DIDerivedType(tag: DW_TAG_member, name: "b", scope: !5, file: !1, baseType: !2, size: 32)
+!8 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "Base2", file: !1, size: 32, elements: !9)
+!9 = !{!17}
+!10 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "S", file: !1, size: 128, elements: !11)
+!11 = !{!12, !13, !14, !15, !16, !18, !19}
+!12 = !DIDerivedType(tag: DW_TAG_inheritance, scope: !10, baseType: !5, extraData: i32 0)
+!13 = !DIDerivedType(tag: DW_TAG_inheritance, scope: !10, baseType: !8, offset: 32, extraData: i32 0)
+!14 = !DIDerivedType(tag: DW_TAG_variable, name: "s1", scope: !10, file: !1, baseType: !2, flags: DIFlagStaticMember)
+!15 = !DIDerivedType(tag: DW_TAG_member, name: "x", scope: !10, file: !1, baseType: !2, size: 32, offset: 64)
+!16 = !DIDerivedType(tag: DW_TAG_variable, name: "s2", scope: !10, file: !1, baseType: !2, flags: DIFlagStaticMember)
+!17 = !DIDerivedType(tag: DW_TAG_member, name: "c", scope: !8, file: !1, baseType: !2, size: 32)
+!18 = !DIDerivedType(tag: DW_TAG_member, name: "y", scope: !10, file: !1, baseType: !2, size: 32, offset: 96)
+!19 = !DISubprogram(name: "m", scope: !10, file: !1, type: !28, spFlags: 0)
+!20 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "V", file: !1, size: 192, elements: !21)
+!21 = !{!22, !23, !25}
+!22 = !DIDerivedType(tag: DW_TAG_inheritance, scope: !20, baseType: !5, offset: 24, flags: DIFlagVirtual, extraData: i32 0)
+!23 = !DIDerivedType(tag: DW_TAG_member, name: "_vptr$V", scope: !1, file: !1, baseType: !24, size: 64, flags: DIFlagArtificial)
+!24 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: null, size: 64)
+!25 = !DIDerivedType(tag: DW_TAG_member, name: "v", scope: !20, file: !1, baseType: !2, size: 32, offset: 64)
+!26 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "Empty", file: !1, size: 8, elements: !27)
+!27 = !{}
+!28 = !DISubroutineType(types: !29)
+!29 = !{null}
+!30 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "E", file: !1, size: 32, elements: !31)
+!31 = !{!32, !33}
+!32 = !DIDerivedType(tag: DW_TAG_inheritance, scope: !30, baseType: !26, extraData: i32 0)
+!33 = !DIDerivedType(tag: DW_TAG_member, name: "e", scope: !30, file: !1, baseType: !2, size: 32)
+!40 = distinct !DICompositeType(tag: DW_TAG_union_type, name: "U", file: !1, size: 64, elements: !41)
+!41 = !{!42, !43, !44, !45}
+!42 = !DIDerivedType(tag: DW_TAG_member, name: "a", scope: !40, file: !1, baseType: !2, size: 32)
+!43 = !DIDerivedType(tag: DW_TAG_variable, name: "s", scope: !40, file: !1, baseType: !2, flags: DIFlagStaticMember)
+!44 = !DIDerivedType(tag: DW_TAG_member, name: "b", scope: !40, file: !1, baseType: !4, size: 64)
+!45 = !DISubprogram(name: "m", scope: !40, file: !1, type: !28, spFlags: 0)
+!50 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "BF", file: !1, size: 96, elements: !51)
+!51 = !{!52, !53, !54, !55, !56, !57}
+!52 = !DIDerivedType(tag: DW_TAG_inheritance, scope: !50, baseType: !5, extraData: i32 0)
+!53 = !DISubprogram(name: "m", scope: !50, file: !1, type: !28, spFlags: 0)
+!54 = !DIDerivedType(tag: DW_TAG_member, name: "x", scope: !50, file: !1, baseType: !3, size: 3, offset: 32, flags: DIFlagBitField, extraData: i64 32)
+!55 = !DIDerivedType(tag: DW_TAG_member, name: "y", scope: !50, file: !1, baseType: !2, size: 5, offset: 35, flags: DIFlagBitField, extraData: i64 32)
+!56 = !DIDerivedType(tag: DW_TAG_variable, name: "s", scope: !50, file: !1, baseType: !2, flags: DIFlagStaticMember)
+!57 = !DIDerivedType(tag: DW_TAG_member, name: "z", scope: !50, file: !1, baseType: !2, size: 32, offset: 64)
+!60 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus_14, file: !1, isOptimized: true, runtimeVersion: 0, emissionKind: FullDebug)
+!61 = distinct !DISubprogram(name: "s_y", scope: !1, file: !1, type: !62, spFlags: DISPFlagDefinition, unit: !60)
+!62 = !DISubroutineType(types: !63)
+!63 = !{null}
+!64 = !DILocation(line: 1, scope: !61)
diff --git a/llvm/test/CodeGen/BPF/CORE/struct-cxx-fwd-decl.ll b/llvm/test/CodeGen/BPF/CORE/struct-cxx-fwd-decl.ll
new file mode 100644
index 0000000000000..b63b2495f1c3f
--- /dev/null
+++ b/llvm/test/CodeGen/BPF/CORE/struct-cxx-fwd-decl.ll
@@ -0,0 +1,59 @@
+; RUN: not opt -O2 %s -o /dev/null 2>&1 | FileCheck %s
+
+; Clang often only declares a C++ record in the debug info of a translation
+; unit, e.g. a class whose vtable or constructor is emitted elsewhere. A CO-RE
+; access to one of its fields cannot be described and is reported as an error
+; at the access instead of crashing, and a field info call on it is dropped.
+;
+; struct Base { int pad; int b; };
+; struct V : virtual Base { int v; };
+; unsigned long get_v(V *v) {
+;   return (unsigned long)__builtin_preserve_access_index(&v->v);
+; }
+; unsigned info_v(V *v) { return __builtin_preserve_field_info(v->v, 0); }
+
+; CHECK: error: test.cpp:3:1: in function get_v i64 (ptr): CO-RE access to a field of 'V', which the debug info does not describe; the type may only be declared there (e.g. clang's -fstandalone-debug emits it in full)
+; CHECK: error: test.cpp:4:1: in function info_v i32 (ptr): CO-RE access to a field of 'V', which the debug info does not describe
+
+target triple = "bpf"
+
+%struct.V = type <{ ptr, i32, %struct.Base, [4 x i8] }>
+%struct.Base = type { i32, i32 }
+
+define dso_local i64 @get_v(ptr %v) !dbg !10 {
+entry:
+  %0 = call ptr @llvm.preserve.struct.access.index.p0.p0(ptr elementtype(%struct.V) %v, i32 1, i32 0), !dbg !14, !llvm.preserve.access.index !15
+  %1 = ptrtoint ptr %0 to i64, !dbg !14
+  ret i64 %1, !dbg !14
+}
+
+define dso_local i32 @info_v(ptr %v) !dbg !20 {
+entry:
+  %0 = call ptr @llvm.preserve.struct.access.index.p0.p0(ptr elementtype(%struct.V) %v, i32 1, i32 0), !dbg !21, !llvm.preserve.access.index !15
+  %1 = call i32 @llvm.bpf.preserve.field.info.p0(ptr %0, i64 0), !dbg !21
+  ret i32 %1, !dbg !21
+}
+
+declare ptr @llvm.preserve.struct.access.index.p0.p0(ptr, i32 immarg, i32 immarg)
+declare i32 @llvm.bpf.preserve.field.info.p0(ptr, i64 immarg)
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!3, !4}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus_14, file: !1, producer: "clang", isOptimized: true, runtimeVersion: 0, emissionKind: FullDebug, enums: !2)
+!1 = !DIFile(filename: "test.cpp", directory: "/")
+!2 = !{}
+!3 = !{i32 7, !"Dwarf Version", i32 5}
+!4 = !{i32 2, !"Debug Info Version", i32 3}
+!10 = distinct !DISubprogram(name: "get_v", scope: !1, file: !1, line: 3, type: !11, scopeLine: 3, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0)
+!11 = !DISubroutineType(types: !12)
+!12 = !{!13, !16}
+!13 = !DIBasicType(name: "unsigned long", size: 64, encoding: DW_ATE_unsigned)
+!14 = !DILocation(line: 3, column: 1, scope: !10)
+!15 = !DICompositeType(tag: DW_TAG_structure_type, name: "V", file: !1, line: 2, flags: DIFlagFwdDecl | DIFlagNonTrivial, identifier: "_ZTS1V")
+!16 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !15, size: 64)
+!20 = distinct !DISubprogram(name: "info_v", scope: !1, file: !1, line: 4, type: !22, scopeLine: 4, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0)
+!21 = !DILocation(line: 4, column: 1, scope: !20)
+!22 = !DISubroutineType(types: !23)
+!23 = !{!24, !16}
+!24 = !DIBasicType(name: "unsigned int", size: 32, encoding: DW_ATE_unsigned)

>From 714c8343faaa17b9abefa9c73d971cc3083bbf18 Mon Sep 17 00:00:00 2001
From: Maxim Skokov <skokovmaksimevg at gmail.com>
Date: Sun, 27 Sep 2026 21:58:13 +0300
Subject: [PATCH 2/2] [BPF] Treat CO-RE access indices as DI element indices

The di_index operand of llvm.preserve.{struct,union}.access.index is an
index into the record's debug info elements; #226790 makes clang emit it
that way for C++ records, whose elements also include bases, static data
members, methods and the vtable pointer. Look the element up directly
instead of mapping a field ordinal to it, and only check that it is a
data member before taking its position among the BTF members. An index
that does not refer to one, e.g. into a record the debug info only
declares, is still reported as an error at the access, and no longer
crashes when it is the parent of a nested access.

Assisted-by: Claude Code (Claude Opus 5.5)
---
 .../Target/BPF/BPFAbstractMemberAccess.cpp    | 33 ++++++++++-----
 llvm/lib/Target/BPF/BPFCORE.h                 | 20 ---------
 .../CodeGen/BPF/CORE/struct-cxx-fields.ll     | 33 ++++++++-------
 .../CodeGen/BPF/CORE/struct-cxx-fwd-decl.ll   | 41 ++++++++++++++-----
 4 files changed, 70 insertions(+), 57 deletions(-)

diff --git a/llvm/lib/Target/BPF/BPFAbstractMemberAccess.cpp b/llvm/lib/Target/BPF/BPFAbstractMemberAccess.cpp
index 003459f56a1f6..4c5e61caaca1f 100644
--- a/llvm/lib/Target/BPF/BPFAbstractMemberAccess.cpp
+++ b/llvm/lib/Target/BPF/BPFAbstractMemberAccess.cpp
@@ -299,6 +299,20 @@ static const DIType * stripQualifiers(const DIType *Ty) {
   return Ty;
 }
 
+/// Return the field that a CO-RE access index, an index into the record's DI
+/// elements, refers to, or null if that element is not a data member.
+static DIDerivedType *getDIRecordField(const DICompositeType *CTy,
+                                       uint64_t AccessIndex) {
+  DINodeArray Elements = CTy->getElements();
+  if (AccessIndex >= Elements.size())
+    return nullptr;
+  auto *Field = dyn_cast<DIDerivedType>(Elements[AccessIndex]);
+  if (!Field || Field->getTag() != dwarf::DW_TAG_member ||
+      Field->isStaticMember())
+    return nullptr;
+  return Field;
+}
+
 /// Return the position of Field among the BTF members of record CTy.
 static uint64_t getBTFMemberIndex(const DICompositeType *CTy,
                                   const DINode *Field) {
@@ -536,7 +550,7 @@ bool BPFAbstractMemberAccess::IsValidAIChain(const MDNode *ParentType,
   if (PTyTag == dwarf::DW_TAG_array_type)
     Ty = PTy->getBaseType();
   else
-    Ty = dyn_cast_or_null<DIType>(getDIRecordField(PTy, ParentAI));
+    Ty = getDIRecordField(PTy, ParentAI);
   if (!Ty)
     return false;
 
@@ -695,7 +709,7 @@ uint32_t BPFAbstractMemberAccess::GetFieldInfo(uint32_t InfoKind,
       PatchImm += AccessIndex * calcArraySize(CTy, 1) *
                   (EltTy->getSizeInBits() >> 3);
     } else if (Tag == dwarf::DW_TAG_structure_type) {
-      auto *MemberTy = cast<DIDerivedType>(getDIRecordField(CTy, AccessIndex));
+      auto *MemberTy = cast<DIDerivedType>(CTy->getElements()[AccessIndex]);
       if (!MemberTy->isBitField()) {
         PatchImm += MemberTy->getOffsetInBits() >> 3;
       } else {
@@ -713,7 +727,7 @@ uint32_t BPFAbstractMemberAccess::GetFieldInfo(uint32_t InfoKind,
       auto *EltTy = stripQualifiers(CTy->getBaseType());
       return calcArraySize(CTy, 1) * (EltTy->getSizeInBits() >> 3);
     } else {
-      auto *MemberTy = cast<DIDerivedType>(getDIRecordField(CTy, AccessIndex));
+      auto *MemberTy = cast<DIDerivedType>(CTy->getElements()[AccessIndex]);
       uint32_t SizeInBits = MemberTy->getSizeInBits();
       if (!MemberTy->isBitField())
         return SizeInBits >> 3;
@@ -736,7 +750,7 @@ uint32_t BPFAbstractMemberAccess::GetFieldInfo(uint32_t InfoKind,
         report_fatal_error("Invalid array expression for llvm.bpf.preserve.field.info");
       BaseTy = stripQualifiers(CTy->getBaseType());
     } else {
-      auto *MemberTy = cast<DIDerivedType>(getDIRecordField(CTy, AccessIndex));
+      auto *MemberTy = cast<DIDerivedType>(CTy->getElements()[AccessIndex]);
       BaseTy = stripQualifiers(MemberTy->getBaseType());
     }
 
@@ -768,7 +782,7 @@ uint32_t BPFAbstractMemberAccess::GetFieldInfo(uint32_t InfoKind,
       auto *EltTy = stripQualifiers(CTy->getBaseType());
       SizeInBits = calcArraySize(CTy, 1) * EltTy->getSizeInBits();
     } else {
-      MemberTy = cast<DIDerivedType>(getDIRecordField(CTy, AccessIndex));
+      MemberTy = cast<DIDerivedType>(CTy->getElements()[AccessIndex]);
       SizeInBits = MemberTy->getSizeInBits();
       IsBitField = MemberTy->isBitField();
     }
@@ -799,7 +813,7 @@ uint32_t BPFAbstractMemberAccess::GetFieldInfo(uint32_t InfoKind,
       auto *EltTy = stripQualifiers(CTy->getBaseType());
       SizeInBits = calcArraySize(CTy, 1) * EltTy->getSizeInBits();
     } else {
-      MemberTy = cast<DIDerivedType>(getDIRecordField(CTy, AccessIndex));
+      MemberTy = cast<DIDerivedType>(CTy->getElements()[AccessIndex]);
       SizeInBits = MemberTy->getSizeInBits();
       IsBitField = MemberTy->isBitField();
     }
@@ -984,10 +998,9 @@ Value *BPFAbstractMemberAccess::computeBaseAndAccessKey(CallInst *Call,
       if (!Field) {
         Call->getContext().diagnose(DiagnosticInfoUnsupported(
             *Call->getFunction(),
-            "CO-RE access to a field of '" + CTy->getName() +
-                "', which the debug info does not describe; the type may only "
-                "be declared there (e.g. clang's -fstandalone-debug emits it "
-                "in full)",
+            "CO-RE access index " + Twine(AccessIndex) +
+                " does not refer to a field of '" + CTy->getName() +
+                "' in the debug info",
             Call->getDebugLoc()));
         return nullptr;
       }
diff --git a/llvm/lib/Target/BPF/BPFCORE.h b/llvm/lib/Target/BPF/BPFCORE.h
index 1a4d65b98e70b..c2c09c7fba85b 100644
--- a/llvm/lib/Target/BPF/BPFCORE.h
+++ b/llvm/lib/Target/BPF/BPFCORE.h
@@ -39,26 +39,6 @@ inline bool isBTFRecordElement(const DINode *Element) {
   }
 }
 
-/// Whether a record element is a field that a CO-RE access index counts.
-/// Clang does not count bases or its vtable pointer member ("_vptr$<class>").
-inline bool isDIRecordField(const DINode *Element) {
-  if (Element->getTag() == dwarf::DW_TAG_inheritance ||
-      !isBTFRecordElement(Element))
-    return false;
-  const auto *DTy = dyn_cast<DIDerivedType>(Element);
-  return !DTy || !(DTy->isArtificial() && DTy->getName().starts_with("_vptr$"));
-}
-
-/// Return the field a CO-RE access index refers to, or null if the debug info
-/// does not describe the record's fields.
-inline DINode *getDIRecordField(const DICompositeType *CTy,
-                                uint64_t AccessIndex) {
-  for (DINode *Element : CTy->getElements())
-    if (isDIRecordField(Element) && AccessIndex-- == 0)
-      return Element;
-  return nullptr;
-}
-
 /// Return the bit offset used to order an element of a BTF structure record.
 inline uint64_t getBTFRecordElementOffset(const DINode *Element) {
   switch (Element->getTag()) {
diff --git a/llvm/test/CodeGen/BPF/CORE/struct-cxx-fields.ll b/llvm/test/CodeGen/BPF/CORE/struct-cxx-fields.ll
index de26896113b10..a07f9779877aa 100644
--- a/llvm/test/CodeGen/BPF/CORE/struct-cxx-fields.ll
+++ b/llvm/test/CodeGen/BPF/CORE/struct-cxx-fields.ll
@@ -1,13 +1,12 @@
 ; RUN: opt -O2 %s -S | FileCheck %s
 ; RUN: opt -O2 %s | llc -mtriple=bpfel -filetype=asm -o - | FileCheck %s --check-prefix=BTF
 
-; Clang numbers the fields of a record for a CO-RE access, while the debug
-; info of a C++ record also lists base classes, static data members, methods
-; and the vtable pointer among its elements. The access index is mapped to the
-; field it counts, and the access string index is the position of that field
-; among the BTF members, where non-virtual bases come first as anonymous
-; members and the vtable pointer is a member too. The name of each relocation
-; global encodes <kind>:<patched value>$<access string>.
+; The debug info of a C++ record also lists base classes, static data members,
+; methods and the vtable pointer among its elements. The DI index of a CO-RE
+; access is the position of the field among these elements, while the access
+; string index is its position among the BTF members, where non-virtual bases
+; come first as anonymous members and the vtable pointer is a member too. The
+; name of each relocation global encodes <kind>:<patched value>$<access string>.
 ;
 ; struct Base { int b; };
 ; struct Base2 { int c; };
@@ -53,56 +52,56 @@ target triple = "bpf"
 %struct.BF = type { %struct.Base, i8, i32 }
 
 define ptr @s_x(ptr %p) {
-  %r = call ptr @llvm.preserve.struct.access.index.p0.p0(ptr elementtype(%struct.S) %p, i32 2, i32 0), !llvm.preserve.access.index !10
+  %r = call ptr @llvm.preserve.struct.access.index.p0.p0(ptr elementtype(%struct.S) %p, i32 2, i32 3), !llvm.preserve.access.index !10
   ret ptr %r
 }
 
 define ptr @s_y(ptr %p) !dbg !61 {
-  %r = call ptr @llvm.preserve.struct.access.index.p0.p0(ptr elementtype(%struct.S) %p, i32 3, i32 1), !dbg !64, !llvm.preserve.access.index !10
+  %r = call ptr @llvm.preserve.struct.access.index.p0.p0(ptr elementtype(%struct.S) %p, i32 3, i32 5), !dbg !64, !llvm.preserve.access.index !10
   ret ptr %r
 }
 
 define ptr @v_v(ptr %p) {
-  %r = call ptr @llvm.preserve.struct.access.index.p0.p0(ptr elementtype(%struct.V) %p, i32 1, i32 0), !llvm.preserve.access.index !20
+  %r = call ptr @llvm.preserve.struct.access.index.p0.p0(ptr elementtype(%struct.V) %p, i32 1, i32 2), !llvm.preserve.access.index !20
   ret ptr %r
 }
 
 define ptr @e_e(ptr %p) {
-  %r = call ptr @llvm.preserve.struct.access.index.p0.p0(ptr elementtype(%struct.E) %p, i32 0, i32 0), !llvm.preserve.access.index !30
+  %r = call ptr @llvm.preserve.struct.access.index.p0.p0(ptr elementtype(%struct.E) %p, i32 0, i32 1), !llvm.preserve.access.index !30
   ret ptr %r
 }
 
 define ptr @u_b(ptr %p) {
-  %r = call ptr @llvm.preserve.union.access.index.p0.p0(ptr %p, i32 1), !llvm.preserve.access.index !40
+  %r = call ptr @llvm.preserve.union.access.index.p0.p0(ptr %p, i32 2), !llvm.preserve.access.index !40
   ret ptr %r
 }
 
 define i32 @bf_y_size(ptr %p) {
-  %f = call ptr @llvm.preserve.struct.access.index.p0.p0(ptr elementtype(%struct.BF) %p, i32 1, i32 1), !llvm.preserve.access.index !50
+  %f = call ptr @llvm.preserve.struct.access.index.p0.p0(ptr elementtype(%struct.BF) %p, i32 1, i32 3), !llvm.preserve.access.index !50
   %r = call i32 @llvm.bpf.preserve.field.info.p0(ptr %f, i64 1)
   ret i32 %r
 }
 
 define i32 @bf_y_signed(ptr %p) {
-  %f = call ptr @llvm.preserve.struct.access.index.p0.p0(ptr elementtype(%struct.BF) %p, i32 1, i32 1), !llvm.preserve.access.index !50
+  %f = call ptr @llvm.preserve.struct.access.index.p0.p0(ptr elementtype(%struct.BF) %p, i32 1, i32 3), !llvm.preserve.access.index !50
   %r = call i32 @llvm.bpf.preserve.field.info.p0(ptr %f, i64 3)
   ret i32 %r
 }
 
 define i32 @bf_x_signed(ptr %p) {
-  %f = call ptr @llvm.preserve.struct.access.index.p0.p0(ptr elementtype(%struct.BF) %p, i32 1, i32 0), !llvm.preserve.access.index !50
+  %f = call ptr @llvm.preserve.struct.access.index.p0.p0(ptr elementtype(%struct.BF) %p, i32 1, i32 2), !llvm.preserve.access.index !50
   %r = call i32 @llvm.bpf.preserve.field.info.p0(ptr %f, i64 3)
   ret i32 %r
 }
 
 define i32 @bf_y_lshift(ptr %p) {
-  %f = call ptr @llvm.preserve.struct.access.index.p0.p0(ptr elementtype(%struct.BF) %p, i32 1, i32 1), !llvm.preserve.access.index !50
+  %f = call ptr @llvm.preserve.struct.access.index.p0.p0(ptr elementtype(%struct.BF) %p, i32 1, i32 3), !llvm.preserve.access.index !50
   %r = call i32 @llvm.bpf.preserve.field.info.p0(ptr %f, i64 4)
   ret i32 %r
 }
 
 define i32 @bf_z_offset(ptr %p) {
-  %f = call ptr @llvm.preserve.struct.access.index.p0.p0(ptr elementtype(%struct.BF) %p, i32 2, i32 2), !llvm.preserve.access.index !50
+  %f = call ptr @llvm.preserve.struct.access.index.p0.p0(ptr elementtype(%struct.BF) %p, i32 2, i32 5), !llvm.preserve.access.index !50
   %r = call i32 @llvm.bpf.preserve.field.info.p0(ptr %f, i64 0)
   ret i32 %r
 }
diff --git a/llvm/test/CodeGen/BPF/CORE/struct-cxx-fwd-decl.ll b/llvm/test/CodeGen/BPF/CORE/struct-cxx-fwd-decl.ll
index b63b2495f1c3f..716009c26ae91 100644
--- a/llvm/test/CodeGen/BPF/CORE/struct-cxx-fwd-decl.ll
+++ b/llvm/test/CodeGen/BPF/CORE/struct-cxx-fwd-decl.ll
@@ -1,39 +1,53 @@
 ; RUN: not opt -O2 %s -o /dev/null 2>&1 | FileCheck %s
 
-; Clang often only declares a C++ record in the debug info of a translation
-; unit, e.g. a class whose vtable or constructor is emitted elsewhere. A CO-RE
-; access to one of its fields cannot be described and is reported as an error
-; at the access instead of crashing, and a field info call on it is dropped.
+; IR from a producer that only declares a C++ record in the debug info has no
+; DI element for a CO-RE access to one of its fields. The access is reported
+; as an error instead of crashing, also when it is the parent of a nested
+; access, and a field info call on it is dropped.
 ;
 ; struct Base { int pad; int b; };
-; struct V : virtual Base { int v; };
+; struct Inner { int a, x; };
+; struct V : virtual Base { int v; Inner in; };
 ; unsigned long get_v(V *v) {
 ;   return (unsigned long)__builtin_preserve_access_index(&v->v);
 ; }
 ; unsigned info_v(V *v) { return __builtin_preserve_field_info(v->v, 0); }
+; unsigned long get_in_x(V *v) {
+;   return (unsigned long)__builtin_preserve_access_index(&v->in.x);
+; }
 
-; CHECK: error: test.cpp:3:1: in function get_v i64 (ptr): CO-RE access to a field of 'V', which the debug info does not describe; the type may only be declared there (e.g. clang's -fstandalone-debug emits it in full)
-; CHECK: error: test.cpp:4:1: in function info_v i32 (ptr): CO-RE access to a field of 'V', which the debug info does not describe
+; CHECK: error: test.cpp:3:1: in function get_v i64 (ptr): CO-RE access index 2 does not refer to a field of 'V' in the debug info
+; CHECK: error: test.cpp:4:1: in function info_v i32 (ptr): CO-RE access index 2 does not refer to a field of 'V' in the debug info
+; CHECK: error: test.cpp:5:1: in function get_in_x i64 (ptr): CO-RE access index 3 does not refer to a field of 'V' in the debug info
 
 target triple = "bpf"
 
-%struct.V = type <{ ptr, i32, %struct.Base, [4 x i8] }>
+%struct.V = type <{ ptr, i32, %struct.Inner, %struct.Base, [4 x i8] }>
 %struct.Base = type { i32, i32 }
+%struct.Inner = type { i32, i32 }
 
 define dso_local i64 @get_v(ptr %v) !dbg !10 {
 entry:
-  %0 = call ptr @llvm.preserve.struct.access.index.p0.p0(ptr elementtype(%struct.V) %v, i32 1, i32 0), !dbg !14, !llvm.preserve.access.index !15
+  %0 = call ptr @llvm.preserve.struct.access.index.p0.p0(ptr elementtype(%struct.V) %v, i32 1, i32 2), !dbg !14, !llvm.preserve.access.index !15
   %1 = ptrtoint ptr %0 to i64, !dbg !14
   ret i64 %1, !dbg !14
 }
 
 define dso_local i32 @info_v(ptr %v) !dbg !20 {
 entry:
-  %0 = call ptr @llvm.preserve.struct.access.index.p0.p0(ptr elementtype(%struct.V) %v, i32 1, i32 0), !dbg !21, !llvm.preserve.access.index !15
+  %0 = call ptr @llvm.preserve.struct.access.index.p0.p0(ptr elementtype(%struct.V) %v, i32 1, i32 2), !dbg !21, !llvm.preserve.access.index !15
   %1 = call i32 @llvm.bpf.preserve.field.info.p0(ptr %0, i64 0), !dbg !21
   ret i32 %1, !dbg !21
 }
 
+define dso_local i64 @get_in_x(ptr %v) !dbg !30 {
+entry:
+  %0 = call ptr @llvm.preserve.struct.access.index.p0.p0(ptr elementtype(%struct.V) %v, i32 2, i32 3), !dbg !31, !llvm.preserve.access.index !15
+  %1 = call ptr @llvm.preserve.struct.access.index.p0.p0(ptr elementtype(%struct.Inner) %0, i32 1, i32 1), !dbg !31, !llvm.preserve.access.index !32
+  %2 = ptrtoint ptr %1 to i64, !dbg !31
+  ret i64 %2, !dbg !31
+}
+
 declare ptr @llvm.preserve.struct.access.index.p0.p0(ptr, i32 immarg, i32 immarg)
 declare i32 @llvm.bpf.preserve.field.info.p0(ptr, i64 immarg)
 
@@ -57,3 +71,10 @@ declare i32 @llvm.bpf.preserve.field.info.p0(ptr, i64 immarg)
 !22 = !DISubroutineType(types: !23)
 !23 = !{!24, !16}
 !24 = !DIBasicType(name: "unsigned int", size: 32, encoding: DW_ATE_unsigned)
+!25 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!30 = distinct !DISubprogram(name: "get_in_x", scope: !1, file: !1, line: 5, type: !11, scopeLine: 5, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0)
+!31 = !DILocation(line: 5, column: 1, scope: !30)
+!32 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "Inner", file: !1, line: 2, size: 64, elements: !33, identifier: "_ZTS5Inner")
+!33 = !{!34, !35}
+!34 = !DIDerivedType(tag: DW_TAG_member, name: "a", scope: !32, file: !1, line: 2, baseType: !25, size: 32)
+!35 = !DIDerivedType(tag: DW_TAG_member, name: "x", scope: !32, file: !1, line: 2, baseType: !25, size: 32, offset: 32)



More information about the llvm-commits mailing list