Chaz
January 29, 2026, 1:08am
1
Zig knows not to free compile-time known memory
I know pzittlau has already dug into the implementation of Allocator.free() and answered this question, but now I have a tangent, a question about this other idea.
Given a pointer, is there a way to tell that the memory it points to is comptime-known? (Or from any other specific region, like the stack?)
Somewhere in the compiler/linker they must know where to place something but afaik there is no easy way to get to it.
In theory there is the address_space of the pointer struct in the Type enum:
pub const Pointer = struct {
// [...]
address_space: AddressSpace,
// [...]
}
But it isn’t really used for that and an address space is also something a bit different from the memory region.
The other alternative is to do it at runtime. On linux you can use a combination of auxv, /proc/self/maps and the ELF file(which you can get with /proc/self/exe) to find in which segment(or section) if any the pointer points to.
2 Likes
Here is something I hacked together for Zig 0.15.1. It’s by no means good code but just for illustration.
const std = @import("std");
const elf = std.elf;
const mem = std.mem;
const fs = std.fs;
const assert = std.debug.assert;
const static_str = "I am a compile-time constant";
const global_const: u64 = 64;
var global_var: u64 = 64;
pub fn main() !void {
try printMaps();
// Stack variable
var stack_var: u64 = 0xDEADBEEF;
// Heap variable
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const heap_ptr = try allocator.create(u64);
defer allocator.destroy(heap_ptr);
heap_ptr.* = 12345;
try checkPointer(@ptrCast(&stack_var), "Stack Var");
try checkPointer(@ptrCast(heap_ptr), "Heap Ptr");
try checkPointer(@ptrCast(static_str), "Static String");
try checkPointer(@ptrCast(&global_var), "Global Variable");
try checkPointer(@ptrCast(&global_var), "Global Constant");
try checkPointer(@ptrCast(&printMaps), "Function Pointer");
}
fn printMaps() !void {
const path = "/proc/self/maps";
var reader = try std.fs.cwd().openFile(path, .{});
var buffer: [1024 * 1024]u8 = undefined;
const size = try reader.readAll(&buffer);
std.debug.print("\n{s}\n", .{buffer[0..size]});
}
fn checkPointer(ptr: *const anyopaque, name: []const u8) !void {
const addr = @intFromPtr(ptr);
// Open ELF File to check Sections
const exe_file = try fs.openFileAbsolute("/proc/self/exe", .{});
defer exe_file.close();
var exe_buffer: [128]u8 = undefined;
var exe_reader_inst = exe_file.reader(&exe_buffer);
const exe_reader = &exe_reader_inst.interface;
const ehdr = try elf.Header.read(exe_reader);
var load_bias: usize = 0;
if (ehdr.type == .DYN) {
// Calculate Load Bias (using auxv)
// We need this because the addresses in the ELF file (like .rodata) are relative.
// The Load Bias = Runtime_Address_of_Headers (AT_PHDR) - File_Offset_of_Headers (phoff)
var at_phdr: ?usize = null;
if (std.os.linux.elf_aux_maybe) |auxv| {
var i: usize = 0;
while (auxv[i].a_type != elf.AT_NULL) : (i += 1) {
if (auxv[i].a_type == elf.AT_PHDR) {
at_phdr = auxv[i].a_un.a_val;
break;
}
}
}
load_bias = at_phdr.? - ehdr.phoff;
}
// Find the String Table Section Header first to get its file offset
var strtab_offset: u64 = 0;
var find_strtab_iter = ehdr.iterateSectionHeaders(&exe_reader_inst);
var sh_idx: usize = 0;
while (try find_strtab_iter.next()) |shdr| : (sh_idx += 1) {
if (sh_idx == ehdr.shstrndx) {
strtab_offset = shdr.sh_offset;
break;
}
}
var section_iter = ehdr.iterateSectionHeaders(&exe_reader_inst);
while (try section_iter.next()) |shdr| {
// Calculate where this section actually lives in memory right now
const section_start = shdr.sh_addr + load_bias;
const section_end = section_start + shdr.sh_size;
if (addr >= section_start and addr < section_end) {
var name_buf: [64]u8 = undefined;
// Read the name using preadAll to avoid disturbing the iterator's buffered reader
const len = try exe_file.preadAll(&name_buf, strtab_offset + shdr.sh_name);
const section_name = std.mem.sliceTo(name_buf[0..len], 0);
std.debug.print("{s:<15} ({p}) -> ELF Section ({s})\n", .{ name, ptr, section_name });
return;
}
}
// Check /proc/self/maps (Stack / Heap)
// If it wasn't in a specific ELF section, check the OS memory map
const maps_file = try fs.openFileAbsolute("/proc/self/maps", .{});
defer maps_file.close();
var maps_buffer: [4096]u8 = undefined;
var maps_reader_inst = maps_file.reader(&maps_buffer);
const maps_reader = &maps_reader_inst.interface;
while (true) {
const line = maps_reader.takeDelimiterExclusive('\n') catch |err| switch (err) {
error.EndOfStream => break,
else => return err,
};
// Line format: 00400000-00401000 r-xp ... /path/to/file or [stack]
var it = mem.tokenizeAny(u8, line, " ");
const range_str = it.next() orelse continue;
// Parse range "start-end"
var range_it = mem.splitScalar(u8, range_str, '-');
const start = try std.fmt.parseInt(usize, range_it.first(), 16);
const end = try std.fmt.parseInt(usize, range_it.next().?, 16);
if (addr >= start and addr < end) {
const rest = it.rest();
if (mem.containsAtLeast(u8, rest, 1, "[stack]")) {
std.debug.print("{s:<15} ({p}) -> Stack\n", .{ name, ptr });
return;
} else if (mem.containsAtLeast(u8, rest, 1, "[heap]")) {
std.debug.print("{s:<15} ({p}) -> Heap (brk)\n", .{ name, ptr });
return;
} else {
std.debug.print("{s:<15} ({p}) -> Heap (mmap/anon)\n", .{ name, ptr });
return;
}
}
}
std.debug.print("{s:<15} ({p}) -> Unknown Region\n", .{ name, ptr });
}
2 Likes
Chaz
January 29, 2026, 2:01pm
4
The broad areas to search for and some working code! Thank you!!
1 Like