[libcxx-commits] [libcxx] [llvm] [libc++] Fix optional<T>::transform to optional<T&> (GH#220332) (PR #220500)

Yuvraj Choudhary via libcxx-commits libcxx-commits at lists.llvm.org
Wed Sep 2 05:27:52 PDT 2026


https://github.com/yuvrajmnit007 updated https://github.com/llvm/llvm-project/pull/220500

>From 07ca36c17990f859c42ecbab5332bd75d2f904f7 Mon Sep 17 00:00:00 2001
From: Yuvraj Choudhary <yuvrajchoudhary3768 at gmail.com>
Date: Tue, 1 Sep 2026 10:31:26 +0530
Subject: [PATCH 1/2] [TLOF] Don't place unnamed_addr constants with
 undef/poison in mergeable sections

The ELF linker deduplicates constants in SHF_MERGE sections (.rodata.cstN)
by comparing their raw byte content. LLVM emits undef and poison bytes as
zeros, so a constant like:

  @a = private unnamed_addr constant <{ [4 x i8], [4 x i8] }>
         <{ [4 x i8] undef, [4 x i8] zeroinitializer }>, align 4

emits 8 zero bytes -- byte-identical to a zeroinitializer constant of the
same size:

  @b = private unnamed_addr constant <{ [4 x i8], [4 x i8] }>
         zeroinitializer, align 4

The linker then merges them to the same address, making 'icmp ne ptr @a, @b'
evaluate to false at runtime even though @a and @b have semantically distinct
initializers. This also causes GVN and SCCP to produce wrong results: they
assume the two constants cannot alias, but the linker says otherwise.

Fix: in TargetLoweringObjectFile::getKindForGlobal(), add a recursive helper
containsUndefOrPoison() and use it to gate the MergeableConst* section kinds.
If the initializer contains any undef or poison sub-element, fall back to
SectionKind::getReadOnly() (plain .rodata) instead, preventing the incorrect
linker merge.

PoisonValue is a subclass of UndefValue, so isa<UndefValue> catches both.

Fixes: https://github.com/llvm/llvm-project/issues/220091

Related: rust-lang/rust#161973, rust-lang/rust#162078
---
 llvm/lib/Target/TargetLoweringObjectFile.cpp  | 32 +++++++++
 .../X86/unnamed-addr-undef-not-mergeable.ll   | 71 +++++++++++++++++++
 2 files changed, 103 insertions(+)
 create mode 100644 llvm/test/CodeGen/X86/unnamed-addr-undef-not-mergeable.ll

diff --git a/llvm/lib/Target/TargetLoweringObjectFile.cpp b/llvm/lib/Target/TargetLoweringObjectFile.cpp
index b94c374e1dbd5..8562ef04e0550 100644
--- a/llvm/lib/Target/TargetLoweringObjectFile.cpp
+++ b/llvm/lib/Target/TargetLoweringObjectFile.cpp
@@ -80,6 +80,26 @@ static bool isNullOrUndef(const Constant *C) {
   return true;
 }
 
+/// Return true if the constant contains any undef or poison sub-elements,
+/// recursively traversing aggregate types. PoisonValue is a subclass of
+/// UndefValue, so isa<UndefValue> catches both.
+///
+/// Such constants must not be placed in mergeable sections: the ELF linker
+/// merges constants in those sections by comparing byte content, but undef/
+/// poison bytes are emitted as zeros, making them byte-identical to a
+/// zeroinitializer constant. Allowing this merge would violate unnamed_addr
+/// semantics. See https://github.com/llvm/llvm-project/issues/220091.
+static bool containsUndefOrPoison(const Constant *C) {
+  if (isa<UndefValue>(C))
+    return true;
+  if (!isa<ConstantAggregate>(C))
+    return false;
+  for (const auto *Operand : C->operand_values())
+    if (containsUndefOrPoison(cast<Constant>(Operand)))
+      return true;
+  return false;
+}
+
 static bool isSuitableForBSS(const GlobalVariable *GV) {
   const Constant *C = GV->getInitializer();
 
@@ -316,6 +336,18 @@ SectionKind TargetLoweringObjectFile::getKindForGlobal(const GlobalObject *GO,
       if (!GVar->hasGlobalUnnamedAddr())
         return SectionKind::getReadOnly();
 
+      // Do not place constants whose initializer contains undef or poison
+      // sub-elements into mergeable sections. The ELF linker deduplicates
+      // constants in such sections by comparing raw byte content, but undef
+      // and poison bytes are both emitted as zeros. This would make a constant
+      // like `<{ [4 x i8] undef, [4 x i8] zeroinitializer }>` byte-identical
+      // to `zeroinitializer`, allowing the linker to merge them to the same
+      // address even though they are not semantically equivalent. Placing the
+      // constant in non-mergeable .rodata prevents this incorrect merge.
+      // See https://github.com/llvm/llvm-project/issues/220091.
+      if (containsUndefOrPoison(C))
+        return SectionKind::getReadOnly();
+
       // If initializer is a null-terminated string, put it in a "cstring"
       // section of the right width.
       if (ArrayType *ATy = dyn_cast<ArrayType>(C->getType())) {
diff --git a/llvm/test/CodeGen/X86/unnamed-addr-undef-not-mergeable.ll b/llvm/test/CodeGen/X86/unnamed-addr-undef-not-mergeable.ll
new file mode 100644
index 0000000000000..e6a2576fdd56d
--- /dev/null
+++ b/llvm/test/CodeGen/X86/unnamed-addr-undef-not-mergeable.ll
@@ -0,0 +1,71 @@
+; RUN: llc < %s -mtriple=x86_64-unknown-linux-gnu | FileCheck %s
+;
+; Regression test for https://github.com/llvm/llvm-project/issues/220091
+;
+; unnamed_addr constants that contain undef or poison sub-elements must NOT
+; be placed in ELF mergeable sections (.rodata.cstN, SHF_MERGE "aM" flag).
+;
+; The ELF linker merges constants in those sections by comparing raw byte
+; content. LLVM emits undef and poison bytes as zeros, which makes such a
+; constant byte-identical to a zeroinitializer constant of the same size.
+; The linker would then assign them the same address even though they are
+; semantically distinct under the LLVM IR spec, leading to miscompilation.
+;
+; Fix: getKindForGlobal() now returns ReadOnly (plain .rodata) instead of a
+; MergeableConst* section kind when the initializer contains undef or poison.
+
+; ---------------------------------------------------------------------------
+; @undef_and_zero: struct with undef first half, zero second half (8 bytes).
+; Even though it's 8 bytes and unnamed_addr, it must NOT go to .rodata.cst8
+; because the undef half would be emitted as zeros, making it byte-identical
+; to @all_zero below.
+; ---------------------------------------------------------------------------
+ at undef_and_zero = private unnamed_addr constant <{ [4 x i8], [4 x i8] }> <{ [4 x i8] undef, [4 x i8] zeroinitializer }>, align 4
+
+; CHECK:     .type	.Lundef_and_zero, at object
+; CHECK-NOT: .section	.rodata.cst8
+; CHECK:     .section	.rodata,"a", at progbits
+
+; ---------------------------------------------------------------------------
+; @all_zero: pure zeroinitializer of the same 8-byte size.
+; This SHOULD still go to .rodata.cst8 (no regression for non-undef case).
+; ---------------------------------------------------------------------------
+ at all_zero = private unnamed_addr constant i64 0, align 8
+
+; CHECK:     .type	.Lall_zero, at object
+; CHECK:     .section	.rodata.cst8,"aM", at progbits,8
+
+; ---------------------------------------------------------------------------
+; @nested_undef: undef nested inside a struct field.
+; Must be non-mergeable even though it's an aggregate with a concrete field.
+; ---------------------------------------------------------------------------
+ at nested_undef = private unnamed_addr constant { i32, i32 } { i32 undef, i32 42 }, align 4
+
+; CHECK:     .type	.Lnested_undef, at object
+; CHECK:     .section	.rodata,"a", at progbits
+
+; ---------------------------------------------------------------------------
+; @poison_field: poison value (PoisonValue is a subclass of UndefValue).
+; Must also be non-mergeable.
+; ---------------------------------------------------------------------------
+ at poison_field = private unnamed_addr constant { i32, i32 } { i32 poison, i32 0 }, align 4
+
+; CHECK:     .type	.Lpoison_field, at object
+; (stays in .rodata from the previous section, no new .section directive needed)
+
+; ---------------------------------------------------------------------------
+; @no_undef_4: fully concrete constant -- still gets mergeable section.
+; ---------------------------------------------------------------------------
+ at no_undef_4 = private unnamed_addr constant i32 1234, align 4
+
+; CHECK:     .type	.Lno_undef_4, at object
+; CHECK:     .section	.rodata.cst4,"aM", at progbits,4
+
+; Dummy function to prevent globals from being dead-stripped.
+define ptr @use() {
+  %a = select i1 true,  ptr @undef_and_zero, ptr @all_zero
+  %b = select i1 false, ptr @nested_undef,   ptr @poison_field
+  %c = select i1 true,  ptr %a,              ptr @no_undef_4
+  %d = select i1 false, ptr %b,              ptr %c
+  ret ptr %d
+}

>From 691e7a621b3e527ee873de6a720be358befdb19e Mon Sep 17 00:00:00 2001
From: Yuvraj Choudhary <yuvrajchoudhary3768 at gmail.com>
Date: Wed, 2 Sep 2026 01:21:42 +0530
Subject: [PATCH 2/2] [libc++] Fix optional<T>::transform to optional<T&>
 (GH#220332)

---
 libcxx/include/__optional/optional_ref.h             |  3 +++
 .../optional/optional.monadic/transform.pass.cpp     | 12 ++++++++++++
 2 files changed, 15 insertions(+)

diff --git a/libcxx/include/__optional/optional_ref.h b/libcxx/include/__optional/optional_ref.h
index 20270e0594df5..0ed3869c2136e 100644
--- a/libcxx/include/__optional/optional_ref.h
+++ b/libcxx/include/__optional/optional_ref.h
@@ -79,6 +79,9 @@ class optional<_Tp&> : public __optional_ref_iterator_base<_Tp&> {
 private:
   _Tp* __value_ = nullptr;
 
+  template <class>
+  friend class optional;
+
   template <class _Up>
   _LIBCPP_HIDE_FROM_ABI constexpr void __convert_init_ref_val(_Up&& __val) {
     _Tp& __r(std::forward<_Up>(__val));
diff --git a/libcxx/test/std/utilities/optional/optional.monadic/transform.pass.cpp b/libcxx/test/std/utilities/optional/optional.monadic/transform.pass.cpp
index 96303c5796f6d..9a0ab6488ca16 100644
--- a/libcxx/test/std/utilities/optional/optional.monadic/transform.pass.cpp
+++ b/libcxx/test/std/utilities/optional/optional.monadic/transform.pass.cpp
@@ -321,6 +321,18 @@ constexpr bool test_ref() {
     assert(&(*o2) == &j);
   }
 
+  // https://github.com/llvm/llvm-project/issues/220332
+  {
+    struct foo {
+      int& member() { return i_; }
+      int i_ = 0;
+    };
+    std::optional<foo> opt(std::in_place);
+    std::same_as<std::optional<int&>> decltype(auto) mem = opt.transform(&foo::member);
+    assert(mem.has_value());
+    assert(&(*mem) == &opt->i_);
+  }
+
   return true;
 }
 #endif



More information about the libcxx-commits mailing list