C++ Logo

std-proposals

Advanced search

Re: [std-proposals] Delete...why not a parameter by reference?!

From: organicoman <organicoman_at_[hidden]>
Date: Tue, 02 Sep 2025 01:11:17 +0100
Hello Oliver, There is a lot of email traffic in the community and that's good to see.I just saw your reply, buried inside.Thanks again for your time and efforts, they are much appreciated. It looks like we are both trying so hard to repeat the same things we are saying over and over. I would like to reboot this thread, but this time we will use a Q&A approach, likewise, I can lay down my thought process, and we can pin point any wrong assumptions. If the error is correct-able, then I will adjust and carry on, otherwise the proposal is void since one of it's pilar is weak.Shall we?Sent from my Galaxy
-------- Original message --------From: Oliver Hunt <oliver_at_[hidden]> Date: 8/31/25 2:27 AM (GMT+01:00) To: std-proposals_at_[hidden] Cc: Simon Schröder <dr.simon.schroeder_at_[hidden]>, organicoman <organicoman_at_[hidden]> Subject: Re: [std-proposals] Delete...why not a parameter by reference?! On Aug 30, 2025, at 3:08 PM, organicoman via Std-Proposals <std-proposals_at_[hidden]> wrote:> On Aug 27, 2025, at 9:41 PM, organicoman via Std-Proposals <std-proposals_at_[hidden]> wrote:> > In the example above, we have a double free bug despite the correct code.> It is not correct code. With or without the ‘throw’ you’ll have a double free. The destructor of A always gets called at some point. You are violating basic class invariance principles of RAII. And again: a unique_ptr would have solved this problem without any overhead.Nope, std::unique_ptr won't solve this problem.No: any case where you have pulled the pointer out of a unique_ptr represents a control flow path leads to raw pointer usage, in which case your proposal is also not going to do anything.More over as I have stated repeatedly, you cannot just blindly carry a reference around. A reference is literally just a pointer, requiring passing a reference around, means requiring that the storage for that reference is live, so now you just have another pointer that can result in a use after free. Following from that, any case where you do end up - out of necessity - with a non-glvalue of the pointer, you’ve made all your functions require a reference, so you now have to store your pointer somewhere, and pass a reference to that copy.At that point you now have all of the downsides and problems of this proposal *and* you are not getting any benefit because that reference is not a reference to the storage you plan on passing to delete. On top of that, people may believe that because they’re passing these values by reference they’re protected by this feature, despite that not being that case now, as the reference that they have is not to a pointer that will be cleared.You must know that the delete expression can throw if the destructor of the object throws, thus, you will be UB.Yes, and that is true for your proposal as well.Plus, before RAII we should observe the "easy to use hard to misuse " principle for designing API's....delete as many other memory related functions like , std::free, std::deallocate, or kfree (kernell free)..are easy to misuse, unsafe to use(otherwise we won't have the famous memory safety issues)....if inside the call the function throws there's no way to reason about the resource (freed no freed?).You are literally saying: RAII - a technique that manages lifetime automatically - goes wrong, if you try to simultaneously manage that memory manually. Yes, if you write code that is that wrong, you will have real bugs, but then you are turning around and saying your proposal works, because a developer who manually deletes a pointer managed by an automatic memory management tool, can be assumed to do the correct thing with your proposal, which requires extensive, subtle, and trivially failing code to be written instead.In other words: a developer who writes code that is extremely and blatantly incorrect, when using a mechanism that automatically manages lifetime for them, is assumed to correctly do something that is significantly harder to do correctly, and significantly easier to do incorrectly.That's why , taking the pointer by reference and null it asap, is the best strategy for safety.No, as has been said multiple times, it makes the code less safe.> > Anyway, I'm past this proposal now. If it is difficult to be understood, it will be difficult to be advocated for, thus not worth the effort.Actually, your proposal is quite easy to understand: You want to automatically null a pointer when it gets deleted. It is just impossible to implement without breaking any (reasonable) existing C++ code (unless recompiled). I'm not talking about the implementation (of course it is easy to understand) , I'm talking about the benefits.   If all the functions above take the pointer by reference and nulls it out directly after freeing the resource, we will have the chance to:1- inspect it, either in the natural return from that function or when we return because of an exception.But the inspection does not provide you with meaningful information - you certainly cannot say “if it is not zero it has not been freed”, because if the object has been deleted you cannot read the pointer.2- get a safe behavior, by default, if we double call the function on the same pointerNo we don’t: as has been said repeatedly this does not (and cannot) make all use of the pointer be through a single storage location, it _creates_ new opportunities for use after free errors, it silently changes the semantics of existing code to introduce memory errors.3- changing the provenance of the pointer by reassigning it, doesn't cause a memory leak.4- we can optimize some pointer comparisons like for example:{ T* p = new T; Foo(p); // takes by reference and may freeOr it is freed through any of the myriad ways in which have been explained previously without updating this reference T* q = new T; if( p == q) // this can be optimized out { }}In the snippet above, q == p is always false, even if Foo calls delete on p then its old value gets recycled then assigned into q in the next allocation. Given (q == p) is always false, then the if statement can be optimized out.Correct, this check does nothing at all. But that has nothing to do with your feature, and it again demonstrates a failure to understand core concepts of the language.T *q = new T;Creates a new object, there for *by definition* that pointer cannot have the same value as any other pointer, so by the language specification this check is always false - the actual values, or the preceding control flow does not matter. `q` points to an object with a lifetime that has just begun. Nothing could have modified the value of `p` prior toif (p == q)Therefore it does not matter what value might actually have at runtime, it does not matter whether you have or have not written null to it, it does not matter what your allocator does. By definition, any value that `p` has is from before the creation of the object that `q` is pointing to, and so it is definitionally impossible for it to have the same value. The fact that in an actual implementation it _could_ have the same value does not matter: the only way that could happen in a conforming implementation is if the lifetime of the object p points to has ended, in which case examine the value of the pointer at all is already an error - I believe even a null check would be UB (*if* the object was deleted).As with previous replies on this thread, you are misunderstand core language behavior. I’ll try to explain with an explicit example:struct Foo { /*...*/ };void noDelete(Foo *&f) {}void nulledAfterDelete(Foo *&f) { delete f; f = nullptr; // modelling your proposal}void notNulledAfterDelete(Foo *&f) { delete f;}void test(void (*doSomething)(Foo*&)) { Foo *f1 = new Foo; doSomething(f1); Foo *f2 = new Foo; if (f1 == f2) { // The condition. }};Let’s now consider what is the case when we call each of these functions:* Calling noDelete => the object has not been deleted, so it still points to the original object, and therefore cannot have the same value as f2* Calling nulledAfterDelete => the object has been deleted, and we set the value to null, the comparison always fails because `new` has created a new object, and that definitionally cannot be a nullptr* Calling notNulledAfterDelete => the object has been deleted, so the f1 is no longer pointing to an object, we just allocated a new object and f2 is pointing to it - f1 is definitionally not pointing to an object so they cannot be the same.I.e. f1 and f2 are by definition never able to be the same. Moreover in the case of notNulledAfterDelete, we are comparing to an invalid pointer (it is a non-null pointer that is not pointing to an object), which makes the comparison UB, so if the compiler sees that path it would also be permitted to assume that the comparison is always true.Writing null to the pointer does not open up this option, it already exists — as a trivial example: https://godbolt.org/z/16Meacds3 the compiled code does not even contain the string literal for that branch. The “-fno-exceptions” bit is just to simplify the code, it does not change the optimization.5- by setting the pointer to nullptr, it is always a good indicator for a possible change in pointer provenance, which can be used for pointer zap, or pointer provenance optimization techniques.It absolutely is not. Again, this has been explained many times by multiple people in this thread. After the object is deleted any comparison to that pointer is UB if it is not set to the nullptr, which means any comparison can be assumed to be false if the compiler can show that it is not possible for the pointer to have been set to the value of the pointer you’re comparing to.I’m just going to be blunt sorry.This proposal is bad.You have ignored the repeated explanations of why this is the case, and you do not seem to have understood any of the explanations of why this does not actually solve any of the hard cases, the language details that have been explained as part of explaining why it does not work, and you also seem uninterested in trying to understand these reasons.There seems to be a very fundamental failure to understand both the bug class, the attack vector, the abstract machine memory model, and the language semantics, and without understanding these things, you are not understanding why what you are proposing does not work, except in very specific niche cases, and in many it actually makes things worse and introduces additional memory errors.A bullet pointed summary, this proposal* Tries to resolve UaF errors, but having the delete and presumably delete[], by set the object parameter in the expression set to nullptr if the expression is an lvalue. It fails if the expression is not.* The practical benefit of the proposal is to make specific use after free paths less exploitable, these paths are already either trivially detectable, if not already, by the compiler itself. Beyond the compiler, standard static analysis tools can already statically detect all realistic cases this feature can prevent, and do so statically ahead of time. These tools can also detect many cases this proposal does not address.At the same time:* It simply does not solve the problem it is presented as solving - multiple people has tried to explain why, and you do not appear to have been interested in understanding the explanations. * It _adds_ rather than removes opportunities for use-after-free bugs.* It significantly impacts performance: directly through the mandatory indirection, the additional pointers stores required to maintain that indirection, and most expensively: removing the ability to reason about the value of a pointer: the value of your pointers may alias, any opaque operation forces at least a reload, etc.* The “improvement” it provides mandates global use of pointer references, as the moment a pointer is ever copied by values any potential safety the proposal disappears completely* Many core language behaviors in C++ mean that you end up with the pointer by value - at which point you cannot ever get back to a reference, so have to store the pointer in a new location. At that point you now have multiple storage locations for the same pointer, and any potential benefit from your proposal is gone.* Global adoption of references to those pointers now trivially, and silently changes the behavior of existing code, making wrong in ways that are almost trivially going to create new memory errors.* The need to pass pointers by reference now means you need to guarantee the lifetime of the storage for the pointer value, which means now you have an additional storage lifetime that you have to get correct or you have a UaF (UaF includes values on the stack, not just dynamic allocation).* It is extremely error prone: it is trivial to accidentally decay to a value, meaning again any potential benefit is lost* Adoption is extremely error prone: there’s a lot of existing code that passes T*&, but now that no longer means the same thing - instead of referencing say a local work value, you’re referencing the original storage. Knowing the correct behavior at any given point will require analysis of the caller and the callee. If those semantics are wrong, the caller needs to store the value locally, and provide a reference to that storage, and again you have lost and potential advantage.* Is not ABI compatible - so no standard library code can adopt it, which then means any use of the standard library will decay to a pointer, and you have lost any potential benefit* An explicit bullet point for the above: this approach introduces many new paths to a number of memory errors, including additional use after free vulnerabilities, the exact problem it is claiming to resolve.* It is not compatible with existing code* By design it can lead to the delete operator itself performing a use after free.* Any existing code that passes pointers to non-dynamically allocated objects cannot now do so: they have to store the address of the object, and the pass a pointer to that temporary. It is not reasonable or ok to say “those functions should just take a pointer by value” because then those functions again break this proposal by making delete no longer zero the owning reference.* The problem it is solving depends on manual lifetime management, but the constraint it requires for even a modicum of safety is easily resolved through the use of RAII objects like unique_ptr, or for more complex cases (that this proposal does not address) shared_ptr and weak_ptr. The examples given for such uses being insufficient depend on the developer ignoring that they are using those RAII objects an manually releasing the memory anyway, but if they are doing that it is unreasonable to assume that they are doing the much more complex work required to have this proposal work at all, let alone avoid introducing new security bugs.On top of all of this, the proposal actively harms the effectiveness of both compiler diagnostics and static analysis tools by adding large amounts of indirection to all pointers, making more or less all use of local value tracking irrelevant, and requiring global reasoning for any use of the pointers where previously local value tracking was sufficient.The TLDR: this does not solve the problem it claims to solve, the cases it does solve are the trivial cases that are already solvable by existing tools (either language features, or static diagnostic tools), at the same time it creates new paths to - or directly creates - memory safety errors, makes existing code silently become incorrect, is difficult to adopt safely requiring manual adoption that is easy to get wrong and even if correct requires every call site to also be correct, failure to make all the required code changes in every location renders it irrelevant, and finally the core safety of the feature is dependent on the adoption of a set of implicit semantics of what it means to pass reference parameters around that does not currently exist, and does not match the semantics implied in existing code.I think every example given of this proposal being better than the existing C++ facilities depends on the developer making errors when using existing facilities, but not making any errors when using this proposal. Ignoring the questionable nature of that comparison, it is not reasonable for a proposal to assume perfect code for everything other than the specific problem that a proposal is trying to address.—Oliver

Received on 2025-09-02 00:11:28