Repository navigation
feat(stack-buffer): bound stack array indexes with ScalarEvolution on a promoted copy - #264
Merged
Merged
Conversation
The six places that report an access repeated the same ten fields and told loads from stores apart twice. Collect the loads and stores of the GEP once and fill the common fields in one helper, so the next change adds a field in one place. The reports are unchanged.
…write The overflow check counted every store using the GEP as an access to the array, including a store of the address itself, as in `char* end = &buf[10]` or the end pointer of a range-based for. Forming a pointer one past the end is valid, so these reports were false positives. Only a load or store whose pointer operand is the GEP counts now. A write through such a pointer kept in a local variable is no longer reported. It was reported only by this mistake, on the line that formed the address: the check does not follow pointers through memory.
PromotedValues answers value questions about the original IR on a copy of it where promotable locals live in registers, so ScalarEvolution sees loop counters. The analyzed IR is not changed. The copy is sanitized so its facts hold for the original program: - each promotable slot starts from an opaque value, so a read that may be uninitialized stays unknown; - wrap and poison flags, optnone, mustprogress, willreturn and the loop metadata are dropped; - functions with lifetime markers are left out. It holds the module's declarations, and copies one function body at a time, on its first query. Index ranges use the endpoint method: affine recurrences with a no-wrap ScalarEvolution proves, in loops that exit only from their header, with every input bounded by the program and not by its type. Anything else is unknown.
…dexes On `buf[i]` straight on a stack array, the pilot decides each load or store through the address whenever the copy establishes the index values where that access runs, so the access's own conditions count: - an index proven in bounds gets no report; - an index range past the end gets a potential report, as before; - an index the access takes past the end on every run of the function that returns gets a Warning under StackBufferOverflow.EveryRun or NegativeStackIndex.EveryRun, without "potential"; - an access in a loop that runs no iteration keeps its report and gets the unreachable-code note. The heuristics decide the other accesses, as before. Seven fixtures change with it. Nine reports on loops with a constant count become "on every run" at the same place, `buf[-3]` in ranges_test.c is now found, and a dead access in bound-storage-if-statement.c reads index 11, not 10.
The 22 loop cases the pilot was built on, one per file: 11 safe loops and 11 out-of-bounds variants of them. Six more cover its scope: an index that may be uninitialized, a trip count bounded only by its type, a loop going below zero, an access that does not run on every iteration, an index starting from the value a previous loop left, and an address formed one past the end next to a write that goes there.
This was referenced Nov 7, 2026
Closed
| bool endpoints(ScalarEvolution& scev, const LoopInfo& loops, const SCEV* value, | ||
| const BasicBlock* block, SmallVectorImpl<const SCEV*>& out) | ||
| { | ||
| const auto* recurrence = dyn_cast<SCEVAddRecExpr>(value); |
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
Stack-array overflow checks on
buf[i]used syntactic heuristics on the -O0 IR, where loopcounters live in memory. This PR adds a pilot that bounds those indexes with ScalarEvolution.
ScalarEvolution runs on a promoted copy of each function. The analyzed IR does not change.
Shared infrastructure:
PromotedValues(include/analysis/PromotedValues.hpp).optnone,mustprogress,willreturnand the loop metadata are dropped;[min, max], with a flag for each endpoint that the access takes on every run of the function that returns.StackBufferAnalysisconsumes it on one scope only: a GEP[0, i]straight on a stack array's alloca. Aliases, struct fields, globals and VLAs keep the heuristics. The pilot answers for each load or store through the address, under the conditions that access runs on:StackBufferOverflow.EveryRunandNegativeStackIndex.EveryRun, without "potential"Fix: a store of an element's address is not a write to it. The check took any store using the GEP for an access, including
char* end = &buf[10]and the end pointer of a range-basedfor. Only a load or store whose pointer operand is the GEP counts now. Fixes False positive: StackBufferOverflow on the end of a range-based for over a local array #206.Diagnostic changes
The analyzer's full output was compared on every fixture, default and smt-z3 passes, against #262's head (c59f696). Diagnostic blocks were compared regardless of their order. The two passes change in the same way.
Seven existing fixtures change. No warning disappears from them.
bound-storage-for-statement.c(2);indirection-profonde-aliasing.c(2; the access throughptrstays potential);ne-loop-guard-last-value.c;ranges_test.c;smt-path-loop-counter-is-kept.c;security/buffer-overflow/01_buffer_overflow.c.ranges_test.c:147,for (i = -3; i < 5; ++i) buf[i] = …, writesbuf[-3]. The fixture's comment already said so.bound-storage-if-statement.creads index 11 instead of 10 on a dead access (i == 11).One false positive disappears, from the address fix:
offset_of-container_of/range_for_pointer_slot_induction_no_invalid_base.cppno longer reports the range-for end pointer&cExtensions[2]. That fixture does not pin stack-buffer diagnostics.Tests
test/bound-storage/loop-index-bounds/:test/bound-storage/element-address-is-not-a-write.ccovers the end-pointer idiom and an address formed in a loop but written only below the bound.testPromotedValueschecks the range, the endpoints, the proof, the lifetime-marker exclusion, and that the original IR keeps its slots, loads and flags.testIndexBoundsPerAccessuses one address shared by three accesses with different guards. Only the two that may run withi == 10are reported, and only the unguarded one as "on every run". A store of the address alone is not reported.testIndexBoundsPerAccess;Verification
python3 run_test.py --jobs=4 --no-cachepasses 2637/2637. It covers both passes and the unit tests, and ctest passes 2/2. Environment: Linux x86_64, LLVM 20.1.8, Z3, the CI's CMake options.test/bound-storageandtest/offset_of-container_of. Only reports on address stores disappear: the range-for end pointer and those in the new fixtures.Cost
Measured per process (CPU time, peak RSS) on the 934 fixture runs, both passes:
param-bad.cwith Z3 (the solver checks the new potential report)On a synthetic module of 3000 functions (macOS, two runs), user CPU rises from 0.97 s to 1.23 s and peak RSS from 290–310 MB to 325–331 MB.
Limits
buf[i]on a stack array with a variable index. A constant out-of-bounds index gets at most the unreachable note.break,returnorexit()makes the index unknown;wrap-unknown.cstays silent;p = &buf[10]; *p = 0;) is not reported. It was reported before only through the address-store mistake, on the line that formed the address. fix(stack-buffer): check accesses through a local pointer to an array element #265 restores the report at the access when it is in the same function. A write by a callee through such a pointer (char* p = &buf[8]; f(p);) loses its report with this PR and has none after fix(stack-buffer): check accesses through a local pointer to an array element #265: tracked in StackBufferOverflow: a callee's write past the end of its caller's stack array is not reported #266.