Running glibc applications on musl libc Void Linux installation

TLDR: use voidnsrun

Some time ago I got tired of Gentoo and decided to try Void Linux. They officially support musl libc, so I thought why not? and installed it on my main working machine.

Everything was great, I was (and I am) really impressed by Void and I doubt I will go back to Gentoo anytime soon. But there was one little thing I haven't thought of at the time of installation: proprietary software.

Proprietary software for Linux is almost always glibc-linked and nobody provides builds for alternative libc implementations such as musl. I wanted to run at least Vivaldi browser and JetBrains IDEs (PhpStorm, CLion, PyCharm, Android Studio etc.) so I started looking for solutions. (Of course I could just go back and do a glibc reinstall, but where's fun in that?)

Void documentation suggests to perform a glibc base system installation via base-bootstrap to a separate directory (I'll be referring to it as the glibc container), chroot to it and run glibc binaries from within this chroot. Using chroot seemed too primitive and inconvenient, even if I wrote scripts to automate all steps, so I kept looking.

Then I found this blog post discussing another way, using mount_namespaces(7) as an elegant alternative to chroot. The idea is simple: create new mount namespace, bind-mount the /usr from the glibc container at your real /usr, thus substituting your real /usr with "fake" /usr from the glibc container where all binaries and libraries are glibc-linked, and launch your glibc program inside this mount namespace. The mounts will be invisible for the rest of the system. Cool, isn't it?

The code in the blog post wasn't anywhere "production-ready", though, so I decided to write a new ready-to-use program based on this idea.

First attempt: writing voidnsrun

To use mount namespaces, one needs root privileges. It's possible to use user namespaces instead, but they break setuid binaries (or maybe I just don't know how to prepare them properly), so I decided to stick to classic old mount namespaces. To be able to use them, you either need to be root or the binary must be owned by root and have setuid bit. We'll do the latter, but remember that setuid in general is a security risk and one have to be very careful when writing setuid programs.

Disclaimer: all code in this article is only to demonstrate the ideas. In real programs you need to check return values of almost all function calls and do a lot of other stuff.

The program is supposed to be used like this:

$ voidnsrun glibc_program --arguments ...

where voidnsrun is the name of the program we're going to write, glibc_program is the name of our glibc program and --arguments ... are its arguments.

I'll omit all boring stuff like arguments processing logic, user input validation and so on, if you're interested in that, read the code of the final program. So let's get straight to the task.

First, we need to create new mount namespace, or in other words "unshare" the current namespace of the process and change to the private copy of this namespace. Use unshare(2) for that:

unshare(CLONE_NEWNS);

Then bind-mount your glibc container's (which, I'll assume for simplicity, resides in /glibc) /usr (this won't be visible outside the new namespace):

mount("/glibc/usr", "/usr", NULL, MS_BIND|MS_REC, NULL);

(Note that you should also bind-mount /etc and /var when you use xbps utilities such as xbps-install.)

As we don't need root anymore, drop it:

uid_t uid = getuid();
gid_t gid = getgid();
setreuid(uid, uid);
setregid(gid, gid);

And finally, launch our program:

execvp(argv[1], (char *const *)argv+1);

Surprisingly, it works. It launches your glibc program and, like Steve Jobs used to say, it just works.

But... try to launch some complex software this way, specifically software that launches another software. You'll notice that something's not right.

Let's take JetBrains PhpStorm as an example:

  • it has built-in terminal;
  • it can run your host's php;
  • it can open its help and documentation in a browser;
  • it can use fonts installed to /usr/share/fonts;
  • and so on...

You got it already? The PhpStorm exists in its own private mount namespace with substituted /usr, so every child process it creates will inherit this namespace. When you open built-in terminal, it will work, but actually it will be glibc-linked /glibc/usr/bin/bash from the bind-mounted /usr. All stuff you have installed in your root /usr will be missing because you haven't installed them in the glibc container. For example. you don't have a duplicate of firefox there? PhpStorm won't be able to open a browser.

What to do then? One "solution" would be to duplicate all programs you need in the glibc container, but it's a mess and not a real solution if you ask me.

Even weirder stuff happens in CLion. When you compile a program, it produces glibc binaries, because it runs glibc compiler, glibc linker, etc... You can launch and test the compiled program from built-in terminal in the IDE, but not outside it, because you would have to rebuild it with musl first. Oh well.

So I started thinking and eventually came up with an idea. What happens if you put together mount namespaces, bind mounts, forks and unix sockets?

Second attempt: writing voidnsundo

Let's focus on the PhpStorm example:

  • we want browser to work, and it must be the same browser we use on our musl system;
  • we want built-in terminal to run the original (or "native") shell from our musl system;
  • we don't want to duplicate all our fonts in /glibc.

We've learned how a process can create its own private mount namespace. All children of this process will inherit that namespace.

But there is also setns(2) system call that allows the calling process (actually, thread, but we're writing single-threaded programs here, so let's not complicate it) to change the namespace. It accepts a file descriptor and a namespace type.

So it seems we need to learn how to launch processes in the original (or "parent") namespace from within the glibc container's namespace. If PhpStorm could just launch /usr/bin/firefox in the right mount namespace, that would solve our problem.

Let's write a program which we'll call voidnsundo that will do the opposite to what voidnsrun does: it will change to the original namespace and launch specified command. voidnsundo is supposed to be used from within the glibc container, so it needs to be glibc-linked. You might need to install make and gcc to the glibc container:

$ sudo voidnsrun xbps-install make gcc

But to use the setns(2), we must have a file descriptor that refers to target namespace, in our case it's the namespace voidnsrun was in before calling unshare(2). We need to improve voidnsrun before we can write voidnsundo.

Improving voidnsrun

A process'es (is that right?) mount namespace file descriptor can be retrieved by open(2)-ing /proc/<pid>/ns/mnt pseudo-file (/proc/self/ns/mnt for the current process). So voidnsrun can get this file descriptor before unshare(2)-ing:

int nsfd = open("/proc/self/ns/mnt", O_RDONLY);

But how can another program access it in future?

The answer is unix sockets.

It's possible to send and receive file descriptors over unix sockets. I don't want to copypaste the whole fd-sending-receiving functions, you can see them here. For the article, I'll just declare them like so:

int send_fd(int sock, int fd);
int recv_fd(int sock);

So voidnsrun will get the fd, unshare, then it will create a tiny tmpfs filesystem that'll only be visible in the unshared namespace:

mount("tmpfs", "/run/voidnsrun", "tmpfs", 0, "size=4k,mode=0700,uid=0,gid=0");

The tmpfs at /run/voidnsrun will be used to store a socket file. A user may launch multiple voidnsrun instances, and this trick will make possible for each instance's socket to have the same /run/voidnsrun/sock path, but in each namespace it'll refer to a different socket.

Then it forks:

pid_t ppid_before_fork = getpid();
pid_t pid = fork();

In the child process, it will start a unix socket server. It'll just send nsfd to every client connecting to the socket. The server should stop and this child process should die when the parent dies, so take care of that too.

if (pid == 0) {
    /* Catch SIGTERM: it will be sent here when parent dies. The signal will
     * interrupt the accept() call, so we can clean up and exit immediately. */
    struct sigaction sa = {0};
    sa.sa_handler = onterm;
    sigaction(SIGTERM, &sa, NULL);

    /* Ignore SIGINT. Otherwise it will be affected by Ctrl+C in the parent
     * process. */
    signal(SIGINT, SIG_IGN);

    /* Set the child to get SIGTERM when parent thread dies. */
    prctl(PR_SET_PDEATHSIG, SIGTERM);

    /* Maybe it already has died? */
    if (getppid() != ppid_before_fork) {
        // exit logic here
    }

    /* Create unix socket. */
    int sock_fd = socket(AF_UNIX, SOCK_STREAM, 0);

    struct sockaddr_un sock_addr = {0};
    sock_addr.sun_family = AF_UNIX;

    strcpy(sock_addr.sun_path, "/run/voidnsrun/sock");

    bind(sock_fd, (struct sockaddr *)&sock_addr, sizeof(sock_addr));
    listen(sock_fd, 1);

    /* Accept incoming connections until SIGTERM. */
    while (!term_caught) {
        sock_conn = accept(sock_fd, NULL, 0);
        if (sock_conn == -1)
            continue;
        send_fd(sock_conn, nsfd);
    }
}
volatile sig_atomic_t term_caught = 0;
void onterm(int sig)
{
    term_caught = 1;
}

In the parent process, as before, drop root and launch program:

if (pid != 0) {
    uid_t uid = getuid();
    gid_t gid = getgid();
    setreuid(uid, uid);
    setregid(gid, gid);

    execvp(argv[1], (char *const *)argv+1);
}

Now we're ready to write the "undo" program.

Writing voidnsundo

Get file descriptor:

int sockfd, nsfd;

sockfd = socket(AF_UNIX, SOCK_STREAM, 0);

struct sockaddr_un sock_addr = {0};
sock_addr.sun_family  = AF_UNIX;
strcpy(sock_addr.sun_path, "/run/voidnsrun/sock");

connect(sockfd, (struct sockaddr *)&sock_addr, sizeof(sock_addr));
nsfd = recv_fd(sockfd);

Change mount namespace:

setns(nsfd, CLONE_NEWNS);

Drop root and launch program:

uid_t uid = getuid();
gid_t gid = getgid();
setreuid(uid, uid);
setregid(gid, gid);

execvp(argv[1], (char *const *)argv+1);

As you should remember voidnsundo is supposed to be used from within the glibc container, so it has to be built with glibc tools. You can use voidnsrun for that: just enter the glibc container with voidnsrun /bin/bash and compile it as usually.

It's time for some demo:

As you can see, we successfully enter private namespace with voidnsrun and then enter the original one with voidnsundo.

One more thing

Okay so now we can launch programs in the parent namespace from within glibc container. But how does it help us with PhpStorm being unable to launch a browser because there's no /usr/bin/firefox in the glibc container or built-in terminal in the IDE being not musl native? Bind-mounts, that's how!

In voidnsrun, after the unshare(2) call, we bind mount our voidnsundo binary to /usr/bin/bash and /usr/bin/firefox. (If there's no /usr/bin/firefox file then create an empty file there prior to mounting.) As you understand these bind mounts are only visible in the unshared namespace.

bool exists(const char *s);
bool mkfile(const char *s);
char *s = "/usr/bin/firefox";

if (!exists(s)) 
    mkfile(s);

mount("/usr/local/bin/voidnsundo", s, NULL, MS_BIND, NULL);

// same for /usr/bin/bash or other targets

In voidnsundo, add a check if it's been launched as voidnsundo:

char realpath_buf[PATH_MAX];
bool binded = strcmp(basename(argv[0]), "voidnsundo") != 0;
if (binded) {
    int bytes = readlink("/proc/self/exe", realpath_buf, PATH_MAX);
    realpath_buf[bytes] = '\0';
}

When executing a program, add a check too: if it's "binded", or in other words has not been launched as voidnsundo but as some other program, launch this program:

if (binded)
    argv[0] = realpath_buf;
execvp(argv[0], (char *const *)argv);

This trick will make voidnsundo, bind-mounted to /usr/bin/firefox, launch /usr/bin/firefox in the original namespace. Same with shell. Pure magic!

Preserving some /usr subdirectories

Okay, we've got this far and we've fixed browser and shell in PhpStorm. But what about fonts? We substitute the whole /usr when creating a mount namespace for glibc container to launch our app, and fonts are usually installed to /usr/share/fonts. Of course you could just duplicate them to the glibc container installation, even by using sudo voidnsrun xbps-install <font-package> for that, but that's no fun. We need the whole /usr to be the glibc /usr with an exception of share/fonts subdirectory. How can we achieve that?

It seems bind-mounts work here too and linux does not prohibit such weird things.

In voidnsrun, after unsharing but before /usr mounting, create tmpfs at /oldroot and bind-mount the real /usr to /oldroot/usr:

// it's convenient to use tmpfs here because it will be destroyed when the
// last process will exit the namespace, along with all files and mounts inside it

mount("tmpfs", "/oldroot", "tmpfs", 0, "size=4k,mode=0700,uid=0,gid=0");

struct stat st;
stat("/usr", &st);
mkdir("/oldroot/usr", st.st_mode);

mount("/usr", "/oldroot/usr", NULL, MS_BIND|MS_REC, NULL);

Then, after mounting /glibc/usr to /usr, bind-mount some directories back from oldroot:

mount("/oldroot/usr/share/fonts", "/usr/share/fonts", NULL, MS_BIND|MS_REC, NULL);

Ha-ha, that works!

The End

You can download the ready to use program here: https://github.com/gch1p/voidnsrun

It supports some runtime configuration and you can specify with arguments what you want to mount and where. I use it daily and it seems to be pretty stable. Of course there can be bugs, and if you find any, contact me.

If you have any comments, contact me by email.
powered by OpenBSD
© ch1p 2021