# Dangling Pointers

### Dangling Pointers

A dangling pointer is a pointer that refers to memory that is no longer valid.

The pointer still contains an address, but the value at that address no longer belongs to the thing the pointer thinks it points to.

This is one of the most serious bugs in low-level programming.

A dangling pointer can cause wrong results, crashes, memory corruption, or security problems.

#### A Simple Dangling Pointer

This function is wrong:

```zig
fn badPointer() *i32 {
    var x: i32 = 123;
    return &x;
}
```

The variable `x` is local to `badPointer`.

It lives only while `badPointer` is running.

When the function returns, `x` is gone.

So this return value is invalid:

```zig
return &x;
```

The function returns the address of dead stack memory.

The caller receives a pointer, but that pointer no longer points to a valid `i32`.

#### Why This Is Dangerous

Memory may still contain the old bytes for a short time.

That makes dangling pointer bugs confusing.

For example, this may appear to work sometimes:

```zig
const p = badPointer();
std.debug.print("{}\n", .{p.*});
```

You might see:

```text
123
```

But that does not mean the code is correct.

The stack memory used by `x` may not have been overwritten yet. Later, another function call may reuse the same stack space. Then the pointer may read a different value.

A dangling pointer is invalid even if it appears to work once.

#### Returning a Pointer to Caller-Owned Memory

This version is valid:

```zig
fn chooseFirst(a: *i32, b: *i32) *i32 {
    _ = b;
    return a;
}

pub fn main() void {
    var x: i32 = 10;
    var y: i32 = 20;

    const p = chooseFirst(&x, &y);
    p.* = 99;

    _ = y;
}
```

The function returns `a`, but `a` points to `x`, which lives in `main`.

When `chooseFirst` returns, `x` still exists.

So the returned pointer is valid.

The important question is not “did this pointer come from a function?”

The important question is:

Does the memory behind the pointer still exist?

#### Dangling Slices

Slices can dangle too.

A slice is a pointer plus a length. If the pointer inside the slice becomes invalid, the whole slice is invalid.

This is wrong:

```zig
fn badSlice() []u8 {
    var data = [_]u8{ 1, 2, 3 };
    return data[0..];
}
```

The array `data` is local to `badSlice`.

The returned slice points into `data`.

When `badSlice` returns, `data` is gone.

The slice now points to invalid stack memory.

A slice is not safe just because it has a length. The length only says how many items are visible. It does not make the memory live longer.

#### Returning a Slice to Caller-Owned Memory

This version is valid:

```zig
fn firstTwo(buffer: []u8) []u8 {
    return buffer[0..2];
}

pub fn main() void {
    var data = [_]u8{ 10, 20, 30, 40 };

    const result = firstTwo(data[0..]);

    _ = result;
}
```

The function returns a slice into `buffer`.

The original memory is owned by `main`.

After `firstTwo` returns, `data` still exists.

So `result` is valid while `data` is valid.

This is a common Zig pattern: the caller provides memory, and the function returns a view into that memory.

#### Dangling Heap Pointers

Dangling pointers are not only about stack memory.

They can also happen with heap memory.

This is wrong:

```zig
const buffer = try allocator.alloc(u8, 16);
allocator.free(buffer);

buffer[0] = 42;
```

After this line:

```zig
allocator.free(buffer);
```

the memory no longer belongs to your program.

The slice `buffer` still exists as a value, but it points to freed memory.

Using it after `free` is a use-after-free bug.

#### Use-After-Free

A use-after-free bug happens when code uses memory after releasing it.

Example:

```zig
const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    const allocator = gpa.allocator();

    const buffer = try allocator.alloc(u8, 4);
    allocator.free(buffer);

    buffer[0] = 1; // wrong
}
```

The bug is here:

```zig
buffer[0] = 1;
```

The memory has already been freed.

The pointer or slice was not erased automatically. It still contains an address. But the address is no longer valid for use.

#### Double Free

Another related bug is freeing the same memory twice.

```zig
const buffer = try allocator.alloc(u8, 16);

allocator.free(buffer);
allocator.free(buffer); // wrong
```

After the first `free`, ownership has ended.

The second `free` tries to release memory that is no longer owned.

That can corrupt allocator metadata or crash the program.

The rule is:

Free heap memory exactly once.

#### Ownership Prevents Dangling Pointers

Dangling pointer bugs often come from unclear ownership.

When you see a pointer or slice, ask:

Who owns this memory?

How long does it live?

Who is allowed to free it?

Can this pointer outlive the owner?

Example:

```zig
fn fill(buffer: []u8) void {
    for (buffer) |*byte| {
        byte.* = 0;
    }
}
```

This function does not own `buffer`.

It only borrows it temporarily.

It should not free it.

It should not store the slice somewhere that outlives the caller.

The caller owns the memory.

#### Borrowing Must Be Temporary

When a function receives a pointer or slice, it usually borrows memory.

```zig
fn increment(value: *i32) void {
    value.* += 1;
}
```

This function borrows an `i32` and modifies it.

The borrow is only needed during the function call.

This is valid:

```zig
pub fn main() void {
    var x: i32 = 10;
    increment(&x);
}
```

The pointer does not escape. It is only used during the call.

This is much safer than storing the pointer for later use.

#### Storing Pointers Can Be Risky

A struct can store a pointer:

```zig
const Holder = struct {
    value: *i32,
};
```

This is valid, but now the struct depends on memory owned somewhere else.

Example:

```zig
pub fn main() void {
    var x: i32 = 10;

    const holder = Holder{ .value = &x };

    holder.value.* = 20;
}
```

This is fine because `holder` and `x` are both used inside `main`.

But this would be dangerous if `holder` escaped while `x` died.

When a struct stores a pointer, its lifetime depends on the lifetime of the pointed-to memory.

That relationship is not automatically enforced by Zig in all cases. You must design the API carefully.

#### A Dangerous Global Pointer

Global or long-lived pointers are especially risky.

```zig
var saved: ?*i32 = null;

fn savePointer(p: *i32) void {
    saved = p;
}
```

Now consider:

```zig
fn bad() void {
    var x: i32 = 10;
    savePointer(&x);
}
```

After `bad` returns, `saved` points to dead stack memory.

The pointer escaped from the function.

This is a common source of dangling pointers.

A useful rule:

Do not store a pointer unless you know the pointed-to memory will live at least as long as the stored pointer.

#### Safer Pattern: Store Values Instead of Pointers

Sometimes the simplest fix is to store a copy of the value.

Instead of:

```zig
const Holder = struct {
    value: *i32,
};
```

use:

```zig
const Holder = struct {
    value: i32,
};
```

Now the struct owns its own value.

It does not depend on external memory.

This is not always possible or efficient, but it is often the simplest design.

Use pointers when you need sharing, mutation, large values, or external memory.

Use values when ownership should be simple.

#### Safer Pattern: Caller Provides Storage

Instead of returning a pointer to local memory, let the caller provide the memory.

Wrong:

```zig
fn makeNumber() *i32 {
    var x: i32 = 42;
    return &x;
}
```

Better:

```zig
fn writeNumber(out: *i32) void {
    out.* = 42;
}
```

Use it like this:

```zig
pub fn main() void {
    var x: i32 = undefined;

    writeNumber(&x);

    _ = x;
}
```

The caller owns `x`.

The function only writes into it.

No dangling pointer is created.

#### Safer Pattern: Allocate and Return Ownership

If the data must outlive the function, allocate it and make ownership clear.

```zig
fn makeNumber(allocator: std.mem.Allocator) !*i32 {
    const p = try allocator.create(i32);
    p.* = 42;
    return p;
}
```

The caller must later destroy it:

```zig
const p = try makeNumber(allocator);
defer allocator.destroy(p);
```

Full example:

```zig
const std = @import("std");

fn makeNumber(allocator: std.mem.Allocator) !*i32 {
    const p = try allocator.create(i32);
    p.* = 42;
    return p;
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    const allocator = gpa.allocator();

    const p = try makeNumber(allocator);
    defer allocator.destroy(p);

    std.debug.print("{}\n", .{p.*});
}
```

The pointer is valid because the memory comes from the heap.

The ownership rule is also clear: the caller receives the pointer and must destroy it.

#### Safer Pattern: Return a Value

If the value is small, just return it.

```zig
fn makeNumber() i32 {
    return 42;
}
```

This is simpler than returning a pointer.

Use the simplest ownership model that works.

For small values, returning by value is usually better.

#### Null Does Not Fix Dangling Pointers

Optional pointers are useful, but they do not automatically prevent dangling pointers.

This can still be wrong:

```zig
var saved: ?*i32 = null;

fn bad() void {
    var x: i32 = 10;
    saved = &x;
}
```

The pointer is optional, but when it is not null, it may still point to dead memory.

`null` means “no pointer.”

It does not mean “safe pointer.”

You still need correct lifetime rules.

#### Setting a Pointer to Null After Free

Sometimes after freeing memory, code sets an optional pointer to `null`.

```zig
var maybe_buffer: ?[]u8 = try allocator.alloc(u8, 16);

if (maybe_buffer) |buffer| {
    allocator.free(buffer);
    maybe_buffer = null;
}
```

This can help avoid accidental reuse through that optional variable.

But it only fixes that one variable.

Any other pointer or slice to the same memory would still be dangling.

So this is a helpful habit in some designs, but it does not replace ownership discipline.

#### Common Mistake: Returning Views Into Temporary Buffers

This is a common bug in text processing:

```zig
fn makeMessage() []const u8 {
    var buffer: [64]u8 = undefined;

    const message = buffer[0..5];
    return message;
}
```

The returned slice points into `buffer`, which dies at the end of the function.

Better options:

Return a string literal if the text is static:

```zig
fn makeMessage() []const u8 {
    return "hello";
}
```

Let the caller provide the buffer:

```zig
fn writeMessage(buffer: []u8) []u8 {
    buffer[0] = 'h';
    buffer[1] = 'e';
    buffer[2] = 'l';
    buffer[3] = 'l';
    buffer[4] = 'o';

    return buffer[0..5];
}
```

Or allocate:

```zig
fn makeMessage(allocator: std.mem.Allocator) ![]u8 {
    const message = try allocator.alloc(u8, 5);
    @memcpy(message, "hello");
    return message;
}
```

In the allocation version, the caller must free the returned slice.

#### Common Mistake: Keeping a Slice After Its Owner Changes

Some containers may reallocate their internal memory.

For example, a dynamic array can move its buffer when it grows.

Conceptually:

```zig
const old_items = list.items;

try list.append(123);

// old_items may no longer be valid
```

If `append` causes the list to allocate a new larger buffer, the old slice may point to freed memory.

The exact details depend on the container, but the general rule matters:

Do not keep pointers or slices into a container across operations that may reallocate or invalidate them.

When using a container, read its API carefully.

#### A Checklist for Pointer Lifetime

Before returning or storing a pointer, check these questions:

Does the pointed-to memory live long enough?

Is the memory stack memory that will disappear soon?

Is the memory heap memory that might be freed?

Could a container reallocate and move the memory?

Who owns the memory?

Who is responsible for cleanup?

Could two parts of the program free the same memory?

These questions prevent many bugs.

#### The Main Idea

A dangling pointer points to memory that is no longer valid.

It can come from returning a pointer to a local variable, returning a slice to a local array, using heap memory after freeing it, freeing the same memory twice, or keeping a pointer into a container after the container moves its storage.

The core rule is:

A pointer or slice must not outlive the memory it refers to.

When in doubt, make ownership explicit. Return values for small data. Let callers provide buffers. Allocate only when necessary, and make the caller’s cleanup responsibility clear.

