[Nix-dev] A proot alternative for user nix installations

Luca Bruno lethalman88 at gmail.com
Wed Mar 11 12:28:15 CET 2015


So I've played a little with linux unshare, and I was able to install
nix and use it without any problem.

How it works for the user:
1) Download nix binary tarball
2) Create an empty ROOTDIR
3) Run ./theprogrambelow ROOTDIR bash

Now you are in a shell with the whole original filesystem, except you
own the root of the filesystem now. Since ROOTDIR is owned by the user,
you can create /nix as user and install nix and do all the fancy things.

I suggest you to export NIX_CONF_DIR=/nix/etc/nix because of course you
cannot write to /etc/nix as user.

What the program does:

1) Unshare the user and mount namespace
2) Create and bind mount all /* directories under ROOTDIR, except /nix
(in case the system already has a nix installation)
3) Chroot to ROOTDIR
4) Execute a given command

This is the program to be compiled, I'd like some feedback on this so we
can create a nice wiki page.

#define _GNU_SOURCE
#include <sched.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <signal.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <limits.h>
#include <errno.h>
#include <sys/mount.h>
#include <sys/types.h>
#include <dirent.h>
#include <sys/stat.h>

#define err_exit(msg) { perror(msg); exit(EXIT_FAILURE); }

static void usage(char *pname) {
    fprintf(stderr, "Usage: %s <rootdir> <command>\n", pname);
   
    exit(EXIT_FAILURE);
}

static void update_map(char *mapping, char *map_file) {
    int fd;
       
    fd = open(map_file, O_WRONLY);
    if (fd < 0) {
        err_exit("map open");
    }

    int map_len = strlen(mapping);
    if (write(fd, mapping, map_len) != map_len) {
        err_exit("map write");
    }
   
    close(fd);
}

int main(int argc, char *argv[]) {
    char map_buf[1024];
    char path_buf[PATH_MAX];
    char path_buf2[PATH_MAX];
    char cwd[PATH_MAX];

    if (argc < 3) {
        usage(argv[0]);
    }
   
    char *rootdir = realpath(argv[1], NULL);
    if (!rootdir) {
        err_exit("realpath");
    }
   
    uid_t uid = getuid();
    gid_t gid = getgid();

    if (unshare (CLONE_NEWNS | CLONE_NEWUSER) < 0) {
        err_exit("unshare");
    }

    // bind mount all / stuff into rootdir
    DIR* d = opendir("/");
    if (!d) {
        err_exit("open /");
    }

    struct dirent *ent;
    while ((ent = readdir(d))) {
        // do not bind mount an existing nix installation
        if (!strcmp (ent->d_name, ".") || !strcmp (ent->d_name, "..") ||
!strcmp (ent->d_name, "nix")) {
            continue;
        }

        snprintf(path_buf, sizeof(path_buf), "/%s", ent->d_name);
       
        struct stat statbuf;
        if (lstat(path_buf, &statbuf) < 0) {
            fprintf(stderr, "Cannot stat %s: %s\n", path_buf,
strerror(errno));
            continue;
        }

        snprintf(path_buf2, sizeof(path_buf2), "%s/%s", rootdir,
ent->d_name);
       
        if (S_ISDIR(statbuf.st_mode)) {
            mkdir(path_buf2, statbuf.st_mode & ~S_IFMT);
            if (mount(path_buf, path_buf2, "none", MS_BIND | MS_REC,
NULL) < 0) {
                fprintf(stderr, "Cannot bind mount %s to %s: %s\n",
path_buf, path_buf2, strerror(errno));
            }
        }
    }

    // map the original uid/gid in the new ns
    snprintf(map_buf, sizeof(map_buf), "%d %d 1", uid, uid);
    update_map(map_buf, "/proc/self/uid_map");
   
    snprintf(map_buf, sizeof(map_buf), "%d %d 1", gid, gid);
    update_map(map_buf, "/proc/self/gid_map");

    if (!getcwd(cwd, PATH_MAX)) {
        err_exit("getcwd");
    }

    chdir("/");
    if (chroot (rootdir) < 0) {
        err_exit("chroot");
    }
    chdir(cwd);

    // execute the command
    execvp(argv[2], argv+2);
    err_exit("execvp");
}




More information about the nix-dev mailing list