Repository navigation
perf(stack-buffer): resolve pointer variables once per function (#215) - #246
Merged
Merged
Conversation
…nded time Eight pointers chained from one buffer and then assigned from each other under a condition form one strongly connected component with 7! paths between its members. StackBufferOverflow and MultipleStores used to walk every path, with a scan of the function at each step, and never finished on such functions (#215: zlib crc32.c, cppcheck simplecpp.cpp). The last pointer is chosen by a conditional operator, so the fixture also covers a PHI of pointers, which no fixture exercised before.
Two pointers assigned from each other: resolving the first reaches the second, which reaches the first again while it is still being resolved. A resolver that remembers what it computed under that cut resolves the second pointer to nothing and loses the write through it.
A safe access through the last of 66 chained pointers is resolved first. The old resolver gave up past 64 pointers; results remembered under that cut must not survive the query, or the write through the second pointer goes unreported. The safe access now counts as a store into the buffer too, which MultipleStores reports.
Two pointers to arrays of different sizes, assigned from each other under a condition, form one component that reaches both arrays. Several arrays mean no array, as for any pointer, so neither write is reported, even though both arrays overflow. The old resolver reported each pointer against its own array; the result of a component now depends on the function alone.
resolveArrayAllocaFromPointer traced a pointer back to its stack array by recursion: the recursion stack only cut the current path, nothing remembered a result, and every visit of a pointer variable scanned the whole function for the stores into it. With k pointer variables assigned from one another it enumerated (k-1)! paths; zlib's crc32.c and cppcheck's simplecpp.cpp never finished (#215). ArrayAllocaResolver replaces it, one instance per function. It indexes the stores into each alloca once, then resolves pointer variables and PHIs with an iterative Tarjan walk: a strongly connected component is finished only after every component it reaches, so its result is computed from final results and does not depend on the query that started the walk. A component resolves to the one array its members' sources lead to; a source that leads nowhere is ignored, two arrays make it ambiguous. Alias paths are kept as witnesses, one segment per node, and copied once per query. There is no depth limit any more. On acyclic alias graphs the results and alias paths are those of the old resolver. A cycle that reaches two arrays now resolves to no array, where each pointer used to be reported against its own array. Fixes #215.
The witnesses of a component were settled by rounds that rescanned every member: on a ring of n pointers with one array source, n rounds. A level worklist over the reverse dependencies settles each member once and follows each edge between members once, with the same choice of witness, the first source among those settled at a lower level, so the alias paths are unchanged. Ring of 8000 pointers: the StackBufferOverflow step goes from 595 ms to 5 ms.
…rray Three pointers assigned in a ring from one buffer form one strongly connected component where only the first points to the array directly. The fixture writes through each pointer in two functions, in opposite orders, and expects the same overflows with the same alias paths in both: a result must not depend on the walk that first reached the pointer. The three writes also make MultipleStores report the buffer of each function, so both users of the resolver are exercised.
A pointer takes a conditional operator over another pointer and the buffer, and the other pointer is reassigned in a loop from a conditional operator over the first and itself: the component holds two pointer variables and two PHIs, and one alias path goes through a PHI. Both writes must be reported and MultipleStores must see both stores.
…ontrol case The fixture now says what it pins: a component that reaches two arrays resolves to no array, so the writes through it go unreported even though both arrays overflow. This is a deliberate trade-off that follows the contract for any pointer with several possible arrays, not a gain in detection. The control case has the same shape with both pointers from one array, and its overflow must be reported, with its alias path and the MultipleStores note.
…00 pointers Two unit tests call StackBufferOverflow and MultipleStores directly. A hand-written module holds a loop whose two pointer PHIs take each other's value, the cycle mem2reg leaves in optimized code: both PHIs must resolve to the buffer through its decay, and both stores must count. A ring of 5000 pointer variables with one array source is built with IRBuilder, as -O0 code leaves it. Every pointer must find the array, 2500 and 4999 steps away included, the alias paths must not depend on the order of the accesses, and the walk must finish on a component of that size. A time bound would be fragile; the generous CI timeout stands for termination.
Contributor
Author
Tests added after reviewFour commits add the tests asked for. All of them exercise both users of the resolver, StackBufferOverflow and MultipleStores.
The deep chain and the clique fixtures stay as they are. Mutation checkTwo deliberately wrong variants of the resolver were built and run against the unit tests and the whole suite, then reverted. Each one is the defect met during review. A. Premature publication of a cycle result. Every node is finished as its own component as soon as its sources are walked, so a node reached inside a cycle keeps the result computed with the cycle cut.
B. Giving up past 64 pointers. The walk stops entering nodes past 64 frames.
With the resolver as submitted: |
This was referenced Oct 31, 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.
Summary
StackBufferOverflow and MultipleStoresToStackBuffer never finished on functions whose local pointer variables are assigned from one another (#215: zlib
crc32.c, four other zlib files, cppchecksimplecpp.cpp). The resolver that traces a pointer back to its stack array now resolves each pointer variable and PHI once per function, by strongly connected components. The result of a node depends on the function alone, never on the query that reached it.Fixes #215.
Root cause
resolveArrayAllocaFromPointerInternalrecursed over the values stored into a pointer variable. Its recursion stack only cut the current path, nothing remembered a result, and every visit of a pointer variable scanned the whole function for the stores into it. Withkpointer variables assigned from one another it enumerated(k-1)!paths, each with a scan of the function: 15 s fork = 5, no termination fork = 6.Change
ArrayAllocaResolver, one instance per function, replaces the recursion:On acyclic alias graphs, the results and alias paths are those of the old resolver: the human output is identical on all 430 fixtures, except the one noted below.
Behavior changes
test/bound-storage/alias-cycle-two-arrays.cand noted in the changelog. A follow-up could report such an access against every reachable array when all of them overflow.test/bound-storage/alias-chain-deep.c.Not related to this change: MultipleStores diagnostics come out in a run-dependent order on
test/false-positive-repro/stb-like-next-code-uninitialized.cwith the old binary too (#220).Verification
k = 5/6/8crc32_zwith the braided code (-U__ARM_FEATURE_CRC32on Apple arm64)deflate.c,infback.c,inffast.c,inflate.cexternals/simplecpp/simplecpp.cppata294079e6(-std=c++11)resolveArrayAllocaFromPointerInternalrun_test.pyctestunit testsNew fixtures:
alias-cycle-terminates.c(a clique of eight pointers in one component, plus a PHI of pointers),alias-cycle.c(two pointers assigned from each other),alias-chain-deep.c(66 chained pointers),alias-cycle-two-arrays.c(the ambiguous component).