Skip to content

fix(stack-buffer): check accesses through a local pointer to an array element - #265

Merged
SizzleUnrlsd merged 1 commit into
mainfrom
fix/stack-buffer-access-through-pointer
Nov 8, 2026
Merged

SizzleUnrlsd merged 1 commit into
mainfrom
fix/stack-buffer-access-through-pointer

Conversation

@SizzleUnrlsd

@SizzleUnrlsd SizzleUnrlsd commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #264, itself stacked on #262. The base branch is feat/stack-buffer-index-bounds.
Merge this PR right after #264, before any release. #264 alone no longer reports char* p = &buf[10]; *p = 42; at all.

Summary

#264 stopped counting a store of an element's address (p = &buf[10]) as a write to that element. That report was misplaced, but it was the only one for a real overflow through p. This PR checks the access through the pointer variable, where it happens:

char buf[10];
char* p = &buf[10]; // no report: forming the address is valid
*p = 42;            // reported here, alias path buf -> arrayidx -> p

The fix lives in how StackBufferAnalysis collects the loads and stores that use an address. The index-bounds pilot is unchanged.

  • Which addresses are followed: only those computed from the array itself (&buf[i], buf + i, an array field of a struct). Their index counts from the array's start. A step from a pointer variable (--end) is not followed.
  • Which loads stand for the address: a load of the variable counts when its MemorySSA clobber is the store of that address to that variable, with the same type. tryGetConstFromValue and the SMT encoding of loads already rely on this contract (fix(analysis): read local constants from their MemorySSA clobber #262).
  • Why the index is the right one: the clobber dominates the load, so on every path from the store to the load the index keeps the value it had when the address was formed. The pilot and the heuristics then judge the index at the access, under the conditions the access runs on, as for a direct buf[i].
  • Which loads are left out: a load that may read another store has a merge for its clobber. Examples are the next iteration of a loop, or code after a branch that assigns the variable.
  • Copies (q = p) are followed the same way.
  • Report: the alias path names each variable, as for ptr = buf; ptr[i].

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.

Tests

  • test/bound-storage/access-through-pointer-variable.c, with strict details, 7 reports:
    • true overflows: a write through p, a read through a copy q = p, and buf + 10;
    • a reassignment each way: &buf[10] then &buf[9] gives no report; &buf[0] then &buf[10] is reported;
    • in a loop: an access on every iteration reaches 10 on every run, and one guarded by i > 4 may reach 10;
    • across iterations: a write through the address the previous iteration formed gives no report, and one after a do/while loop reaches 10 on every run.
  • Already covered by feat(stack-buffer): bound stack array indexes with ScalarEvolution on a promoted copy #264, still passing: forming &buf[10] without dereferencing it, and a conditional access through a pointer that is safe (element-address-is-not-a-write.c).
  • Unit test: --end from &buf[10] reads buf[9] and is not reported (testIndexBoundsPerAccess).
  • Each guard fails when it should:
    • accepting any load of the variable reports the in-bounds reassignment and the previous-iteration write, the latter as "reaches 10 on every run";
    • not following variables loses all 7 reports;
    • following from any address breaks the unit test.

Verification

  • Full suite: python3 run_test.py --jobs=4 --no-cache passes 2653/2653. 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.
  • Build warnings: none new; the only warning is the existing -Wunused-result in the unit tests. clang-format is clean.
  • CI: ci.yml only runs for pull requests into main, so the test jobs run here once the stack is retargeted.

Cost

Measured per process on the 936 fixture runs, both passes:

MemorySSA is built only in functions that store an address computed from an array and then load the variable.

Limits

  • Indexing a pointer variable: p = &buf[10]; p[0] = 1; gets no stack-buffer report. p[i] resolves to buf but counts i from the array's start, as before this PR. The offsetof/container_of check reports it as an invalid base.
  • Variables assigned on some paths only are not followed. if (c) p = &buf[10]; *p = 1; is not reported.
  • Variables a call may write, or whose address escapes, are not followed. MemorySSA takes the call for the clobber.
  • Addresses passed to another function are not checked in the callee. char* p = &buf[8]; f(p);, where f writes *p, was reported before feat(stack-buffer): bound stack array indexes with ScalarEvolution on a promoted copy #264 only by the misplaced report on the line that forms the address. That report is gone, and the write in f has none. This PR restores the report only for accesses in the same function. The callee case is tracked in StackBufferOverflow: a callee's write past the end of its caller's stack array is not reported #266. f(&buf[8]) passed directly was never reported.
  • Calls through the pointer (memset(p, …)) are not checked, as for a direct address.
  • Global arrays indexed by a constant: &g[10] is a constant expression, not an instruction, and is not seen. Neither is g[10] used directly.

… element

Since a store of an element's address stopped counting as a write,
`char* p = &buf[10]; *p = 42;` got no report at all. The access through
`p` is now checked where it happens, at `*p = 42`, with `p` in the
alias path.

Only addresses computed from the array itself are followed (`&buf[i]`,
`buf + i`, an array field of a struct): their index counts from the
array's start. A load of a variable stands for such an address when its
MemorySSA clobber is the store of the address to that variable, the
contract tryGetConstFromValue already relies on. That store dominates
the load, so the index still holds the value it had when the address
was formed, and the pilot and the heuristics judge it at the access,
under the conditions the access runs on. A load that may read another
store, such as one in the next iteration of a loop or after a branch
that assigns the variable, is not followed. Copies from one variable to
another are followed the same way.

access-through-pointer-variable.c covers a write, a read through a
copy, `buf + 10`, a reassignment each way, an access on every iteration,
a guarded one, one through the address the previous iteration formed,
and one after the loop. A unit test checks that a step back from a
pointer variable is not taken for an index of the array.
Base automatically changed from feat/stack-buffer-index-bounds to main November 8, 2026 20:07
@SizzleUnrlsd SizzleUnrlsd reopened this Nov 8, 2026
@SizzleUnrlsd
SizzleUnrlsd merged commit 4a8810a into main Nov 8, 2026
21 checks passed
@SizzleUnrlsd
SizzleUnrlsd deleted the fix/stack-buffer-access-through-pointer branch November 8, 2026 20:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant