Skip to content
VLSI Mentor

C Part I · Module 6 · Arrays

Array Bounds and What Happens When You Cross Them

The previous chapter ended on a deliberate cliffhanger. Every traversal it recommended ends by computing an index one element past the end of the array and then declining to use it, and the obvious question is what would happen if it did not decline. The answer is more uncomfortable than most beginners expect and more interesting than the usual summary of it. C performs no check, there is no error, and the program will usually carry on as though nothing happened — which is precisely what makes this the most consequential bug class in the language. This chapter explains why there is no check, what the standard actually says, which of the plausible stories about the consequences are true, and what tooling exists to make a bug that is invisible by default into one you can see in a report with exact byte offsets.

Foundation17 min readCPart IFundamentalsArrays

Part I · Module 6 · Chapter 6.4 · Array Bounds and What Happens When You Cross Them

1. There Is Nothing to Check Against

Start with why, because the rule makes far more sense derived than announced.

Chapter 6.1 established, with sizeof as evidence, that int samples[4] occupies exactly sixteen bytes and that all sixteen are elements. There is no stored count, no end marker, no header. The array is a run of storage and nothing else.

Now ask what a run-time bounds check would consult. Given samples[i] with i computed moments earlier, the machine would need the array's length — and the length is not there. The compiler knew it, from the declaration, while compiling. At run time it is gone.

C does not check array bounds, and the reason is visible in the layout: an array carries nothing a check could use.

This is not an oversight to be worked around resentfully. It is the direct consequence of arrays being exactly their elements and nothing more, which is also what makes them predictable, cheap and suitable for the hardware-adjacent work Part II is heading toward. The cost is that the bounds are your responsibility, completely.

Five cells in a row. The first four are the elements of samples: index 0 at bytes 1000 to 1003, index 1 at 1004 to 1007, index 2 at 1008 to 1011, and index 3 at 1012 to 1015. These four make up the whole array. The fifth cell, at bytes 1016 to 1019, is labelled samples index 4 and marked as not part of the array; it is storage belonging to something else or to nothing. An arrow from the array object points to the first element, where its storage begins.samplesthe whole array · 16 bytessamples[0]1000-1003samples[1]1004-1007samples[2]1008-1011samples[3]1012-1015samples[4]1016-1019 · notyoursstarts here12
Figure 1 — the boundary. The four elements of int samples[4] occupy bytes 1000 to 1015, and the array ends there. The storage immediately after is not part of samples; it may belong to another object, it may be unused, or it may be memory the program is not allowed to touch. Writing samples[4] means writing into that storage, and nothing in the array or in the generated code marks where the boundary is.

2. What the Standard Says

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    int samples[4] = {10, 20, 30, 40};
    size_t count = 4;

    int total = 0;
    for (size_t i = 0; i <= count; i++) {      /* <= instead of < */
        total = total + samples[i];
    }

This is Chapter 6.3's traversal with one character changed, and it is the single most common array bug in C. On the final pass i is 4, and samples[4] reads storage that is not part of the array.

The classification is short and absolute:

Accessing an array outside the range of its elements is undefined behaviour. The C standard imposes no requirements on what the program does.

Not "returns an unspecified value". Not "is implementation-defined". No requirements at all — on that access, and on the program as a whole.

That is why this chapter does not run the program and show you what it printed. Whatever number appeared would be one accident on one machine on one day, and presenting it as what C does would teach you something false. Chapter 1.3 set up this distinction and Chapter 2.3 used it; this is its sharpest application.

3. What the Compiler Catches

The compiler is not entirely helpless, and knowing exactly where its help stops is worth more than a general reassurance.

A constant index, on an array it can see:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    int a[4] = {0};
    a[5] = 99;
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
warning: array index 5 is past the end of the array (that has type 'int[4]')
    [-Warray-bounds]

It knows the type is int[4], it can see the index is 5, and it says so. Note that it is a warning — the program still builds.

A computed index:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    int a[4] = {0};
    int where = 5;
    a[where] = 99;
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
(no diagnostics)

Nothing. And a computed index is the interesting case — it is what loops produce, what arithmetic produces, and what input produces. The one the compiler catches is the one you were least likely to write.

SituationCaught at compile time?
Constant index, array declared in viewyes — -Warray-bounds warning
Index computed from a variableno
Index derived from a loop boundno
Index derived from inputno

4. What Actually Happens When You Run It

Since C requires nothing, what a particular run does depends on what happens to be next to the array and what the compiler chose to generate. Four outcomes are common, and none of them is guaranteed:

It silently corrupts another object. The storage past the array may belong to another variable. Writing there changes that variable, which then misbehaves somewhere else entirely — in a function that looks correct, for reasons that appear to have nothing to do with the array. This is the worst case, because the symptom and the cause are far apart.

Nothing visible happens. The storage may be padding or unused. The program produces the right answers and the bug stays hidden until the layout changes — a new variable, a different compiler, a different optimisation level.

The program is terminated. If the access reaches memory the operating system protects, the program is stopped. On a hosted system this is the lucky case: it is loud and immediate.

The program behaves in a way no storage story explains. A compiler is entitled to assume your program has no undefined behaviour, and to optimise on that basis. If a correct program could not have reached a certain point, code for that case may be transformed or removed. The result can be a program whose behaviour cannot be accounted for by any story about which bytes were overwritten.

5. Making It Visible

The situation sounds bleak and is not, because the checking C does not do at run time can be added by tooling during development.

Rebuild the §3 program with AddressSanitizer:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
cc -std=c17 -Wall -Wextra -Wpedantic -fsanitize=address -g bounds.c -o bounds

Running it now produces a report instead of a mystery:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ERROR: AddressSanitizer: stack-buffer-overflow on address 0x00016b876650
WRITE of size 4 at 0x00016b876650 thread T0
    #0 in main bounds.c:6

  This frame has 1 object(s):
    [32, 48) 'a' (line 4) <== Memory access at offset 52 overflows this variable

Read the last line against Figure 1. The object a occupies bytes 32 to 48 of its frame — sixteen bytes, four int elements, exactly as Chapter 6.1 established. The access was at offset 52, which is past 48. The tool is describing the same picture you already have, with real numbers.

The classic off-by-one from §2 produces the same kind of report:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ERROR: AddressSanitizer: stack-buffer-overflow
READ of size 4 at ...
    #0 in main loop.c:8

    [32, 48) 'samples' (line 4) <== Memory access at offset 48 overflows this variable

Offset 48 is one element past a sixteen-byte array — the exact index Chapter 6.3 §4 said the loop computes and never uses.

UndefinedBehaviorSanitizer reports it more briefly:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
cc -std=c17 -Wall -Wextra -Wpedantic -fsanitize=undefined -g bounds.c -o bounds
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
runtime error: index 5 out of bounds for type 'int[4]'

Two points about these tools, and both matter.

What you are reading is the sanitizer's report, not C's behaviour. The tool adds checks that are not otherwise there, detects the access, and tells you. Without it the behaviour remains undefined — the same program built normally makes no promise at all, and the fact that it ran quietly proves nothing.

They are development tools. They make the program slower and larger, so the normal arrangement is to build your tests and development builds with them and ship without. A test suite run under a sanitizer finds this class of bug on the day it is written rather than in the field.

6. The Habits

Nothing in this chapter changes what you should write. It explains why Chapter 6.3's recommendations were not fussiness.

Write i < count. It cannot produce an index of count. Chapter 6.3 §5 showed that i <= count - 1 can be far worse than it looks.

Take the count from the declaration. The sizeof idiom cannot disagree with the array; a repeated literal can.

Keep the count beside the array. Every time you handle an array you need its length from somewhere, and Chapter 6.1 established there is nowhere to get it except from the type or from a value you carry. Chapter 7.7 is where this stops being a convenience and starts being mandatory.

Validate an index that came from outside. An index derived from input, a packet field or a calculation is exactly the case the compiler cannot check. Check it against the count before using it — once, at the point it enters your code.

Build your tests with a sanitizer. It is one flag, and it converts the most dangerous bug class in C into a report with a line number.

7. What You Should Be Able to Say Now

  1. C does not check array bounds, because the array stores nothing a check could consult — Chapter 6.1's figure shows all the bytes are elements.
  2. An out-of-range access is undefined behaviour: the standard imposes no requirements.
  3. That is why no out-of-range result is printed in this chapter — any value shown would be one accident, not a rule.
  4. The compiler warns about a constant out-of-range index on a visible array (-Warray-bounds) — and it is only a warning.
  5. It catches nothing for a computed index, which is the case loops and input produce.
  6. Possible consequences include silent corruption of another object, no visible effect, termination, and transformations based on the assumption that UB does not occur.
  7. "It crashes" and "it gives garbage" are both wrong as descriptions — nothing is promised, including those.
  8. A program that appears to work with an out-of-range access has not got away with it.
  9. AddressSanitizer reports the overflow with byte offsets that match the layout; UBSan reports the index and the type.
  10. Those reports are the tool's, not C's. Without the tool, the behaviour is still undefined.

8. Exercises

Exercise 1 — name the valid range. For each declaration, give the valid indices and the first index that is not valid.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int a[1];
int b[10];
char c[256];
double d[3];

Exercise 2 — find the out-of-range access. Each of these reads or writes outside its array. Say which index does it and on which pass.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int v[5];
for (size_t i = 0; i <= 5; i++) { v[i] = 0; }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int v[5];
for (size_t i = 1; i <= 5; i++) { v[i] = 0; }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int v[5];
for (size_t i = 5; i > 0u; i--) { v[i] = 0; }

Exercise 3 — predict the diagnostics. For each, say whether -Wall -Wextra -Wpedantic reports anything, and why.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int a[4]; a[4] = 1;
int a[4]; int k = 4; a[k] = 1;
int a[4]; for (int i = 0; i <= 4; i++) { a[i] = 0; }

Exercise 4 — classify four claims. For each, say whether it is accurate, and correct it if not.

  • an out-of-range read returns an unspecified value
  • an out-of-range write always crashes the program
  • the behaviour of an out-of-range access is undefined
  • if the program produces the right answer, the access was harmless

Exercise 5 — read the report. An AddressSanitizer report says [64, 104) 'buffer' (line 12) <== Memory access at offset 104 overflows this variable. Say how many bytes the array occupies, how many int elements that is on a 4-byte-int implementation, which index was accessed, and what the correct last index would have been.

Exercise 6 — explain the silence. A colleague says their program "can't have an array bug, because it compiles with no warnings and runs fine." Give two reasons that is not evidence, using §3 and §4, and name the one change to their build command that would actually test the claim.

Exercise 7 — repair and justify. This is meant to copy the last four readings into a smaller array. Identify every out-of-range access, repair it, and state the valid index range for both arrays.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int source[8] = {1,2,3,4,5,6,7,8};
int recent[4];

for (size_t i = 0; i <= 4; i++) {
    recent[i] = source[4 + i];
}

Exercise 8 — validate an index. A value selected arrives from outside your code and will be used as settings[selected], where settings has 6 elements and selected is an int. Write the check you would perform before the access, say which two conditions it must test and why both are needed, and explain what you would do if it fails.

9. Summary

  • C performs no bounds check, because Chapter 6.1's layout leaves nothing to check against.
  • An out-of-range access is undefined behaviour — no requirements at all.
  • The compiler catches a constant out-of-range index on a visible array, as a warning, and catches nothing computed.
  • Consequences range from silent corruption of a neighbour, through no visible effect, to termination — and to transformations that assume the access never happened.
  • It does not reliably crash and it does not "return garbage" — both are outcomes, not rules.
  • Appearing to work is not evidence of correctness.
  • AddressSanitizer and UBSan turn it into a report with byte offsets and line numbers; their output is the tool's, not C's.
  • The habits: i < count, the count from the declaration, carry the count with the array, validate indices from outside, and build tests with a sanitizer.

Every array so far has been a single run of elements, which is the right picture for a list of samples and the wrong one for anything with two dimensions — a grid of pixels, a table of calibration values indexed by gain and by channel, a matrix. C can express those, and the way it does is not a new kind of object but the existing one applied to itself. The next chapter builds that, and the layout it produces is one you can already predict from Chapter 6.1's figure, which is exactly why it is the last chapter of this module rather than the first.

Standards & specifications

Governing standard
ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)

Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C Programming curriculum.