Running Factorio with musl libc
Factorio: Space Age has recently been released, and I have been meaning to start a new run with my friends on a dedicated server. It was meant to run on my NAS which runs Alpine Linux, a small and simple distribution. Factorio has a headless client, which contains the entire game logic without the assets. This is great for servers, but upon running the client, I was greeted with the following errors:
neuromancer:~/factorio$ ./bin/x64/factorio
Error relocating /home/zsolti/factorio/bin/x64/factorio: __res_nquery: symbol not found
Error relocating /home/zsolti/factorio/bin/x64/factorio: __dn_expand: symbol not found
Error relocating /home/zsolti/factorio/bin/x64/factorio: pthread_cond_clockwait: symbol not found
The game apparently relies on some functions which musl does not implement.
The Alpine Linux wiki
suggests installing gcompat,
a compatibility layer which provides bare minimum implementations for many non-standard glibc additions.
Unfortunately, it does not implement res_nquery
, dn_expand
or pthread_cond_clockwait
.
Let’s try to resolve this, as I really want to build yet another spaghetti factory which my friends and I will abandon after 10 or so hours.
dn_expand
So I began reading the sources of musl
and gcompat
. Apparently musl does implement
dn_expand
. The following test
code works as expected too:
#include <resolv.h>
#include <stdio.h>
void main(void)
{
printf("dn_expand: %p\n", (void *)dn_expand);
}
neuromancer:~$ gcc foo.c -lresolv -o foo
neuromancer:~$ ./foo
dn_expand: 0x7f1ec57644f8
After some digging around I’ve found that musl does not export the original symbol __dn_expand
.
Here is what glibc on my Artix box exports:
~ % strings /usr/lib/libc.so.6 | grep dn_expand
__libc_dn_expand
__dn_expand
And here is what is exported by musl on Alpine:
neuromancer:~$ strings /lib/ld-musl-x86_64.so.1 | grep dn_expand
dn_expand
On a second glance, musl doesn’t seem to export the internal symbols for anything, which makes perfect sense, after all, they are internal. You can read some more about the topic in this SO thread.
We should also take into account that the game actually links to libresolv.so.2
as seen here:
neuromancer:~/factorio$ ldd ./bin/x64/factorio
[...]
libresolv.so.2 => /lib/libresolv.so.2 (0x7f767f216000)
[...]
Error relocating ./bin/x64/factorio: __res_nquery: symbol not found
Error relocating ./bin/x64/factorio: __dn_expand: symbol not found
Error relocating ./bin/x64/factorio: pthread_cond_clockwait: symbol not found
Which, if you have it installed, is provided by gcompat
, so I suppose that’s the best place to export it,
via a thunk function just like __res_search
right above it:
libgcompat/resolv.c | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/libgcompat/resolv.c b/libgcompat/resolv.c
index 0b81d0c..67a2ce6 100644
--- a/libgcompat/resolv.c
+++ b/libgcompat/resolv.c
@@ -46,3 +46,9 @@ int __res_search(const char *dname, int class, int type, unsigned char *answer,
{
return res_search(dname, class, type, answer, anslen);
}
+
+int __dn_expand(const unsigned char *msg, const unsigned char *eomorig,
+ const unsigned char *comp_dn, char *exp_dn, int length)
+{
+ return dn_expand(msg, eomorig, comp_dn, exp_dn, length);
+}
--
2.47.0
res_nquery
gcompat uses a clever hack to implement res_ninit
,
and we can use something similar to reuse the non-reentrant but already implemented res_query
:
libgcompat/resolv.c | 24 ++++++++++++++++++++++++
1 file changed, 24 insertions(+)
diff --git a/libgcompat/resolv.c b/libgcompat/resolv.c
index b449b0d..ef58275 100644
--- a/libgcompat/resolv.c
+++ b/libgcompat/resolv.c
@@ -9,6 +9,7 @@
#include <resolv.h> /* res_state */
#include <stddef.h> /* NULL */
#include <string.h> /* memcpy, memset */
+#include <stdlib.h>
#include "alias.h" /* weak_alias */
@@ -48,3 +49,26 @@ int __res_search(const char *dname, int class, int type, unsigned char *answer,
}
weak_alias(dn_expand, __dn_expand);
+
+int __res_nquery(res_state statep, const char *dname, int class, int type,
+ unsigned char *answer, int anslen)
+{
+ int rc;
+ res_state tmp = malloc(sizeof(*statep));
+ if (!statep || statep == &_res) {
+ free(tmp);
+ return -1;
+ }
+
+ /* save ctx to tmp */
+ memcpy(tmp, &_res, sizeof(_res));
+ /* use the provided context */
+ memcpy(&_res, statep, sizeof(*statep));
+ rc = res_query(dname, class, type, answer, anslen);
+ /* restore everything */
+ memcpy(statep, &_res, sizeof(_res));
+ memcpy(&_res, tmp, sizeof(*tmp));
+ free(tmp);
+ return rc;
+}
+weak_alias(__res_nquery, res_nquery);
--
2.47.0
We simply swap the global context _res
to the provided one statep
, call the non-reentrant version,
and restore things to their original place. I did not bother to read the implementation, I don’t actually know
if res_query
can modify the state, so the memcpy(statep, &_res, sizeof(_res))
line could be unnecessary.
res_state
is an incomplete type, the actual definition of the struct is internal and mustn’t be relied upon.
Thankfully we can still use its size by calling sizeof
on a dereferenced pointer (I am not actually sure
how standard this is, but gcompat
seems to use it and it links just fine).
You may think this implementation is prone to race conditions, and you’d be right! The non-reentrant versions of the resolver API use a global context anyway, so I hope Factorio doesn’t use both APIs at the same time.
After grabbing the latest gcompat
commit and applying my patches, we are down to a single blocking error:
neuromancer:~/factorio$ ./bin/x64/factorio
Error relocating /home/zsolti/factorio/bin/x64/factorio: pthread_cond_clockwait: symbol not found
pthread_cond_clockwait
This one’s virtually impossible to semi-correctly implement without patching musl itself,
as much of the threading code we’d need to touch is internal to the actual libc on the system. According to
the POSIX standard,
pthread_cond_clockwait
is just like pthread_cond_timedwait
(which musl does implement!), except it lets you
specify a clock different than CLOCK_REALTIME
. The main reason to use it (that I can think of) is
using CLOCK_MONOTONIC
, so time increases monotonically. Fun fact, I did find a
patch to implement this in musl,
it might even get merged one day!
In the meantime, though…
libgcompat/pthread.c | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/libgcompat/pthread.c b/libgcompat/pthread.c
index ddc74ee..d221f21 100644
--- a/libgcompat/pthread.c
+++ b/libgcompat/pthread.c
@@ -96,3 +96,11 @@ int pthread_mutexattr_setkind_np(pthread_mutexattr_t *attr, int kind)
return pthread_mutexattr_settype(attr, kind);
}
+
+int pthread_cond_clockwait(pthread_cond_t *restrict cond,
+ pthread_mutex_t *restrict mutex,
+ clockid_t clock_id,
+ const struct timespec *restrict abstime)
+{
+ return pthread_cond_timedwait(cond, mutex, abstime);
+}
--
2.47.0
I’m sorry, I know, this can go terribly wrong. I never checked which clock Factorio uses, and I don’t take responsibility for any corrupt save files.
However, it works!
The most ideal solution would of course be if the developers made the game compatible with musl
libc, however, I couldn’t think of an alternative to pthread_cond_clockwait
off the top of my head.
I’m also not sure what exactly the DNS related code in the game is for, but maybe getaddrinfo
can
serve the same purpose.
On the other hand, musl-based distributions are already pretty much impossible to game on, so the extra effort is most likely better spent on developing everyone’s favourite factory builder.
It did crash once, and I will definitely need to test it a lot, but it was a fun afternoon hack which finally got me writing code again. I of course uploaded my branch to Github. Until later!