From e42df368840ce96a4d561b4fec4d4ea07c30dbb6 Mon Sep 17 00:00:00 2001 From: Evgeny Zinoviev Date: Sun, 29 Nov 2020 20:16:36 +0300 Subject: change name to voidnsrun, add command-line arguments, rewrite readme --- Makefile | 4 +- README.md | 119 ++++++++++++++++++++++++++++++----------- glibcrun.c | 101 ----------------------------------- voidnsrun.c | 174 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 265 insertions(+), 133 deletions(-) delete mode 100644 glibcrun.c create mode 100644 voidnsrun.c diff --git a/Makefile b/Makefile index 4e8c506..050d4ef 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,13 @@ CC := gcc -PROGRAM = glibcrun +PROGRAM = voidnsrun CFLAGS = -O2 -std=c99 -Wall -W LDFLAGS = INSTALL = /usr/bin/env install PREFIX = /usr/local -OBJS = glibcrun.o +OBJS = voidnsrun.o all: $(PROGRAM) diff --git a/README.md b/README.md index 4e136d7..6363c9c 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,117 @@ -# glibcrun +# voidnsrun -`glibcrun` is utility for launching glibc linked binaries in isolated namespaces -in musl-libc Void Linux installations. +`voidnsrun` is utility for launching programs in an isolated namespace with +alternative `/usr` tree. Its primary goal is to run glibc-linked binaries in +musl-libc Void Linux environments or vice-versa. -It creates new private mount namespace for the running process, "replacing" `/usr` -and `/var/db/xbps` with directories from your glibc basedir using bind -mounts, and launches your glibc program. +It creates a new private mount namespace, transparently substitutes `/usr` and +some other directories with directories from your alternative root using bind +mounts, and launches your program. -## Creating glibc chroot +## Installation -I will use `/glibc` directory name for an example, you can use any other path you -want. +Just clone the repo, and then: ``` -# mkdir /glibc -# XBPS_ARCH=x86_64 xbps-install --repository=http://alpha.de.repo.voidlinux.org/current -r /glibc -S base-voidstrap +$ make +$ sudo make install ``` -When it's done you may want to chroot into it, e.g. to install some dependencies -for your glibc software. +Note that installed binary must be owned by root and have suid bit. `make install` +should handle it. -## Installing glibcrun +## Usage -Just clone the repo, and then: +``` +voidnsrun [OPTIONS] [PROGRAM [ARGS]] + +Options: + -h: print this help + -m : add bind mount + -r : altroot path. If this option is not present, + VOIDNSRUN_DIR environment variable is used. + -v: print version +``` + +`voidnsrun` needs to know the path to your alternative root directory and it can +read it from the `VOIDNSRUN_DIR` environment variable or you can use `-r` +argument to specify it. + +You may want to add something like this to your `~/.bashrc` or similar script: ``` -$ make -$ sudo make install +export VOIDNSRUN_DIR=/glibc ``` -Note that installed binary must be owned as root and have suid bit. `make install` -should handle it, but anyway. +By default, `voidnsrun` binds these paths from alternative root to the new +namespace: +- `/usr` +- `/var/db/xbps` +- `/etc/xbps.d` -## Usage +If you want to mount something else, use `-m` argument. + +## Example -`glibcrun` needs to know the path to your glibc base directory and it reads it from -the `GLIBCRUN_DIR` environment variable. You may want to add something like this -to your `~/.bashrc` or similar script: +Let's imagine you want to use some proprietary glibc-linked app on your +musl-libc Void Linux box. Let it be Vivaldi browser for the example. You +unpacked it to `/opt/vivaldi` and it doesn't work, obviously. +First, you need to perform an alternative glibc base system installation to a +separate new directory: ``` -export GLIBCRUN_DIR=/glibc +# mkdir /glibc +# XBPS_ARCH=x86_64 xbps-install --repository=http://alpha.de.repo.voidlinux.org/current -r /glibc -S base-voidstrap +``` + +Export path to this installation for `voidnsrun`: +``` +export VOIDNSRUN_DIR=/glibc ``` -When `glibcrun` is run without arguments it will attempt to launch a shell from your -`SHELL` variable, otherwise it will treat the first argument as a path to an executable -and the rest as a list of arguments. +Try launching your app: +``` +voidnsrun /opt/vivaldi/vivaldi +``` -Example: +It won't work just yet: +``` +/opt/vivaldi/vivaldi: error while loading shared libraries: libgobject-2.0.so.0: cannot open shared object file: No such file or directory +``` + +Now you need to install all its dependencies into your glibc installation. Use +`xlocate` from `xtools` package to find a package responsible for a file (or +just guess it): +``` +$ xlocate libgobject-2.0.so.0 +Signal-Desktop-1.38.1_1 /usr/lib/signal-desktop/resources/app.asar.unpacked/node_modules/sharp/vendor/lib/libgobject-2.0.so -> /usr/lib/signal-desktop/resources/app.asar.unpacked/node_modules/sharp/vendor/lib/libgobject-2.0.so.0.5600.4 +Signal-Desktop-1.38.1_1 /usr/lib/signal-desktop/resources/app.asar.unpacked/node_modules/sharp/vendor/lib/libgobject-2.0.so.0 -> /usr/lib/signal-desktop/resources/app.asar.unpacked/node_modules/sharp/vendor/lib/libgobject-2.0.so.0.5600.4 +Signal-Desktop-1.38.1_1 /usr/lib/signal-desktop/resources/app.asar.unpacked/node_modules/sharp/vendor/lib/libgobject-2.0.so.0.5600.4 +glib-2.66.2_1 /usr/lib/libgobject-2.0.so.0 -> /usr/lib/libgobject-2.0.so.0.6600.2 +glib-2.66.2_1 /usr/lib/libgobject-2.0.so.0.6600.2 +glib-devel-2.66.2_1 /usr/share/gdb/auto-load/usr/lib/libgobject-2.0.so.0.6600.2-gdb.py +libglib-devel-2.66.2_1 /usr/lib/libgobject-2.0.so -> /usr/lib/libgobject-2.0.so.0 +``` + +Sync repos and install `glib`. You can use `voidnsrun` for this purpose too. +Also, I think you should bind `/etc` and `/var` while using `xbps-install` via +`voidnsrun`, to not mess with your main system files. +``` +$ sudo voidnsrun -r /glibc -m /etc -m /var xbps-install -Su +$ sudo voidnsrun -r /glibc -m /etc -m /var xbps-install glib +``` +Try launching vivaldi again: ``` -glibcrun /opt/palemoon/palemoon -ProfileManager +$ voidnsrun /opt/vivaldi/vivaldi +/opt/vivaldi/vivaldi: error while loading shared libraries: libnss3.so: cannot open shared object file: No such file or directory ``` -will launch `/opt/palemoon/palemoon -ProfileManager`. +As you can see, it no longer complains about missing `libgobject-2.0.so.0`, now +it's `libnss3.so`. Repeat steps above for all missing dependencies, and in the +end, it will work. (If it's not, then something's still missing. In particular, +make sure to install fonts related packages: `xorg-fonts`, `freetype`, +`fontconfig`, `libXft`.) ## License diff --git a/glibcrun.c b/glibcrun.c deleted file mode 100644 index efedba1..0000000 --- a/glibcrun.c +++ /dev/null @@ -1,101 +0,0 @@ -/** - * Copyright (c) 2020 Evgeny Zinoviev . - * - * This program is licensed under the BSD 2-Clause License. - * https://opensource.org/licenses/BSD-2-Clause - */ - -#define _GNU_SOURCE -#include -#include -#include -#include -#include -#include -#include -#include -#include - -const char *var_name = "GLIBCRUN_DIR"; - -#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0])) -#define ERROR(f_, ...) fprintf(stderr, (f_), ##__VA_ARGS__) - -int main(int argc, char *argv[]) { - int result; - - /* Get glibc base path */ - const char *dir = getenv(var_name); - if (!dir) { - ERROR("error: environment variable %s not found.\n", var_name); - return 1; - } - - /* Validate it */ - struct stat st; - result = stat(dir, &st); - if (result != 0 || !S_ISDIR(st.st_mode)) { - ERROR("error: %s is not a directory.\n", dir); - return 1; - } - - /* Get shell from current env to reuse it if needed */ - const char *shell = getenv("SHELL"); - if (!shell) - shell = "/bin/sh"; - - /* Do the unshare magic */ - result = unshare(CLONE_NEWNS); - if (result == -1) { - ERROR("unshare: %s\n", strerror(errno)); - return 1; - } - - /* Mount glibc stuff to our private namespace */ - const char *const mountpoints[] = {"/usr", "/var/db/xbps"}; - char buf[PATH_MAX]; - - for (int i = 0; i < (int)ARRAY_SIZE(mountpoints); i++) { - strcpy(buf, dir); - strcat(buf, mountpoints[i]); - result = mount(buf, mountpoints[i], NULL, MS_BIND|MS_REC, NULL); - if (result == -1) { - ERROR("mount(%s): %s\n", mountpoints[i], strerror(errno)); - return 1; - } - } - - /* Drop root */ - uid_t uid = getuid(); - gid_t gid = getgid(); - - result = setreuid(uid, uid); - if (result == -1) { - ERROR("setreuid: %s\n", strerror(errno)); - return 1; - } - - result = setregid(gid, gid); - if (result == -1) { - ERROR("setregid: %s\n", strerror(errno)); - return 1; - } - - /* Launch program or shell */ - const char *exec_cmd; - char *const *exec_args = NULL; - if (argc < 2) - exec_cmd = shell; - else { - exec_cmd = argv[1]; - exec_args = (char *const *)argv+1; - } - - result = execvp(exec_cmd, exec_args); - if (result == -1) { - ERROR("execvp(%s): %s\n", exec_cmd, strerror(errno)); - return 1; - } - - return 0; -} diff --git a/voidnsrun.c b/voidnsrun.c new file mode 100644 index 0000000..e121c0f --- /dev/null +++ b/voidnsrun.c @@ -0,0 +1,174 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +const char *var_name = "VOIDNSRUN_DIR"; +const char *prog_version = "1.0"; + +#define USERMOUNTS_MAX 8 + +#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0])) +#define ERROR(f_, ...) fprintf(stderr, (f_), ##__VA_ARGS__) + +bool isdir(const char *s) +{ + struct stat st; + int result = stat(s, &st); + if (result == -1) + ERROR("stat: %s\n", strerror(errno)); + return result == 0 && S_ISDIR(st.st_mode); +} + +void usage(const char *progname) +{ + printf("Usage: %s [OPTIONS] [PROGRAM [ARGS]]\n", progname); + printf("\n" + "Options:\n" + " -h: print this help\n" + " -m : add bind mount\n" + " -r : altroot path. If this option is not present,\n" + " %s environment variable is used.\n" + " -v: print version\n", + var_name); +} + +bool mount_list( + const char *dirptr, + size_t dirlen, + const char **mountpoints, + size_t len, + bool ignore_missing) +{ + char buf[PATH_MAX]; + for (size_t i = 0; i < len; i++) { + /* Check if it's safe to proceed. */ + if (dirlen + strlen(mountpoints[i]) >= PATH_MAX) { + ERROR("error: path %s%s is too large.\n", dirptr, mountpoints[i]); + return false; + } + strcpy(buf, dirptr); + strcat(buf, mountpoints[i]); + if (!isdir(buf)) { + if (!ignore_missing) { + ERROR("error: source mount dir %s does not exists.\n", buf); + return false; + } else + continue; + } + if (!isdir(mountpoints[i])) { + ERROR("error: mountpoint %s does not exists.\n", mountpoints[i]); + return false; + } + if (mount(buf, mountpoints[i], NULL, MS_BIND|MS_REC, NULL) == -1) { + ERROR("mount: failed to mount %s: %s\n", + mountpoints[i], strerror(errno)); + return false; + } + } + return true; +} + +int main(int argc, char **argv) +{ + if (argc < 2) { + usage(argv[0]); + return 0; + } + + int result; + char *dir = NULL; + size_t dirlen; + const char *usermounts[USERMOUNTS_MAX] = {0}; + int usermounts_count = 0; + int c; + while ((c = getopt(argc, argv, "vhm:r:")) != -1) { + switch (c) { + case 'v': + printf("%s\n", prog_version); + return 0; + case 'h': + usage(argv[0]); + return 0; + case 'r': + dir = optarg; + break; + case 'm': + if (usermounts_count < USERMOUNTS_MAX) { + usermounts[usermounts_count++] = optarg; + break; + } else { + ERROR("error: only up to %d user mounts allowed.\n", + USERMOUNTS_MAX); + return 1; + } + case '?': + return 1; + } + } + + /* Get alternative root dir. */ + if (!dir) + dir = getenv(var_name); + if (!dir) { + ERROR("error: environment variable %s not found.\n", var_name); + return 1; + } + + /* Validate it. */ + if (!isdir(dir)) { + ERROR("error: %s is not a directory.\n", dir); + return 1; + } + + dirlen = strlen(dir); + + /* Do the unshare magic. */ + result = unshare(CLONE_NEWNS); + if (result == -1) { + ERROR("unshare: %s\n", strerror(errno)); + return 1; + } + + /* Mount stuff from altroot to our private namespace. */ + const char *mountpoints[] = {"/usr", "/var/db/xbps", "/etc/xbps.d"}; + + if (!mount_list(dir, dirlen, mountpoints, ARRAY_SIZE(mountpoints), true)) + return 1; + + if (usermounts_count > 0 && + !mount_list(dir, dirlen, usermounts, usermounts_count, false)) + return 1; + + /* Drop root. */ + uid_t uid = getuid(); + gid_t gid = getgid(); + + result = setreuid(uid, uid); + if (result == -1) { + ERROR("setreuid: %s\n", strerror(errno)); + return 1; + } + + result = setregid(gid, gid); + if (result == -1) { + ERROR("setregid: %s\n", strerror(errno)); + return 1; + } + + /* Launch program. */ + result = execvp(argv[optind], (char *const *)argv+optind); + if (result == -1) { + ERROR("execvp(%s): %s\n", argv[optind], strerror(errno)); + return 1; + } + + return 0; +} -- cgit v1.2.3