RFC: Updating the semantics of the noescape attribute

TL;DR: I want to make two changes to the noescape attribute:

  • disallow freeing the memory
  • allow annotating parameters of record type with noescape

Disallowing free()

At the moment the noescape attribute explicitly allows freeing the memory. The description reads (emphasis mine):

noescape placed on a function parameter of a pointer type is used to inform the compiler that the pointer cannot escape: that is, no reference to the object the pointer points to that is derived from the parameter value will survive after the function returns. Users are responsible for making sure parameters annotated with noescapedo not actually escape. Calling free() on such a parameter does not constitute an escape.

This is in line with the LLVM attributes emitted in clang codegen, as noescape applies capture(none) , but does not apply nofree . As such the description is an accurate reflection of the current behaviour, but I would argue that this is not a very useful affordance, and that the vast majority of uses of noescape (past, present and future) would benefit more from explicitly not allowing the function to free the memory. I base this on two facts:

  1. Most functions don’t deallocate their parameters, and for the ones that do it’s very rare to use noescape. I haven’t found a single example of this combination.
  2. The combined properties of not escaping and not freeing the pointer means that the function call has no impact on the pointer’s lifetime.

1) means that although this is a source breaking change, it’s unlikely to have significant negative impact. It also means that if we decided instead to create a separate nofree attribute (this doesn’t exist in clang, only in LLVM), most uses of noescape would either require extra noise in the form of an additional attribute, or underspecify the contract of the function.

2) means that we’re providing much more useful information about the semantics than “this does not escape” does alone. It makes it easier for lifetime analyses to make firmer statements about the lifetime of the pointer: not only does it not introduce any uses or aliases after the function has returned, the pointer is also guaranteed to still be alive after the function has returned. This also means that it is okay to pass a pointer to an object on the stack, which is not the case if the callee tries to free the memory. The noescape attribute was in fact originally introduced to allow the compiler to allocate block parameters on the stack rather than the heap, and this use case constitutes almost all uses at Apple at the moment. This heap elision still works for blocks under the current semantics simply because calling free on a block pointer is never allowed, since these are automatically managed by the runtime using ref counts. Extending the semantics to forbid freeing the memory, regardless of pointer type, would be more in line with the original purpose of the attribute and could in theory unlock similar heap elisions for arbitrary allocations.

Clang’s lifetime analysis already has some basic diagnostics to detect misuses of noescape. This can be extended to also (opportunistically) diagnose freed memory for noescape parameters. As the lifetime analysis grows both in scope and in popularity, I expect noescape to gain more traction, making changes like the one outlined above harder to make later without breaking existing code. At the same time, the proposed semantics would provide strictly more information to the lifetime analysis than the existing semantics.

To be extra careful we can stagger the change such that the docs and diagnostics are updated in the next release of clang, but wait another release with updating codegen to pass nofree down to LLVM (which would make freeing the memory UB) to give users time to adjust, if someone does happen to free a noescape pointer. I have not seen anyone do that, neither internally at Apple, nor in open source code, but I think breaking changes warrant being careful anyways.

Allowing noescape on records

To enable even more uses of noescape for communicating lifetime information to the lifetime analysis I would also like to propose expanding the attribute to allow annotating record types (it currently only allows annotating pointer or reference types), to enable annotation of pointer-like types (e.g. std::span ). The semantics here would be that no pointer or reference in the object is allowed to escape. At the moment I’m not aware of any way to lower this information to LLVM for record types, but this is still relevant in and of itself purely for static analysis imho. As is the case with plain pointers, noescape does not cascade onto nested pointer levels — only the outermost pointers/references in the object. Here are a few examples to clarify:

void *g1;
size_t g2;

void foo(std::span<std::span<int>> s [[clang::noescape]]) {
  g1 = s[0].data(); // Ok, nested pointer can escape
  g2 = s.size();    // Ok, non-pointer value can escape

  g1 = s.data();    // Not ok
}

struct MyView {
  std::span<int> s;
};

std::span<int> g3;

void bar(MyView v [[clang::noescape]]) {
  g1 = v.s.data();  // Not ok
  g3 = v.s;         // Not ok, pointer value escapes as part of span
}

This change was initially rejected because of the limited use of noescape within clang, but since it’s been picked up by the lifetime analysis I think there’s more value to users now, by preventing view-types from accidentally escaping (by other means than just through async blocks).

Bonus: memory safe interop

In addition to helping clang’s own lifetime analysis, noescape can also be used for interop with safe languages: a parameter with noescape can be passed from a safe language without violating lifetime safety, but only if that function does not free the memory. If, instead of a raw pointer, the parameter is a std::span , the function can also be made bounds safe (if built with hardened C++ and -Wunsafe-buffer-usage ). We would like to combine these two properties as part of Swift’s safe interop feature to allow spans to be passed from Swift to C++ in a memory safe manner. This way, the borrow checker can accurately track its lifetime and the user would not have to write unsafe when making the function call. I want to make it clear that I think both of these changes are valuable even when considering only C and C++ in a vacuum, but as a bonus it also provides value to the wider ecosystem interoperating with these languages.


:white_check_mark: This RFC was (partially) accepted on July 02 20026

5 Likes

CC @AaronBallman

Thank you for the RFC! It looks like uses of noescape in the wild are pretty rare: context:global -file:.*t… - Sourcegraph which suggests there is a possibility of changing the semantics, but I’d really like to hear from some WebKit folks as the hits we’re seeing are mostly in their code base. My primary concern is: if people are relying on the ability to free already today, changing it so that’s not allowed will silently break their code. So how much code is that and how loud can we make the breakage/how much time can we give people to transition?

There are some more uses with the GNU syntax: context:global -file:.*t… - Sourcegraph
However most hits are macro defines. I have spent some time searching for uses of those defines, but there aren’t a ton and I didn’t find anything out of the ordinary. I wasn’t able to find any code that was clearly freeing, but there could of course be some edge cases I missed. Like I mentioned in the RFC, we can at least add diagnostics to highlight the most egregious violations of the new semantics, where memory is free’d in the direct function body, however at the moment there are no warnings against this:

void foo(int *p); // no promise to not escape
void bar(int *p [[clang::noescape]]) {
  foo(p);
}

We could in theory add an extra strict mode warning against that, but -Wlifetime-safety-noescape is already opt-in, and the strict version would be even more so, in the sense that even fewer projects would likely enable it. Would it make sense to at least enable the current -Wlifetime-safety-noescape by default? That one seems to only contain warnings that I would imagine any user of noescape would want. Another option would be to call out -Wlifetime-safety-noescape in the noescape attribute docs. CC @usx95 since he added this warning.

In terms of time to transition, I’m in no hurry to enable any optimisations based on noescape – at Apple we are mostly concerned with the assumptions static analyses are reasonably allowed to make based on the presence of noescape – so I would propose we give developers plenty of time to adjust before we pass on nofree to LLVM: at least one major release.

WebKit’s use of noescape has been a big factor in our drive to tighten up the semantics, but I’ll ping some people and see if I can get them to present their perspective.

1 Like

Hi Aaron!

In WebKit, our primary use of NOESCAPE today is to aid lifetime analysis. Specifically, if a lambda is NOESCAPE, then it is not required to ensure the lifetimes of its captures by e.g. capturing smart pointers. This is a performance optimization, and also a way to reduce the “WTF’s per minute” metric when programmers make their code conform to our statically enforced lifetime rules. Our current usage of NOESCAPE is neither harmed nor helped by this proposal, since free(lambda) is malformed, and free(lambda), though insane, would not extend the lifetimes of the lambda’s captures.

Independent of lambdas, we’d like to massively expand our use of NOESCAPE, in combination with -Wdangling, -Wexperimental-lifetime-safety, etc. to enforce a full view of the lifetime of a non-refcounted pointer. Summarizing a deep topic: if a pointer only flows into lifetimebound and noescape parameters, then we know the full scope of its usage, inter-procedurally. Now we can be really accurate about pointer lifetimes.

Freeing a pointer arguably doesn’t expand the scope of its usage, but does shrink the scope of its lifetime. Since our goal is lifetime > usage, the two concerns are equivalent algebraically.

So, consistent with the explanation in the RFC, I support prohibiting free() on a NOESCAPE parameter. Else, we will have to mark every parameter NOESCAPE NOFREE. Which is fine, we can use macros. But I don’t think anybody benefits from that.

I will add another argument: free() escapes the pointer. It inserts the pointer into a global heap from which we will read the pointer back out again, in any program scope, via malloc(). QED.

1 Like

We also use [[_Clang::__noescape__]] in libc++, and we do have one place where it’s on an argument that’s freed. I’m not sure it helps with any optimizations there, but it definitely didn’t hurt either so far.

1 Like

Oh interesting! Man, there’s so many ways to spell the same attribute in source… Which function is it that frees the argument? I’m curious to see whether it affects codegen.

1 Like

Thank you!

That’s an implementation detail of some implementations of free(), right? You can write a free() implementation which never stores the pointer argument it was given, and so we would need the API itself to say whether it does or does not escape the pointer.

That said…from Clang’s docs:

no reference to the object the pointer points to that is derived from the parameter value will survive after the function returns.

That doesn’t seem to square with the semantics of captures(none) when thinking about free() because of the IR description:

The call’s behavior depends on any bit of the pointer carrying information (address capture only).

so this does seem like a bug with noescape lowering to captures(none); it seems like you cannot use noescape even on a function like:

[[clang::noescape]] void func(int *ptr) {
  if (ptr == 0) {
    // ...
  }
}

because the comparison to zero means the call’s behavior depends on the bits in ptr carrying information. But I think we want to support this kind of use for the attribute because the pointer value is never stored anywhere else, so it meets the “no reference to the object the pointer points to that is derived from the parameter value will survive after the function returns” requirements for Clang.

But it’s possible I’m misunderstanding the LLVM IR requirements. CC @efriedma-quic @nikic

@AaronBallman Yes, that’s correct. If [[clang::noescape]] does not intend to exclude capture of the integral address value, then the correct LLVM IR attribute to use would be captures(address). This still enables most optimizations (anything related to alias analysis), but excludes anything that may affect object identity.

Historically, LLVM had a nocapture attribute that implied that both the address and the provenance are not captured. Nowadays it’s possible to specify these separately.

That’s a good point, noescape lowering to capture(none) is probably a bit more strict than people would expect, and may end up requiring some language lawyering to use correctly. I agree that captures(address) is probably a better fit than captures(none), but for the sake of the discussion I also want to highlight that Aaron’s example would also be fine with captures(address_is_null), and the nullability bit seems like a bit of information that is significantly easier to accidentally escape than the integral address as a whole. But at the same time, if address_is_null doesn’t provide significantly better optimisation opportunities than plain address I don’t see the point of making it harder to understand than it needs to be.

Edit: would clang explicitly adding captures(address) prevent LLVM from inferring captures(none), or would it still upgrade the the strictness with access to the function body?

Kinda. But free() is a standards-specified function, and the standard (7.24.3.3 The free function) says: “The free function causes the space pointed to by ptr to be deallocated, that is, made available for further allocation” (emphasis mine).

Sounds like an escape to me.

LLVM will upgrade the strictness if possible.

free() destroys the provenance of the pointer, so it is not possible to make further accesses through it after the call to free() – even if a new allocation happens to be placed at the same address. As such, free() does not escape the pointer in the sense that is relevant here.

2 Likes

I guess this is fair. A code path that frees clearly attests that it will not increase the scope of an access. For example, if we imagine a callee that calls a closure argument and then frees it [1], a caller can know that it need not heap allocate the closure’s captures.

I guess this clarifies the argument: While it is possible to give noescape and nofree independent definition, it is not terribly practical. Lifetime safety analysis benefits greatly, and is not harmed, if noescape implies nofree because then there is a single attribute that means “this parameter has no effect on object lifetime”.

[1] I don’t believe this is possible today with either lambdas or blocks, but we can imagine the hypothetical.

1 Like

Yeah, the number of spellings is sometimes a bit annoying. Unfortunately most of them make sense to some extent. It’s __destroy_barrier_algorithm_base.

Isn’t free() a demonstration of the practicality? That’s a case that’s definitely not nofree but is noescape. I imagine it’s not at all uncommon to have functions which accept a pointer in order to free it (so it’s noescape). Or is this just not a situation that we get actual optimization benefits out of?

free() demonstrates the possibility, not the practicality.

Can you give some examples of how you would use this distinction? That will teach us the practicality.

An example along the lines of what I was thinking about is, would it be reasonable to mark this C API as noescape?

I believe it is reasonable as I don’t see any pointers escaping from that function. And it’s not all that rare to have functions (particularly in C) that handle memory management for you without escaping the pointer, so I think plenty of other examples exist. But whether the attribute is important for optimization purposes in these cases is something I don’t feel qualified to speak to.

If we consider free as not escaping its parameter, it raises the question of why not treat other user-defined functions that has the same semantics as free similarly. E.g., imagine a custom allocator that manages its own memory pool — a function like myfree() that appends to the freelist, would then also qualify as @noescape. Special casing only the system free while potentially excluding other similar functions makes it a bit asymmetric.

Separately, from a practical standpoint, if we consider, heap-to-stack promotion as an important application of @noescape, it would require that a function also not free its parameter. Otherwise, if we promoted a value to stack, a “noescape” function that frees would be appending a stack address to the freelist.

More broadly, there are many forms of escape that are arguably benign — caching, runtime instrumentation (like asan), for instance, are forms of escape that is often harmless in practice. Depending on the use case, an analysis or optimization may reasonably permit certain benign escapes. Trying to capture all of these intricacies within a single annotation would be quite challenging, and including some while leaving out others would be inconsistent. Instead, keeping @noescape as the lowest common denominator — a strict property with a well-defined guarantee — would make it broadly applicable and reliable across the widest range of use cases.

There are. Back in the day, I tried to have nocapture stores for that reason, e.g., a store of a pointer that is known to be harmless wrt. capturing because all it does is pass the pointer along via memory rather than as a value. This should not be too different from the “noescape record types”, but on IR.

We even had 2 proposals to implement this, as it turns out, you cannot just say a store is harmless.

In case people have too much time on their hands:

The above is reference [0] in the RFC below. [1] and [2] are old patches we could even find if need be.

Because those other functions aren’t defined by the standard and so we don’t know what other semantics they have which might make noescape invalid on them while still valid on free(). If a user writes a custom function which behaves the same as free() they’re welcome to mark the function themselves, but free() is one we can have special knowledge about because it’s part of the implementation. (FWIW, I think free_sized() and free_aligned_sized() from later C standards should be included for the same reason if we continue to keep the semantics of noescape what they are today.)

To be clear, I’m not trying to argue we can’t change the definition here. But because this attribute has existed in the wild for so long with the semantics it has, changing those semantics should be done with caution. But it sounds to me like we do want some changes here: either noescape should continue to be allowed on free() at which point Clang should switch to lowering to captures(address), or we’re changing the semantics to not be allowed on free() so Clang adds nofree when lowering, right?