Regression: Segfault on Linux x86_64 with `cosmo_dlopen` when calling foreign function

Author: HeavenVolkoffCreated Feb 11, 2026Updated Mar 16, 2026
Labelscritical severity

Contact Details

[email protected]

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
c
#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:

bash
$> cosmocc -o sdl2_test.com sdl2_test.c

Execution:

Requires libSDL2 installed on host.

bash
$> ./sdl2_test.com
Segmentation fault         (core dumped) ./sd_test.com

sdl2_test.com.trace

Proposed 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:

Image Click to expand Patch content
diff
--- 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