How can an out-of-bounds array write at index equal to the length silently corrupt neighbouring data instead of failing?
answer
- the formula does not know what is valid
- something lives just past the last element
- hardware protects pages, not elements
- checked runtimes compare against a stored length
basics
~20 sThe address formula base + i × element-size works for any i, valid or not; index equal to the length lands one element past the array, on whatever is stored next. Unchecked runtimes just write there; checked runtimes compare the index against the stored length first and raise an error.
solid answer
~50 sAddress arithmetic has no notion of validity: `base + n * element_size` is a perfectly computable address that happens to sit one element past the block. The hardware will not object, because memory protection works at page granularity — kilobytes — and the address one element past an array almost always falls inside the same mapped region, often inside the same allocation. So the write succeeds and lands on whatever neighbours the array: another field, another variable, allocator bookkeeping. The corruption surfaces later, far from the bug, which is why these bugs read like ghost stories in postmortems. A checked runtime prevents this by storing the array's length and emitting a compare-and-branch before each access, raising a defined error on failure. That check is not free, but it is cheap — a predictable branch — and optimizers routinely hoist or eliminate it in loops that provably stay in range.
code
pseudocode · 4 linesbuffer = allocate_array(n) // capacity n, valid indices 0..n-1
for i in 0..n // inclusive bound: n+1 iterations
buffer[i] = read_sample()
// last iteration writes buffer[n], one element past the endgo deeper
Know that valid indices run 0 to length minus 1, that the loop shown writes one element too many, and that some environments raise an error while others silently corrupt memory.
Explain the mechanism: address math computes invalid addresses happily, hardware protection is page-granular so nearby overruns trap nothing, and checked runtimes add a compare against a stored length. Be ready to spot the inclusive-bound bug in a fragment.
Bring the postmortem instinct: corruption that surfaces far from its cause, allocator metadata damage that breaks later frees, and the security dimension — unchecked out-of-bounds writes are the buffer-overflow vulnerability class. Know why bounds checks measure cheap (prediction, elimination).
Own the policy question: where your organisation runs checked builds versus raw performance builds, what sanitizer coverage in testing buys against unchecked production code, and how you justify the few-percent check overhead against corruption and exploit risk.
## The bug in the fragment The loop in the example runs `i` from `0` through `n` *inclusive* — that is `n + 1` iterations against a buffer with valid indices `0` through `n − 1`. The final iteration writes to `buffer[n]`, one element past the end. This is the classic off-by-one, and what happens next depends entirely on which of two worlds the code lives in. ## Why the address is computable at all Indexing is pure arithmetic: `address = base + i * element_size`. The formula is just as happy to produce the address one slot past the block (`i = n`) or one slot before it (`i = −1`) as any valid address. Nothing in the arithmetic knows where the array ends — bounds are a bookkeeping concept, and someone has to do the bookkeeping. ## World one: raw memory, no checks In unchecked execution, the write to `buffer[n]` simply happens. Why doesn't the hardware stop it? Because hardware memory protection operates on *pages* — typically 4 KB or more. The memory-management unit can trap access to an unmapped page, but it has no idea that bytes 0–1999 of a mapped page are "your buffer" and bytes 2000–2001 are "someone else's counter". One element past an array almost always lands in the same page, frequently in the same allocation, so no trap fires. What actually sits there varies: an adjacent field of the same record, a neighbouring local variable, another object placed next by the allocator, or the allocator's own metadata for the block. Overwriting each has its own failure signature — a value that changes "by itself", a crash much later when the corrupted metadata is used to free memory, or in the worst case an exploitable vulnerability: unchecked out-of-bounds writes are the root of the buffer-overflow class of security bugs, where an attacker who controls the written data gets to overwrite something the program trusted. The defining property of this world is *distance between cause and effect*. The bug is in the fill loop; the symptom is a corrupted price field, or a crash three subsystems away, minutes later. That is exactly why postmortems of such corruption so often start with "the impossible happened". ## World two: checked runtimes A checked runtime stores the array's length alongside the data and compiles every access `buffer[i]` into roughly: `if i >= length (or i < 0): raise error; else: access`. The failure becomes immediate, local, and named — the program stops (or the error is handled) at the faulty access, with the offending index in hand. Mainstream managed runtimes — the JVM and the .NET CLR, for example — check every array index this way, while raw-memory languages like C and C++ define no behavior for the out-of-bounds case and rely on opt-in sanitizers to catch it during testing. ## What about index −1? Under unchecked arithmetic, `base − element_size` points just *before* the block — commonly at allocator metadata, so writing there tends to break a later free or allocation rather than crashing on the spot. And if the index variable is unsigned, −1 wraps to the maximum representable value, producing an enormous address that usually *does* hit an unmapped page and trap. Checked runtimes reject a negative index like any other out-of-range value. Note that some ecosystems define negative indexing as a *feature* meaning "count from the end" — that is a deliberate language semantic layered on top, not what the raw address math does. ## Is bounds checking expensive? Less than intuition suggests. The check is a compare and a branch that is almost always not taken, which branch predictors handle nearly for free. More importantly, optimizing compilers perform bounds-check *elimination*: in a loop whose induction variable provably stays within `0..length−1`, the per-iteration check is redundant and gets hoisted or removed entirely. The measured cost in real programs is typically a few percent at worst — small compared with the debugging and security cost of silent corruption. The honest engineering statement is: bounds checking is *cheap*, not *free*, and the hardware gives you page-granular protection, not element-granular protection.
- What does index -1 actually do?Unchecked, it addresses one element before the base — often allocator metadata, so the damage surfaces later when that memory is freed or reused. If the index type is unsigned, -1 wraps to a huge value whose address usually hits an unmapped page and traps. Checked runtimes reject it like any out-of-range index; ecosystems where -1 means 'last element' implement that as an explicit language feature, not raw address math.
- Isn't bounds checking too expensive to leave on in production?Usually not. Each check is a compare plus a branch that is almost never taken, which branch predictors make nearly free, and compilers eliminate checks in loops that provably stay in range. Measured overhead is typically a few percent at worst — far cheaper than debugging silent corruption or shipping an overflow vulnerability.
- Why doesn't the hardware catch the write one element past the end?Memory protection is page-granular — the unit is kilobytes, not elements. The address one slot past an array almost always falls in the same mapped page, often the same allocation, so no fault fires. A trap happens only when the stray address reaches an unmapped page, which is why small overruns are silent while wild ones crash.
A row of 100 numbered lockers at the end of a hallway: the numbering scheme happily describes where locker 101 would be, and if you shove a bag into that spot you are just piling it onto whatever the building keeps there.
saying these in an interview costs you the question
- Assumes any out-of-bounds access crashes immediately
- Claims the hardware bounds-checks individual array elements
- Cannot say what memory index equal to the length actually addresses
- Expects index -1 to mean the last element everywhere, rather than as a specific language feature