Uniformization of bounds checking
This post documents my plans for improving the checkers that report out-of-bounds errors.
@NoQ @Xazax-hun @steakhal @ anybody else:
If you have any high-level feedback (suggestions, concerns, plans etc.), please share it here. If this rough overview looks acceptable, I’ll dive into creating concrete commits and uploading them for review.
I also have one concrete question: What should the analyzer do if it sees pointer arithmetic that produces an out-of-bounds pointer (which isn’t the legitimate past-the-end pointer, but a “really” out-of-bounds one)? This is undefined behavior according to the standard, but intuitively isn’t as serious as actually accessing out-of-bounds memory.
Overview of relevant checkers
The checker security.ArrayBound (previously known as alpha.security.ArrayBoundV2) has sophisticated logic for determining whether the used memory location is out-of-bounds, but only applies this in the “basic” context where “use” means the subscript, dereference or arrow operator.
We also have several other out-of-bounds checkers:
- alpha.security.ReturnPtrRange reports situations when a function returns an out-of-bounds pointer.
- alpha.unix.cstring.OutOfBounds reports out-of-bounds access by C string functions (including raw memory handling like
memcpy). - alpha.unix.cstring.BufferOverlap reports use of overlapping buffers in
memcpyand similar functions. This is not exactly an out-of-bounds checker, but needs to perform similar checks, so perhaps it’s worth to think about sharing logic with it. - alpha.core.PointerArithm is currently NOT a bounds checker (it just reports non-array locations participating in pointer arithmetics) but does similar things and there may be plans to generalize it for some sort of bounds checking.
- alpha.cplusplus.IteratorRange reports iterators used outside their valid ranges. (I’m not familiar with this checker, and AFAIK all the iterator checkers have serious technical debt.)
- osx.coreFoundation.containers.OutOfBounds is presumably another checker that does bounds checking, but I know nothing about it.
- Honourable mention goes to the function
isArrayIndexOutOfBounds()withinUndefResultChecker.cppwhich is only used to add " due to array index out of bounds" to the “The left/right operand of ‘X’ is a garbage value” reports.
Currently (November 2025) none of these checkers use the logic of security.ArrayBound; and many of them use the “traditional” logic, which was used in the old alpha.security.ArrayBound (V1) before its removal. (See appendix 1 for a description of this “traditional” logic and its drawbacks.)
Anatomy of a bounds check
Bounds checking can happen in two kinds of situations:
- Actual access where out-of-bounds offset triggers significant issues (e.g. pointer dereference, array subscripting, access through
memcpyand similar functions). - Hypothetical access where the out-of-bounds offset is suspicious but doesn’t cause immediate problems (e.g. performing pointer arithmetic, returning a pointer,
&array[idx]expressions).- In these situations it’s well-defined to create a past-the-end pointer.
- Forming other out-of-bounds pointers is still undefined behavior (but is intuitively “less severe” than actually dereferencing them).
The bounds check can emit three kinds of output:
- A. Bug report: when it finds an error, it should create a bug report:
- the message should describe the accessed memory region, the index/offset (if concrete) and the extent of the memory region (if concrete and the error is an overflow)
- the relevant values (index/offset, extent) should be marked as interesting if they are symbolic
- if the memory area is not dynamically allocated, we should mark its declaration with a note tag (security.OutOfBounds doesn’t do this yet, but alpha.security.ReturnPtrRange has this feature and I think we should adapt it)
- the error node may be fatal or non-fatal
- actual out-of-bounds access is definitely fatal; and although hypothetical out-of-bounds access is also undefined behavior, perhaps it should be a non-fatal error node
- B. Assumption: In some ambiguous situations (access may be legal, may be out of bounds), it is useful to assume that the access is legal and:
- produce a new state where the index/offset is definitely in bounds;
- attach a note tag that describes this assumption.
- (see Appendix 2 for two code examples where the assumptions are helpful).
- C. Nothing: In other cases (e.g. the access is definitely in bounds) the bounds check returns without doing anything.
To produce this output, the bounds checking algorithm consumes lots of input parameters:
- The
ProgramState. - The access kind: actual or hypothetical.
- Message fragment strings that describe the access operation (e.g. distinguish simple pointer dereference and a
memcpycall). - Strings that describe the accessed memory region (for message generation).
- The extent/size of the accessed memory region: either an SVal or a concrete number.
- The offset/index that is being accessed: either an SVal or a concrete number.
In many cases (e.g dereferencing a pointer) parameters 4-6. (accessed region, its extent, the offset) are all calculated from a single location SVal (i.e. pointer) by decomposing it into a {base region, offset} pair. However, there are also situations (e.g. applying the array subscript operator on an array with a statically known size) where these parameters are known separately, as e.g PR#159357 by Alejandro Alvarez proposes.
The bounds checking algorithm (as implemented in security.ArrayBound (V2) ) is composed of two or three subroutine calls:
- the first one checks the lower bound (index >= 0),
- the second checks the upper bound (index < size),
- finally – if needed – the third one checks whether the pointer is a past-the-end pointer (index == size).
This comparison subroutine is itself nontrivial – in fact, I’d say that it is the core innovation of the V2 array bounds checker. The main difficulty is that it needs to approximate a proper mathematical comparison with evalBinOp – and evalBinOp messes up everything with overflows and type conversions. The current implementation is “good enough” for use in production, but I have several long-term plans that would improve accuracy in a few corner cases.
The algorithm is complicated by several additional aspects:
- The offset/index and the extent/size may be measured either in bytes (char units) or as element counts. The current implementation converts everything to bytes for calculations, then converts back to element counts when it generates the messages (when possible), but it would be better to do the calculations/comparisons on element counts when it’s possible.
- In many cases it would be better to e.g. assume $x < y$ instead of $4 x < 4 y$ (where the ‘4’ is
sizeof(int)). The analyzer cannot see the connection between these two relationships (because strictly speaking, they are not equivalent due to overflow) – and $x < y$ is the one that would probably occur e.g. in a conditional statement. - If we convert the byte offsets to element counts (instead of the other way around), it would be more natural to handle cases where the extent is not a multiple of the element size (round down in the division). By the way currently the analyzer doesn’t report overflows if the beginning of the accessed object fits into the memory area.
- Avoiding the back-and-forth conversion might also simplify the code.
- In many cases it would be better to e.g. assume $x < y$ instead of $4 x < 4 y$ (where the ‘4’ is
- When the offset or the extent is tainted, the analyzer should switch to a “pessimistic” mode where ambiguous situations are also reported. These reports need different messages and we are planning to reassign them to new separate checker(s) under
optin.taint.*. - As the comparisons are made, the code must report the assumptions. This is needed for two things:
- composing the message when the check finishes with an assumption (output B);
- distinguishing “underflow or overflow” reports and “definitely overflow” reports.
- There are also a few tricks that make the message easier to read.
Suggested architecture
As each bounds checker needs most of the complications (which are described in the previous section), it is important to reuse the same code in all of them. There are several “customization points” where the various checkers need to introduce different behavior, so the natural architecture would be a base class and several subclasses.
As a rough draft I propose the following interface:
struct PtrDiff {
std::variant<NonLoc, int64_t> Amount; // or maybe just a NonLoc if we wrap known integers into nonloc::ConcreteInt
std::optional<Type> ElemType; // nullopt means byte offsets
bool isTainted(/* suitable context */) const;
}
// When a checker performs a bounds check, it constructs an instance of a
// subclass of this class, then calls the "main" method
// `performCheckAndTransition()` on it.
class BoundsValidatorBase {
protected:
BoundsValidatorBase(CheckerContext &C, ProgramStateRef State, std::string
AccessedRegionName, PtrDiff Extent, PtrDiff Offset)
: /* ... save everything into data members ... */ {}
////// Customization points -- defined by subclasses /////
virtual bool isActualAccess() const = 0;
// Probably there will be several methods of this kind
virtual std::string describeAccessOperation() const = 0;
virtual const BugType *getBugType(bool isTaintReport) const = 0;
// The constructor is also a customization point: subclasses should define
// their own public constructors with different parametrizations.
////// Shared logic /////
public:
// The "main" function: performs the bounds check, then emits the bug report
// or assumption (if needed).
void performCheckAndTransition(ExplodedNode *N);
protected:
// The comparison subroutine which already exists in ArrayBoundChecker, but
// should be generalized a bit to handle the PtrDiff type.
static std::pair<ProgramStateRef, ProgramStateRef>
compareValueToThreshold(ProgramStateRef State, PtrDiff Value,
PtrDiff Threshold, SValBuilder &SVB,
bool CheckEquality=false);
StateUpdateReporter SUR;
}
I’m planning to introduce this gradually:
- Add a new source file and header in the
Core(analyzer engine) directory and move the logic fromArrayBoundChecker.cppto these new files. (NFC) - Generalize and improve this new bounds checking logic to make it ready for use in other checkers (e.g. support doing the calculations with indices and not byte offsets).
- These may slightly improve the quality of
security.ArrayBoundresults as well.
- These may slightly improve the quality of
- Reimplement the other bounds checkers as wrappers around the shared bounds checking library.
Appendix 1: The “traditional” logic
Several checkers (ReturnPtrRange, cstring.OutOfBounds, isArrayIndexOutOfBounds()) use the “traditional” logic, which is copypasted from the old alpha.security.ArrayBound (V1) checker. Note that this V1 checker produced too many false positives, so it was was deleted from the codebase when security.ArrayBound (originally alpha.security.ArrayBoundV2) became stable.
These checkers use the method ProgramState::assumeInBoundDual() as the “backend” of bounds checking, which relies on weird mathematical trick: to check 0 <= Idx < UpperBound it assume()s that Idx + MIN < UpperBound + MIN where MIN is the minimal value of the array index type (which is always long long).
- This exploits that the analyzer models signed integer overflow as if it was well-defined.
- I had vague plans for e.g. including an
--assume-no-signed-overflowflag for the analyzer (because that would get rid of lots of false positives), but this abuse of the overflow modeling prevents that.
- I had vague plans for e.g. including an
- The comparison may be a signed comparison (by default) or an unsigned comparison (when either
IdxorUpperBoundis anunsigned long long, which enjoys precedence). - The mathematical trick is logically correct with a signed comparison, but with the unsigned comparison it sometimes produces incorrect results: e.g.
Idx == -3andUpperBound = 5ULLwould produce an “in bounds” result. - If one of
IdxandUpperBoundis concrete, while the other is symbolic, then the analyzer (hopefully) can follow the additions and deduce the correct range for the symbol. - However, if
IdxandUpperBoundare both symbolic, then I’m pretty sure that the analyzer won’t see the connection between e.g.Idx < UpperBound(which could appear e.g. in anif) andIdx + MIN < UpperBound + MIN(which is assumed during the bounds checking).
I think it’s important to switch to using the approach of security.ArrayBound (V2) with these checkers. We should also consider eventually eliminating ProgramState::assumeInBoundDual() from the codebase because its tricky approach causes too many problems.
Appendix 2. Why emit assumptions?
Assuming that the access is in bounds (when it may be in bounds and the index is not tainted) can improve the quality of results. The following code show two concrete examples:
int TenElements[10];
int example1(int arg) {
// We need to save our assumptions about the value of 'arg' to deduce that
// these two can't be in bounds simultaneously:
return TenElements[arg] + TenElements[arg + 10];
}
int example2(int arg, int arg2, bool flag) {
if (flag) {
TenElements[arg] = arg2;
}
// ...
if (arg > 20) {
// If an execution path where flag was true reaches this point, then the
// program is already in an undefined corrupted state (after overflowing
// TenElements) and this overflow is not modeled by the analyzer.
// Results from these paths would be inaccurate, so it is better if
// TenElements[arg] produces an assumption that prunes these paths.
}
}
If I recall correctly @steakhal told that the additional assumptions previously caused slowdowns in some cases (by slowing down the Z3 refutation in outlier cases), but if I understand correctly that issue was mitigated in other ways since then so it isn’t relevant anymore.