Zig Version
0.16.0-dev.1484+d0ba6642b
Steps to Reproduce and Observed Behavior
A fairly minimal repro
exe.c
void doSomething(void);
int main(void) {
doSomething();
}
dl_exe.c
#include <dlfcn.h>
typedef void (*zigFn)(void);
int main(void) {
void* lib = dlopen("./zig-out/lib/libso.so", RTLD_LAZY);
zigFn doSomething = dlsym(lib, "doSomething");
doSomething();
}
so.zig
conststd=@import("std");constglobal=struct{threadlocalvarx:i32=4;};pubnoinlinefndoErr()!void{returnerror.OhNo;}pubexportfndoSomething()void{_=std.posix.write(0,"hello\n")catch{};if(global.x==4){doErr()catch{};}_=std.posix.write(0,"world\n")catch{};}
build.zig
conststd=@import("std");pubfnbuild(b:*std.Build)void{constso=b.addLibrary(.{.name="so",.linkage=.dynamic,.root_module=b.createModule(.{.root_source_file=b.path("so.zig"),.target=b.graph.host,.optimize=.Debug,}),});constdl_exe=b.addExecutable(.{.name="dl_exe",.root_module=b.createModule(.{.target=b.graph.host,.optimize=.Debug,}),});dl_exe.addCSourceFile(.{.file=b.path("dl_exe.c"),});dl_exe.linkLibC();constexe=b.addExecutable(.{.name="exe",.root_module=b.createModule(.{.target=b.graph.host,.optimize=.Debug,}),});exe.addCSourceFile(.{.file=b.path("exe.c"),});exe.linkLibrary(so);exe.linkLibC();b.installArtifact(so);b.installArtifact(dl_exe);b.installArtifact(exe);}
When running dl_exe, we segfault in builtin.returnError. Disassembly in gdb shows this is on an access of %r9 which holds 0x0. When running exe we do not see this
I believe this is because zig's codegen is not backing up caller saved registers when calling __tls_get_addr. On the normal path __tls_get_addr goes through the fast path and doesn't touch %r9. In the slow path it ends up in __tls_get_addr_slow, which eventually ends up in _dl_update_slotinfo which has some
0x00007ffff7fd84a0 <+416>: xor %edx,%edx
0x00007ffff7fd84a2 <+418>: xor %ecx,%ecx
0x00007ffff7fd84a4 <+420>: xor %esi,%esi
0x00007ffff7fd84a6 <+422>: xor %edi,%edi
0x00007ffff7fd84a8 <+424>: xor %r9d,%r9d
0x00007ffff7fd84ab <+427>: xor %r10d,%r10d
In this case %r9 gets clobbered, zig doesn't restore, and when writing the return trace (i think that's what r9 is...) we dereference null
This manifests fairly easily when using the standard library. Any function that locks a mutex will try to access a thread local thread id and segfault if any error is returned.
Note that if we do an actual extern c call, zig's compiler seems to correctly restore r9 from the stack after the call as expected.
I'm not in tune enough with zig codegen to file a fix, but I believe this is a fairly strong root cause
Expected Behavior
dl_exe does not segfault
### Zig Version
0.16.0-dev.1484+d0ba6642b
### Steps to Reproduce and Observed Behavior
A fairly minimal repro
exe.c
```c
void doSomething(void);
int main(void) {
doSomething();
}
```
dl_exe.c
```c
#include <dlfcn.h>
typedef void (*zigFn)(void);
int main(void) {
void* lib = dlopen("./zig-out/lib/libso.so", RTLD_LAZY);
zigFn doSomething = dlsym(lib, "doSomething");
doSomething();
}
```
so.zig
```zig
const std = @import("std");
const global = struct {
threadlocal var x: i32 = 4;
};
pub noinline fn doErr() !void {
return error.OhNo;
}
pub export fn doSomething() void {
_ = std.posix.write(0, "hello\n") catch {};
if (global.x == 4) {
doErr() catch {};
}
_ = std.posix.write(0, "world\n") catch {};
}
```
build.zig
```zig
const std = @import("std");
pub fn build(b: *std.Build) void {
const so = b.addLibrary( .{
.name = "so",
.linkage = .dynamic,
.root_module = b.createModule(.{
.root_source_file = b.path("so.zig"),
.target = b.graph.host,
.optimize = .Debug,
}),
});
const dl_exe = b.addExecutable(.{
.name = "dl_exe",
.root_module = b.createModule(.{
.target = b.graph.host,
.optimize = .Debug,
}),
});
dl_exe.addCSourceFile(.{
.file = b.path("dl_exe.c"),
});
dl_exe.linkLibC();
const exe = b.addExecutable(.{
.name = "exe",
.root_module = b.createModule(.{
.target = b.graph.host,
.optimize = .Debug,
}),
});
exe.addCSourceFile(.{
.file = b.path("exe.c"),
});
exe.linkLibrary(so);
exe.linkLibC();
b.installArtifact(so);
b.installArtifact(dl_exe);
b.installArtifact(exe);
}
```
When running dl_exe, we segfault in builtin.returnError. Disassembly in gdb shows this is on an access of %r9 which holds 0x0. When running exe we do not see this
I believe this is because zig's codegen is not backing up caller saved registers when calling __tls_get_addr. On the normal path __tls_get_addr goes through the fast path and doesn't touch %r9. In the slow path it ends up in __tls_get_addr_slow, which eventually ends up in _dl_update_slotinfo which has some
```
0x00007ffff7fd84a0 <+416>: xor %edx,%edx
0x00007ffff7fd84a2 <+418>: xor %ecx,%ecx
0x00007ffff7fd84a4 <+420>: xor %esi,%esi
0x00007ffff7fd84a6 <+422>: xor %edi,%edi
0x00007ffff7fd84a8 <+424>: xor %r9d,%r9d
0x00007ffff7fd84ab <+427>: xor %r10d,%r10d
```
In this case %r9 gets clobbered, zig doesn't restore, and when writing the return trace (i think that's what r9 is...) we dereference null
This manifests fairly easily when using the standard library. Any function that locks a mutex will try to access a thread local thread id and segfault if any error is returned.
Note that if we do an actual extern c call, zig's compiler seems to correctly restore r9 from the stack after the call as expected.
I'm not in tune enough with zig codegen to file a fix, but I believe this is a fairly strong root cause
### Expected Behavior
dl_exe does not segfault