Repository navigation
fix(analysis): resolve calls through constructor and destructor aliases - #190
Merged
Merged
Conversation
…every target On ELF targets, clang calls a constructor or a destructor defined out of its class through an alias (C1 -> C2, D1 -> D2); Mach-O makes the same call without one. The analyzer takes the alias call for an indirect one (#179): on x86-64 and AArch64 Linux, an object that only reaches its constructor escapes, the constructor no longer initializes it, and the caller's max stack is unknown. - ctor-alias-no-escape.cpp: the constructor keeps nothing: no finding. - ctor-alias-stores-this.cpp, ctor-alias-passes-this.cpp: the constructor stores this in a global, or passes it to an unknown callback: reported, with the expectations in the files. - ctor-dtor-alias.cpp: a constructor and a destructor through aliases: only the real read of an uninitialized local is reported. run_test.py, check_calls_through_aliases: each of these, and a constructor defined in another file than its caller, analyzed for x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu and arm64-apple-macosx, default and z3: the same findings on every target, the expected ones, and known max stacks. On main 112ffcf, the 10 cases fail on both Linux targets. Refs #179
On ELF targets, clang emits a constructor or a destructor defined out of its class as an alias of the base-object one and calls the alias. getCalledFunction() and stripPointerCasts() do not look through a GlobalAlias, so every analysis took that call for an indirect one (#179): the caller's max stack was unknown, StackPointerEscape reported the object as escaping, and the uninitialized-read analysis no longer saw the constructor initialize it. The same objects were fine on macOS, which makes these calls without an alias. directCallee() (IRValueUtils) gives the function a call reaches through pointer casts and aliases. Every resolution of a callee now uses it: the stack computation and its call graphs, StackPointerEscape, the uninitialized, const-parameter, resource lifetime and ownership analyses, SizeMinusK, OOB read, integer overflow, TOCTOU, command injection, memory intrinsics, duplicate conditions, null dereference, the never-return pass and the resource summary cache keys. The six file-local copies of that lookup and lifetime_detail's go. CallResolver also indexes the aliases of defined functions: a constructor defined in another file is reached through its alias, as a function of that file is. Refs #179
…is rebuilt Review of #190: following aliases with stripPointerCastsAndAliases() also follows a weak alias, which the link may replace by another definition. A caller of a weak alias got the max stack of the aliasee, 32 bytes, while the replacing definition alone takes 272. And the resource summaries change with alias resolution under an unchanged cache version, so a cache left by main kept the wrapper that did not acquire. check_calls_through_aliases, on x86-64 and AArch64 Linux (clang has no aliases on Darwin), default and z3: the caller of a weak alias keeps an unknown max stack, with the definition that replaces it and without; the caller of a plain alias gets a known one. check_resource_cache_rebuilds_alias_summaries: a wrapper returns what an alias of an acquiring function returns. With an empty cache its summary acquires; with that cache rewritten as a previous version left it (previous schema, a wrapper that does not acquire), the run must leave the same summaries as with an empty one, not only the same output. check_ownership_wrapper_metadata plants the previous schema, v4, and expects the rebuilt summaries under the current one, v5. On 2513b35, the weak alias cases and both cache checks fail; the plain alias cases pass. Refs #179
…maries Review of #190. directCallee() followed every alias with stripPointerCastsAndAliases(), weak ones included: the link may replace a weak alias by another definition, so its aliasee is not the callee for sure. It now follows aliases one at a time and gives no callee for an interposable one, as for an indirect call. A constructor's C1 alias is not interposable and is still followed; CallResolver already indexes only aliases with an exact definition. Following aliases changes the resource summaries, which a cache written by a previous version kept: the resource summary cache moves to schema resource-summary-cache-v5 and key cross-tu-resource-summary-v5, so those summaries are rebuilt, as for the previous change of the summaries. A weak function that is not an alias is still taken as the callee; that predates #190 and is left to its own issue. Refs #179
check_resource_cache_rebuilds_alias_summaries compared every file of the cache with those of a run with an empty cache. A run also writes entries for intermediate rounds of the fixed point, which it does not read again: on the Linux CI hosts, two of them keep what the test planted, and the check failed on c69e6dd although the summaries the run uses were rebuilt. The check now compares the files that the run with the previous cache writes under the schema of the run with an empty one: each must equal that run's file, and one must hold the wrapper's acquisition. On 2513b35, whose schema is the planted one, every planted file counts and the check still fails. Refs #179
… cache The resource summary cache keys hash the IR as printed, which differs between a fresh compile and a module read from the compile IR cache. CI runs the suite with a compile IR cache: the run with an empty summary cache compiled, the run with the previous cache read its IR from the compile IR cache, and wrote its summaries under other keys than those planted. check_resource_cache_rebuilds_alias_summaries then failed on 0b0a756 in CI, and does locally with CI's --compile-ir-cache-dir. The check now gives its runs a compile IR cache of its own and fills it with a first run, so that both compared runs read their IR from it, whatever the runner passes. It passes with and without CI's option on c69e6dd, and fails with and without it on 2513b35. Refs #179
This was referenced Oct 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
On ELF targets, clang emits a constructor or a destructor defined out of its class (
C1,D1) as an alias of the base-object one (C2,D2), and calls the alias. Mach-O makes the same call without an alias.getCalledFunction()andstripPointerCasts()do not look through aGlobalAlias, so every analysis took that call for an indirect one. On x86-64 and AArch64 Linux:StackPointerEscapereported the object as escaping through an "indirect call";Alert 251, on
const CallResolver resolver(modules);inStackComputation.cpp, is this case.Changes
One lookup for every callee.
directCallee()(IRValueUtils) gives the function a call reaches through pointer casts and aliases. It gives no callee for an interposable (weak) alias, which the link may replace by another definition: its target is not known. Every resolution of a callee now uses it:StackPointerEscape;The six file-local copies of that lookup, and
lifetime_detail::resolveDirectCallee, are removed.Duplicate conditions. They still require a call through a declaration without a prototype to be deterministic elsewhere; that case is now recognized by comparing function types.
Never-return pass. It finds callers by scanning the calls of the module, since a call through an alias is a user of the alias, not of the function.
Calls across files.
CallResolveralso indexes the aliases of defined functions, so a constructor defined in another file is reached through its alias.Cache. Following aliases changes the resource summaries, so the resource summary cache moves to schema
resource-summary-cache-v5and keycross-tu-resource-summary-v5. Summaries cached by a previous version are then rebuilt, as for the previous change of the summaries (8420450).Tests
New fixtures:
test/escape-stack/ctor-alias-no-escape.cpp: the constructor keeps nothing, so nothing is reported;test/escape-stack/ctor-alias-stores-this.cppandctor-alias-passes-this.cpp: the constructor storesthisin a global, or passes it to an unknown callback. Both are reported, with their expectations in the files;test/false-positive-repro/ctor-dtor-alias.cpp: a constructor and a destructor through aliases; only the real uninitialized read ofvis reported.run_test.py,check_calls_through_aliases:x86_64-unknown-linux-gnu,aarch64-unknown-linux-gnuandarm64-apple-macosx, with the default and the z3 configurations.On main 112ffcf, all 10 cases fail on both Linux targets; with the fix, all pass.
check_resource_cache_rebuilds_alias_summaries. A wrapper returns what an alias of an acquiring function returns:acquire_ret);--compile-ir-cache-dirhad made the two runs use different keys.check_ownership_wrapper_metadata. It now plants the previous schema, v4, and expects the rebuilt summaries under v5.On 2513b35, the weak-alias cases and both cache checks fail, with and without CI's compile IR cache.
Validation
All local runs are on macOS arm64.
Tested head: a60d109.
run_test.py: 2380/2380, both without and with CI'sCORETRACE_RUN_TEST_EXTRA_ANALYZER_ARGS=--compile-ir-cache-dir=…. Both unit-test binaries pass.x86_64-unknown-linux-gnu, only the four new fixtures change, as described above. 149 units do not compile for that target without a Linux sysroot (system headers), on both sides; the Linux CI runs them natively.Code scanning
StackPointerEscapeonresolver,StackComputation.cpp:1134) is the False positive: StackPointerEscape #179 false positive, dismissed onmain. It is no longer reported by this PR's self-analysis on Linux.ConstParameterNotModified.ReferenceRvaluePreferValue), the same message, the same line (parallelFor,AnalyzerApp.cpp:217) and function. They were fingerprinted again because this PR changesAnalyzerApp.cpp.main.Out of scope
A plain weak function, not an alias, is still taken as the callee by the stack computation, as it is on
main: #191.The resource summary cache misses once when a module's IR changes origin, from a fresh compile to the compile IR cache: its keys hash the printed IR, which differs between the two. This is unchanged from
main; the check above works around it.Merge order
This PR does not conflict with #187 (
git merge-tree).Closes #179