-
Notifications
You must be signed in to change notification settings - Fork 14.6k
Description
#include <new>
struct foo {
~foo() { delete m_buf; }
char *get_lazy() noexcept [[clang::lifetimebound]] {
if(!m_buf) { m_buf = new (std::nothrow) char; }
return static_cast<char *>(m_buf);
}
char *m_buf = nullptr;
};
char *not_leaky(foo &&buf [[clang::lifetimebound]] = {}) noexcept {
return buf.get_lazy();
}
int main() {
#if 1
not_leaky(); // warning: Potential leak of memory pointed to by field 'm_buf' [clang-analyzer-cplusplus.NewDeleteLeaks]
#else
not_leaky({}); // making the temporary explicit (instead of defaulted) fixes the false-positive
#endif
}
This should not contain any leaks - get_lazy() does perform an allocation, but foo::m_buf takes ownership of it, and ~foo frees it.
However, with #if 1
clang-analyzer-cplusplus.NewDeleteLeaks reports a perceived leak
<source>:21:1: warning: Potential leak of memory pointed to by field 'm_buf' [clang-analyzer-cplusplus.NewDeleteLeaks]
21 | }
| ^
<source>:17:2: note: Calling 'not_leaky'
17 | not_leaky(); // warning: Potential leak of memory pointed to by field 'm_buf' [clang-analyzer-cplusplus.NewDeleteLeaks]
| ^~~~~~~~~~~
<source>:12:9: note: Calling 'foo::get_lazy'
12 | return buf.get_lazy();
| ^~~~~~~~~~~~~~
<source>:5:6: note: Assuming field 'm_buf' is null
5 | if(!m_buf) { m_buf = new char; }
| ^~~~~~
<source>:5:3: note: Taking true branch
5 | if(!m_buf) { m_buf = new char; }
| ^
<source>:5:24: note: Memory is allocated
5 | if(!m_buf) { m_buf = new char; }
| ^~~~~~~~
<source>:12:9: note: Returned allocated memory
12 | return buf.get_lazy();
| ^~~~~~~~~~~~~~
<source>:17:2: note: Returned allocated memory
17 | not_leaky(); // warning: Potential leak of memory pointed to by field 'm_buf' [clang-analyzer-cplusplus.NewDeleteLeaks]
| ^~~~~~~~~~~
<source>:21:1: note: Potential leak of memory pointed to by field 'm_buf'
21 | }
| ^
See https://godbolt.org/z/n9hc5Pdxh, reproducing it on clang trunk with "clang version 22.0.0git (https://github.com/llvm/llvm-project.git 82d7405)"
If you switch to #if 0
(i.e., switch from not_leaky()
to not_leaky({})
, making the temporary explicitly represented in the source code instead of being a defaulted argument, the clang-analyzer-cplusplus.NewDeleteLeaks warnings go away.
The generated assembly is identical either way, showing main does call the destructor, which does call delete. So I don't see any way the potential leak can be real.
mov qword ptr [rbp - 8], 0
lea rdi, [rbp - 8]
call not_leaky(foo&&)
lea rdi, [rbp - 8]
call foo::~foo() [base object destructor]
I used std::nothrow
new and noexcept
just to simplify the control flow so there wouldn't be `std::bad_alloc exception paths cluttering up the assembler, but it the false-positive is the same regardless.
I have also included [[clang::lifetimebound]]
annotations because that's what I was doing to the "real" code I reduced this example from, but they also aren't actually necessary to reproduce the NewDeleteLeaks report. However, the seemingly spurious dependency on whether the temporary argument is defaulted or explicitly passed certainly suggests it might be something similar to #68596 / #112047, with the analyzer not following the lifetime of temporary buried inside CXXDefaultArgExpr
...