Skip to content

[LangRef] Do not allow free via synchronization in nofree#195658

Merged
nikic merged 4 commits into
llvm:mainfrom
nikic:nofree-nosync
May 28, 2026
Merged

[LangRef] Do not allow free via synchronization in nofree#195658
nikic merged 4 commits into
llvm:mainfrom
nikic:nofree-nosync

Conversation

@nikic

@nikic nikic commented May 4, 2026

Copy link
Copy Markdown
Contributor

The nofree attribute is currently specified to only forbid direct free calls inside the function. A nofree function is still allowed to compel a pointer to be freed by a different thread through synchronization.

This is currently only spelled out for the function-level nofree attribute, but I assume the same semantics also hold for argument nofree (and this matches how the Attributor implementation infers it).

The original motivation for this definition was to keep the attributes orthogonal and independently inferable. However, the problem is that nosync is a too strong condition: It excludes any synchronization, not just synchronization that results in the free of a pointer.

Some frontends like Rust can guarantee that most pointer arguments cannot be freed for the duration of a function call, including via synchronization. However, they cannot guarantee that no synchronization takes place at all. The current definition of nofree makes this inexpressible in LLVM IR, which blocks further progress on dereferenceable-at-point semantics.

This PR makes two changes to the nofree spec:

  • Function-level nofree now disallows free via synchronization, specified in terms of happens-before.
  • Argument-level nofree is now specified in terms of provenance: The argument has derived provenance that makes it UB to free during the execution of the function (which includes freeing on another thread with appropriate synchronization). This also clarified that argument-level nofree only applies to the particular pointer (similar to captures etc). The underlying object may still be freed via a different argument to which the same pointer is passed. (I'm open to changing this, but that would essentially limit inference to noalias arguments.)

Note: This PR doesn't include the necessary inference changes to comply with the new semantics yet. I'd like to make sure we're aligned on the direction first.

The nofree attribute is currently specified to only forbid direct
free calls inside the function. A nofree function is still allowed
to compel a pointer to be freed by a different thread through
synchronization.

This is currently only spelled out for the function-level nofree
attribute, but I assume the same semantics also hold for argument
nofree (and this matches how the Attributor implementation infers
it).

The original motivation for this definition was to keep the
attributes orthogonal and independently inferable. However, the
problem is that nosync is a too strong condition: It excludes *any*
synchronization, not just synchronization that results in the
free of a pointer.

Some frontends like Rust can guarantee that most pointer arguments
cannot be freed for the duration of a function call, including via
synchronization. However, they cannot guarantee that no
synchronization takes place at all. The current definition of
nofree makes this inexpressible.

This PR makes a few changes to the nofree spec:

 * Function-level nofree now excludes free via synchronization.
 * Argument-level nofree now also excludes free via synchronization
   (it previously made not statement in either direction).
 * Argument-level nofree is now specified to only apply to the
   particular pointer (similar to `captures` etc). The underlying
   object may still be freed via a different argument to which the
   same pointer is passed. (I'm open to changing this, but that
   would essentially limit inference to noalias arguments.)

Note: This PR doesn't include the necessary inference changes to
comply with the new semantics yet. I'd like to make sure we're
aligned on the direction first.
@llvmorg-github-actions

Copy link
Copy Markdown

@llvm/pr-subscribers-llvm-ir

Author: Nikita Popov (nikic)

Changes

The nofree attribute is currently specified to only forbid direct free calls inside the function. A nofree function is still allowed to compel a pointer to be freed by a different thread through synchronization.

This is currently only spelled out for the function-level nofree attribute, but I assume the same semantics also hold for argument nofree (and this matches how the Attributor implementation infers it).

The original motivation for this definition was to keep the attributes orthogonal and independently inferable. However, the problem is that nosync is a too strong condition: It excludes any synchronization, not just synchronization that results in the free of a pointer.

Some frontends like Rust can guarantee that most pointer arguments cannot be freed for the duration of a function call, including via synchronization. However, they cannot guarantee that no synchronization takes place at all. The current definition of nofree makes this inexpressible in LLVM IR, which blocks further progress on dereferenceable-at-point semantics.

This PR makes a few changes to the nofree spec:

  • Function-level nofree now disallows free via synchronization.
  • Argument-level nofree now also disallows free via synchronization (it previously made not statement in either direction).
  • Argument-level nofree is now specified to only apply to the particular pointer (similar to captures etc). The underlying object may still be freed via a different argument to which the same pointer is passed. (I'm open to changing this, but that would essentially limit inference to noalias arguments.)

Note: This PR doesn't include the necessary inference changes to comply with the new semantics yet. I'd like to make sure we're aligned on the direction first.


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

1 Files Affected:

  • (modified) llvm/docs/LangRef.rst (+13-13)
diff --git a/llvm/docs/LangRef.rst b/llvm/docs/LangRef.rst
index 912cca72e1ad5..57e5743dde39e 100644
--- a/llvm/docs/LangRef.rst
+++ b/llvm/docs/LangRef.rst
@@ -1543,8 +1543,11 @@ Currently, only the following parameter attributes are defined:
       is null is captured in some other way.
 
 ``nofree``
-    This indicates that the callee does not free the pointer argument. This is not
-    a valid attribute for return values.
+    This indicates that the callee does not free the pointer argument, or
+    cause it to be freed through synchronization.
+
+    This is not a valid attribute for return values. This attribute applies
+    only to the particular copy of the pointer passed in this argument.
 
 .. _nest:
 
@@ -2328,20 +2331,17 @@ For example:
 ``nofree``
     This function attribute indicates that the function does not, directly or
     transitively, call a memory-deallocation function (``free``, for example)
-    on a memory allocation which existed before the call.
+    on a memory allocation which existed before the call, or make such a call
+    visible through synchronization.
 
-    As a result, uncaptured pointers that are known to be dereferenceable
-    prior to a call to a function with the ``nofree`` attribute are still
-    known to be dereferenceable after the call. The capturing condition is
-    necessary in environments where the function might communicate the
-    pointer to another thread which then deallocates the memory.  Alternatively,
-    ``nosync`` would ensure such communication cannot happen and even captured
-    pointers cannot be freed by the function.
+    As a result, pointers that are known to be dereferenceable prior to a call
+    to a function with the ``nofree`` attribute are still known to be
+    dereferenceable after the call.
 
     A ``nofree`` function is explicitly allowed to free memory which it
-    allocated or (if not ``nosync``) arrange for another thread to free
-    memory on its behalf.  As a result, perhaps surprisingly, a ``nofree``
-    function can return a pointer to a previously deallocated
+    allocated or arrange for another thread to free such memory on its behalf.
+    As a result, perhaps surprisingly, a ``nofree`` function can return a
+    pointer to a previously deallocated
     :ref:`allocated object<allocatedobjects>`.
 ``noimplicitfloat``
     Disallows implicit floating-point code. This inhibits optimizations that

@efriedma-quic efriedma-quic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'd recommend defining nofree for pointer arguments in terms of provenance: if you put nofree on an argument, the provenance of that pointer is not used to free the pointer. Seems pretty easy to reason about: if nothing else has the provenance, the pointer won't be freed; if something else has the provenance, all bets are off. Pairs well with captures(none) or captures(address).

I think you need to take a non-provenance approach for whole-function nofree, though. Your current definition might be okay? Hard to infer, though; it excludes any form of atomic operation (including "monotonic" operations).

@nikic

nikic commented May 4, 2026

Copy link
Copy Markdown
Contributor Author

I'd recommend defining nofree for pointer arguments in terms of provenance: if you put nofree on an argument, the provenance of that pointer is not used to free the pointer. Seems pretty easy to reason about: if nothing else has the provenance, the pointer won't be freed; if something else has the provenance, all bets are off. Pairs well with captures(none) or captures(address).

Yeah, defining it in terms of provenance makes a lot of sense.

I think you need to take a non-provenance approach for whole-function nofree, though. Your current definition might be okay? Hard to infer, though; it excludes any form of atomic operation (including "monotonic" operations).

For the monotonic operations, I guess the concern is around something like this?

T1:
shared_loc.store_monotonic(p)
while (shared_loc.load_monotonic()) { /* spin */ }

T2:
loop {
   p = shared_loc.load_monotonic()
   if (p) {
     free(p)
     shared_loc.store_monotonic(nullptr)
   }
}

And then T1 cannot be nofree?

We currently use the combination of nofree + nosync to determine that it's "really nofree" and nosync excludes monotonic (though in terms of implementation, we treat this inconsistently between Attributor and FunctionAttrs).

@efriedma-quic

Copy link
Copy Markdown
Contributor

nosync excludes monotonic

You just rewrote the definition of nosync so that it explicitly references the C++ "synchronize with"... and monotonic operations can't create such an edge on their own, I think. (You can pair a monotonic operation with a fence, but the edge is defined in terms of the fences.)

Thinking about it a bit more, I guess it's actually fine for a nofree function to contain monotonic stores? Monotonic stores do weird things to causality; a wait loop doesn't actually form an edge like you might expect.

Probably want to try to use the C++ vocabulary in the definition, though. "make such a call visible through synchronization" doesn't map directly to anything. Maybe what we want is something like "any call to free which happens-before the function returns also happens-before the function entry".

@nikic

nikic commented May 5, 2026

Copy link
Copy Markdown
Contributor Author

nosync excludes monotonic

You just rewrote the definition of nosync so that it explicitly references the C++ "synchronize with"... and monotonic operations can't create such an edge on their own, I think.

Sorry, what I wrote there was very unclear. I meant "excludes" as in "does not cover" not as in "prohibits".

Thinking about it a bit more, I guess it's actually fine for a nofree function to contain monotonic stores? Monotonic stores do weird things to causality; a wait loop doesn't actually form an edge like you might expect.

Okay, then we should be good. That matches our existing modelling.

@nikic

nikic commented May 5, 2026

Copy link
Copy Markdown
Contributor Author

I've tried to update the wording based on feedback.

I guess the argument nofree is still somewhat ambiguous in how "during the execution of the function" interacts with synchronization. This is probably something we should be defining more generally somewhere, because we have lots of attributes with function-local effects.

@efriedma-quic efriedma-quic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

@antoniofrighetto

antoniofrighetto commented May 6, 2026

Copy link
Copy Markdown
Contributor

Argument-level nofree is now specified to only apply to the particular pointer (similar to captures etc). The underlying object may still be freed via a different argument to which the same pointer is passed. (I'm open to changing this, but that would essentially limit inference to noalias arguments.)

So, given:

define void @callee(ptr nofree %a, ptr %b) {
  call void @free(ptr %b)
  ret void
}

We are saying that nofree only constrains frees performed through a pointer derived from %a, regardless of whether %a and %b happen to alias, correct? I wonder if the alternative would be more correct (free(%b) UB if they alias), though I guess this would be not particularly useful. Would this be primarily helpful to optimizations on the caller side (i.e., we would like to conclude that nothing derived from %a is free'd?).

Comment thread llvm/docs/LangRef.rst Outdated
allocation which existed before the call, either through direct calls to
a memory-deallocation function like ``free``, or through synchronization.
Freeing through synchronization here means that a deallocation
happens-before the function exit but does not happens-before the function

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Minor nit:

Suggested change
happens-before the function exit but does not happens-before the function
happens-before the function exit but does not happen-before the function

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

happens-before is a term of art from the memory model. I think it's conventional to always spell it as happens-before even when non-grammatical. (I've italicized it to maybe make that clearer.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Oh, agree the hyphenated form shouldn't change.

@nikic

nikic commented May 6, 2026

Copy link
Copy Markdown
Contributor Author

So, given:

define void @callee(ptr nofree %a, ptr %b) {
  call void @free(ptr %b)
  ret void
}

We are saying that nofree only constrains frees performed through a pointer derived from %a, regardless of whether %a and %b happen to alias, correct?

Yes, this is correct. The above code is well-defined.

I wonder if the alternative would be more correct (free(%b) UB if they alias), though I guess this would be not particularly useful. Would this be primarily helpful to optimizations on the caller side (i.e., we would like to conclude that nothing derived from %a is free'd?).

For optimization inside the callee, effectively only the combination of noalias nofree is useful. For optimization inside the caller (and inference), nofree can be useful in isolation.

Some years ago we also discussed the possibility of introducing another attribute like nofreeobj which is stronger than nofree and does not allow freeing the underlying object even through aliasing pointers. This means it's usable in isolation inside the function, but on the other hand it can't really be inferred for non-noalias pointers.

I believe the noalias nofree combination covers pretty much exactly the use cases needed in Rust -- in particular, the two cases that are not noalias (& !Freeze and &mut !Unpin) are also the ones that are not nofree. So we wouldn't have a use case for something like nofreeobj at this time.

@antoniofrighetto antoniofrighetto left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM, thanks for the background.

I guess the argument nofree is still somewhat ambiguous in how "during the execution of the function" interacts with synchronization.

Which makes me realize why, among other things, the old spec was not addressing / engaging with synchronization at all. Just wondering, could it make sense to mirror the function-level happens-before formulation for the argument attribute as well, e.g., a free through a pointer based on this argument on another thread violates argument nofree only if it happens-before the function exit and does not happens-before the function entry, or might it be too contrived? I think the temporal / informal form (until function returns) is still fine though.

@nikic

nikic commented May 6, 2026

Copy link
Copy Markdown
Contributor Author

Which makes me realize why, among other things, the old spec was not addressing / engaging with synchronization at all. Just wondering, could it make sense to mirror the function-level happens-before formulation for the argument attribute as well, e.g., a free through a pointer based on this argument on another thread violates argument nofree only if it happens-before the function exit and does not happens-before the function entry, or might it be too contrived? I think the temporal / informal form (until function returns) is still fine though.

Done, though I omitted the part about happens-before the function entry, as the pointer/provenance is only created on function entry.

TBH I'm not entirely happy with this definition of "well-defined if not (free happens-before return)". I think for the provenance-based argument attribute, a more natural definition would have been "well-defined if return happens-before free", because this allows us to determine whether UB occurs at the point of the free. But this definition would be incompatible with the function-level nofree, which is also undesirable, because it prevents cross-inference.

@nikic nikic merged commit 191aa29 into llvm:main May 28, 2026
11 checks passed
@nikic nikic deleted the nofree-nosync branch May 28, 2026 06:56
nikic added a commit that referenced this pull request May 28, 2026
This updates nofree inference after the semantics changes in
#195658.

FunctionAttrs currently only supports function-level nofree inference,
so we just need to check for potentially synchronizing instructions.

Attributor also supports argument nofree, in which case we make
additional changes:
* For callsite arguments, in addition to checking nofree, we also need
to check nocapture. If the call itself doesn't free the arg, it may
still capture it and then we may free it via the captured pointer later.
The way this was handled was already incorrect prior to the semantics
changes.
* Reformulate the handling for other instructions in terms of looking
for provenance captures, as that's what the code was essentially doing,
in a crude way. I'm including this to avoid some regressions for cases
where we can no longer infer function nofree, but can still infer
argument nofree.
llvm-sync Bot pushed a commit to arm/arm-toolchain that referenced this pull request May 28, 2026
This updates nofree inference after the semantics changes in
llvm/llvm-project#195658.

FunctionAttrs currently only supports function-level nofree inference,
so we just need to check for potentially synchronizing instructions.

Attributor also supports argument nofree, in which case we make
additional changes:
* For callsite arguments, in addition to checking nofree, we also need
to check nocapture. If the call itself doesn't free the arg, it may
still capture it and then we may free it via the captured pointer later.
The way this was handled was already incorrect prior to the semantics
changes.
* Reformulate the handling for other instructions in terms of looking
for provenance captures, as that's what the code was essentially doing,
in a crude way. I'm including this to avoid some regressions for cases
where we can no longer infer function nofree, but can still infer
argument nofree.
llvm-upstreamsync Bot pushed a commit to qualcomm/cpullvm-toolchain that referenced this pull request May 28, 2026
This updates nofree inference after the semantics changes in
llvm/llvm-project#195658.

FunctionAttrs currently only supports function-level nofree inference,
so we just need to check for potentially synchronizing instructions.

Attributor also supports argument nofree, in which case we make
additional changes:
* For callsite arguments, in addition to checking nofree, we also need
to check nocapture. If the call itself doesn't free the arg, it may
still capture it and then we may free it via the captured pointer later.
The way this was handled was already incorrect prior to the semantics
changes.
* Reformulate the handling for other instructions in terms of looking
for provenance captures, as that's what the code was essentially doing,
in a crude way. I'm including this to avoid some regressions for cases
where we can no longer infer function nofree, but can still infer
argument nofree.
nikic added a commit that referenced this pull request May 29, 2026
After #195658 we only need the
function to be nofree, as this also precludes freeing via
synchronization now. Being nosync is no longer required.
llvm-sync Bot pushed a commit to arm/arm-toolchain that referenced this pull request May 29, 2026
After llvm/llvm-project#195658 we only need the
function to be nofree, as this also precludes freeing via
synchronization now. Being nosync is no longer required.
llvm-upstreamsync Bot pushed a commit to qualcomm/cpullvm-toolchain that referenced this pull request May 29, 2026
After llvm/llvm-project#195658 we only need the
function to be nofree, as this also precludes freeing via
synchronization now. Being nosync is no longer required.
nikic added a commit that referenced this pull request Jun 1, 2026
Based on the argument nofree semantics specified in
#195658, we can conclude that
an argument with both nofree and noalias cannot be freed.

This also handles the case of readonly + noalias, to be consistent with
the logic for functions (and because we had a FIXME for it...)
llvm-sync Bot pushed a commit to arm/arm-toolchain that referenced this pull request Jun 1, 2026
Based on the argument nofree semantics specified in
llvm/llvm-project#195658, we can conclude that
an argument with both nofree and noalias cannot be freed.

This also handles the case of readonly + noalias, to be consistent with
the logic for functions (and because we had a FIXME for it...)
llvm-upstreamsync Bot pushed a commit to qualcomm/cpullvm-toolchain that referenced this pull request Jun 1, 2026
Based on the argument nofree semantics specified in
llvm/llvm-project#195658, we can conclude that
an argument with both nofree and noalias cannot be freed.

This also handles the case of readonly + noalias, to be consistent with
the logic for functions (and because we had a FIXME for it...)
JonathanBrouwer added a commit to JonathanBrouwer/rust that referenced this pull request Jun 4, 2026
Emit nofree attribute

Treat the semantics of pointee.size as "dereferenceable-at-point" and always specify the size. Instead, use a separate NoFree attribute to determine whether dereferenceability extends to the whole function.

Then in the LLVM backend, only actually emit dereferenceable if nofree is also set, as dereferenceable currently implies nofree.

In addition, explicitly emit the nofree attribute, which will help when LLVM switches dereferenceable to be at-point.

Relevant recent LLVM PR: llvm/llvm-project#195658
rust-timer added a commit to rust-lang/rust that referenced this pull request Jun 4, 2026
Rollup merge of #156281 - nikic:nofree, r=RalfJung

Emit nofree attribute

Treat the semantics of pointee.size as "dereferenceable-at-point" and always specify the size. Instead, use a separate NoFree attribute to determine whether dereferenceability extends to the whole function.

Then in the LLVM backend, only actually emit dereferenceable if nofree is also set, as dereferenceable currently implies nofree.

In addition, explicitly emit the nofree attribute, which will help when LLVM switches dereferenceable to be at-point.

Relevant recent LLVM PR: llvm/llvm-project#195658
github-actions Bot pushed a commit to rust-lang/rustc-dev-guide that referenced this pull request Jun 20, 2026
Emit nofree attribute

Treat the semantics of pointee.size as "dereferenceable-at-point" and always specify the size. Instead, use a separate NoFree attribute to determine whether dereferenceability extends to the whole function.

Then in the LLVM backend, only actually emit dereferenceable if nofree is also set, as dereferenceable currently implies nofree.

In addition, explicitly emit the nofree attribute, which will help when LLVM switches dereferenceable to be at-point.

Relevant recent LLVM PR: llvm/llvm-project#195658
Kobzol pushed a commit to Kobzol/rust that referenced this pull request Jun 21, 2026
Emit nofree attribute

Treat the semantics of pointee.size as "dereferenceable-at-point" and always specify the size. Instead, use a separate NoFree attribute to determine whether dereferenceability extends to the whole function.

Then in the LLVM backend, only actually emit dereferenceable if nofree is also set, as dereferenceable currently implies nofree.

In addition, explicitly emit the nofree attribute, which will help when LLVM switches dereferenceable to be at-point.

Relevant recent LLVM PR: llvm/llvm-project#195658
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants