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

Nikita Popov via llvm-commits llvm-commits at lists.llvm.org
Thu Jul 16 06:01:26 PDT 2026


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

>From 7f4711b473f43361e7fde96cd685fba15e4d30f8 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.md                          | 22 ++++++++++++++++++-
 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, 62 insertions(+), 1 deletion(-)

diff --git a/llvm/docs/LangRef.md b/llvm/docs/LangRef.md
index f555ca543ffdc..8c563cbd079c0 100644
--- a/llvm/docs/LangRef.md
+++ b/llvm/docs/LangRef.md
@@ -1587,7 +1587,27 @@ Currently, only the following parameter attributes are defined:
 
     This is not a valid attribute for return values.
 
-(nest)=
+`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 from this point forward, for the duration of the
+    program's execution.
+
+    Unlike `nofree`, it is not possible to free the underlying object through
+    a different pointer either.
+
+    `nofreeobj` on arguments implies `nofree`. `nofreeobj` is primarily useful
+    in cases where it's not possible to mark the argument as `noalias`.
+    The combination of `nofree` and `noalias` does *not* imply `nofreeobj`
+    by itself, because it still allows freeing the underlying object through
+    a different pointer, as long as the object is never accessed through the
+    `noalias` pointer. However, it does imply `nofreeobj` if such an access
+    exists, for example in the form of the implied read of a `dereferenceable`
+    attribute.
+
 
 `nest`
 :   This indicates that the pointer parameter can be excised using the
diff --git a/llvm/include/llvm/Bitcode/LLVMBitCodes.h b/llvm/include/llvm/Bitcode/LLVMBitCodes.h
index 358f9a65a80af..226be19624db0 100644
--- a/llvm/include/llvm/Bitcode/LLVMBitCodes.h
+++ b/llvm/include/llvm/Bitcode/LLVMBitCodes.h
@@ -826,6 +826,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 c57323ed41525..aaccc9de11d49 100644
--- a/llvm/lib/Bitcode/Reader/BitcodeReader.cpp
+++ b/llvm/lib/Bitcode/Reader/BitcodeReader.cpp
@@ -2176,6 +2176,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 f4ce522c23b8e..9378846900570 100644
--- a/llvm/lib/Bitcode/Writer/BitcodeWriter.cpp
+++ b/llvm/lib/Bitcode/Writer/BitcodeWriter.cpp
@@ -866,6 +866,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 6d0170dd23e75..368d6241a48ca 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 4c33848dacf51..745f47aa561ee 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 aae6e38fc73bf74a23578ed9d580e76ea4a96df9 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 834d54468d47a3eeb56812bb67ba72fa51e891f9 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.md                              |  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.md b/llvm/docs/LangRef.md
index 8c563cbd079c0..3c717938fc14e 100644
--- a/llvm/docs/LangRef.md
+++ b/llvm/docs/LangRef.md
@@ -12928,7 +12928,7 @@ address width of 32 bits (`p1:64:64:64:32` {ref}`datalayout string<langref_datal
 ##### 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:
@@ -12952,10 +12952,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 memory pointed by 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 d10d16075fecb..65c488f50064c 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 368d6241a48ca..50882ec0d7b10 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 fa0fe7a2e6092..09949bc8cff42 100644
--- a/llvm/lib/IR/Verifier.cpp
+++ b/llvm/lib/IR/Verifier.cpp
@@ -357,7 +357,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);
@@ -5229,11 +5229,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) {
@@ -5842,8 +5843,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 3505f2f7bbadac2a674bc91bab212cfc7ab55033 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 af037473e3653..6c53cf2cc7e2d 100644
--- a/llvm/docs/ReleaseNotes.md
+++ b/llvm/docs/ReleaseNotes.md
@@ -54,6 +54,11 @@ Makes programs 10x faster by doing Special New Thing.
 
 ### Changes to the LLVM IR
 
+* 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
 
 ### Changes to building LLVM



More information about the llvm-commits mailing list