Issue |
149236
|
Summary |
false-positive clang-analyzer-cplusplus.NewDeleteLeaks on allocation owned by temporary initialized by a default argument
|
Labels |
false-positive
|
Assignees |
|
Reporter |
puetzk
|
```c++
#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 82d7405b3bb911c86d0b07c8a6bec5044acbdf66)"
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 does is identical either way, showing main does call the destructor, which does call delete.
```
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`...
_______________________________________________
llvm-bugs mailing list
llvm-bugs@lists.llvm.org
https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-bugs