[clang] 564d69a - [Clang][counted_by] Honor counted_by in __bdos on direct struct access (#201161)
via cfe-commits
cfe-commits at lists.llvm.org
Mon Jun 8 17:49:44 PDT 2026
Author: Kees Cook
Date: 2026-06-08T17:49:39-07:00
New Revision: 564d69a8dec3183ebd78a779efdd8903bbe89f68
URL: https://github.com/llvm/llvm-project/commit/564d69a8dec3183ebd78a779efdd8903bbe89f68
DIFF: https://github.com/llvm/llvm-project/commit/564d69a8dec3183ebd78a779efdd8903bbe89f68.diff
LOG: [Clang][counted_by] Honor counted_by in __bdos on direct struct access (#201161)
__builtin_dynamic_object_size on a flexible array member must consult
the 'counted_by' attribute even when the containing struct is accessed
directly (a local or global variable) rather than through a pointer
dereference. The pointer-deref form (p->fam) already worked because the
constant evaluator could not determine the LValue for an opaque
parameter and fell through to the counted_by-aware runtime path in
CGBuiltin. The direct form (af.fam, gaf.fam) was being folded by
tryEvaluateBuiltinObjectSize to a layout-derived size (e.g. trailing
struct padding for locals, trailing initializer data for globals)
silently bypassing emitCountedBySize.
Make the AST constant evaluator refuse to fold __bdos on the same
operands that CGBuiltin's __bdos lowering classifies as a counted_by
FAM access. The check runs after the existing negative-offset early
return so that obviously out-of-bounds operands like &p->array[-42]
still fold to 0, preserving the behavior the sanitizer-bounds test in
attr-counted-by.c (test35) relies on.
Rather than duplicate the Expr-walking logic, promote CGBuiltin's
StructFieldAccess visitor into a shared function findStructFieldAccess
declared in clang/AST/Expr.h, with the visitor moved to an
anonymous-namespace implementation in Expr.cpp. Both CGBuiltin's
emitCountedBySize and the new ExprConstant check use it, so they
recognize
exactly the same set of expression shapes.
Note that the helper deliberately treats `&af.fam` (address-of-array)
as *not* a counted_by access. `&af.fam` designates the array object
as a whole and gets the layout-derived size, while `af.fam` (decayed)
and `&af.fam[idx]` designate element-pointers and get the count-based
size. This matches GCC 16's behavior.
The regression test covers the full {local, global, pointer-arg} x
{fam, &fam[idx], &fam} matrix (9 tests) with both -O0 and -O2 RUN
lines, autogenerated via update_cc_test_checks.py. -O0 checks the
frontend output directly: emitCountedBySize's counted_by.gep /
counted_by.load / flexible_array_member_size for the counted_by tests,
and the raw @llvm.objectsize.i64.p0 intrinsic call for the '&fam'
layout tests. -O2 pins down the post-optimization shape (smax(%n, 0)
for the runtime tests, ret i64 <constant> for the layout tests). All
nine tests match GCC 16's __bdos output.
Fixes: #200014
Added:
clang/test/CodeGen/attr-counted-by-issue200014.c
Modified:
clang/include/clang/AST/Expr.h
clang/lib/AST/ByteCode/InterpBuiltin.cpp
clang/lib/AST/Expr.cpp
clang/lib/AST/ExprConstant.cpp
clang/lib/CodeGen/CGBuiltin.cpp
Removed:
################################################################################
diff --git a/clang/include/clang/AST/Expr.h b/clang/include/clang/AST/Expr.h
index b91bf4a5375fb..eeac69cb1d0eb 100644
--- a/clang/include/clang/AST/Expr.h
+++ b/clang/include/clang/AST/Expr.h
@@ -7548,6 +7548,22 @@ inline const StreamingDiagnostic &operator<<(const StreamingDiagnostic &DB,
return DB;
}
+/// Walk @p E through parens, implicit casts, unary &/*, array subscripts and
+/// comma operators to find the head of a struct-field access -- typically a
+/// MemberExpr, or an LValueToRValue ImplicitCastExpr over a pointer-typed
+/// field. Returns nullptr for shapes we don't handle (multiple subscripts,
+/// non-comma binary ops, or '&fam' on an array lvalue which designates the
+/// array-as-a-whole rather than an element pointer).
+///
+/// If @p OutArrayIndex / @p OutArrayElementTy are non-null, they receive the
+/// index expression and base array type for forms like '&p->fam[idx]'.
+///
+/// Shared by CGBuiltin's __builtin_*_object_size lowering and the AST
+/// constant evaluator so they recognize the same 'counted_by' access shapes.
+const Expr *findStructFieldAccess(const Expr *E,
+ const Expr **OutArrayIndex = nullptr,
+ QualType *OutArrayElementTy = nullptr);
+
} // end namespace clang
#endif // LLVM_CLANG_AST_EXPR_H
diff --git a/clang/lib/AST/ByteCode/InterpBuiltin.cpp b/clang/lib/AST/ByteCode/InterpBuiltin.cpp
index 9b76cdb75ef70..679895f797df5 100644
--- a/clang/lib/AST/ByteCode/InterpBuiltin.cpp
+++ b/clang/lib/AST/ByteCode/InterpBuiltin.cpp
@@ -2460,6 +2460,16 @@ UnsignedOrNone evaluateBuiltinObjectSize(const ASTContext &ASTCtx,
if (Kind == 1)
return std::nullopt;
}
+ // For Type=1, defer to the runtime path on a true incomplete-array
+ // flexible array member (e.g. 'char fam[]') even when the base is a
+ // concrete local/global. Without this, the bytecode interpreter would
+ // happily fold &af.fam to 'NumElems * elemSize = 0' below; the default
+ // const-evaluator avoids the same trap, and CGBuiltin emits
+ // @llvm.objectsize for the correct layout-derived answer (matching
+ // GCC's __bos/__bdos on '&af.fam').
+ if (Kind == 1 && pointsToLastObject(Ptr) && Ptr.getFieldDesc()->isArray() &&
+ Ptr.getFieldDesc()->getType()->isIncompleteArrayType())
+ return std::nullopt;
}
// The "closest surrounding subobject" is NOT a base class,
diff --git a/clang/lib/AST/Expr.cpp b/clang/lib/AST/Expr.cpp
index babb30750e41b..f3acb366dc079 100644
--- a/clang/lib/AST/Expr.cpp
+++ b/clang/lib/AST/Expr.cpp
@@ -25,6 +25,7 @@
#include "clang/AST/IgnoreExpr.h"
#include "clang/AST/Mangle.h"
#include "clang/AST/RecordLayout.h"
+#include "clang/AST/StmtVisitor.h"
#include "clang/AST/TypeBase.h"
#include "clang/Basic/Builtins.h"
#include "clang/Basic/CharInfo.h"
@@ -5702,3 +5703,66 @@ APValue &CompoundLiteralExpr::getStaticValue() const {
assert(StaticValue);
return *StaticValue;
}
+
+namespace {
+/// Visitor that walks an Expr to the head of a struct-field access chain;
+/// see clang::findStructFieldAccess.
+class StructFieldAccessVisitor
+ : public ConstStmtVisitor<StructFieldAccessVisitor, const Expr *> {
+ bool AddrOfSeen = false;
+
+public:
+ const Expr *ArrayIndex = nullptr;
+ QualType ArrayElementTy;
+
+ const Expr *VisitMemberExpr(const MemberExpr *E) {
+ if (AddrOfSeen && E->getType()->isArrayType())
+ // '&fam' designates the array object as a whole, not the
+ // pointer-to-element value that 'fam' decays to.
+ return nullptr;
+ return E;
+ }
+
+ const Expr *VisitArraySubscriptExpr(const ArraySubscriptExpr *E) {
+ if (ArrayIndex)
+ // We don't support multiple subscripts.
+ return nullptr;
+
+ AddrOfSeen = false; // '&ptr->array[idx]' is okay.
+ ArrayIndex = E->getIdx();
+ ArrayElementTy = E->getBase()->getType();
+ return Visit(E->getBase());
+ }
+ const Expr *VisitCastExpr(const CastExpr *E) {
+ if (E->getCastKind() == CK_LValueToRValue)
+ return E;
+ return Visit(E->getSubExpr());
+ }
+ const Expr *VisitParenExpr(const ParenExpr *E) {
+ return Visit(E->getSubExpr());
+ }
+ const Expr *VisitUnaryAddrOf(const UnaryOperator *E) {
+ AddrOfSeen = true;
+ return Visit(E->getSubExpr());
+ }
+ const Expr *VisitUnaryDeref(const UnaryOperator *E) {
+ AddrOfSeen = false;
+ return Visit(E->getSubExpr());
+ }
+ const Expr *VisitBinaryOperator(const BinaryOperator *Op) {
+ return Op->isCommaOp() ? Visit(Op->getRHS()) : nullptr;
+ }
+};
+} // namespace
+
+const Expr *clang::findStructFieldAccess(const Expr *E,
+ const Expr **OutArrayIndex,
+ QualType *OutArrayElementTy) {
+ StructFieldAccessVisitor V;
+ const Expr *Result = V.Visit(E);
+ if (OutArrayIndex)
+ *OutArrayIndex = V.ArrayIndex;
+ if (OutArrayElementTy)
+ *OutArrayElementTy = V.ArrayElementTy;
+ return Result;
+}
diff --git a/clang/lib/AST/ExprConstant.cpp b/clang/lib/AST/ExprConstant.cpp
index f574443dad8c3..f88a751a18563 100644
--- a/clang/lib/AST/ExprConstant.cpp
+++ b/clang/lib/AST/ExprConstant.cpp
@@ -16291,8 +16291,14 @@ static bool determineEndOffset(EvalInfo &Info, SourceLocation ExprLoc,
///
/// If @p WasError is non-null, this will report whether the failure to evaluate
/// is to be treated as an Error in IntExprEvaluator.
+///
+/// If @p IsDynamic is true (i.e. we're evaluating
+/// __builtin_dynamic_object_size) and the operand designates a flexible array
+/// member annotated with 'counted_by', we refuse to fold so that IR generation
+/// can emit the count-based runtime size computation.
static std::optional<uint64_t>
-tryEvaluateBuiltinObjectSize(const Expr *E, unsigned Type, EvalInfo &Info) {
+tryEvaluateBuiltinObjectSize(const Expr *E, unsigned Type, EvalInfo &Info,
+ bool IsDynamic = false) {
// Determine the denoted object.
LValue LVal;
{
@@ -16319,6 +16325,23 @@ tryEvaluateBuiltinObjectSize(const Expr *E, unsigned Type, EvalInfo &Info) {
if (LVal.getLValueOffset().isNegative())
return 0;
+ // For __builtin_dynamic_object_size on a counted_by-annotated flexible
+ // array member, defer to IR generation (emitCountedBySize in CGBuiltin):
+ // its runtime computation uses the live 'count' field and is more accurate
+ // than the layout/initializer-derived size we'd produce here. Use the same
+ // findStructFieldAccess form-recognition CGBuiltin does, so we refuse to
+ // fold on exactly the shapes that path handles (and, importantly, *not*
+ // on '&af.fam' which designates the array-as-a-whole and stays on the
+ // layout-derived path to match GCC). Checked after the negative-offset
+ // early return above so that obviously out-of-bounds operands still fold
+ // to 0, preserving existing behavior.
+ if (IsDynamic) {
+ const auto *ME = dyn_cast_or_null<MemberExpr>(findStructFieldAccess(E));
+ const auto *FD = ME ? dyn_cast<FieldDecl>(ME->getMemberDecl()) : nullptr;
+ if (FD && FD->getType()->isCountAttributedType())
+ return std::nullopt;
+ }
+
CharUnits EndOffset;
if (!determineEndOffset(Info, E->getExprLoc(), Type, LVal, EndOffset))
return std::nullopt;
@@ -16458,8 +16481,9 @@ bool IntExprEvaluator::VisitBuiltinCallExpr(const CallExpr *E,
E->getArg(1)->EvaluateKnownConstInt(Info.Ctx).getZExtValue();
assert(Type <= 3 && "unexpected type");
+ bool IsDynamic = BuiltinOp == Builtin::BI__builtin_dynamic_object_size;
if (std::optional<uint64_t> Size =
- tryEvaluateBuiltinObjectSize(E->getArg(0), Type, Info))
+ tryEvaluateBuiltinObjectSize(E->getArg(0), Type, Info, IsDynamic))
return Success(*Size, E);
if (E->getArg(0)->HasSideEffects(Info.Ctx))
diff --git a/clang/lib/CodeGen/CGBuiltin.cpp b/clang/lib/CodeGen/CGBuiltin.cpp
index e5c47073de30f..682b125890fe1 100644
--- a/clang/lib/CodeGen/CGBuiltin.cpp
+++ b/clang/lib/CodeGen/CGBuiltin.cpp
@@ -903,58 +903,6 @@ CodeGenFunction::evaluateOrEmitBuiltinObjectSize(const Expr *E, unsigned Type,
return emitBuiltinObjectSize(E, Type, ResType, EmittedE, IsDynamic);
}
-namespace {
-
-/// StructFieldAccess is a simple visitor class to grab the first MemberExpr
-/// from an Expr. It records any ArraySubscriptExpr we meet along the way.
-class StructFieldAccess
- : public ConstStmtVisitor<StructFieldAccess, const Expr *> {
- bool AddrOfSeen = false;
-
-public:
- const Expr *ArrayIndex = nullptr;
- QualType ArrayElementTy;
-
- const Expr *VisitMemberExpr(const MemberExpr *E) {
- if (AddrOfSeen && E->getType()->isArrayType())
- // Avoid forms like '&ptr->array'.
- return nullptr;
- return E;
- }
-
- const Expr *VisitArraySubscriptExpr(const ArraySubscriptExpr *E) {
- if (ArrayIndex)
- // We don't support multiple subscripts.
- return nullptr;
-
- AddrOfSeen = false; // '&ptr->array[idx]' is okay.
- ArrayIndex = E->getIdx();
- ArrayElementTy = E->getBase()->getType();
- return Visit(E->getBase());
- }
- const Expr *VisitCastExpr(const CastExpr *E) {
- if (E->getCastKind() == CK_LValueToRValue)
- return E;
- return Visit(E->getSubExpr());
- }
- const Expr *VisitParenExpr(const ParenExpr *E) {
- return Visit(E->getSubExpr());
- }
- const Expr *VisitUnaryAddrOf(const clang::UnaryOperator *E) {
- AddrOfSeen = true;
- return Visit(E->getSubExpr());
- }
- const Expr *VisitUnaryDeref(const clang::UnaryOperator *E) {
- AddrOfSeen = false;
- return Visit(E->getSubExpr());
- }
- const Expr *VisitBinaryOperator(const clang::BinaryOperator *Op) {
- return Op->isCommaOp() ? Visit(Op->getRHS()) : nullptr;
- }
-};
-
-} // end anonymous namespace
-
/// Find a struct's flexible array member. It may be embedded inside multiple
/// sub-structs, but must still be the last field.
static const FieldDecl *FindFlexibleArrayMemberField(CodeGenFunction &CGF,
@@ -1044,12 +992,12 @@ llvm::Value *CodeGenFunction::emitCountedBySize(const Expr *E,
// structure. Therefore, because of the above issue, we choose to match what
// GCC does for consistency's sake.
- StructFieldAccess Visitor;
- E = Visitor.Visit(E);
+ const Expr *Idx = nullptr;
+ QualType ArrayElementTy;
+ E = findStructFieldAccess(E, &Idx, &ArrayElementTy);
if (!E)
return nullptr;
- const Expr *Idx = Visitor.ArrayIndex;
if (Idx) {
if (Idx->HasSideEffects(getContext()))
// We can't have side-effects.
@@ -1069,14 +1017,14 @@ llvm::Value *CodeGenFunction::emitCountedBySize(const Expr *E,
// __counted_by on either a flexible array member or a pointer into a struct
// with a flexible array member.
if (const auto *ME = dyn_cast<MemberExpr>(E))
- return emitCountedByMemberSize(ME, Idx, EmittedE, Visitor.ArrayElementTy,
- Type, ResType);
+ return emitCountedByMemberSize(ME, Idx, EmittedE, ArrayElementTy, Type,
+ ResType);
// __counted_by on a pointer in a struct.
if (const auto *ICE = dyn_cast<ImplicitCastExpr>(E);
ICE && ICE->getCastKind() == CK_LValueToRValue)
- return emitCountedByPointerSize(ICE, Idx, EmittedE, Visitor.ArrayElementTy,
- Type, ResType);
+ return emitCountedByPointerSize(ICE, Idx, EmittedE, ArrayElementTy, Type,
+ ResType);
return nullptr;
}
diff --git a/clang/test/CodeGen/attr-counted-by-issue200014.c b/clang/test/CodeGen/attr-counted-by-issue200014.c
new file mode 100644
index 0000000000000..02c50d23b6def
--- /dev/null
+++ b/clang/test/CodeGen/attr-counted-by-issue200014.c
@@ -0,0 +1,347 @@
+// NOTE: Assertions have been autogenerated by utils/update_cc_test_checks.py UTC_ARGS: --version 6
+//
+// FRONTEND prefix: post-mem2reg/sroa output of clang -O0 lit-side. Pins
+// down the IR emitCountedBySize emits directly (so a regression that
+// re-folds __bdos at AST time would lose 'counted_by.load' / 'mul' /
+// 'flexible_array_member_size' here). The same prefix is used for both
+// the default constant-evaluator and -fexperimental-new-constant-interpreter
+// runs: those must produce identical IR for every test below, which is the
+// regression guard for "the bytecode interpreter also defers to runtime
+// for counted_by FAM __bdos."
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -O0 -Wno-missing-declarations \
+// RUN: -disable-O0-optnone -emit-llvm -o - %s \
+// RUN: | opt -S -passes=mem2reg,sroa | FileCheck %s --check-prefix=FRONTEND
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -O0 -Wno-missing-declarations \
+// RUN: -fexperimental-new-constant-interpreter -disable-O0-optnone \
+// RUN: -emit-llvm -o - %s \
+// RUN: | opt -S -passes=mem2reg,sroa | FileCheck %s --check-prefix=FRONTEND
+// O2 prefix: post-optimization shape; pins down the concrete folded
+// constants for the '&fam' layout tests.
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -O2 -Wno-missing-declarations \
+// RUN: -emit-llvm -o - %s | FileCheck %s --check-prefix=O2
+
+// See https://github.com/llvm/llvm-project/issues/200014
+//
+// __builtin_dynamic_object_size on a flexible array member must consult the
+// 'counted_by' attribute on the same expression shapes the IR runtime path
+// in CGBuiltin handles -- and *only* those.
+//
+// Pre-fix, the direct-struct-access tests (local/global x {fam, &fam[idx]})
+// were folded by the AST constant evaluator to layout-derived constants
+// (trailing struct padding for locals; trailing initializer data for
+// globals), silently bypassing emitCountedBySize.
+//
+// The '&fam' (array-as-a-whole) tests are *intentionally* layout-derived in
+// both GCC and Clang: '&af.fam' designates the array object itself, not
+// the pointer-to-element that 'af.fam' decays to, and findStructFieldAccess
+// bails out on that shape.
+//
+// Behavior matrix (verified against GCC 16 for __bdos(_, 1)):
+//
+// form \ base | local af (count=%n) | global gaf (count=10,init=15) | pointer p
+// --------------+---------------------+-------------------------------+-----------
+// fam (decay) | runtime: count*1 | runtime: count*1 | runtime
+// &fam[idx] | runtime: count-idx | runtime: count-idx | runtime
+// &fam (whole) | layout: 7 (pad) | layout: 15 (init) | unknown:-1
+//
+// See the RUN-line block above for the prefix conventions
+// (FRONTEND covers both the default and bytecode -O0 const-evaluators;
+// O2 covers post-optimization).
+
+typedef __SIZE_TYPE__ size_t;
+
+struct annotated_flex {
+ size_t count;
+ char induce_padding;
+ char fam[] __attribute__((counted_by(count)));
+};
+
+// Initializer length 15 bytes ("i am very long" + NUL); count lies as 10.
+struct annotated_flex gaf = {
+ .fam = "i am very long",
+ .count = 10,
+};
+
+extern size_t sink;
+
+// Test: local x fam (decayed pointer-to-element).
+// Pre-fix bug: AST const-eval folded this to trailing struct padding (7).
+// Expected: runtime counted_by path -- look for 'counted_by.load' in
+// FRONTEND IR and 'smax(%n, 0)' at -O2.
+// FRONTEND-LABEL: define dso_local i64 @local_fam(
+// FRONTEND-SAME: i64 noundef [[N:%.*]]) #[[ATTR0:[0-9]+]] {
+// FRONTEND-NEXT: [[ENTRY:.*:]]
+// FRONTEND-NEXT: [[AF:%.*]] = alloca [[STRUCT_ANNOTATED_FLEX:%.*]], align 8
+// FRONTEND-NEXT: [[COUNT:%.*]] = getelementptr inbounds nuw [[STRUCT_ANNOTATED_FLEX]], ptr [[AF]], i32 0, i32 0
+// FRONTEND-NEXT: store i64 [[N]], ptr [[COUNT]], align 8
+// FRONTEND-NEXT: [[INDUCE_PADDING:%.*]] = getelementptr inbounds nuw [[STRUCT_ANNOTATED_FLEX]], ptr [[AF]], i32 0, i32 1
+// FRONTEND-NEXT: store i8 0, ptr [[INDUCE_PADDING]], align 8
+// FRONTEND-NEXT: [[TMP0:%.*]] = getelementptr i8, ptr [[AF]], i64 9
+// FRONTEND-NEXT: call void @llvm.memset.p0.i64(ptr align 1 [[TMP0]], i8 0, i64 7, i1 false)
+// FRONTEND-NEXT: [[FAM:%.*]] = getelementptr inbounds nuw [[STRUCT_ANNOTATED_FLEX]], ptr [[AF]], i32 0, i32 2
+// FRONTEND-NEXT: [[ARRAYDECAY:%.*]] = getelementptr inbounds [0 x i8], ptr [[FAM]], i64 0, i64 0
+// FRONTEND-NEXT: [[TMP1:%.*]] = ptrtoint ptr [[ARRAYDECAY]] to i64
+// FRONTEND-NEXT: store i64 [[TMP1]], ptr @sink, align 8
+// FRONTEND-NEXT: [[FAM1:%.*]] = getelementptr inbounds nuw [[STRUCT_ANNOTATED_FLEX]], ptr [[AF]], i32 0, i32 2
+// FRONTEND-NEXT: [[ARRAYDECAY2:%.*]] = getelementptr inbounds [0 x i8], ptr [[FAM1]], i64 0, i64 0
+// FRONTEND-NEXT: [[COUNTED_BY_GEP:%.*]] = getelementptr inbounds [[STRUCT_ANNOTATED_FLEX]], ptr [[AF]], i32 0, i32 0
+// FRONTEND-NEXT: [[COUNTED_BY_LOAD:%.*]] = load i64, ptr [[COUNTED_BY_GEP]], align 4
+// FRONTEND-NEXT: [[FLEXIBLE_ARRAY_MEMBER_SIZE:%.*]] = mul nuw i64 [[COUNTED_BY_LOAD]], 1
+// FRONTEND-NEXT: [[TMP2:%.*]] = icmp sgt i64 [[FLEXIBLE_ARRAY_MEMBER_SIZE]], -1
+// FRONTEND-NEXT: [[TMP3:%.*]] = select i1 [[TMP2]], i64 [[FLEXIBLE_ARRAY_MEMBER_SIZE]], i64 0
+// FRONTEND-NEXT: ret i64 [[TMP3]]
+//
+// O2-LABEL: define dso_local noundef range(i64 0, -9223372036854775808) i64 @local_fam(
+// O2-SAME: i64 noundef [[N:%.*]]) local_unnamed_addr #[[ATTR0:[0-9]+]] {
+// O2-NEXT: [[ENTRY:.*:]]
+// O2-NEXT: [[AF:%.*]] = alloca [[STRUCT_ANNOTATED_FLEX:%.*]], align 8
+// O2-NEXT: call void @llvm.lifetime.start.p0(ptr nonnull [[AF]]) #[[ATTR6:[0-9]+]]
+// O2-NEXT: [[TMP0:%.*]] = getelementptr inbounds nuw i8, ptr [[AF]], i64 9
+// O2-NEXT: [[TMP1:%.*]] = ptrtoint ptr [[TMP0]] to i64
+// O2-NEXT: store i64 [[TMP1]], ptr @sink, align 8, !tbaa [[LONG_TBAA5:![0-9]+]]
+// O2-NEXT: [[TMP2:%.*]] = call i64 @llvm.smax.i64(i64 [[N]], i64 0)
+// O2-NEXT: call void @llvm.lifetime.end.p0(ptr nonnull [[AF]]) #[[ATTR6]]
+// O2-NEXT: ret i64 [[TMP2]]
+//
+size_t local_fam(size_t n) {
+ struct annotated_flex af = { .count = n };
+ sink = (size_t)(__UINTPTR_TYPE__)af.fam;
+ return __builtin_dynamic_object_size(af.fam, 1);
+}
+
+// Test: local x &fam[idx].
+// Pre-fix bug: AST const-eval folded this to padding-minus-idx (5).
+// Expected: runtime counted_by path with index subtracted -- look for
+// 'counted_by.load' + 'sub' in FRONTEND IR and 'add %n, -2' at -O2.
+// FRONTEND-LABEL: define dso_local i64 @local_subscript(
+// FRONTEND-SAME: i64 noundef [[N:%.*]]) #[[ATTR0]] {
+// FRONTEND-NEXT: [[ENTRY:.*:]]
+// FRONTEND-NEXT: [[AF:%.*]] = alloca [[STRUCT_ANNOTATED_FLEX:%.*]], align 8
+// FRONTEND-NEXT: [[COUNT:%.*]] = getelementptr inbounds nuw [[STRUCT_ANNOTATED_FLEX]], ptr [[AF]], i32 0, i32 0
+// FRONTEND-NEXT: store i64 [[N]], ptr [[COUNT]], align 8
+// FRONTEND-NEXT: [[INDUCE_PADDING:%.*]] = getelementptr inbounds nuw [[STRUCT_ANNOTATED_FLEX]], ptr [[AF]], i32 0, i32 1
+// FRONTEND-NEXT: store i8 0, ptr [[INDUCE_PADDING]], align 8
+// FRONTEND-NEXT: [[TMP0:%.*]] = getelementptr i8, ptr [[AF]], i64 9
+// FRONTEND-NEXT: call void @llvm.memset.p0.i64(ptr align 1 [[TMP0]], i8 0, i64 7, i1 false)
+// FRONTEND-NEXT: [[FAM:%.*]] = getelementptr inbounds nuw [[STRUCT_ANNOTATED_FLEX]], ptr [[AF]], i32 0, i32 2
+// FRONTEND-NEXT: [[ARRAYIDX:%.*]] = getelementptr inbounds [0 x i8], ptr [[FAM]], i64 0, i64 2
+// FRONTEND-NEXT: [[TMP1:%.*]] = ptrtoint ptr [[ARRAYIDX]] to i64
+// FRONTEND-NEXT: store i64 [[TMP1]], ptr @sink, align 8
+// FRONTEND-NEXT: [[FAM1:%.*]] = getelementptr inbounds nuw [[STRUCT_ANNOTATED_FLEX]], ptr [[AF]], i32 0, i32 2
+// FRONTEND-NEXT: [[ARRAYIDX2:%.*]] = getelementptr inbounds [0 x i8], ptr [[FAM1]], i64 0, i64 2
+// FRONTEND-NEXT: [[COUNTED_BY_GEP:%.*]] = getelementptr inbounds [[STRUCT_ANNOTATED_FLEX]], ptr [[AF]], i32 0, i32 0
+// FRONTEND-NEXT: [[COUNTED_BY_LOAD:%.*]] = load i64, ptr [[COUNTED_BY_GEP]], align 4
+// FRONTEND-NEXT: [[FLEXIBLE_ARRAY_MEMBER_SIZE:%.*]] = mul nuw i64 [[COUNTED_BY_LOAD]], 1
+// FRONTEND-NEXT: [[RESULT:%.*]] = sub nuw i64 [[FLEXIBLE_ARRAY_MEMBER_SIZE]], 2
+// FRONTEND-NEXT: [[TMP2:%.*]] = icmp sgt i64 [[RESULT]], -1
+// FRONTEND-NEXT: [[TMP3:%.*]] = and i1 true, [[TMP2]]
+// FRONTEND-NEXT: [[TMP4:%.*]] = select i1 [[TMP3]], i64 [[RESULT]], i64 0
+// FRONTEND-NEXT: ret i64 [[TMP4]]
+//
+// O2-LABEL: define dso_local noundef range(i64 0, -9223372036854775808) i64 @local_subscript(
+// O2-SAME: i64 noundef [[N:%.*]]) local_unnamed_addr #[[ATTR0]] {
+// O2-NEXT: [[ENTRY:.*:]]
+// O2-NEXT: [[AF:%.*]] = alloca [[STRUCT_ANNOTATED_FLEX:%.*]], align 8
+// O2-NEXT: call void @llvm.lifetime.start.p0(ptr nonnull [[AF]]) #[[ATTR6]]
+// O2-NEXT: [[ARRAYIDX:%.*]] = getelementptr inbounds nuw i8, ptr [[AF]], i64 11
+// O2-NEXT: [[TMP0:%.*]] = ptrtoint ptr [[ARRAYIDX]] to i64
+// O2-NEXT: store i64 [[TMP0]], ptr @sink, align 8, !tbaa [[LONG_TBAA5]]
+// O2-NEXT: [[RESULT:%.*]] = add i64 [[N]], -2
+// O2-NEXT: [[TMP1:%.*]] = call i64 @llvm.smax.i64(i64 [[RESULT]], i64 0)
+// O2-NEXT: call void @llvm.lifetime.end.p0(ptr nonnull [[AF]]) #[[ATTR6]]
+// O2-NEXT: ret i64 [[TMP1]]
+//
+size_t local_subscript(size_t n) {
+ struct annotated_flex af = { .count = n };
+ sink = (size_t)(__UINTPTR_TYPE__)&af.fam[2];
+ return __builtin_dynamic_object_size(&af.fam[2], 1);
+}
+
+// Test: local x &fam (array-as-a-whole).
+// Intentionally layout-derived, NOT counted_by (matches GCC). The frontend
+// must fall through to @llvm.objectsize, which folds to the trailing struct
+// padding (7) at -O2.
+// FRONTEND-LABEL: define dso_local i64 @local_addrof(
+// FRONTEND-SAME: i64 noundef [[N:%.*]]) #[[ATTR0]] {
+// FRONTEND-NEXT: [[ENTRY:.*:]]
+// FRONTEND-NEXT: [[AF:%.*]] = alloca [[STRUCT_ANNOTATED_FLEX:%.*]], align 8
+// FRONTEND-NEXT: [[COUNT:%.*]] = getelementptr inbounds nuw [[STRUCT_ANNOTATED_FLEX]], ptr [[AF]], i32 0, i32 0
+// FRONTEND-NEXT: store i64 [[N]], ptr [[COUNT]], align 8
+// FRONTEND-NEXT: [[INDUCE_PADDING:%.*]] = getelementptr inbounds nuw [[STRUCT_ANNOTATED_FLEX]], ptr [[AF]], i32 0, i32 1
+// FRONTEND-NEXT: store i8 0, ptr [[INDUCE_PADDING]], align 8
+// FRONTEND-NEXT: [[TMP0:%.*]] = getelementptr i8, ptr [[AF]], i64 9
+// FRONTEND-NEXT: call void @llvm.memset.p0.i64(ptr align 1 [[TMP0]], i8 0, i64 7, i1 false)
+// FRONTEND-NEXT: [[FAM:%.*]] = getelementptr inbounds nuw [[STRUCT_ANNOTATED_FLEX]], ptr [[AF]], i32 0, i32 2
+// FRONTEND-NEXT: [[TMP1:%.*]] = ptrtoint ptr [[FAM]] to i64
+// FRONTEND-NEXT: store i64 [[TMP1]], ptr @sink, align 8
+// FRONTEND-NEXT: [[FAM1:%.*]] = getelementptr inbounds nuw [[STRUCT_ANNOTATED_FLEX]], ptr [[AF]], i32 0, i32 2
+// FRONTEND-NEXT: [[TMP2:%.*]] = call i64 @llvm.objectsize.i64.p0(ptr [[FAM1]], i1 false, i1 true, i1 true)
+// FRONTEND-NEXT: ret i64 [[TMP2]]
+//
+// O2-LABEL: define dso_local noundef i64 @local_addrof(
+// O2-SAME: i64 noundef [[N:%.*]]) local_unnamed_addr #[[ATTR0]] {
+// O2-NEXT: [[ENTRY:.*:]]
+// O2-NEXT: [[AF:%.*]] = alloca [[STRUCT_ANNOTATED_FLEX:%.*]], align 8
+// O2-NEXT: call void @llvm.lifetime.start.p0(ptr nonnull [[AF]]) #[[ATTR6]]
+// O2-NEXT: [[TMP0:%.*]] = getelementptr inbounds nuw i8, ptr [[AF]], i64 9
+// O2-NEXT: [[TMP1:%.*]] = ptrtoint ptr [[TMP0]] to i64
+// O2-NEXT: store i64 [[TMP1]], ptr @sink, align 8, !tbaa [[LONG_TBAA5]]
+// O2-NEXT: call void @llvm.lifetime.end.p0(ptr nonnull [[AF]]) #[[ATTR6]]
+// O2-NEXT: ret i64 7
+//
+size_t local_addrof(size_t n) {
+ struct annotated_flex af = { .count = n };
+ sink = (size_t)(__UINTPTR_TYPE__)&af.fam;
+ return __builtin_dynamic_object_size(&af.fam, 1);
+}
+
+// Test: global x fam (decayed pointer-to-element).
+// Pre-fix bug: AST const-eval folded this to the static initializer length
+// (15 = strlen("i am very long")+1), bypassing the counted_by lie of 10.
+// Expected: runtime path -- 'load i64, ptr @gaf' (the count field) in both
+// FRONTEND and O2 IR.
+// FRONTEND-LABEL: define dso_local i64 @global_fam(
+// FRONTEND-SAME: ) #[[ATTR0]] {
+// FRONTEND-NEXT: [[ENTRY:.*:]]
+// FRONTEND-NEXT: [[COUNTED_BY_LOAD:%.*]] = load i64, ptr @gaf, align 4
+// FRONTEND-NEXT: [[FLEXIBLE_ARRAY_MEMBER_SIZE:%.*]] = mul nuw i64 [[COUNTED_BY_LOAD]], 1
+// FRONTEND-NEXT: [[TMP0:%.*]] = icmp sgt i64 [[FLEXIBLE_ARRAY_MEMBER_SIZE]], -1
+// FRONTEND-NEXT: [[TMP1:%.*]] = select i1 [[TMP0]], i64 [[FLEXIBLE_ARRAY_MEMBER_SIZE]], i64 0
+// FRONTEND-NEXT: ret i64 [[TMP1]]
+//
+// O2-LABEL: define dso_local range(i64 0, -9223372036854775808) i64 @global_fam(
+// O2-SAME: ) local_unnamed_addr #[[ATTR2:[0-9]+]] {
+// O2-NEXT: [[ENTRY:.*:]]
+// O2-NEXT: [[COUNTED_BY_LOAD:%.*]] = load i64, ptr @gaf, align 8
+// O2-NEXT: [[TMP0:%.*]] = tail call i64 @llvm.smax.i64(i64 [[COUNTED_BY_LOAD]], i64 0)
+// O2-NEXT: ret i64 [[TMP0]]
+//
+size_t global_fam(void) {
+ return __builtin_dynamic_object_size(gaf.fam, 1);
+}
+
+// Test: global x &fam[idx].
+// Pre-fix bug: folded to (initializer_length - idx) = 12.
+// Expected: runtime path with index subtracted -- 'load i64, ptr @gaf' then
+// 'sub' in FRONTEND IR / 'add %count, -3' at -O2.
+// FRONTEND-LABEL: define dso_local i64 @global_subscript(
+// FRONTEND-SAME: ) #[[ATTR0]] {
+// FRONTEND-NEXT: [[ENTRY:.*:]]
+// FRONTEND-NEXT: [[COUNTED_BY_LOAD:%.*]] = load i64, ptr @gaf, align 4
+// FRONTEND-NEXT: [[FLEXIBLE_ARRAY_MEMBER_SIZE:%.*]] = mul nuw i64 [[COUNTED_BY_LOAD]], 1
+// FRONTEND-NEXT: [[RESULT:%.*]] = sub nuw i64 [[FLEXIBLE_ARRAY_MEMBER_SIZE]], 3
+// FRONTEND-NEXT: [[TMP0:%.*]] = icmp sgt i64 [[RESULT]], -1
+// FRONTEND-NEXT: [[TMP1:%.*]] = and i1 true, [[TMP0]]
+// FRONTEND-NEXT: [[TMP2:%.*]] = select i1 [[TMP1]], i64 [[RESULT]], i64 0
+// FRONTEND-NEXT: ret i64 [[TMP2]]
+//
+// O2-LABEL: define dso_local range(i64 0, -9223372036854775808) i64 @global_subscript(
+// O2-SAME: ) local_unnamed_addr #[[ATTR2]] {
+// O2-NEXT: [[ENTRY:.*:]]
+// O2-NEXT: [[COUNTED_BY_LOAD:%.*]] = load i64, ptr @gaf, align 8
+// O2-NEXT: [[RESULT:%.*]] = add i64 [[COUNTED_BY_LOAD]], -3
+// O2-NEXT: [[TMP0:%.*]] = tail call i64 @llvm.smax.i64(i64 [[RESULT]], i64 0)
+// O2-NEXT: ret i64 [[TMP0]]
+//
+size_t global_subscript(void) {
+ return __builtin_dynamic_object_size(&gaf.fam[3], 1);
+}
+
+// Test: global x &fam (array-as-a-whole).
+// Intentionally layout-derived. Frontend emits @llvm.objectsize, which
+// folds to the initializer length (15) at -O2. Matches GCC.
+// FRONTEND-LABEL: define dso_local i64 @global_addrof(
+// FRONTEND-SAME: ) #[[ATTR0]] {
+// FRONTEND-NEXT: [[ENTRY:.*:]]
+// FRONTEND-NEXT: [[TMP0:%.*]] = call i64 @llvm.objectsize.i64.p0(ptr getelementptr inbounds nuw (i8, ptr @gaf, i64 9), i1 false, i1 true, i1 true)
+// FRONTEND-NEXT: ret i64 [[TMP0]]
+//
+// O2-LABEL: define dso_local noundef i64 @global_addrof(
+// O2-SAME: ) local_unnamed_addr #[[ATTR3:[0-9]+]] {
+// O2-NEXT: [[ENTRY:.*:]]
+// O2-NEXT: ret i64 15
+//
+size_t global_addrof(void) {
+ return __builtin_dynamic_object_size(&gaf.fam, 1);
+}
+
+// Test: pointer x fam (decayed pointer-to-element).
+// This form already worked pre-fix: const-eval can't resolve %p, so it
+// fell through to CGBuiltin's counted_by path. Kept here as a regression
+// boundary -- the runtime IR must keep loading the count from %p.
+// FRONTEND-LABEL: define dso_local i64 @ptr_fam(
+// FRONTEND-SAME: ptr noundef [[P:%.*]]) #[[ATTR0]] {
+// FRONTEND-NEXT: [[ENTRY:.*:]]
+// FRONTEND-NEXT: [[FAM:%.*]] = getelementptr inbounds nuw [[STRUCT_ANNOTATED_FLEX:%.*]], ptr [[P]], i32 0, i32 2
+// FRONTEND-NEXT: [[ARRAYDECAY:%.*]] = getelementptr inbounds [0 x i8], ptr [[FAM]], i64 0, i64 0
+// FRONTEND-NEXT: [[COUNTED_BY_GEP:%.*]] = getelementptr inbounds [[STRUCT_ANNOTATED_FLEX]], ptr [[P]], i32 0, i32 0
+// FRONTEND-NEXT: [[COUNTED_BY_LOAD:%.*]] = load i64, ptr [[COUNTED_BY_GEP]], align 4
+// FRONTEND-NEXT: [[FLEXIBLE_ARRAY_MEMBER_SIZE:%.*]] = mul nuw i64 [[COUNTED_BY_LOAD]], 1
+// FRONTEND-NEXT: [[TMP0:%.*]] = icmp sgt i64 [[FLEXIBLE_ARRAY_MEMBER_SIZE]], -1
+// FRONTEND-NEXT: [[TMP1:%.*]] = select i1 [[TMP0]], i64 [[FLEXIBLE_ARRAY_MEMBER_SIZE]], i64 0
+// FRONTEND-NEXT: ret i64 [[TMP1]]
+//
+// O2-LABEL: define dso_local range(i64 0, -9223372036854775808) i64 @ptr_fam(
+// O2-SAME: ptr noundef readonly captures(none) [[P:%.*]]) local_unnamed_addr #[[ATTR4:[0-9]+]] {
+// O2-NEXT: [[ENTRY:.*:]]
+// O2-NEXT: [[COUNTED_BY_LOAD:%.*]] = load i64, ptr [[P]], align 4
+// O2-NEXT: [[TMP0:%.*]] = tail call i64 @llvm.smax.i64(i64 [[COUNTED_BY_LOAD]], i64 0)
+// O2-NEXT: ret i64 [[TMP0]]
+//
+size_t ptr_fam(struct annotated_flex *p) {
+ return __builtin_dynamic_object_size(p->fam, 1);
+}
+
+// Test: pointer x &fam[idx].
+// Same regression-boundary purpose as ptr_fam: runtime path with index
+// subtracted from the count loaded via %p.
+// FRONTEND-LABEL: define dso_local i64 @ptr_subscript(
+// FRONTEND-SAME: ptr noundef [[P:%.*]]) #[[ATTR0]] {
+// FRONTEND-NEXT: [[ENTRY:.*:]]
+// FRONTEND-NEXT: [[FAM:%.*]] = getelementptr inbounds nuw [[STRUCT_ANNOTATED_FLEX:%.*]], ptr [[P]], i32 0, i32 2
+// FRONTEND-NEXT: [[ARRAYIDX:%.*]] = getelementptr inbounds [0 x i8], ptr [[FAM]], i64 0, i64 3
+// FRONTEND-NEXT: [[COUNTED_BY_GEP:%.*]] = getelementptr inbounds [[STRUCT_ANNOTATED_FLEX]], ptr [[P]], i32 0, i32 0
+// FRONTEND-NEXT: [[COUNTED_BY_LOAD:%.*]] = load i64, ptr [[COUNTED_BY_GEP]], align 4
+// FRONTEND-NEXT: [[FLEXIBLE_ARRAY_MEMBER_SIZE:%.*]] = mul nuw i64 [[COUNTED_BY_LOAD]], 1
+// FRONTEND-NEXT: [[RESULT:%.*]] = sub nuw i64 [[FLEXIBLE_ARRAY_MEMBER_SIZE]], 3
+// FRONTEND-NEXT: [[TMP0:%.*]] = icmp sgt i64 [[RESULT]], -1
+// FRONTEND-NEXT: [[TMP1:%.*]] = and i1 true, [[TMP0]]
+// FRONTEND-NEXT: [[TMP2:%.*]] = select i1 [[TMP1]], i64 [[RESULT]], i64 0
+// FRONTEND-NEXT: ret i64 [[TMP2]]
+//
+// O2-LABEL: define dso_local range(i64 0, -9223372036854775808) i64 @ptr_subscript(
+// O2-SAME: ptr noundef readonly captures(none) [[P:%.*]]) local_unnamed_addr #[[ATTR4]] {
+// O2-NEXT: [[ENTRY:.*:]]
+// O2-NEXT: [[COUNTED_BY_LOAD:%.*]] = load i64, ptr [[P]], align 4
+// O2-NEXT: [[RESULT:%.*]] = add i64 [[COUNTED_BY_LOAD]], -3
+// O2-NEXT: [[TMP0:%.*]] = tail call i64 @llvm.smax.i64(i64 [[RESULT]], i64 0)
+// O2-NEXT: ret i64 [[TMP0]]
+//
+size_t ptr_subscript(struct annotated_flex *p) {
+ return __builtin_dynamic_object_size(&p->fam[3], 1);
+}
+
+// Test: pointer x &fam (array-as-a-whole over an opaque base).
+// Layout-derived path, but @llvm.objectsize can't determine the size of
+// the underlying object, so it returns -1 (size_t max). Matches GCC.
+// FRONTEND-LABEL: define dso_local i64 @ptr_addrof(
+// FRONTEND-SAME: ptr noundef [[P:%.*]]) #[[ATTR0]] {
+// FRONTEND-NEXT: [[ENTRY:.*:]]
+// FRONTEND-NEXT: [[FAM:%.*]] = getelementptr inbounds nuw [[STRUCT_ANNOTATED_FLEX:%.*]], ptr [[P]], i32 0, i32 2
+// FRONTEND-NEXT: [[TMP0:%.*]] = call i64 @llvm.objectsize.i64.p0(ptr [[FAM]], i1 false, i1 true, i1 true)
+// FRONTEND-NEXT: ret i64 [[TMP0]]
+//
+// O2-LABEL: define dso_local i64 @ptr_addrof(
+// O2-SAME: ptr noundef readnone [[P:%.*]]) local_unnamed_addr #[[ATTR3]] {
+// O2-NEXT: [[ENTRY:.*:]]
+// O2-NEXT: ret i64 -1
+//
+size_t ptr_addrof(struct annotated_flex *p) {
+ return __builtin_dynamic_object_size(&p->fam, 1);
+}
+//.
+// O2: [[META3:![0-9]+]] = !{!"omnipotent char", [[META4:![0-9]+]], i64 0}
+// O2: [[META4]] = !{!"Simple C/C++ TBAA"}
+// O2: [[LONG_TBAA5]] = !{[[META6:![0-9]+]], [[META6]], i64 0}
+// O2: [[META6]] = !{!"long", [[META3]], i64 0}
+//.
More information about the cfe-commits
mailing list