summaryrefslogtreecommitdiff
path: root/nix/libutil/spawn.cc
diff options
context:
space:
mode:
authorReepca Russelstein <reepca@russelstein.xyz>2025-04-17 23:32:03 -0500
committerJohn Kehayias <john.kehayias@protonmail.com>2025-06-24 10:07:56 -0400
commitbe8aca065118aa4485c02f991c51bea89034defa (patch)
tree79331f4087c66b62b47aa98100d57cb43e604163 /nix/libutil/spawn.cc
parent7173c2c0cad8afc9d8d1ad26f345b5a04f47716a (diff)
daemon: add and use spawn.cc and spawn.hh.
This adds a mechanism for manipulating and running "spawn phases" similarly to how builder-side code manipulates "build phases". The main difference is that spawn phases take a (reference to a) single structure that they can both read from and write to, with their writes being visible to subsequent phases. The base structure type for this is SpawnContext. It also adds some predefined phase sequences, namely basicSpawnPhases and cloneSpawnPhases, and exposes each of the actions performed by these phases. Finally, it modifies build.cc to replace runChild() with use of this new code. * nix/libutil/util.cc (keepOnExec, waitForMessage): new functions. * nix/libutil.util.hh (keepOnExec, waitForMessage): add prototypes. * nix/libutil/spawn.cc, nix/libutil/spawn.hh: new files. (addPhaseAfter, addPhaseBefore, prependPhase, appendPhase, deletePhase, replacePhase, reset_writeToStderrAction, restoreAffinityAction, setsidAction, earlyIOSetupAction, dropAmbientCapabilitiesAction, chrootAction, chdirAction, closeMostFDsAction, setPersonalityAction, oomSacrificeAction, setIDsAction, restoreSIGPIPEAction, setupSuccessAction, execAction, getBasicSpawnPhases, usernsInitSyncAction, usernsSetIDsAction, initLoopbackAction, setHostAndDomainAction, makeFilesystemsPrivateAction, makeChrootSeparateFilesystemAction, statfsToMountFlags, bindMount, mountIntoChroot, mountIntoChrootAction, mountProcAction, mountDevshmAction, mountDevptsAction, pivotRootAction, lockMountsAction, getCloneSpawnPhases, runChildSetup, runChildSetupEntry, cloneChild, idMapToIdentityMap, unshareAndInitUserns): new procedures. * nix/local.mk (libutil_a_SOURCES): add spawn.cc. (libutil_headers): add spawn.hh. * nix/libstore/build.cc (restoreSIGPIPE, DerivationGoal::runChild, childEntry): removed procedures. (DerivationGoal::{dirsInChroot,env,readiness}): removed. (execBuilderOrBuiltin, execBuilderOrBuiltinAction, clearRootWritePermsAction): new procedures. (DerivationGoal::startBuilder): modified to use a CloneSpawnContext if chroot builds are available, otherwise a SpawnContext. Change-Id: Ifd50110de077378ee151502eda62b99973d083bf Change-Id: I76e10d3f928cc30566e1e6ca79077196972349f8 spawn.cc, util.cc, util.hh changes Change-Id: I287320e63197cb4f65665ee5b3fdb3a0e125ebac Signed-off-by: John Kehayias <john.kehayias@protonmail.com>
Diffstat (limited to 'nix/libutil/spawn.cc')
-rw-r--r--nix/libutil/spawn.cc829
1 files changed, 829 insertions, 0 deletions
diff --git a/nix/libutil/spawn.cc b/nix/libutil/spawn.cc
new file mode 100644
index 00000000000..93bab9f59e4
--- /dev/null
+++ b/nix/libutil/spawn.cc
@@ -0,0 +1,829 @@
1/* GNU Guix --- Functional package management for GNU
2 Copyright (C) 2025 Caleb Ristvedt <reepca@russelstein.xyz>
3
4 This file is part of GNU Guix.
5
6 GNU Guix is free software; you can redistribute it and/or modify it
7 under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 3 of the License, or (at
9 your option) any later version.
10
11 GNU Guix is distributed in the hope that it will be useful, but
12 WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. */
18
19/* Process spawning and setup code. */
20
21#include <spawn.hh>
22#include <util.hh>
23#include <affinity.hh>
24#include <stddef.h>
25#include <unistd.h>
26#include <grp.h>
27#include <limits.h>
28#include <sys/wait.h>
29#include <cstring>
30#include <cstdlib>
31
32#if HAVE_SYS_MOUNT_H
33#include <sys/mount.h>
34#endif
35
36#if HAVE_SCHED_H
37#include <sched.h>
38#endif
39
40#if HAVE_STATVFS
41#include <sys/statvfs.h>
42#endif
43
44#if HAVE_SYS_SYSCALL_H
45#include <sys/syscall.h>
46#endif
47
48#if HAVE_SYS_PRCTL_H
49#include <sys/prctl.h>
50#endif
51
52#ifdef __linux__
53#include <sys/personality.h>
54#endif
55
56#if defined(SYS_pivot_root)
57#define pivot_root(new_root, put_old) (syscall(SYS_pivot_root, new_root,put_old))
58#endif
59
60
61#define CLONE_ENABLED defined(CLONE_NEWNS)
62
63#if CLONE_ENABLED
64#include <sys/ioctl.h>
65#include <net/if.h>
66#include <netinet/in.h>
67#endif
68
69namespace nix {
70
71
72void addPhaseAfter(Phases & phases, string afterLabel, string addLabel, Action addAction)
73{
74 for(auto i = phases.begin(); i != phases.end(); i++)
75 if((*i).label == afterLabel) {
76 i++; /* std::vector::insert inserts before, not after */
77 Phase p;
78 p.label = addLabel;
79 p.action = addAction;
80 phases.insert(i, p);
81 return;
82 }
83 throw Error(format("label `%1%' not found in phases") % afterLabel);
84}
85
86
87void addPhaseBefore(Phases & phases, string beforeLabel, string addLabel, Action addAction)
88{
89 for(auto i = phases.begin(); i != phases.end(); i++)
90 if((*i).label == beforeLabel) {
91 Phase p;
92 p.label = addLabel;
93 p.action = addAction;
94 phases.insert(i, p);
95 return;
96 }
97 throw Error(format("label `%1%' not found in phases") % beforeLabel);
98}
99
100
101void prependPhase(Phases & phases, string addLabel, Action addAction)
102{
103 Phase p;
104 p.label = addLabel;
105 p.action = addAction;
106 phases.insert(phases.begin(), p);
107}
108
109
110void appendPhase(Phases & phases, string addLabel, Action addAction)
111{
112 Phase p;
113 p.label = addLabel;
114 p.action = addAction;
115 phases.push_back(p);
116}
117
118
119void deletePhase(Phases & phases, string delLabel)
120{
121 for(auto i = phases.begin(); i != phases.end(); i++)
122 if((*i).label == delLabel) {
123 phases.erase(i);
124 return;
125 }
126 throw Error(format("label `%1%' not found in phases") % delLabel);
127}
128
129
130void replacePhase(Phases & phases, string replaceLabel, Action newAction)
131{
132 for(auto i = phases.begin(); i != phases.end(); i++)
133 if((*i).label == replaceLabel) {
134 (*i).action = newAction;
135 return;
136 }
137 throw Error(format("label `%1' not found in phases") % replaceLabel);
138}
139
140
141/* A curated selection of predefined actions */
142
143void reset_writeToStderrAction(SpawnContext & ctx)
144{
145 _writeToStderr = 0;
146}
147
148
149void restoreAffinityAction(SpawnContext & ctx)
150{
151 restoreAffinity();
152}
153
154
155void setsidAction(SpawnContext & ctx)
156{
157 /* Puts the current process in a separate session, which implies a
158 separate process group, so it doesn't receive group-directed signals
159 sent at the parent. The new session initially has no controlling
160 terminal, so it also doesn't receive terminal signals and can't open
161 /dev/tty. */
162 if(ctx.setsid && setsid() == (pid_t)-1)
163 throw SysError("creating a new session");
164}
165
166
167void earlyIOSetupAction(SpawnContext & ctx)
168{
169 for(auto i = ctx.earlyCloseFDs.begin(); i != ctx.earlyCloseFDs.end(); i++)
170 if(close(*i) == -1)
171 throw SysError("closing fd");
172
173 if(ctx.logFD != -1) {
174 if(dup2(ctx.logFD, STDOUT_FILENO) == -1)
175 throw SysError("cannot dup2 log fd into stdout fd");
176 if(dup2(ctx.logFD, STDERR_FILENO) == -1)
177 throw SysError("cannot dup2 log fd into stderr fd");
178 }
179
180 if(ctx.setStdin) {
181 if(ctx.stdinFD != -1) {
182 if(dup2(ctx.stdinFD, STDIN_FILENO) == -1)
183 throw SysError("cannot dup2 fd into stdin fd");
184 }
185 else {
186 /* Doesn't make sense for it to be writable, but compatibility... */
187 AutoCloseFD fd = open(ctx.stdinFile.c_str(), O_RDWR);
188 if(fd == -1)
189 throw SysError(format("cannot open `%1%'") % ctx.stdinFile);
190 if(dup2(fd, STDIN_FILENO) == -1)
191 throw SysError("cannot dup2 fd into stdin fd");
192 }
193 }
194}
195
196
197void dropAmbientCapabilitiesAction(SpawnContext & ctx)
198{
199 /* Drop ambient capabilities such as CAP_CHOWN that might have been granted
200 when starting guix-daemon. */
201 if(ctx.dropAmbientCapabilities)
202#if HAVE_SYS_PRCTL_H
203 prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0);
204#else
205 throw Error("dropping ambient capabilities is not supported on this system");
206#endif
207}
208
209
210void chrootAction(SpawnContext & ctx)
211{
212 if(ctx.doChroot)
213#if HAVE_CHROOT
214 if(chroot(ctx.chrootRootDir.c_str()) == -1)
215 throw SysError(format("cannot change root directory to '%1%'") % ctx.chrootRootDir);
216#else
217 throw Error("chroot is not supported on this system");
218#endif
219}
220
221
222void chdirAction(SpawnContext & ctx)
223{
224 if(ctx.setcwd)
225 if(chdir(ctx.cwd.c_str()) == -1)
226 throw SysError(format("changing into `%1%'") % ctx.cwd);
227}
228
229
230void closeMostFDsAction(SpawnContext & ctx)
231{
232 if(ctx.closeMostFDs) closeMostFDs(ctx.preserveFDs);
233 for(auto i = ctx.preserveFDs.begin(); i != ctx.preserveFDs.end(); i++)
234 keepOnExec(*i);
235}
236
237
238void setPersonalityAction(SpawnContext & ctx)
239{
240 if(ctx.setPersona)
241#ifdef __linux__
242 if(personality(ctx.persona) == -1)
243 throw SysError("cannot set personality");
244#else
245 throw Error("setting the personality is not supported on this system");
246#endif
247}
248
249
250void oomSacrificeAction(SpawnContext & ctx)
251{
252#ifdef __linux__
253 if(ctx.oomSacrifice)
254 /* Ask the kernel to eagerly kill us & our children if it runs out of
255 memory, regardless of blame, to preserve ‘real’ user data &
256 state. */
257 try {
258 writeFile("/proc/self/oom_score_adj", "1000"); // 100%
259 } catch(...) { ignoreException(); }
260#endif
261}
262
263
264void setIDsAction(SpawnContext & ctx)
265{
266 if(ctx.setSupplementaryGroups)
267 if(setgroups(ctx.supplementaryGroups.size(),
268 ctx.supplementaryGroups.data()) == -1)
269 throw SysError("cannot set supplementary groups");
270
271 if(ctx.setgid)
272 if(setgid(ctx.group) == -1 ||
273 getgid() != ctx.group ||
274 getegid() != ctx.group)
275 throw SysError("setgid failed");
276
277 if(ctx.setuid)
278 if(setuid(ctx.user) == -1 ||
279 getuid() != ctx.user ||
280 geteuid() != ctx.user)
281 throw SysError("setuid failed");
282}
283
284
285void restoreSIGPIPEAction(SpawnContext & ctx)
286{
287 /* Restore default handling of SIGPIPE, otherwise some programs will
288 randomly say "Broken pipe". */
289 struct sigaction act, oact;
290 act.sa_handler = SIG_DFL;
291 act.sa_flags = 0;
292 sigemptyset(&act.sa_mask);
293 if (sigaction(SIGPIPE, &act, &oact)) throw SysError("resetting SIGPIPE");
294}
295
296
297void setupSuccessAction(SpawnContext & ctx)
298{
299 if(ctx.signalSetupSuccess)
300 writeFull(STDERR_FILENO, "\n");
301}
302
303
304void execAction(SpawnContext & ctx)
305{
306 Strings envStrs;
307 std::vector<char *> envPtrs;
308 char **env;
309 if(ctx.inheritEnv) {
310 for(auto i = ctx.env.begin(); i != ctx.env.end(); i++)
311 if(setenv(i->first.c_str(), i->second.c_str(), 1) == -1)
312 throw SysError("setenv");
313 env = environ;
314 } else {
315 for(auto i = ctx.env.begin(); i != ctx.env.end(); i++)
316 envStrs.push_back(i->first + "=" + i->second);
317 /* Need to keep the envPtrs vector alive as long as its .data()! */
318 envPtrs = stringsToCharPtrs(envStrs);
319 env = envPtrs.data();
320 }
321 if(execvpe(ctx.program.c_str(), stringsToCharPtrs(ctx.args).data(), env) == -1)
322 throw SysError(format("executing `%1%'") % ctx.program);
323}
324
325
326Phases getBasicSpawnPhases()
327{
328 return { { "reset_writeToStderr", reset_writeToStderrAction },
329 { "restoreAffinity", restoreAffinityAction },
330 { "setsid", setsidAction },
331 { "earlyIOSetup", earlyIOSetupAction },
332 { "dropAmbientCapabilities", dropAmbientCapabilitiesAction },
333 { "chroot", chrootAction },
334 { "chdir", chdirAction },
335 { "closeMostFDs", closeMostFDsAction },
336 { "setPersonality", setPersonalityAction },
337 { "oomSacrifice", oomSacrificeAction },
338 { "setIDs", setIDsAction },
339 { "restoreSIGPIPE", restoreSIGPIPEAction },
340 { "setupSuccess", setupSuccessAction },
341 { "exec", execAction } };
342}
343
344
345void usernsInitSyncAction(SpawnContext & sctx)
346{
347#if CLONE_ENABLED
348 CloneSpawnContext & ctx = (CloneSpawnContext &) sctx;
349 if((ctx.cloneFlags & CLONE_NEWUSER) != 0) {
350 /* Close the earlyCloseFDs before we try reading anything */
351 for(auto i = ctx.earlyCloseFDs.begin(); i != ctx.earlyCloseFDs.end(); i++)
352 if(close(*i) == -1)
353 throw SysError("closing fd");
354 /* Don't try closing them again later */
355 ctx.earlyCloseFDs.clear();
356 /* Wait for the parent process to initialize the UID/GID mapping of
357 our user namespace. */
358 waitForMessage(ctx.setupFD, "go\n");
359 }
360#endif
361}
362
363
364void usernsSetIDsAction(SpawnContext & sctx)
365{
366#if CLONE_ENABLED
367 CloneSpawnContext & ctx = (CloneSpawnContext &) sctx;
368 if((ctx.cloneFlags & CLONE_NEWUSER) != 0) {
369 /* Note: 'man capabilities' says that a transition from zero to
370 nonzero uids causes capabilities to be lost, but doesn't say what
371 happens when a transition from an unmapped (possibly zero) uid to a
372 nonzero uid happens. */
373 if(ctx.usernsSetuid)
374 /* Since we presumably have CAP_SETUID, this sets the real,
375 effective, saved, and filesystem uids */
376 if(setuid(ctx.usernsUser) != 0)
377 throw SysError("setuid");
378 if(ctx.usernsSetgid)
379 /* Ditto but with gids */
380 if(setgid(ctx.usernsGroup) != 0)
381 throw SysError("setgid");
382 }
383#endif
384}
385
386
387void initLoopbackAction(SpawnContext & sctx)
388{
389#if CLONE_ENABLED
390 CloneSpawnContext & ctx = (CloneSpawnContext &) sctx;
391 if(((ctx.cloneFlags & CLONE_NEWNET) != 0) && ctx.initLoopback) {
392 AutoCloseFD fd(socket(PF_INET, SOCK_DGRAM, IPPROTO_IP));
393 if (fd == -1) throw SysError("cannot open IP socket");
394
395 struct ifreq ifr;
396 strcpy(ifr.ifr_name, "lo");
397 ifr.ifr_flags = IFF_UP | IFF_LOOPBACK | IFF_RUNNING;
398 if (ioctl(fd, SIOCSIFFLAGS, &ifr) == -1)
399 throw SysError("cannot set loopback interface flags");
400
401 fd.close();
402 }
403#endif
404}
405
406
407void setHostAndDomainAction(SpawnContext & sctx)
408{
409#if CLONE_ENABLED
410 CloneSpawnContext & ctx = (CloneSpawnContext &) sctx;
411 if((ctx.cloneFlags & CLONE_NEWUTS) != 0) {
412 if (sethostname(ctx.hostname.c_str(),
413 strlen(ctx.hostname.c_str())) == -1)
414 throw SysError("cannot set host name");
415 if (setdomainname(ctx.domainname.c_str(),
416 strlen(ctx.domainname.c_str())) == -1)
417 throw SysError("cannot set domain name");
418 }
419#endif
420}
421
422
423void makeFilesystemsPrivateAction(SpawnContext & sctx)
424{
425#if CLONE_ENABLED && HAVE_SYS_MOUNT_H && defined(MS_REC) && defined(MS_PRIVATE)
426 CloneSpawnContext & ctx = (CloneSpawnContext &) sctx;
427 if((ctx.cloneFlags & CLONE_NEWNS) != 0) {
428 if(mount(0, "/", 0, MS_REC|MS_PRIVATE, 0) == -1)
429 throw SysError("unable to make `/' private mount");
430 }
431#endif
432}
433
434
435void makeChrootSeparateFilesystemAction(SpawnContext & sctx)
436{
437#if CLONE_ENABLED && HAVE_SYS_MOUNT_H && defined(MS_BIND)
438 CloneSpawnContext & ctx = (CloneSpawnContext &) sctx;
439 if(((ctx.cloneFlags & CLONE_NEWNS) != 0) && ctx.doChroot) {
440 /* Bind-mount chroot directory to itself, to treat it as a different
441 filesystem from /, as needed for pivot_root. Alternatively, mount
442 a tmpfs on it. */
443 if(ctx.mountTmpfsOnChroot) {
444 if(mount("none", ctx.chrootRootDir.c_str(), "tmpfs", 0, 0) == -1)
445 throw SysError(format("unable to mount tmpfs on `%1%'") % ctx.chrootRootDir);
446 }
447 else {
448 if(mount(ctx.chrootRootDir.c_str(), ctx.chrootRootDir.c_str(), 0, MS_BIND, 0) == -1)
449 throw SysError(format("unable to bind mount ‘%1%’") % ctx.chrootRootDir);
450 }
451 }
452#endif
453}
454
455
456static int statfsToMountFlags(int f_flags)
457{
458#if HAVE_SYS_MOUNT_H && HAVE_STATVFS
459 int ret = 0;
460#if defined(ST_RDONLY) && defined(MS_RDONLY)
461 if((f_flags & ST_RDONLY) != 0) ret |= MS_RDONLY;
462#endif
463#if defined(ST_NOSUID) && defined(MS_NOSUID)
464 if((f_flags & ST_NOSUID) != 0) ret |= MS_NOSUID;
465#endif
466#if defined(ST_NODEV) && defined(MS_NODEV)
467 if((f_flags & ST_NODEV) != 0) ret |= MS_NODEV;
468#endif
469#if defined(ST_NOEXEC) && defined(MS_NOEXEC)
470 if((f_flags & ST_NOEXEC) != 0) ret |= MS_NOEXEC;
471#endif
472#if defined(ST_NOATIME) && defined(MS_NOATIME)
473 if((f_flags & ST_NOATIME) != 0) ret |= MS_NOATIME;
474#endif
475#if defined(ST_NODIRATIME) && defined(MS_NODIRATIME)
476 if((f_flags & ST_NODIRATIME) != 0) ret |= MS_NODIRATIME;
477#endif
478#if defined(ST_RELATIME) && defined(MS_RELATIME)
479 if((f_flags & ST_RELATIME) != 0) ret |= MS_RELATIME;
480#endif
481 return ret;
482#else
483 throw Error("statfsToMountFlags not supported on this platform");
484#endif
485}
486
487
488void bindMount(Path source, Path target, bool readOnly)
489{
490#if HAVE_SYS_MOUNT_H && defined(MS_BIND)
491 struct stat st;
492 if (lstat(source.c_str(), &st) == -1)
493 throw SysError(format("getting attributes of path `%1%'") % source);
494
495 if(S_ISDIR(st.st_mode))
496 createDirs(target);
497 else if(S_ISLNK(st.st_mode)) {
498 /* bind-mounts follow symlinks, thus representing their target and not
499 the symlink itself. Create a copy of the symlink instead.*/
500 createDirs(dirOf(target));
501 createSymlink(readLink(source), target);
502 return;
503 }
504 else {
505 createDirs(dirOf(target));
506 writeFile(target, "");
507 }
508
509 /* This may fail with EINVAL unless we specify MS_REC, specifically if we
510 are in an unprivileged mount namespace and not specifying MS_REC would
511 reveal subtrees that had been covered up. */
512 if (mount(source.c_str(), target.c_str(), 0, MS_BIND|MS_REC, 0) == -1)
513 throw SysError(format("bind mount from `%1%' to `%2%' failed") % source % target);
514 if(readOnly) {
515#if defined(MS_REMOUNT) && defined(MS_RDONLY)
516 /* Extra flags passed with MS_BIND are ignored, hence the extra
517 MS_REMOUNT. */
518 unsigned long mount_flags = MS_BIND | MS_REMOUNT | MS_RDONLY;
519 /* MS_BIND | MS_REMOUNT sets all mountpoint flags, so we may get EPERM
520 unless we preserve the other flags (for example because it would
521 result in trying to clear the nosuid flag). */
522#if HAVE_STATVFS
523 struct statvfs stvfs;
524 if(statvfs(target.c_str(), &stvfs) == -1)
525 throw SysError(format("statvfs of `%1%'") % target);
526 mount_flags |= statfsToMountFlags(stvfs.f_flag);
527#endif
528
529 if (mount(source.c_str(), target.c_str(), 0, mount_flags, 0) == -1)
530 throw SysError(format("read-only remount of `%1%' failed") % target);
531#else
532 throw Error("remounting read-only is not supported on this platform");
533#endif
534 }
535#endif
536}
537
538
539void mountIntoChroot(std::map<Path, Path> filesInChroot,
540 set<Path> readOnlyFiles,
541 Path chrootRootDir)
542{
543#if HAVE_SYS_MOUNT_H && defined(MS_BIND)
544 for(auto i = filesInChroot.begin(); i != filesInChroot.end(); i++) {
545 Path source = i->second;
546 Path target = chrootRootDir + i->first;
547 bool readOnly = readOnlyFiles.find(i->first) != readOnlyFiles.end();
548 bindMount(source, target, readOnly);
549 }
550#else
551 throw Error("bind mounting not supported on this platform");
552#endif
553}
554
555
556void mountIntoChrootAction(SpawnContext & sctx)
557{
558#if CLONE_ENABLED && HAVE_SYS_MOUNT_H && defined(MS_BIND)
559 CloneSpawnContext & ctx = (CloneSpawnContext &) sctx;
560 if((ctx.cloneFlags & CLONE_NEWNS) != 0 && ctx.doChroot) {
561 mountIntoChroot(ctx.filesInChroot, ctx.readOnlyFilesInChroot, ctx.chrootRootDir);
562 }
563#endif
564}
565
566
567void mountProcAction(SpawnContext & sctx)
568{
569#if CLONE_ENABLED && HAVE_SYS_MOUNT_H
570 CloneSpawnContext & ctx = (CloneSpawnContext &) sctx;
571 if((ctx.cloneFlags & CLONE_NEWNS) != 0 && ctx.mountProc) {
572 Path target = (ctx.doChroot ? ctx.chrootRootDir : "") + "/proc";
573 createDirs(target);
574 if(mount("none", target.c_str(), "proc", 0, 0) == -1)
575 throw SysError(format("mounting `%1%'") % target);
576 }
577#endif
578}
579
580
581void mountDevshmAction(SpawnContext & sctx)
582{
583#if CLONE_ENABLED && HAVE_SYS_MOUNT_H
584 CloneSpawnContext & ctx = (CloneSpawnContext &) sctx;
585 if((ctx.cloneFlags & CLONE_NEWNS) != 0 && ctx.mountDevshm) {
586 Path target = (ctx.doChroot ? ctx.chrootRootDir : "") + "/dev/shm";
587 createDirs(target);
588 if(mount("none", target.c_str(), "tmpfs", 0, 0) == -1)
589 throw SysError(format("mounting `%1%'") % target);
590 }
591#endif
592}
593
594
595void mountDevptsAction(SpawnContext & sctx)
596{
597#if CLONE_ENABLED && HAVE_SYS_MOUNT_H
598 CloneSpawnContext & ctx = (CloneSpawnContext &) sctx;
599 if((ctx.cloneFlags & CLONE_NEWNS) != 0 && ctx.maybeMountDevpts) {
600 Path chroot = (ctx.doChroot ? ctx.chrootRootDir : "");
601 Path target = chroot + "/dev/pts";
602 if(pathExists(chroot + "/dev/ptmx")) return;
603 createDirs(target);
604 if(mount("none", target.c_str(), "devpts", 0, "newinstance,mode=0620") == -1)
605 throw SysError(format("mounting `%1%'") % target);
606 createSymlink("/dev/pts/ptmx", chroot + "/dev/ptmx");
607 /* Make sure /dev/pts/ptmx is world-writable. With some Linux
608 versions, it is created with permissions 0. */
609 Path targetPtmx = chroot + "/dev/pts/ptmx";
610 if (chmod(targetPtmx.c_str(), 0666) == -1)
611 throw SysError(format("setting permissions on `%1%'") % targetPtmx);
612 }
613#endif
614}
615
616
617void pivotRootAction(SpawnContext & sctx)
618{
619#if CLONE_ENABLED && HAVE_SYS_MOUNT_H
620 CloneSpawnContext & ctx = (CloneSpawnContext &) sctx;
621 if((ctx.cloneFlags & CLONE_NEWNS) != 0 && ctx.doChroot) {
622 if (chdir(ctx.chrootRootDir.c_str()) == -1)
623 throw SysError(format("cannot change directory to '%1%'") % ctx.chrootRootDir);
624
625 if (mkdir("real-root", 0) == -1)
626 throw SysError("cannot create real-root directory");
627
628 if (pivot_root(".", "real-root") == -1)
629 throw SysError(format("cannot pivot old root directory onto '%1%'") % (ctx.chrootRootDir + "/real-root"));
630
631 if (chroot(".") == -1)
632 throw SysError(format("cannot change root directory to '%1%'") % ctx.chrootRootDir);
633
634 if (umount2("real-root", MNT_DETACH) == -1)
635 throw SysError("cannot unmount real root filesystem");
636
637 if (rmdir("real-root") == -1)
638 throw SysError("cannot remove real-root directory");
639 }
640#endif
641}
642
643
644string idMapToIdentityMap(const string & map)
645{
646 std::vector<string> mapLines =
647 tokenizeString<std::vector<string> >(map, "\n");
648 string out;
649
650 for(auto & i : mapLines) {
651 std::vector<string> elements =
652 tokenizeString<std::vector<string> >(i, " ");
653 out.append(elements.at(0) + " " + elements.at(0) + " " + elements.at(2) + "\n");
654 }
655 return out;
656}
657
658
659/* Initializing a user namespace with more than one id mapped requires
660 * capabilities in the *parent* user namespace, which may not even have any
661 * processes in it after unshare is called. So fork a child and have it do
662 * the initialization. */
663void unshareAndInitUserns(int flags, const string & uidMap,
664 const string & gidMap, bool allowSetgroups)
665{
666#if CLONE_ENABLED
667 pid_t pid_ = getpid();
668 string pid = std::to_string(pid_);
669 Pipe toChild;
670 Pipe fromChild;
671 toChild.create();
672 fromChild.create();
673 pid_t child = fork();
674 if(child == -1)
675 throw SysError("creating child process");
676 if(child == 0) {
677 try {
678 toChild.writeSide.close();
679 fromChild.readSide.close();
680 waitForMessage(toChild.readSide, "ready\n");
681 writeFile("/proc/" + pid + "/uid_map", uidMap);
682 writeFile("/proc/" + pid + "/setgroups",
683 allowSetgroups ? "allow" : "deny");
684 writeFile("/proc/" + pid + "/gid_map", gidMap);
685 writeFull(fromChild.writeSide, (unsigned char*)"go\n", 3);
686 } catch(...) {
687 /* Don't unwind the stack in case of exception, halt
688 * immediately. */
689 _exit(1);
690 }
691 _exit(EXIT_SUCCESS);
692 } else {
693 toChild.readSide.close();
694 fromChild.writeSide.close();
695 if(unshare(flags) == -1)
696 throw SysError("unshare");
697 writeFull(toChild.writeSide, (unsigned char*)"ready\n", 6);
698 waitForMessage(fromChild.readSide, "go\n");
699 int status;
700 while(waitpid(child, &status, 0) == -1) {
701 if(errno != EINTR)
702 throw SysError("reaping userns init process");
703 }
704 if(!(WIFEXITED(status) != 0 && WEXITSTATUS(status) == EXIT_SUCCESS))
705 throw Error(format("userns init child exited with status %1%") % WEXITSTATUS(status));
706 }
707#endif
708}
709
710
711void lockMountsAction(SpawnContext & sctx)
712{
713#if CLONE_ENABLED && HAVE_SYS_MOUNT_H
714 CloneSpawnContext & ctx = (CloneSpawnContext &) sctx;
715 if(ctx.lockMounts) {
716 string uidMap;
717 string gidMap;
718 if(ctx.lockMountsMapAll) {
719 string oldUidMap = readFile("/proc/self/uid_map", true);
720 string oldGidMap = readFile("/proc/self/gid_map", true);
721 uidMap = idMapToIdentityMap(oldUidMap);
722 gidMap = idMapToIdentityMap(oldGidMap);
723 } else {
724 string uid = std::to_string(getuid());
725 string gid = std::to_string(getgid());
726 uidMap = uid + " " + uid + " 1";
727 gidMap = gid + " " + gid + " 1";
728 }
729 unshareAndInitUserns(CLONE_NEWNS | CLONE_NEWUSER,
730 uidMap, gidMap, ctx.lockMountsAllowSetgroups);
731 /* Check that mounts inherited in our new mount namespace are "locked"
732 together and cannot be separated from within our mount namespace.
733 Since umount(2) is documented to fail with EINVAL when attempting
734 to unmount one of the mounts that are locked together, check that
735 this is what we get. */
736 int ret = umount("/proc");
737 assert(ret == -1 && errno == EINVAL);
738 }
739#endif
740}
741
742
743Phases getCloneSpawnPhases()
744{
745#if CLONE_ENABLED
746 return { { "reset_writeToStderr", reset_writeToStderrAction },
747 { "usernsInitSync", usernsInitSyncAction },
748 { "usernsSetIDs", usernsSetIDsAction },
749 { "restoreAffinity", restoreAffinityAction },
750 { "setsid", setsidAction },
751 { "earlyIOSetup", earlyIOSetupAction },
752 { "dropAmbientCapabilities", dropAmbientCapabilitiesAction },
753 { "initLoopback", initLoopbackAction },
754 { "setHostAndDomain", setHostAndDomainAction },
755 { "makeFilesystemsPrivate", makeFilesystemsPrivateAction },
756 { "makeChrootSeparateFilesystem", makeChrootSeparateFilesystemAction },
757 { "mountIntoChroot", mountIntoChrootAction },
758 { "mountProc", mountProcAction },
759 { "mountDevshm", mountDevshmAction },
760 { "mountDevpts", mountDevptsAction },
761 { "chroot", pivotRootAction },
762 { "chdir", chdirAction },
763 { "closeMostFDs", closeMostFDsAction },
764 { "setPersonality", setPersonalityAction },
765 { "oomSacrifice", oomSacrificeAction },
766 /* Being put in a user namespace with only the current ids mapped
767 would tend to prevent switching to other ones, but if this
768 comes after setIDs then the per-process "dumpable" flag may be
769 reset, which will cause /proc/self to become root-owned,
770 making /proc/self/uid_map inaccessible. If you need
771 lockMounts to preserve the id mappings, and you have the
772 necessary capabilities in the parent user namespace, set
773 CloneSpawnContext.lockMountsMapAll = true. */
774 { "lockMounts", lockMountsAction },
775 { "setIDs", setIDsAction },
776 { "restoreSIGPIPE", restoreSIGPIPEAction },
777 { "setupSuccess", setupSuccessAction },
778 { "exec", execAction }};
779#else
780 throw Error("clone not supported on this platform");
781#endif
782}
783
784
785void runChildSetup(SpawnContext & ctx)
786{
787 ctx.currentPhase = 0;
788 try {
789 /* Should not return regularly from this */
790 while(true) {
791 ctx.phases.at(ctx.currentPhase).action(ctx);
792 ctx.currentPhase++;
793 }
794 } catch (std::exception & e) {
795 try {
796 writeFull(STDERR_FILENO,
797 "while setting up the child process: " +
798 (ctx.currentPhase < (ssize_t)ctx.phases.size() ?
799 "in phase " + ctx.phases[ctx.currentPhase].label + ": " : "") +
800 string(e.what()) + "\n");
801 } catch (std::exception & e2) {
802 _exit(1);
803 }
804 _exit(1);
805 }
806 abort(); /* Should never be reached */
807}
808
809
810int runChildSetupEntry(void *data)
811{
812 runChildSetup(* (SpawnContext *)data);
813 return 1;
814}
815
816
817int cloneChild(CloneSpawnContext & ctx)
818{
819 char stack[32 * 1024];
820 /* Ensure proper alignment on the stack. On aarch64, it has to be 16
821 bytes. */
822 char *alignedStack = (char *)(((uintptr_t)stack + sizeof(stack) - 8) & ~(uintptr_t)0xf);
823 int ret = clone(runChildSetupEntry, alignedStack, ctx.cloneFlags, (void *) &ctx);
824 if(ret == -1)
825 throw SysError("clone");
826 return ret;
827}
828
829}