From fe869e57b0d1020f0d2c6a01d814e8222168f7fc Mon Sep 17 00:00:00 2001 From: Nikita Popov Date: Mon, 4 May 2026 15:54:09 +0200 Subject: [PATCH 1/4] [LangRef] Do not allow free via synchronization in nofree 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/docs/LangRef.rst | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) 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`. ``noimplicitfloat`` Disallows implicit floating-point code. This inhibits optimizations that From c11fc0a911fb8b3a929d4479f5584867cecccc6b Mon Sep 17 00:00:00 2001 From: Nikita Popov Date: Tue, 5 May 2026 11:57:22 +0200 Subject: [PATCH 2/4] Provenance based definition, happens-before --- llvm/docs/LangRef.rst | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/llvm/docs/LangRef.rst b/llvm/docs/LangRef.rst index 57e5743dde39e..028ad92293f10 100644 --- a/llvm/docs/LangRef.rst +++ b/llvm/docs/LangRef.rst @@ -1543,11 +1543,20 @@ 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, or - cause it to be freed through synchronization. + This indicates that a pointer based on this argument cannot be freed during + the execution of the function. - This is not a valid attribute for return values. This attribute applies - only to the particular copy of the pointer passed in this argument. + More formally, a ``nofree`` argument provides the callee with a new pointer + with the same address and a derived provenance, where the derived + provenance has the same permissions as the original, except that the + underlying object cannot be freed until the function returns (or unwinds), + otherwise the behavior is undefined. This includes frees in nested function + calls and on other threads. + + Notably, it is still possible to free the underlying object through a + pointer that is not based on the argument. + + This is not a valid attribute for return values. .. _nest: @@ -2329,10 +2338,12 @@ For example: internal linkage and only has one call site, so the original call is dead after inlining. ``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, or make such a call - visible through synchronization. + This function attribute indicates that the function does not free any memory + 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 + entry. 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 From 8ac7ca7f107b55fe2a6cc6f50269bcdcebcf5375 Mon Sep 17 00:00:00 2001 From: Nikita Popov Date: Wed, 6 May 2026 10:44:33 +0200 Subject: [PATCH 3/4] Italicize happens-before --- llvm/docs/LangRef.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/llvm/docs/LangRef.rst b/llvm/docs/LangRef.rst index 028ad92293f10..92fb1e115d0d5 100644 --- a/llvm/docs/LangRef.rst +++ b/llvm/docs/LangRef.rst @@ -2342,8 +2342,8 @@ For example: 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 - entry. + *happens-before* the function exit but does not *happens-before* the + function entry. 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 From f98f0dd25a8473403abdd044b7664187a90d0e97 Mon Sep 17 00:00:00 2001 From: Nikita Popov Date: Wed, 6 May 2026 17:32:53 +0200 Subject: [PATCH 4/4] Try to clarify synchronization interaction --- llvm/docs/LangRef.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/llvm/docs/LangRef.rst b/llvm/docs/LangRef.rst index 92fb1e115d0d5..e358bcc621064 100644 --- a/llvm/docs/LangRef.rst +++ b/llvm/docs/LangRef.rst @@ -1550,8 +1550,8 @@ Currently, only the following parameter attributes are defined: with the same address and a derived provenance, where the derived provenance has the same permissions as the original, except that the underlying object cannot be freed until the function returns (or unwinds), - otherwise the behavior is undefined. This includes frees in nested function - calls and on other threads. + otherwise the behavior is undefined. This includes frees of the pointer on + other threads if the free *happens-before* the function return. Notably, it is still possible to free the underlying object through a pointer that is not based on the argument.