A buffer overflow writes beyond a buffer’s valid positions. Bounds are its permitted limits. Compare capacity with the number of bytes that would actually be stored. For a C string, a null terminator is an extra zero byte marking the end, so its storage requirement includes both content and terminator.
An out-of-bounds C access can cause undefined behavior: the language promises no particular result. A crash, an apparently normal run or corrupted data can therefore not be assumed from this drawing alone. The supplied Python model only judges the counts; it never performs the unsafe write.
Before you begin
- Recommended first
- Enough C and assembly to follow a compare and a jump, A beginner license check, Where practice is allowed, Memory and the stack, as a picture
- Estimated time
- 18 min reading, 16 min activity. Setup and optional practice may take longer.
- Where you work
- Use the printed memory and source models in a desktop or mobile browser, with paper or a text editor. No compilation, debugger, pwntools installation, mitigation change or executable is needed. The layouts are conceptual, not measured process memory. Optional later tools need a separately checked owned environment and budget.
- Without a lab
- All evidence needed for the activity appears on this page. Copy it into notes if downloads are unavailable. Read the labeled model and predict each result; a paper trace practises the reasoning while tool operation waits for a lab. Reading needs no JavaScript. Answer checking needs JavaScript and a secure context; saving progress also needs usable browser storage.
After this lesson you can:
- Show that six letters plus a terminator need seven positions.
- Mark write Q as the activity write that does not fit.
- Keep the C file's refused path as a source reading, not a run.
01Example
Worked ink model, different from the guided pad counts and independent question.
ASCII text purple: 6 bytes
Required null terminator: 1 byte
Total needed: 7 positions
Buffer ink capacity: 6 positions
Predicted model result: does-not-fit
Action: refuse before copying; no neighbor is written02A write that does not fit
Use needed bytes = content bytes + any required terminator. If need exceeds capacity, refuse or choose suitable storage; silently omitting the terminator does not create a valid C string. For non-ASCII text, character count and byte count can differ, so this worksheet states bytes explicitly.
In bounds-model.py, def defines judge; its arguments name capacity, payload length and the 0-or-1 terminator flag. It rejects a negative payload or an invalid flag, computes need, then returns a label after comparing need > capacity. These are deliberately bounded worksheet inputs; the function is not a general validator for every Python type or capacity.
Running a file that only defines this function prints nothing unless something calls it and displays the return. The historical record reports four direct judge calls matching the printed card; here you trace the same source without executing it.
Compare capacity with all bytes a write needs, including a required terminator, before predicting whether it fits.Model. A teaching model drawn by hand. It shows the idea, not real tool output.
Read the full explanation below.
ink buffer · 6 positions · active
purpleNeeded bytes, including the terminator
p u r p l e 6 positions needed; 6 available.
The six ASCII purple bytes alone use all six positions.
Follow the explanation
- The six ASCII purple bytes alone use all six positions.
- Include the required terminator: 7 needed exceeds capacity 6. Refuse before copy; neighbor remains unchanged.
Read every state and value
1. The six ASCII purple bytes alone use all six positions.
- Object
- ink buffer
- Sample
- purple
- Bytes shown
- p · u · r · p · l · e
- Needed
- 6
- Fit
- pending
- Lifetime
- active
- Selected Index
- 5
2. Include the required terminator: 7 needed exceeds capacity 6. Refuse before copy; neighbor remains unchanged.
- Object
- ink buffer
- Sample
- purple
- Bytes shown
- p · u · r · p · l · e · \0
- Needed
- 7
- Fit
- does-not-fit
- Lifetime
- active
- Selected Index
- Not established
"""Safe bounds model. Not an exploit.
A buffer is a fixed number of positions. judge() reports whether a
requested write fits. It does not perform the write, and it has no
neighbor to change.
terminator is 0 or 1. A 1 means one extra position for a trailing null,
the way a C string needs room past its letters. This file is not a C
compiler. Running it prints nothing, because it only defines judge().
"""
def judge(capacity, payload, terminator):
if payload < 0 or terminator not in (0, 1):
return "rejected-input"
need = payload + terminator
if need > capacity:
return "does-not-fit"
return "fits"
Historical 2026-10-12 model-function results: direct judge() calls matched the rows below.
This card preserves that record; the current activity traces source without running it. Running the file by itself still prints nothing.
Worked ink card: judge(6, 6, 1) -> does-not-fit
Six letters plus one terminator need seven positions. ink has six.
Activity pad card:
Write P: judge(8, 8, 0) -> fits
Write Q: judge(8, 9, 0) -> does-not-fit
Write R: judge(8, 7, 1) -> fits
Apply the capacity rule to each new request rather than copying a model result.
03Trace pad
The guided buffer pad has eight positions. Compare each requested byte count with that same capacity.
- Write P: eight content bytes plus zero terminator bytes need eight positions; this fits exactly.
- Write Q: nine plus zero need nine positions; this exceeds capacity and does not fit.
- Write R: seven plus one terminator need eight positions; this fits. State why counting only visible characters could miss the extra byte.
- Stop after the comparison. These labels are predictions from the safe model, not a captured overflow or crash.
04A C file that refuses
Read instrumented-demo.c as a refusal example. #define CAP 4 gives the comparison limit a name. const char sample[]="abcd" creates an array inferred from the four ASCII letters plus the literal’s null terminator, so sizeof sample is five bytes. The > comparison selects puts("refused") and return 1 before any copy.
Recorded sample: GCC13.3.0 built this owned source and it printed refused with exit status 1. That historical result is a refusal, not an overflow, memory map or mitigation test. This lesson does not repeat the build. A later execution would need the exact owned source and a separately checked isolated environment.
#include <stdio.h>
/* Reading copy. Not an exploit.
CAP is four positions. The sample array is the four letters in quotes.
A string literal also stores a null terminator, so the array is larger
than CAP. The function refuses before it would copy. On the card it
prints refused and returns 1.
*/
#define CAP 4
int main(void) {
const char sample[] = "abcd";
if (sizeof sample > CAP) {
puts("refused");
return 1;
}
puts("fits");
return 0;
}
05Optional challenge
For about ten optional minutes, annotate the source with the two counts and the branch that refuses. Then propose a safe requirement: validate the full needed size before a copy or provide sufficient storage. Do not change the original or compile it for this activity.
06Fresh cases for the checks
Use these fictional cases for the checks below. They have different inputs from the worked examples. Read them here or make notes; no installation or external service is needed.
Bounds case A: independent writes to empty buffersOwned fixture. An original file made for this course. Follow the activity's instructions in your own lab.
Every buffer has 5 byte positions with valid indices 0 through 4.
Write L: 4 ASCII letters plus one null terminator, starting at index 0.
Write M: 5 ASCII letters plus one null terminator, starting at index 0.
Write N: exactly 5 raw bytes with no terminator, starting at index 0.
Treat these as size requests; no copying occurs.
Bounds case B: fresh source ruleOwned fixture. An original file made for this course. Follow the activity's instructions in your own lab.
int store(int n) {
if (n >= 5) { return 2; }
/* copy n letters and one terminator into 5 positions */
return 0;
}
Assume n is a nonnegative integer. Source only: no executable or run exists.
Input for this case: n = 7.
07Explain your evidence
Optional written self-check: explain one decision using the supplied case. No tool run is required.
Check your explanation: include the terminator only when requested; identify the first invalid zero-based index; trace the guard before any copy; distinguish refusal predicted from source from observed execution.
08Common mistakes
If the terminator is counted twice, check whether the prompt’s write count already includes it. Add only what the stated model requires.
If fits is taken to prove all input is safe, narrow the conclusion to this capacity comparison; other requirements still exist.
If an oversized request is described as an observed crash, keep the model’s refusal separate from undefined behavior in a real unsafe program.
References
- CERT C: ARR30-C, out-of-bounds pointers and subscripts
Array bounds, out-of-range subscripts and pointer operations; invalid accesses can cause undefined behavior.
- CERT C: STR31-C, room for characters and the null terminator
String storage must have room for the character data and the null terminator.