Repository navigation
fix(stack-buffer): check what callees access through a stack array's address - #267
Merged
Merged
Conversation
… parameters A summary lists, for each pointer parameter of a function, the loads, stores and memory intrinsics of a constant length made through it, at a constant offset from the address it holds on entry, on every run of the function that returns: the block of the access post-dominates the entry. The walk follows GEPs with a constant offset, the variables the pointer is stored in, and the calls it is passed to, whose own accesses it shifts by the offset. For each parameter it keeps the reads and the writes that reach furthest either way, with the place of the access. An access on some runs only, at a variable offset, or through a function pointer is left out: a summary lists accesses that happen, and never shows that a call is safe. A byval parameter points to the callee's own copy and gets no accesses. Summaries are computed callees first, over each cycle of calls in rounds, and across modules in rounds. A symbol defined once, by a definition the linker cannot replace, publishes its summary. Offsets past 2^32 bytes, and rounds that do not settle within their budget, make a summary incomplete; the accesses it lists still happen. The variables are read back through FunctionFacts::loadsReadingStore, the loads that read exactly what a store wrote. A local slot written by that store alone, whose address is only loaded from, needs no MemorySSA: only the loads the store dominates read it. Any other slot takes the MemorySSA clobber, as in #265. Unit tests cover the slot shortcut against a call or an alias that rewrites the slot and against a load before the store, the accesses left out, the budgets, incomplete summaries, byval, and across modules a chain, a weak and a twice-defined function, and a cycle. Nothing reads the summaries yet. Refs #266
…address Since a store of an element's address stopped counting as a write (#264), `char* p = &buf[8]; f(p);` got no report when f writes *p, and #265 only follows the pointer to accesses in the same function. A call that receives the address of a stack array, computed from the array at a constant offset, is now checked against the callee's access summary. The address may reach the call directly, through the variables #265 follows, or at a constant step from such a variable (`char* p = buf; f(p + 8);`), whose offset is counted in bytes; the loads and stores through a step stay with their own GEP. An access outside the array is reported on the call as a potential overflow, with its bytes, the function that makes it and its place. The constant index of the address may be in bounds: `&buf[7]` passed to a function that writes p[1] overflows. Across the files analyzed together, the summaries are published like the size-minus-one wrappers, and reach the calls to declarations. collectAccesses reads the variables back through FunctionFacts::loadsReadingStore. callee-access-write.c and callee-access-read.c cover the six criteria of #266 for writes and for reads, steps from a variable, and multi-byte reads past the end from a valid start. callee-access-calls.c covers moves and copies of the parameter in the callee, a write before the array, and cycles of calls. check_stack_buffer_callee_cross_tu runs a chain over three files in both orders, a weak definition and a static homonym. Fixes #266
Base automatically changed from
fix/stack-buffer-access-through-pointer
to
main
November 8, 2026 20:22
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
#264 removed the misplaced report on the store of
&buf[8], and #265 restored the report for accesses in the same function. A write in a callee still got none (#266). This PR checks a call that receives the address of a stack array against what the callee is known to access through it:A summary per function (
ParamAccessSummary). For each pointer parameter, it records the loads, stores and memory intrinsics of a constant length made through it. Each access is at a constant offset from the address the parameter holds on entry.Only accesses on every run of the callee count. An access counts when its block post-dominates the entry. An access behind a condition is left out of the summary, whatever the call passes; the guard is not evaluated at the call site. Take
write_if, which writes*ponly when its second argument is set:write_if(&buf[8], 0)gives no report for that reason;write_if(&buf[8], 1), although it writes past the end.A summary lists accesses that happen. It never shows that a call is safe. Nothing is concluded from an access it does not list.
The check at the call. It applies when the address is computed from a stack array at a constant offset. The address may reach the call in three ways:
char* p = buf; f(p + 8);), counted in bytes. The loads and stores through a step stay with their own GEP.An access outside the array is reported on the call as a potential overflow. The report gives the bytes, the function that makes the access, and its place. The constant index may be in bounds:
&buf[7]passed to a function that writesp[1]overflows.Across files. The summaries follow the pattern of the size-minus-one wrappers: rounds over the modules analyzed together, and publication only for a symbol defined once, by a definition the linker cannot replace. A weak definition and a static homonym publish nothing.
Budgets. Offsets past 2^32 bytes, and rounds over a cycle of calls (in a module or across modules) that do not settle within 8, make a summary incomplete. The accesses an incomplete summary lists still happen, and a caller that uses it becomes incomplete too.
Reading a variable back goes through
FunctionFacts::loadsReadingStore, shared with fix(stack-buffer): check accesses through a local pointer to an array element #265'scollectAccesses.Diagnostic changes
The analyzer's full output was compared on every fixture, in the default and smt-z3 passes. Diagnostic blocks were compared regardless of their order.
callee-access-write.c,callee-access-read.c,callee-access-calls.c, andcross-tu-callee-access-static.c. No output of an existing fixture changes. This includes those of fix(stack-buffer): check accesses through a local pointer to an array element #265, whosecollectAccessesnow goes throughloadsReadingStore.cross-tu-callee-access-use.c, where c59f696 still reports the store of&buf[8](the misplaced report feat(stack-buffer): bound stack array indexes with ScalarEvolution on a promoted copy #264 removed).leaf_write_badchanges, from silent to detected in all five. The 20 safe cases stay silent. The other 9 interprocedural cases stay silent too: they need variable offsets, return values,char**, struct fields or function pointers.Tests
test/bound-storage/callee-access-write.candcallee-access-read.c, with strict details. They cover the six criteria of StackBufferOverflow: a callee's write past the end of its caller's stack array is not reported #266 for writes and for reads:buf + 7;buf + 8;They also cover:
p + 8andend - 1from pointer variables;buf + 6, and two ints from&values[3].callee-access-calls.c:check_stack_buffer_callee_cross_tuinrun_test.py, in both passes:testParamAccessSummary:Each guard fails a test when removed:
--endfalse positive that fix(stack-buffer): check accesses through a local pointer to an array element #265's unit test pins);Verification
python3 run_test.py --jobs=4 --no-cachepasses 2696/2696. It covers both passes, the unit tests and the new cross-TU check, and ctest passes 2/2. Environment: Linux x86_64, LLVM 20.1.8, Z3, the CI's CMake options.-Wunused-resultin the unit tests. clang-format is clean.ci.ymlonly runs for pull requests intomain, so the test jobs run here once the stack is retargeted.Cost
infinite-recursion*.cpp) did not reproduce over five runs each. Medians: 0.51 s for fix(stack-buffer): check accesses through a local pointer to an array element #265 and 0.42 s here, and 0.41 s and 0.44 s.Limits
Accesses on some runs of the callee only are left out of the summary. They are silent because of that exclusion, not because the guard is evaluated at the call site:
write_if(&buf[8], 1)writes past the end and gets no report, likewrite_if(&buf[8], 0), which does not write;if (out) *out = x;and for a recursive call under a condition.Evaluating the callee's guards with the call's arguments is left for a later change.
Variable offsets and lengths are not followed:
p[n],memset(p, 0, n), or a length passed as an argument.Pointers that leave the parameter are not followed: through a return value, a
char**out-parameter, a struct field, or a function pointer.A caller's guard proven false keeps the report with the "appears unreachable" note, as for a direct access. This PR keeps that policy; how dead code is reported is a separate decision.
Objects: stack arrays only, with constant addresses. Global arrays and VLAs are not checked. For an array field reached through a decayed pointer, the bound is the whole struct.
A step applied directly to an element address (
f(&buf[2] + 6)) is not followed.f(buf + 8)andchar* p = &buf[2]; f(p + 6);are.A symbol defined in several files, or weakly, publishes no summary.
"On every run" (
StackBufferOverflow.EveryRun) is not used for these reports yet. Every report is "potential".Fixes #266