Reviving Life is Strange: Before the Storm on Modern Linux with a glibc Shim



Life is Strange: Before the Storm shipped with native Linux support back in 2017. That was a different era - glibc 2.26 was current, and some developers made the unfortunate choice of linking against internal, undocumented glibc symbols. Fast forward to 2026, and the game refuses to start on modern distributions like Fedora 43. The symbols it depends on no longer exist.

The fix is a small shim library that brings back the missing functions. After implementing it, I played through the entire game without issues.

LiS-BTS

The Problem: Missing glibc Internals

When you try to launch the game on a modern system, it fails silently or crashes immediately. Running it from the terminal reveals the culprit:

./LifeIsStrange: symbol lookup error: ./LifeIsStrange: undefined symbol: __libc_dlopen_mode, version GLIBC_PRIVATE

The game’s binary was linked against __libc_dlopen_mode and __libc_dlsym - internal glibc functions that were never part of the public API. These symbols lived in a special version namespace called GLIBC_PRIVATE, which is exactly what it sounds like: private implementation details that upstream reserves the right to change at will.

In glibc 2.34, the dynamic loader was merged into libc proper, and these internal symbols were restructured. The functions still exist internally, but they’re no longer exported with the same symbol names and versions that older binaries expect.

The Solution: A Compatibility Shim

Since the internal functions are gone, we provide replacements. The game doesn’t actually need the internal implementation - it just needs something that loads shared libraries. The standard POSIX functions dlopen and dlsym do exactly that.

The trick is making our replacement functions appear with the exact symbol names and versions the game expects. GCC’s __symver__ attribute handles this:

#include <dlfcn.h>

__attribute__((__symver__("__libc_dlopen_mode@GLIBC_PRIVATE")))
void* libc_dlopen_mode(const char *filename, int flags)
{
    return dlopen(filename, flags);
}

__attribute__((__symver__("__libc_dlsym@GLIBC_PRIVATE")))
void *libc_dlsym(void *restrict handle, const char *restrict symbol)
{
    return dlsym(handle, symbol);
}

The __symver__ attribute tells the linker to export these functions under specific versioned symbol names. When the game’s dynamic linker searches for __libc_dlopen_mode@GLIBC_PRIVATE, it finds our shim instead of the missing glibc symbol.

Building the Shim Library

Create a working directory and add three files:

libc_dlopen_mode.c:

#include <dlfcn.h>

__attribute__((__symver__("__libc_dlopen_mode@GLIBC_PRIVATE")))
void* libc_dlopen_mode(const char *filename, int flags)
{
    return dlopen(filename, flags);
}

__attribute__((__symver__("__libc_dlsym@GLIBC_PRIVATE")))
void *libc_dlsym(void *restrict handle, const char *restrict symbol)
{
    return dlsym(handle, symbol);
}

version.map:

GLIBC_PRIVATE {
global:
    *;
};

The version script defines our custom GLIBC_PRIVATE version tag. Without it, the linker wouldn’t know how to apply the version specified in __symver__.

Makefile:

.PHONY: all clean

RES_LIB_NAME    =   liblibc_dlopen_mode
RES_LIB         =   $(RES_LIB_NAME).so
SRCS            =   \
    libc_dlopen_mode.c

CFLAGS  +=  -fpic -shared -flto -Wl,--version-script -Wl,version.map -Wl,--as-needed

all: $(RES_LIB)

$(RES_LIB): $(SRCS) version.map
    $(CC) $(CFLAGS) -o $@ $(SRCS)

clean:
    $(RM) $(RES_LIB)

Build and install:

make
sudo cp liblibc_dlopen_mode.so /usr/local/lib/

You can verify the symbols are correctly versioned:

$ nm -D /usr/local/lib/liblibc_dlopen_mode.so
                 w __cxa_finalize@GLIBC_2.2.5
                 U dlopen@GLIBC_2.34
                 U dlsym@GLIBC_2.34
0000000000001100 T libc_dlopen_mode@GLIBC_PRIVATE
0000000000001120 T libc_dlsym@GLIBC_PRIVATE

The T entries show our exported functions with the correct GLIBC_PRIVATE version tag.

Configuring Steam

Open Steam, right-click on Life is Strange: Before the Storm, and select Properties. In the Launch Options field, add:

LD_PRELOAD=/usr/local/lib/liblibc_dlopen_mode.so %command%

If you’re using GameMode for performance optimization:

LD_PRELOAD=/usr/local/lib/liblibc_dlopen_mode.so gamemoderun %command%

The LD_PRELOAD environment variable tells the dynamic linker to load our shim library before any others. When the game binary starts and requests __libc_dlopen_mode@GLIBC_PRIVATE, the linker finds it in our preloaded library instead of failing with an undefined symbol error.

Why This Works

The game uses these internal functions to dynamically load additional libraries at runtime. The actual implementation details don’t matter to the game - it just needs a function that accepts a filename and flags, calls the kernel to map a shared object, and returns a handle. The public dlopen and dlsym functions do exactly this.

Our shim acts as a translation layer: the game calls what it thinks are internal glibc functions, and we redirect those calls to the stable public API. The game can’t tell the difference.

Other Affected Games

Life is Strange: Before the Storm isn’t alone. Several games from the 2015-2018 era made the same mistake of depending on glibc internals. If you encounter similar “undefined symbol” errors mentioning GLIBC_PRIVATE, this same technique should work - just verify which specific symbols are missing and add corresponding wrapper functions.

Caveats

A few things to keep in mind:

  • This is a workaround, not a proper fix. The correct solution would be for the game developers to rebuild against the public API. Unfortunately, many older Linux ports are abandoned.
  • The shim intercepts calls globally within the game process. This is fine for single-player games but be mindful when using LD_PRELOAD with any software that handles sensitive data.
  • Future glibc changes could theoretically break even the public dlopen/dlsym API, though this is extremely unlikely given POSIX compatibility requirements.

Conclusion

A few lines of C and a Steam launch option are all it takes to revive this abandoned Linux port. The game runs flawlessly - I completed all three episodes and the bonus Farewell episode without a single crash.

It’s a shame that native Linux ports from this era are becoming unplayable due to dependency rot, but at least the fix is straightforward. Sometimes the best compatibility layer is just a thin wrapper that pretends to be something it isn’t.


References