Std.c.ioctl() does not overwrite variable when it is marked constant

For example, in this code

const sizeWin: posix.winsize = undefined;
std.c.ioctl(std.posix.STDOUT_FILENO, posix.T.IOCGWINSZ, &sizeWin);

The variable sizeWin would not be overwritten, but if I were to declare it as var sizeWin = undefined; it would.

Now this makes sense from zig’s standpoint, yes, but ioctl() does not have knowledge that I declared it as const nor (I believe) does zig mark it in some way in memory to make it read-only (if this is even something that can be done).

So, if all that ioctl() does is get the adress and overwrite it, why does it care if the variable is declared const or var?

Thanks!

Depending on where the const is declared, zig can put it in the static and read only section of memory. (Though I’d be surprised you don’t get some kind of error trying to write to a read only section). It’s hard to tell without seeing fuller context of the code.

As a second note, a constant that is undefined is pretty useless. You have no idea what the data is behind that constant, and you can’t mutate it. Undefined is a declaration that you will not try to read the value without writing to it first. Since you cannot write to a const, you should never make a constant undefined.

5 Likes

As for the first point, this would be the full code snapshot I was using

 fn fetchSizeLinux() !TerminalSize {
        if (!std.posix.isatty(std.posix.STDOUT_FILENO)) {
            return error.NotATTY;
        }

        const sizeWin: std.posix.winsize = undefined;

        const ret = std.c.ioctl(std.posix.STDOUT_FILENO, posix.T.IOCGWINSZ, &sizeWin);

        if (std.posix.errno(ret) != .SUCCESS) {
            return error.IoctlCallFailed;
        }

        return sizeWin;
    }

And ret was always 0, except when I tried passing some invalid adresses to ioctl(), while testing. It seems weird to me too that ioctl() would not return -1, but it seems to be the case.

As for the second, yeah, I dont know what the heck I was thinking

It is surprising that you don’t trigger a memory protection crash.

The reason you don’t get a compile error is that it is using var args, which have no way to be type checked, even in c compilers have special case type checking on known varargs functions in libc. Zig doesn’t do that.

IDK what you want explained? A constant is constant, it shouldn’t be able to be changed, and the compiler is allowed to take advantage of that e.g: put it in read only memory or inline it into the instructions.

2 Likes

There’s a couple of factors at play here. The first is that sizeWin may be stack allocated here, so might not end up in read only memory. The second is that it doesn’t look like Zig’s linker is placing constants in read-only memory.

With LLVM (-OReleaseSmall), the following faults for me:

pub fn main() !void {
    const sizeWin: std.posix.winsize = undefined;
    const ret = std.c.ioctl(std.posix.STDOUT_FILENO, std.posix.T.IOCGWINSZ, &sizeWin);
    std.log.info("return: {}", .{ret});
    std.log.info("error: {}", .{std.posix.errno(ret)});
}

Output

info: return: -1
info: error: .FAULT

Presumably LLVM sees that the constant is compile-time known, and places it in .rodata, whereas Zig’s native backend doesn’t (even if I make sizeWin global).

If I force sizeWin to be stack allocated, then it succeeds again:

pub fn main() !void {
    var stack_allocated: std.posix.winsize = undefined;
    _ = &stack_allocated;
    const sizeWin: std.posix.winsize = stack_allocated;
    const ret = std.c.ioctl(std.posix.STDOUT_FILENO, std.posix.T.IOCGWINSZ, &sizeWin);
    std.log.info("return: {}", .{ret});
    std.log.info("error: {}", .{std.posix.errno(ret)});
}

Output:

info: return: 0
info: error: .SUCCESS

I get .SUCCESS for both of the above examples when using the native backend. I believe that is also valid behaviour. A block local constant being placed on the stack isn’t surprising.

The following should fault on both LLVM and Zig’s own backend, but doesn’t. It only fails on LLVM. A container level constant declaration really should be placed in .rodata in both cases.

const sizeWin: std.posix.winsize = undefined;

pub fn main() !void {
    const ret = std.c.ioctl(std.posix.STDOUT_FILENO, std.posix.T.IOCGWINSZ, &sizeWin);
    std.log.info("return: {}", .{ret});
    std.log.info("error: {}", .{std.posix.errno(ret)});
}
3 Likes

I figured it was an issue with the custom backend/linker, just couldn’t be bothered to check :sweat_smile:.

Unfortunately, due to c varargs, I don’t think this can be caught at compile time, at least it would be rather difficult that’s an understatement

Fixing the backend to at least ensure it becomes a fault is the only practical solution RN.

Seeing as you figured out the cause, could/have you make an issue if you can’t find an existing one