---
title: 'Bounds Checking'
source: 'https://academia.sh/en/courses/c-memory/bounds-checking'
course: 'Memory and Pointers in C'
language: en
updated: '2026-09-26T03:02:43+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - "Recognizes record_create's limit parameter as a contract given by the caller"
    - "Copies a source of unknown length into a fixed-size buffer with snprintf's truncation notification"
    - 'Measures that the limit is written in the caller, not the source, with two different limit values'
---

# Bounds Checking

record_create's limit parameter takes its length not from the source but from the caller; the lesson runs how this parameter prevents a buffer overflow, snprintf's truncation notification, and whose responsibility the length is.

The previous lesson measured `record_create`'s cleanup on the error path; that error path
arose from a single condition: the given name's length exceeded the `limit` parameter. This
lesson puts that parameter itself at the center. `limit` is not a constant of the language or
of the `owner.c` unit — it is a value the **caller** gives on every call, and it is exactly
that value that prevents a buffer overflow.

The previous two lessons measured the "unowned" and "callee" classes; this lesson moves to
the third class: **caller**. The difference comes down to one question — who prevents a
buffer overflow? In the previous lesson the answer was "the called function itself":
`record_create` bounded the block it allocated itself. In this lesson, neither of the two
functions measured (`record_create`'s `limit`, `bounded_copy`'s `dest_size`) **sets its own**
limit — the caller sets the limit and the function only **complies** with it. Whether the
limit is correct is the caller's responsibility, not the function's.

## The Caller Gives the Length

Looking at the signature `record_create(const char *name, size_t limit)`, where `limit` comes
from is not visible; the signature only takes a `size_t`. But it is written in the comment in
the contract's header file: the function checks `name`'s length against `limit` and returns
`NULL` if it does not fit. This check is not part of the language — `strlen` knows no limit,
`memcpy` knows no limit; the only thing that knows the limit is the comparison inside
`record_create`'s own body.

What needs attention at this point is **where** `limit` comes from. `size_t limit` appearing
in the signature does not mean the value is constant or safe — the caller may derive it from
a constant, from user input, or from another computation. `record_create` does not check
whether the `limit` given to it is **correct**, it only checks whether `name` fits that
`limit`. If the limit itself is wrong — for example larger than the real destination's
capacity — the function still works correctly, but what the check protects is no longer the
real destination, it is the number the caller wrongly reported. This distinction is taken up
again in the "Bounding Measurement" section at the end of the lesson.

```c
// counter.h — a wrapper that counts every allocation and every free
#ifndef COUNTER_H
#define COUNTER_H

#include <stddef.h>

void *count_alloc(size_t size);
void *count_grow(void *old, size_t size);
void count_free(void *block);
int allocated_count(void);
int freed_count(void);

#endif
```

```c
// counter.c — the counters live only in this unit
#include <stdlib.h>
#include "counter.h"

static int allocated = 0;
static int freed = 0;

void *count_alloc(size_t size) {
    void *block = malloc(size);
    if (block != NULL) { allocated++; }
    return block;
}

void *count_grow(void *old, size_t size) {
    int new_block = (old == NULL);
    void *grown = realloc(old, size);
    if (grown != NULL && new_block) { allocated++; }
    return grown;
}

void count_free(void *block) {
    if (block != NULL) { freed++; }
    free(block);
}

int allocated_count(void) { return allocated; }
int freed_count(void) { return freed; }
```

```c
// owner.h — the record's inside is hidden, the ownership contract is in the comments
#ifndef OWNER_H
#define OWNER_H

#include <stddef.h>

typedef struct Record Record;

/* The caller owns it and frees it with record_destroy. If the name does not
   fit the limit, returns NULL; the function frees what it allocated so far. */
Record *record_create(const char *name, size_t limit);

/* Frees the record together with the name buffer inside it. */
void record_destroy(Record *r);

/* The record owns it: the returned pointer is valid until the record is
   destroyed, and the caller does not free it. */
const char *record_name(const Record *r);

/* The caller owns it: the returned copy is freed with count_free. */
char *record_name_copy(const Record *r);

#endif
```

```c
// owner.c — the unit where the contract is carried out
#include <string.h>
#include "owner.h"
#include "counter.h"

struct Record {
    char *name;
    size_t length;
};

Record *record_create(const char *name, size_t limit) {
    Record *r = count_alloc(sizeof *r);
    if (r == NULL) { return NULL; }

    r->length = strlen(name);
    if (r->length >= limit) {
        count_free(r);
        return NULL;
    }

    r->name = count_alloc(r->length + 1);
    if (r->name == NULL) {
        count_free(r);
        return NULL;
    }
    memcpy(r->name, name, r->length + 1);
    return r;
}

void record_destroy(Record *r) {
    if (r == NULL) { return; }
    count_free(r->name);
    count_free(r);
}

const char *record_name(const Record *r) { return r->name; }

char *record_name_copy(const Record *r) {
    char *copy = count_alloc(r->length + 1);
    if (copy != NULL) { memcpy(copy, r->name, r->length + 1); }
    return copy;
}
```

**RO7.** `record_create` can give two different results when called with the same `name` but
two different `limit` values — what decides the result is not `name`, it is the `limit` the
caller gives. The measurement below shows exactly this: the same name, with one narrow and
one wide limit.

This observation looks at `record_create` from a deliberately different angle than the
previous lesson's measurement of it. The previous lesson held `limit` **constant** and
triggered the error path by changing `name`'s length; this lesson does the opposite — it
holds `name` **constant** and changes `limit`. Both trigger the same comparison (`r->length
>= limit`), but showing which side changes clarifies which parameter the contract belongs to:
the comparison looks at both variables, but because the question this lesson asks is
specifically "who gives the limit," the side that must stay constant is `name`, and the side
that must change is `limit`.

## Truncating Copy into a Fixed-Size Buffer

`record_create` allocates and copies the source; but sometimes the destination is a **fixed-
size** local buffer and the source's length is not known in advance. In that case using
`strcpy` leaves the door open to a buffer overflow: `strcpy` never knows the destination's
size, it copies the source as-is. `snprintf`, on the other hand, takes the destination's size
as a parameter and never exceeds it; when it truncates, it **reports** this through its return
value — the return value is the total length that would have been written had there been no
truncation.

The difference between these two patterns is clearer when compared with `record_create` from
the previous section. `record_create` returns `NULL` without writing anything when the limit
is exceeded — no partial write is involved. `bounded_copy` behaves differently: it writes
**as much as fits** into the destination and separately reports the real length. Both prevent
a buffer overflow, but one says "either it fits fully or I write nothing," while the other
says "I write as much as fits, but I also tell you how many characters did not fit." Which
pattern is right depends on what the destination is used for: for data whose integrity must
not be broken, such as a record name, the first is preferred; for text where truncation is
acceptable, such as a log line, the second is.

```c
// truncate.h — declaration of a truncating copy into a bounded destination
#ifndef TRUNCATE_H
#define TRUNCATE_H

#include <stddef.h>

/* The destination's size is dest_size. The returned value is the total
   length that would have been written had there been no truncation; if
   this value is larger than dest_size, truncation occurred. */
size_t bounded_copy(char *dest, size_t dest_size, const char *source);

#endif
```

```c
// truncate.c — copying without exceeding the limit, using snprintf
#include <stdio.h>
#include "truncate.h"

size_t bounded_copy(char *dest, size_t dest_size, const char *source) {
    int needed = snprintf(dest, dest_size, "%s", source);
    if (needed < 0) {
        return 0;
    }
    return (size_t)needed;
}
```

**RO8.** Comparing `bounded_copy`'s returned value with `dest_size - 1` tells whether
truncation occurred: the return value is the length that does **not** fit the destination,
not the length actually written to the destination. These two numbers may be the same (no
truncation) or different (truncation) — the lesson shows both in the measurement below.
Comparing against `dest_size - 1` follows from the fact that `snprintf` always adds a
terminating zero byte to the destination: a `dest_size`-byte space fits at most `dest_size -
1` characters plus one terminator.

## One Measurement with Two Limits

The single `main` below measures the same two sources with two different limits: a narrow and
a wide `limit` for `record_create`, a small and a sufficient `dest_size` for `bounded_copy`.

```c
// measurement.c — measuring the caller-given limit with two different values
#include <stdio.h>
#include "owner.h"
#include "counter.h"
#include "truncate.h"

static void observe(const char *name, const char *result) {
    printf("%-28s%s\n", name, result);
}

int main(void) {
    char buffer[64];

    printf("%-28s%s\n", "observation", "result");

    /* --- record_create: same name, two different limits --- */
    Record *narrow = record_create("toughness", 6);
    snprintf(buffer, sizeof buffer, "record %s", narrow == NULL ? "rejected" : "accepted");
    observe("narrow limit (6)", buffer);

    Record *wide = record_create("toughness", 32);
    snprintf(buffer, sizeof buffer, "record %s", wide == NULL ? "rejected" : "accepted");
    observe("wide limit (32)", buffer);
    record_destroy(wide);

    /* --- bounded_copy: same source, two different destination sizes --- */
    char small[8];
    size_t needed_small = bounded_copy(small, sizeof small, "toughness class");
    snprintf(buffer, sizeof buffer, "needed %zu, fits %zu, %s",
             needed_small, sizeof small - 1,
             needed_small >= sizeof small ? "truncated" : "not truncated");
    observe("small dest (8)", buffer);

    char sufficient[32];
    size_t needed_sufficient = bounded_copy(sufficient, sizeof sufficient, "toughness class");
    snprintf(buffer, sizeof buffer, "needed %zu, fits %zu, %s",
             needed_sufficient, sizeof sufficient - 1,
             needed_sufficient >= sizeof sufficient ? "truncated" : "not truncated");
    observe("sufficient dest (32)", buffer);

    return 0;
}
```

```text
observation                 result
narrow limit (6)            record rejected
wide limit (32)             record accepted
small dest (8)              needed 15, fits 7, truncated
sufficient dest (32)        needed 15, fits 31, not truncated
```

**RO9.** `"toughness"` is nine characters; the narrow limit (**6**) rejects it, the wide limit
(**32**) accepts the same name — the difference lies not in the source, but in the number the
caller gives. In `bounded_copy` too, the same source (`"toughness class"`, fifteen characters)
is measured with two different destination sizes: the eight-byte destination is truncated
because it fits seven characters, the thirty-two-byte destination is not truncated because it
comfortably fits fifteen characters — the `needed` value stays the **same** across both calls,
only the destination's capacity changes.

Two of the four observations come through `record_create`, two through `bounded_copy`, but
all four share the same structure: **same source, two different limits**. This repetition is
deliberate — the lesson does not have a single piece of evidence, it has two independent
pieces of evidence in two independent functions showing the same principle. `record_create`
rejects entirely when the limit is exceeded; `bounded_copy` partially accepts and reports the
state when the limit is exceeded. Two different responses show how the same source (the
caller-given limit) can determine two different outcomes. The `before`/`diff` arrangement
inside `main` is not needed here, because no observation depends on the previous one's
counter state — both are measured independently with their own input and their own limit;
of the `narrow` and `wide` pointers, only `wide` is freed, because `narrow` is already `NULL`,
and `record_destroy(NULL)` safely does nothing.

## The Bounding Measurement

Whether the `limit` and `dest_size` parameters are called with the **correct** value is not
something the compiler checks. Giving an excessively large limit disconnected from the real
size, like `record_create(name, 1000000)`, also compiles; in that case the function still
applies its own internal check correctly, but the check becomes **meaningless**, because the
caller has misreported the real destination's size. Bounds checking does not save anything if
the bound itself is wrong.

The same bound applies to `bounded_copy`'s `dest_size` parameter as well. The `sizeof small`
expression works correctly here because `small` is a real array variable and the compiler
knows its size; but if `sizeof` were applied to a pointer parameter — for example if `small`
were taken as a function parameter written `char *small` — `sizeof small` would give the
pointer's own size, not the array's size, and this would silently produce a wrong
`dest_size`. This lesson does not fall into that trap because `sizeof small` and `sizeof
sufficient` are called directly over the array definitions inside `main`, but the distinction
can be missed when carrying the pattern into another function; `dest_size` must always be
computed **in the scope where the array is defined**, not after it has decayed into a
pointer.

Even once the limit's correctness has been established, the limit's **persistence** is a
separate question. The `limit` given to `record_create` is valid only for that single call;
when the same `Record` is later passed to another function, the original `limit` value is not
carried along with it — only the pointer `record_name` returns and the copy `record_name_copy`
produces are carried along. This shows that the caller's responsibility does not end with a
single call: the caller that first gives the limit must also document under what assumption
(for example "this name will be at most thirty-one characters") it was given, because later
code can learn that assumption only by reading the documentation, not by reading it from the
source.

## Summary

- `record_create`'s `limit` parameter comes from the **caller**, not the source; the same
  name gave two different results with two different limits.
- `snprintf` provided a copy that takes the destination's size as a parameter, never exceeds
  it, and reports truncation through its return value; `strcpy` never provides this report at
  all.
- When the same source was measured with two different destination sizes, the `needed` length
  stayed constant, only whether it was truncated changed — the limit's correctness depends on
  the destination's real capacity.
- The memory this lesson measures is in the **caller** class: the party that knows and
  reports the destination's size is the call itself that gives the `limit` or `dest_size`
  parameter in the code.
- These three lessons measured three separate classes in the course's "who owns it" axis in
  sequence: the previous two lessons measured the unowned and callee classes through moments
  the contract broke and the function failed, this lesson measured the caller class by showing
  that the limit is written in the caller, not the source. What the three lessons share is
  that in none of them is the protecting party the language — protection each time comes from
  a line that a separate party added itself.

## Next Step

These three lessons measured the moments the contract broke, the function failed, and the
caller gave a limit — all three ran on the same platform, the same compiler. This shared
assumption has never been questioned so far: every measurement produced the same result under
the same `cc`, the same flags, the same operating system. But even defined behavior itself is
not identical everywhere — part of it is defined by the standard, while which **value** it
takes can remain implementation-defined; even the width of `size_t` itself, the type of the
length `bounded_copy` returns as measured in this lesson, can vary from platform to platform.
The next lesson takes up how much this contract varies **across platforms**: which behavior
stays defined, which is implementation-defined, and how code keeps this portable. The three
examples in this lesson are therefore evidence of only one platform — even though the result
itself stays unchanged, which assumptions carry that result has not yet been written down
explicitly. That writing-down work begins without changing a single line of the lesson's
code, only by changing the build environment.
