| 1 | |
| 2 | Copyright (C) 2025 Caleb Ristvedt <[email protected]>
|
| 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 |
|
| 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 <fcntl.h>
|
| 30 | #include <cstring>
|
| 31 | #include <cstdlib>
|
| 32 | #include <cassert>
|
| 33 | #include <format>
|
| 34 |
|
| 35 | #if HAVE_SYS_MOUNT_H
|
| 36 | #include <sys/mount.h>
|
| 37 | #endif
|
| 38 |
|
| 39 | #if HAVE_SCHED_H
|
| 40 | #include <sched.h>
|
| 41 | #endif
|
| 42 |
|
| 43 | #if HAVE_STATVFS
|
| 44 | #include <sys/statvfs.h>
|
| 45 | #endif
|
| 46 |
|
| 47 | #if HAVE_SYS_SYSCALL_H
|
| 48 | #include <sys/syscall.h>
|
| 49 | #endif
|
| 50 |
|
| 51 | #if HAVE_SYS_PRCTL_H
|
| 52 | #include <sys/prctl.h>
|
| 53 | #endif
|
| 54 |
|
| 55 | #ifdef __linux__
|
| 56 | #include <sys/personality.h>
|
| 57 | #include <linux/seccomp.h>
|
| 58 | #include <linux/filter.h>
|
| 59 | #endif
|
| 60 |
|
| 61 | #if defined(SYS_pivot_root)
|
| 62 | #define pivot_root(new_root, put_old) (syscall(SYS_pivot_root, new_root,put_old))
|
| 63 | #endif
|
| 64 |
|
| 65 | #if defined(CLONE_NEWNS)
|
| 66 | #define CLONE_ENABLED 1
|
| 67 | #else
|
| 68 | #define CLONE_ENABLED 0
|
| 69 | #endif
|
| 70 |
|
| 71 | #if CLONE_ENABLED
|
| 72 | #include <sys/ioctl.h>
|
| 73 | #include <net/if.h>
|
| 74 | #include <netinet/in.h>
|
| 75 | #endif
|
| 76 |
|
| 77 | namespace nix {
|
| 78 |
|
| 79 |
|
| 80 | void addPhaseAfter(Phases & phases, string afterLabel, string addLabel, Action addAction)
|
| 81 | {
|
| 82 | for(auto i = phases.begin(); i != phases.end(); i++)
|
| 83 | if((*i).label == afterLabel) {
|
| 84 | i++;
|
| 85 | Phase p;
|
| 86 | p.label = addLabel;
|
| 87 | p.action = addAction;
|
| 88 | phases.insert(i, p);
|
| 89 | return;
|
| 90 | }
|
| 91 | throw Error(std::format("label `{}' not found in phases", afterLabel));
|
| 92 | }
|
| 93 |
|
| 94 |
|
| 95 | void addPhaseBefore(Phases & phases, string beforeLabel, string addLabel, Action addAction)
|
| 96 | {
|
| 97 | for(auto i = phases.begin(); i != phases.end(); i++)
|
| 98 | if((*i).label == beforeLabel) {
|
| 99 | Phase p;
|
| 100 | p.label = addLabel;
|
| 101 | p.action = addAction;
|
| 102 | phases.insert(i, p);
|
| 103 | return;
|
| 104 | }
|
| 105 | throw Error(std::format("label `{}' not found in phases", beforeLabel));
|
| 106 | }
|
| 107 |
|
| 108 |
|
| 109 | void prependPhase(Phases & phases, string addLabel, Action addAction)
|
| 110 | {
|
| 111 | Phase p;
|
| 112 | p.label = addLabel;
|
| 113 | p.action = addAction;
|
| 114 | phases.insert(phases.begin(), p);
|
| 115 | }
|
| 116 |
|
| 117 |
|
| 118 | void appendPhase(Phases & phases, string addLabel, Action addAction)
|
| 119 | {
|
| 120 | Phase p;
|
| 121 | p.label = addLabel;
|
| 122 | p.action = addAction;
|
| 123 | phases.push_back(p);
|
| 124 | }
|
| 125 |
|
| 126 |
|
| 127 | void deletePhase(Phases & phases, string delLabel)
|
| 128 | {
|
| 129 | for(auto i = phases.begin(); i != phases.end(); i++)
|
| 130 | if((*i).label == delLabel) {
|
| 131 | phases.erase(i);
|
| 132 | return;
|
| 133 | }
|
| 134 | throw Error(std::format("label `{}' not found in phases", delLabel));
|
| 135 | }
|
| 136 |
|
| 137 |
|
| 138 | void replacePhase(Phases & phases, string replaceLabel, Action newAction)
|
| 139 | {
|
| 140 | for(auto i = phases.begin(); i != phases.end(); i++)
|
| 141 | if((*i).label == replaceLabel) {
|
| 142 | (*i).action = newAction;
|
| 143 | return;
|
| 144 | }
|
| 145 | throw Error(std::format("label `{}' not found in phases", replaceLabel));
|
| 146 | }
|
| 147 |
|
| 148 |
|
| 149 |
|
| 150 |
|
| 151 | static void reset_writeToStderrAction(SpawnContext & ctx)
|
| 152 | {
|
| 153 | _writeToStderr = 0;
|
| 154 | }
|
| 155 |
|
| 156 |
|
| 157 | static void restoreAffinityAction(SpawnContext & ctx)
|
| 158 | {
|
| 159 | restoreAffinity();
|
| 160 | }
|
| 161 |
|
| 162 |
|
| 163 | static void setsidAction(SpawnContext & ctx)
|
| 164 | {
|
| 165 | |
| 166 | separate process group, so it doesn't receive group-directed signals
|
| 167 | sent at the parent. The new session initially has no controlling
|
| 168 | terminal, so it also doesn't receive terminal signals and can't open
|
| 169 | /dev/tty.
|
| 170 | if(ctx.setsid && setsid() == (pid_t)-1)
|
| 171 | throw SysError("creating a new session");
|
| 172 | }
|
| 173 |
|
| 174 |
|
| 175 | static void earlyIOSetupAction(SpawnContext & ctx)
|
| 176 | {
|
| 177 | for(auto i = ctx.earlyCloseFDs.begin(); i != ctx.earlyCloseFDs.end(); i++)
|
| 178 | if(close(*i) == -1)
|
| 179 | throw SysError("closing fd");
|
| 180 |
|
| 181 | if(ctx.logFD != -1) {
|
| 182 | if(dup2(ctx.logFD, STDOUT_FILENO) == -1)
|
| 183 | throw SysError("cannot dup2 log fd into stdout fd");
|
| 184 | if(dup2(ctx.logFD, STDERR_FILENO) == -1)
|
| 185 | throw SysError("cannot dup2 log fd into stderr fd");
|
| 186 | }
|
| 187 |
|
| 188 | if(ctx.setStdin) {
|
| 189 | if(ctx.stdinFD != -1) {
|
| 190 | if(dup2(ctx.stdinFD, STDIN_FILENO) == -1)
|
| 191 | throw SysError("cannot dup2 fd into stdin fd");
|
| 192 | }
|
| 193 | else {
|
| 194 |
|
| 195 | AutoCloseFD fd = open(ctx.stdinFile.c_str(), O_RDWR);
|
| 196 | if(fd == -1)
|
| 197 | throw SysError(std::format("cannot open `{}'", ctx.stdinFile));
|
| 198 | if(dup2(fd, STDIN_FILENO) == -1)
|
| 199 | throw SysError("cannot dup2 fd into stdin fd");
|
| 200 | }
|
| 201 | }
|
| 202 | }
|
| 203 |
|
| 204 |
|
| 205 | static void dropAmbientCapabilitiesAction(SpawnContext & ctx)
|
| 206 | {
|
| 207 | |
| 208 | when starting guix-daemon.
|
| 209 | if(ctx.dropAmbientCapabilities)
|
| 210 | #if HAVE_SYS_PRCTL_H
|
| 211 | prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0);
|
| 212 | #else
|
| 213 | throw Error("dropping ambient capabilities is not supported on this system");
|
| 214 | #endif
|
| 215 | }
|
| 216 |
|
| 217 |
|
| 218 | static void chrootAction(SpawnContext & ctx)
|
| 219 | {
|
| 220 | if(ctx.doChroot)
|
| 221 | #if HAVE_CHROOT
|
| 222 | if(chroot(ctx.chrootRootDir.c_str()) == -1)
|
| 223 | throw SysError(std::format("cannot change root directory to '{}'", ctx.chrootRootDir));
|
| 224 | #else
|
| 225 | throw Error("chroot is not supported on this system");
|
| 226 | #endif
|
| 227 | }
|
| 228 |
|
| 229 |
|
| 230 | static void chdirAction(SpawnContext & ctx)
|
| 231 | {
|
| 232 | if(ctx.setcwd)
|
| 233 | if(chdir(ctx.cwd.c_str()) == -1)
|
| 234 | throw SysError(std::format("changing into `{}'", ctx.cwd));
|
| 235 | }
|
| 236 |
|
| 237 |
|
| 238 | static void closeMostFDsAction(SpawnContext & ctx)
|
| 239 | {
|
| 240 | if(ctx.closeMostFDs) closeMostFDs(ctx.preserveFDs);
|
| 241 | for(auto i = ctx.preserveFDs.begin(); i != ctx.preserveFDs.end(); i++)
|
| 242 | keepOnExec(*i);
|
| 243 | }
|
| 244 |
|
| 245 |
|
| 246 | static void setPersonalityAction(SpawnContext & ctx)
|
| 247 | {
|
| 248 | if(ctx.setPersona)
|
| 249 | #ifdef __linux__
|
| 250 | if(personality(ctx.persona) == -1)
|
| 251 | throw SysError("cannot set personality");
|
| 252 | #else
|
| 253 | throw Error("setting the personality is not supported on this system");
|
| 254 | #endif
|
| 255 | }
|
| 256 |
|
| 257 |
|
| 258 | static void oomSacrificeAction(SpawnContext & ctx)
|
| 259 | {
|
| 260 | #ifdef __linux__
|
| 261 | if(ctx.oomSacrifice)
|
| 262 | |
| 263 | memory, regardless of blame, to preserve ‘real’ user data &
|
| 264 | state.
|
| 265 | try {
|
| 266 | writeFile("/proc/self/oom_score_adj", "1000"); |
| 267 | } catch(...) { ignoreException(); }
|
| 268 | #endif
|
| 269 | }
|
| 270 |
|
| 271 |
|
| 272 | static void setIDsAction(SpawnContext & ctx)
|
| 273 | {
|
| 274 | if(ctx.setSupplementaryGroups)
|
| 275 | if(setgroups(ctx.supplementaryGroups.size(),
|
| 276 | ctx.supplementaryGroups.data()) == -1)
|
| 277 | throw SysError("cannot set supplementary groups");
|
| 278 |
|
| 279 | if(ctx.setgid)
|
| 280 | if(setgid(ctx.group) == -1 ||
|
| 281 | getgid() != ctx.group ||
|
| 282 | getegid() != ctx.group)
|
| 283 | throw SysError("setgid failed");
|
| 284 |
|
| 285 | if(ctx.setuid)
|
| 286 | if(setuid(ctx.user) == -1 ||
|
| 287 | getuid() != ctx.user ||
|
| 288 | geteuid() != ctx.user)
|
| 289 | throw SysError("setuid failed");
|
| 290 | }
|
| 291 |
|
| 292 | static void setNoNewPrivsAction(SpawnContext & ctx)
|
| 293 | {
|
| 294 | if(ctx.setNoNewPrivs)
|
| 295 | #if __linux__ && defined(PR_SET_NO_NEW_PRIVS)
|
| 296 | if(prctl(PR_SET_NO_NEW_PRIVS, 0, 0, 0, 0) == -1)
|
| 297 | throw SysError("setting PR_SET_NO_NEW_PRIVS");
|
| 298 | #else
|
| 299 | throw Error("setting PR_SET_NO_NEW_PRIVS not supported on this system");
|
| 300 | #endif
|
| 301 | }
|
| 302 |
|
| 303 | static void addSeccompFilterAction(SpawnContext & ctx)
|
| 304 | {
|
| 305 | if(ctx.addSeccompFilter) {
|
| 306 | #if __linux__ && defined(PR_SET_SECCOMP) && defined(SECCOMP_MODE_FILTER)
|
| 307 | |
| 308 | * just use prctl.
|
| 309 | if(ctx.seccompFilter.size() > USHRT_MAX)
|
| 310 | throw Error("seccomp filter too large");
|
| 311 | struct sock_fprog prog;
|
| 312 | prog.len = (unsigned short) ctx.seccompFilter.size();
|
| 313 | prog.filter = ctx.seccompFilter.data();
|
| 314 | if(prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) == -1)
|
| 315 | throw SysError("installing seccomp filter");
|
| 316 | #else
|
| 317 | throw Error("setting seccomp filter not supported on this system");
|
| 318 | #endif
|
| 319 | }
|
| 320 | }
|
| 321 |
|
| 322 |
|
| 323 | static void restoreSIGPIPEAction(SpawnContext & ctx)
|
| 324 | {
|
| 325 | |
| 326 | randomly say "Broken pipe".
|
| 327 | struct sigaction act, oact;
|
| 328 | act.sa_handler = SIG_DFL;
|
| 329 | act.sa_flags = 0;
|
| 330 | sigemptyset(&act.sa_mask);
|
| 331 | if (sigaction(SIGPIPE, &act, &oact)) throw SysError("resetting SIGPIPE");
|
| 332 | }
|
| 333 |
|
| 334 |
|
| 335 | static void setupSuccessAction(SpawnContext & ctx)
|
| 336 | {
|
| 337 | if(ctx.signalSetupSuccess)
|
| 338 | writeFull(STDERR_FILENO, "\n");
|
| 339 | }
|
| 340 |
|
| 341 |
|
| 342 | void execAction(SpawnContext & ctx) |
| 343 | {
|
| 344 | Strings envStrs;
|
| 345 | std::vector<char *> envPtrs;
|
| 346 | char **env;
|
| 347 | if(ctx.inheritEnv) {
|
| 348 | for(auto i = ctx.env.begin(); i != ctx.env.end(); i++)
|
| 349 | if(setenv(i->first.c_str(), i->second.c_str(), 1) == -1)
|
| 350 | throw SysError("setenv");
|
| 351 | env = environ;
|
| 352 | } else {
|
| 353 | for(auto i = ctx.env.begin(); i != ctx.env.end(); i++)
|
| 354 | envStrs.push_back(i->first + "=" + i->second);
|
| 355 |
|
| 356 | envPtrs = stringsToCharPtrs(envStrs);
|
| 357 | env = envPtrs.data();
|
| 358 | }
|
| 359 | if(execvpe(ctx.program.c_str(), stringsToCharPtrs(ctx.args).data(), env) == -1)
|
| 360 | throw SysError(std::format("executing `{}'", ctx.program));
|
| 361 | }
|
| 362 |
|
| 363 |
|
| 364 | Phases getBasicSpawnPhases()
|
| 365 | {
|
| 366 | return { { "reset_writeToStderr", reset_writeToStderrAction },
|
| 367 | { "restoreAffinity", restoreAffinityAction },
|
| 368 | { "setsid", setsidAction },
|
| 369 | { "earlyIOSetup", earlyIOSetupAction },
|
| 370 | { "dropAmbientCapabilities", dropAmbientCapabilitiesAction },
|
| 371 | { "chroot", chrootAction },
|
| 372 | { "chdir", chdirAction },
|
| 373 | { "closeMostFDs", closeMostFDsAction },
|
| 374 | { "setPersonality", setPersonalityAction },
|
| 375 | { "oomSacrifice", oomSacrificeAction },
|
| 376 | { "setIDs", setIDsAction },
|
| 377 | { "setNoNewPrivs", setNoNewPrivsAction },
|
| 378 | { "addSeccompFilter", addSeccompFilterAction },
|
| 379 | { "restoreSIGPIPE", restoreSIGPIPEAction },
|
| 380 | { "setupSuccess", setupSuccessAction },
|
| 381 | { "exec", execAction } };
|
| 382 | }
|
| 383 |
|
| 384 |
|
| 385 | static void usernsInitSyncAction(SpawnContext & sctx)
|
| 386 | {
|
| 387 | #if CLONE_ENABLED
|
| 388 | CloneSpawnContext & ctx = (CloneSpawnContext &) sctx;
|
| 389 | if((ctx.cloneFlags & CLONE_NEWUSER) != 0) {
|
| 390 |
|
| 391 | for(auto i = ctx.earlyCloseFDs.begin(); i != ctx.earlyCloseFDs.end(); i++)
|
| 392 | if(close(*i) == -1)
|
| 393 | throw SysError("closing fd");
|
| 394 |
|
| 395 | ctx.earlyCloseFDs.clear();
|
| 396 | |
| 397 | our user namespace.
|
| 398 | waitForMessage(ctx.setupFD, "go\n");
|
| 399 | }
|
| 400 | #endif
|
| 401 | }
|
| 402 |
|
| 403 |
|
| 404 | static void usernsSetIDsAction(SpawnContext & sctx)
|
| 405 | {
|
| 406 | #if CLONE_ENABLED
|
| 407 | CloneSpawnContext & ctx = (CloneSpawnContext &) sctx;
|
| 408 | if((ctx.cloneFlags & CLONE_NEWUSER) != 0) {
|
| 409 | |
| 410 | nonzero uids causes capabilities to be lost, but doesn't say what
|
| 411 | happens when a transition from an unmapped (possibly zero) uid to a
|
| 412 | nonzero uid happens.
|
| 413 | if(ctx.usernsSetuid)
|
| 414 | |
| 415 | effective, saved, and filesystem uids
|
| 416 | if(setuid(ctx.usernsUser) != 0)
|
| 417 | throw SysError("setuid");
|
| 418 | if(ctx.usernsSetgid)
|
| 419 |
|
| 420 | if(setgid(ctx.usernsGroup) != 0)
|
| 421 | throw SysError("setgid");
|
| 422 | }
|
| 423 | #endif
|
| 424 | }
|
| 425 |
|
| 426 |
|
| 427 | static void initLoopbackAction(SpawnContext & sctx)
|
| 428 | {
|
| 429 | #if CLONE_ENABLED
|
| 430 | CloneSpawnContext & ctx = (CloneSpawnContext &) sctx;
|
| 431 | if(((ctx.cloneFlags & CLONE_NEWNET) != 0) && ctx.initLoopback) {
|
| 432 | AutoCloseFD fd(socket(PF_INET, SOCK_DGRAM, IPPROTO_IP));
|
| 433 | if (fd == -1) throw SysError("cannot open IP socket");
|
| 434 |
|
| 435 | struct ifreq ifr;
|
| 436 | strcpy(ifr.ifr_name, "lo");
|
| 437 | ifr.ifr_flags = IFF_UP | IFF_LOOPBACK | IFF_RUNNING;
|
| 438 | if (ioctl(fd, SIOCSIFFLAGS, &ifr) == -1)
|
| 439 | throw SysError("cannot set loopback interface flags");
|
| 440 |
|
| 441 | fd.close();
|
| 442 | }
|
| 443 | #endif
|
| 444 | }
|
| 445 |
|
| 446 |
|
| 447 | static void setHostAndDomainAction(SpawnContext & sctx)
|
| 448 | {
|
| 449 | #if CLONE_ENABLED
|
| 450 | CloneSpawnContext & ctx = (CloneSpawnContext &) sctx;
|
| 451 | if((ctx.cloneFlags & CLONE_NEWUTS) != 0) {
|
| 452 | if (sethostname(ctx.hostname.c_str(),
|
| 453 | strlen(ctx.hostname.c_str())) == -1)
|
| 454 | throw SysError("cannot set host name");
|
| 455 | if (setdomainname(ctx.domainname.c_str(),
|
| 456 | strlen(ctx.domainname.c_str())) == -1)
|
| 457 | throw SysError("cannot set domain name");
|
| 458 | }
|
| 459 | #endif
|
| 460 | }
|
| 461 |
|
| 462 |
|
| 463 | static void makeFilesystemsPrivateAction(SpawnContext & sctx)
|
| 464 | {
|
| 465 | #if CLONE_ENABLED && HAVE_SYS_MOUNT_H && defined(MS_REC) && defined(MS_PRIVATE)
|
| 466 | CloneSpawnContext & ctx = (CloneSpawnContext &) sctx;
|
| 467 | if((ctx.cloneFlags & CLONE_NEWNS) != 0) {
|
| 468 | if(mount(0, "/", 0, MS_REC|MS_PRIVATE, 0) == -1)
|
| 469 | throw SysError("unable to make `/' private mount");
|
| 470 | }
|
| 471 | #endif
|
| 472 | }
|
| 473 |
|
| 474 |
|
| 475 | static void makeChrootSeparateFilesystemAction(SpawnContext & sctx)
|
| 476 | {
|
| 477 | #if CLONE_ENABLED && HAVE_SYS_MOUNT_H && defined(MS_BIND)
|
| 478 | CloneSpawnContext & ctx = (CloneSpawnContext &) sctx;
|
| 479 | if(((ctx.cloneFlags & CLONE_NEWNS) != 0) && ctx.doChroot) {
|
| 480 | |
| 481 | filesystem from /, as needed for pivot_root. Alternatively, mount
|
| 482 | a tmpfs on it.
|
| 483 | if(ctx.mountTmpfsOnChroot) {
|
| 484 | if(mount("none", ctx.chrootRootDir.c_str(), "tmpfs", 0, 0) == -1)
|
| 485 | throw SysError(std::format("unable to mount tmpfs on `{}'", ctx.chrootRootDir));
|
| 486 | }
|
| 487 | else {
|
| 488 | if(mount(ctx.chrootRootDir.c_str(), ctx.chrootRootDir.c_str(), 0, MS_BIND, 0) == -1)
|
| 489 | throw SysError(std::format("unable to bind mount `{}'", ctx.chrootRootDir));
|
| 490 | }
|
| 491 | }
|
| 492 | #endif
|
| 493 | }
|
| 494 |
|
| 495 |
|
| 496 | static int statfsToMountFlags(int f_flags)
|
| 497 | {
|
| 498 | #if HAVE_SYS_MOUNT_H && HAVE_STATVFS
|
| 499 | int ret = 0;
|
| 500 | #if defined(ST_RDONLY) && defined(MS_RDONLY)
|
| 501 | if((f_flags & ST_RDONLY) != 0) ret |= MS_RDONLY;
|
| 502 | #endif
|
| 503 | #if defined(ST_NOSUID) && defined(MS_NOSUID)
|
| 504 | if((f_flags & ST_NOSUID) != 0) ret |= MS_NOSUID;
|
| 505 | #endif
|
| 506 | #if defined(ST_NODEV) && defined(MS_NODEV)
|
| 507 | if((f_flags & ST_NODEV) != 0) ret |= MS_NODEV;
|
| 508 | #endif
|
| 509 | #if defined(ST_NOEXEC) && defined(MS_NOEXEC)
|
| 510 | if((f_flags & ST_NOEXEC) != 0) ret |= MS_NOEXEC;
|
| 511 | #endif
|
| 512 | #if defined(ST_NOATIME) && defined(MS_NOATIME)
|
| 513 | if((f_flags & ST_NOATIME) != 0) ret |= MS_NOATIME;
|
| 514 | #endif
|
| 515 | #if defined(ST_NODIRATIME) && defined(MS_NODIRATIME)
|
| 516 | if((f_flags & ST_NODIRATIME) != 0) ret |= MS_NODIRATIME;
|
| 517 | #endif
|
| 518 | #if defined(ST_RELATIME) && defined(MS_RELATIME)
|
| 519 | if((f_flags & ST_RELATIME) != 0) ret |= MS_RELATIME;
|
| 520 | #endif
|
| 521 | return ret;
|
| 522 | #else
|
| 523 | throw Error("statfsToMountFlags not supported on this platform");
|
| 524 | #endif
|
| 525 | }
|
| 526 |
|
| 527 |
|
| 528 | static void bindMount(Path source, Path target, bool readOnly)
|
| 529 | {
|
| 530 | #if HAVE_SYS_MOUNT_H && defined(MS_BIND)
|
| 531 | struct stat st;
|
| 532 | if (lstat(source.c_str(), &st) == -1)
|
| 533 | throw SysError(std::format("getting attributes of path `{}'", source));
|
| 534 |
|
| 535 | if(S_ISDIR(st.st_mode))
|
| 536 | createDirs(target);
|
| 537 | else if(S_ISLNK(st.st_mode)) {
|
| 538 | |
| 539 | the symlink itself. Create a copy of the symlink instead.
|
| 540 | createDirs(dirOf(target));
|
| 541 | createSymlink(readLink(source), target);
|
| 542 | return;
|
| 543 | }
|
| 544 | else {
|
| 545 | struct stat st2;
|
| 546 | createDirs(dirOf(target));
|
| 547 | |
| 548 | * checking for its existence and type
|
| 549 | while(true){
|
| 550 | if(lstat(target.c_str(), &st2) != -1) {
|
| 551 | if(!S_ISREG(st2.st_mode))
|
| 552 | throw Error(std::format("mount target `{}' exists but is not a regular file", target));
|
| 553 | break;
|
| 554 | }
|
| 555 | if(errno != ENOENT) {
|
| 556 | throw SysError(std::format("stat'ing path `{}'", target));
|
| 557 | }
|
| 558 | AutoCloseFD fd = open(target.c_str(),
|
| 559 | O_WRONLY | O_NOFOLLOW | O_CREAT | O_EXCL,
|
| 560 | 0600);
|
| 561 | if(fd != -1) {
|
| 562 | fd.close();
|
| 563 | break;
|
| 564 | }
|
| 565 | |
| 566 | * permission issue with the parent directory
|
| 567 | if(errno != EEXIST)
|
| 568 | throw SysError(std::format("Creating placeholder regular file target mount `{}'", target));
|
| 569 | }
|
| 570 | }
|
| 571 |
|
| 572 | |
| 573 | are in an unprivileged mount namespace and not specifying MS_REC would
|
| 574 | reveal subtrees that had been covered up.
|
| 575 | if (mount(source.c_str(), target.c_str(), 0, MS_BIND|MS_REC, 0) == -1)
|
| 576 | throw SysError(std::format("bind mount from `{}' to `{}' failed", source, target));
|
| 577 | if(readOnly) {
|
| 578 | #if defined(MS_REMOUNT) && defined(MS_RDONLY)
|
| 579 | |
| 580 | MS_REMOUNT.
|
| 581 | unsigned long mount_flags = MS_BIND | MS_REMOUNT | MS_RDONLY;
|
| 582 | |
| 583 | unless we preserve the other flags (for example because it would
|
| 584 | result in trying to clear the nosuid flag).
|
| 585 | #if HAVE_STATVFS
|
| 586 | struct statvfs stvfs;
|
| 587 | if(statvfs(target.c_str(), &stvfs) == -1)
|
| 588 | throw SysError(std::format("statvfs of `{}'", target));
|
| 589 | mount_flags |= statfsToMountFlags(stvfs.f_flag);
|
| 590 | #endif
|
| 591 |
|
| 592 | if (mount(source.c_str(), target.c_str(), 0, mount_flags, 0) == -1)
|
| 593 | throw SysError(std::format("read-only remount of `{}' failed", target));
|
| 594 | #else
|
| 595 | throw Error("remounting read-only is not supported on this platform");
|
| 596 | #endif
|
| 597 | }
|
| 598 | #endif
|
| 599 | }
|
| 600 |
|
| 601 |
|
| 602 | static void mountIntoChroot(std::map<Path, Path> filesInChroot,
|
| 603 | set<Path> readOnlyFiles,
|
| 604 | Path chrootRootDir)
|
| 605 | {
|
| 606 | #if HAVE_SYS_MOUNT_H && defined(MS_BIND)
|
| 607 | for(auto i = filesInChroot.begin(); i != filesInChroot.end(); i++) {
|
| 608 | Path source = i->second;
|
| 609 | Path target = chrootRootDir + i->first;
|
| 610 | bool readOnly = readOnlyFiles.find(i->first) != readOnlyFiles.end();
|
| 611 | bindMount(source, target, readOnly);
|
| 612 | }
|
| 613 | #else
|
| 614 | throw Error("bind mounting not supported on this platform");
|
| 615 | #endif
|
| 616 | }
|
| 617 |
|
| 618 |
|
| 619 | static void mountIntoChrootAction(SpawnContext & sctx)
|
| 620 | {
|
| 621 | #if CLONE_ENABLED && HAVE_SYS_MOUNT_H && defined(MS_BIND)
|
| 622 | CloneSpawnContext & ctx = (CloneSpawnContext &) sctx;
|
| 623 | if((ctx.cloneFlags & CLONE_NEWNS) != 0 && ctx.doChroot) {
|
| 624 | mountIntoChroot(ctx.filesInChroot, ctx.readOnlyFilesInChroot, ctx.chrootRootDir);
|
| 625 | }
|
| 626 | #endif
|
| 627 | }
|
| 628 |
|
| 629 |
|
| 630 | static void mountProcAction(SpawnContext & sctx)
|
| 631 | {
|
| 632 | #if CLONE_ENABLED && HAVE_SYS_MOUNT_H
|
| 633 | CloneSpawnContext & ctx = (CloneSpawnContext &) sctx;
|
| 634 | if((ctx.cloneFlags & CLONE_NEWNS) != 0 && ctx.mountProc) {
|
| 635 | Path target = (ctx.doChroot ? ctx.chrootRootDir : "") + "/proc";
|
| 636 | createDirs(target);
|
| 637 | if(mount("none", target.c_str(), "proc", 0, 0) == -1)
|
| 638 | throw SysError(std::format("mounting `{}'", target));
|
| 639 | }
|
| 640 | #endif
|
| 641 | }
|
| 642 |
|
| 643 |
|
| 644 | static void mountDevshmAction(SpawnContext & sctx)
|
| 645 | {
|
| 646 | #if CLONE_ENABLED && HAVE_SYS_MOUNT_H
|
| 647 | CloneSpawnContext & ctx = (CloneSpawnContext &) sctx;
|
| 648 | if((ctx.cloneFlags & CLONE_NEWNS) != 0 && ctx.mountDevshm) {
|
| 649 | Path target = (ctx.doChroot ? ctx.chrootRootDir : "") + "/dev/shm";
|
| 650 | createDirs(target);
|
| 651 | if(mount("none", target.c_str(), "tmpfs", 0, 0) == -1)
|
| 652 | throw SysError(std::format("mounting `{}'", target));
|
| 653 | }
|
| 654 | #endif
|
| 655 | }
|
| 656 |
|
| 657 |
|
| 658 | static void mountDevptsAction(SpawnContext & sctx)
|
| 659 | {
|
| 660 | #if CLONE_ENABLED && HAVE_SYS_MOUNT_H
|
| 661 | CloneSpawnContext & ctx = (CloneSpawnContext &) sctx;
|
| 662 | if((ctx.cloneFlags & CLONE_NEWNS) != 0 && ctx.maybeMountDevpts) {
|
| 663 | Path chroot = (ctx.doChroot ? ctx.chrootRootDir : "");
|
| 664 | Path target = chroot + "/dev/pts";
|
| 665 | if(pathExists(chroot + "/dev/ptmx")) return;
|
| 666 | createDirs(target);
|
| 667 | if(mount("none", target.c_str(), "devpts", 0, "newinstance,mode=0620") == -1)
|
| 668 | throw SysError(std::format("mounting `{}'", target));
|
| 669 | createSymlink("/dev/pts/ptmx", chroot + "/dev/ptmx");
|
| 670 | |
| 671 | versions, it is created with permissions 0.
|
| 672 | Path targetPtmx = chroot + "/dev/pts/ptmx";
|
| 673 | if (chmod(targetPtmx.c_str(), 0666) == -1)
|
| 674 | throw SysError(std::format("setting permissions on `{}'", targetPtmx));
|
| 675 | }
|
| 676 | #endif
|
| 677 | }
|
| 678 |
|
| 679 |
|
| 680 | static void pivotRootAction(SpawnContext & sctx)
|
| 681 | {
|
| 682 | #if CLONE_ENABLED && HAVE_SYS_MOUNT_H
|
| 683 | CloneSpawnContext & ctx = (CloneSpawnContext &) sctx;
|
| 684 | if((ctx.cloneFlags & CLONE_NEWNS) != 0 && ctx.doChroot) {
|
| 685 | if (chdir(ctx.chrootRootDir.c_str()) == -1)
|
| 686 | throw SysError(std::format("cannot change directory to '{}'", ctx.chrootRootDir));
|
| 687 |
|
| 688 | if (mkdir("real-root", 0) == -1)
|
| 689 | throw SysError("cannot create real-root directory");
|
| 690 |
|
| 691 | if (pivot_root(".", "real-root") == -1)
|
| 692 | throw SysError(std::format("cannot pivot old root directory onto '{}'", (ctx.chrootRootDir + "/real-root")));
|
| 693 |
|
| 694 | if (chroot(".") == -1)
|
| 695 | throw SysError(std::format("cannot change root directory to '{}'", ctx.chrootRootDir));
|
| 696 |
|
| 697 | if (umount2("real-root", MNT_DETACH) == -1)
|
| 698 | throw SysError("cannot unmount real root filesystem");
|
| 699 |
|
| 700 | if (rmdir("real-root") == -1)
|
| 701 | throw SysError("cannot remove real-root directory");
|
| 702 | }
|
| 703 | #endif
|
| 704 | }
|
| 705 |
|
| 706 |
|
| 707 | static string idMapToIdentityMap(const string & map)
|
| 708 | {
|
| 709 | std::vector<string> mapLines =
|
| 710 | tokenizeString<std::vector<string> >(map, "\n");
|
| 711 | string out;
|
| 712 |
|
| 713 | for(auto & i : mapLines) {
|
| 714 | std::vector<string> elements =
|
| 715 | tokenizeString<std::vector<string> >(i, " ");
|
| 716 | out.append(elements.at(0) + " " + elements.at(0) + " " + elements.at(2) + "\n");
|
| 717 | }
|
| 718 | return out;
|
| 719 | }
|
| 720 |
|
| 721 |
|
| 722 | |
| 723 | * capabilities in the *parent* user namespace, which may not even have any
|
| 724 | * processes in it after unshare is called. So fork a child and have it do
|
| 725 | * the initialization.
|
| 726 | void unshareAndInitUserns(int flags, const string & uidMap,
|
| 727 | const string & gidMap, bool allowSetgroups)
|
| 728 | {
|
| 729 | #if CLONE_ENABLED
|
| 730 | pid_t pid_ = getpid();
|
| 731 | string pid = std::to_string(pid_);
|
| 732 | Pipe toChild;
|
| 733 | Pipe fromChild;
|
| 734 | toChild.create();
|
| 735 | fromChild.create();
|
| 736 | pid_t child = fork();
|
| 737 | if(child == -1)
|
| 738 | throw SysError("creating child process");
|
| 739 | if(child == 0) {
|
| 740 | try {
|
| 741 | toChild.writeSide.close();
|
| 742 | fromChild.readSide.close();
|
| 743 | waitForMessage(toChild.readSide, "ready\n");
|
| 744 | writeFile("/proc/" + pid + "/uid_map", uidMap);
|
| 745 | writeFile("/proc/" + pid + "/setgroups",
|
| 746 | allowSetgroups ? "allow" : "deny");
|
| 747 | writeFile("/proc/" + pid + "/gid_map", gidMap);
|
| 748 | writeFull(fromChild.writeSide, (unsigned char*)"go\n", 3);
|
| 749 | } catch(...) {
|
| 750 | |
| 751 | * immediately.
|
| 752 | _exit(1);
|
| 753 | }
|
| 754 | _exit(EXIT_SUCCESS);
|
| 755 | } else {
|
| 756 | toChild.readSide.close();
|
| 757 | fromChild.writeSide.close();
|
| 758 | if(unshare(flags) == -1)
|
| 759 | throw SysError("unshare");
|
| 760 | writeFull(toChild.writeSide, (unsigned char*)"ready\n", 6);
|
| 761 | waitForMessage(fromChild.readSide, "go\n");
|
| 762 | int status;
|
| 763 | while(waitpid(child, &status, 0) == -1) {
|
| 764 | if(errno != EINTR)
|
| 765 | throw SysError("reaping userns init process");
|
| 766 | }
|
| 767 | if(!(WIFEXITED(status) != 0 && WEXITSTATUS(status) == EXIT_SUCCESS))
|
| 768 | throw Error(std::format("userns init child exited with status {}", WEXITSTATUS(status)));
|
| 769 | }
|
| 770 | #endif
|
| 771 | }
|
| 772 |
|
| 773 |
|
| 774 | static void lockMountsAction(SpawnContext & sctx)
|
| 775 | {
|
| 776 | #if CLONE_ENABLED && HAVE_SYS_MOUNT_H
|
| 777 | CloneSpawnContext & ctx = (CloneSpawnContext &) sctx;
|
| 778 | if(ctx.lockMounts) {
|
| 779 | string uidMap;
|
| 780 | string gidMap;
|
| 781 | if(ctx.lockMountsMapAll) {
|
| 782 | string oldUidMap = readFile("/proc/self/uid_map", true);
|
| 783 | string oldGidMap = readFile("/proc/self/gid_map", true);
|
| 784 | uidMap = idMapToIdentityMap(oldUidMap);
|
| 785 | gidMap = idMapToIdentityMap(oldGidMap);
|
| 786 | } else {
|
| 787 | string uid = std::to_string(getuid());
|
| 788 | string gid = std::to_string(getgid());
|
| 789 | uidMap = uid + " " + uid + " 1";
|
| 790 | gidMap = gid + " " + gid + " 1";
|
| 791 | }
|
| 792 | unshareAndInitUserns(CLONE_NEWNS | CLONE_NEWUSER,
|
| 793 | uidMap, gidMap, ctx.lockMountsAllowSetgroups);
|
| 794 | |
| 795 | together and cannot be separated from within our mount namespace.
|
| 796 | Since umount(2) is documented to fail with EINVAL when attempting
|
| 797 | to unmount one of the mounts that are locked together, check that
|
| 798 | this is what we get.
|
| 799 | int ret = umount("/proc");
|
| 800 | assert(ret == -1 && errno == EINVAL);
|
| 801 | }
|
| 802 | #endif
|
| 803 | }
|
| 804 |
|
| 805 |
|
| 806 | Phases getCloneSpawnPhases()
|
| 807 | {
|
| 808 | #if CLONE_ENABLED
|
| 809 | return { { "reset_writeToStderr", reset_writeToStderrAction },
|
| 810 | { "usernsInitSync", usernsInitSyncAction },
|
| 811 | { "usernsSetIDs", usernsSetIDsAction },
|
| 812 | { "restoreAffinity", restoreAffinityAction },
|
| 813 | { "setsid", setsidAction },
|
| 814 | { "earlyIOSetup", earlyIOSetupAction },
|
| 815 | { "dropAmbientCapabilities", dropAmbientCapabilitiesAction },
|
| 816 | { "initLoopback", initLoopbackAction },
|
| 817 | { "setHostAndDomain", setHostAndDomainAction },
|
| 818 | { "makeFilesystemsPrivate", makeFilesystemsPrivateAction },
|
| 819 | { "makeChrootSeparateFilesystem", makeChrootSeparateFilesystemAction },
|
| 820 | { "mountIntoChroot", mountIntoChrootAction },
|
| 821 | { "mountProc", mountProcAction },
|
| 822 | { "mountDevshm", mountDevshmAction },
|
| 823 | { "mountDevpts", mountDevptsAction },
|
| 824 | { "chroot", pivotRootAction },
|
| 825 | { "chdir", chdirAction },
|
| 826 | { "closeMostFDs", closeMostFDsAction },
|
| 827 | { "setPersonality", setPersonalityAction },
|
| 828 | { "oomSacrifice", oomSacrificeAction },
|
| 829 | |
| 830 | would tend to prevent switching to other ones, but if this
|
| 831 | comes after setIDs then the per-process "dumpable" flag may be
|
| 832 | reset, which will cause /proc/self to become root-owned,
|
| 833 | making /proc/self/uid_map inaccessible. If you need
|
| 834 | lockMounts to preserve the id mappings, and you have the
|
| 835 | necessary capabilities in the parent user namespace, set
|
| 836 | CloneSpawnContext.lockMountsMapAll = true.
|
| 837 | { "lockMounts", lockMountsAction },
|
| 838 | { "setIDs", setIDsAction },
|
| 839 | { "setNoNewPrivs", setNoNewPrivsAction },
|
| 840 | { "addSeccompFilter", addSeccompFilterAction },
|
| 841 | { "restoreSIGPIPE", restoreSIGPIPEAction },
|
| 842 | { "setupSuccess", setupSuccessAction },
|
| 843 | { "exec", execAction }};
|
| 844 | #else
|
| 845 | throw Error("clone not supported on this platform");
|
| 846 | #endif
|
| 847 | }
|
| 848 |
|
| 849 |
|
| 850 | void runChildSetup(SpawnContext & ctx)
|
| 851 | {
|
| 852 | ctx.currentPhase = 0;
|
| 853 | try {
|
| 854 |
|
| 855 | while(true) {
|
| 856 | ctx.phases.at(ctx.currentPhase).action(ctx);
|
| 857 | ctx.currentPhase++;
|
| 858 | }
|
| 859 | } catch (std::exception & e) {
|
| 860 | try {
|
| 861 | writeFull(STDERR_FILENO,
|
| 862 | "while setting up the child process: " +
|
| 863 | (ctx.currentPhase < (ssize_t)ctx.phases.size() ?
|
| 864 | "in phase " + ctx.phases[ctx.currentPhase].label + ": " : "") +
|
| 865 | string(e.what()) + "\n");
|
| 866 | } catch (std::exception & e2) {
|
| 867 | _exit(1);
|
| 868 | }
|
| 869 | _exit(1);
|
| 870 | }
|
| 871 | abort();
|
| 872 | }
|
| 873 |
|
| 874 |
|
| 875 | static int runChildSetupEntry(void *data)
|
| 876 | {
|
| 877 | runChildSetup(* (SpawnContext *)data);
|
| 878 | return 1;
|
| 879 | }
|
| 880 |
|
| 881 |
|
| 882 | int cloneChild(CloneSpawnContext & ctx)
|
| 883 | {
|
| 884 | #if CLONE_ENABLED
|
| 885 | char stack[32 * 1024];
|
| 886 | |
| 887 | bytes.
|
| 888 | char *alignedStack = (char *)(((uintptr_t)stack + sizeof(stack) - 8) & ~(uintptr_t)0xf);
|
| 889 | int ret = clone(runChildSetupEntry, alignedStack, ctx.cloneFlags, (void *) &ctx);
|
| 890 | if(ret == -1)
|
| 891 | throw SysError("clone");
|
| 892 | return ret;
|
| 893 | #else
|
| 894 | throw Error("clone not supported on this platform");
|
| 895 | #endif
|
| 896 | }
|
| 897 |
|
| 898 |
|
| 899 | }
|