@@ -0,0 +1,307 @@
1+/*
2+ * This file is based on @lethalman's nix-user-chroot. This file has
3+ * diverged from it though.
4+ *
5+ * Usage: nix-user-chroot <nixpath> <command>
6+ */
7+
8+#include <sched.h>
9+#include <unistd.h>
10+#include <stdlib.h>
11+#include <sys/wait.h>
12+#include <signal.h>
13+#include <fcntl.h>
14+#include <stdio.h>
15+#include <string.h>
16+#include <limits.h>
17+#include <errno.h>
18+#include <sys/mount.h>
19+#include <sys/types.h>
20+#include <dirent.h>
21+#include <sys/stat.h>
22+#include <stdint.h>
23+#include <list>
24+#include <string>
25+
26+using namespace std;
27+
28+#define err_exit(format, ...) { fprintf(stderr, format ": %s\n", ##__VA_ARGS__, strerror(errno)); exit(EXIT_FAILURE); }
29+static int child_proc(const char *rootdir, const char *nixdir, uint8_t clear_env, list<struct DirMapping> dirMappings, list<struct SetEnv> envMappings, const char *executable, char * const new_argv[]);
30+
31+volatile uint8_t child_died = 0;
32+int child_pid = 0;
33+
34+static void usage(const char *pname) {
35+ fprintf(stderr, "Usage: %s -n <nixpath> -- <command>\n", pname);
36+ fprintf(stderr, "\t-c\tclear all env vars\n");
37+ fprintf(stderr, "\t-m <src>:<src>\tmap src on the host to dest in the sandbox\n");
38+ fprintf(stderr, "\t-d\tdelete all default dir mappings, may break things\n");
39+ fprintf(stderr, "\t-p <var>\tpreserve the value of a variable across the -c clear\n");
40+ fprintf(stderr, "\t-e\tadd an /escape-hatch to the sandbox, and run (outside the sandbox) any strings written to it\n");
41+
42+ exit(EXIT_FAILURE);
43+}
44+
45+static void update_map(const char *mapping, const char *map_file) {
46+ int fd;
47+
48+ fd = open(map_file, O_WRONLY);
49+ if (fd < 0) {
50+ err_exit("map open");
51+ }
52+
53+ int map_len = strlen(mapping);
54+ if (write(fd, mapping, map_len) != map_len) {
55+ err_exit("map write");
56+ }
57+
58+ close(fd);
59+}
60+
61+static void add_path(string src, string dest, string rootdir) {
62+ string path_buf2;
63+
64+ struct stat statbuf;
65+ if (stat(src.c_str(), &statbuf) < 0) {
66+ fprintf(stderr, "Cannot stat %s: %s\n", src.c_str(), strerror(errno));
67+ return;
68+ }
69+
70+ path_buf2 = rootdir + "/" + dest;
71+
72+ if (S_ISDIR(statbuf.st_mode)) {
73+ mkdir(path_buf2.c_str(), statbuf.st_mode & ~S_IFMT);
74+ if (mount(src.c_str(), path_buf2.c_str(), "none", MS_BIND | MS_REC, NULL) < 0) {
75+ fprintf(stderr, "Cannot bind mount %s to %s: %s\n", src.c_str(), path_buf2.c_str(), strerror(errno));
76+ }
77+ } else if (S_ISREG(statbuf.st_mode)) {
78+ printf("bind-mounting file %s not supported", src.c_str());
79+ }
80+}
81+
82+struct DirMapping {
83+ string src;
84+ string dest;
85+};
86+
87+struct SetEnv {
88+ string key;
89+ string value;
90+};
91+
92+struct DirMapping parseMapping(string input) {
93+ auto pos = input.find(":");
94+ string src = input.substr(0, pos);
95+ string dest = input.substr(pos + 1);
96+ return (struct DirMapping){ src, dest };
97+}
98+
99+static void handle_child_death(int signo, siginfo_t *info, void *context) {
100+ if ( (child_pid == 0) || (info->si_pid == child_pid) ) {
101+ child_died = 1;
102+ }
103+}
104+
105+int main(int argc, char *argv[]) {
106+ uint8_t clear_env = 0;
107+ uint8_t enable_escape_hatch = 0;
108+ char *nixdir = NULL;
109+ list<struct DirMapping> dirMappings;
110+ list<struct SetEnv> envMappings;
111+ const char *t;
112+
113+#define x(y) dirMappings.push_back({ "/" y, y })
114+ x("dev");
115+ x("proc");
116+ x("sys");
117+ x("run");
118+ x("tmp");
119+ x("var");
120+ x("etc");
121+ x("usr");
122+ x("home");
123+ x("root");
124+#undef x
125+
126+ int opt;
127+ while ((opt = getopt(argc, argv, "cen:m:dp:")) != -1) {
128+ switch (opt) {
129+ case 'c':
130+ clear_env = 1;
131+ break;
132+ case 'e':
133+ enable_escape_hatch = 1;
134+ break;
135+ case 'n':
136+ // determine absolute directory for nix dir
137+ nixdir = realpath(optarg, NULL);
138+ if (!nixdir) {
139+ err_exit("realpath(%s)", optarg);
140+ }
141+ break;
142+ case 'm':
143+ dirMappings.push_back(parseMapping(optarg));
144+ break;
145+ case 'd':
146+ dirMappings.clear();
147+ break;
148+ case 'p':
149+ t = getenv(optarg);
150+ if (t) {
151+ envMappings.push_back({ optarg, t });
152+ }
153+ break;
154+ }
155+ }
156+
157+ if (!nixdir) {
158+ fprintf(stderr, "-n <nixdir> is required\n");
159+ exit(EXIT_FAILURE);
160+ }
161+
162+ if (argc <= optind) {
163+ usage(argv[0]);
164+ }
165+
166+ const char *tmpdir = getenv("TMPDIR");
167+ if (!tmpdir) {
168+ tmpdir = "/tmp";
169+ }
170+
171+ char template_[PATH_MAX];
172+ int needed = snprintf(template_, PATH_MAX, "%s/nixXXXXXX", tmpdir);
173+ if (needed < 0) {
174+ err_exit("TMPDIR too long: '%s'", tmpdir);
175+ }
176+
177+ char *rootdir = mkdtemp(template_);
178+ if (!rootdir) {
179+ err_exit("mkdtemp(%s)", template_);
180+ }
181+
182+ int unrace[2];
183+
184+ if (pipe(unrace)) {
185+ err_exit("pipe()");
186+ }
187+
188+ struct sigaction handle_child;
189+ handle_child.sa_sigaction = handle_child_death;
190+ handle_child.sa_flags = SA_SIGINFO;
191+
192+ struct sigaction old_handler;
193+ if (sigaction(SIGCHLD, &handle_child, &old_handler)) {
194+ err_exit("sigaction()");
195+ }
196+
197+ int child;
198+ child = child_pid = fork();
199+ if (child < 0) {
200+ err_exit("fork()");
201+ } else if (child == 0) {
202+ sigaction(SIGCHLD, &old_handler, NULL);
203+ close(unrace[1]);
204+ char buf[10];
205+ read(unrace[0], buf, 10);
206+ close(unrace[0]);
207+ return child_proc(rootdir, nixdir, clear_env, dirMappings, envMappings, argv[optind], argv + optind);
208+ } else {
209+ close(unrace[0]);
210+ char fifopath[PATH_MAX];
211+ if (enable_escape_hatch) {
212+ snprintf(fifopath, PATH_MAX, "%s/escape-hatch", rootdir);
213+ mkfifo(fifopath, 0600);
214+ }
215+ close(unrace[1]);
216+ if (enable_escape_hatch) {
217+ char buffer[1024];
218+ while (!child_died) {
219+ int fd = open(fifopath, O_RDONLY);
220+ if (fd < 0) {
221+ if (errno == EINTR) continue;
222+ fprintf(stderr, "error opening escape-hatch: %s\n", strerror(errno));
223+ continue;
224+ }
225+ int size = read(fd, buffer, 1024);
226+ buffer[size] = 0;
227+ system(buffer);
228+ close(fd);
229+ }
230+ }
231+ int status;
232+ int ret = waitpid(child, &status, 0);
233+ return WEXITSTATUS(status);
234+ }
235+}
236+
237+static int child_proc(const char *rootdir, const char *nixdir, uint8_t clear_env, list<struct DirMapping> dirMappings, list<struct SetEnv> envMappings, const char *executable, char * const new_argv[]) {
238+ // get uid, gid before going to new namespace
239+ uid_t uid = getuid();
240+ gid_t gid = getgid();
241+
242+ // "unshare" into new namespace
243+ if (unshare(CLONE_NEWNS | CLONE_NEWUSER) < 0) {
244+ if (errno == EPERM) {
245+ fputs("Run the following to enable unprivileged namespace use:\nsudo bash -c \"sysctl -w kernel.unprivileged_userns_clone=1 ; echo kernel.unprivileged_userns_clone=1 > /etc/sysctl.d/nix-user-chroot.conf\"\n\n", stderr);
246+ exit(EXIT_FAILURE);
247+ } else {
248+ err_exit("unshare()");
249+ }
250+ }
251+
252+ // add necessary system stuff to rootdir namespace
253+ for (list<struct DirMapping>::iterator it = dirMappings.begin();
254+ it != dirMappings.end(); ++it) {
255+ struct DirMapping m = *it;
256+ add_path(m.src, m.dest, rootdir);
257+ }
258+
259+ // make sure nixdir exists
260+ struct stat statbuf2;
261+ if (stat(nixdir, &statbuf2) < 0) {
262+ err_exit("stat(%s)", nixdir);
263+ }
264+
265+ char path_buf[PATH_MAX];
266+ // mount /nix to new namespace
267+ snprintf(path_buf, sizeof(path_buf), "%s/nix", rootdir);
268+ mkdir(path_buf, statbuf2.st_mode & ~S_IFMT);
269+ if (mount(nixdir, path_buf, "none", MS_BIND | MS_REC, NULL) < 0) {
270+ err_exit("mount(%s, %s)", nixdir, path_buf);
271+ }
272+
273+ // fixes issue #1 where writing to /proc/self/gid_map fails
274+ // see user_namespaces(7) for more documentation
275+ int fd_setgroups = open("/proc/self/setgroups", O_WRONLY);
276+ if (fd_setgroups > 0) {
277+ write(fd_setgroups, "deny", 4);
278+ close(fd_setgroups);
279+ }
280+
281+ // map the original uid/gid in the new ns
282+ char map_buf[1024];
283+ snprintf(map_buf, sizeof(map_buf), "%d %d 1", uid, uid);
284+ update_map(map_buf, "/proc/self/uid_map");
285+ snprintf(map_buf, sizeof(map_buf), "%d %d 1", gid, gid);
286+ update_map(map_buf, "/proc/self/gid_map");
287+
288+ // chroot to rootdir
289+ if (chroot(rootdir) < 0) {
290+ err_exit("chroot(%s)", rootdir);
291+ }
292+
293+ chdir("/");
294+
295+ if (clear_env) clearenv();
296+ setenv("PATH", ENV_PATH, 1);
297+
298+ for (list<struct SetEnv>::iterator it = envMappings.begin();
299+ it != envMappings.end(); ++it) {
300+ struct SetEnv e = *it;
301+ setenv(e.key.c_str(), e.value.c_str(), 1);
302+ }
303+
304+ // execute the command
305+ execvp(executable, new_argv);
306+ err_exit("execvp(%s)", executable);
307+}