[llvm] [IR] Add nofreeobj parameter/return attribute (PR #206445)

Nikita Popov via llvm-commits llvm-commits at lists.llvm.org
Mon Jun 29 03:06:56 PDT 2026


https://github.com/nikic created https://github.com/llvm/llvm-project/pull/206445

Alternative to https://github.com/llvm/llvm-project/pull/203491.

This adds a `nofreeobj` attribute, which says that the underlying object cannot be freed. For arguments during the execution of the function, for returns from that point forward. This is unlike `nofree` which only prevents freed through the specific pointer, and as such requires `noalias` to be useful for function-internal reasoning.

This means that the old `dereferenceable` becomes strictly equivalent to `dereferenceable` + `nofreeobj` under dereferenceable-at-point semantics.

Also rename `!nofree` to `!nofreeobj`, because it's semantics match the `nofreeobj` attribute, not `nofree`.

The motivation here is intrinsincs like `@llvm.amdgcn.kernarg.segment.ptr` which are `dereferenceable` + `nofreeobj`, but not `noalias`.

>From ab3d2fbf134f718adc7357a75a9f8168ded3cea7 Mon Sep 17 00:00:00 2001
From: Nikita Popov <npopov at redhat.com>
Date: Mon, 29 Jun 2026 11:43:09 +0200
Subject: [PATCH 1/4] Introduce nofreeobj

---
 llvm/docs/LangRef.rst                           | 11 +++++++++++
 llvm/include/llvm/Bitcode/LLVMBitCodes.h        |  1 +
 llvm/include/llvm/IR/Attributes.td              |  4 ++++
 llvm/lib/Bitcode/Reader/BitcodeReader.cpp       |  2 ++
 llvm/lib/Bitcode/Writer/BitcodeWriter.cpp       |  2 ++
 llvm/lib/IR/Value.cpp                           |  9 +++++++++
 llvm/lib/Transforms/Utils/CodeExtractor.cpp     |  1 +
 .../ValueTracking/memory-dereferenceable.ll     | 17 +++++++++++++++++
 llvm/test/Bitcode/attributes.ll                 |  5 +++++
 9 files changed, 52 insertions(+)

diff --git a/llvm/docs/LangRef.rst b/llvm/docs/LangRef.rst
index 5640eec987133..1a15783366418 100644
--- a/llvm/docs/LangRef.rst
+++ b/llvm/docs/LangRef.rst
@@ -1574,6 +1574,17 @@ Currently, only the following parameter attributes are defined:
 
     This is not a valid attribute for return values.
 
+``nofreeobj``
+    On arguments, this indicates that the underlying object of the argument
+    cannot be freed during the execution of the function (where "during" is in
+    the same sense as for ``nofree``).
+
+    On return values, this indicates that the underlying object of the return
+    value cannot be freed at all after this point.
+
+    Unlike ``nofree``, it is not possible to free the underlying object through
+    a different pointer either.
+
 .. _nest:
 
 ``nest``
diff --git a/llvm/include/llvm/Bitcode/LLVMBitCodes.h b/llvm/include/llvm/Bitcode/LLVMBitCodes.h
index af4cac996a78c..97c47b09ab9ad 100644
--- a/llvm/include/llvm/Bitcode/LLVMBitCodes.h
+++ b/llvm/include/llvm/Bitcode/LLVMBitCodes.h
@@ -821,6 +821,7 @@ enum AttributeKindCodes {
   ATTR_KIND_NOOUTLINE = 107,
   ATTR_KIND_FLATTEN = 108,
   ATTR_KIND_NOIPA = 109,
+  ATTR_KIND_NOFREEOBJ = 110,
 };
 
 enum ComdatSelectionKindCodes {
diff --git a/llvm/include/llvm/IR/Attributes.td b/llvm/include/llvm/IR/Attributes.td
index 4e45100b54d38..ea523cad69cd5 100644
--- a/llvm/include/llvm/IR/Attributes.td
+++ b/llvm/include/llvm/IR/Attributes.td
@@ -201,8 +201,12 @@ def NoDuplicate : EnumAttr<"noduplicate", IntersectPreserve, [FnAttr]>;
 def NoExt : EnumAttr<"noext", IntersectPreserve, [ParamAttr, RetAttr]>;
 
 /// Function does not deallocate memory.
+/// Argument cannot be freed based on the argument pointer.
 def NoFree : EnumAttr<"nofree", IntersectAnd, [FnAttr, ParamAttr]>;
 
+/// Underlying object of argument/return cannot be freed.
+def NoFreeObj : EnumAttr<"nofreeobj", IntersectAnd, [ParamAttr, RetAttr]>;
+
 /// Argument is dead if the call unwinds.
 def DeadOnUnwind : EnumAttr<"dead_on_unwind", IntersectAnd, [ParamAttr]>;
 
diff --git a/llvm/lib/Bitcode/Reader/BitcodeReader.cpp b/llvm/lib/Bitcode/Reader/BitcodeReader.cpp
index f6366061c9b52..6ab187db5e9ae 100644
--- a/llvm/lib/Bitcode/Reader/BitcodeReader.cpp
+++ b/llvm/lib/Bitcode/Reader/BitcodeReader.cpp
@@ -2152,6 +2152,8 @@ static Attribute::AttrKind getAttrFromCode(uint64_t Code) {
     return Attribute::NoDuplicate;
   case bitc::ATTR_KIND_NOFREE:
     return Attribute::NoFree;
+  case bitc::ATTR_KIND_NOFREEOBJ:
+    return Attribute::NoFreeObj;
   case bitc::ATTR_KIND_NO_IMPLICIT_FLOAT:
     return Attribute::NoImplicitFloat;
   case bitc::ATTR_KIND_NO_INLINE:
diff --git a/llvm/lib/Bitcode/Writer/BitcodeWriter.cpp b/llvm/lib/Bitcode/Writer/BitcodeWriter.cpp
index c4058fe66ff0b..430878dbfa839 100644
--- a/llvm/lib/Bitcode/Writer/BitcodeWriter.cpp
+++ b/llvm/lib/Bitcode/Writer/BitcodeWriter.cpp
@@ -862,6 +862,8 @@ static uint64_t getAttrKindEncoding(Attribute::AttrKind Kind) {
     return bitc::ATTR_KIND_NO_DUPLICATE;
   case Attribute::NoFree:
     return bitc::ATTR_KIND_NOFREE;
+  case Attribute::NoFreeObj:
+    return bitc::ATTR_KIND_NOFREEOBJ;
   case Attribute::NoImplicitFloat:
     return bitc::ATTR_KIND_NO_IMPLICIT_FLOAT;
   case Attribute::NoInline:
diff --git a/llvm/lib/IR/Value.cpp b/llvm/lib/IR/Value.cpp
index d3f90bd81bbe2..ad0aa63dab720 100644
--- a/llvm/lib/IR/Value.cpp
+++ b/llvm/lib/IR/Value.cpp
@@ -861,12 +861,21 @@ bool Value::canBeFreed() const {
     // another pointer to the same allocation. Readonly implies nofree.
     if ((A->hasNoFreeAttr() || A->onlyReadsMemory()) && A->hasNoAliasAttr())
       return false;
+
+    // nofreeobj means that the underlying object cannot be freed, even
+    // through a different pointer.
+    if (A->hasAttribute(Attribute::NoFreeObj))
+      return false;
   }
 
   if (auto *ITP = dyn_cast<IntToPtrInst>(this);
       ITP && ITP->hasMetadata(LLVMContext::MD_nofree))
     return false;
 
+  if (auto *CB = dyn_cast<CallBase>(this))
+    if (CB->hasRetAttr(Attribute::NoFreeObj))
+      return false;
+
   const Function *F = nullptr;
   if (auto *I = dyn_cast<Instruction>(this))
     F = I->getFunction();
diff --git a/llvm/lib/Transforms/Utils/CodeExtractor.cpp b/llvm/lib/Transforms/Utils/CodeExtractor.cpp
index 975294ab00f73..c612135f44fc2 100644
--- a/llvm/lib/Transforms/Utils/CodeExtractor.cpp
+++ b/llvm/lib/Transforms/Utils/CodeExtractor.cpp
@@ -1052,6 +1052,7 @@ Function *CodeExtractor::constructFunctionDeclaration(
       case Attribute::Range:
       case Attribute::Initializes:
       case Attribute::NoExt:
+      case Attribute::NoFreeObj:
       //  These are not really attributes.
       case Attribute::None:
       case Attribute::EndAttrKinds:
diff --git a/llvm/test/Analysis/ValueTracking/memory-dereferenceable.ll b/llvm/test/Analysis/ValueTracking/memory-dereferenceable.ll
index f7e69b0c1f04f..09c8e74a5c675 100644
--- a/llvm/test/Analysis/ValueTracking/memory-dereferenceable.ll
+++ b/llvm/test/Analysis/ValueTracking/memory-dereferenceable.ll
@@ -301,6 +301,23 @@ define void @infer_missing_noalias2(ptr dereferenceable(8) readonly %p) {
   ret void
 }
 
+; CHECK-LABEL: 'nofreeobj_arg'
+; CHECK: %p
+define void @nofreeobj_arg(ptr dereferenceable(8) nofreeobj %p) {
+  call void @mayfree()
+  %v = load i32, ptr %p
+  ret void
+}
+
+; CHECK-LABEL: 'nofreeobj_ret'
+; CHECK: %p
+define void @nofreeobj_ret() {
+  %p = call dereferenceable(8) nofreeobj ptr @foo()
+  call void @mayfree()
+  %v = load i32, ptr %p
+  ret void
+}
+
 ; Just check that we don't crash.
 ; CHECK-LABEL: 'opaque_type_crasher'
 define void @opaque_type_crasher(ptr dereferenceable(16) %a, i1 %arg) {
diff --git a/llvm/test/Bitcode/attributes.ll b/llvm/test/Bitcode/attributes.ll
index f696f2dd12323..3d5272f053251 100644
--- a/llvm/test/Bitcode/attributes.ll
+++ b/llvm/test/Bitcode/attributes.ll
@@ -597,6 +597,11 @@ define void @noipa() noipa {
   ret void
 }
 
+; CHECK: define nofreeobj ptr @nofreeobj(ptr nofreeobj %p)
+define nofreeobj ptr @nofreeobj(ptr nofreeobj %p) {
+  ret ptr %p
+}
+
 ; CHECK: attributes #0 = { noreturn }
 ; CHECK: attributes #1 = { nounwind }
 ; CHECK: attributes #2 = { memory(none) }

>From 435cb5d44d1456320793a67726b9a84da82e7185 Mon Sep 17 00:00:00 2001
From: Nikita Popov <npopov at redhat.com>
Date: Mon, 29 Jun 2026 11:53:25 +0200
Subject: [PATCH 2/4] Verify type for nofree/nofreeobj

---
 llvm/lib/IR/Attributes.cpp   |  4 +++-
 llvm/test/Verifier/nofree.ll | 16 ++++++++++++++++
 2 files changed, 19 insertions(+), 1 deletion(-)
 create mode 100644 llvm/test/Verifier/nofree.ll

diff --git a/llvm/lib/IR/Attributes.cpp b/llvm/lib/IR/Attributes.cpp
index 4087b25951a1c..e69b848c0eb66 100644
--- a/llvm/lib/IR/Attributes.cpp
+++ b/llvm/lib/IR/Attributes.cpp
@@ -2520,7 +2520,9 @@ AttributeMask AttributeFuncs::typeIncompatible(Type *Ty, AttributeSet AS,
           .addAttribute(Attribute::DeadOnUnwind)
           .addAttribute(Attribute::Initializes)
           .addAttribute(Attribute::Captures)
-          .addAttribute(Attribute::DeadOnReturn);
+          .addAttribute(Attribute::DeadOnReturn)
+          .addAttribute(Attribute::NoFree)
+          .addAttribute(Attribute::NoFreeObj);
     if (ASK & ASK_UNSAFE_TO_DROP)
       Incompatible.addAttribute(Attribute::Nest)
           .addAttribute(Attribute::SwiftError)
diff --git a/llvm/test/Verifier/nofree.ll b/llvm/test/Verifier/nofree.ll
new file mode 100644
index 0000000000000..2775613c75426
--- /dev/null
+++ b/llvm/test/Verifier/nofree.ll
@@ -0,0 +1,16 @@
+; RUN: not opt -S < %s 2>&1 | FileCheck %s
+
+; CHECK: Attribute 'nofree' applied to incompatible type!
+define void @test(i32 nofree %p) {
+  ret void
+}
+
+; CHECK: Attribute 'nofreeobj' applied to incompatible type!
+define void @test2(i32 nofreeobj %p) {
+  ret void
+}
+
+; CHECK: Attribute 'nofreeobj' applied to incompatible type!
+define nofreeobj i32 @test3() {
+  ret i32 0
+}

>From 1c39227d70581d739d505b81444e0fd6f9ab2323 Mon Sep 17 00:00:00 2001
From: Nikita Popov <npopov at redhat.com>
Date: Mon, 29 Jun 2026 11:54:08 +0200
Subject: [PATCH 3/4] Rename !nofree to !nofreeobj

---
 llvm/docs/LangRef.rst                             |  9 +++++----
 llvm/include/llvm/IR/FixedMetadataKinds.def       |  2 +-
 llvm/lib/IR/Value.cpp                             |  2 +-
 llvm/lib/IR/Verifier.cpp                          | 15 ++++++++-------
 .../Transforms/LICM/hoist-speculatable-load.ll    |  4 ++--
 llvm/test/Verifier/nofree_metadata.ll             |  8 ++++----
 6 files changed, 21 insertions(+), 19 deletions(-)

diff --git a/llvm/docs/LangRef.rst b/llvm/docs/LangRef.rst
index 1a15783366418..f8b1ced5837d0 100644
--- a/llvm/docs/LangRef.rst
+++ b/llvm/docs/LangRef.rst
@@ -13352,7 +13352,7 @@ Syntax:
 
 ::
 
-      <result> = inttoptr <ty> <value> to <ty2>[, !dereferenceable !<deref_bytes_node>][, !dereferenceable_or_null !<deref_bytes_node>][, !nofree !<empty_node>]            ; yields ty2
+      <result> = inttoptr <ty> <value> to <ty2>[, !dereferenceable !<deref_bytes_node>][, !dereferenceable_or_null !<deref_bytes_node>][, !nofreeobj !<empty_node>]            ; yields ty2
 
 Overview:
 """""""""
@@ -13377,10 +13377,11 @@ metadata name ``<deref_bytes_node>`` corresponding to a metadata node with one
 ``i64`` entry.
 See ``dereferenceable_or_null`` metadata.
 
-The optional ``!nofree`` metadata must reference a single metadata name
+The optional ``!nofreeobj`` metadata must reference a single metadata name
 ``<empty_node>`` corresponding to a metadata node with no entries.
-The existence of the ``!nofree`` metadata on the instruction tells the optimizer
-that the memory pointed by the pointer will not be freed after this point.
+The existence of the ``!nofreeobj`` metadata on the instruction tells the
+optimizer that the underlying object of the pointer will not be freed after
+this point.
 
 Semantics:
 """"""""""
diff --git a/llvm/include/llvm/IR/FixedMetadataKinds.def b/llvm/include/llvm/IR/FixedMetadataKinds.def
index 552c1f60f2d38..ece7d8ae87b56 100644
--- a/llvm/include/llvm/IR/FixedMetadataKinds.def
+++ b/llvm/include/llvm/IR/FixedMetadataKinds.def
@@ -54,7 +54,7 @@ LLVM_FIXED_MD_KIND(MD_coro_outside_frame, "coro.outside.frame", 39)
 LLVM_FIXED_MD_KIND(MD_mmra, "mmra", 40)
 LLVM_FIXED_MD_KIND(MD_noalias_addrspace, "noalias.addrspace", 41)
 LLVM_FIXED_MD_KIND(MD_callee_type, "callee_type", 42)
-LLVM_FIXED_MD_KIND(MD_nofree, "nofree", 43)
+LLVM_FIXED_MD_KIND(MD_nofreeobj, "nofreeobj", 43)
 LLVM_FIXED_MD_KIND(MD_captures, "captures", 44)
 LLVM_FIXED_MD_KIND(MD_alloc_token, "alloc_token", 45)
 LLVM_FIXED_MD_KIND(MD_implicit_ref, "implicit.ref", 46)
diff --git a/llvm/lib/IR/Value.cpp b/llvm/lib/IR/Value.cpp
index ad0aa63dab720..6bbb601a6c08e 100644
--- a/llvm/lib/IR/Value.cpp
+++ b/llvm/lib/IR/Value.cpp
@@ -869,7 +869,7 @@ bool Value::canBeFreed() const {
   }
 
   if (auto *ITP = dyn_cast<IntToPtrInst>(this);
-      ITP && ITP->hasMetadata(LLVMContext::MD_nofree))
+      ITP && ITP->hasMetadata(LLVMContext::MD_nofreeobj))
     return false;
 
   if (auto *CB = dyn_cast<CallBase>(this))
diff --git a/llvm/lib/IR/Verifier.cpp b/llvm/lib/IR/Verifier.cpp
index 648446555793b..12d16565b11f1 100644
--- a/llvm/lib/IR/Verifier.cpp
+++ b/llvm/lib/IR/Verifier.cpp
@@ -356,7 +356,7 @@ class Verifier : public InstVisitor<Verifier>, VerifierSupport {
   void visitNoFPClassMetadata(Instruction &I, MDNode *Range, Type *Ty);
   void visitNoaliasAddrspaceMetadata(Instruction &I, MDNode *Range, Type *Ty);
   void visitDereferenceableMetadata(Instruction &I, MDNode *MD);
-  void visitNofreeMetadata(Instruction &I, MDNode *MD);
+  void visitNoFreeObjMetadata(Instruction &I, MDNode *MD);
   void visitProfMetadata(Instruction &I, MDNode *MD);
   void visitCallStackMetadata(MDNode *MD);
   void visitMemProfMetadata(Instruction &I, MDNode *MD);
@@ -5179,11 +5179,12 @@ void Verifier::visitDereferenceableMetadata(Instruction& I, MDNode* MD) {
         &I);
 }
 
-void Verifier::visitNofreeMetadata(Instruction &I, MDNode *MD) {
-  Check(I.getType()->isPointerTy(), "nofree applies only to pointer types", &I);
-  Check((isa<IntToPtrInst>(I)), "nofree applies only to inttoptr instruction",
+void Verifier::visitNoFreeObjMetadata(Instruction &I, MDNode *MD) {
+  Check(I.getType()->isPointerTy(), "nofreeobj applies only to pointer types",
         &I);
-  Check(MD->getNumOperands() == 0, "nofree metadata must be empty", &I);
+  Check((isa<IntToPtrInst>(I)),
+        "nofreeobj applies only to inttoptr instruction", &I);
+  Check(MD->getNumOperands() == 0, "nofreeobj metadata must be empty", &I);
 }
 
 void Verifier::visitProfMetadata(Instruction &I, MDNode *MD) {
@@ -5787,8 +5788,8 @@ void Verifier::visitInstruction(Instruction &I) {
   if (MDNode *MD = I.getMetadata(LLVMContext::MD_dereferenceable_or_null))
     visitDereferenceableMetadata(I, MD);
 
-  if (MDNode *MD = I.getMetadata(LLVMContext::MD_nofree))
-    visitNofreeMetadata(I, MD);
+  if (MDNode *MD = I.getMetadata(LLVMContext::MD_nofreeobj))
+    visitNoFreeObjMetadata(I, MD);
 
   if (MDNode *TBAA = I.getMetadata(LLVMContext::MD_tbaa))
     TBAAVerifyHelper.visitTBAAMetadata(&I, TBAA);
diff --git a/llvm/test/Transforms/LICM/hoist-speculatable-load.ll b/llvm/test/Transforms/LICM/hoist-speculatable-load.ll
index 31236e8f29d60..ab754a1f9a9d6 100644
--- a/llvm/test/Transforms/LICM/hoist-speculatable-load.ll
+++ b/llvm/test/Transforms/LICM/hoist-speculatable-load.ll
@@ -4,7 +4,7 @@
 define void @f(i32 %ptr_i, ptr %ptr2, i1 %cond) {
 ; CHECK-LABEL: @f(
 ; CHECK-NEXT:  entry:
-; CHECK-NEXT:    [[PTR:%.*]] = inttoptr i32 [[PTR_I:%.*]] to ptr, !nofree [[META0:![0-9]+]]
+; CHECK-NEXT:    [[PTR:%.*]] = inttoptr i32 [[PTR_I:%.*]] to ptr, !nofreeobj [[META0:![0-9]+]]
 ; CHECK-NEXT:    call void @llvm.assume(i1 true) [ "align"(ptr [[PTR]], i32 16), "dereferenceable"(ptr [[PTR]], i32 16) ]
 ; CHECK-NEXT:    br i1 [[COND:%.*]], label [[FOR_BODY_LR_PH:%.*]], label [[IF0:%.*]]
 ; CHECK:       if0:
@@ -27,7 +27,7 @@ define void @f(i32 %ptr_i, ptr %ptr2, i1 %cond) {
 ; CHECK-NEXT:    ret void
 ;
 entry:
-  %ptr = inttoptr i32 %ptr_i to ptr, !nofree !{}
+  %ptr = inttoptr i32 %ptr_i to ptr, !nofreeobj !{}
   call void @llvm.assume(i1 true) [ "align"(ptr %ptr, i32 16), "dereferenceable"(ptr %ptr, i32 16) ]
   br i1 %cond, label %for.body.lr.ph, label %if0
 
diff --git a/llvm/test/Verifier/nofree_metadata.ll b/llvm/test/Verifier/nofree_metadata.ll
index e04f5b9f1c522..40a4d3e28bcd1 100644
--- a/llvm/test/Verifier/nofree_metadata.ll
+++ b/llvm/test/Verifier/nofree_metadata.ll
@@ -2,14 +2,14 @@
 
 declare ptr @dummy()
 
-; CHECK: nofree applies only to inttoptr instruction
+; CHECK: nofreeobj applies only to inttoptr instruction
 define void @test_not_inttoptr() {
-  call ptr @dummy(), !nofree !{}
+  call ptr @dummy(), !nofreeobj !{}
   ret void
 }
 
-; CHECK: nofree metadata must be empty
+; CHECK: nofreeobj metadata must be empty
 define void @test_invalid_arg(i32 %p) {
-  inttoptr i32 %p to ptr, !nofree !{i32 0}
+  inttoptr i32 %p to ptr, !nofreeobj !{i32 0}
   ret void
 }

>From 8f3884613ca0e22a74d8096f71459b97cdea9af5 Mon Sep 17 00:00:00 2001
From: Nikita Popov <npopov at redhat.com>
Date: Mon, 29 Jun 2026 12:03:19 +0200
Subject: [PATCH 4/4] Release note

---
 llvm/docs/ReleaseNotes.md | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/llvm/docs/ReleaseNotes.md b/llvm/docs/ReleaseNotes.md
index d1448eb469614..0544fe5fafd2d 100644
--- a/llvm/docs/ReleaseNotes.md
+++ b/llvm/docs/ReleaseNotes.md
@@ -102,6 +102,11 @@ Makes programs 10x faster by doing Special New Thing.
   outlining. Add the `noinline` and `nooutline` attributes as well in cases
   where inlining and outlining should additionally be disabled.
 
+* Added `nofreeobj` attribute for attributes and returns, which forbids
+  freeing the underlying object (as opposed to only frees through that specific
+  pointer). Renamed `!nofree` metadata to `!nofreeobj`, as it has the same
+  semantics.
+
 ### Changes to LLVM infrastructure
 
 * Removed ``Constant::isZeroValue``. It was functionally identical to



More information about the llvm-commits mailing list