Memory ownership and lifetime

Always ask yourself: who owns the memory behind input and how long it lives?
When you receive a []const u8 parameter, you’re getting a slice, which is just a pointer + length. You have no idea where that pointer points to.
Example:

// a stack buffer
var buf: [64]u8 = undefined;
const s = buf[0..5];
store_input(allocator, s); // buf dies when caller's scope ends

// a temporary allocation they're about to free
const s = try allocator.alloc(u8, 10);
defer allocator.free(s);
store_input(allocator, s); // freed right after this line

// a string literal — this one is actually fine
store_input(allocator, "hello"); // static memory, lives forever
store_input

So if store_input just returns input directly:

fn store_input(input: []const u8) []const u8 {
return input; // you're just returning the same pointer
}
store_input direct

The caller gets back a slice pointing to memory they already own and may free or invalidate at any time. If you store that returned slice somewhere and access it later, it’s a dangling pointer. Classic use-after-free.

By doing allocator.dupe(u8, input) instead:

fn store_input(allocator: std.mem.Allocator, input: []const u8) ![]const u8 {
return allocator.dupe(u8, input);
}
store_input on the heap

… you’re allocating new memory on the heap, copying the bytes from input into it, and returning a pointer to that. Now the returned slice is completely independent of whatever the caller passed in. Its lifetime is controlled by whoever holds the allocator and decides to free it, typically the caller, after they’re done with it:

const stored = try store_input(allocator, some_input);
defer allocator.free(stored); // caller owns it, caller frees it
controlled lifetime

The key mental model is: a []const u8 parameter is a borrowed view, not owned data. If you need to outlive the borrow, you must make your own copy.

AdP

Archived: Zig