===============
= ptrcnull.me =
===============
writeups and other stuff

Running Spotify on musl

When trying to run Spotify on Alpine, most answers on the internet pointed to Flatpak. And don’t get me wrong - Flatpak is great and allows for running glibc-based apps with minimal setup, but… I’d really like to get Spotify to run natively, mostly because containers are a bit heavier in terms of disk space and interoperability is still not perfect.

Let’s start by downloading Spotify’s deb package from the official repository and unpacking it into downloads. Most of the files are in usr/share/spotify, so we’ll try to just go there and run it.

$ cd usr/share/spotify
$ ./spotify
zsh: no such file or directory: ./spotify

Oh right, it’s missing some stuff. What stuff though?

$ ldd spotify
(114 lines skipped...)
Error relocating ./libcef.so: __libc_realloc: symbol not found
Error relocating ./libcef.so: __libc_free: symbol not found
Error relocating ./libcef.so: __fdelt_chk: symbol not found
Error relocating ./libcef.so: __fprintf_chk: symbol not found
Error relocating ./libcef.so: __sprintf_chk: symbol not found
Error relocating ./libcef.so: initstate_r: symbol not found
Error relocating ./libcef.so: random_r: symbol not found
Error relocating ./libcef.so: gnu_get_libc_version: symbol not found
Error relocating ./libcef.so: __register_atfork: symbol not found
Error relocating ./libcef.so: __longjmp_chk: symbol not found
Error relocating ./libcef.so: __libc_stack_end: symbol not found
Error relocating ./spotify: strtoll_l: symbol not found
Error relocating ./spotify: strtoull_l: symbol not found
Error relocating ./spotify: __isnanf: symbol not found
Error relocating ./spotify: __isinff: symbol not found
Error relocating ./spotify: __isnan: symbol not found
Error relocating ./spotify: __isinf: symbol not found
Error relocating ./spotify: __cxa_thread_atexit_impl: symbol not found

It seems that it uses a lot of glibc-specific stuff, but we can use Adelie Linux’s gcompat to make it work.

$ apk add gcompat
(1/3) Installing musl-obstack (1.2.2-r0)
(2/3) Installing libucontext (1.1-r0)
(3/3) Installing gcompat (1.0.0-r2)
OK: 11425 MiB in 1571 packages
$ patchelf --set-interpreter /lib/ld-musl-x86_64.so.1 spotify
$ patchelf --add-needed libgcompat.so.0 spotify
$ ldd spotify
(97 lines skipped...)
Error relocating ./libcef.so: __mbrlen: symbol not found
Error relocating ./libcef.so: __close: symbol not found
Error relocating ./libcef.so: initstate_r: symbol not found
Error relocating ./libcef.so: random_r: symbol not found

That’s much better, but there are still a few things missing. random_r was added a few months ago, but __mbrlen and __close were still missing, so I cloned gcompat and added them in.

Note: gcompat seems to be temporarily no longer maintained as A. Wilcox has retired, so I pushed my changed to a fork on GitHub instead of creating a merge request to the original repo.

After recompiling gcompat and trying to run Spotify again, it still complains about missing glibc:

$ ./spotify
Error loading shared library ld-linux-x86-64.so.2: No such file or directory (needed by libcef.so)

Fortunately, another patchelf, this time to libcef.so, makes the error go away:

$ patchelf --remove-needed ld-linux-x86-64.so.2 libcef.so
$ ./spotify

Aaand… it crashes with boost::filesystem::read_symlink: No such file or directory: "/proc/self/exe". That’s weird, /proc/self/exe should be present, read_symlink just calls readlink, and readlink uses the syscall, right?

Nope, gcompat replaces readlink with its own function to intercept /proc/self/exe (ugh), but as we don’t use the loader, we can remove it from the Makefile and build again.

Another recompile of gcompat later, maybe now it would launch?

$ ./spotify
[1]    26559 segmentation fault (core dumped)  ./spotify

Whoops, it doesn’t look good… After running gdb spotify core, it looks like it segfaults in savestate_r () from /lib/libgcompat.so.0. We could try to fix this or we can replace it with a fair dice roll:

diff --git a/libgcompat/random_r.c b/libgcompat/random_r.c
index 5bc7053..f6e7071 100644
--- a/libgcompat/random_r.c
+++ b/libgcompat/random_r.c
@@ -141,4 +141,7 @@ int random_r(struct random_data *restrict buf, int32_t *restrict result) {
        }
 
+       *result = 4;
+       return 0;
+
        if (buf->n == 0) {
                k = buf->x[0] = lcg31(buf->x[0]);

(and also add return 0; before first savestate_r in initstate_r)

Aaand… it shows a window!

Empty white window on gray background, with title Spotify

Sadly, after a while, the enthusiasm goes away - the window stays white and doesn’t display any content. Command line argument --show-console doesn’t give us any helpful info either:

$ ./spotify --show-console
14:54:32.302 I [f:156  ] Access allowance changed from online: 0 stream: 0, sync: 0, persistent conn: 0 to online: 1 stream: 1, sync: 0, persistent conn: 1
14:54:32.334 I [f:77   ] Connectivity policy is initially allow_all
14:54:32.334 I [f:79   ] Connection type is initially unknown
14:54:32.334 I [f:96   ] Enabling all persistent connections
14:54:32.353 I [f:150  ] D-Bus name 'org.freedesktop.NetworkManager' does not exist on system bus

Okay, but A. Wilcox had the same issue - maybe we can use that?

$ ldd spotify
	/lib/ld-musl-x86_64.so.1 (0x7f0b5dbf5000)
	libgcompat.so.0 => /lib/libgcompat.so.0 (0x7f0b5dbe1000)
	libm.so.6 => /lib/ld-musl-x86_64.so.1 (0x7f0b5dbf5000)
	libdl.so.2 => /lib/ld-musl-x86_64.so.1 (0x7f0b5dbf5000)
	libasound.so.2 => /usr/lib/libasound.so.2 (0x7f0b5daf6000)
	libatomic.so.1 => /usr/lib/libatomic.so.1 (0x7f0b5daed000)
	libcurl-gnutls.so.4 => /usr/lib/libcurl-gnutls.so.4 (0x7f0b5da73000)
	libcef.so => ./libcef.so (0x7f0b52887000)
(86 lines skipped...)

It seems to match (libm/libdl before libcef), so let’s try removing them and see what happens:

$ patchelf --remove-needed libm.so.6 libcef.so
$ patchelf --remove-needed libdl.so.2 libcef.so
$ ./spotify --show-console
15:02:04.445 I [f:156  ] Access allowance changed from online: 0 stream: 0, sync: 0, persistent conn: 0 to online: 1 stream: 1, sync: 0, persistent conn: 1
15:02:04.491 I [f:77   ] Connectivity policy is initially allow_all
15:02:04.491 I [f:79   ] Connection type is initially unknown
15:02:04.492 I [f:96   ] Enabling all persistent connections
15:02:04.500 I [f:150  ] D-Bus name 'org.freedesktop.NetworkManager' does not exist on system bus

Unfortunately, it still shows the white window and strace reveals that it still segfaults:

28567 +++ killed by SIGSEGV (core dumped) +++
28566 +++ killed by SIGSEGV (core dumped) +++
28565 +++ killed by SIGSEGV (core dumped) +++
28564 +++ killed by SIGSEGV (core dumped) +++
28563 +++ killed by SIGSEGV (core dumped) +++

This is where the post ends, at least for now (2021-11-19).

If you manage to make it further than I did, please DM me on Telegram or send an e-mail.

Update (2021-12-03)

Thanks to an email I got from Filip, I was finally able to make it work!

It turned out that the missing piece of the puzzle was --single-process - it somehow forces CEF into submission and while it’s not perfect (segfault after closing the window), it launches successfully and even plays music (tested on PulseAudio and PipeWire).

Spotify window, with song “Oh No!” by “Marina” shown paused, partially obscured by terminal window with command “./spotify –single-process”