Date: Sat, 23 Aug 2025 02:41:07 -0700
>> On Aug 23, 2025, at 2:20 AM, organicoman <organicoman_at_[hidden]> wrote:
>>
>>
>>
>>>>
>>>> Use after free, double free...etc will be more like to be caught in code
>>>> {
>>>> m_ptr = new T;
>>>>
>>>> // if all the following take m_ptr by reference
>>>> inspect_maybe_free(m_ptr);
>>>> transform_maybe_free(m_ptr);
>>>> maybe_free(m_ptr);
>>>>
>>>> // i can detect if delete was called before.
>>>> if(m_ptr) delete m_ptr;
>>>> }
>>>>
>>>> With the current implementation, you cannot do this.
>>>>
>>>> This is in practice what I would consider an actual trivial UaF, because straight line examples you’ve presented are so easily automatically detected and presented.
>>
>>
>> I don't see how my example is a trivial straight line UaF or double free.
Your entire example is predicated on code that goes:
m_ptr = new
{may free}
{may free}
{may free}
delete m_ptr
This can cause UaF, but static analysis is _very_ good a dealing with lifetime->potential lifetime change->potential lifetime change->potential lifetime change->…
Which is why this is generally not a control flow that causes significant concern.
Where we run into problems is
f(new T)
{do something}
Delete an object
{do something}
Resize an object
{query a cache}
{map a function over an array}
…
…
Examples where the entire lifetime and use of a function is bound essentially to a single lexical scope are _vastly_ easier to reason about. That’s why rust’s lifetime model, and the borrow checking, is derived from lexical scoping. It makes reasoning about object lifetime something that can be reasoned about locally.
>> Given the current implementation of delete,
>> If you pass the pointer by copy, when you return back to the caller, nothing can tell if the resource was freed inside the callees.
Nothing can tell you by your model either - unless you use a handle to that pointer, at which point you have either moved your lifetime issues from the pointer referenced by the handle, to the lifetime of the handle itself, _or_ you use any of the variety of RAII types available to you that make this something that _you_ can reason about.
>> If you pass by reference, you have to zero out the pointer manually in the callee scope (error prone).
But you cannot store the reference unless the “reference” is either a smart pointer (rendering the feature moot) or creating an implicit allocation of an object that holes your reference, which means you need to control that lifetime.
>> So, for an analyzer, it cannot tell what happens inside the callees unless it traverses all the calling tree, inside each callee...., and that is not trivial!
This is exactly what static analyzers already do, and are very good at it, they break down and struggle one you break the linear lifetime flow your example is using.
>> But your suggestion of a compiler flag, would do the job I guess.
That would only resolve the lvalue you delete - it can’t update every other reference.
If that is something you want, std::weak_ptr provides such a functionality.
—Oliver
>>
>>
>>
>>>>
>>>> Use after free, double free...etc will be more like to be caught in code
>>>> {
>>>> m_ptr = new T;
>>>>
>>>> // if all the following take m_ptr by reference
>>>> inspect_maybe_free(m_ptr);
>>>> transform_maybe_free(m_ptr);
>>>> maybe_free(m_ptr);
>>>>
>>>> // i can detect if delete was called before.
>>>> if(m_ptr) delete m_ptr;
>>>> }
>>>>
>>>> With the current implementation, you cannot do this.
>>>>
>>>> This is in practice what I would consider an actual trivial UaF, because straight line examples you’ve presented are so easily automatically detected and presented.
>>
>>
>> I don't see how my example is a trivial straight line UaF or double free.
Your entire example is predicated on code that goes:
m_ptr = new
{may free}
{may free}
{may free}
delete m_ptr
This can cause UaF, but static analysis is _very_ good a dealing with lifetime->potential lifetime change->potential lifetime change->potential lifetime change->…
Which is why this is generally not a control flow that causes significant concern.
Where we run into problems is
f(new T)
{do something}
Delete an object
{do something}
Resize an object
{query a cache}
{map a function over an array}
…
…
Examples where the entire lifetime and use of a function is bound essentially to a single lexical scope are _vastly_ easier to reason about. That’s why rust’s lifetime model, and the borrow checking, is derived from lexical scoping. It makes reasoning about object lifetime something that can be reasoned about locally.
>> Given the current implementation of delete,
>> If you pass the pointer by copy, when you return back to the caller, nothing can tell if the resource was freed inside the callees.
Nothing can tell you by your model either - unless you use a handle to that pointer, at which point you have either moved your lifetime issues from the pointer referenced by the handle, to the lifetime of the handle itself, _or_ you use any of the variety of RAII types available to you that make this something that _you_ can reason about.
>> If you pass by reference, you have to zero out the pointer manually in the callee scope (error prone).
But you cannot store the reference unless the “reference” is either a smart pointer (rendering the feature moot) or creating an implicit allocation of an object that holes your reference, which means you need to control that lifetime.
>> So, for an analyzer, it cannot tell what happens inside the callees unless it traverses all the calling tree, inside each callee...., and that is not trivial!
This is exactly what static analyzers already do, and are very good at it, they break down and struggle one you break the linear lifetime flow your example is using.
>> But your suggestion of a compiler flag, would do the job I guess.
That would only resolve the lvalue you delete - it can’t update every other reference.
If that is something you want, std::weak_ptr provides such a functionality.
—Oliver
Received on 2025-08-23 09:41:23