Regression: Segfault on Linux x86_64 with `cosmo_dlopen` when calling foreign function
Contact Details
What happened?
I have encountered a regression in the master branch (compared to 4.0.2) where APE binaries using cosmo_dlopen to load host libraries SEGFAULT on Linux x86_64 when trying to call foreign functions imported with cosmo_dlsym. I have tested the same binary on macOS aarch64, Linux aarch64 and Windows x86_64 and it worked fine on those environments. Also, when compiled with Cosmopolitan 4.0.2 it work correctly on all platforms.
I bisected the issue to commit 5ddb5c2. This commit changed the TLS ABI and switched from %gs to %fs as the address for storing the Cosmo TIB in Linux and FreeBSD hosts. I think this may have introduced some incompatibility with host libraries, but I am not sure why as I am a bit out of my depth here.
Reproduction
Code (sdl2_test.c):
Click to expand C source#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dlfcn.h>
#include <cosmo.h>
typedef struct SDL_Window SDL_Window;
typedef struct SDL_Renderer SDL_Renderer;
typedef struct SDL_Texture SDL_Texture;
#define SDL_INIT_VIDEO 0x00000020u
#define SDL_WINDOWPOS_CENTERED 0x2FFF0000u
#define SDL_WINDOW_SHOWN 0x00000004u
#define SDL_QUIT 0x100
#define SDL_KEYDOWN 0x300
typedef struct {
uint32_t type;
uint32_t timestamp;
uint32_t windowID;
uint32_t state;
uint8_t repeat;
uint8_t padding2;
uint8_t padding3;
struct {
uint32_t scancode;
int32_t sym;
uint16_t mod;
uint32_t unused;
} keysym;
} SDL_KeyboardEvent;
typedef union {
uint32_t type;
SDL_KeyboardEvent key;
uint8_t padding[56];
} SDL_Event;
typedef struct {
int x, y, w, h;
} SDL_Rect;
typedef int (*PFN_SDL_Init)(uint32_t flags);
typedef void (*PFN_SDL_Quit)(void);
typedef SDL_Window* (*PFN_SDL_CreateWindow)(
const char* title, int x, int y, int w, int h, uint32_t flags
);
typedef SDL_Renderer* (*PFN_SDL_CreateRenderer)(SDL_Window* window, int index, uint32_t flags);
typedef int (*PFN_SDL_SetRenderDrawColor)(
SDL_Renderer* renderer, uint8_t r, uint8_t g, uint8_t b, uint8_t a
);
typedef int (*PFN_SDL_RenderClear)(SDL_Renderer* renderer);
typedef void (*PFN_SDL_RenderPresent)(SDL_Renderer* renderer);
typedef int (*PFN_SDL_RenderFillRect)(SDL_Renderer* renderer, const SDL_Rect* rect);
typedef int (*PFN_SDL_PollEvent)(SDL_Event* event);
typedef void (*PFN_SDL_DestroyRenderer)(SDL_Renderer* renderer);
typedef void (*PFN_SDL_DestroyWindow)(SDL_Window* window);
typedef const char* (*PFN_SDL_GetError)(void);
typedef const char* (*PFN_SDL_GetPlatform)(void);
typedef int (*PFN_SDL_GetCPUCount)(void);
typedef int (*PFN_SDL_GetSystemRAM)(void);
typedef struct {
PFN_SDL_Init Init;
PFN_SDL_Quit Quit;
PFN_SDL_CreateWindow CreateWindow;
PFN_SDL_CreateRenderer CreateRenderer;
PFN_SDL_SetRenderDrawColor SetRenderDrawColor;
PFN_SDL_RenderClear RenderClear;
PFN_SDL_RenderPresent RenderPresent;
PFN_SDL_RenderFillRect RenderFillRect;
PFN_SDL_PollEvent PollEvent;
PFN_SDL_DestroyRenderer DestroyRenderer;
PFN_SDL_DestroyWindow DestroyWindow;
PFN_SDL_GetError GetError;
PFN_SDL_GetPlatform GetPlatform;
PFN_SDL_GetCPUCount GetCPUCount;
PFN_SDL_GetSystemRAM GetSystemRAM;
} SDL_API;
SDL_API SDL;
void* load_sym(void* lib, const char* name) {
void* sym = cosmo_dlsym(lib, name);
if (!sym) {
return NULL;
}
return cosmo_dltramp(sym);
}
/* -- Minimal 5x7 Bitmap Font Data (ASCII 32-127 approx) -- */
// Each byte represents a column of the char (vertical scan)
const unsigned char font_data[][5] = {
{0,0,0,0,0}, {0,0,95,0,0}, {0,7,0,7,0}, {20,127,20,127,20}, // space ! " #
{36,42,127,42,18}, {35,19,8,100,98}, {54,73,85,34,5}, {0,5,3,0,0}, // $ % & '
{0,28,34,65,0}, {0,65,34,28,0}, {20,8,62,8,20}, {8,8,62,8,8}, // ( ) * +
{0,80,96,0,0}, {8,8,8,8,8}, {0,96,96,0,0}, {32,16,8,4,2}, // , - . /
{62,81,73,69,62}, {0,66,127,64,0}, {66,97,81,73,70}, {33,65,69,75,49}, // 0 1 2 3
{24,20,18,127,16}, {39,69,69,69,57}, {60,74,73,73,48}, {1,1,113,9,7}, // 4 5 6 7
{54,73,73,73,54}, {6,73,73,41,30}, {0,54,54,0,0}, {0,86,54,0,0}, // 8 9 : ;
{8,20,34,65,0}, {20,20,20,20,20}, {0,65,34,20,8}, {2,1,81,9,6}, // < = > ?
{50,73,121,65,62}, {126,17,17,17,126}, {127,73,73,73,54}, {62,65,65,65,34}, // @ A B C
{127,65,65,34,28}, {127,73,73,73,65}, {127,9,9,9,1}, {62,65,73,73,122}, // D E F G
{127,8,8,8,127}, {0,65,127,65,0}, {32,64,65,63,1}, {127,8,20,34,65}, // H I J K
{127,64,64,64,64}, {127,2,12,2,127}, {127,4,8,16,127}, {62,65,65,65,62}, // L M N O
{127,9,9,9,6}, {62,65,81,33,94}, {127,9,25,41,70}, {70,73,73,73,49}, // P Q R S
{1,1,127,1,1}, {63,64,64,64,63}, {31,32,64,32,31}, {63,64,56,64,63}, // T U V W
{99,20,8,20,99}, {7,8,112,8,7}, {97,81,73,69,67}, {0,127,65,65,0}, // X Y Z [
{2,4,8,16,32}, {0,65,65,127,0}, {4,2,1,2,4}, {64,64,64,64,64}, // \ ] ^ _
{0,1,2,4,0}, {32,84,84,84,120}, {127,72,68,68,56}, {56,68,68,68,32}, // ` a b c
{56,68,68,72,127}, {56,84,84,84,24}, {8,126,9,1,2}, {12,82,82,82,62}, // d e f g
{127,8,4,4,120}, {0,68,125,64,0}, {32,64,68,61,0}, {127,16,40,68,0}, // h i j k
{0,65,127,64,0}, {124,4,24,4,120}, {124,8,4,4,120}, {56,68,68,68,56}, // l m n o
{252,36,36,36,24}, {24,36,36,24,252}, {124,8,4,4,8}, {72,84,84,84,32}, // p q r s
{4,63,68,64,32}, {60,64,64,32,124}, {28,32,64,32,28}, {60,64,48,64,60}, // t u v w
{68,40,16,40,68}, {12,80,80,80,60}, {68,100,84,76,68}, {8,54,65,65,0}, // x y z {
{0,0,119,0,0}, {0,65,65,54,8}, {8,4,4,4,8} // | } ~
};
void draw_char(SDL_Renderer* r, char c, int x, int y, int scale) {
if (c < 32 || c > 126) {
c = 32;
}
int index = c - 32;
for (int col = 0; col < 5; col++) {
unsigned char col_data = font_data[index][col];
for (int row = 0; row < 8; row++) {
if ((col_data >> row) & 1) {
SDL_Rect rect = {x + (col * scale), y + (row * scale), scale, scale};
SDL.RenderFillRect(r, &rect);
}
}
}
}
void draw_string(SDL_Renderer* r, const char* str, int x, int y, int scale) {
while (*str) {
draw_char(r, *str, x, y, scale);
x += (6 * scale); // 5 width + 1 spacing
str++;
}
}
int main(int argc, char* argv[]) {
const char* lib_name;
if (IsWindows()) {
lib_name = "SDL2.dll";
} else if (IsXnu()) {
lib_name = "libSDL2.dylib";
} else {
lib_name = "libSDL2.so";
}
void* lib = cosmo_dlopen(lib_name, RTLD_LAZY);
if (!lib) {
// Try versioned name for Linux as a fallback
if (!IsWindows() && !IsXnu()) {
lib = cosmo_dlopen("libSDL2-2.0.so.0", RTLD_LAZY);
}
if (!lib) {
fprintf(stderr, "Failed to load SDL2: %s\n", cosmo_dlerror());
fprintf(stderr, "Make sure SDL2 is installed on your system.\n");
return 1;
}
}
#define BIND(x) \
SDL.x = (PFN_SDL_##x)load_sym(lib, "SDL_" #x); \
if (!SDL.x) { \
fprintf(stderr, "Missing symbol: %s\n", #x); \
return 1; \
}
BIND(Init);
BIND(Quit);
BIND(CreateWindow);
BIND(CreateRenderer);
BIND(SetRenderDrawColor);
BIND(RenderClear);
BIND(RenderPresent);
BIND(RenderFillRect);
BIND(PollEvent);
BIND(DestroyRenderer);
BIND(DestroyWindow);
BIND(GetError);
BIND(GetPlatform);
BIND(GetCPUCount);
BIND(GetSystemRAM);
if (SDL.Init(SDL_INIT_VIDEO) != 0) {
fprintf(stderr, "SDL_Init Error: %s\n", SDL.GetError());
return 1;
}
char info_os[64];
char info_cpu[64];
char info_ram[64];
snprintf(info_os, sizeof(info_os), "OS: %s", SDL.GetPlatform());
snprintf(info_cpu, sizeof(info_cpu), "CPU: %d Cores", SDL.GetCPUCount());
snprintf(info_ram, sizeof(info_ram), "RAM: %d MB", SDL.GetSystemRAM());
SDL_Window* win = SDL.CreateWindow("Cosmopolitan SDL2 Info", SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED, 800, 400, SDL_WINDOW_SHOWN);
if (!win) {
fprintf(stderr, "CreateWindow Error: %s\n", SDL.GetError());
SDL.Quit();
return 1;
}
SDL_Renderer* ren =
SDL.CreateRenderer(win, -1, 0);
if (!ren) {
fprintf(stderr, "CreateRenderer Error: %s\n", SDL.GetError());
SDL.DestroyWindow(win);
SDL.Quit();
return 1;
}
int running = 1;
SDL_Event e;
int scale = 3;
while (running) {
while (SDL.PollEvent(&e)) {
if (e.type == SDL_QUIT) {
running = 0;
} else if (e.type == SDL_KEYDOWN) {
if (e.key.keysym.sym == 0x1B) { // Escape
running = 0;
}
}
}
// Clear background (Dark Blue)
SDL.SetRenderDrawColor(ren, 20, 20, 40, 255);
SDL.RenderClear(ren);
// Set text color
SDL.SetRenderDrawColor(ren, 255, 255, 255, 255);
// Draw title
draw_string(ren, "SYSTEM INFO", 20, 20, 6);
// Draw system info
draw_string(ren, info_os, 20, 90, 4);
draw_string(ren, info_cpu, 20, 130, 4);
draw_string(ren, info_ram, 20, 170, 4);
SDL.RenderPresent(ren);
usleep(16000);
}
cosmo_dlclose(lib);
return 0;
}Compilation Command:
$> cosmocc -o sdl2_test.com sdl2_test.cExecution:
Requires libSDL2 installed on host.
$> ./sdl2_test.com
Segmentation fault (core dumped) ./sd_test.comProposed fix
I was able to fix this and restore the cosmo_dlopen and cosmo_dlsym functionality in x86_64 Linux by reverting the %gs => %fs change and also updating some places to always use the new __get_tls_* macros.
After applying the patch below to master, the newly compiled APE binary worked flawlessly on x86_64 Linux, while continuing to work fine on macOS aarch64, Linux aarch64 and Windows x86_64:
--- a/ape/specification.md
+++ b/ape/specification.md
@@ -361,13 +361,13 @@ Here's the TLS memory layout on x86_64:
```
__get_tls()
│
- %fs Linux/FreeBSD/NetBSD/OpenBSD
+ %fs NetBSD/OpenBSD
_Thread_local │
┌───┬──────────┬──────────┼───┐
│pad│ .tdata │ .tbss │tib│
└───┴──────────┴──────────┼───┘
│
- Windows/Mac %gs
+ Linux/FreeBSD/Windows/Mac %gs
```
Quite possibly the greatest challenge in Actually Portable Executable
--- a/libc/dlopen/foreign_tramp.S
+++ b/libc/dlopen/foreign_tramp.S
@@ -56,11 +56,7 @@ foreign_tramp:
mov %rax,-0xc0(%rbp)
// switch to foreign tls
-#if SupportsXnu() || SupportsWindows()
call __get_tls_rax
-#else
- mov %fs:0x30,%rax
-#endif
mov %rax,-0xc8(%rbp)
mov __foreign+8(%rip),%rdi
call __set_tls
--- a/libc/proc/vfork.S
+++ b/libc/proc/vfork.S
@@ -74,7 +74,7 @@ vfork:
#endif
pop %rbp
#endif
- mov %fs:0x30,%r9 // get thread information block
+ call __get_tls_r9 // get thread information block
mov 0x3c(%r9),%r8d // avoid question of @vforksafe errno
pop %rsi // saves return address in a register
mov __NR_vfork(%rip),%eax
--- a/libc/runtime/clone.c
+++ b/libc/runtime/clone.c
@@ -522,6 +522,12 @@ static errno_t CloneSilicon(int (*fn)(void *), char *stk, size_t stksz,
////////////////////////////////////////////////////////////////////////////////
// GNU/SYSTEMD
+struct LinuxCloneArgs {
+ int (*func)(void *);
+ void *arg;
+ char *tls;
+};
+
int sys_clone_linux(int flags, // rdi
long sp, // rsi
atomic_int *ptid, // rdx
@@ -530,12 +536,28 @@ int sys_clone_linux(int flags, // rdi
void *func, // r9
void *arg); // 8(rsp)
+dontinstrument static int AmdLinuxThreadEntry(void *arg) {
+ struct LinuxCloneArgs *wt = arg;
+#if defined(__x86_64__)
+ sys_set_tls(ARCH_SET_GS, wt->tls);
+#endif
+ return wt->func(wt->arg);
+}
+
static int CloneLinux(int (*func)(void *), char *stk, size_t stksz, int flags,
void *arg, void *tls, atomic_int *ptid,
atomic_int *ctid) {
long sp = (intptr_t)stk + stksz;
#if defined(__x86_64__)
+ sp -= sizeof(struct LinuxCloneArgs);
+ sp &= -alignof(struct LinuxCloneArgs);
+ struct LinuxCloneArgs *wt = (struct LinuxCloneArgs *)sp;
sp &= -16; // align the stack
+ wt->arg = arg;
+ wt->tls = tls;
+ wt->func = func;
+ func = AmdLinuxThreadEntry;
+ arg = wt;
#elif defined(__aarch64__)
sp &= -128; // for kernels <=4.6
#endif
--- a/libc/runtime/set_tls.c
+++ b/libc/runtime/set_tls.c
@@ -39,9 +39,9 @@ dontinstrument textstartup void __set_tls(struct CosmoTib *tib) {
if (IsWindows()) {
__set_tls_win32(tib);
} else if (IsLinux()) {
- sys_set_tls(ARCH_SET_FS, tib);
+ sys_set_tls(ARCH_SET_GS, tib);
} else if (IsFreebsd()) {
- sys_set_tls(AMD64_SET_FSBASE, tib);
+ sys_set_tls(AMD64_SET_GSBASE, tib);
} else if (IsNetbsd()) {
// netbsd has sysarch(X86_SET_FSBASE) but we can't use that because
// signal handlers will cause it to be reset due to not setting the
--- a/libc/sysv/gc.S
+++ b/libc/sysv/gc.S
@@ -37,11 +37,7 @@ __gc: beg
#ifdef __x86_64__
-#if SupportsXnu() || SupportsWindows()
call __get_tls_rcx
-#else
- mov %fs:0x30,%rcx
-#endif
mov 0x48(%rcx),%rcx // tls::garbages
decl (%rcx) // --g->i
mov (%rcx),%r8d // r8 = g->i
--- a/libc/sysv/systemfive.S
+++ b/libc/sysv/systemfive.S
@@ -107,11 +107,7 @@ systemfive_cp:
systemfive_cancellable: // our pthread_cancel() miracle code
cmpb $0,__tls_enabled(%rip) // inspired by the musl libc design!
je 1f // we handle linux and bsd together!
-#if SupportsXnu() || SupportsWindows()
call __get_tls_r10 // CosmoTib::tib_self
-#else
- mov %fs:0x30,%r10 // CosmoTib::tib_self
-#endif
mov 0x28(%r10),%r10 // CosmoTib::tib_pthread
test %r10,%r10 // is it a posix thread?
jz 1f // it's spawn() probably
@@ -137,11 +133,7 @@ systemfive_cancellable_end: // i/o calls park here for long time
jne systemfive_errno // werent interrupted by OnSigCancel
cmpb $0,__tls_enabled(%rip) // make sure it's safe to grab %fs:0
je systemfive_errno // tls is disabled we can't continue
-#if SupportsXnu() || SupportsWindows()
call __get_tls_rcx // CosmoTib::tib_self
-#else
- mov %fs:0x30,%rcx // CosmoTib::tib_self
-#endif
mov 0x28(%rcx),%rcx // CosmoTib::tib_pthread
test %rcx,%rcx // is it a posix thread?
jz systemfive_errno // it's spawn() probably
--- a/libc/sysv/tlsasm.S
+++ b/libc/sysv/tlsasm.S
@@ -28,13 +28,13 @@
.section \section,"ax",@progbits
.align 16
\name: mov __hostos(%rip),%\reg32
- test $_HOSTWINDOWS|_HOSTXNU,%\reg32
+ test $_HOSTWINDOWS|_HOSTXNU|_HOSTLINUX|_HOSTFREEBSD,%\reg32
jnz 1f
- mov %fs:0x30,%\reg64 // Linux, FreeBSD, OpenBSD, NetBSD
+ mov %fs:0x30,%\reg64 // OpenBSD, NetBSD
ret
1: test $_HOSTWINDOWS,%\reg32
jnz 1f
- mov %gs:0x30,%\reg64 // XNU
+ mov %gs:0x30,%\reg64 // Linux, FreeBSD, XNU
ret
1: mov __tls_index(%rip),%\reg32 // Windows
mov %gs:0x1480(,%\reg64,8),%\reg64 // Hooray!
@@ -61,13 +61,13 @@
.macro define_tls_adder name:req reg64:req tmp64:req tmp32:req
.section .privileged.\name,"ax",@progbits
.align 16
-\name: testb $_HOSTWINDOWS|_HOSTXNU,__hostos(%rip)
+\name: testb $_HOSTWINDOWS|_HOSTXNU|_HOSTLINUX|_HOSTFREEBSD,__hostos(%rip)
jnz 1f
- add %fs:0x30,%\reg64 // Linux, FreeBSD, OpenBSD, NetBSD
+ add %fs:0x30,%\reg64 // OpenBSD, NetBSD
ret
1: testb $_HOSTWINDOWS,__hostos(%rip)
jnz 1f
- add %gs:0x30,%\reg64 // XNU
+ add %gs:0x30,%\reg64 // Linux, FreeBSD, XNU
ret
1: push %\tmp64
mov __tls_index(%rip),%\tmp32 // Windows
--- a/libc/thread/tls.h
+++ b/libc/thread/tls.h
@@ -75,7 +75,7 @@ extern char __tls_enabled;
void __set_tls(struct CosmoTib *) libcesque;
struct CosmoTib *__get_tls_rax(void) dontthrow pureconst;
-#if defined(__x86_64__) && (SupportsWindows() || SupportsXnu())
+#if defined(__x86_64__)
#define __get_tls_privileged() __get_tls_rax()
#else
#define __get_tls_privileged() __get_tls()
@@ -87,12 +87,8 @@ struct CosmoTib *__get_tls_rax(void) dontthrow pureconst;
* This can't be used in privileged functions.
*/
forceinline pureconst struct CosmoTib *__get_tls(void) {
-#ifdef __chibicc__
+#ifdef __x86_64__
return __get_tls_rax();
-#elif __x86_64__
- struct CosmoTib *__tib;
- __asm__("movq\t%%fs:0, %0" : "=r"(__tib));
- return __tib;
#elif defined(__aarch64__)
register struct CosmoTib *__tls __asm__("x28");
return __tls - 1;
I did not open a PR because there may be a better fix for this. However, if the above patch is fine, just say so, and I will open a PR with it.
Version
Nightly version of Cosmopolitan built from commit https://github.com/jart/cosmopolitan/commit/9ba2b50805cae693631c15aed8576c8f6c6f6faa
What operating system are you seeing the problem on?
Linux
Source: jart/cosmopolitan