PART II

Chapter 5 - Copying the Wrong Amount

Memory copying looks simple.

A source address, a destination address, and a number of bytes:

memcpy(destination, source, length);

But this small interface hides a substantial amount of reasoning.

The program must know that the source is valid for `length` bytes. It must know that the destination is valid for `length` bytes. It must know that `length` means what the programmer thinks it means. And it must ensure that the regions involved satisfy the requirements of the operation.

The important point is that a memory copy is not fundamentally about pointers.

It is about ranges.

A useful mental model is:

source range:
    [source, source + length)

destination range:
    [destination, destination + length)

The copy is correct only if both ranges describe valid memory for the operation being performed.

This makes copying an excellent example of the central theme of this book:

> I calculated, addressed, allocated, copied, or freed the wrong amount/location of memory.

In this chapter, the focus is the amount.


1. The size is part of the operation

Consider:

memcpy(dst, src, n);

It is tempting to think of this as:

copy from src to dst

But that is incomplete.

The actual operation is:

copy n bytes from src to dst

The value of `n` is therefore just as important as either pointer.

If `src` and `dst` are correct but `n` is too large, the operation is wrong.

If `n` is too small, the operation may also be wrong, even if it does not immediately cause memory corruption.

For example:

memcpy(dst, src, sizeof(struct header));

may be correct if `src` and `dst` refer to complete `struct header` objects.

But:

memcpy(dst, src, sizeof(*src));

is only correct if the object represented by `src` is actually the object whose size is being used.

The expression that produces the size is therefore part of the safety argument.


2. The copy has two independent bounds

Suppose:

memcpy(dst, src, n);

There are two separate questions:

Is there at least n bytes available at src?

and:

Is there at least n bytes available at dst?

These are independent.

A source can be too short while the destination is large enough.

A destination can be too short while the source is large enough.

Both can be too short.

This matters because a common mistake is to validate only one side.

For example:

if (n <= src_size)
    memcpy(dst, src, n);

The check establishes something about the source.

It says nothing about `dst`.

The destination requires its own bound.

Conceptually:

n <= source_available
n <= destination_available

Both conditions must hold.


3. The copy length is often derived

The simplest example is:

memcpy(dst, src, n);

where `n` comes directly from an input.

Real programs more often calculate it.

For example:

n = count * sizeof(*src);
memcpy(dst, src, n);

Now the copy depends on the entire calculation chain:

count
  ->
count * sizeof(*src)
  ->
n
  ->
memcpy

If `count` is wrong, `n` may be wrong.

If the multiplication overflows, `n` may be wrong.

If `sizeof(*src)` is not the size the programmer intended, `n` may be wrong.

The copy is merely where the incorrect value is finally consumed.

This is one reason that looking only at the call to `memcpy` is often insufficient.


4. Too large is the obvious failure

Suppose:

char dst[32];
char src[32];

and:

memcpy(dst, src, 40);

The destination does not contain 40 writable bytes.

The source does not contain 40 readable bytes.

The length is wrong for both sides.

The important lesson is not the particular number 40.

It is that the programmer has asserted, through the call, that both ranges are at least 40 bytes long.

That assertion is false.


5. Too small can also be a bug

An undersized copy is less dramatic.

It may not corrupt memory.

But it can still violate the program's intended behavior.

Consider:

memcpy(dst, src, sizeof(struct message));

Suppose the programmer intended to copy a complete message but the actual required size includes additional variable-length data.

The copy may remain entirely inside valid memory while producing an incomplete object.

This can cause later code to operate on partially initialized or inconsistent state.

In security-sensitive code, that can matter.

A memory-safety analysis should therefore not equate "wrong amount" exclusively with "too much."

The question is:

Is the amount correct for the operation?

Too much can cause an immediate spatial violation.

Too little can create an incorrect object that causes a later failure.


6. Size and capacity are different things

A common pattern is:

size_t length;
size_t capacity;

where:

length

describes how much meaningful data currently exists, while:

capacity

describes how much storage is available.

A copy into the buffer generally needs to respect capacity.

For example:

memcpy(buffer, data, length);

may be safe if:

length <= capacity

But if:

length > capacity

the copy is too large for the destination.

The mistake often occurs because the program has confused two different concepts:

how much data exists

and:

how much memory exists.

Those values may be equal in some states.

They are not interchangeable.


7. Remaining capacity matters

The problem becomes more interesting when the destination is not empty.

Suppose:

buffer

has capacity:

capacity

and already contains:

used

bytes.

A copy beginning at:

buffer + used

cannot use the entire capacity.

The available space is:

capacity - used

Therefore the relevant condition is:

length <= capacity - used

not merely:

length <= capacity

This is a recurring pattern in memory safety.

A buffer's total size is not necessarily the amount of space available at a particular location.

The location changes the remaining range.

This connects copying directly to the pointer-arithmetic and bounds reasoning from earlier chapters.


8. Offset plus length

Consider:

memcpy(buffer + offset, src, length);

The destination is no longer simply:

buffer

It is:

buffer + offset

The relevant range is:

[buffer + offset, buffer + offset + length)

If `buffer` has `capacity` bytes, the operation is valid only if the copied range remains within those bytes.

Conceptually:

offset + length <= capacity

But there is a subtle issue here.

Writing the check exactly this way can itself introduce an integer-overflow problem.

If:

offset + length

overflows, the result may appear smaller than `capacity`.

A safer conceptual form is:

offset <= capacity
length <= capacity - offset

This is a useful example of how the chapters connect.

A bounds check is itself an arithmetic operation.

Memory safety can therefore depend on getting the arithmetic of the check right.


9. The source has the same problem

The same reasoning applies to the source.

Consider:

memcpy(dst, src + offset, length);

If the source contains:

source_size

bytes, the relevant condition is:

offset <= source_size
length <= source_size - offset

It is not enough to establish:

length <= source_size

because the copy does not begin at the start of the source.

The complete source range matters.

This is why a useful mental transformation is:

pointer + offset + length

into:

starting position
+
amount consumed

The operation must remain inside the object's boundary.


10. Two-dimensional bounds

With a copy involving offsets on both sides:

memcpy(dst + dst_offset,
       src + src_offset,
       length);

there are now two range calculations.

The source requires:

src_offset + length <= src_capacity

The destination requires:

dst_offset + length <= dst_capacity

Neither condition implies the other.

The programmer has to establish both.

This is one reason memory-copy code becomes difficult to review when many derived values are involved.

The number of values is small, but the relationships between them are numerous.


11. `sizeof` is powerful, but only when the object is right

One of the safest-looking ways to specify a copy length is:

memcpy(dst, src, sizeof(*src));

This is often preferable to manually writing a type size.

But `sizeof` does not know the programmer's intent.

It tells us the size of the type or expression being measured.

It does not tell us whether that is the correct amount to copy.

For example:

struct packet {
    size_t length;
    char data[1];
};

A copy of:

sizeof(struct packet)

does not necessarily copy the entire logical packet if `data` represents a variable-length tail.

Similarly, if a pointer points to only part of a larger object, `sizeof(*p)` describes the pointed-to type, not the amount of memory that happens to be available beyond `p`.

`sizeof` prevents many manual size mistakes.

It does not eliminate the need to understand the object being copied.


12. Arrays are a common source of confusion

Suppose:

char src[100];

Then:

sizeof(src)

is 100.

But if:

char *src;

then:

sizeof(src)

is the size of the pointer, not the size of the memory to which it points.

This is a classic C distinction.

It becomes particularly dangerous when code changes from an array to a pointer while retaining an apparently sensible `sizeof` expression.

For example:

memcpy(dst, src, sizeof(src));

may copy 100 bytes in one context and only the size of a pointer in another.

The expression still compiles.

The resulting copy may still look reasonable during review.

But the meaning has changed.

The lesson is not "never use `sizeof`."

It is:

Know what object the expression actually measures.

13. Structure copies are usually straightforward

For a complete structure object:

struct header src;
struct header dst;

this is generally straightforward:

memcpy(&dst, &src, sizeof(dst));

The size corresponds to the complete object.

But problems appear when a structure contains pointers.

For example:

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

A copy of the structure copies the pointer value.

It does not copy the characters pointed to by `name`.

Thus:

memcpy(&dst, &src, sizeof(dst));

produces two structures whose `name` members point to the same memory.

That may be exactly what is intended.

It may also be completely wrong.

This is a different kind of "wrong amount."

The byte count may be correct for the structure itself while being wrong for the logical data represented by the structure.

The distinction between an object's representation and the resources it refers to is therefore important.


14. Shallow copies and deep copies

Consider:

struct object {
    size_t length;
    char *data;
};

A shallow copy copies:

length
data

It does not copy:

data[0] ... data[length - 1]

A deep copy needs a second operation:

new_data = malloc(length);
memcpy(new_data, object->data, length);

Now the original size problem appears again.

The deep copy has to calculate and allocate the right amount before copying it.

This gives another chain:

logical length
    ->
allocation size
    ->
destination capacity
    ->
copy length

A mistake at any stage can invalidate the next one.


15. The dangerous assumption: "the source is the size"

A frequent pattern is:

memcpy(dst, src, sizeof(*src));

This is safe only if the source object really is the complete object whose size is being copied.

Suppose:

src

points into a larger byte buffer.

The pointer may be perfectly valid.

The first object may be valid.

But the program still needs to know that at least:

sizeof(*src)

bytes remain available from that location.

A valid pointer does not automatically imply that an arbitrary number of bytes can be read from it.

The pointer establishes a location.

The length establishes a range.

Both matter.


16. Strings are copies with hidden assumptions

String functions are another form of memory-copy operation.

Consider:

strcpy(dst, src);

Unlike `memcpy`, there is no explicit length.

The function derives the amount from the source string's terminator.

Conceptually, it performs something like:

find the terminating null byte
determine the string length
copy the string and terminator

The absence of an explicit length does not remove the size problem.

It merely moves the responsibility.

The source must contain a valid null-terminated string.

The destination must have enough space for the entire string and its terminator.

If either assumption is false, the operation can access memory outside the intended ranges.

The amount is still there.

It is simply implicit.


17. `strlen` does not validate a string

Consider:

n = strlen(src);
memcpy(dst, src, n + 1);

This may look careful.

But `strlen` itself requires that `src` contain a null terminator within accessible memory.

If the terminator is missing, `strlen` will continue reading until it encounters one or encounters invalid memory.

The bug therefore occurred before `memcpy`.

The copy length was derived from a read whose own bounds were not established.

Again, the chain matters.


18. The terminator creates another byte

Suppose a string contains:

"hello"

The characters occupy five bytes:

h e l l o

A C string requires an additional byte:

\0

Therefore the storage requirement is:

strlen(s) + 1

not:

strlen(s)

This small difference is one of the most persistent sources of C memory bugs.

For example:

char *copy = malloc(strlen(src));
strcpy(copy, src);

The allocation provides space for the characters but not the terminating byte.

The allocation is therefore too small for the operation that follows.

The copy is where the memory corruption occurs.

The allocation calculation is where the chain first became wrong.


19. `memmove` does not make the length safe

`memmove` handles overlapping source and destination regions.

It does not validate their sizes.

For example:

memmove(dst, src, length);

still requires valid source and destination ranges of the requested length.

Its special property concerns overlap, not bounds.

This distinction is useful because a function can solve one class of memory problem while leaving another completely untouched.

The programmer still has to establish:

source range is valid
destination range is valid

20. Overlap is a separate question

Suppose:

src

and:

dst

refer to overlapping regions.

For `memcpy`, overlapping source and destination regions are not permitted.

`memmove` exists specifically to handle this case.

Thus a memory copy has at least two kinds of correctness questions:

Are the ranges valid?

and:

Are the ranges allowed to overlap for this operation?

These should not be confused.

A copy can have the correct size and still be invalid because of overlap.

Conversely, a non-overlapping copy can still be invalid because its length is too large.


21. Partial copies and stale data

A copy that is too small can create a less obvious security problem.

Suppose a structure contains:

length
data

and code intends to replace the entire object.

If only part of the object is copied, old contents may remain.

This can lead to:

stale pointers
stale lengths
stale flags
stale credentials
stale metadata

The memory operation itself may remain within bounds.

The problem is that the program's logical object is now inconsistent with the bytes actually written.

This matters particularly when partially initialized objects are later trusted.

Memory safety is not only about crossing a physical boundary.

The logical boundary of an object matters too.


22. Copying structured input

Network protocols and file formats often contain a length followed by variable-sized data.

For example:

struct message {
    uint32_t length;
    unsigned char data[];
};

A parser may receive a buffer and want to construct a message object.

The conceptual operation is:

header size
    +
data length
    ->
allocation size
    ->
copy header
    ->
copy data

The danger is obvious once the chain is written down.

If the input length is incorrect, then:

the allocation may be wrong

or:

the copy length may be wrong

or:

the source range may be wrong

or:

all three.

The copy is therefore not an isolated operation.

It is one step in interpreting untrusted data as memory layout.


23. Trusting the source's length

Suppose a structure says:

length = 1000

and the program receives a buffer containing only 100 bytes.

If code performs:

memcpy(dst, src->data, src->length);

the length field is being treated as a statement about the actual source memory.

But a value stored in memory does not make that much memory exist.

The program needs an independent fact establishing the available input size.

This distinction is fundamental:

declared length

versus:

available length.

A length field is metadata.

It is not evidence by itself.


24. The destination has the same issue

Suppose:

dst->capacity = 1000;

but the actual allocation contains only 100 bytes.

Then:

memcpy(dst->data, src, dst->capacity);

is not made safe by the metadata.

The program's internal description of an object can diverge from the actual allocation.

Once that happens, later operations can faithfully follow the wrong description.

This is one of the most dangerous aspects of memory corruption: corrupted metadata can become the source of subsequent memory errors.


25. Copy loops are just manual `memcpy`

Not every copy uses a library function.

Consider:

for (i = 0; i < length; ++i)
    dst[i] = src[i];

This is conceptually the same range operation.

The source range is:

[src, src + length)

The destination range is:

[dst, dst + length)

The same questions apply.

In fact, manual loops introduce additional opportunities for mistakes:

wrong initial index
wrong termination condition
wrong increment
off-by-one
signed/unsigned comparison
incorrect element size

The absence of `memcpy` does not make the operation conceptually different.


26. Element counts versus byte counts

One particularly important distinction is:

number of elements

versus:

number of bytes.

Suppose:

int src[100];
int dst[100];

Then:

memcpy(dst, src, 100);

copies 100 bytes.

It does not copy 100 integers.

To copy all 100 integers, the length must be:

100 * sizeof(src[0])

This seems elementary.

The interesting cases are where the value has already been transformed.

For example:

count = number of records

then:

bytes = count * record_size

then:

memcpy(dst, src, bytes);

Each stage must preserve the intended unit.

A value called `size` is not enough information.

We need to know:

size of what?

27. Multiplication can make the copy dangerous

Suppose:

bytes = count * element_size;

The result is intended to describe the copy length.

If the multiplication overflows, `bytes` may become smaller than intended.

That can have two very different consequences.

If the allocation also uses the same incorrect `bytes`, the allocation and copy may agree with each other while both disagree with the intended object size.

If the allocation uses a checked calculation but the copy uses an unchecked calculation, the destination may be large enough while the copy length is wrong.

If the copy length is computed differently from the allocation size, the two operations can disagree about the size of the object.

The most dangerous bugs are often not isolated arithmetic errors.

They are inconsistencies between calculations that are supposed to describe the same memory.


28. The allocation and copy must agree

Suppose:

size = calculate_size(count);
p = malloc(size);

Later:

n = calculate_copy_size(count);
memcpy(p, src, n);

There are now two calculations describing related quantities.

The program needs:

n <= size

and, depending on the intended object:

n == the amount of data that should be copied

If the calculations use different rules, types, or overflow behavior, they can disagree.

This is a powerful review technique:

> Find every place where the program calculates the size of the same logical object, and compare the calculations.

Duplicated size calculations are opportunities for divergence.


29. Prefer one authoritative size

A safer design often looks like:

size = calculate_size(...);

p = malloc(size);

...

memcpy(p, src, size);

The exact design will vary, but the principle is useful:

calculate a size once,
validate it,
and preserve its meaning.

Every time the same size is recalculated, the program creates another opportunity for the calculations to disagree.

This is not merely a style preference.

It reduces the number of relationships that have to remain true.


30. The danger of changing units halfway through

Suppose:

count

is measured in elements.

Then:

bytes = count * sizeof(*src);

Now `bytes` is measured in bytes.

If code later does:

offset += bytes;

that may be correct if `offset` is also a byte offset.

But if `offset` is an element index, the calculation silently mixes units.

The compiler may not help.

Both values may have type `size_t`.

The program therefore needs to preserve semantic information that the type system does not express.

This is one reason memory bugs often survive ordinary compiler warnings.


31. A useful review pattern

When reviewing a copy, write down four things:

source object
source available range
destination object
destination available range

Then identify:

copy length

For example:

source:
    packet
    available = packet_size

destination:
    buffer
    capacity = buffer_capacity - offset

copy:
    length = payload_length

The central questions become:

payload_length <= packet_size

and:

payload_length <= buffer_capacity - offset

This simple transformation often exposes problems that are hard to see in the original code.


32. Do not confuse validation with trust

Consider:

if (length <= MAX)
    memcpy(dst, src, length);

The check establishes only:

length <= MAX

It does not establish:

length <= source_available

or:

length <= destination_available

The constant may be useful.

It is simply not the same property.

A maximum input size can reduce risk without proving that a particular memory operation is safe.

The question is always:

Safe relative to what boundary?

33. Validation order matters

Suppose a program receives:

count

and wants to allocate:

count * sizeof(*items)

A robust sequence conceptually looks like:

validate count
    ->
validate multiplication
    ->
calculate allocation size
    ->
allocate
    ->
establish destination capacity
    ->
validate copy length
    ->
copy

The exact implementation will depend on the application.

The important idea is that later operations should depend only on facts already established.

If code copies first and validates afterward, the validation is irrelevant to that copy.


34. A copy can expose an earlier bug

Suppose:

size = count * sizeof(*item);

buffer = malloc(size);

...

memcpy(buffer, input, count * sizeof(*item));

If the multiplication in the allocation wraps, the allocation may be too small.

The copy repeats the same arithmetic and may produce the same wrapped value.

In that case, the copy might not immediately exceed the allocation.

That can be misleading.

The program may still have created an object smaller than intended.

Later code may use the logical `count` and access elements that were never allocated.

The first incorrect operation was the allocation calculation.

The eventual memory corruption may occur somewhere else.

This is why a memory-safe copy cannot be analyzed entirely independently of the code that created its ranges.


35. A copy can also be the first visible failure

The reverse can happen.

Suppose the allocation is correct:

buffer = malloc(capacity);

but a later calculation produces:

length > capacity;

The allocation itself is fine.

The copy is the first operation that violates the boundary.

In this case the copy really is where the spatial memory error begins.

The same surface symptom can therefore have different roots.

That is why the chain has to be reconstructed rather than assumed.


36. `memcpy` does not know your object model

The function sees:

destination
source
length

It does not know:

this is a packet

or:

this is an array of records

or:

this is a string

or:

this is the last field in a structure

or:

these 200 bytes are supposed to represent 10 objects.

The programmer supplies that meaning through the length.

This is a general property of low-level interfaces.

The closer an API is to raw memory, the more of the correctness argument remains outside the API.


37. The destination's physical size wins

Suppose the program has:

logical_size = 4096;

but:

allocated_size = 1024;

The program cannot safely copy 4096 bytes simply because its metadata says that the object is 4096 bytes long.

The physical allocation establishes what memory exists.

Logical metadata can describe intended contents.

It cannot create storage.

This is one of the most important rules in memory-safety analysis:

> A size stored in a variable is not the same thing as the amount of memory that actually exists.

The relationship between the two has to be established.


38. The source's physical availability wins too

The same principle applies to input.

Suppose:

input_length = 4096;

but only:

1024

bytes have actually been received.

Then:

memcpy(dst, input, input_length);

does not become valid because the protocol says the message should contain 4096 bytes.

The program has to establish how much input is actually available before treating the declared length as a readable range.

This distinction is especially important in parsers.


39. Copying is where abstract sizes become physical effects

An incorrect size calculation is initially just a number.

An incorrect allocation turns that number into an object boundary.

An incorrect pointer calculation turns it into a location.

A copy turns the resulting assumptions into actual reads and writes.

That is why copying is such an important stage in the chain.

It is the point where the program says:

"Take these bytes from here and put them there."

If the ranges are wrong, the machine performs the operation anyway.

C does not generally insert a runtime boundary check around the operation.

The correctness has to come from the program.


40. A compact mental model

When you encounter:

memcpy(dst, src, length);

translate it mentally into:

read:
    length bytes
    starting at src

write:
    length bytes
    starting at dst

Then ask:

Where did src come from?

Where did dst come from?

Where did length come from?

How much readable memory exists at src?

How much writable memory exists at dst?

Are the ranges allowed to overlap?

Does length have the correct unit?

Can any calculation producing it overflow or truncate?

Those questions are usually more useful than asking whether `memcpy` itself is "safe."


41. A practical example

Consider:

struct item {
    uint32_t id;
    uint32_t value;
};

void copy_items(struct item *dst,
                size_t dst_count,
                const struct item *src,
                size_t src_count,
                size_t count)
{
    if (count > src_count)
        return;

    memcpy(dst, src, count * sizeof(*src));
}

At first glance, the code checks the source.

But what about the destination?

There is no check that:

count <= dst_count

So the function can read a valid number of items from `src` and still write beyond `dst`.

The source-side invariant has been established.

The destination-side invariant has not.

This is a good example of why every copy has two independent bounds.


42. Improving the example

Conceptually, the function needs both:

count <= src_count

and:

count <= dst_count

Then the multiplication needs to produce the intended byte count.

If `count` is a `size_t` and `sizeof(*src)` is also converted appropriately, the multiplication still needs to be considered as part of the calculation.

The important point is not the exact defensive coding pattern.

It is the structure of the proof:

number of elements
    ->
number of bytes
    ->
source range
    ->
destination range

Every arrow represents an assumption.


43. What to look for in real code

When reviewing code that copies memory, look especially closely at:

memcpy
memmove
memset
strcpy
strncpy
strcat
strncat
sprintf
snprintf
manual copy loops

Also look for wrappers.

A function such as:

copy_packet(dst, packet);

may eventually call:

memcpy

with a size derived several layers above.

Do not stop at the library call.

Trace the length back to its origin.

Then trace the destination back to its allocation.

Then establish the source's actual available range.

The important information is often several functions away from the copy itself.


44. The central lesson

A memory copy is not:

pointer A -> pointer B

It is:

range A -> range B

The range has a location and a size.

Both matter.

And the size is rarely an isolated constant.

It is usually the final result of a chain:

input
  ->
count
  ->
arithmetic
  ->
size
  ->
allocation or bound
  ->
pointer
  ->
copy

If the chain produces the wrong amount, the copy may expose the mistake.

If the chain produces the wrong location, the copy may expose that instead.

If the copy is too small, it may leave the program with an incomplete or inconsistent object.

If the source or destination has already become invalid, the copy may merely be the first operation to touch the invalid range.

The useful question is therefore not:

"Is memcpy dangerous?"

It is:

"What exact ranges does this operation read and write,
 and why does the program believe those ranges are valid?"

That question takes us directly to the next stage of the chain:

bounds.

Once we know how much memory an operation intends to use, we have to establish where the boundaries of that memory actually are.

← Previous 7 of 13 Next →