PART II

Chapter 8 - Addressing the Wrong Location

Memory safety is often described in terms of size:

too many bytes

too little space

out-of-bounds access

But an operation can use exactly the right number of bytes and still access the wrong memory.

Consider:

memcpy(dst, src, 16);

Suppose 16 bytes are valid at both addresses.

The operation can still be wrong if:

dst

does not point to the intended object.

This chapter is therefore about location.

The central question is:

> Does this address identify the object the program thinks it identifies?


1. Size can be correct while location is wrong

Suppose a program intends to update:

object_a

but a pointer calculation produces:

object_b

and the operation writes exactly:

sizeof(object_a)

bytes.

The size is correct.

The location is not.

The result may be memory corruption even though every length calculation is perfectly reasonable.

This is why memory-safety analysis needs two separate questions:

How much?

and:

Where?

2. Pointer arithmetic is object-relative

Consider:

p + offset

The result is not simply an arbitrary numerical address.

It is a pointer derived from:

p

and:

offset.

The intended meaning is generally:

move within the object associated with p.

If `offset` is wrong, the resulting location may no longer identify the intended element or subobject.

This makes pointer arithmetic part of the memory boundary calculation.


3. Indexing hides pointer arithmetic

These expressions are closely related:

array[index]

and:

*(array + index)

The index determines the location.

If:

index

is wrong, the resulting access is wrong.

This is why array indexing should be reviewed in the same way as explicit pointer arithmetic.

Ask:

What object does array refer to?

What does index mean?

What is the valid range of index?

How was index calculated?

4. Index versus byte offset

One of the most common location errors is confusing:

element index

with:

byte offset.

Suppose:

int array[100];

If:

index = 10;

then:

array + index

points to the eleventh `int`.

But:

(char *)array + index

moves only:

10 bytes.

Those are different locations.

The type of the pointer determines the unit of pointer arithmetic.

This is powerful, but it also means that changing pointer types can change the meaning of an offset.


5. Scaling happens automatically

For:

p + i

where `p` points to an object of size `S`, the conceptual address displacement is:

i * S

This is why:

int *p;
p + 3;

does not mean:

three bytes later.

It means:

three int objects later.

A programmer who calculates an offset in bytes and then applies it to a typed pointer can therefore land at the wrong location.

The number may be correct.

The unit is wrong.


6. Converting pointers changes the unit

Consider:

char *bytes = (char *)items;

Now:

bytes + offset

moves by:

offset bytes.

Whereas:

items + offset

moves by:

offset elements.

Both expressions use the same integer.

They do not identify the same location.

This is an important review technique:

> Whenever a pointer changes type, reconsider the units of every offset used with it.


7. Two-dimensional indexing

Consider:

matrix[row][column]

The location depends on two values.

For a row-major array, the conceptual calculation includes something like:

row * row_width + column

If either value is wrong, the final location is wrong.

The same pattern appears in:

images
packet tables
multidimensional buffers
flattened arrays
grids
matrix calculations.

The arithmetic may look like ordinary indexing.

It is really address calculation.


8. Flattened structures

Suppose an image has:

width
height
channels

and pixels are stored in one linear buffer.

A location might be calculated from:

y * width * channels
    +
x * channels
    +
channel

Every number has a unit.

The result must identify the intended byte or element.

An error in any multiplication or addition can produce the wrong location.

This connects pointer arithmetic directly back to Chapter 1.

The arithmetic is not separate from the pointer.

It is how the pointer is created.


9. The location calculation can overflow

Suppose:

offset = row * width * element_size;

The programmer may correctly validate:

row < height

and:

column < width

yet still have a problem if the multiplication used to calculate the byte offset overflows.

The logical coordinates may be valid.

The resulting address calculation may not represent the intended location.

This is a crucial distinction:

valid coordinates

do not automatically imply:

valid address calculation.

The arithmetic connecting them must also be valid.


10. The classic offset pattern

Consider:

p = buffer + header_size + index * element_size;

This single expression contains several opportunities for error.

header_size

might be wrong.

index

might be out of range.

element_size

might be wrong.

The multiplication might overflow.

The addition might overflow.

The resulting pointer might not identify the intended element.

The expression is therefore best understood as a chain of calculations rather than one line of pointer syntax.


11. The wrong object

A pointer can be valid while referring to the wrong object.

Suppose:

p

points to a valid structure.

But the program intended:

q

which points to another structure.

An operation through `p` can therefore be entirely within valid memory and still violate the program's logical memory model.

This matters especially in programs with:

linked lists
trees
object pools
arenas
custom allocators
shared buffers
multiple related structures.

The address may be valid.

The object identity is wrong.


12. Interior pointers

Suppose:

buffer

points to an allocation.

Then:

buffer + 20

may point to an interior location.

That can be useful.

But the program must know what that location represents.

An interior pointer might identify:

an array element

a structure member

a serialized field

the beginning of a subobject

or:

nothing meaningful at all.

The fact that the pointer is inside the allocation does not establish that it is the correct location for the intended operation.


13. Pointer provenance and object identity

C pointer semantics are more subtle than simply treating pointers as integers.

For memory-safety reasoning, it is useful to retain the identity of the object from which a pointer was derived.

Think of:

p

as meaning not merely:

address X

but:

a location within object O.

Then pointer arithmetic means:

move to another location associated with O.

This mental model prevents a common mistake:

"The numeric address happens to be inside another allocation,
 so the pointer must be usable there."

The fact that two objects happen to occupy nearby addresses does not make them interchangeable.


14. The wrong base pointer

Consider:

dst = buffer + offset;

Suppose the programmer intended `buffer` to point to the beginning of an object.

But it actually points to:

the beginning of a subobject

or:

an unrelated field

or:

an already advanced position.

The offset can be perfectly correct relative to the intended base and still produce the wrong final address.

When debugging a bad pointer, do not inspect only the final offset.

Trace the pointer back to its base.


15. Double offsets

A particularly common mistake occurs when a pointer has already been advanced:

p = buffer + offset;

and later code does:

p += offset;

The final pointer is:

buffer + 2 * offset

rather than:

buffer + offset.

Both operations may look individually reasonable.

The problem is that the same logical displacement was applied twice.

This kind of bug is especially common in layered parsing and buffer-processing code.


16. Stale pointers

A pointer can also identify the wrong location because the object it originally referred to has moved or ceased to exist.

For example:

p = buffer;

followed by:

buffer = realloc(buffer, new_size);

If the allocation moves, `p` no longer identifies the current allocation.

The numerical address stored in `p` has not changed.

The object's location has.

This is therefore a location problem intertwined with lifetime.

The pointer has become stale.


17. Reallocation changes the map

Before:

buffer -> allocation A

After a successful reallocation, the program may have:

buffer -> allocation B

A stale pointer may still contain:

allocation A's old address

The old address might now be:

unmapped

or:

reused for another object.

The stale pointer therefore no longer represents the intended object.

This is why pointer validity is temporal as well as spatial.


18. NULL is a special wrong location

A NULL pointer is not merely a small address.

Dereferencing it is invalid because it does not identify an appropriate object.

For example:

p = get_object();

p->field = value;

If `get_object()` returns NULL, the destination is wrong before the write begins.

The important point is that a pointer can fail because it identifies:

no object

rather than:

an object with insufficient capacity.

These are different failure modes.


19. Near-NULL is not meaningfully better

A corrupted pointer may become a small non-NULL value.

The program may then attempt to access:

address 0x10

or:

address 0x100

The fact that the value is not zero does not make it a valid object location.

Again, pointer validity is not a simple numeric property.

The address must identify an appropriate accessible object.


20. Wrong source, not just wrong destination

Location errors affect reads too.

Consider:

memcpy(dst, src + offset, length);

The destination may be perfect.

The source may be wrong.

The program could therefore read:

the wrong field

the wrong record

the wrong packet

another object

freed memory

unrelated data.

This can lead to information disclosure without any destination overflow.

The same amount of data can be copied correctly from the wrong place.


21. Correct size, wrong object

Imagine:

struct credentials a;
struct credentials b;

and:

memcpy(&a, &b, sizeof(a));

The size is correct.

The source and destination are valid.

But suppose the programmer intended to copy from:

c

instead of:

b.

There is no spatial memory violation in the operation.

It is nevertheless a logical memory error.

This distinction matters when diagnosing bugs.

Not every wrong-location bug is necessarily an out-of-bounds access.

Some are valid accesses to the wrong object.

Security consequences depend on what the wrong object contains and what the operation does with it.


22. Pointer calculations from attacker-controlled values

A common vulnerability pattern is:

p = base + user_offset;

followed by:

access(p);

The attacker may control:

user_offset.

The security question becomes:

What locations can the attacker make p identify?

If the bounds are weak, the resulting pointer may reach memory outside the intended object.

The important property is not simply that the offset is untrusted.

It is that the offset controls an address calculation whose valid range has not been adequately established.


23. A pointer is not a capability by itself

It is tempting to think:

"I have a pointer, therefore I have access to the object."

In C, the programmer must still reason about:

which object

which subobject

which offset

which lifetime

which access size.

A pointer value does not carry a complete proof that the intended operation is valid.

The programmer has to preserve that proof through the calculations that follow.


24. Bounds and location are inseparable

Suppose:

p = buffer + offset;

and then:

memcpy(p, src, length);

To establish that the destination range is valid, we need both:

the correct base object

and:

a valid offset.

Then we need:

enough remaining capacity for length.

Conceptually:

base object
    ->
offset
    ->
location
    ->
remaining capacity
    ->
access length

A location cannot be validated independently from its object and its boundary.


25. The complete range

One of the most useful habits from the previous chapters is to stop thinking about a pointer as a single location.

For:

p = buffer + offset;

followed by a write of:

length

bytes, think:

[buffer + offset,
 buffer + offset + length)

Then ask:

Does this entire range belong to the intended object?

This immediately exposes several classes of bugs:

wrong base
wrong offset
wrong length
wrong object
insufficient capacity
stale pointer.

The range is the thing that the operation actually uses.


26. Location errors can precede size errors

Consider:

p = buffer + attacker_offset;
memcpy(p, src, fixed_length);

The length might be a constant and completely safe.

The vulnerability can still exist because:

attacker_offset

moves the destination to an unintended location.

The chain is:

attacker-controlled offset
    ->
wrong address
    ->
fixed-size write
    ->
corruption.

The size did not need to be wrong.

The location was enough.


27. Size errors can produce location errors

The reverse is also common.

Suppose:

offset = index * element_size;

If the multiplication overflows, the resulting offset may be smaller than intended.

The program may then calculate:

buffer + offset

and access the wrong location.

Here the root cause is arithmetic.

The visible problem is a bad address.

This is why bug categories should not be treated as mutually exclusive.

A single defect can move through several categories as it propagates.


28. A practical review method

For any pointer calculation, write down:

base object
base location
offset
offset unit
resulting location
access size

Then ask:

Is the base pointer associated with the intended object?

Is the offset measured in the correct unit?

Can the offset calculation overflow?

Is the resulting location within the intended object?

Is the complete access range within the object?

This method is especially effective for:

packet parsers
binary decoders
image processing
array manipulation
custom allocators
object pools
serialization code.

29. The central lesson

A memory operation can have:

the right size

and still use:

the wrong location.

The location may be wrong because:

the base pointer is wrong

the offset is wrong

the unit is wrong

the index is wrong

the arithmetic overflowed

the pointer is stale

the object was confused with another object

the pointer identifies no valid object.

The most useful question is therefore:

> What object does this address belong to, and how did the program establish that this is the object it intended?

Once that question is answered, the next question is naturally:

How much of that object is actually being accessed?

That brings the two dimensions back together.

A memory operation is safe only when both its location and its amount are correct.

The chain is now complete:

integer arithmetic
    ->
allocation size
    ->
pointer arithmetic
    ->
bounds
    ->
memory access

And once that access goes wrong, the next question is no longer merely whether the program has a bug.

It is what that bug allows an attacker to do.

← Previous 10 of 13 Next →