From a01c6470f9d8107c9c93c5589d025661977d5ff8 Mon Sep 17 00:00:00 2001 From: aadanen Date: Thu, 27 Aug 2026 17:02:01 -0700 Subject: [PATCH 1/6] impl and tests --- lib/checkleakautovar.cpp | 65 ++++++++++++++++++++++++++++++++++++---- test/testleakautovar.cpp | 49 ++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 5 deletions(-) diff --git a/lib/checkleakautovar.cpp b/lib/checkleakautovar.cpp index e5220e701a3..5c4de8cdeaf 100644 --- a/lib/checkleakautovar.cpp +++ b/lib/checkleakautovar.cpp @@ -267,6 +267,40 @@ static const Token * isFunctionCall(const Token * nameToken) return nullptr; } +/** checks if tok is part of the LHS of an anonymous function call: + * tok should be the first token in a unary expression that evaluates to a + * function pointer, or the name of a lambda or function + * (*func_ptr)(arg) + * or + * (lambda)(arg) + * or + * get_function()(arg) + * + * @param token on the LHS of a function call + * @return opening parenthesis token or nullptr if not a function call + */ +static const Token * isAnonymousFunctionCall(const Token * tok) +{ + // match one of the supported LHS patterns + if (tok->previous()->str() == "(" && !tok->previous()->isBinaryOp() && tok->linkAt(-1)) { + tok = tok->linkAt(-1)->next(); + } else if (!tok->isStandardType() && tok->isName() && tok->linkAt(1)) { + tok = tok->linkAt(1)->next(); + } else { + return nullptr; + } + + // skip over potential template arguments + if (tok->link() && tok->str() == "<") + tok = tok->link()->next(); + + // return the opening parenthesis + if (tok && tok->link() && !tok->isCast() && tok->str() == "(") + return tok; + + return nullptr; +} + static const Token* getOutparamAllocation(const Token* tok, const Library& library) { if (!tok) @@ -787,11 +821,22 @@ bool CheckLeakAutoVarImpl::checkScope(const Token * const startToken, } } + // a regular function call can return an anonymous function + openingPar = isAnonymousFunctionCall(ftok); + if (openingPar) { + functionCall(nullptr, openingPar, varInfo, VarInfo::AllocInfo(0, VarInfo::NOALLOC), nullptr); + tok = openingPar->link(); + } + continue; - } + + // top level call to an anonymous function + } else if (const Token *openingPar = isAnonymousFunctionCall(tok)) { + functionCall(nullptr, openingPar, varInfo, VarInfo::AllocInfo(0, VarInfo::NOALLOC), nullptr); + tok = openingPar->link(); // goto => weird execution path - else if (tok->str() == "goto") { + } else if (tok->str() == "goto") { varInfo.clear(); return false; } @@ -941,6 +986,7 @@ const Token * CheckLeakAutoVarImpl::checkTokenInsideExpression(const Token * con // check for function call const Token * const openingPar = inFuncCall ? nullptr : isFunctionCall(tok); + const Token * const anonOpeningPar = isAnonymousFunctionCall(tok); if (openingPar) { const Library::AllocFunc* allocFunc = mSettings.library.getDeallocFuncInfo(tok); VarInfo::AllocInfo alloc(allocFunc ? allocFunc->groupId : 0, VarInfo::DEALLOC, tok); @@ -948,10 +994,19 @@ const Token * CheckLeakAutoVarImpl::checkTokenInsideExpression(const Token * con alloc.status = VarInfo::NOALLOC; functionCall(tok, openingPar, varInfo, alloc, nullptr); const std::string &returnValue = mSettings.library.returnValue(tok); + if (startsWith(returnValue, "arg")) // the function returns one of its argument, we need to process a potential assignment return openingPar; - return isCPPCast(tok->astParent()) ? openingPar : openingPar->link(); + + if (!anonOpeningPar) + return isCPPCast(tok->astParent()) ? openingPar : openingPar->link(); + } + + // check for anonymous function call + if (anonOpeningPar) { + functionCall(nullptr, anonOpeningPar, varInfo, VarInfo::AllocInfo(0, VarInfo::NOALLOC), nullptr); + return anonOpeningPar->link(); } return nullptr; @@ -1022,7 +1077,7 @@ void CheckLeakAutoVarImpl::functionCall(const Token *tokName, const Token *tokOp const bool isLeakIgnore = mSettings.library.isLeakIgnore(mSettings.library.getFunctionName(tokName)); if (mSettings.library.getReallocFuncInfo(tokName)) return; - if (tokName->next()->valueType() && tokName->next()->valueType()->container && tokName->next()->valueType()->container->stdStringLike) + if (tokName && tokName->next()->valueType() && tokName->next()->valueType()->container && tokName->next()->valueType()->container->stdStringLike) return; const Token * const tokFirstArg = tokOpeningPar->next(); @@ -1269,7 +1324,7 @@ void CheckLeakAutoVarImpl::ret(const Token *tok, VarInfo &varInfo, const bool is const auto use = possibleUsage.find(varid); if (use == possibleUsage.end()) { leakError(tok, var->name(), it->second.type); - } else if (!use->second.first->variable()) { // TODO: handle constructors + } else if (use->second.first && !use->second.first->variable()) { // TODO: handle constructors configurationInfo(tok, use->second); } } diff --git a/test/testleakautovar.cpp b/test/testleakautovar.cpp index cb5f9d53c8a..2b0a2b3882d 100644 --- a/test/testleakautovar.cpp +++ b/test/testleakautovar.cpp @@ -118,6 +118,7 @@ class TestLeakAutoVar : public TestFixture { // handling function calls TEST_CASE(functioncall1); + TEST_CASE(anonymousFunctionCall1); // goto TEST_CASE(goto1); @@ -1910,6 +1911,54 @@ class TestLeakAutoVar : public TestFixture { ASSERT_EQUALS("[test.cpp:4:1]: (error) Memory leak: b [memleak]\n", errout_str()); } + void anonymousFunctionCall1() { // #14990 + // function pointer + check("void f(void (*fptr)(void *)) {\n" + "void *buf = malloc(1);\n" + "(*fptr)(buf);\n" + "}\n"); + ASSERT_EQUALS("", errout_str()); + + // lambda + check("void f() {\n" + "auto x = [](void *ptr) { g(ptr) };\n" + "void *p = malloc(1);\n" + "(x)(p);\n" + "}\n"); + ASSERT_EQUALS("", errout_str()); + + // Function returning a function pointer + check("void f() {\n" + " void *buf = malloc(1);\n" + " get_function()(buf);\n" + "}\n"); + ASSERT_EQUALS("", errout_str()); + + // Function returning a function pointer, passed as an arg to a normal + // function + check("void f() {\n" + " void *buf = malloc(1);\n" + " foo(get_function()(buf));\n" + "}\n"); + ASSERT_EQUALS("[test.c:3:29]: (information) --check-library: Function foo() should have configuration [checkLibraryNoReturn]\n" + "[test.c:4:1]: (information) --check-library: Function foo() should have / configuration [checkLibraryUseIgnore]\n", + errout_str()); + + // Function returning a function pointer, passed as an arg to another + // function returning a function pointer + check("void f() {\n" + " void *buf = malloc(1);\n" + " get_function()(get_function()(buf));\n" + "}\n"); + ASSERT_EQUALS("", errout_str()); + + check("void f() {\n" + " void *buf = malloc(1);\n" + " get_function(buf)(get_function(NULL)(NULL));\n" + "}\n"); + ASSERT_EQUALS("[test.c:4:1]: (information) --check-library: Function get_function() should have / configuration [checkLibraryUseIgnore]\n", errout_str()); + } + void goto1() { check("static void f() {\n" " int err = -ENOMEM;\n" From ad5b97c9f56e330b99416930689c553b5578f02b Mon Sep 17 00:00:00 2001 From: aadanen Date: Thu, 27 Aug 2026 08:00:32 -0700 Subject: [PATCH 2/6] formatting --- lib/checkleakautovar.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/checkleakautovar.cpp b/lib/checkleakautovar.cpp index 5c4de8cdeaf..97f015a1e4c 100644 --- a/lib/checkleakautovar.cpp +++ b/lib/checkleakautovar.cpp @@ -994,7 +994,6 @@ const Token * CheckLeakAutoVarImpl::checkTokenInsideExpression(const Token * con alloc.status = VarInfo::NOALLOC; functionCall(tok, openingPar, varInfo, alloc, nullptr); const std::string &returnValue = mSettings.library.returnValue(tok); - if (startsWith(returnValue, "arg")) // the function returns one of its argument, we need to process a potential assignment return openingPar; From a4169642af3dab4f4499b3b33d5121f4dc03e133 Mon Sep 17 00:00:00 2001 From: aadanen Date: Thu, 27 Aug 2026 11:10:13 -0700 Subject: [PATCH 3/6] formatting --- lib/checkleakautovar.cpp | 8 ++++---- test/testleakautovar.cpp | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/checkleakautovar.cpp b/lib/checkleakautovar.cpp index 97f015a1e4c..d4272ca7023 100644 --- a/lib/checkleakautovar.cpp +++ b/lib/checkleakautovar.cpp @@ -279,7 +279,7 @@ static const Token * isFunctionCall(const Token * nameToken) * @param token on the LHS of a function call * @return opening parenthesis token or nullptr if not a function call */ -static const Token * isAnonymousFunctionCall(const Token * tok) +static const Token * isAnonymousFunctionCall(const Token * tok) { // match one of the supported LHS patterns if (tok->previous()->str() == "(" && !tok->previous()->isBinaryOp() && tok->linkAt(-1)) { @@ -829,13 +829,13 @@ bool CheckLeakAutoVarImpl::checkScope(const Token * const startToken, } continue; - - // top level call to an anonymous function + + // top level call to an anonymous function } else if (const Token *openingPar = isAnonymousFunctionCall(tok)) { functionCall(nullptr, openingPar, varInfo, VarInfo::AllocInfo(0, VarInfo::NOALLOC), nullptr); tok = openingPar->link(); - // goto => weird execution path + // goto => weird execution path } else if (tok->str() == "goto") { varInfo.clear(); return false; diff --git a/test/testleakautovar.cpp b/test/testleakautovar.cpp index 2b0a2b3882d..097a09ac508 100644 --- a/test/testleakautovar.cpp +++ b/test/testleakautovar.cpp @@ -1914,16 +1914,16 @@ class TestLeakAutoVar : public TestFixture { void anonymousFunctionCall1() { // #14990 // function pointer check("void f(void (*fptr)(void *)) {\n" - "void *buf = malloc(1);\n" - "(*fptr)(buf);\n" + "void *buf = malloc(1);\n" + "(*fptr)(buf);\n" "}\n"); ASSERT_EQUALS("", errout_str()); - + // lambda check("void f() {\n" - "auto x = [](void *ptr) { g(ptr) };\n" - "void *p = malloc(1);\n" - "(x)(p);\n" + "auto x = [](void *ptr) { g(ptr) };\n" + "void *p = malloc(1);\n" + "(x)(p);\n" "}\n"); ASSERT_EQUALS("", errout_str()); @@ -1941,9 +1941,9 @@ class TestLeakAutoVar : public TestFixture { " foo(get_function()(buf));\n" "}\n"); ASSERT_EQUALS("[test.c:3:29]: (information) --check-library: Function foo() should have configuration [checkLibraryNoReturn]\n" - "[test.c:4:1]: (information) --check-library: Function foo() should have / configuration [checkLibraryUseIgnore]\n", + "[test.c:4:1]: (information) --check-library: Function foo() should have / configuration [checkLibraryUseIgnore]\n", errout_str()); - + // Function returning a function pointer, passed as an arg to another // function returning a function pointer check("void f() {\n" From c7e3afd5637b4f007d8f8ec1d6d4ca86f4949198 Mon Sep 17 00:00:00 2001 From: aadanen Date: Thu, 27 Aug 2026 12:50:32 -0700 Subject: [PATCH 4/6] sanitizer --- lib/checkleakautovar.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/checkleakautovar.cpp b/lib/checkleakautovar.cpp index d4272ca7023..ec4d4be0c76 100644 --- a/lib/checkleakautovar.cpp +++ b/lib/checkleakautovar.cpp @@ -276,7 +276,7 @@ static const Token * isFunctionCall(const Token * nameToken) * or * get_function()(arg) * - * @param token on the LHS of a function call + * @param tok on the LHS of a function call * @return opening parenthesis token or nullptr if not a function call */ static const Token * isAnonymousFunctionCall(const Token * tok) @@ -831,9 +831,9 @@ bool CheckLeakAutoVarImpl::checkScope(const Token * const startToken, continue; // top level call to an anonymous function - } else if (const Token *openingPar = isAnonymousFunctionCall(tok)) { - functionCall(nullptr, openingPar, varInfo, VarInfo::AllocInfo(0, VarInfo::NOALLOC), nullptr); - tok = openingPar->link(); + } else if (const Token *lpar = isAnonymousFunctionCall(tok)) { + functionCall(nullptr, lpar, varInfo, VarInfo::AllocInfo(0, VarInfo::NOALLOC), nullptr); + tok = lpar->link(); // goto => weird execution path } else if (tok->str() == "goto") { From 2621d76ae32fd0c0869a6dc3b4432bfd5a7234a7 Mon Sep 17 00:00:00 2001 From: aadanen Date: Thu, 27 Aug 2026 16:32:39 -0700 Subject: [PATCH 5/6] make functionCall not segfault --- lib/checkleakautovar.cpp | 43 +++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/lib/checkleakautovar.cpp b/lib/checkleakautovar.cpp index ec4d4be0c76..e16f44f454e 100644 --- a/lib/checkleakautovar.cpp +++ b/lib/checkleakautovar.cpp @@ -1072,9 +1072,8 @@ void CheckLeakAutoVarImpl::changeAllocStatus(VarInfo &varInfo, const VarInfo::Al void CheckLeakAutoVarImpl::functionCall(const Token *tokName, const Token *tokOpeningPar, VarInfo &varInfo, const VarInfo::AllocInfo& allocation, const Library::AllocFunc* af) { - // Ignore function call? - const bool isLeakIgnore = mSettings.library.isLeakIgnore(mSettings.library.getFunctionName(tokName)); - if (mSettings.library.getReallocFuncInfo(tokName)) + const bool isLeakIgnore = tokName ? mSettings.library.isLeakIgnore(mSettings.library.getFunctionName(tokName)) : false; + if (tokName && mSettings.library.getReallocFuncInfo(tokName)) return; if (tokName && tokName->next()->valueType() && tokName->next()->valueType()->container && tokName->next()->valueType()->container->stdStringLike) return; @@ -1125,26 +1124,30 @@ void CheckLeakAutoVarImpl::functionCall(const Token *tokName, const Token *tokOp // Is variable allocated? if (!isnull && (!af || af->arg == argNr)) { - const Library::AllocFunc* deallocFunc = mSettings.library.getDeallocFuncInfo(tokName); + const Library::AllocFunc* deallocFunc = tokName ? mSettings.library.getDeallocFuncInfo(tokName) : nullptr; VarInfo::AllocInfo dealloc(deallocFunc ? deallocFunc->groupId : 0, VarInfo::DEALLOC, tokName); - if (const Library::AllocFunc* allocFunc = mSettings.library.getAllocFuncInfo(tokName)) { - if (mSettings.library.getDeallocFuncInfo(tokName)) { - changeAllocStatus(varInfo, dealloc.type == 0 ? allocation : dealloc, tokName, arg); - } - if (allocFunc->arg == argNr && - !(arg->variable() && arg->variable()->isArgument() && arg->valueType() && arg->valueType()->pointer > 1) && - (isAddressOf || (arg->valueType() && arg->valueType()->pointer == 2))) { - leakIfAllocated(arg, varInfo); - VarInfo::AllocInfo& varAlloc = varInfo.alloctype[arg->varId()]; - varAlloc.type = allocFunc->groupId; - varAlloc.status = VarInfo::ALLOC; - varAlloc.allocTok = arg; + if (tokName) { + if (const Library::AllocFunc* allocFunc = mSettings.library.getAllocFuncInfo(tokName)) { + if (mSettings.library.getDeallocFuncInfo(tokName)) { + changeAllocStatus(varInfo, dealloc.type == 0 ? allocation : dealloc, tokName, arg); + } + if (allocFunc->arg == argNr && + !(arg->variable() && arg->variable()->isArgument() && arg->valueType() && arg->valueType()->pointer > 1) && + (isAddressOf || (arg->valueType() && arg->valueType()->pointer == 2))) { + leakIfAllocated(arg, varInfo); + VarInfo::AllocInfo& varAlloc = varInfo.alloctype[arg->varId()]; + varAlloc.type = allocFunc->groupId; + varAlloc.status = VarInfo::ALLOC; + varAlloc.allocTok = arg; + } } + else if (isLeakIgnore) + checkTokenInsideExpression(arg, varInfo); + else + changeAllocStatus(varInfo, dealloc.type == 0 ? allocation : dealloc, tokName, arg); + } else { + changeAllocStatus(varInfo, allocation, nullptr, arg); } - else if (isLeakIgnore) - checkTokenInsideExpression(arg, varInfo); - else - changeAllocStatus(varInfo, dealloc.type == 0 ? allocation : dealloc, tokName, arg); } } // Check smart pointer From 7d4276c9436180225dcf89777895666ae2958ee0 Mon Sep 17 00:00:00 2001 From: aadanen Date: Fri, 28 Aug 2026 07:30:12 -0700 Subject: [PATCH 6/6] (int) is not lambda call --- lib/checkleakautovar.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/checkleakautovar.cpp b/lib/checkleakautovar.cpp index e16f44f454e..bf40904f2f4 100644 --- a/lib/checkleakautovar.cpp +++ b/lib/checkleakautovar.cpp @@ -282,7 +282,17 @@ static const Token * isFunctionCall(const Token * nameToken) static const Token * isAnonymousFunctionCall(const Token * tok) { // match one of the supported LHS patterns - if (tok->previous()->str() == "(" && !tok->previous()->isBinaryOp() && tok->linkAt(-1)) { + // TODO: check if tok->previous()->isCast(). can't right now because + // + // auto x = [](void *ptr) { g(ptr) }; + // void *p = malloc(1); + // (x)(p); + // ^ + // the lpar surrounding x has isCast() == true, so checking isCast() would + // have false positive leaks, while allowing casts to take ownership of + // resources is instead a false negative + if (tok->previous()->str() == "(" && !tok->previous()->isBinaryOp() && + tok->linkAt(-1) && !tok->isStandardType()) { tok = tok->linkAt(-1)->next(); } else if (!tok->isStandardType() && tok->isName() && tok->linkAt(1)) { tok = tok->linkAt(1)->next();