A bug or just a footgun?

I’m trying to figure out if the below is a bug (missed warning or error) or just a footgun related returning a pointer to a stack value.

I had this bug when coding day 17 for AoC 2024 and noticed that the outSlice() method was returning garbage. I think what’s happening is that the VM instance gets passed by value from main() into the outSlice() method, and then that method returns a pointer to a local VM copy’s out buffer.

This head scratcher got me thinking: why would I ever pass self as anything else than a pointer?

const std = @import("std");

const VM = struct {
    const max_output = 32;
    out: [max_output]u8 = undefined,
    out_len: usize,

    pub fn init() VM {
        return VM{
            .out_len = 0,
        };
    }

    pub fn appendOut(self: *@This(), v: u8) void {
        self.out[self.out_len] = v;
        self.out_len += 1;
    }

    pub fn outSlicePtr(self: *VM) []const u8 {
        return self.out[0..self.out_len];
    }

    pub fn outSlice(self: VM) []const u8 {
        return self.out[0..self.out_len];
    }

    pub fn print(self: VM) void {
        std.debug.print("output: ", .{});
        for (self.out[0..self.out_len], 0..) |v, i| {
            if (i == 0) {
                std.debug.print("{d}", .{v});
            } else {
                std.debug.print(",{d}", .{v});
            }
        }
        std.debug.print("\n", .{});
    }
};

pub fn main() void {
    var vm = VM.init();
    vm.print();
    vm.appendOut(1);
    vm.appendOut(2);
    vm.appendOut(3);
    vm.print();

    const out = vm.outSlicePtr();
    std.debug.print("works:  {any}\n", .{out});

    const out2 = vm.outSlice();
    std.debug.print("broken: {any}\n", .{out2});
}

For your case It is better to use *const VM.


When you have an immutable structure and the size of the structure is small (up to the size of two registers). The reason is because you don’t need to allocate extra memory for the object.

Example:

const Rational = struct {
    num: i32, 
    den: u32,

    fn add(x: Rational, y: Rational) Rational {
        if (x.den == y.den) {
            return .{ .num = x.num + y.num, .den = x.den};
        }
        ...
    }
 };

outSlice returns a pointer to temporary memory. Since the out field is an array, it is copied when you pass VM by value. You return a pointer (slice) to this local copy which becomes invalid after the function returns.

1 Like

It’s a bug in your code. We can’t blame the compiler, pointers to stack variables is a surprisingly hard problem for static analyzers.

For the same reasons that you would pass any parameter by value.

I should have read the question fully, sorry. I can see you already understand that.

1 Like

Yeah, so the thing is I’ve managed to do the “return stack address” bug in Zig a bunch of times with relative ease, whereas this would be a rare occurrence if I was writing C. It’s probably because I’m more experienced in C and more vigilant because I know C is a glass cannon, but still… somehow I’m a bit blind to this class of bugs in Zig.

BTW, GCC issues a warning about this with default settings:

#include <stdio.h>

typedef struct {
    char buf[256];
} Test;


static char* foo(Test a) {
    return &a.buf[0];
}

int main() {
    Test a;
    char* p = foo(a);
}

gcc foo.c
foo.c: In function ‘foo’:
foo.c:10:12: warning: function returns address of local variable [-Wreturn-local-addr]
   10 |     return &a.buf[0];
      |            ^~~~~~~~~

I’d imagine this would be the type of thing Zig linting tools would be able to catch in the future. In my opinion, as a bug this is at least on the same level of severity as the current “const vs var” or “unused variable” errors Zig issues.

BTW I hope I don’t come across as a complainer. This particular type of bug just seems to come easy for me and I can’t quite point my finger what it is about the language that makes it easier than in C.

2 Likes

I think this and related issues are about preventing these errors:

3 Likes