llvm / llvm/llvm-project

clang-static-analyzer: False positive UAF when removing STAILQ entries

Open
#167,586 13 comments 0 reactions 0 assignees View on GitHub
clang:static analyzer false-positive
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

Static analyzer reports a false positive UAF (`clang-analyzer-cplusplus.NewDelete`) when an element of a TAILQ (singly-linked list with head/tail pointers from [](https://man7.org/linux/man-pages/man7/queue.7.html)) is removed and deleted (in a safe manner) inside a FOREACH loop.

Minimal(ish) repro:
```
#include
#include

struct Foo {
Foo(int _n) : n(_n) {}

int n = 0;
STAILQ_ENTRY(Foo) next = {};
};

STAILQ_HEAD(foos, Foo)
foos = STAILQ_HEAD_INITIALIZER(foos);

int main() {
Foo *fi;

for (int i = 1; i <= 3; i++) {
fi = new Foo(i);
STAILQ_INSERT_TAIL(&foos, fi, next);
}

STAILQ_FOREACH(fi, &foos, next) {
printf("%d\n", fi->n);
}
printf("\n");

bool removed = false;
do {
removed = false;
STAILQ_FOREACH(fi, &foos, next) {
printf("%p\n", fi); // False UAF reported here
if (fi->n == 1) {
STAILQ_REMOVE(&foos, fi, Foo, next);
printf(" delete %p\n", fi);
delete fi;
removed = true;
break;
}
}
} while (removed);
printf("\n");

STAILQ_FOREACH(fi, &foos, next) {
printf("%d\n", fi->n);
}

return 0;
}
```

clang-tidy 21.1.5 warning:

```
[rojer@nbf /tmp]$ ~/Downloads/LLVM-21.1.5-Linux-X64/bin/clang-tidy --version
LLVM (http://llvm.org/):
LLVM version 21.1.5
Optimized build.
[rojer@nbf /tmp]$ ~/Downloads/LLVM-21.1.5-Linux-X64/bin/clang-tidy qq.cpp
Error while trying to load a compilation database:
Could not auto-detect compilation database for file "qq.cpp"
No compilation database found in /tmp or any parent directory
fixed-compilation-database: Error while opening fixed database: No such file or directory
json-compilation-database: Error while opening JSON database: No such file or directory
Running without flags.
1 warning generated.
/tmp/qq.cpp:31:7: warning: Use of memory after it is freed [clang-analyzer-cplusplus.NewDelete]
31 | printf("%p\n", fi); // False UAF reported here
| ^ ~~
/tmp/qq.cpp:17:3: note: Loop condition is true. Entering loop body
17 | for (int i = 1; i <= 3; i++) {
| ^
/tmp/qq.cpp:19:5: note: Loop condition is false. Exiting loop
19 | STAILQ_INSERT_TAIL(&foos, fi, next);
| ^
/usr/include/x86_64-linux-gnu/sys/queue.h:239:46: note: expanded from macro 'STAILQ_INSERT_TAIL'
239 | #define STAILQ_INSERT_TAIL(head, elm, field) do { \
| ^
/tmp/qq.cpp:17:3: note: Loop condition is true. Entering loop body
17 | for (int i = 1; i <= 3; i++) {
| ^
/tmp/qq.cpp:19:5: note: Loop condition is false. Exiting loop
19 | STAILQ_INSERT_TAIL(&foos, fi, next);
| ^
/usr/include/x86_64-linux-gnu/sys/queue.h:239:46: note: expanded from macro 'STAILQ_INSERT_TAIL'
239 | #define STAILQ_INSERT_TAIL(head, elm, field) do { \
| ^
/tmp/qq.cpp:17:3: note: Loop condition is true. Entering loop body
17 | for (int i = 1; i <= 3; i++) {
| ^
/tmp/qq.cpp:19:5: note: Loop condition is false. Exiting loop
19 | STAILQ_INSERT_TAIL(&foos, fi, next);
| ^
/usr/include/x86_64-linux-gnu/sys/queue.h:239:46: note: expanded from macro 'STAILQ_INSERT_TAIL'
239 | #define STAILQ_INSERT_TAIL(head, elm, field) do { \
| ^
/tmp/qq.cpp:17:3: note: Loop condition is false. Execution continues on line 22
17 | for (int i = 1; i <= 3; i++) {
| ^
/tmp/qq.cpp:22:3: note: Loop condition is true. Entering loop body
22 | STAILQ_FOREACH(fi, &foos, next) {
| ^
/usr/include/x86_64-linux-gnu/sys/queue.h:270:2: note: expanded from macro 'STAILQ_FOREACH'
270 | for ((var) = ((head)->stqh_first); \
| ^
/tmp/qq.cpp:22:3: note: Loop condition is false. Execution continues on line 25
22 | STAILQ_FOREACH(fi, &foos, next) {
| ^
/usr/include/x86_64-linux-gnu/sys/queue.h:270:2: note: expanded from macro 'STAILQ_FOREACH'
270 | for ((var) = ((head)->stqh_first); \
| ^
/tmp/qq.cpp:30:5: note: Loop condition is true. Entering loop body
30 | STAILQ_FOREACH(fi, &foos, next) {
| ^
/usr/include/x86_64-linux-gnu/sys/queue.h:270:2: note: expanded from macro 'STAILQ_FOREACH'
270 | for ((var) = ((head)->stqh_first); \
| ^
/tmp/qq.cpp:32:11: note: Assuming field 'n' is not equal to 1
32 | if (fi->n == 1) {
| ^~~~~~~~~~
/tmp/qq.cpp:32:7: note: Taking false branch
32 | if (fi->n == 1) {
| ^
/tmp/qq.cpp:30:5: note: Loop condition is true. Entering loop body
30 | STAILQ_FOREACH(fi, &foos, next) {
| ^
/usr/include/x86_64-linux-gnu/sys/queue.h:270:2: note: expanded from macro 'STAILQ_FOREACH'
270 | for ((var) = ((head)->stqh_first); \
| ^
/tmp/qq.cpp:32:11: note: Assuming field 'n' is equal to 1
32 | if (fi->n == 1) {
| ^~~~~~~~~~
/tmp/qq.cpp:32:7: note: Taking true branch
32 | if (fi->n == 1) {
| ^
/tmp/qq.cpp:33:9: note: Assuming 'fi' is equal to field 'stqh_first'
33 | STAILQ_REMOVE(&foos, fi, Foo, next);
| ^
/usr/include/x86_64-linux-gnu/sys/queue.h:257:6: note: expanded from macro 'STAILQ_REMOVE'
257 | if ((head)->stqh_first == (elm)) { \
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~
/tmp/qq.cpp:33:9: note: Taking true branch
33 | STAILQ_REMOVE(&foos, fi, Foo, next);
| ^
/usr/include/x86_64-linux-gnu/sys/queue.h:257:2: note: expanded from macro 'STAILQ_REMOVE'
257 | if ((head)->stqh_first == (elm)) { \
| ^
/tmp/qq.cpp:33:9: note: Taking false branch
33 | STAILQ_REMOVE(&foos, fi, Foo, next);
| ^
/usr/include/x86_64-linux-gnu/sys/queue.h:258:3: note: expanded from macro 'STAILQ_REMOVE'
258 | STAILQ_REMOVE_HEAD((head), field); \
| ^
/usr/include/x86_64-linux-gnu/sys/queue.h:252:2: note: expanded from macro 'STAILQ_REMOVE_HEAD'
252 | if (((head)->stqh_first = (head)->stqh_first->field.stqe_next) == NULL) \
| ^
/tmp/qq.cpp:33:9: note: Loop condition is false. Exiting loop
33 | STAILQ_REMOVE(&foos, fi, Foo, next);
| ^
/usr/include/x86_64-linux-gnu/sys/queue.h:258:3: note: expanded from macro 'STAILQ_REMOVE'
258 | STAILQ_REMOVE_HEAD((head), field); \
| ^
/usr/include/x86_64-linux-gnu/sys/queue.h:251:41: note: expanded from macro 'STAILQ_REMOVE_HEAD'
251 | #define STAILQ_REMOVE_HEAD(head, field) do { \
| ^
/tmp/qq.cpp:33:9: note: Loop condition is false. Exiting loop
33 | STAILQ_REMOVE(&foos, fi, Foo, next);
| ^
/usr/include/x86_64-linux-gnu/sys/queue.h:256:47: note: expanded from macro 'STAILQ_REMOVE'
256 | #define STAILQ_REMOVE(head, elm, type, field) do { \
| ^
/tmp/qq.cpp:35:9: note: Memory is released
35 | delete fi;
| ^~~~~~~~~
/tmp/qq.cpp:37:9: note: Execution continues on line 40
37 | break;
| ^
/tmp/qq.cpp:28:3: note: Loop condition is true. Execution continues on line 29
28 | do {
| ^
/tmp/qq.cpp:30:5: note: Loop condition is true. Entering loop body
30 | STAILQ_FOREACH(fi, &foos, next) {
| ^
/usr/include/x86_64-linux-gnu/sys/queue.h:270:2: note: expanded from macro 'STAILQ_FOREACH'
270 | for ((var) = ((head)->stqh_first); \
| ^
/tmp/qq.cpp:31:7: note: Use of memory after it is freed
31 | printf("%p\n", fi); // False UAF reported here
| ^ ~~
[rojer@nbf /tmp]$
```

Contributor guide

Open the contributing guide

Research direction

Reproduce the report with the minimal C++ example using clang-tidy and the clang-analyzer-cplusplus.NewDelete checker, with sys/queue.h macros involved. Trace the checker behavior around STAILQ_REMOVE, delete, and the following STAILQ_FOREACH; done means the valid removal pattern no longer produces a false UAF warning while genuine use-after-free cases remain diagnosed.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
compilers, devtools
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.