From 3bc2ed8b8b01a37f6f84ec3901871807e7a146ab Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 27 Feb 2026 00:25:21 +0900 Subject: [PATCH 01/26] feat(cli): filter human function listing in warnings-only mode --- src/app/AnalyzerApp.cpp | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/src/app/AnalyzerApp.cpp b/src/app/AnalyzerApp.cpp index 3d4a313..60dd1db 100644 --- a/src/app/AnalyzerApp.cpp +++ b/src/app/AnalyzerApp.cpp @@ -399,6 +399,33 @@ static AnalysisResult filterWarningsOnly(const AnalysisResult& result, const Ana return filtered; } +static AnalysisResult filterFunctionsWithDiagnostics(const AnalysisResult& result) +{ + AnalysisResult filtered; + filtered.config = result.config; + filtered.diagnostics = result.diagnostics; + + std::unordered_set warnedFunctions; + warnedFunctions.reserve(result.diagnostics.size()); + for (const auto& d : result.diagnostics) + { + if (!d.funcName.empty()) + warnedFunctions.insert(d.funcName); + } + + if (warnedFunctions.empty()) + return filtered; + + filtered.functions.reserve(result.functions.size()); + for (const auto& f : result.functions) + { + if (warnedFunctions.count(f.name) != 0) + filtered.functions.push_back(f); + } + + return filtered; +} + struct LoadedInputModule { std::string filename; @@ -970,10 +997,13 @@ static int emitHumanOutput(const std::vector& results, const Anal for (std::size_t r = 0; r < results.size(); ++r) { const auto& inputFilename = results[r].first; - const AnalysisResult result = + AnalysisResult result = (cfg.onlyFiles.empty() && cfg.onlyDirs.empty() && cfg.onlyFunctions.empty()) ? filterResult(results[r].second, cfg, normalizedFilters) : results[r].second; + result = filterWarningsOnly(result, cfg); + if (cfg.warningsOnly) + result = filterFunctionsWithDiagnostics(result); if (multiFile) { @@ -1029,8 +1059,6 @@ static int emitHumanOutput(const std::vector& results, const Anal { if (d.funcName != f.name) continue; - if (result.config.warningsOnly && d.severity == DiagnosticSeverity::Info) - continue; if (d.line != 0) llvm::outs() << "\tat line " << d.line << ", column " << d.column << "\n"; llvm::outs() << d.message << "\n"; @@ -1867,7 +1895,7 @@ namespace ctrace::stack::app RunResult runAnalyzerApp(cli::ParsedArguments parsedArgs, llvm::LLVMContext& context) { - AnalyzerApp app; + AnalyzerApp app = {}; AppResult runResult = app.run(std::move(parsedArgs), context); RunResult result; From 69f3b0c7a6c40ea3b02344e8ec75ed44e4ab8775 Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 27 Feb 2026 00:25:27 +0900 Subject: [PATCH 02/26] test(fixtures): add warnings-only function-listing fixture --- ...tialized-local-warnings-only-function-filter.c | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 test/uninitialized-variable/uninitialized-local-warnings-only-function-filter.c diff --git a/test/uninitialized-variable/uninitialized-local-warnings-only-function-filter.c b/test/uninitialized-variable/uninitialized-local-warnings-only-function-filter.c new file mode 100644 index 0000000..e2377ba --- /dev/null +++ b/test/uninitialized-variable/uninitialized-local-warnings-only-function-filter.c @@ -0,0 +1,15 @@ +static int read_uninitialized_value(void) +{ + int value; + return value; +} + +static int clean_value(void) +{ + return 7; +} + +int main(void) +{ + return read_uninitialized_value() + clean_value(); +} From 400ab55e4c33c83bcb94d497aa776dcd32426b45 Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 27 Feb 2026 00:25:31 +0900 Subject: [PATCH 03/26] test(regression): extend warnings-only and self-analysis checks --- run_test.py | 183 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 180 insertions(+), 3 deletions(-) diff --git a/run_test.py b/run_test.py index b7d13ff..394025f 100755 --- a/run_test.py +++ b/run_test.py @@ -989,6 +989,7 @@ def check_cli_parsing_and_filters() -> bool: ok = True sample = RUN_CONFIG.test_dir / "false-positif/unique_ptr_state.cpp" + sample_warning = RUN_CONFIG.test_dir / "uninitialized-variable/uninitialized-local-basic.c" sample_c = RUN_CONFIG.test_dir / "alloca/oversized-constant.c" resource_model = Path("models/resource-lifetime/generic.txt") escape_model = Path("models/stack-escape/generic.txt") @@ -1171,7 +1172,12 @@ def run_success_case(label: str, args: list[str], required: Optional[list[str]] ("--resource-summary-cache-dir space", [str(sample), "--resource-summary-cache-dir", str(resource_cache), "--only-function=transition"], ["Function:"], "text"), ("--resource-summary-cache-dir equals", [str(sample), f"--resource-summary-cache-dir={resource_cache}", "--only-function=transition"], ["Function:"], "text"), ("--resource-summary-cache-memory-only", [str(sample), "--resource-summary-cache-memory-only", "--only-function=transition"], ["Function:"], "text"), - ("--warnings-only", [str(sample), "--warnings-only", "--only-function=transition"], ["Function:"], "text"), + ( + "--warnings-only", + [str(sample_warning), "--warnings-only"], + ["Function: read_uninitialized_basic"], + "text", + ), ("--format=json", [str(sample), "--format=json"], [], "json"), ("--format=sarif", [str(sample), "--format=sarif"], [], "sarif"), ("--format=human", [str(sample), "--format=human", "--only-function=transition"], ["Function:"], "text"), @@ -1235,6 +1241,170 @@ def check_only_func_uninitialized() -> bool: return True +def check_warnings_only_filters_function_listing() -> bool: + """ + Regression: --warnings-only must only list functions that carry warnings/errors. + """ + print("=== Testing --warnings-only function listing filter ===") + sample = ( + RUN_CONFIG.test_dir + / "uninitialized-variable/uninitialized-local-warnings-only-function-filter.c" + ) + result = run_analyzer([str(sample), "--warnings-only"]) + output = (result.stdout or "") + (result.stderr or "") + + if result.returncode != 0: + print(f" ❌ analyzer failed (code {result.returncode})") + print(output) + print() + return False + + required = [ + "Function: read_uninitialized_value", + "potential read of uninitialized local variable 'value'", + ] + forbidden = [ + "Function: clean_value", + "Function: main", + ] + + for needle in required: + if needle not in output: + print(f" ❌ missing expected output: {needle}") + print(output) + print() + return False + + for needle in forbidden: + if needle in output: + print(f" ❌ unexpected function listed in --warnings-only output: {needle}") + print(output) + print() + return False + + print(" ✅ --warnings-only function listing filter OK\n") + return True + + +def check_uninitialized_verbose_ctor_trace() -> bool: + """ + Regression: --verbose must expose whether default-constructor evidence was + detected (at constructor mark time and/or never-init triage). + """ + print("=== Testing verbose constructor detection trace ===") + cases = [ + ( + "default ctor detected", + RUN_CONFIG.test_dir / "uninitialized-variable/uninitialized-local-opaque-ctor.cpp", + [ + "[uninit][ctor]", + "local=obj", + "default_ctor_detected=yes", + "action=mark_default_ctor", + ], + ), + ( + "default ctor not detected", + RUN_CONFIG.test_dir / "uninitialized-variable/uninitialized-local-cpp-trivial-ctor.cpp", + [ + "[uninit][ctor]", + "local=app", + "default_ctor_detected=no", + "action=suppress_never_initialized", + ], + ), + ] + + for label, fixture, needles in cases: + result = run_analyzer([str(fixture), "--verbose", "--warnings-only"]) + output = (result.stdout or "") + (result.stderr or "") + if result.returncode != 0: + print(f" ❌ {label} failed (code {result.returncode})") + print(output) + print() + return False + for needle in needles: + if needle not in output: + print(f" ❌ {label}: missing expected verbose trace token: {needle}") + print(output) + print() + return False + + print(" ✅ verbose ctor trace OK\n") + return True + + +def check_uninitialized_unsummarized_defined_bool_out_param() -> bool: + """ + Regression: defined-but-unsummarized bool/status calls guarded by return-value + control flow must mark out-param writes (self-analysis case). + """ + print("=== Testing unsummarized defined bool out-param fallback ===") + sample = Path("src/analysis/SizeMinusKWrites.cpp") + compdb = Path("build/compile_commands.json") + + if not sample.exists(): + print(f" [info] sample not found, skipping: {sample}\n") + return True + if not compdb.exists(): + print(f" [info] compile_commands not found, skipping: {compdb}\n") + return True + + result = run_analyzer( + [str(sample), f"--compile-commands={compdb}", "--warnings-only"] + ) + output = (result.stdout or "") + (result.stderr or "") + if result.returncode != 0: + print(f" ❌ analyzer failed (code {result.returncode})") + print(output) + print() + return False + + forbidden = "potential read of uninitialized local variable 'lf'" + if forbidden in output: + print(f" ❌ unexpected warning still present: {forbidden}") + print(output) + print() + return False + + print(" ✅ unsummarized defined bool out-param fallback OK\n") + return True + + +def check_uninitialized_optional_receiver_index_repro() -> bool: + """ + Reproducer: optional receiver-index tracking passed by value can trigger + a false positive on local initialization. + """ + print("=== Testing optional receiver index false-positive reproducer ===") + sample = ( + RUN_CONFIG.test_dir + / "uninitialized-variable/uninitialized-local-cpp-optional-receiver-index.cpp" + ) + + if not sample.exists(): + print(f" [info] sample not found, skipping: {sample}\n") + return True + + result = run_analyzer([str(sample), "--warnings-only"]) + output = (result.stdout or "") + (result.stderr or "") + if result.returncode != 0: + print(f" ❌ analyzer failed (code {result.returncode})") + print(output) + print() + return False + + expected = "potential read of uninitialized local variable 'methodReceiverIdx'" + if expected not in output: + print(f" ❌ expected warning not found: {expected}") + print(output) + print() + return False + + print(" ✅ optional receiver index false-positive reproduced\n") + return True + + def check_unknown_alloca_virtual_callback_escape() -> bool: """ Regression: unknown-origin unnamed allocas must not be silently treated as @@ -1647,7 +1817,7 @@ def check_compdb_as_default_input_source() -> bool: ] compdb.write_text(json.dumps(entries), encoding="utf-8") - result = run_analyzer([f"--compile-commands={compdb}", "--warnings-only"]) + result = run_analyzer([f"--compile-commands={compdb}"]) output = (result.stdout or "") + (result.stderr or "") if result.returncode != 0: print(f" ❌ default compdb input run failed (code {result.returncode})") @@ -1722,7 +1892,6 @@ def check_exclude_dir_filter() -> bool: [ f"--compile-commands={compdb}", f"--exclude-dir={skip_dir.parent},{tmpdir / 'does-not-exist'}", - "--warnings-only", ] ) output = (result.stdout or "") + (result.stderr or "") @@ -2066,6 +2235,14 @@ def record_ok(ok: bool): global_ok = False if not record_ok(check_only_func_uninitialized()): global_ok = False + if not record_ok(check_warnings_only_filters_function_listing()): + global_ok = False + if not record_ok(check_uninitialized_verbose_ctor_trace()): + global_ok = False + if not record_ok(check_uninitialized_unsummarized_defined_bool_out_param()): + global_ok = False + if not record_ok(check_uninitialized_optional_receiver_index_repro()): + global_ok = False if not record_ok(check_unknown_alloca_virtual_callback_escape()): global_ok = False if not record_ok(check_compdb_as_default_input_source()): From 94b754e3fd6c98bc9c7ad7f10d80938b25e4b4ee Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 27 Feb 2026 00:25:39 +0900 Subject: [PATCH 04/26] fix(analysis): reduce false positives in invalid base reconstruction --- src/analysis/InvalidBaseReconstruction.cpp | 177 ++++++++++++++++++--- 1 file changed, 151 insertions(+), 26 deletions(-) diff --git a/src/analysis/InvalidBaseReconstruction.cpp b/src/analysis/InvalidBaseReconstruction.cpp index 278c5d8..4751d27 100644 --- a/src/analysis/InvalidBaseReconstruction.cpp +++ b/src/analysis/InvalidBaseReconstruction.cpp @@ -1,6 +1,7 @@ #include "analysis/InvalidBaseReconstruction.hpp" #include +#include #include #include #include @@ -185,7 +186,7 @@ namespace ctrace::stack::analysis } break; } - return Cur ? Cur : V; + return Cur; } static const llvm::Value* getPtrToIntOperand(const llvm::Value* V) @@ -762,37 +763,160 @@ namespace ctrace::stack::analysis return originMember.value() == resultMember.value(); } - static bool isMemberProjectionWithMatchingPointeeType( - int64_t originOffset, int64_t resultOffset, const llvm::StructType* structType, - uint64_t allocaSize, const llvm::DataLayout& DL, const llvm::Type* sourceElementType) + struct ProjectionBounds { - if (!sourceElementType) + uint64_t begin = 0; + uint64_t end = 0; + }; + + static bool findProjectionBoundsAtOffset(const llvm::Type* currentType, + const llvm::Type* sourceElementType, + const llvm::DataLayout& DL, uint64_t baseOffset, + uint64_t queryOffset, ProjectionBounds& out, + unsigned depth = 0) + { + using namespace llvm; + + if (!currentType || !sourceElementType || depth > 12) return false; - if (originOffset < 0 || resultOffset < 0) + + const TypeSize currentAllocSize = DL.getTypeAllocSize(const_cast(currentType)); + if (currentAllocSize.isScalable()) return false; - if (!structType) + const uint64_t currentSize = currentAllocSize.getFixedValue(); + if (baseOffset > std::numeric_limits::max() - currentSize) return false; - uint64_t uOrigin = static_cast(originOffset); - if (uOrigin >= allocaSize) + const uint64_t currentEnd = baseOffset + currentSize; + if (queryOffset < baseOffset || queryOffset >= currentEnd) return false; - if (!isOffsetWithinSameAllocaMember(originOffset, resultOffset, structType, allocaSize, - DL)) + + if (currentType == sourceElementType && queryOffset == baseOffset) { - return false; + out.begin = baseOffset; + out.end = currentEnd; + return true; } - auto memberIdx = getStructMemberIndexAtOffset(structType, DL, uOrigin); - if (!memberIdx.has_value()) + if (const auto* arrayTy = dyn_cast(currentType)) + { + Type* elemTy = arrayTy->getElementType(); + const TypeSize elemAllocSize = DL.getTypeAllocSize(elemTy); + if (elemAllocSize.isScalable()) + return false; + const uint64_t elemSize = elemAllocSize.getFixedValue(); + if (elemSize == 0) + return false; + + const uint64_t elemCount = arrayTy->getNumElements(); + if (elemSize > 0 && elemCount > std::numeric_limits::max() / elemSize) + return false; + const uint64_t arraySpan = elemSize * elemCount; + if (queryOffset < baseOffset || queryOffset >= baseOffset + arraySpan) + return false; + + const uint64_t rel = queryOffset - baseOffset; + if (elemTy == sourceElementType && (rel % elemSize) == 0) + { + out.begin = baseOffset; + out.end = baseOffset + arraySpan; + return true; + } + + const uint64_t elemIdx = rel / elemSize; + if (elemIdx >= elemCount) + return false; + const uint64_t elemBase = baseOffset + elemIdx * elemSize; + return findProjectionBoundsAtOffset(elemTy, sourceElementType, DL, elemBase, + queryOffset, out, depth + 1); + } + + const auto* structTy = dyn_cast(currentType); + if (!structTy) return false; - llvm::Type* memberType = structType->getElementType(memberIdx.value()); - if (memberType == sourceElementType) - return true; - if (auto* memberArray = llvm::dyn_cast(memberType)) - return memberArray->getElementType() == sourceElementType; + auto* mutableStructTy = const_cast(structTy); + const StructLayout* layout = DL.getStructLayout(mutableStructTy); + const unsigned memberCount = structTy->getNumElements(); + for (unsigned i = 0; i < memberCount; ++i) + { + Type* memberTy = structTy->getElementType(i); + const TypeSize memberAllocSize = DL.getTypeAllocSize(memberTy); + if (memberAllocSize.isScalable()) + continue; + + const uint64_t memberSize = memberAllocSize.getFixedValue(); + const uint64_t memberOffset = layout->getElementOffset(i); + if (baseOffset > std::numeric_limits::max() - memberOffset) + continue; + const uint64_t memberBase = baseOffset + memberOffset; + if (memberBase > std::numeric_limits::max() - memberSize) + continue; + const uint64_t memberEnd = memberBase + memberSize; + if (queryOffset < memberBase || queryOffset >= memberEnd) + continue; + + if (memberTy == sourceElementType && queryOffset == memberBase) + { + out.begin = memberBase; + out.end = memberEnd; + return true; + } + + if (const auto* memberArray = dyn_cast(memberTy)) + { + Type* elemTy = memberArray->getElementType(); + const TypeSize elemAllocSize = DL.getTypeAllocSize(elemTy); + if (!elemAllocSize.isScalable() && elemTy == sourceElementType) + { + const uint64_t elemSize = elemAllocSize.getFixedValue(); + if (elemSize > 0) + { + const uint64_t rel = queryOffset - memberBase; + if (rel < memberSize && (rel % elemSize) == 0) + { + out.begin = memberBase; + out.end = memberEnd; + return true; + } + } + } + } + + if (findProjectionBoundsAtOffset(memberTy, sourceElementType, DL, memberBase, + queryOffset, out, depth + 1)) + { + return true; + } + } + return false; } + static bool isProjectionWithinSourceSubobjectBounds( + int64_t originOffset, int64_t resultOffset, const llvm::Type* allocatedType, + uint64_t allocaSize, const llvm::DataLayout& DL, const llvm::Type* sourceElementType) + { + if (!sourceElementType || !allocatedType) + return false; + if (originOffset < 0 || resultOffset < 0) + return false; + + const uint64_t uOrigin = static_cast(originOffset); + const uint64_t uResult = static_cast(resultOffset); + if (uOrigin >= allocaSize || uResult >= allocaSize) + return false; + + ProjectionBounds bounds; + if (!findProjectionBoundsAtOffset(allocatedType, sourceElementType, DL, 0, uOrigin, + bounds)) + { + return false; + } + if (bounds.begin > bounds.end || bounds.end > allocaSize) + return false; + return uResult >= bounds.begin && uResult < bounds.end; + } + static void analyzeInvalidBaseReconstructionsInFunction( llvm::Function& F, const llvm::DataLayout& DL, std::vector& out) @@ -808,6 +932,7 @@ namespace ctrace::stack::analysis std::string name; uint64_t size = 0; const StructType* structType = nullptr; + const Type* allocatedType = nullptr; }; std::map allocaInfo; @@ -829,6 +954,7 @@ namespace ctrace::stack::analysis info.name = std::move(varName); info.size = sizeOpt.value(); info.structType = getAllocaStructType(AI); + info.allocatedType = AI->getAllocatedType(); allocaInfo[AI] = std::move(info); } } @@ -993,17 +1119,16 @@ namespace ctrace::stack::analysis const std::string& varName = it->second.name; uint64_t allocaSize = it->second.size; - const StructType* structType = it->second.structType; + const Type* allocatedType = it->second.allocatedType; int64_t resultOffset = origin.offset + gepOffset; bool isOutOfBounds = (resultOffset < 0) || (static_cast(resultOffset) >= allocaSize); - bool isMemberOffset = isOffsetWithinSameAllocaMember( - origin.offset, resultOffset, structType, allocaSize, DL); - bool allowMemberSuppression = isMemberProjectionWithMatchingPointeeType( - origin.offset, resultOffset, structType, allocaSize, DL, - sourceElementType); + bool allowMemberSuppression = + isProjectionWithinSourceSubobjectBounds( + origin.offset, resultOffset, allocatedType, allocaSize, DL, + sourceElementType); std::string targetType; Type* targetTy = GEP->getType(); @@ -1013,7 +1138,7 @@ namespace ctrace::stack::analysis auto& entry = agg[origin.alloca]; entry.memberOffsets.insert(origin.offset); entry.anyOutOfBounds |= isOutOfBounds; - if (resultOffset != 0 && !(allowMemberSuppression && isMemberOffset)) + if (resultOffset != 0 && !allowMemberSuppression) entry.anyNonZeroResult = true; entry.varName = varName; entry.targetType = targetType; From 579c830b3ce0a2d1e0aeec2c1e3a74f83bb7777f Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 27 Feb 2026 00:25:44 +0900 Subject: [PATCH 05/26] refactor(escape): expose local-strategy helper in internal API --- src/analysis/StackPointerEscapeInternal.hpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/analysis/StackPointerEscapeInternal.hpp b/src/analysis/StackPointerEscapeInternal.hpp index 7a1b438..50bb140 100644 --- a/src/analysis/StackPointerEscapeInternal.hpp +++ b/src/analysis/StackPointerEscapeInternal.hpp @@ -91,8 +91,13 @@ namespace ctrace::stack::analysis const std::vector& candidatesForCall(const llvm::CallBase& CB) const; private: + using CandidatesByVTableSlot = + std::unordered_map>; + std::unordered_map> candidatesByFunctionType; + std::unordered_map + virtualCandidatesByFunctionTypeAndSlot; std::vector empty; }; From 8964c2765e39744925a851ce538e28369cf7c5bb Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 27 Feb 2026 00:25:52 +0900 Subject: [PATCH 06/26] fix(escape): treat local strategy ownership as non-escaping --- src/analysis/StackPointerEscapeModel.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/analysis/StackPointerEscapeModel.cpp b/src/analysis/StackPointerEscapeModel.cpp index 406f04d..a722c19 100644 --- a/src/analysis/StackPointerEscapeModel.cpp +++ b/src/analysis/StackPointerEscapeModel.cpp @@ -158,7 +158,8 @@ namespace ctrace::stack::analysis { return CB.paramHasAttr(argIndex, llvm::Attribute::NoCapture) || CB.paramHasAttr(argIndex, llvm::Attribute::ByVal) || - CB.paramHasAttr(argIndex, llvm::Attribute::ByRef); + CB.paramHasAttr(argIndex, llvm::Attribute::ByRef) || + CB.paramHasAttr(argIndex, llvm::Attribute::StructRet); } bool isStdLibCallee(const llvm::Function* F) From 9b26129807a58fcc9197868eb1bff2ebb93b8840 Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 27 Feb 2026 00:25:59 +0900 Subject: [PATCH 07/26] feat(escape): add local strategy target resolution --- src/analysis/StackPointerEscapeResolver.cpp | 111 ++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/src/analysis/StackPointerEscapeResolver.cpp b/src/analysis/StackPointerEscapeResolver.cpp index c11a796..76d6a02 100644 --- a/src/analysis/StackPointerEscapeResolver.cpp +++ b/src/analysis/StackPointerEscapeResolver.cpp @@ -2,13 +2,26 @@ #include #include +#include #include #include +#include +#include + namespace ctrace::stack::analysis { namespace { + static void appendUniqueFunction(std::vector& out, + const llvm::Function* F) + { + if (!F) + return; + if (std::find(out.begin(), out.end(), F) == out.end()) + out.push_back(F); + } + static bool isLikelyItaniumCppMethodName(llvm::StringRef name) { if (!name.empty() && name.front() == '\1') @@ -32,6 +45,63 @@ namespace ctrace::stack::analysis } return true; } + + static void collectVTableEntries(const llvm::Constant* node, + std::vector& out) + { + if (!node) + return; + + if (const auto* array = llvm::dyn_cast(node)) + { + for (const llvm::Use& operand : array->operands()) + collectVTableEntries(llvm::dyn_cast(operand.get()), out); + return; + } + if (const auto* strct = llvm::dyn_cast(node)) + { + for (const llvm::Use& operand : strct->operands()) + collectVTableEntries(llvm::dyn_cast(operand.get()), out); + return; + } + if (node->getType()->isPointerTy()) + out.push_back(node); + } + + static const llvm::Function* functionFromVTableEntry(const llvm::Constant* entry) + { + if (!entry) + return nullptr; + return llvm::dyn_cast(entry->stripPointerCasts()); + } + + static std::optional extractVirtualDispatchSlotIndex(const llvm::CallBase& CB) + { + const llvm::Value* calledVal = CB.getCalledOperand(); + const auto* fnLoad = + calledVal ? llvm::dyn_cast(calledVal->stripPointerCasts()) : nullptr; + if (!fnLoad) + return std::nullopt; + + const auto* slotGEP = llvm::dyn_cast( + fnLoad->getPointerOperand()->stripPointerCasts()); + if (!slotGEP || slotGEP->getNumOperands() < 2) + return std::nullopt; + + const auto* vtableLoad = + llvm::dyn_cast(slotGEP->getPointerOperand()->stripPointerCasts()); + if (!vtableLoad) + return std::nullopt; + if (!isLikelyVPtrLoadFromObjectRoot(*vtableLoad)) + return std::nullopt; + + const auto* slotIndex = + llvm::dyn_cast(slotGEP->getOperand(slotGEP->getNumOperands() - 1)); + if (!slotIndex || slotIndex->isNegative()) + return std::nullopt; + + return static_cast(slotIndex->getZExtValue()); + } } // namespace IndirectTargetResolver::IndirectTargetResolver(const llvm::Module& module) @@ -45,6 +115,35 @@ namespace ctrace::stack::analysis candidatesByFunctionType[candidate.getFunctionType()].push_back(&candidate); } + + for (const llvm::GlobalVariable& global : module.globals()) + { + if (!global.hasInitializer()) + continue; + + llvm::StringRef name = global.getName(); + if (!name.empty() && name.front() == '\1') + name = name.drop_front(); + if (!name.starts_with("_ZTV")) + continue; + + std::vector entries; + collectVTableEntries(global.getInitializer(), entries); + if (entries.size() <= 2) + continue; + + for (std::size_t i = 2; i < entries.size(); ++i) + { + const llvm::Function* target = functionFromVTableEntry(entries[i]); + if (!target || target->isDeclaration()) + continue; + + const unsigned slotIndex = static_cast(i - 2); + std::vector& perSlot = + virtualCandidatesByFunctionTypeAndSlot[target->getFunctionType()][slotIndex]; + appendUniqueFunction(perSlot, target); + } + } } const std::vector& @@ -53,6 +152,18 @@ namespace ctrace::stack::analysis const llvm::FunctionType* calleeTy = CB.getFunctionType(); if (!calleeTy) return empty; + + if (const std::optional slotIndex = extractVirtualDispatchSlotIndex(CB)) + { + auto byType = virtualCandidatesByFunctionTypeAndSlot.find(calleeTy); + if (byType != virtualCandidatesByFunctionTypeAndSlot.end()) + { + auto bySlot = byType->second.find(*slotIndex); + if (bySlot != byType->second.end() && !bySlot->second.empty()) + return bySlot->second; + } + } + auto it = candidatesByFunctionType.find(calleeTy); if (it == candidatesByFunctionType.end()) return empty; From 94e4b1e6efe298a658773ec547d2f851f514a30a Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 27 Feb 2026 00:26:05 +0900 Subject: [PATCH 08/26] fix(escape): suppress false escapes for local strategy usage --- src/analysis/StackPointerEscape.cpp | 110 ++++++++++++++++++++++++---- 1 file changed, 97 insertions(+), 13 deletions(-) diff --git a/src/analysis/StackPointerEscape.cpp b/src/analysis/StackPointerEscape.cpp index 6483565..43679ce 100644 --- a/src/analysis/StackPointerEscape.cpp +++ b/src/analysis/StackPointerEscape.cpp @@ -22,6 +22,9 @@ namespace ctrace::stack::analysis { namespace { + using FunctionArgHardEscapeMap = + std::unordered_map>; + struct DeferredCallback { StackPointerEscapeIssue issue; @@ -235,6 +238,20 @@ namespace ctrace::stack::analysis return summaryStateForArg(summaries, callee, argIndex) == EscapeSummaryState::NoEscape; } + static bool summaryHasLocalHardEscape(const FunctionArgHardEscapeMap& hardEscapes, + const llvm::Function* callee, unsigned argIndex) + { + if (!callee) + return false; + const auto it = hardEscapes.find(callee); + if (it == hardEscapes.end()) + return false; + const std::vector& perArg = it->second; + if (argIndex >= perArg.size()) + return false; + return perArg[argIndex]; + } + static bool isOpaqueCalleeForEscapeReasoning(const FunctionEscapeSummaryMap& summaries, const llvm::Function* callee) { @@ -341,6 +358,7 @@ namespace ctrace::stack::analysis SmallPtrSet visited; SmallVector worklist; + SmallPtrSet localSlotsContainingTrackedAddr; worklist.push_back(&arg); while (!worklist.empty()) @@ -370,6 +388,7 @@ namespace ctrace::stack::analysis { if (dstAI->getFunction() == &F) { + localSlotsContainingTrackedAddr.insert(dstAI); worklist.push_back(dstAI); continue; } @@ -383,7 +402,19 @@ namespace ctrace::stack::analysis { if (LI->getPointerOperand()->stripPointerCasts() != V) continue; - if (LI->getType()->isPointerTy()) + + bool shouldPropagateLoadedPointer = true; + if (const AllocaInst* srcAI = + getUnderlyingAlloca(LI->getPointerOperand(), returnedArgAliases)) + { + if (srcAI->getFunction() == &F && + !localSlotsContainingTrackedAddr.contains(srcAI)) + { + shouldPropagateLoadedPointer = false; + } + } + + if (shouldPropagateLoadedPointer && LI->getType()->isPointerTy()) worklist.push_back(LI); continue; } @@ -437,7 +468,12 @@ namespace ctrace::stack::analysis if (!isLikelyVirtualDispatchCall(*CB)) { - facts.hardEscape = true; + // Unknown non-virtual callback target reached through a + // parameter: keep the summary conservative (Unknown) instead of + // forcing a hard escape. Strong diagnostics are still emitted at + // the originating callsite when we see the local address passed to + // an unresolved callback directly. + facts.hasOpaqueExternalCall = true; continue; } @@ -469,10 +505,17 @@ namespace ctrace::stack::analysis } } - if (dep.hasUnknownTarget || dep.candidates.empty()) + if (dep.candidates.empty()) { facts.hardEscape = true; } + else if (dep.hasUnknownTarget) + { + // Keep this dependency conservative-but-unknown when at least one + // candidate is analyzable. We only promote to hard escape if no + // candidate can be reasoned about. + facts.hasOpaqueExternalCall = true; + } if (!dep.candidates.empty()) facts.indirectDeps.push_back(std::move(dep)); @@ -542,11 +585,26 @@ namespace ctrace::stack::analysis llvm::Module& mod, const std::function& shouldAnalyze, IndirectTargetResolver& targetResolver, const ReturnedPointerArgAliasMap& returnedArgAliases, const StackEscapeModel& model, - StackEscapeRuleMatcher& ruleMatcher) + StackEscapeRuleMatcher& ruleMatcher, FunctionArgHardEscapeMap* hardEscapesOut) { FunctionEscapeFactsMap factsMap = buildFunctionEscapeFacts( mod, shouldAnalyze, targetResolver, returnedArgAliases, model, ruleMatcher); + if (hardEscapesOut) + { + hardEscapesOut->clear(); + for (const auto& entry : factsMap) + { + const llvm::Function* F = entry.first; + const FunctionEscapeFacts& facts = entry.second; + std::vector perArg; + perArg.reserve(facts.perArg.size()); + for (const ParamEscapeFacts& paramFacts : facts.perArg) + perArg.push_back(paramFacts.hardEscape); + hardEscapesOut->emplace(F, std::move(perArg)); + } + } + FunctionEscapeSummaryMap summaries; for (const auto& entry : factsMap) { @@ -663,6 +721,7 @@ namespace ctrace::stack::analysis static void analyzeStackPointerEscapesInFunction( llvm::Function& F, const FunctionEscapeSummaryMap& summaries, + const FunctionArgHardEscapeMap& hardEscapesByArg, IndirectTargetResolver& targetResolver, const ReturnedPointerArgAliasMap& returnedArgAliases, const StackEscapeModel& model, StackEscapeRuleMatcher& ruleMatcher, std::vector& out) @@ -855,17 +914,40 @@ namespace ctrace::stack::analysis { const std::vector& candidates = targetResolver.candidatesForCall(*CB); - bool allCandidatesNoEscape = !candidates.empty(); + bool hasCandidate = false; + bool hasMayEscapeCandidate = false; for (const Function* candidate : candidates) { - if (!summarySaysNoEscape(summaries, candidate, - argIndex)) + hasCandidate = true; + if (!candidate) + continue; + if (argIndex >= candidate->arg_size()) + continue; + if (ruleMatcher.modelSaysNoEscapeArg(model, candidate, + argIndex)) + continue; + if (isStdLibCallee(candidate)) + continue; + const EscapeSummaryState candidateState = + summaryStateForArg(summaries, candidate, + argIndex); + if (candidateState == EscapeSummaryState::MayEscape) { - allCandidatesNoEscape = false; - break; + if (summaryHasLocalHardEscape( + hardEscapesByArg, candidate, argIndex)) + { + hasMayEscapeCandidate = true; + break; + } } } - if (allCandidatesNoEscape) + + // For virtual dispatch, only emit a strong callback + // escape when at least one target summary proves + // potential capture. Unknown candidates are kept + // non-diagnostic to avoid broad type-based false + // positives. + if (hasCandidate && !hasMayEscapeCandidate) continue; } @@ -963,8 +1045,10 @@ namespace ctrace::stack::analysis IndirectTargetResolver targetResolver(mod); StackEscapeRuleMatcher ruleMatcher; const ReturnedPointerArgAliasMap returnedArgAliases = buildReturnedPointerArgAliases(mod); + FunctionArgHardEscapeMap hardEscapesByArg; const FunctionEscapeSummaryMap summaries = buildFunctionEscapeSummaries( - mod, shouldAnalyze, targetResolver, returnedArgAliases, model, ruleMatcher); + mod, shouldAnalyze, targetResolver, returnedArgAliases, model, ruleMatcher, + &hardEscapesByArg); for (llvm::Function& F : mod) { @@ -972,8 +1056,8 @@ namespace ctrace::stack::analysis continue; if (!shouldAnalyze(F)) continue; - analyzeStackPointerEscapesInFunction(F, summaries, targetResolver, returnedArgAliases, - model, ruleMatcher, issues); + analyzeStackPointerEscapesInFunction(F, summaries, hardEscapesByArg, targetResolver, + returnedArgAliases, model, ruleMatcher, issues); } return issues; } From af0a1cee29c9d943756912031e4be7b052ca5f07 Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 27 Feb 2026 00:26:13 +0900 Subject: [PATCH 09/26] test(fixtures): add local strategy no-escape regression case --- .../virtual-strategy-local-no-escape.cpp | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 test/escape-stack/virtual-strategy-local-no-escape.cpp diff --git a/test/escape-stack/virtual-strategy-local-no-escape.cpp b/test/escape-stack/virtual-strategy-local-no-escape.cpp new file mode 100644 index 0000000..bdd45c6 --- /dev/null +++ b/test/escape-stack/virtual-strategy-local-no-escape.cpp @@ -0,0 +1,130 @@ +#include +#include +#include + +struct AppStatus +{ + std::string error; + bool ok = true; + + bool isOk() const + { + return ok; + } +}; + +struct AnalysisEntry +{ + int value = 0; +}; + +struct RunPlan +{ + int mode = 0; +}; + +static AppStatus runShared(const RunPlan& plan, std::vector& results) +{ + results.push_back({plan.mode}); + return {}; +} + +static AppStatus runDirect(const RunPlan& plan, std::vector& results) +{ + if (plan.mode == 0) + results.push_back({1}); + return {}; +} + +class AnalysisExecutionStrategy +{ + public: + virtual ~AnalysisExecutionStrategy() = default; + virtual AppStatus execute(RunPlan& plan, std::vector& results) const = 0; +}; + +class SharedExecutionStrategy final : public AnalysisExecutionStrategy +{ + public: + AppStatus execute(RunPlan& plan, std::vector& results) const override + { + return runShared(plan, results); + } +}; + +class DirectExecutionStrategy final : public AnalysisExecutionStrategy +{ + public: + AppStatus execute(RunPlan& plan, std::vector& results) const override + { + return runDirect(plan, results); + } +}; + +class OutputStrategy +{ + public: + virtual ~OutputStrategy() = default; + virtual int emit(const RunPlan& plan, const std::vector& results) const = 0; +}; + +class JsonOutputStrategy final : public OutputStrategy +{ + public: + int emit(const RunPlan& plan, const std::vector& results) const override + { + return static_cast(results.size()) + plan.mode; + } +}; + +class HumanOutputStrategy final : public OutputStrategy +{ + public: + int emit(const RunPlan&, const std::vector& results) const override + { + return static_cast(results.size()); + } +}; + +static std::unique_ptr makeExecutionStrategy(const RunPlan& plan) +{ + if (plan.mode != 0) + return std::make_unique(); + return std::make_unique(); +} + +static std::unique_ptr makeOutputStrategy(int mode) +{ + if (mode != 0) + return std::make_unique(); + return std::make_unique(); +} + +class StrategyRunner +{ + public: + int run(int mode) const + { + RunPlan plan = {}; + plan.mode = mode; + + std::vector results; + std::unique_ptr executionStrategy = makeExecutionStrategy(plan); + AppStatus executionStatus = executionStrategy->execute(plan, results); + if (!executionStatus.isOk()) + return 1; + + std::unique_ptr outputStrategy = makeOutputStrategy(plan.mode); + return outputStrategy->emit(plan, results); + } +}; + +int runWithStrategies(int mode) +{ + StrategyRunner runner; + return runner.run(mode); +} + +// not contains: stack pointer escape: address of variable 'plan' escapes this function +// not contains: stack pointer escape: address of variable 'results' escapes this function +// not contains: stack pointer escape: address of variable 'executionStatus' escapes this function From ee6365bc24b0b1aa9a851726b786ba59fd1ad77e Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 27 Feb 2026 00:26:25 +0900 Subject: [PATCH 10/26] feat(uninitialized): improve ctor and out-param initialization reasoning --- src/analysis/UninitializedVarAnalysis.cpp | 610 +++++++++++++++++++++- 1 file changed, 584 insertions(+), 26 deletions(-) diff --git a/src/analysis/UninitializedVarAnalysis.cpp b/src/analysis/UninitializedVarAnalysis.cpp index ed68a31..a86434f 100644 --- a/src/analysis/UninitializedVarAnalysis.cpp +++ b/src/analysis/UninitializedVarAnalysis.cpp @@ -19,17 +19,21 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include #include #include +#include + #include "analysis/AnalyzerUtils.hpp" #include "analysis/IRValueUtils.hpp" @@ -48,6 +52,8 @@ namespace ctrace::stack::analysis } }; + using RangeSet = std::vector; + enum class InitLatticeState { Uninit, @@ -67,6 +73,8 @@ namespace ctrace::stack::analysis const llvm::AllocaInst* alloca = nullptr; const llvm::Argument* param = nullptr; std::uint64_t sizeBytes = 0; // 0 means unknown upper bound. + RangeSet nonPaddingRanges; + bool hasNonPaddingLayout = false; }; struct TrackedObjectContext @@ -83,7 +91,6 @@ namespace ctrace::stack::analysis std::uint64_t end = 0; }; - using RangeSet = std::vector; using InitRangeState = std::vector; struct PointerSlotWriteEffect @@ -284,6 +291,230 @@ namespace ctrace::stack::analysis return true; } + static const llvm::DIType* stripDebugTypeSugar(const llvm::DIType* type) + { + const llvm::DIType* current = type; + while (const auto* derived = llvm::dyn_cast_or_null(current)) + { + switch (derived->getTag()) + { + case llvm::dwarf::DW_TAG_const_type: + case llvm::dwarf::DW_TAG_volatile_type: + case llvm::dwarf::DW_TAG_restrict_type: + case llvm::dwarf::DW_TAG_typedef: + case llvm::dwarf::DW_TAG_reference_type: + case llvm::dwarf::DW_TAG_rvalue_reference_type: + case llvm::dwarf::DW_TAG_atomic_type: + current = derived->getBaseType(); + continue; + default: + break; + } + break; + } + return current; + } + + static bool debugTypeHasDataMembers( + const llvm::DIType* type, unsigned depth, + llvm::SmallPtrSetImpl& visitedTypes) + { + if (!type || depth > 12) + return false; + type = stripDebugTypeSugar(type); + if (!type) + return false; + if (!visitedTypes.insert(type).second) + return false; + + const auto* composite = llvm::dyn_cast(type); + if (!composite) + return true; + + for (const llvm::Metadata* elem : composite->getElements()) + { + const auto* derived = llvm::dyn_cast(elem); + if (!derived) + continue; + + const unsigned tag = derived->getTag(); + if (tag == llvm::dwarf::DW_TAG_member) + return true; + if (tag == llvm::dwarf::DW_TAG_inheritance && + debugTypeHasDataMembers(derived->getBaseType(), depth + 1, visitedTypes)) + { + return true; + } + } + + return false; + } + + static const llvm::DIType* getAllocaDebugDeclaredType(const llvm::AllocaInst& AI) + { + auto* nonConstAI = const_cast(&AI); + for (llvm::DbgDeclareInst* ddi : llvm::findDbgDeclares(nonConstAI)) + { + const llvm::DILocalVariable* var = ddi ? ddi->getVariable() : nullptr; + if (var && var->getType()) + return var->getType(); + } + + for (llvm::DbgVariableRecord* dvr : llvm::findDVRDeclares(nonConstAI)) + { + const llvm::DILocalVariable* var = dvr ? dvr->getVariable() : nullptr; + if (var && var->getType()) + return var->getType(); + } + + llvm::SmallVector dbgUsers; + llvm::SmallVector dbgRecords; + llvm::findDbgUsers(dbgUsers, nonConstAI, &dbgRecords); + for (llvm::DbgVariableIntrinsic* dvi : dbgUsers) + { + const llvm::DILocalVariable* var = dvi ? dvi->getVariable() : nullptr; + if (var && var->getType()) + return var->getType(); + } + for (llvm::DbgVariableRecord* dvr : dbgRecords) + { + const llvm::DILocalVariable* var = dvr ? dvr->getVariable() : nullptr; + if (var && var->getType()) + return var->getType(); + } + + return nullptr; + } + + static bool isSingleByteDummyStructAlloca(const llvm::AllocaInst& AI, + const llvm::DataLayout& DL) + { + const auto* structTy = llvm::dyn_cast(AI.getAllocatedType()); + if (!structTy || structTy->getNumElements() != 1) + return false; + if (!structTy->getElementType(0)->isIntegerTy(8)) + return false; + return getAllocaSizeBytes(AI, DL) == 1; + } + + static bool shouldTreatAsDataLessDebugObject(const llvm::AllocaInst& AI, + const llvm::DataLayout& DL) + { + if (!isSingleByteDummyStructAlloca(AI, DL)) + return false; + + const llvm::DIType* declaredType = getAllocaDebugDeclaredType(AI); + if (!declaredType) + return false; + + llvm::SmallPtrSet visitedTypes; + return !debugTypeHasDataMembers(declaredType, 0, visitedTypes); + } + + static bool collectNonPaddingLeafRangesForType(const llvm::Type* ty, + const llvm::DataLayout& DL, + std::uint64_t baseOffset, unsigned depth, + RangeSet& out) + { + if (!ty || depth > 12) + return false; + + if (const auto* structTy = llvm::dyn_cast(ty)) + { + auto* mutableStructTy = const_cast(structTy); + const llvm::StructLayout* layout = DL.getStructLayout(mutableStructTy); + const unsigned memberCount = structTy->getNumElements(); + for (unsigned idx = 0; idx < memberCount; ++idx) + { + const std::uint64_t memberOffset = layout->getElementOffset(idx); + if (!collectNonPaddingLeafRangesForType( + structTy->getElementType(idx), DL, + saturatingAdd(baseOffset, memberOffset), depth + 1, out)) + { + return false; + } + } + return true; + } + + if (const auto* arrayTy = llvm::dyn_cast(ty)) + { + const std::uint64_t count = arrayTy->getNumElements(); + if (count > 256) + return false; + + const llvm::TypeSize elemAllocSize = DL.getTypeAllocSize(arrayTy->getElementType()); + if (elemAllocSize.isScalable()) + return false; + + const std::uint64_t elemStride = elemAllocSize.getFixedValue(); + for (std::uint64_t idx = 0; idx < count; ++idx) + { + if (!collectNonPaddingLeafRangesForType( + arrayTy->getElementType(), DL, + saturatingAdd(baseOffset, idx * elemStride), depth + 1, out)) + { + return false; + } + } + return true; + } + + const llvm::TypeSize storeSize = DL.getTypeStoreSize(const_cast(ty)); + if (storeSize.isScalable()) + return false; + + const std::uint64_t width = storeSize.getFixedValue(); + if (width == 0) + return true; + + addRange(out, baseOffset, saturatingAdd(baseOffset, width)); + return true; + } + + static bool computeAllocaNonPaddingRanges(const llvm::AllocaInst& AI, + const llvm::DataLayout& DL, RangeSet& out) + { + out.clear(); + const llvm::Type* allocatedTy = AI.getAllocatedType(); + if (!allocatedTy) + return false; + if (!collectNonPaddingLeafRangesForType(allocatedTy, DL, 0, 0, out)) + return false; + + if (shouldTreatAsDataLessDebugObject(AI, DL)) + out.clear(); + + return true; + } + + static bool isRangeCoveredRespectingNonPaddingLayout(const TrackedMemoryObject& obj, + const RangeSet& initialized, + std::uint64_t begin, + std::uint64_t end) + { + if (isRangeCovered(initialized, begin, end)) + return true; + + if (!obj.hasNonPaddingLayout) + { + return isRangeCoveredAllowingSmallInteriorGaps(initialized, begin, end); + } + + for (const ByteRange& relevant : obj.nonPaddingRanges) + { + const std::uint64_t clippedBegin = std::max(begin, relevant.begin); + const std::uint64_t clippedEnd = std::min(end, relevant.end); + if (clippedBegin >= clippedEnd) + continue; + + if (!isRangeCovered(initialized, clippedBegin, clippedEnd)) + return false; + } + + return true; + } + static InitLatticeState classifyInitState(const RangeSet& initialized, std::uint64_t totalSize) { @@ -660,7 +891,14 @@ namespace ctrace::stack::analysis continue; unsigned idx = static_cast(tracked.objects.size()); - tracked.objects.push_back({TrackedObjectKind::Alloca, AI, nullptr, sizeBytes}); + TrackedMemoryObject obj; + obj.kind = TrackedObjectKind::Alloca; + obj.alloca = AI; + obj.param = nullptr; + obj.sizeBytes = sizeBytes; + obj.hasNonPaddingLayout = computeAllocaNonPaddingRanges(*AI, DL, + obj.nonPaddingRanges); + tracked.objects.push_back(std::move(obj)); tracked.allocaIndex[AI] = idx; } } @@ -670,7 +908,8 @@ namespace ctrace::stack::analysis if (!arg.getType()->isPointerTy()) continue; unsigned idx = static_cast(tracked.objects.size()); - tracked.objects.push_back({TrackedObjectKind::PointerParam, nullptr, &arg, 0}); + tracked.objects.push_back( + {TrackedObjectKind::PointerParam, nullptr, &arg, 0, {}, false}); tracked.paramIndex[&arg] = idx; } } @@ -1115,6 +1354,153 @@ namespace ctrace::stack::analysis return symbol.contains("aSE"); } + static bool isLikelyCppMethodSymbol(llvm::StringRef symbol) + { + return symbol.starts_with("_ZN"); + } + + static bool getDebugObjectPointerArgIndex(const llvm::Function& callee, + unsigned& outObjectArgIdx) + { + const llvm::DISubprogram* SP = callee.getSubprogram(); + if (!SP) + return false; + + const auto* subroutineType = + llvm::dyn_cast_or_null(SP->getType()); + if (!subroutineType) + return false; + + const auto typeArray = subroutineType->getTypeArray(); + if (typeArray.size() < 2) + return false; // [0] return type, [1..] params + + bool hasDebugObjectParamIdx = false; + unsigned debugObjectParamIdx = 0; + for (unsigned i = 1; i < typeArray.size(); ++i) + { + const auto* paramType = llvm::dyn_cast_or_null(typeArray[i]); + if (!paramType) + continue; + if (!(paramType->getFlags() & llvm::DINode::FlagObjectPointer)) + continue; + debugObjectParamIdx = i - 1; + hasDebugObjectParamIdx = true; + break; + } + + if (!hasDebugObjectParamIdx) + return false; + + const unsigned irArgCount = static_cast(callee.arg_size()); + const unsigned debugParamCount = static_cast(typeArray.size() - 1); + if (irArgCount < debugParamCount) + return false; + + // Hidden ABI-only params (e.g. sret) are lowered in front of source-level params. + const unsigned hiddenPrefix = irArgCount - debugParamCount; + const unsigned objectArgIdx = hiddenPrefix + debugObjectParamIdx; + if (objectArgIdx >= irArgCount) + return false; + + outObjectArgIdx = objectArgIdx; + return true; + } + + static bool getLikelyCppMethodReceiverArgIndex(const llvm::Function& callee, + unsigned& outReceiverIdx) + { + if (getDebugObjectPointerArgIndex(callee, outReceiverIdx)) + return true; + + if (!isLikelyCppMethodSymbol(callee.getName())) + return false; + + unsigned receiverIdx = 0; + for (const llvm::Argument& arg : callee.args()) + { + if (!arg.hasStructRetAttr()) + break; + ++receiverIdx; + } + + if (receiverIdx >= static_cast(callee.arg_size())) + return false; + outReceiverIdx = receiverIdx; + return true; + } + + static bool isLikelyDefaultConstructorThisArg(const llvm::CallBase& CB, + const llvm::Function* callee, unsigned argIdx) + { + if (!callee || argIdx != 0) + return false; + if (!isLikelyCppConstructorSymbol(callee->getName())) + return false; + // Itanium ABI: default constructor call usually carries only the `this` pointer. + return CB.arg_size() == 1; + } + + static void markConstructedOnPointerOperand(const llvm::Value* ptrOperand, + const TrackedObjectContext& tracked, + const llvm::DataLayout& DL, + llvm::BitVector* constructedSeen) + { + if (!constructedSeen || !ptrOperand || !ptrOperand->getType()->isPointerTy()) + return; + + unsigned objectIdx = 0; + std::uint64_t baseOffset = 0; + bool hasConstOffset = false; + if (!resolveTrackedObjectBase(ptrOperand, tracked, DL, objectIdx, baseOffset, + hasConstOffset)) + { + return; + } + + if (objectIdx >= constructedSeen->size()) + return; + if (!isAllocaObject(tracked.objects[objectIdx])) + return; + + constructedSeen->set(objectIdx); + } + + static void markDefaultCtorOnPointerOperand(const llvm::Value* ptrOperand, + const TrackedObjectContext& tracked, + const llvm::DataLayout& DL, + llvm::BitVector* defaultCtorSeen) + { + if (!defaultCtorSeen || !ptrOperand || !ptrOperand->getType()->isPointerTy()) + return; + + unsigned objectIdx = 0; + std::uint64_t baseOffset = 0; + bool hasConstOffset = false; + if (!resolveTrackedObjectBase(ptrOperand, tracked, DL, objectIdx, baseOffset, + hasConstOffset)) + { + return; + } + + if (objectIdx >= defaultCtorSeen->size()) + return; + if (!isAllocaObject(tracked.objects[objectIdx])) + return; + + if (!defaultCtorSeen->test(objectIdx)) + { + const TrackedMemoryObject& obj = tracked.objects[objectIdx]; + const llvm::Function* F = obj.alloca ? obj.alloca->getFunction() : nullptr; + coretrace::log(coretrace::Level::Debug, + "[uninit][ctor] func={} local={} default_ctor_detected=yes " + "action=mark_default_ctor\n", + F ? F->getName().str() : std::string(""), + getTrackedObjectName(obj)); + } + defaultCtorSeen->set(objectIdx); + } + static void markKnownWriteOnPointerOperand( const llvm::Value* ptrOperand, const TrackedObjectContext& tracked, const llvm::DataLayout& DL, InitRangeState& initialized, llvm::BitVector* writeSeen, @@ -1456,29 +1842,117 @@ namespace ctrace::stack::analysis } } + static bool + unsummarizedDefinedCallArgMayWriteThrough(const llvm::CallBase& CB, + const llvm::Function* callee, unsigned argIdx, + bool hasMethodReceiverIdx, + unsigned methodReceiverIdx) + { + if (!callee || callee->isDeclaration() || callee->isIntrinsic()) + return false; + if (callee->isVarArg()) + return false; + if (argIdx >= CB.arg_size()) + return false; + if (hasMethodReceiverIdx && argIdx == methodReceiverIdx) + return false; + + const llvm::Value* actual = CB.getArgOperand(argIdx); + if (!actual || !actual->getType()->isPointerTy()) + return false; + + if (callee->getReturnType()->isVoidTy() || !callee->getReturnType()->isIntegerTy(1)) + return false; + if (!declarationCallReturnIsControlChecked(CB)) + return false; + + if (CB.paramHasAttr(argIdx, llvm::Attribute::ReadOnly) || + CB.paramHasAttr(argIdx, llvm::Attribute::ReadNone)) + { + return false; + } + if (CB.paramHasAttr(argIdx, llvm::Attribute::WriteOnly)) + return true; + + if (argIdx >= callee->arg_size()) + return false; + + const auto& attrs = callee->getAttributes(); + if (attrs.hasParamAttr(argIdx, llvm::Attribute::ReadOnly) || + attrs.hasParamAttr(argIdx, llvm::Attribute::ReadNone)) + { + return false; + } + if (attrs.hasParamAttr(argIdx, llvm::Attribute::WriteOnly)) + return true; + + return isLikelyStatusOutParamDeclarationArg(CB, *callee, argIdx); + } + + static void applyUnsummarizedDefinedCallWriteEffects(const llvm::CallBase& CB, + const llvm::Function* callee, + const TrackedObjectContext& tracked, + const llvm::DataLayout& DL, + InitRangeState& initialized, + llvm::BitVector* writeSeen, + FunctionSummary* currentSummary) + { + if (!callee || callee->isDeclaration()) + return; + + unsigned methodReceiverIdx = 0; + const bool hasMethodReceiverIdx = + callee ? getLikelyCppMethodReceiverArgIndex(*callee, methodReceiverIdx) : false; + for (unsigned argIdx = 0; argIdx < CB.arg_size(); ++argIdx) + { + if (!unsummarizedDefinedCallArgMayWriteThrough(CB, callee, argIdx, + hasMethodReceiverIdx, + methodReceiverIdx)) + { + continue; + } + + const llvm::Value* ptrOperand = CB.getArgOperand(argIdx); + const std::uint64_t inferredSize = inferWriteSizeFromPointerOperand(ptrOperand, DL); + markKnownWriteOnPointerOperand(ptrOperand, tracked, DL, initialized, writeSeen, + currentSummary, inferredSize); + } + } + static void applyKnownCallWriteEffects(const llvm::CallBase& CB, const llvm::Function* callee, const TrackedObjectContext& tracked, const llvm::DataLayout& DL, InitRangeState& initialized, llvm::BitVector* writeSeen, + llvm::BitVector* constructedSeen, llvm::BitVector* defaultCtorSeen, FunctionSummary* currentSummary) { const bool isCtor = callee && isLikelyCppConstructorSymbol(callee->getName()); + unsigned methodReceiverIdx = 0; + const bool hasMethodReceiverIdx = + callee ? getLikelyCppMethodReceiverArgIndex(*callee, methodReceiverIdx) : false; for (unsigned argIdx = 0; argIdx < CB.arg_size(); ++argIdx) { + const llvm::Value* ptrOperand = CB.getArgOperand(argIdx); + const bool isMethodReceiver = hasMethodReceiverIdx && argIdx == methodReceiverIdx; + if (isMethodReceiver) + { + markConstructedOnPointerOperand(ptrOperand, tracked, DL, constructedSeen); + } + bool shouldWrite = CB.paramHasAttr(argIdx, llvm::Attribute::StructRet); if (!shouldWrite && isCtor && argIdx == 0) shouldWrite = true; if (!shouldWrite) continue; - const llvm::Value* ptrOperand = CB.getArgOperand(argIdx); const bool isSRet = CB.paramHasAttr(argIdx, llvm::Attribute::StructRet); const bool isCtorThis = isCtor && argIdx == 0; const std::uint64_t inferredSize = inferWriteSizeFromPointerOperand(ptrOperand, DL); if (isSRet) { + markConstructedOnPointerOperand(ptrOperand, tracked, DL, constructedSeen); markKnownWriteOnPointerOperand(ptrOperand, tracked, DL, initialized, writeSeen, currentSummary, inferredSize); continue; @@ -1488,6 +1962,9 @@ namespace ctrace::stack::analysis { // For constructor "this", treat the object as initialized even if // size inference from the pointer operand is not available. + markConstructedOnPointerOperand(ptrOperand, tracked, DL, constructedSeen); + if (isLikelyDefaultConstructorThisArg(CB, callee, argIdx)) + markDefaultCtorOnPointerOperand(ptrOperand, tracked, DL, defaultCtorSeen); markKnownWriteOnPointerOperand(ptrOperand, tracked, DL, initialized, writeSeen, currentSummary, inferredSize); } @@ -1498,29 +1975,50 @@ namespace ctrace::stack::analysis const llvm::CallBase& CB, const llvm::Function* callee, const FunctionSummary& calleeSummary, const TrackedObjectContext& tracked, const llvm::DataLayout& DL, InitRangeState& initialized, llvm::BitVector* writeSeen, + llvm::BitVector* constructedSeen, llvm::BitVector* defaultCtorSeen, FunctionSummary* currentSummary) { const bool isCtor = callee && isLikelyCppConstructorSymbol(callee->getName()); + unsigned methodReceiverIdx = 0; + const bool hasMethodReceiverIdx = + callee ? getLikelyCppMethodReceiverArgIndex(*callee, methodReceiverIdx) : false; for (unsigned argIdx = 0; argIdx < CB.arg_size(); ++argIdx) { + const llvm::Value* actual = CB.getArgOperand(argIdx); + const bool isMethodReceiver = hasMethodReceiverIdx && argIdx == methodReceiverIdx; + if (isMethodReceiver) + { + markConstructedOnPointerOperand(actual, tracked, DL, constructedSeen); + } + const bool isSRet = CB.paramHasAttr(argIdx, llvm::Attribute::StructRet); const bool isCtorThis = isCtor && argIdx == 0; - if (!isSRet && !isCtorThis) - continue; - const bool hasCalleeEffect = argIdx < calleeSummary.paramEffects.size() && calleeSummary.paramEffects[argIdx].hasAnyEffect(); if (hasCalleeEffect) continue; - const llvm::Value* actual = CB.getArgOperand(argIdx); + // Keep constructor/sret behavior for empty summaries, and use a + // conservative bool/status fallback for other defined callees. if (isSRet) { + markConstructedOnPointerOperand(actual, tracked, DL, constructedSeen); markKnownWriteOnPointerOperand(actual, tracked, DL, initialized, writeSeen, currentSummary); } - else + else if (isCtorThis) + { + markConstructedOnPointerOperand(actual, tracked, DL, constructedSeen); + if (isLikelyDefaultConstructorThisArg(CB, callee, argIdx)) + markDefaultCtorOnPointerOperand(actual, tracked, DL, defaultCtorSeen); + const std::uint64_t inferredSize = inferWriteSizeFromPointerOperand(actual, DL); + markKnownWriteOnPointerOperand(actual, tracked, DL, initialized, writeSeen, + currentSummary, inferredSize); + } + else if (unsummarizedDefinedCallArgMayWriteThrough(CB, callee, argIdx, + hasMethodReceiverIdx, + methodReceiverIdx)) { const std::uint64_t inferredSize = inferWriteSizeFromPointerOperand(actual, DL); markKnownWriteOnPointerOperand(actual, tracked, DL, initialized, writeSeen, @@ -1609,22 +2107,40 @@ namespace ctrace::stack::analysis const llvm::CallBase& CB, const llvm::Function& callee, const FunctionSummary& calleeSummary, const TrackedObjectContext& tracked, const llvm::DataLayout& DL, InitRangeState& initialized, llvm::BitVector* writeSeen, - llvm::BitVector* readBeforeInitSeen, FunctionSummary* currentSummary, + llvm::BitVector* constructedSeen, llvm::BitVector* defaultCtorSeen, + llvm::BitVector* readBeforeInitSeen, + FunctionSummary* currentSummary, std::vector* emittedIssues) { + const bool isCtor = isLikelyCppConstructorSymbol(callee.getName()); + unsigned methodReceiverIdx = 0; + const bool hasMethodReceiverIdx = + getLikelyCppMethodReceiverArgIndex(callee, methodReceiverIdx); const unsigned maxArgs = std::min(static_cast(CB.arg_size()), static_cast(calleeSummary.paramEffects.size())); for (unsigned argIdx = 0; argIdx < maxArgs; ++argIdx) { - const PointerParamEffectSummary& effect = calleeSummary.paramEffects[argIdx]; - if (!effect.hasAnyEffect()) - continue; - const llvm::Value* actual = CB.getArgOperand(argIdx); if (!actual || !actual->getType()->isPointerTy()) continue; + const bool isCtorThis = isCtor && argIdx == 0; + const bool isSRet = CB.paramHasAttr(argIdx, llvm::Attribute::StructRet); + const bool isMethodReceiver = hasMethodReceiverIdx && argIdx == methodReceiverIdx; + if (isCtorThis || isSRet || isMethodReceiver) + { + markConstructedOnPointerOperand(actual, tracked, DL, constructedSeen); + if (isLikelyDefaultConstructorThisArg(CB, &callee, argIdx)) + markDefaultCtorOnPointerOperand(actual, tracked, DL, defaultCtorSeen); + } + + const PointerParamEffectSummary& effect = calleeSummary.paramEffects[argIdx]; + if (!effect.hasAnyEffect()) + { + continue; + } + unsigned objectIdx = 0; std::uint64_t baseOffset = 0; bool hasConstOffset = false; @@ -1653,8 +2169,8 @@ namespace ctrace::stack::analysis { continue; } - if (!isRangeCoveredAllowingSmallInteriorGaps(initialized[objectIdx], - clippedBegin, clippedEnd)) + if (!isRangeCoveredRespectingNonPaddingLayout( + obj, initialized[objectIdx], clippedBegin, clippedEnd)) { hasReadBeforeWrite = true; uncoveredReadRanges.push_back({clippedBegin, clippedEnd}); @@ -1810,6 +2326,11 @@ namespace ctrace::stack::analysis { writeSeen->set(objectIdx); } + if (wroteSomething && constructedSeen && isAllocaObject(obj) && + objectIdx < constructedSeen->size()) + { + constructedSeen->set(objectIdx); + } } } @@ -1818,6 +2339,7 @@ namespace ctrace::stack::analysis const llvm::DataLayout& DL, const FunctionSummaryMap& summaries, const ExternalSummaryMapByName* externalSummariesByName, InitRangeState& initialized, llvm::BitVector* writeSeen, + llvm::BitVector* constructedSeen, llvm::BitVector* defaultCtorSeen, llvm::BitVector* readBeforeInitSeen, FunctionSummary* currentSummary, std::vector* emittedIssues) { @@ -1829,8 +2351,8 @@ namespace ctrace::stack::analysis access)) { const TrackedMemoryObject& obj = tracked.objects[access.objectIdx]; - bool isDefInit = - isRangeCovered(initialized[access.objectIdx], access.begin, access.end); + bool isDefInit = isRangeCoveredRespectingNonPaddingLayout( + obj, initialized[access.objectIdx], access.begin, access.end); if (!isDefInit) { if (isAllocaObject(obj)) @@ -1964,8 +2486,9 @@ namespace ctrace::stack::analysis { const TrackedMemoryObject& srcObj = tracked.objects[srcAccess.objectIdx]; - bool srcDefInit = isRangeCovered(initialized[srcAccess.objectIdx], - srcAccess.begin, srcAccess.end); + bool srcDefInit = isRangeCoveredRespectingNonPaddingLayout( + srcObj, initialized[srcAccess.objectIdx], srcAccess.begin, + srcAccess.end); if (!srcDefInit) { if (isAllocaObject(srcObj)) @@ -2137,9 +2660,11 @@ namespace ctrace::stack::analysis if (!hasSummary) { applyKnownCallWriteEffects(*CB, callee, tracked, DL, initialized, writeSeen, - currentSummary); + constructedSeen, defaultCtorSeen, currentSummary); applyExternalDeclarationCallWriteEffects(*CB, callee, tracked, DL, initialized, writeSeen, currentSummary); + applyUnsummarizedDefinedCallWriteEffects(*CB, callee, tracked, DL, initialized, + writeSeen, currentSummary); } if (!callee) return; @@ -2148,12 +2673,14 @@ namespace ctrace::stack::analysis return; applyCalleeSummaryAtCall(*CB, *callee, *calleeSummary, tracked, DL, initialized, - writeSeen, readBeforeInitSeen, currentSummary, emittedIssues); + writeSeen, constructedSeen, defaultCtorSeen, readBeforeInitSeen, + currentSummary, emittedIssues); if (!currentSummary) { applySummaryGapCallWriteFallbacks(*CB, callee, *calleeSummary, tracked, DL, - initialized, writeSeen, currentSummary); + initialized, writeSeen, constructedSeen, + defaultCtorSeen, currentSummary); } } @@ -2206,7 +2733,8 @@ namespace ctrace::stack::analysis for (const llvm::Instruction& I : BB) { transferInstruction(I, tracked, DL, summaries, externalSummariesByName, - state, nullptr, nullptr, nullptr, nullptr); + state, nullptr, nullptr, nullptr, nullptr, nullptr, + nullptr); } InitRangeState& oldIn = inState[&BB]; @@ -2235,13 +2763,16 @@ namespace ctrace::stack::analysis for (const llvm::Instruction& I : BB) { transferInstruction(I, tracked, DL, summaries, externalSummariesByName, - state, nullptr, nullptr, outSummary, nullptr); + state, nullptr, nullptr, nullptr, nullptr, outSummary, + nullptr); } } return; } llvm::BitVector writeSeen(trackedCount, false); + llvm::BitVector constructedSeen(trackedCount, false); + llvm::BitVector defaultCtorSeen(trackedCount, false); llvm::BitVector readBeforeInitSeen(trackedCount, false); for (const llvm::BasicBlock& BB : F) @@ -2253,7 +2784,8 @@ namespace ctrace::stack::analysis for (const llvm::Instruction& I : BB) { transferInstruction(I, tracked, DL, summaries, externalSummariesByName, state, - &writeSeen, &readBeforeInitSeen, nullptr, outIssues); + &writeSeen, &constructedSeen, &defaultCtorSeen, + &readBeforeInitSeen, nullptr, outIssues); } } @@ -2274,11 +2806,37 @@ namespace ctrace::stack::analysis continue; const std::string varName = deriveAllocaName(AI); + const bool hasDefaultCtor = defaultCtorSeen.test(idx); + + if (obj.hasNonPaddingLayout && obj.nonPaddingRanges.empty()) + { + coretrace::log(coretrace::Level::Debug, + "[uninit][ctor] func={} local={} default_ctor_detected={} " + "action=suppress_never_initialized\n", + F.getName().str(), varName, + hasDefaultCtor ? "yes" : "no"); + continue; + } + if (varName.empty() || varName == "") continue; if (isLikelyCompilerTemporaryName(varName)) continue; + if (constructedSeen.test(idx)) + { + coretrace::log(coretrace::Level::Debug, + "[uninit][ctor] func={} local={} default_ctor_detected={} " + "action=suppress_never_initialized\n", + F.getName().str(), varName, hasDefaultCtor ? "yes" : "no"); + continue; + } + + coretrace::log(coretrace::Level::Debug, + "[uninit][ctor] func={} local={} default_ctor_detected={} " + "action=emit_never_initialized\n", + F.getName().str(), varName, hasDefaultCtor ? "yes" : "no"); + unsigned line = 0; unsigned column = 0; getAllocaDeclarationLocation(AI, varName, line, column); From b6251b896ebd6e1e5ed9e4f51981196e1f0a6a3e Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 27 Feb 2026 00:26:31 +0900 Subject: [PATCH 11/26] test(fixtures): add default-member return initialization case --- ...alized-local-cpp-default-member-return.cpp | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 test/uninitialized-variable/uninitialized-local-cpp-default-member-return.cpp diff --git a/test/uninitialized-variable/uninitialized-local-cpp-default-member-return.cpp b/test/uninitialized-variable/uninitialized-local-cpp-default-member-return.cpp new file mode 100644 index 0000000..506cf88 --- /dev/null +++ b/test/uninitialized-variable/uninitialized-local-cpp-default-member-return.cpp @@ -0,0 +1,33 @@ +#include +#include + +struct StorageKeyLike +{ + int scope = 0; + std::string key = ""; + std::string displayName = ""; + std::uint64_t offset = 0; + int argumentIndex = -1; + void* localAlloca = nullptr; +}; + +static StorageKeyLike build_storage_key_like(bool earlyA, bool earlyB) +{ + StorageKeyLike out; + if (earlyA) + return out; + if (earlyB) + return out; + + out.scope = 1; + out.key = "ok"; + return out; +} + +int default_member_record_return_paths_should_not_warn(bool a, bool b) +{ + StorageKeyLike value = build_storage_key_like(a, b); + return value.scope; +} + +// not contains: potential read of uninitialized local variable 'out' From eebc7bc85c80370d9dc4a3c1625ea3c8b8f38efd Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 27 Feb 2026 00:26:51 +0900 Subject: [PATCH 12/26] test(fixtures): add empty lambda capture initialization case --- ...ialized-local-cpp-empty-lambda-capture.cpp | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 test/uninitialized-variable/uninitialized-local-cpp-empty-lambda-capture.cpp diff --git a/test/uninitialized-variable/uninitialized-local-cpp-empty-lambda-capture.cpp b/test/uninitialized-variable/uninitialized-local-cpp-empty-lambda-capture.cpp new file mode 100644 index 0000000..6e69d26 --- /dev/null +++ b/test/uninitialized-variable/uninitialized-local-cpp-empty-lambda-capture.cpp @@ -0,0 +1,30 @@ +enum class MemKind +{ + None, + A, + B, + C +}; + +int empty_lambda_captured_indirect_call_should_not_warn(void) +{ + auto classifyByName = [&](int v) -> MemKind + { + if (v == 1) + return MemKind::A; + if (v == 2) + return MemKind::B; + if (v == 3) + return MemKind::C; + return MemKind::None; + }; + + MemKind kind = [&]() -> MemKind + { + return classifyByName(1); + }(); + + return static_cast(kind); +} + +// not contains: local variable 'classifyByName' is never initialized From 5b3b700c0a86d72468388a2e7caad9caf68a95e4 Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 27 Feb 2026 00:26:55 +0900 Subject: [PATCH 13/26] test(fixtures): add lambda receiver initialization case --- .../uninitialized-local-cpp-lambda-receiver.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 test/uninitialized-variable/uninitialized-local-cpp-lambda-receiver.cpp diff --git a/test/uninitialized-variable/uninitialized-local-cpp-lambda-receiver.cpp b/test/uninitialized-variable/uninitialized-local-cpp-lambda-receiver.cpp new file mode 100644 index 0000000..7e3013c --- /dev/null +++ b/test/uninitialized-variable/uninitialized-local-cpp-lambda-receiver.cpp @@ -0,0 +1,11 @@ +int lambda_receiver_object_should_not_warn_never_initialized(void) +{ + auto buildCanonicalize = [&](int v) + { + return v + 1; + }; + + return buildCanonicalize(41); +} + +// not contains: local variable 'buildCanonicalize' is never initialized From bcf30385c60707b20a59fc12035e8e11bc3bc4cc Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 27 Feb 2026 00:26:59 +0900 Subject: [PATCH 14/26] test(fixtures): add optional receiver index reproducer --- ...ized-local-cpp-optional-receiver-index.cpp | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 test/uninitialized-variable/uninitialized-local-cpp-optional-receiver-index.cpp diff --git a/test/uninitialized-variable/uninitialized-local-cpp-optional-receiver-index.cpp b/test/uninitialized-variable/uninitialized-local-cpp-optional-receiver-index.cpp new file mode 100644 index 0000000..5239deb --- /dev/null +++ b/test/uninitialized-variable/uninitialized-local-cpp-optional-receiver-index.cpp @@ -0,0 +1,28 @@ +#include + +static std::optional getMethodReceiverIdx(bool hasReceiver) +{ + if (hasReceiver) + return 0u; + return std::nullopt; +} + +static bool argMayWriteThrough(unsigned argIdx, std::optional methodReceiverIdx) +{ + if (methodReceiverIdx && argIdx == *methodReceiverIdx) + return false; + return true; +} + +int optional_receiver_index_should_not_warn(bool hasReceiver) +{ + const std::optional methodReceiverIdx = getMethodReceiverIdx(hasReceiver); + int writes = 0; + for (unsigned argIdx = 0; argIdx < 3; ++argIdx) + { + if (!argMayWriteThrough(argIdx, methodReceiverIdx)) + continue; + ++writes; + } + return writes; +} From edce03a3f783c7f2f32c3a69281fe415ba7ad332 Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 27 Feb 2026 00:27:06 +0900 Subject: [PATCH 15/26] test(fixtures): add cpp record never-initialized case --- ...nitialized-local-cpp-record-never-init.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 test/uninitialized-variable/uninitialized-local-cpp-record-never-init.cpp diff --git a/test/uninitialized-variable/uninitialized-local-cpp-record-never-init.cpp b/test/uninitialized-variable/uninitialized-local-cpp-record-never-init.cpp new file mode 100644 index 0000000..2edf6e2 --- /dev/null +++ b/test/uninitialized-variable/uninitialized-local-cpp-record-never-init.cpp @@ -0,0 +1,19 @@ +struct NonEmptyRecord +{ + int value; + + int get() const + { + return value; + } +}; + +int non_empty_record_method_read_should_warn(void) +{ + NonEmptyRecord obj; + return obj.get(); +} + +// at line 14, column 16 +// [ !!Warn ] potential read of uninitialized local variable 'obj' +// ↳ this call may read the value before any definite initialization in '_ZNK14NonEmptyRecord3getEv' From 0a8430d4ec1760d5f48b670aa0e59ea0ee2bf925 Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 27 Feb 2026 00:27:11 +0900 Subject: [PATCH 16/26] test(fixtures): add trivial constructor initialization case --- .../uninitialized-local-cpp-trivial-ctor.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 test/uninitialized-variable/uninitialized-local-cpp-trivial-ctor.cpp diff --git a/test/uninitialized-variable/uninitialized-local-cpp-trivial-ctor.cpp b/test/uninitialized-variable/uninitialized-local-cpp-trivial-ctor.cpp new file mode 100644 index 0000000..c747f40 --- /dev/null +++ b/test/uninitialized-variable/uninitialized-local-cpp-trivial-ctor.cpp @@ -0,0 +1,17 @@ +class TrivialAnalyzerApp +{ + public: + int run() const + { + return 7; + } +}; + +int trivial_ctor_object_should_not_warn_never_initialized(void) +{ + TrivialAnalyzerApp app; + return app.run(); +} + +// not contains: local variable 'app' is never initialized +// not contains: potential read of uninitialized local variable 'app' From 523d2acb19be5ff1c7eca15320e318e27ff15854 Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 27 Feb 2026 00:27:16 +0900 Subject: [PATCH 17/26] test(fixtures): add nested subobject projection no-diagnostic case --- ...gep_nested_subobject_reference_no_diag.cpp | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 test/offset_of-container_of/gep_nested_subobject_reference_no_diag.cpp diff --git a/test/offset_of-container_of/gep_nested_subobject_reference_no_diag.cpp b/test/offset_of-container_of/gep_nested_subobject_reference_no_diag.cpp new file mode 100644 index 0000000..cac3d0a --- /dev/null +++ b/test/offset_of-container_of/gep_nested_subobject_reference_no_diag.cpp @@ -0,0 +1,34 @@ +struct AnalysisConfig +{ + bool quiet = false; + bool warningsOnly = false; + unsigned jobs = 0; +}; + +struct ParsedArguments +{ + AnalysisConfig config = {}; + bool verbose = false; +}; + +struct ParseResult +{ + ParsedArguments parsed = {}; + int status = 0; +}; + +int nested_subobject_projection_should_not_warn(void) +{ + ParseResult result{}; + ParsedArguments& parsed = result.parsed; + AnalysisConfig& cfg = parsed.config; + + cfg.quiet = false; + cfg.warningsOnly = true; + cfg.jobs = 2; + + return static_cast(result.parsed.config.jobs); +} + +// not contains: potential UB: invalid base reconstruction via offsetof/container_of +// not contains: unable to verify that derived pointer points to a valid object From 83046edca02f67f23806ff81897a91098f94d1b5 Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 27 Feb 2026 00:27:22 +0900 Subject: [PATCH 18/26] fix(report): stabilize function-level file fallback in JSON output --- src/report/ReportSerialization.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/report/ReportSerialization.cpp b/src/report/ReportSerialization.cpp index 301941f..f4d7080 100644 --- a/src/report/ReportSerialization.cpp +++ b/src/report/ReportSerialization.cpp @@ -41,7 +41,7 @@ namespace ctrace::stack default: if (static_cast(c) < 0x20) { - char buf[7]; + char buf[7] = {}; std::snprintf(buf, sizeof(buf), "\\u%04x", c & 0xFF); out += buf; } From 648c0a82b55b60d05abe438f40a1594ecd134b86 Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 27 Feb 2026 00:27:32 +0900 Subject: [PATCH 19/26] docs(readme): clarify warnings-only behavior in human output --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dcde490..f293e1f 100644 --- a/README.md +++ b/README.md @@ -213,7 +213,7 @@ Ready-to-adapt workflow examples: --format=json|sarif|human --analysis-profile=fast|full selects analysis precision/performance profile (default: full) --quiet disables diagnostics entirely ---warnings-only keeps only important diagnostics +--warnings-only hides info-level diagnostics; in human output it also lists only functions with warnings/errors --stack-limit= overrides stack limit (bytes, or KiB/MiB/GiB) --compile-arg= passes an extra argument to the compiler --compile-commands= uses compile_commands.json (file or directory) From 1945e97c79eaa7f67361a3bb70b6f1ebf1bfcd86 Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 27 Feb 2026 00:27:39 +0900 Subject: [PATCH 20/26] docs(contributing): add repository contribution guidelines --- CONTRIBUTING.md | 122 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..57d7aba --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,122 @@ +# Contributing to coretrace-stack-analyzer + +Thanks for contributing. + +This document defines the expected workflow for code, tests, and pull requests. + +## Development Setup + +Prerequisites: +- CMake `>= 3.16` +- LLVM/Clang `>= 19` (20 recommended) +- A C++20 compiler + +Build: + +```bash +./build.sh --type Release +``` + +If LLVM/Clang are not auto-detected: + +```bash +LLVM_DIR=/path/to/llvm/lib/cmake/llvm \ +Clang_DIR=/path/to/llvm/lib/cmake/clang \ +./build.sh --type Release +``` + +## Local Validation Before Opening a PR + +Run formatting check: + +```bash +./scripts/format-check.sh +``` + +Run regression tests: + +```bash +python3 run_test.py --analyzer ./build/stack_usage_analyzer +``` + +Optional module unit tests (recommended for architectural/internal changes): + +```bash +cmake -S . -B build -DBUILD_ANALYZER_UNIT_TESTS=ON +cmake --build build +cd build && ctest -R analyzer_module_unit_tests +``` + +## Commit Convention (CI Enforced) + +Commit subjects must follow Conventional Commits: + +```text +type(scope): subject +``` + +Allowed `type` values: +- `feat` +- `fix` +- `chore` +- `docs` +- `refactor` +- `perf` +- `ci` +- `build` +- `style` +- `revert` +- `test` + +Rules: +- Subject line max length: **84** characters. +- Use English for commit messages. +- Keep commits focused and atomic when possible. + +## Pull Request Expectations + +A PR should include: +- A clear problem statement and solution summary. +- Behavioral impact (what changed for users/CI/API). +- Validation evidence (commands run, test results). +- Documentation updates when behavior/options/contracts change. + +If relevant, include: +- Example CLI invocation +- JSON/SARIF output impact +- Notes on profile/cross-TU performance impact + +## Architecture Guardrails + +When adding or refactoring features, preserve module boundaries: + +- `src/app/AnalyzerApp.cpp`: application orchestration, strategy selection, input planning. +- `src/analyzer/*`: analysis pipeline coordination, preparation, location resolution, diagnostic emission. +- `src/analysis/*`: analysis logic and findings generation. +- `src/report/ReportSerialization.cpp`: output serialization (JSON/SARIF). +- `src/cli/ArgParser.cpp`: CLI argument parsing and validation. + +Why: +- Keeps analysis logic decoupled from CLI/CI concerns. +- Improves testability with narrow module responsibilities. +- Reduces regression risk by centralizing reporting and orchestration behavior. + +For architecture details, see: +- `docs/architecture/analyzer-modules.md` + +## Adding a New Check (Short Version) + +1. Implement analysis logic under `src/analysis/` (+ header under `include/analysis/`). +2. Integrate the pass into pipeline/module orchestration. +3. Emit diagnostics through `DiagnosticEmitter` (severity, rule ID, message, CWE/confidence if applicable). +4. Add regression tests under `test/`. +5. Update docs (`README`, wiki/docs pages) when new behavior or options are introduced. + +## CI Notes + +Current CI validates at least: +- Conventional commit format +- clang-format compliance +- build/tests/integration workflows + +Before opening a PR, run local checks to reduce CI round-trips. From bed1043b06229b60e09ecc7f23f955d15e087933 Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 27 Feb 2026 00:27:47 +0900 Subject: [PATCH 21/26] docs(patch): record self-analysis findings and follow-up actions --- PATCH.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 PATCH.md diff --git a/PATCH.md b/PATCH.md new file mode 100644 index 0000000..2b16dc8 --- /dev/null +++ b/PATCH.md @@ -0,0 +1,54 @@ +# Statut des faux positifs (mise a jour) + +## Corriges + +### 1) `src/analysis/MemIntrinsicOverflow.cpp:71` + +Warning corrige: +- `local variable 'classifyByName' is never initialized` + +Patch applique: +- `UninitializedVarAnalysis` ignore maintenant les objets C++ vides (ex: lambda sans state) + en se basant sur la forme IR + metadata debug (pas une heuristique sur un nom de variable). + +### 2) `src/analysis/ResourceLifetimeAnalysis.cpp:823, 825, 995, 1007` + +Warning corrige: +- `potential read of uninitialized local variable 'out'` + +Patch applique: +- verification d'initialisation "padding-aware" dans `UninitializedVarAnalysis`: + - on valide l'initialisation des octets semantiques (membres) ; + - les trous de padding de layout ne declenchent plus de faux positifs. + +### 3) `src/cli/ArgParser.cpp` (19 warnings) + +Warnings corriges: +- `potential UB: invalid base reconstruction via offsetof/container_of` +- `unable to verify that derived pointer points to a valid object` + +Patch applique: +- `InvalidBaseReconstruction` utilise maintenant une resolution recursive de sous-objet + (type + offset + bornes de projection) au lieu d'un test limite au membre top-level. +- Les projections C++ valides sur objets imbriques (`result.parsed.config.*`) ne sont plus + confondues avec des patterns `container_of`. + +## Non-regressions ajoutees + +- `test/uninitialized-variable/uninitialized-local-cpp-empty-lambda-capture.cpp` +- `test/uninitialized-variable/uninitialized-local-cpp-default-member-return.cpp` +- `test/offset_of-container_of/gep_nested_subobject_reference_no_diag.cpp` + +## Validation + +- Verification ciblee sur: + - `src/analysis/MemIntrinsicOverflow.cpp` -> `warning=0` + - `src/analysis/ResourceLifetimeAnalysis.cpp` -> `warning=0` + - `src/cli/ArgParser.cpp` -> `warning=0` +- Suite de regression complete: + - `./run_test.py --jobs 4` + - resultat: **413/413 passed** + +## Reste connu + +- Hors faux positifs: vrai positif conserve `src/analysis/InvalidBaseReconstruction.cpp:188`. From cf09d0aaead1dbe8e48aaabda6e3fbf7aeeb700d Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 27 Feb 2026 00:31:18 +0900 Subject: [PATCH 22/26] chore(style): format code with clang-format --- src/analysis/InvalidBaseReconstruction.cpp | 7 +-- src/analysis/StackPointerEscape.cpp | 10 ++-- src/analysis/StackPointerEscapeResolver.cpp | 7 ++- src/analysis/UninitializedVarAnalysis.cpp | 56 +++++++++---------- ...ialized-local-cpp-empty-lambda-capture.cpp | 5 +- ...ninitialized-local-cpp-lambda-receiver.cpp | 5 +- 6 files changed, 40 insertions(+), 50 deletions(-) diff --git a/src/analysis/InvalidBaseReconstruction.cpp b/src/analysis/InvalidBaseReconstruction.cpp index 4751d27..1dd5e5e 100644 --- a/src/analysis/InvalidBaseReconstruction.cpp +++ b/src/analysis/InvalidBaseReconstruction.cpp @@ -1125,10 +1125,9 @@ namespace ctrace::stack::analysis bool isOutOfBounds = (resultOffset < 0) || (static_cast(resultOffset) >= allocaSize); - bool allowMemberSuppression = - isProjectionWithinSourceSubobjectBounds( - origin.offset, resultOffset, allocatedType, allocaSize, DL, - sourceElementType); + bool allowMemberSuppression = isProjectionWithinSourceSubobjectBounds( + origin.offset, resultOffset, allocatedType, allocaSize, DL, + sourceElementType); std::string targetType; Type* targetTy = GEP->getType(); diff --git a/src/analysis/StackPointerEscape.cpp b/src/analysis/StackPointerEscape.cpp index 43679ce..6b38fe8 100644 --- a/src/analysis/StackPointerEscape.cpp +++ b/src/analysis/StackPointerEscape.cpp @@ -923,8 +923,8 @@ namespace ctrace::stack::analysis continue; if (argIndex >= candidate->arg_size()) continue; - if (ruleMatcher.modelSaysNoEscapeArg(model, candidate, - argIndex)) + if (ruleMatcher.modelSaysNoEscapeArg( + model, candidate, argIndex)) continue; if (isStdLibCallee(candidate)) continue; @@ -1046,9 +1046,9 @@ namespace ctrace::stack::analysis StackEscapeRuleMatcher ruleMatcher; const ReturnedPointerArgAliasMap returnedArgAliases = buildReturnedPointerArgAliases(mod); FunctionArgHardEscapeMap hardEscapesByArg; - const FunctionEscapeSummaryMap summaries = buildFunctionEscapeSummaries( - mod, shouldAnalyze, targetResolver, returnedArgAliases, model, ruleMatcher, - &hardEscapesByArg); + const FunctionEscapeSummaryMap summaries = + buildFunctionEscapeSummaries(mod, shouldAnalyze, targetResolver, returnedArgAliases, + model, ruleMatcher, &hardEscapesByArg); for (llvm::Function& F : mod) { diff --git a/src/analysis/StackPointerEscapeResolver.cpp b/src/analysis/StackPointerEscapeResolver.cpp index 76d6a02..9faead4 100644 --- a/src/analysis/StackPointerEscapeResolver.cpp +++ b/src/analysis/StackPointerEscapeResolver.cpp @@ -79,7 +79,8 @@ namespace ctrace::stack::analysis { const llvm::Value* calledVal = CB.getCalledOperand(); const auto* fnLoad = - calledVal ? llvm::dyn_cast(calledVal->stripPointerCasts()) : nullptr; + calledVal ? llvm::dyn_cast(calledVal->stripPointerCasts()) + : nullptr; if (!fnLoad) return std::nullopt; @@ -95,8 +96,8 @@ namespace ctrace::stack::analysis if (!isLikelyVPtrLoadFromObjectRoot(*vtableLoad)) return std::nullopt; - const auto* slotIndex = - llvm::dyn_cast(slotGEP->getOperand(slotGEP->getNumOperands() - 1)); + const auto* slotIndex = llvm::dyn_cast( + slotGEP->getOperand(slotGEP->getNumOperands() - 1)); if (!slotIndex || slotIndex->isNegative()) return std::nullopt; diff --git a/src/analysis/UninitializedVarAnalysis.cpp b/src/analysis/UninitializedVarAnalysis.cpp index a86434f..3c04615 100644 --- a/src/analysis/UninitializedVarAnalysis.cpp +++ b/src/analysis/UninitializedVarAnalysis.cpp @@ -315,9 +315,9 @@ namespace ctrace::stack::analysis return current; } - static bool debugTypeHasDataMembers( - const llvm::DIType* type, unsigned depth, - llvm::SmallPtrSetImpl& visitedTypes) + static bool + debugTypeHasDataMembers(const llvm::DIType* type, unsigned depth, + llvm::SmallPtrSetImpl& visitedTypes) { if (!type || depth > 12) return false; @@ -427,9 +427,9 @@ namespace ctrace::stack::analysis for (unsigned idx = 0; idx < memberCount; ++idx) { const std::uint64_t memberOffset = layout->getElementOffset(idx); - if (!collectNonPaddingLeafRangesForType( - structTy->getElementType(idx), DL, - saturatingAdd(baseOffset, memberOffset), depth + 1, out)) + if (!collectNonPaddingLeafRangesForType(structTy->getElementType(idx), DL, + saturatingAdd(baseOffset, memberOffset), + depth + 1, out)) { return false; } @@ -490,8 +490,7 @@ namespace ctrace::stack::analysis static bool isRangeCoveredRespectingNonPaddingLayout(const TrackedMemoryObject& obj, const RangeSet& initialized, - std::uint64_t begin, - std::uint64_t end) + std::uint64_t begin, std::uint64_t end) { if (isRangeCovered(initialized, begin, end)) return true; @@ -896,8 +895,8 @@ namespace ctrace::stack::analysis obj.alloca = AI; obj.param = nullptr; obj.sizeBytes = sizeBytes; - obj.hasNonPaddingLayout = computeAllocaNonPaddingRanges(*AI, DL, - obj.nonPaddingRanges); + obj.hasNonPaddingLayout = + computeAllocaNonPaddingRanges(*AI, DL, obj.nonPaddingRanges); tracked.objects.push_back(std::move(obj)); tracked.allocaIndex[AI] = idx; } @@ -1842,11 +1841,11 @@ namespace ctrace::stack::analysis } } - static bool - unsummarizedDefinedCallArgMayWriteThrough(const llvm::CallBase& CB, - const llvm::Function* callee, unsigned argIdx, - bool hasMethodReceiverIdx, - unsigned methodReceiverIdx) + static bool unsummarizedDefinedCallArgMayWriteThrough(const llvm::CallBase& CB, + const llvm::Function* callee, + unsigned argIdx, + bool hasMethodReceiverIdx, + unsigned methodReceiverIdx) { if (!callee || callee->isDeclaration() || callee->isIntrinsic()) return false; @@ -1905,9 +1904,8 @@ namespace ctrace::stack::analysis callee ? getLikelyCppMethodReceiverArgIndex(*callee, methodReceiverIdx) : false; for (unsigned argIdx = 0; argIdx < CB.arg_size(); ++argIdx) { - if (!unsummarizedDefinedCallArgMayWriteThrough(CB, callee, argIdx, - hasMethodReceiverIdx, - methodReceiverIdx)) + if (!unsummarizedDefinedCallArgMayWriteThrough( + CB, callee, argIdx, hasMethodReceiverIdx, methodReceiverIdx)) { continue; } @@ -1923,7 +1921,8 @@ namespace ctrace::stack::analysis applyKnownCallWriteEffects(const llvm::CallBase& CB, const llvm::Function* callee, const TrackedObjectContext& tracked, const llvm::DataLayout& DL, InitRangeState& initialized, llvm::BitVector* writeSeen, - llvm::BitVector* constructedSeen, llvm::BitVector* defaultCtorSeen, + llvm::BitVector* constructedSeen, + llvm::BitVector* defaultCtorSeen, FunctionSummary* currentSummary) { const bool isCtor = callee && isLikelyCppConstructorSymbol(callee->getName()); @@ -2016,9 +2015,8 @@ namespace ctrace::stack::analysis markKnownWriteOnPointerOperand(actual, tracked, DL, initialized, writeSeen, currentSummary, inferredSize); } - else if (unsummarizedDefinedCallArgMayWriteThrough(CB, callee, argIdx, - hasMethodReceiverIdx, - methodReceiverIdx)) + else if (unsummarizedDefinedCallArgMayWriteThrough( + CB, callee, argIdx, hasMethodReceiverIdx, methodReceiverIdx)) { const std::uint64_t inferredSize = inferWriteSizeFromPointerOperand(actual, DL); markKnownWriteOnPointerOperand(actual, tracked, DL, initialized, writeSeen, @@ -2108,8 +2106,7 @@ namespace ctrace::stack::analysis const FunctionSummary& calleeSummary, const TrackedObjectContext& tracked, const llvm::DataLayout& DL, InitRangeState& initialized, llvm::BitVector* writeSeen, llvm::BitVector* constructedSeen, llvm::BitVector* defaultCtorSeen, - llvm::BitVector* readBeforeInitSeen, - FunctionSummary* currentSummary, + llvm::BitVector* readBeforeInitSeen, FunctionSummary* currentSummary, std::vector* emittedIssues) { const bool isCtor = isLikelyCppConstructorSymbol(callee.getName()); @@ -2169,8 +2166,8 @@ namespace ctrace::stack::analysis { continue; } - if (!isRangeCoveredRespectingNonPaddingLayout( - obj, initialized[objectIdx], clippedBegin, clippedEnd)) + if (!isRangeCoveredRespectingNonPaddingLayout(obj, initialized[objectIdx], + clippedBegin, clippedEnd)) { hasReadBeforeWrite = true; uncoveredReadRanges.push_back({clippedBegin, clippedEnd}); @@ -2673,8 +2670,8 @@ namespace ctrace::stack::analysis return; applyCalleeSummaryAtCall(*CB, *callee, *calleeSummary, tracked, DL, initialized, - writeSeen, constructedSeen, defaultCtorSeen, readBeforeInitSeen, - currentSummary, emittedIssues); + writeSeen, constructedSeen, defaultCtorSeen, + readBeforeInitSeen, currentSummary, emittedIssues); if (!currentSummary) { @@ -2813,8 +2810,7 @@ namespace ctrace::stack::analysis coretrace::log(coretrace::Level::Debug, "[uninit][ctor] func={} local={} default_ctor_detected={} " "action=suppress_never_initialized\n", - F.getName().str(), varName, - hasDefaultCtor ? "yes" : "no"); + F.getName().str(), varName, hasDefaultCtor ? "yes" : "no"); continue; } diff --git a/test/uninitialized-variable/uninitialized-local-cpp-empty-lambda-capture.cpp b/test/uninitialized-variable/uninitialized-local-cpp-empty-lambda-capture.cpp index 6e69d26..f61e928 100644 --- a/test/uninitialized-variable/uninitialized-local-cpp-empty-lambda-capture.cpp +++ b/test/uninitialized-variable/uninitialized-local-cpp-empty-lambda-capture.cpp @@ -19,10 +19,7 @@ int empty_lambda_captured_indirect_call_should_not_warn(void) return MemKind::None; }; - MemKind kind = [&]() -> MemKind - { - return classifyByName(1); - }(); + MemKind kind = [&]() -> MemKind { return classifyByName(1); }(); return static_cast(kind); } diff --git a/test/uninitialized-variable/uninitialized-local-cpp-lambda-receiver.cpp b/test/uninitialized-variable/uninitialized-local-cpp-lambda-receiver.cpp index 7e3013c..266db08 100644 --- a/test/uninitialized-variable/uninitialized-local-cpp-lambda-receiver.cpp +++ b/test/uninitialized-variable/uninitialized-local-cpp-lambda-receiver.cpp @@ -1,9 +1,6 @@ int lambda_receiver_object_should_not_warn_never_initialized(void) { - auto buildCanonicalize = [&](int v) - { - return v + 1; - }; + auto buildCanonicalize = [&](int v) { return v + 1; }; return buildCanonicalize(41); } From b6b585b10ab13fe512cf1fb398dbe35f49713d2b Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 27 Feb 2026 01:31:03 +0900 Subject: [PATCH 23/26] fix(uninitialized): derive alloca non-padding ranges from debug types first --- src/analysis/UninitializedVarAnalysis.cpp | 121 ++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/src/analysis/UninitializedVarAnalysis.cpp b/src/analysis/UninitializedVarAnalysis.cpp index 3c04615..8bf645e 100644 --- a/src/analysis/UninitializedVarAnalysis.cpp +++ b/src/analysis/UninitializedVarAnalysis.cpp @@ -472,10 +472,131 @@ namespace ctrace::stack::analysis return true; } + static void addRangeFromBitOffsets(RangeSet& out, std::uint64_t beginBits, + std::uint64_t endBits) + { + if (beginBits >= endBits) + return; + const std::uint64_t beginBytes = beginBits / 8; + const std::uint64_t endBytes = (endBits + 7) / 8; + addRange(out, beginBytes, endBytes); + } + + static bool collectNonPaddingRangesForDebugType( + const llvm::DIType* type, std::uint64_t baseOffsetBits, unsigned depth, + llvm::SmallPtrSetImpl& visiting, RangeSet& out) + { + if (!type || depth > 16) + return false; + type = stripDebugTypeSugar(type); + if (!type) + return false; + + if (!visiting.insert(type).second) + return false; + + bool success = false; + if (const auto* composite = llvm::dyn_cast(type)) + { + const unsigned tag = composite->getTag(); + if (tag == llvm::dwarf::DW_TAG_structure_type || + tag == llvm::dwarf::DW_TAG_class_type || tag == llvm::dwarf::DW_TAG_union_type) + { + bool sawMemberLayout = false; + for (const llvm::Metadata* elem : composite->getElements()) + { + const auto* derived = llvm::dyn_cast(elem); + if (!derived) + continue; + + const unsigned elemTag = derived->getTag(); + if (elemTag != llvm::dwarf::DW_TAG_member && + elemTag != llvm::dwarf::DW_TAG_inheritance) + { + continue; + } + + const llvm::DIType* memberType = derived->getBaseType(); + if (!memberType) + continue; + + const std::uint64_t memberOffsetBits = + (tag == llvm::dwarf::DW_TAG_union_type) + ? 0 + : static_cast(derived->getOffsetInBits()); + const std::uint64_t absoluteOffsetBits = + saturatingAdd(baseOffsetBits, memberOffsetBits); + + if (collectNonPaddingRangesForDebugType(memberType, absoluteOffsetBits, + depth + 1, visiting, out)) + { + sawMemberLayout = true; + } + } + + if (sawMemberLayout) + { + success = true; + } + else + { + const std::uint64_t sizeBits = composite->getSizeInBits(); + if (sizeBits == 0) + { + success = true; + } + else + { + addRangeFromBitOffsets(out, baseOffsetBits, + saturatingAdd(baseOffsetBits, sizeBits)); + success = true; + } + } + } + else + { + // For array/vector/other composites, keep conservative full coverage. + const std::uint64_t sizeBits = composite->getSizeInBits(); + if (sizeBits > 0) + { + addRangeFromBitOffsets(out, baseOffsetBits, + saturatingAdd(baseOffsetBits, sizeBits)); + success = true; + } + } + } + else + { + const std::uint64_t sizeBits = type->getSizeInBits(); + if (sizeBits > 0) + { + addRangeFromBitOffsets(out, baseOffsetBits, + saturatingAdd(baseOffsetBits, sizeBits)); + success = true; + } + } + + visiting.erase(type); + return success; + } + static bool computeAllocaNonPaddingRanges(const llvm::AllocaInst& AI, const llvm::DataLayout& DL, RangeSet& out) { out.clear(); + + if (const llvm::DIType* declaredType = getAllocaDebugDeclaredType(AI)) + { + llvm::SmallPtrSet visiting; + if (collectNonPaddingRangesForDebugType(declaredType, 0, 0, visiting, out)) + { + if (shouldTreatAsDataLessDebugObject(AI, DL)) + out.clear(); + return true; + } + out.clear(); + } + const llvm::Type* allocatedTy = AI.getAllocatedType(); if (!allocatedTy) return false; From ad7d352b3b8866cadea3323be9ce2def8f4877a2 Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 27 Feb 2026 01:31:35 +0900 Subject: [PATCH 24/26] test(regression): assert optional receiver index warning stays suppressed across toolchains --- run_test.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/run_test.py b/run_test.py index 394025f..2976813 100755 --- a/run_test.py +++ b/run_test.py @@ -1375,6 +1375,8 @@ def check_uninitialized_optional_receiver_index_repro() -> bool: """ Reproducer: optional receiver-index tracking passed by value can trigger a false positive on local initialization. + + Regression target: this warning must stay suppressed across toolchains. """ print("=== Testing optional receiver index false-positive reproducer ===") sample = ( @@ -1395,13 +1397,13 @@ def check_uninitialized_optional_receiver_index_repro() -> bool: return False expected = "potential read of uninitialized local variable 'methodReceiverIdx'" - if expected not in output: - print(f" ❌ expected warning not found: {expected}") + if expected in output: + print(f" ❌ unexpected warning still present: {expected}") print(output) print() return False - print(" ✅ optional receiver index false-positive reproduced\n") + print(" ✅ optional receiver index false-positive suppressed\n") return True From 039bd8543cb99dd954df27fa793fe5b8f929ab73 Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 27 Feb 2026 01:34:45 +0900 Subject: [PATCH 25/26] ci(workflow): run push builds only on main --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7525a38..093ee00 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ name: Build on: push: branches: - - "**" + - "main" pull_request: branches: - "**" From a9773a3f16b28b1f23b2f8a41223974011935ef3 Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 27 Feb 2026 01:37:52 +0900 Subject: [PATCH 26/26] ci(workflow): restore ci workflow --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 093ee00..7525a38 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ name: Build on: push: branches: - - "main" + - "**" pull_request: branches: - "**"