# Appendix E. Memory Safety Checklist

## Appendix E. Memory Safety Checklist

Zig gives you direct control over memory. That control is useful, but it also means you must follow clear rules.

This checklist gives you a practical way to avoid common memory bugs.

### E.1 Initialize Before Reading

Do not read a value before it has a real value.

```zig
var x: i32 = undefined;
```

This is allowed, but `x` has no meaningful value yet.

Bad:

```zig
var x: i32 = undefined;
std.debug.print("{}\n", .{x});
```

Good:

```zig
var x: i32 = undefined;
x = 10;

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

Use `undefined` only when you will definitely assign a valid value before reading.

### E.2 Free What You Allocate

If you allocate memory, you are responsible for freeing it.

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

The `defer` makes cleanup happen when the current scope ends.

This is one of the most important Zig habits:

```zig
const value = try makeSomething(allocator);
defer destroySomething(allocator, value);
```

Allocate and clean up in the same visible area when possible.

### E.3 Use the Same Allocator to Free

Memory should be freed by the allocator that created it.

Bad:

```zig
const buffer = try allocator_a.alloc(u8, 1024);
allocator_b.free(buffer);
```

Good:

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

A simple rule: the allocator that allocates owns the cleanup path.

### E.4 Do Not Use Memory After Free

After memory is freed, it is no longer valid.

Bad:

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

buffer[0] = 1;
```

This is a use-after-free bug.

Good:

```zig
const buffer = try allocator.alloc(u8, 10);
buffer[0] = 1;

allocator.free(buffer);
```

After `free`, do not read or write through the old slice or pointer.

### E.5 Avoid Returning Pointers to Local Variables

Local variables live only while the function is running.

Bad:

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

When the function returns, `x` is gone. The returned pointer is invalid.

Good:

```zig
fn writeValue(out: *i32) void {
    out.* = 10;
}
```

Or allocate memory with an allocator if the value must live longer.

### E.6 Know Who Owns the Memory

Every allocated object needs an owner.

Ask this question:

Who is responsible for freeing this?

If the answer is unclear, the API design needs work.

Clear ownership example:

```zig
fn makeBuffer(allocator: std.mem.Allocator) ![]u8 {
    return try allocator.alloc(u8, 1024);
}
```

This function returns allocated memory. The caller must free it.

Caller:

```zig
const buffer = try makeBuffer(allocator);
defer allocator.free(buffer);
```

The ownership rule should be documented in the function name, signature, or comments.

### E.7 Prefer Slices Over Raw Pointers

A slice stores both pointer and length.

```zig
[]u8
```

A pointer alone does not know how many items are valid.

```zig
[*]u8
```

Prefer this:

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

Instead of this:

```zig
fn fill(ptr: [*]u8, len: usize) void {
    // more error-prone
}
```

Slices make bounds clearer.

### E.8 Check Slice Bounds

Do not assume a slice has enough items.

Bad:

```zig
fn firstByte(bytes: []const u8) u8 {
    return bytes[0];
}
```

This crashes if `bytes.len == 0`.

Good:

```zig
fn firstByte(bytes: []const u8) ?u8 {
    if (bytes.len == 0) return null;
    return bytes[0];
}
```

Use `?T` when a value may be absent.

### E.9 Be Careful with Sub-Slices

A sub-slice points into the same memory as the original slice.

```zig
const part = buffer[0..4];
```

If `buffer` becomes invalid, `part` also becomes invalid.

Bad:

```zig
const buffer = try allocator.alloc(u8, 10);
const part = buffer[0..4];

allocator.free(buffer);

std.debug.print("{any}\n", .{part});
```

Good:

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

const part = buffer[0..4];
std.debug.print("{any}\n", .{part});
```

Sub-slices do not own memory. They borrow it.

### E.10 Use `defer` for Cleanup

`defer` is one of the best tools for memory safety.

```zig
const file = try std.fs.cwd().openFile("data.txt", .{});
defer file.close();

const buffer = try allocator.alloc(u8, 1024);
defer allocator.free(buffer);
```

This keeps cleanup close to acquisition.

The usual pattern is:

```zig
const resource = try acquire();
defer release(resource);
```

### E.11 Use `errdefer` for Partial Failure

Use `errdefer` when cleanup should happen only if the function returns an error.

Example:

```zig
fn makeThing(allocator: std.mem.Allocator) !*Thing {
    const thing = try allocator.create(Thing);
    errdefer allocator.destroy(thing);

    thing.buffer = try allocator.alloc(u8, 1024);
    errdefer allocator.free(thing.buffer);

    return thing;
}
```

If something fails before `return thing`, `errdefer` cleans up.

If the function succeeds, ownership passes to the caller.

### E.12 Match `create` with `destroy`

For one object:

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

For many objects:

```zig
const items = try allocator.alloc(i32, 100);
defer allocator.free(items);
```

Use the matching pair:

| Allocation | Cleanup |
|---|---|
| `create(T)` | `destroy(ptr)` |
| `alloc(T, n)` | `free(slice)` |

Do not mix them.

### E.13 Avoid Hidden Allocation

A function that allocates should usually take an allocator.

Clear:

```zig
fn joinNames(allocator: std.mem.Allocator, a: []const u8, b: []const u8) ![]u8 {
    return try std.fmt.allocPrint(allocator, "{s} {s}", .{ a, b });
}
```

Unclear:

```zig
fn joinNames(a: []const u8, b: []const u8) ![]u8 {
    // where does memory come from?
}
```

In Zig, allocation should be visible in the API.

### E.14 Use Arena Allocators for Shared Lifetimes

When many objects live and die together, an arena allocator is often simpler.

```zig
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();

const allocator = arena.allocator();
```

Then allocate many objects:

```zig
const a = try allocator.alloc(u8, 100);
const b = try allocator.alloc(u8, 200);
const c = try allocator.alloc(u8, 300);
```

You do not free each one separately. `arena.deinit()` frees them all.

Use arenas for parsers, request handling, temporary data, and build steps.

### E.15 Do Not Store Borrowed Slices Too Long

A borrowed slice points to memory owned by someone else.

Bad pattern:

```zig
const User = struct {
    name: []const u8,
};
```

This is fine only if the memory behind `name` lives long enough.

If you store a slice in a long-lived object, ask:

Where does this memory come from?

How long does it live?

Who frees it?

If the source memory is temporary, copy it:

```zig
user.name = try allocator.dupe(u8, input_name);
```

Then free it later.

### E.16 Be Careful with `std.ArrayList.items`

`list.items` is a slice into the list’s internal buffer.

```zig
const items = list.items;
```

If the list grows, its buffer may move.

Bad:

```zig
const items = list.items;
try list.append(99);

std.debug.print("{}\n", .{items[0]});
```

After `append`, the old `items` slice may be invalid.

Good:

```zig
try list.append(99);
const items = list.items;

std.debug.print("{}\n", .{items[0]});
```

Do not keep old slices across operations that may reallocate.

### E.17 Be Careful with Pointers Into Containers

This is similar to `ArrayList.items`.

Bad:

```zig
const ptr = &list.items[0];
try list.append(99);

ptr.* = 10;
```

If `append` reallocates, `ptr` may point to old memory.

Fix by taking the pointer after all growth:

```zig
try list.append(99);

const ptr = &list.items[0];
ptr.* = 10;
```

### E.18 Prefer Explicit Lifetimes in API Design

Zig does not have Rust-style lifetime annotations. You must design lifetimes clearly.

Good comments help:

```zig
/// Returns a slice that points into `input`.
/// The returned slice must not outlive `input`.
fn firstWord(input: []const u8) []const u8 {
    // ...
}
```

Or:

```zig
/// Returns newly allocated memory.
/// Caller owns the returned slice and must free it with `allocator.free`.
fn copyWord(allocator: std.mem.Allocator, input: []const u8) ![]u8 {
    // ...
}
```

These two functions have different ownership rules. Make that visible.

### E.19 Use Debug Builds While Learning

Debug builds include safety checks.

Use:

```bash
zig build-exe main.zig
```

or:

```bash
zig run main.zig
```

Release-fast builds remove some checks for speed.

Do not start by optimizing. Start by making the program correct.

### E.20 Use the General Purpose Allocator for Leak Checks

During development, use `GeneralPurposeAllocator`.

```zig
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer {
    const status = gpa.deinit();
    if (status == .leak) {
        std.debug.print("memory leak detected\n", .{});
    }
}

const allocator = gpa.allocator();
```

This helps catch memory leaks while testing.

### E.21 Prefer Small Ownership Boundaries

Do not spread allocation and cleanup across many files unless necessary.

Hard to follow:

```zig
// allocated in one module
// stored in another
// freed somewhere else later
```

Easier:

```zig
fn run(allocator: std.mem.Allocator) !void {
    const data = try loadData(allocator);
    defer freeData(allocator, data);

    try process(data);
}
```

Keep the lifetime visible.

### E.22 Checklist Before Returning a Slice

Before returning `[]T` or `[]const T`, ask:

| Question | Safe answer |
|---|---|
| Does it point to local stack memory? | No |
| Does it point to freed memory? | No |
| Does the caller know who owns it? | Yes |
| Does the caller know how long it lives? | Yes |
| If allocated, does caller know how to free it? | Yes |

Bad:

```zig
fn bad() []const u8 {
    var buf = [_]u8{ 'h', 'i' };
    return buf[0..];
}
```

Good:

```zig
fn good(allocator: std.mem.Allocator) ![]u8 {
    const buf = try allocator.alloc(u8, 2);
    buf[0] = 'h';
    buf[1] = 'i';
    return buf;
}
```

Caller frees the result.

### E.23 Checklist Before Storing a Pointer

Before storing `*T`, ask:

| Question | Safe answer |
|---|---|
| What object does it point to? | Known |
| Who owns that object? | Known |
| Can the object move? | No, or pointer is refreshed |
| Can the object be freed first? | No |
| Can another thread mutate it? | Controlled |

Pointers are powerful. They should have clear ownership and lifetime rules.

### E.24 Checklist Before Using `undefined`

Before using `undefined`, ask:

| Question | Safe answer |
|---|---|
| Will every field or byte be written before reading? | Yes |
| Is this needed for performance or API shape? | Yes |
| Would a normal initializer be clearer? | No |
| Can tests catch misuse? | Yes |

Prefer normal initialization when possible.

```zig
var x: i32 = 0;
```

Use `undefined` only when you need it.

### E.25 The Core Memory Rule

Most memory bugs come from one of four mistakes:

| Bug | Meaning |
|---|---|
| Use before init | Reading memory before it has a real value |
| Use after free | Reading memory after cleanup |
| Out of bounds | Reading outside valid range |
| Wrong owner | Freeing or storing memory with unclear ownership |

Zig gives you tools to avoid these bugs, but it does not remove your responsibility.

A safe Zig program is built from visible ownership, careful lifetimes, explicit allocation, and consistent cleanup.

