Skip to content

[Clang][CodeGen] Do not set inbounds flag for struct GEP with null base pointers #130734

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 11, 2025

Conversation

dtcxzyw
Copy link
Member

@dtcxzyw dtcxzyw commented Mar 11, 2025

In the LLVM middle-end we want to fold gep inbounds null, idx -> null: https://alive2.llvm.org/ce/z/5ZkPx-
This pattern is common in real-world programs (dtcxzyw/llvm-opt-benchmark#55 (comment)). Generally, it exists in some (actually) unreachable blocks, which is introduced by JumpThreading.

However, some old-style offsetof macros are still widely used in real-world C/C++ code (e.g., hwloc/slurm/luajit). To avoid breaking existing code and inconvenience to downstream users, this patch removes the inbounds flag from the struct gep if the base pointer is null.

@dtcxzyw dtcxzyw requested review from nikic and efriedma-quic March 11, 2025 08:37
@llvmbot llvmbot added clang Clang issues not falling into any other category clang:codegen IR generation bugs: mangling, exceptions, etc. labels Mar 11, 2025
@llvmbot
Copy link
Member

llvmbot commented Mar 11, 2025

@llvm/pr-subscribers-clang-codegen

Author: Yingwei Zheng (dtcxzyw)

Changes

In the LLVM middle-end we want to fold gep inbounds null, idx -> null: https://alive2.llvm.org/ce/z/5ZkPx-
This pattern is common in real-world programs. Generally, it exists in some (actually) unreachable blocks, which is introduced by JumpThreading.

However, some old-style offsetof macros are still widely used in real-world C/C++ code (e.g., hwloc/slurm/luajit). To avoid breaking existing code and inconvenience to downstream users, this patch removes the inbounds flag from the struct gep if the base pointer is null.


Full diff: https://github.com/llvm/llvm-project/pull/130734.diff

3 Files Affected:

  • (modified) clang/lib/CodeGen/CGBuilder.h (+10-5)
  • (modified) clang/test/CodeGen/catch-nullptr-and-nonzero-offset-in-offsetof-idiom.c (+1-1)
  • (modified) clang/test/CodeGenCXX/catch-nullptr-and-nonzero-offset-in-offsetof-idiom.cpp (+1-1)
diff --git a/clang/lib/CodeGen/CGBuilder.h b/clang/lib/CodeGen/CGBuilder.h
index b8036cf6e6a30..11e8818b33397 100644
--- a/clang/lib/CodeGen/CGBuilder.h
+++ b/clang/lib/CodeGen/CGBuilder.h
@@ -223,11 +223,16 @@ class CGBuilderTy : public CGBuilderBaseTy {
     const llvm::StructLayout *Layout = DL.getStructLayout(ElTy);
     auto Offset = CharUnits::fromQuantity(Layout->getElementOffset(Index));
 
-    return Address(CreateStructGEP(Addr.getElementType(), Addr.getBasePointer(),
-                                   Index, Name),
-                   ElTy->getElementType(Index),
-                   Addr.getAlignment().alignmentAtOffset(Offset),
-                   Addr.isKnownNonNull());
+    // Specially, we don't add inbounds flags if the base pointer is null.
+    // This is a workaround for old-style offsetof macros.
+    llvm::GEPNoWrapFlags NWFlags = llvm::GEPNoWrapFlags::noUnsignedWrap();
+    if (!isa<llvm::ConstantPointerNull>(Addr.getBasePointer()))
+      NWFlags |= llvm::GEPNoWrapFlags::inBounds();
+    return Address(
+        CreateConstGEP2_32(Addr.getElementType(), Addr.getBasePointer(), 0,
+                           Index, Name, NWFlags),
+        ElTy->getElementType(Index),
+        Addr.getAlignment().alignmentAtOffset(Offset), Addr.isKnownNonNull());
   }
 
   /// Given
diff --git a/clang/test/CodeGen/catch-nullptr-and-nonzero-offset-in-offsetof-idiom.c b/clang/test/CodeGen/catch-nullptr-and-nonzero-offset-in-offsetof-idiom.c
index 68c0ee3a3a885..a7cfd77766712 100644
--- a/clang/test/CodeGen/catch-nullptr-and-nonzero-offset-in-offsetof-idiom.c
+++ b/clang/test/CodeGen/catch-nullptr-and-nonzero-offset-in-offsetof-idiom.c
@@ -17,7 +17,7 @@ struct S {
 
 // CHECK-LABEL: @get_offset_of_y_naively(
 // CHECK-NEXT:  entry:
-// CHECK-NEXT:    ret i64 ptrtoint (ptr getelementptr inbounds nuw ([[STRUCT_S:%.*]], ptr null, i32 0, i32 1) to i64)
+// CHECK-NEXT:    ret i64 ptrtoint (ptr getelementptr nuw ([[STRUCT_S:%.*]], ptr null, i32 0, i32 1) to i64)
 //
 uintptr_t get_offset_of_y_naively(void) {
   return ((uintptr_t)(&(((struct S *)0)->y)));
diff --git a/clang/test/CodeGenCXX/catch-nullptr-and-nonzero-offset-in-offsetof-idiom.cpp b/clang/test/CodeGenCXX/catch-nullptr-and-nonzero-offset-in-offsetof-idiom.cpp
index 34d4f4c9e34eb..f00a2c486574c 100644
--- a/clang/test/CodeGenCXX/catch-nullptr-and-nonzero-offset-in-offsetof-idiom.cpp
+++ b/clang/test/CodeGenCXX/catch-nullptr-and-nonzero-offset-in-offsetof-idiom.cpp
@@ -10,7 +10,7 @@ struct S {
 
 // CHECK-LABEL: @_Z23get_offset_of_y_naivelyv(
 // CHECK-NEXT:  entry:
-// CHECK-NEXT:    ret i64 ptrtoint (ptr getelementptr inbounds nuw ([[STRUCT_S:%.*]], ptr null, i32 0, i32 1) to i64)
+// CHECK-NEXT:    ret i64 ptrtoint (ptr getelementptr nuw ([[STRUCT_S:%.*]], ptr null, i32 0, i32 1) to i64)
 //
 uintptr_t get_offset_of_y_naively() {
   return ((uintptr_t)(&(((S *)nullptr)->y)));

@llvmbot
Copy link
Member

llvmbot commented Mar 11, 2025

@llvm/pr-subscribers-clang

Author: Yingwei Zheng (dtcxzyw)

Changes

In the LLVM middle-end we want to fold gep inbounds null, idx -&gt; null: https://alive2.llvm.org/ce/z/5ZkPx-
This pattern is common in real-world programs. Generally, it exists in some (actually) unreachable blocks, which is introduced by JumpThreading.

However, some old-style offsetof macros are still widely used in real-world C/C++ code (e.g., hwloc/slurm/luajit). To avoid breaking existing code and inconvenience to downstream users, this patch removes the inbounds flag from the struct gep if the base pointer is null.


Full diff: https://github.com/llvm/llvm-project/pull/130734.diff

3 Files Affected:

  • (modified) clang/lib/CodeGen/CGBuilder.h (+10-5)
  • (modified) clang/test/CodeGen/catch-nullptr-and-nonzero-offset-in-offsetof-idiom.c (+1-1)
  • (modified) clang/test/CodeGenCXX/catch-nullptr-and-nonzero-offset-in-offsetof-idiom.cpp (+1-1)
diff --git a/clang/lib/CodeGen/CGBuilder.h b/clang/lib/CodeGen/CGBuilder.h
index b8036cf6e6a30..11e8818b33397 100644
--- a/clang/lib/CodeGen/CGBuilder.h
+++ b/clang/lib/CodeGen/CGBuilder.h
@@ -223,11 +223,16 @@ class CGBuilderTy : public CGBuilderBaseTy {
     const llvm::StructLayout *Layout = DL.getStructLayout(ElTy);
     auto Offset = CharUnits::fromQuantity(Layout->getElementOffset(Index));
 
-    return Address(CreateStructGEP(Addr.getElementType(), Addr.getBasePointer(),
-                                   Index, Name),
-                   ElTy->getElementType(Index),
-                   Addr.getAlignment().alignmentAtOffset(Offset),
-                   Addr.isKnownNonNull());
+    // Specially, we don't add inbounds flags if the base pointer is null.
+    // This is a workaround for old-style offsetof macros.
+    llvm::GEPNoWrapFlags NWFlags = llvm::GEPNoWrapFlags::noUnsignedWrap();
+    if (!isa<llvm::ConstantPointerNull>(Addr.getBasePointer()))
+      NWFlags |= llvm::GEPNoWrapFlags::inBounds();
+    return Address(
+        CreateConstGEP2_32(Addr.getElementType(), Addr.getBasePointer(), 0,
+                           Index, Name, NWFlags),
+        ElTy->getElementType(Index),
+        Addr.getAlignment().alignmentAtOffset(Offset), Addr.isKnownNonNull());
   }
 
   /// Given
diff --git a/clang/test/CodeGen/catch-nullptr-and-nonzero-offset-in-offsetof-idiom.c b/clang/test/CodeGen/catch-nullptr-and-nonzero-offset-in-offsetof-idiom.c
index 68c0ee3a3a885..a7cfd77766712 100644
--- a/clang/test/CodeGen/catch-nullptr-and-nonzero-offset-in-offsetof-idiom.c
+++ b/clang/test/CodeGen/catch-nullptr-and-nonzero-offset-in-offsetof-idiom.c
@@ -17,7 +17,7 @@ struct S {
 
 // CHECK-LABEL: @get_offset_of_y_naively(
 // CHECK-NEXT:  entry:
-// CHECK-NEXT:    ret i64 ptrtoint (ptr getelementptr inbounds nuw ([[STRUCT_S:%.*]], ptr null, i32 0, i32 1) to i64)
+// CHECK-NEXT:    ret i64 ptrtoint (ptr getelementptr nuw ([[STRUCT_S:%.*]], ptr null, i32 0, i32 1) to i64)
 //
 uintptr_t get_offset_of_y_naively(void) {
   return ((uintptr_t)(&(((struct S *)0)->y)));
diff --git a/clang/test/CodeGenCXX/catch-nullptr-and-nonzero-offset-in-offsetof-idiom.cpp b/clang/test/CodeGenCXX/catch-nullptr-and-nonzero-offset-in-offsetof-idiom.cpp
index 34d4f4c9e34eb..f00a2c486574c 100644
--- a/clang/test/CodeGenCXX/catch-nullptr-and-nonzero-offset-in-offsetof-idiom.cpp
+++ b/clang/test/CodeGenCXX/catch-nullptr-and-nonzero-offset-in-offsetof-idiom.cpp
@@ -10,7 +10,7 @@ struct S {
 
 // CHECK-LABEL: @_Z23get_offset_of_y_naivelyv(
 // CHECK-NEXT:  entry:
-// CHECK-NEXT:    ret i64 ptrtoint (ptr getelementptr inbounds nuw ([[STRUCT_S:%.*]], ptr null, i32 0, i32 1) to i64)
+// CHECK-NEXT:    ret i64 ptrtoint (ptr getelementptr nuw ([[STRUCT_S:%.*]], ptr null, i32 0, i32 1) to i64)
 //
 uintptr_t get_offset_of_y_naively() {
   return ((uintptr_t)(&(((S *)nullptr)->y)));

Copy link
Contributor

@nikic nikic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this makes sense, but please wait for a second opinion...

Copy link
Collaborator

@efriedma-quic efriedma-quic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I somehow thought we had already done something about this, but I can't find any reference to it.

Anyway, if we're going to support this as an extension, I don't want to bury it in CGBuilder.h. We should decide exactly when we want it to trigger, and make it trigger for those language constructs. Not any construct that happens to go through CreateStructGEP, and not any pointer that happens to get folded to ConstantPointerNull. (Note on some targets, some null pointers are not ConstantPointerNull.)

@dtcxzyw
Copy link
Member Author

dtcxzyw commented Mar 12, 2025

Not any construct that happens to go through CreateStructGEP

Agree. We hit other cases where the optimization will break the stage2 build. See also #130742 (comment)

not any pointer that happens to get folded to ConstantPointerNull. (Note on some targets, some null pointers are not ConstantPointerNull.)

We only perform this optimization in addrspace(0). If some patterns cannot be constant folded into null in the clang codegen, we just treat them as UB.

@efriedma-quic
Copy link
Collaborator

We only perform this optimization in addrspace(0).

I'd still prefer to get this right for all address-spaces, so we don't need to revisit later.

But really, I'm more concerned about the "happens to get folded" part: there isn't a stable set of values that get constant-folded, at the LLVM IR level, it's just whether IRBuilder happens to do today. The only way to get a stable answer is to query the AST (Expr::isNullPointerConstant).

@dtcxzyw
Copy link
Member Author

dtcxzyw commented Mar 13, 2025

I'd still prefer to get this right for all address-spaces, so we don't need to revisit later.

Sentinel pointer value support for non-0 address spaces is still work in progress: #83109
I am ok to add a helper like Address::isConstantNull if needed.

But really, I'm more concerned about the "happens to get folded" part: there isn't a stable set of values that get constant-folded, at the LLVM IR level, it's just whether IRBuilder happens to do today.

I don't expect it to work on more complex patterns like (int*)(x-x).

The only way to get a stable answer is to query the AST (Expr::isNullPointerConstant).

Expr::isNullPointerConstant is unavailable in clang CodeGen unless we store the result into Address.

@efriedma-quic
Copy link
Collaborator

I don't expect it to work on more complex patterns like (int*)(x-x).

Sure, that particular pattern probably doesn't need to work, but at the very least, you're depending on certain casts getting constant-folded. And it's not clear what set of casts will in fact be consistently folded. Even if all the constructs you care about get folded right now, it's not clear that will remain true in the future. And with CIR coming along, they also need a consistent rule to follow.

Expr::isNullPointerConstant is unavailable in clang CodeGen unless we store the result into Address.

Again, the check should not be in CreateStructGEP; this check needs to happen at a higher level. And at that level, you should have an Expr*, not just an Address.


Do you care at all about patterns like ((uintptr_t)(&(((S *)nullptr)->y.z)));?

@dtcxzyw
Copy link
Member Author

dtcxzyw commented Mar 13, 2025

this check needs to happen at a higher level. And at that level, you should have an Expr*, not just an Address.

I tried to use Expr::isNullPointerConstant in CodeGenFunction::EmitMemberExpr:

LValue CodeGenFunction::EmitMemberExpr(const MemberExpr *E) {
if (DeclRefExpr *DRE = tryToConvertMemberExprToDeclRefExpr(*this, E)) {
EmitIgnoredExpr(E->getBase());
return EmitDeclRefLValue(DRE);
}
Expr *BaseExpr = E->getBase();
// If this is s.x, emit s as an lvalue. If it is s->x, emit s as a scalar.

struct S {
  int x, y;
};

using uintptr_t = unsigned long long;
uintptr_t get_offset_of_y_naively() {
  return ((uintptr_t)(&(((S *)nullptr)->y)));
}

The base expr is:

ParenExpr 0x58d207ad2048 'S *'
`-CStyleCastExpr 0x58d207ad2020 'S *' <NoOp>
  `-ImplicitCastExpr 0x58d207ad2008 'S *' <NullToPointer> part_of_explicit_cast
    `-CXXNullPtrLiteralExpr 0x58d207ad1f80 'nullptr_t'

isNullPointerConstant returns false regardless of the value of NullPointerConstantValueDependence.

Do you care at all about patterns like ((uintptr_t)(&(((S *)nullptr)->y.z)));?

Yes. This pattern is used by openssl.

@efriedma-quic
Copy link
Collaborator

Just ran into some related code: see 3d0a540 . Maybe using IgnoreParenCasts() like that code does is appropriate. (I don't really like using IgnoreParenCasts() because there's such a big variety of casts, but it's probably okay in this context.)

Do you care at all about patterns like ((uintptr_t)(&(((S *)nullptr)->y.z)));?

Yes. This pattern is used by openssl.

Note this has two MemberExprs, so if you want to handle this, you'll have to look through the MemberExpr when you're generating the second GEP, I think.

@dtcxzyw
Copy link
Member Author

dtcxzyw commented Mar 19, 2025

@efriedma-quic Any more comments?

Copy link
Collaborator

@efriedma-quic efriedma-quic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs release note.

The big thing I'm not sure about is whether we want a corresponding warning. We're detecting a pattern we know has undefined behavior, so we should maybe warn? CC @AaronBallman @erichkeane

@dtcxzyw dtcxzyw force-pushed the offsetof-inbounds branch from 4177691 to 670ab42 Compare March 20, 2025 10:11
@AaronBallman
Copy link
Collaborator

Needs release note.

The big thing I'm not sure about is whether we want a corresponding warning. We're detecting a pattern we know has undefined behavior, so we should maybe warn? CC @AaronBallman @erichkeane

This pattern is undefined behavior, but we're wanting to define the behavior in this one specific case. My preference is to be honest about that: add documentation to clang/docs/LanguageExtensions.rst saying that we define the behavior of dereferencing a null pointer constant in these specific circumstances (and be clear that it's UB in all other circumstances), issue a pedantic warning about the extension, and make sure we have enough test coverage to help us avoid regressing the extension in the future. Bonus points if the pedantic warning comes with a fixit that suggest offsetof (not __builtin_offsetof because that's still an extension), but I don't think it's strictly required (fixits that require including a header file might be tricky?).

// CHECK-NEXT: ret i64 ptrtoint (ptr getelementptr ([[STRUCT_S:%.*]], ptr getelementptr ([[STRUCT_T:%.*]], ptr null, i32 0, i32 1), i32 0, i32 1) to i64)
//
uintptr_t get_offset_of_y_naively_nested() {
return ((uintptr_t)(&(((T *)nullptr)->s.y)));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to have tests for other null pointer constants like 0, but there's a question of how deep down the rabbit hole we want the extension to go. I think it makes sense to allow it only for null pointer constants, not just any constant expression which converts to a null pointer. But maybe that's too restrictive?

Suggested tests:

nullptr_t{} // Should probably be accepted because this is a valid null pointer constant
constexpr void *null_ptr = nullptr; // Should probably be rejected, null_ptr is not a null pointer constant
0 // Should be accepted because this is a valid null pointer constant
(void *)0 // Should only be accepted in C because it's a valid null pointer constant there, but not in C++

and you should also ensure the C tests cover these cases as well.

@dtcxzyw dtcxzyw force-pushed the offsetof-inbounds branch from f9c7cf9 to ad576ca Compare March 31, 2025 09:04
dtcxzyw added a commit that referenced this pull request Apr 3, 2025
… type (#134221)

If `CreateConstInBoundsGEP2_32` returns a constant null/gep, the cast to
GetElementPtrInst will fail.
This patch uses two static helpers
`GEPOperator::accumulateConstantOffset/GetElementPtrInst::getIndexedType`
to infer offset and result type instead of depending on the GEP result.

This patch is extracted from
#130734.
llvm-sync bot pushed a commit to arm/arm-toolchain that referenced this pull request Apr 3, 2025
… and result type (#134221)

If `CreateConstInBoundsGEP2_32` returns a constant null/gep, the cast to
GetElementPtrInst will fail.
This patch uses two static helpers
`GEPOperator::accumulateConstantOffset/GetElementPtrInst::getIndexedType`
to infer offset and result type instead of depending on the GEP result.

This patch is extracted from
llvm/llvm-project#130734.
dtcxzyw added a commit that referenced this pull request Apr 9, 2025
…134269)

This patch turns off inbounds/nuw flags for member accesses when
`-fwrapv-pointer` is set.

Closes #132449.

It is required by #130734.
llvm-sync bot pushed a commit to arm/arm-toolchain that referenced this pull request Apr 9, 2025
…uct GEPs (#134269)

This patch turns off inbounds/nuw flags for member accesses when
`-fwrapv-pointer` is set.

Closes llvm/llvm-project#132449.

It is required by llvm/llvm-project#130734.
@dtcxzyw dtcxzyw force-pushed the offsetof-inbounds branch from ad576ca to 0f6ff60 Compare April 10, 2025 01:56
@dtcxzyw
Copy link
Member Author

dtcxzyw commented Apr 10, 2025

Rebased on the top of #134269 and #134221

AllinLeeYL pushed a commit to AllinLeeYL/llvm-project that referenced this pull request Apr 10, 2025
…lvm#134269)

This patch turns off inbounds/nuw flags for member accesses when
`-fwrapv-pointer` is set.

Closes llvm#132449.

It is required by llvm#130734.
Copy link
Collaborator

@efriedma-quic efriedma-quic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@dtcxzyw dtcxzyw merged commit 1711996 into llvm:main Apr 11, 2025
12 checks passed
@dtcxzyw dtcxzyw deleted the offsetof-inbounds branch April 11, 2025 01:04
var-const pushed a commit to ldionne/llvm-project that referenced this pull request Apr 17, 2025
…lvm#134269)

This patch turns off inbounds/nuw flags for member accesses when
`-fwrapv-pointer` is set.

Closes llvm#132449.

It is required by llvm#130734.
var-const pushed a commit to ldionne/llvm-project that referenced this pull request Apr 17, 2025
…se pointers (llvm#130734)

In the LLVM middle-end we want to fold `gep inbounds null, idx -> null`:
https://alive2.llvm.org/ce/z/5ZkPx-
This pattern is common in real-world programs
(dtcxzyw/llvm-opt-benchmark#55 (comment)).
Generally, it exists in some (actually) unreachable blocks, which is
introduced by JumpThreading.

However, some old-style offsetof macros are still widely used in
real-world C/C++ code (e.g., hwloc/slurm/luajit). To avoid breaking
existing code and inconvenience to downstream users, this patch removes
the inbounds flag from the struct gep if the base pointer is null.
dtcxzyw added a commit that referenced this pull request Apr 17, 2025
llvm-sync bot pushed a commit to arm/arm-toolchain that referenced this pull request Apr 17, 2025
IanWood1 pushed a commit to IanWood1/llvm-project that referenced this pull request May 6, 2025
IanWood1 pushed a commit to IanWood1/llvm-project that referenced this pull request May 6, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
clang:codegen IR generation bugs: mangling, exceptions, etc. clang Clang issues not falling into any other category
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants