From ff9874af2b6260917dd878c9ffdf6a19ee7359f5 Mon Sep 17 00:00:00 2001 From: keremsahn Date: Thu, 9 Jul 2026 21:29:11 +0300 Subject: [PATCH 1/2] Support for branchs --- include/CppInterOp/CppInterOpTypes.h | 5 +- lib/CppInterOp/CppInterOp.cpp | 178 ++++++++++- .../CppInterOp/FunctionReflectionTest.cpp | 293 +++++++++++++++++- 3 files changed, 458 insertions(+), 18 deletions(-) diff --git a/include/CppInterOp/CppInterOpTypes.h b/include/CppInterOp/CppInterOpTypes.h index 45a470e76..fdeaf2d67 100644 --- a/include/CppInterOp/CppInterOpTypes.h +++ b/include/CppInterOp/CppInterOpTypes.h @@ -412,7 +412,10 @@ enum class AllocType : unsigned char { NewArr, Malloc, Unknown, - CustomAlloc + CustomAlloc, + Null, + OperatorNew, + OperatorNewArr }; inline QualKind operator|(QualKind a, QualKind b) { diff --git a/lib/CppInterOp/CppInterOp.cpp b/lib/CppInterOp/CppInterOp.cpp index 61abcb363..7260cbe6a 100644 --- a/lib/CppInterOp/CppInterOp.cpp +++ b/lib/CppInterOp/CppInterOp.cpp @@ -1650,6 +1650,9 @@ struct AllocationTraverser : RecursiveASTVisitor { // Result var keeps the combination all possible values of previous return // statements std::optional result; + // A map every branch writes the previous value of overwriten variables + std::unordered_map>* undoLog = + nullptr; AllocationTraverser(std::unordered_map>& cache) @@ -1665,6 +1668,109 @@ struct AllocationTraverser : RecursiveASTVisitor { return RecursiveASTVisitor::TraverseDecl(D); } + std::optional join(std::optional a, + std::optional b) { + if (a == AllocType::Null) + return b; + + if (b == AllocType::Null) + return a; + + if (a == b) + return a; + return AllocType::Unknown; + } + void undoOnVarMap() { + for (auto& [VD, oldVal] : *undoLog) { + auto it = varMap.find(VD); + if (it == varMap.end()) + continue; + varMap[VD] = oldVal; + } + } + + bool TraverseIfStmt(clang::IfStmt* IS) { + TraverseStmt(IS->getConditionVariableDeclStmt()); + TraverseStmt(IS->getCond()); + auto* undoLogCopy = undoLog; + std::unordered_map> + undoLogThen; + undoLog = &undoLogThen; + TraverseStmt(IS->getThen()); + auto* elseBranch = IS->getElse(); + // There is no else + if (!elseBranch) { + for (auto& [VD, val] : undoLogThen) { + auto it = varMap.find(VD); + if (it == varMap.end()) + continue; + // Overwrite varMap with join of overwriten and previous value + it->second = join(val, it->second); + // If branch is nested, inform upper branch about your changes + if (undoLogCopy) + undoLogCopy->try_emplace(VD, val); + } + undoLog = undoLogCopy; + return true; + } + + // Save overwriten values in if branch to join + std::unordered_map> + valuesInIfBranch; + for (auto& [VD, val] : undoLogThen) { + auto it = varMap.find(VD); + if (it == varMap.end()) + continue; + valuesInIfBranch[VD] = it->second; + } + + // Take changes on varMap back before going to else branch + undoOnVarMap(); + + std::unordered_map> + undoLogElse; + undoLog = &undoLogElse; + TraverseStmt(elseBranch); + + for (auto& [VD, val] : undoLogElse) { + auto it = varMap.find(VD); + if (it == varMap.end()) + continue; + auto it2 = valuesInIfBranch.find(VD); + // If var is just changed in else branch + if (it2 == valuesInIfBranch.end()) { + it->second = join(val, it->second); + continue; + } + // If var is changed in both + it->second = join(it->second, it2->second); + } + + for (auto& [VD, val] : valuesInIfBranch) { + auto it = varMap.find(VD); + if (it == varMap.end()) + continue; + // If var is just changed in if branch + if (undoLogElse.find(VD) == undoLogElse.end()) { + it->second = join(val, it->second); + continue; + } + // If var is changed in both, here for extra safety + it->second = join(val, it->second); + } + + // Inform upper branch about changes done for both inner if and else branch + if (undoLogCopy) { + for (auto& [VD, val] : undoLogThen) + undoLogCopy->try_emplace(VD, val); + for (auto& [VD, val] : undoLogElse) + undoLogCopy->try_emplace(VD, val); + } + + undoLog = undoLogCopy; + return true; + } + bool VisitVarDecl(VarDecl* VD) { Expr* expr = VD->getInit(); if (expr) @@ -1675,15 +1781,28 @@ struct AllocationTraverser : RecursiveASTVisitor { } bool VisitBinaryOperator(clang::BinaryOperator* BO) { - if (BO->getOpcode() != BO_Assign) - return true; Expr* LHS = BO->getLHS(); LHS = LHS->IgnoreParenCasts(); - if (auto* DRE = dyn_cast(LHS)) { - if (auto* VD = dyn_cast(DRE->getDecl())) { - Expr* RHS = BO->getRHS(); - varMap[VD] = handleExpr(RHS); - } + auto* DRE = dyn_cast(LHS); + if (!DRE) + return true; + + auto* VD = dyn_cast(DRE->getDecl()); + if (!VD) + return true; + + if (BO->getOpcode() == BO_Assign) { + if (undoLog) + undoLog->try_emplace(VD, varMap[VD]); + Expr* RHS = BO->getRHS(); + varMap[VD] = handleExpr(RHS); + return true; + } + if (BO->isCompoundAssignmentOp()) { + if (undoLog) + undoLog->try_emplace(VD, varMap[VD]); + varMap[VD] = AllocType::Unknown; + return true; } return true; } @@ -1695,14 +1814,15 @@ struct AllocationTraverser : RecursiveASTVisitor { std::optional tmp = handleExpr(retExpr); if (!tmp.has_value()) return true; - if (!result.has_value()) + if (!result.has_value()) { result = tmp; + return true; + } // If function's allocation behaviour differs between different cases, // analyzer returns unknown. - else if (*result != tmp) { - result = AllocType::Unknown; + result = join(result, tmp); + if (result == AllocType::Unknown) return false; - } return true; } @@ -1710,6 +1830,19 @@ struct AllocationTraverser : RecursiveASTVisitor { if (const auto* FD = CE->getDirectCallee()) { if (FD->getBuiltinID() == Builtin::ID::BImalloc) return AllocType::Malloc; + if (FD->getBuiltinID() == Builtin::ID::BI__builtin_operator_new) + return AllocType::OperatorNew; + // Detects operator new/new[]/delete/delete[] + if (FD->isReplaceableGlobalAllocationFunction()) { + switch (FD->getOverloadedOperator()) { + case OO_New: + return AllocType::OperatorNew; + case OO_Array_New: + return AllocType::OperatorNewArr; + default: + break; // OO_Delete/OO_Array_Delete + } + } auto it = visitedFuncs.find(FD); if (it == visitedFuncs.end()) { visitedFuncs[FD] = std::nullopt; @@ -1722,8 +1855,16 @@ struct AllocationTraverser : RecursiveASTVisitor { } static AllocType handleNew(const clang::CXXNewExpr* CNE) { - if (CNE->getNumPlacementArgs() > 0) - return AllocType::None; + if (CNE->getNumPlacementArgs() > 0) { + /* + Non-allocating placement allocation functions + void* operator new ( std::size_t count, void* ptr ); + void* operator new[]( std::size_t count, void* ptr ); + */ + const clang::FunctionDecl* OpNew = CNE->getOperatorNew(); + if (OpNew && OpNew->isReservedGlobalPlacementOperator()) + return AllocType::None; + } if (CNE->isArray()) return AllocType::NewArr; return AllocType::New; @@ -1749,6 +1890,15 @@ struct AllocationTraverser : RecursiveASTVisitor { // Case: malloc or another func call if (const auto* CE = dyn_cast(finExpr)) return handleCall(CE); + + // Case: NULL, nullptr or (int*)(__integerLiteral__) + const clang::Expr* parExpr = expr->IgnoreParens(); + while (const auto* CE = dyn_cast(parExpr)) { + if (CE->getCastKind() == CK_NullToPointer) + return AllocType::Null; + parExpr = CE->getSubExpr(); + } + return AllocType::None; } }; @@ -1769,6 +1919,8 @@ AnalyzeAllocType(const clang::FunctionDecl* Fn, if (!CmpStmt) return AllocType::Unknown; AllocationTraverser Traverser(visitedFuncs); + for (auto* parm : Fn->parameters()) + Traverser.VisitVarDecl(parm); Traverser.TraverseStmt(const_cast(CmpStmt)); auto res = Traverser.result; visitedFuncs[Fn] = res; diff --git a/unittests/CppInterOp/FunctionReflectionTest.cpp b/unittests/CppInterOp/FunctionReflectionTest.cpp index 5c5a4e5fd..ed8a13a30 100644 --- a/unittests/CppInterOp/FunctionReflectionTest.cpp +++ b/unittests/CppInterOp/FunctionReflectionTest.cpp @@ -945,6 +945,267 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetAllocType) { auto lam = []() { return (int*)malloc(sizeof(int)); }; return new int(n); } + + int* func34(int n){ + int* ptr = nullptr; + if(n>0) + ptr = new int(n); + return ptr; + } + + int* func35(int n){ + int* ptr = nullptr; + if(n>0) + ptr = new int(n); + else + ptr = new int(n); + return ptr; + } + + int* func36(int n){ + int* ptr = nullptr; + if(n>0) + ptr = new int(n); + else + ptr = (int*)malloc(sizeof(int)); + return ptr; + } + + int* func37(int n){ + int* ptr = nullptr; + int* ptr2 = nullptr; + if(n>0){ + ptr = new int(n); + } else { + ptr2 = new int(n); + } + return ptr; + } + + int* func38(int n){ + int* ptr = nullptr; + if(n>9) + ptr = new int(n); + else if(n>5) + ptr = new int(n); + else if(n>2) + ptr = new int(n); + else + ptr = new int(n); + return ptr; + } + + int* func39(int n){ + int* ptr = nullptr; + if(n>9) + ptr = new int(n); + else if(n>5) + ptr = (int*)malloc(sizeof(int)); + else if(n>2) + ptr = new int(n); + else + ptr = new int(n); + return ptr; + } + + int* func40(int n){ + int* ptr = nullptr; + if(n>0){ + int* tmp = nullptr; + if(n>10) + tmp = new int(n); + else + tmp = new int(n); + ptr = tmp; + } + return ptr; + } + + int* func41(int n){ + int* ptr = nullptr; + if(n>0){ + ptr = new int(n); + ptr = new int(n); + } + return ptr; + } + + int* func42(int n){ + int* ptr = nullptr; + if(int* tmp = new int(n)){ + ptr = tmp; + } + return ptr; + } + + int* func43(int n){ + int* ptr = new int(n); + if(n>0){ + int* tmp = (int*)malloc(sizeof(int)); + ptr = tmp; + } + return ptr; + } + + int* func44(int n){ + int* q = NULL; + if(n>9){ + + } + else if(n>5){ + q = new int(n); + } + else{ + q = new int(n); + } + return q; + } + + int* func45(int n){ + int* x = (int*)0; + if(n>5){ + if(n>8) + x = new int(n); + else + x = new int(n); + } + return x; + } + + int* func46(int n){ + int* p = nullptr; + if(n>20){ + if(n>15){ + if(n>10) + p = new int(n); + else + p = new int(n); + } + else { + p = new int(n); + } + } + else { + p = new int(n); + } + return p; + } + + int* func47(int n){ + int* p = nullptr; + if(n>20){ + if(n>15){ + if(n>10) + p = new int(n); + else + p = new int(n); + } + } + return p; + } + + int* func48(int n){ + int* p = nullptr; + int* q = nullptr; + if(n>30){ + q = new int(n); + if(n>25){ + if(n>22) + p = new int(n); + else + p = new int(n); + } + else { + p = new int(n); + } + } + else if(n>20){ + p = new int(n); + if(n>15) + q = new int(n); + else + q = (int*)malloc(sizeof(int)); + } + else { + p = new int(n); + q = new int(n); + } + return p; + } + + int* func49(int n){ + int* p = (int*)malloc(sizeof(int)); + int* q = new int(n); + if(n>30){ + if(n>25){ + p = (int*)malloc(sizeof(int)); + if(n>20) + q += 1; + } + else { + p = (int*)malloc(sizeof(int)); + } + } + return p; + } + + int* func50(int n){ + int* p = nullptr; + if(n>40){ + p = new int(n); + } + else if(n>30){ + if(n>25){ + if(n>20) + p = new int(n); + else + p = (int*)malloc(sizeof(int)); + } + } + else if(n>10){ + p = new int(n); + } + else { + p = new int(n); + } + return p; + } + + int* func51(int n){ + int* ptr = nullptr; + if(ptr = new int(n)){ + + } + return ptr; + } + + int* func52(int n){ + if(n>0) + return new int(n); + return NULL; + } + + int* func53(int n){ + int* p = static_cast(0); + if(n>0) + p = new int(n); + return p; + } + + int* func54(int n){ + int* p; + if(n>0) + p = new int(n); + else + p = (int*)malloc(sizeof(int)); + return p; + } + void* func55(){ return ::operator new(64); } + void* func56(){ return ::operator new[](64); } + void* func57(void* buf){ return ::operator new(sizeof(int), buf); } + void* func58(){ return __builtin_operator_new(64); } + void* func59(){int* m = (int*)0; return malloc(sizeof(int));} + )"; TestFixture::CreateInterpreter(); Interp->declare(code); @@ -973,7 +1234,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetAllocType) { TESTAC(16, Unknown); TESTAC(17, None); TESTAC(18, None); - TESTAC(19, None); + TESTAC(19, Null); TESTAC(20, Unknown); TESTAC(21, None); TESTAC(22, New); @@ -981,15 +1242,39 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetAllocType) { TESTAC(24, New); TESTAC(25, NewArr); TESTAC(26, Unknown); - // FIXME: Pointer overwriten by a non-assignment operator - TESTAC(27, NewArr); + TESTAC(27, Unknown); TESTAC(28, New); TESTAC(29, New); TESTAC(30, New); TESTAC(31, New); TESTAC(32, New); TESTAC(33, New); - + TESTAC(34, New); + TESTAC(35, New); + TESTAC(36, Unknown); + TESTAC(37, New); + TESTAC(38, New); + TESTAC(39, Unknown); + TESTAC(40, New); + TESTAC(41, New); + TESTAC(42, New); + TESTAC(43, Unknown); + TESTAC(44, New); + TESTAC(45, New); + TESTAC(46, New); + TESTAC(47, New); + TESTAC(48, New); + TESTAC(49, Malloc); + TESTAC(50, Unknown); + TESTAC(51, New); + TESTAC(52, New); + TESTAC(53, New); + TESTAC(54, Unknown); + TESTAC(55, OperatorNew); + TESTAC(56, OperatorNewArr); + TESTAC(57, None); + TESTAC(58, OperatorNew); + TESTAC(59, Malloc); #undef TESTAC Cpp::DeleteInterpreter(); From aeeb54100bcd9d4d027985e748aabf244acff8c4 Mon Sep 17 00:00:00 2001 From: keremsahn Date: Mon, 20 Jul 2026 16:13:30 +0300 Subject: [PATCH 2/2] [memory analysis] extend GetAllocType to see through optional-like wrapper constructions Restrict the pointer-arithmetic Unknown-downgrade in VisitBinaryOperator to pointer-typed variables only, so unrelated scalar compound-assignments no longer poison tracked results. Allow AnalyzeAllocType to also traverse record-typed (class/struct) return values, not just raw pointers, so wrapper types like std::optional get analyzed instead of short-circuiting to None. In handleExpr: - Unwrap unary operator* calls (e.g. on an optional) to recurse into the wrapped value instead of falling back to a call analysis. - Detect nullptr/NULL literals directly via CXXNullPtrLiteralExpr/ GNUNullExpr, since the existing CK_NullToPointer cast walk cannot see through a MaterializeTemporaryExpr (e.g. nullptr_t passed to optional's converting constructor). - Recognize implicit converting-constructor calls (ExprWithCleanups -> ImplicitCastExpr(CK_ConstructorConversion) -> CXXConstructExpr) and recurse into the constructor's argument, so New/Malloc/etc. tags propagate through implicit wrapping (e.g. ). - Add isNullOpt() to recognize std::nullopt/cpp::nullopt construction as AllocType::Null. Known limitation (documented via FIXME + test): optional's copy/move constructor is not a converting constructor, so copying an optional loses its tracked AllocType and reports None. Add FunctionReflection_GetAllocType coverage (func60-func69) for: implicit optional-wrapping of new, std::nullopt join behavior, default-constructed empty structs, direct/list-initialized plain wrapper structs, operator* unwrap (unary vs. binary), direct nullptr returns through optional, and pointer arithmetic inside a for-loop increment clause. --- lib/CppInterOp/CppInterOp.cpp | 49 ++++++++++- .../CppInterOp/FunctionReflectionTest.cpp | 84 ++++++++++++++++++- 2 files changed, 128 insertions(+), 5 deletions(-) diff --git a/lib/CppInterOp/CppInterOp.cpp b/lib/CppInterOp/CppInterOp.cpp index 7260cbe6a..1895f7aa4 100644 --- a/lib/CppInterOp/CppInterOp.cpp +++ b/lib/CppInterOp/CppInterOp.cpp @@ -1798,7 +1798,7 @@ struct AllocationTraverser : RecursiveASTVisitor { varMap[VD] = handleExpr(RHS); return true; } - if (BO->isCompoundAssignmentOp()) { + if (VD->getType()->isPointerType() && BO->isCompoundAssignmentOp()) { if (undoLog) undoLog->try_emplace(VD, varMap[VD]); varMap[VD] = AllocType::Unknown; @@ -1870,6 +1870,20 @@ struct AllocationTraverser : RecursiveASTVisitor { return AllocType::New; } + static bool isNullOpt(const clang::CXXConstructExpr* CCE) { + // Expected: std/cpp::optional constructor + const clang::CXXConstructorDecl* CCD = CCE->getConstructor(); + // optional(nullopt_t) noexcept; -> one arg + if (CCD->getNumParams() != 1) + return false; + clang::QualType ParamTy = + CCD->getParamDecl(0)->getType().getNonReferenceType(); + // FIXME: Copy constructor of optional is not handled + if (const auto* CRD = ParamTy->getAsCXXRecordDecl()) + return CRD->getName() == "nullopt_t"; + return false; + } + std::optional handleExpr(const clang::Expr* expr) { const clang::Expr* finExpr = expr->IgnoreParenCasts(); // Case: return new __type__ @@ -1888,10 +1902,20 @@ struct AllocationTraverser : RecursiveASTVisitor { } // Case: malloc or another func call - if (const auto* CE = dyn_cast(finExpr)) + if (const auto* CE = dyn_cast(finExpr)) { + // Case: std::optional ptr = new int; return *ptr; + if (const auto* COCE = dyn_cast(CE)) { + if (COCE->getOperator() == OverloadedOperatorKind::OO_Star && + !COCE->isInfixBinaryOp()) + return handleExpr(COCE->getArg(0)); + } return handleCall(CE); + } // Case: NULL, nullptr or (int*)(__integerLiteral__) + if (llvm::isa(finExpr) || + llvm::isa(finExpr)) + return AllocType::Null; const clang::Expr* parExpr = expr->IgnoreParens(); while (const auto* CE = dyn_cast(parExpr)) { if (CE->getCastKind() == CK_NullToPointer) @@ -1899,6 +1923,23 @@ struct AllocationTraverser : RecursiveASTVisitor { parExpr = CE->getSubExpr(); } + // Case: constructor cast + if (auto* CCE = dyn_cast(finExpr)) { + // std::nullopt or cpp::nullopt + if (isNullOpt(CCE)) + return AllocType::Null; + const clang::Expr* stripped = expr->IgnoreParens(); + // ExprWithCleanups -> ImplicitCastExpr -> CXXConstructExpr pattern + if (const auto* EWC = dyn_cast(stripped)) + stripped = EWC->getSubExpr(); + // We do not want to analyze explicit conversions + auto* ICE = dyn_cast(stripped); + bool isImplicitConv = + ICE && ICE->getCastKind() == CK_ConstructorConversion; + // ImplicitCastExpr -> CXXConstructExpr pattern + if (isImplicitConv) + return handleExpr(CCE->getArg(0)); + } return AllocType::None; } }; @@ -1908,8 +1949,8 @@ static std::optional AnalyzeAllocType(const clang::FunctionDecl* Fn, std::unordered_map>& visitedFuncs) { - const clang::QualType QT = Fn->getReturnType(); - if (!QT->isPointerType()) + QualType QT = Fn->getReturnType(); + if (!QT->isPointerType() && !QT->isRecordType()) return AllocType::None; const Stmt* fnBody = Fn->getBody(); if (!fnBody) diff --git a/unittests/CppInterOp/FunctionReflectionTest.cpp b/unittests/CppInterOp/FunctionReflectionTest.cpp index ed8a13a30..94f20d71b 100644 --- a/unittests/CppInterOp/FunctionReflectionTest.cpp +++ b/unittests/CppInterOp/FunctionReflectionTest.cpp @@ -833,6 +833,7 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetAllocType) { std::string code = R"( #include #include + #include int* func0(int n){ return new int(n); } @@ -1206,8 +1207,79 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetAllocType) { void* func58(){ return __builtin_operator_new(64); } void* func59(){int* m = (int*)0; return malloc(sizeof(int));} + template + std::optional func60_helper(){ + T* ptr = new T; + return ptr; + } + + std::optional func60(){ + return func60_helper(); + } + + std::optional func61(int n){ + if(n>0) + return std::nullopt; + return new int; + } + + struct Empty { Empty() {} }; + Empty func62() { + Empty e; + return e; + } + + struct Wrapper { int a, b; Wrapper(int x, int y) : a(x), b(y) {} }; + Wrapper func63() { + int a = 1, b = 2; + return Wrapper(a, b); + } + + Wrapper func64() { + int a = 1, b = 2; + return {a, b}; + } + + int* func65(){ + auto ptr = func61(10); + if(ptr) + return *ptr; + return nullptr; + } + + struct Box { + int* p; + Box(int* p) : p(p) {} + Box operator*(const Box& o) const { return Box(nullptr); } + }; + Box func66() { + Box a(new int); + Box b(nullptr); + return a * b; + } + + std::optional func67(){ + return nullptr; + } + + int* func68(){ + int tmp = 10; + int* m = (int*)tmp; + for(int i = 0; i < 10; m+=1){ + return m; + } + return m; + } + + std::optional func69() { + std::optional a = new int; + //FIXME: Copy constructor is called, which takes one arg but is not nullopt constructor + //so it returns none, look at isNullOpt(const clang::CXXConstructExpr* CCE) function + std::optional b = a; + return b; + } )"; - TestFixture::CreateInterpreter(); + TestFixture::CreateInterpreter({"-std=c++17"}); Interp->declare(code); #define TESTAC(N, EXP) \ @@ -1275,6 +1347,16 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_GetAllocType) { TESTAC(57, None); TESTAC(58, OperatorNew); TESTAC(59, Malloc); + TESTAC(60, New); + TESTAC(61, New); + TESTAC(62, None); + TESTAC(63, None); + TESTAC(64, None); + TESTAC(65, New); + TESTAC(66, None); + TESTAC(67, Null); + TESTAC(68, Unknown); + TESTAC(69, None); #undef TESTAC Cpp::DeleteInterpreter();