[LangRef] Do not allow free via synchronization in nofree#195658
Conversation
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.
|
@llvm/pr-subscribers-llvm-ir Author: Nikita Popov (nikic) ChangesThe 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:
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. 1 Files Affected:
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
|
There was a problem hiding this comment.
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).
Yeah, defining it in terms of provenance makes a lot of sense.
For the monotonic operations, I guess the concern is around something like this? 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). |
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". |
Sorry, what I wrote there was very unclear. I meant "excludes" as in "does not cover" not as in "prohibits".
Okay, then we should be good. That matches our existing modelling. |
|
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. |
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?). |
| 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 |
There was a problem hiding this comment.
Minor nit:
| happens-before the function exit but does not happens-before the function | |
| happens-before the function exit but does not happen-before the function |
There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
Oh, agree the hyphenated form shouldn't change.
Yes, this is correct. The above code is well-defined.
For optimization inside the callee, effectively only the combination of Some years ago we also discussed the possibility of introducing another attribute like I believe the |
antoniofrighetto
left a comment
There was a problem hiding this comment.
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.
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. |
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.
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.
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.
After #195658 we only need the function to be nofree, as this also precludes freeing via synchronization now. Being nosync is no longer required.
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.
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.
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...)
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...)
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...)
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
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
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
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
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:
capturesetc). 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.