Thread panic when returning an error after initializing GLFW

Hi, I ran into an issue using GLFW with Zig 0.16.0 on Gentoo (Wayland/niri).

I first hit this in a bigger project, where I could open a window and render OpenGL quads fine, so GLFW itself is working. The panic only shows up when an error propagates out of main unhandled. Here’s the smallest repro I could get it down to:

main.zig

const std = @import("std");
const c = @cImport({
    @cInclude("GLFW/glfw3.h");
});

pub fn main(init: std.process.Init) !void {
    _ = init;
    if (c.glfwInit() == 0) return error.GlfwInitFailed;
    defer c.glfwTerminate();

    return error.DeliberateTestError;
}

build.zig

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const exe = b.addExecutable(.{
        .name = "game",
        .root_module = b.createModule(.{
            .root_source_file = b.path("src/main.zig"),
            .target = target,
            .optimize = optimize,
            .link_libc = true,
        }),
    });

    exe.root_module.linkSystemLibrary("glfw", .{});

    b.installArtifact(exe);

    const run_step = b.step("run", "Run the application");
    const run_cmd = b.addRunArtifact(exe);
    run_step.dependOn(&run_cmd.step);

    run_cmd.step.dependOn(b.getInstallStep());
    if (b.args) |args| {
        run_cmd.addArgs(args);
    }
}

Here is the command I used to run the program:

$ zig build run
thread 19458 panic: attempt to use null value
error return context:

Then it just hangs, has to be killed.

If I remove the return error.DeliberateTestError; line so main exits normally, no problem at all. If I keep the error but don’t call glfwInit(), also no problem and it prints the error trace and exits. So glfwInit() specifically seems to be the thing that triggers it, not window/context creation or anything that comes normally after.

Backtrace from gdb:

$ gdb --args zig-out/bin/game 
GNU gdb (Gentoo 17.2 vanilla) 17.2
Copyright (C) 2025 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-pc-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://bugs.gentoo.org/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from zig-out/bin/game...
(gdb) run
Starting program: /home/thomas/Projects/Games/balian/zig-out/bin/game 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib64/libthread_db.so.1".
[New Thread 0x7ffff5f016c0 (LWP 19707)]
[New Thread 0x7ffff57006c0 (LWP 19708)]
[New Thread 0x7ffff4eff6c0 (LWP 19709)]
[New Thread 0x7fffe7fff6c0 (LWP 19710)]
[New Thread 0x7fffe77fe6c0 (LWP 19711)]
thread 19703 panic: attempt to use null value
error return context:
^C
Thread 1 "game" received signal SIGINT, Interrupt.
0x00000000010db8eb in os.linux.x86_64.syscall4 (number=futex, arg1=19318312, arg2=128, arg3=2, arg4=0)
(gdb) bt
#0  0x00000000010db8eb in os.linux.x86_64.syscall4 ()
#1  0x00000000010db4cf in os.linux.futex_4arg ()
#2  0x00000000010dab54 in Io.Threaded.Thread.futexWaitInner ()
#3  0x00000000010d890f in Io.Threaded.Thread.futexWaitUncancelable ()
#4  0x00000000010d87f7 in Io.Threaded.mutexLock ()
#5  0x00000000010d590d in Io.Threaded.scanEnviron ()
#6  0x00000000010d5a15 in Io.Threaded.environString__anon_13417 ()
#7  0x00000000010d0734 in debug.ElfFile.DebugInfoSearchPaths.native ()
#8  0x00000000010bddee in debug.SelfInfo.Elf.Module.loadElf ()
#9  0x00000000010bd694 in debug.SelfInfo.Elf.Module.getLoadedElf ()
#10 0x000000000103dfa1 in debug.SelfInfo.Elf.getSymbols ()
#11 0x00000000010272cf in debug.printSourceAtAddress ()
#12 0x00000000011241d1 in debug.writeTrace ()
#13 0x0000000001123c89 in debug.writeErrorReturnTrace ()
#14 0x000000000102521f in debug.defaultPanic ()
#15 0x000000000102ffa4 in debug.FullPanic((function 'defaultPanic')).unwrapNull ()
#16 0x00000000010d5ad6 in Io.Threaded.Environ.scan ()
#17 0x00000000010d597f in Io.Threaded.scanEnviron ()
#18 0x0000000001164c30 in Io.Threaded.initLockedStderr ()
#19 0x00000000011653fc in Io.Threaded.lockStderr ()
#20 0x00000000011263ff in Io.lockStderr ()
#21 0x00000000011260c5 in debug.lockStderr ()
#22 0x00000000011d25d5 in log.defaultLog__anon_35555 ()
#23 0x00000000011d24cb in log.log__anon_35548 ()
#24 0x00000000011d246b in log.scoped(.default).err__anon_32808 ()
#25 0x00000000011ccb51 in start.main ()
#26 0x00007ffff7cd21fe in ??? () at /usr/lib64/libc.so.6
#27 0x00007ffff7cd231b in __libc_start_main () at /usr/lib64/libc.so.6
#28 0x00000000012042b5 in _start ()
(gdb) 

I also tested with the official zig-x86_64-linux-0.16.0 binary from ziglang.org (not just my Gentoo-built one), same exact panic and backtrace, so it doesn’t look like a packaging issue on my end.

Let me know if there’s anything else I can test. Thanks in advance!

Copy-pasted your exact code, though did add exe.use_llvm = true; in build.zig (there is some issue when linking with libc).

Running on Arch/Hyprland (Zig 0.16) works as expected.

❯ zig build run
error: DeliberateTestError
/home/eric/zig/temp/src/main.zig:11:5: 0x11725d5 in main (game)
    return error.DeliberateTestError;
    ^
run
└─ run exe game failure
error: process exited with error code 1
failed command: /home/eric/zig/temp/zig-out/bin/game

Build Summary: 3/5 steps succeeded (1 failed)
run transitive failure
└─ run exe game failure

error: the following build command failed with exit code 1:
.zig-cache/o/a2d9c775799e82cbd685e268d70660c6/build /home/eric/.local/share/zvm/0.16.0/zig /home/eric/.l
ocal/share/zvm/0.16.0/lib /home/eric/zig/temp .zig-cache /home/eric/.cache/zig --seed 0x80ddb840 -Zdf98d
c787cea2bd3 run

It really doesn’t appear to be anything with your code, nothing amiss there aside from using the deprecated @cImport.

1 Like

Thanks for your reply! I was not aware that @cImport was deprecated.

Adding exe.root_module.use_llvm = true did not fix it for me.

I tried to use the new translate_c:

build.zig

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const translate_c = b.addTranslateC(.{
        .root_source_file = b.path("vendors/c.h"),
        .target = target,
        .optimize = optimize,
        // .link_libc = true,
    });
    translate_c.linkSystemLibrary("glfw", .{});

    const exe = b.addExecutable(.{
        .name = "game",
        .root_module = b.createModule(.{
            .root_source_file = b.path("src/main.zig"),
            .target = target,
            .optimize = optimize,
            .imports = &.{
                .{
                    .name = "c",
                    .module = translate_c.createModule(),
                },
            },
        }),
    });
    // exe.use_llvm = true;
    b.installArtifact(exe);

    const run_step = b.step("run", "Run the application");
    const run_cmd = b.addRunArtifact(exe);
    run_step.dependOn(&run_cmd.step);

    run_cmd.step.dependOn(b.getInstallStep());
    if (b.args) |args| {
        run_cmd.addArgs(args);
    }
}

c.h

#include <GLFW/glfw3.h>

main.zig

const std = @import("std");
const c = @import("c");

pub fn main(init: std.process.Init) !void {
    _ = init;
    if (c.glfwInit() == 0) return error.GlfwInitFailed;
    errdefer c.glfwTerminate();

    return error.DeliberateTestError;
}

Adding or removing use_llvm or link_libc does not change anything.

I still get the same result:

$ zig build run
thread 12595 panic: attempt to use null value
error return context:

Once again, the code run fine when main do not return an error.

I have a windows machine, i will try to test this code on it…

1 Like

No, I didn’t expect so, was just clarifying that I did make one modification to your example, but this is because of an unrelated linking issue.

My point being is that I don’t think your problem is related to your code or what you are doing in Zig, but something else in the environment. I apologize, I know this is not very helpful in narrowing it down, but it does show that your code runs normally outside of your environment.

1 Like

That actually helps, thank you! Knowing that the exact same code runs normally on your end confirms that my Zig code isn’t the problem here.

For now, I think I will switch over to using the package from GitHub - tiawl/glfw.zig: @glfw packaged for @ziglang · GitHub

If I manage to pin down exactly what is broken in my Gentoo/Wayland environment later on, I will make sure to update this thread with the solution.

I tested my code on Windows, and it worked correctly. Returning an error from main does not cause a panic there, so this confirms again that the issue is related to my environment rather than the code itself.

My Gentoo setup is probably too minimal and may be missing something required at runtime :sweat_smile:

I also tried using the packaged GLFW from tiawl, but I got the same result. It’s strange because I can render quads with OpenGL, handle the window, and process input correctly, yet returning an error still causes the issue…

1 Like

I’ve done some more digging and can confirm it’s also happening on my NixOS PC. This is not a minimal system like my Gentoo one.

I ran the same test using SDL3 instead, and it works perfectly:

const std = @import("std");
const c = @import("c");

pub fn main(init: std.process.Init) !void {
    _ = init;
    if (c.SDL_Init(c.SDL_INIT_VIDEO) == false) return error.SDLInitFailed;
    defer c.SDL_Quit();

    return error.DeliberateTestError;
}
$ ./zig-out/bin/bal 
error: DeliberateTestError
/home/thomas/Projects/Games/bal/src/main.zig:12:5: 0x11cd33f in main (main.zig)
    return error.DeliberateTestError;
    ^

Since the SDL3 path works, I think I’m just going to move over to that for this project but if anyone has any ideas on what might be causing this, I’d still love to know!

Based on entry 16 my suspicion is that GLFW is modifying environ/envp. Zig std incorrectly assumes that envp is immutable and eagerly computes the length and converts it into a slice on startup, and Io.Threaded.Environ.scan will iterate over the full slice by length and not by scanning for the null sentinel, so if GLFW has used libc to modify the environment you end up with this crash. The hang is because stderr is already locked by the time the panic handler tries to lock it for writing.

This is a known bug: https://codeberg.org/ziglang/zig/issues/35512

You might be able to work around it by adding

pub fn main(init: std.process.Init) !void {
    std.debug.lockStderr(&.{});
    std.debug.unlockStderr();
}

as your first two lines of your main function, before you initialize GLFW. This should trigger a scan for terminal color preference-related environment variables (e.g. NO_COLOR) and the result will subsequently be cached for the remainder of the application’s runtime, so that it won’t be affected if libc is used to mutate environ.

Whether or not envp (the C main argument) and environ alias or affect each other is an implementation detail of the libc, so that might explain why some OSes/distros have the problem and others don’t. (And Windows also handles environment variables very differently from POSIX.)

3 Likes

Yes, that was it. Adding those two lines fixed the issue for me. Thanks for your help!

1 Like