# Off-by-one bounds check

A size check that admits the boundary value, letting one index or one terminator past the end through

_Area: Memory corruption and native code. Seen in 6 disclosed reports; 4 confirmed bounties, median $1,400, range $500-$7,500; latest 2021-09-13. Updated 2026-10-23._

## How it works
A bounds check admits one value too many, so the last accepted index is one past the end. The usual mechanism is a wrong operator: the check uses `>` where `>=` was meant, so the boundary value itself passes. The broken assumption is that the check excludes the out-of-range value; when the limit is size-derived, the boundary value is exactly the out-of-range one. Two related shapes: a clear/init length computed one too large, writing one slot past a freshly allocated region; and a NUL terminator written at a length taken straight from a header field, where the bound is that length value rather than a size-derived constant.

## Where it shows up
- `>` against a size-derived constant where the boundary index is reachable: `ovector[99]` on a 99-int array, check `nameIndex > 49` admits `nameIndex==49`, then `nameIndex*2+1==99` [E3](https://bugbountylibrary.com/r/h1%3A47012.md).
- Monotonicity checks using strict inequality on addresses/offsets; equal values slip past and desync allocation from later copy size [E1](https://bugbountylibrary.com/r/p0%3ACVE-2021-1647.md).
- Stack-growth clear length `room - keep` instead of `room - keep - 1`, zeroing one slot past the realloc'd region [E4](https://bugbountylibrary.com/r/h1%3A194906.md).
- Fixed buffer sized to the digit count of an extreme value, ignoring sign and NUL terminator [E5](https://bugbountylibrary.com/r/h1%3A248601.md).
- `buffer[tmp_len]='\0'` where `tmp_len` comes from a manifest length field and is never bounded to `endbuffer - buffer`, especially on error/mismatch branches [E6](https://bugbountylibrary.com/r/h1%3A195586.md).
- Loop-invariant bounds checks not re-evaluated after each iteration advances a cursor [E2](https://bugbountylibrary.com/r/h1%3A1180252.md).

## How to review
For every check, name the exact buffer size and the maximum index admitted, then confirm max index < size. Test the boundary value itself, not just values above it. Check size-derived constants for the `-1` in their derivation. For clear/init lengths, confirm the length equals the allocated delta, not one more. For sprintf into fixed buffers, confirm the size covers sign plus NUL of the extreme value. For every NUL-termination write, confirm the length is bounded by the actual buffer end, not by a header field. For loops that mutate a cursor, confirm the bound is re-checked each iteration. Invariant: every index used to read or write is strictly less than the allocated length, on every path, including the boundary.

## Variants seen
- Boundary value accepted by `>` check against a constant [E3](https://bugbountylibrary.com/r/h1%3A47012.md).
- Equality slipping past a strict-inequality monotonicity check (a monotonicity variant, not classic off-by-one) [E1](https://bugbountylibrary.com/r/p0%3ACVE-2021-1647.md).
- Clear length one too large past a realloc'd stack region [E4](https://bugbountylibrary.com/r/h1%3A194906.md).
- Fixed buffer one byte short for INT_MIN plus NUL [E5](https://bugbountylibrary.com/r/h1%3A248601.md).
- NUL terminator at an attacker-controlled length field, past the buffer end [E6](https://bugbountylibrary.com/r/h1%3A195586.md).
- Cursor advancing past end inside a loop with no re-check [E2](https://bugbountylibrary.com/r/h1%3A1180252.md).

## Impact and severity
Outcomes ranged from heap/stack corruption and crashes to code execution [E1](https://bugbountylibrary.com/r/p0%3ACVE-2021-1647.md)[E2](https://bugbountylibrary.com/r/h1%3A1180252.md)[E4](https://bugbountylibrary.com/r/h1%3A194906.md)[E6](https://bugbountylibrary.com/r/h1%3A195586.md). Severity is highest when the boundary is attacker-reachable and the write is controlled; small overruns still corrupt adjacent metadata [E4](https://bugbountylibrary.com/r/h1%3A194906.md)[E5](https://bugbountylibrary.com/r/h1%3A248601.md)[E6](https://bugbountylibrary.com/r/h1%3A195586.md). The Flash case yielded a fake-length string usable for ASLR bypass or code execution [E3](https://bugbountylibrary.com/r/h1%3A47012.md). Keep severity framing separate for correctness-only overruns.

## Evidence
- E1: https://bugbountylibrary.com/r/p0%3ACVE-2021-1647.md
- E2: https://bugbountylibrary.com/r/h1%3A1180252.md
- E3: https://bugbountylibrary.com/r/h1%3A47012.md
- E4: https://bugbountylibrary.com/r/h1%3A194906.md
- E5: https://bugbountylibrary.com/r/h1%3A248601.md
- E6: https://bugbountylibrary.com/r/h1%3A195586.md
