Support sandbox builds by non-root users
This allows an unprivileged user to perform builds on a diverted store
(i.e. where the physical store location differs from the logical
location).
Example:
  $ NIX_LOG_DIR=/tmp/log NIX_REMOTE="local?real=/tmp/store&state=/tmp/var" nix-build -E \
    'with import <nixpkgs> {}; runCommand "foo" { buildInputs = [procps nettools]; } "id; ps; ifconfig; echo $out > $out"'
will do a build in the Nix store physically in /tmp/store but
logically in /nix/store (and thus using substituters for the latter).
			
			
This commit is contained in:
		
							parent
							
								
									2f8b0e557b
								
							
						
					
					
						commit
						5e51ffb1c2
					
				
					 3 changed files with 69 additions and 53 deletions
				
			
		|  | @ -436,12 +436,11 @@ private: | |||
|     AutoCloseFD fdUserLock; | ||||
| 
 | ||||
|     string user; | ||||
|     uid_t uid; | ||||
|     gid_t gid; | ||||
|     uid_t uid = 0; | ||||
|     gid_t gid = 0; | ||||
|     std::vector<gid_t> supplementaryGIDs; | ||||
| 
 | ||||
| public: | ||||
|     UserLock(); | ||||
|     ~UserLock(); | ||||
| 
 | ||||
|     void acquire(); | ||||
|  | @ -450,8 +449,8 @@ public: | |||
|     void kill(); | ||||
| 
 | ||||
|     string getUser() { return user; } | ||||
|     uid_t getUID() { return uid; } | ||||
|     uid_t getGID() { return gid; } | ||||
|     uid_t getUID() { assert(uid); return uid; } | ||||
|     uid_t getGID() { assert(gid); return gid; } | ||||
|     std::vector<gid_t> getSupplementaryGIDs() { return supplementaryGIDs; } | ||||
| 
 | ||||
|     bool enabled() { return uid != 0; } | ||||
|  | @ -462,12 +461,6 @@ public: | |||
| PathSet UserLock::lockedPaths; | ||||
| 
 | ||||
| 
 | ||||
| UserLock::UserLock() | ||||
| { | ||||
|     uid = gid = 0; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| UserLock::~UserLock() | ||||
| { | ||||
|     release(); | ||||
|  | @ -768,6 +761,9 @@ private: | |||
|     /* Whether this is a fixed-output derivation. */ | ||||
|     bool fixedOutput; | ||||
| 
 | ||||
|     /* Whether to run the build in a private network namespace. */ | ||||
|     bool privateNetwork = false; | ||||
| 
 | ||||
|     typedef void (DerivationGoal::*GoalState)(); | ||||
|     GoalState state; | ||||
| 
 | ||||
|  | @ -1269,16 +1265,13 @@ void DerivationGoal::tryToBuild() | |||
| { | ||||
|     trace("trying to build"); | ||||
| 
 | ||||
|     if (worker.store.storeDir != worker.store.realStoreDir) | ||||
|         throw Error("building with a diverted Nix store is not supported"); | ||||
| 
 | ||||
|     /* Check for the possibility that some other goal in this process
 | ||||
|        has locked the output since we checked in haveDerivation(). | ||||
|        (It can't happen between here and the lockPaths() call below | ||||
|        because we're not allowing multi-threading.)  If so, put this | ||||
|        goal to sleep until another goal finishes, then try again. */ | ||||
|     for (auto & i : drv->outputs) | ||||
|         if (pathIsLockedByMe(i.second.path)) { | ||||
|         if (pathIsLockedByMe(worker.store.toRealPath(i.second.path))) { | ||||
|             debug(format("putting derivation ‘%1%’ to sleep because ‘%2%’ is locked by another goal") | ||||
|                 % drvPath % i.second.path); | ||||
|             worker.waitForAnyGoal(shared_from_this()); | ||||
|  | @ -1290,7 +1283,11 @@ void DerivationGoal::tryToBuild() | |||
|        can't acquire the lock, then continue; hopefully some other | ||||
|        goal can start a build, and if not, the main loop will sleep a | ||||
|        few seconds and then retry this goal. */ | ||||
|     if (!outputLocks.lockPaths(drv->outputPaths(), "", false)) { | ||||
|     PathSet lockFiles; | ||||
|     for (auto & outPath : drv->outputPaths()) | ||||
|         lockFiles.insert(worker.store.toRealPath(outPath)); | ||||
| 
 | ||||
|     if (!outputLocks.lockPaths(lockFiles, "", false)) { | ||||
|         worker.waitForAWhile(shared_from_this()); | ||||
|         return; | ||||
|     } | ||||
|  | @ -1320,7 +1317,7 @@ void DerivationGoal::tryToBuild() | |||
|         Path path = i.second.path; | ||||
|         if (worker.store.isValidPath(path)) continue; | ||||
|         debug(format("removing invalid path ‘%1%’") % path); | ||||
|         deletePath(path); | ||||
|         deletePath(worker.store.toRealPath(path)); | ||||
|     } | ||||
| 
 | ||||
|     /* Don't do a remote build if the derivation has the attribute
 | ||||
|  | @ -1445,7 +1442,7 @@ void DerivationGoal::buildDone() | |||
| #if HAVE_STATVFS | ||||
|             unsigned long long required = 8ULL * 1024 * 1024; // FIXME: make configurable
 | ||||
|             struct statvfs st; | ||||
|             if (statvfs(worker.store.storeDir.c_str(), &st) == 0 && | ||||
|             if (statvfs(worker.store.realStoreDir.c_str(), &st) == 0 && | ||||
|                 (unsigned long long) st.f_bavail * st.f_bsize < required) | ||||
|                 diskFull = true; | ||||
|             if (statvfs(tmpDir.c_str(), &st) == 0 && | ||||
|  | @ -1683,6 +1680,9 @@ void DerivationGoal::startBuilder() | |||
|             useChroot = !fixedOutput && get(drv->env, "__noChroot") != "1"; | ||||
|     } | ||||
| 
 | ||||
|     if (worker.store.storeDir != worker.store.realStoreDir) | ||||
|         useChroot = true; | ||||
| 
 | ||||
|     /* Construct the environment passed to the builder. */ | ||||
|     env.clear(); | ||||
| 
 | ||||
|  | @ -1819,10 +1819,8 @@ void DerivationGoal::startBuilder() | |||
| 
 | ||||
|     /* If `build-users-group' is not empty, then we have to build as
 | ||||
|        one of the members of that group. */ | ||||
|     if (settings.buildUsersGroup != "") { | ||||
|     if (settings.buildUsersGroup != "" && getuid() == 0) { | ||||
|         buildUser.acquire(); | ||||
|         assert(buildUser.getUID() != 0); | ||||
|         assert(buildUser.getGID() != 0); | ||||
| 
 | ||||
|         /* Make sure that no other processes are executing under this
 | ||||
|            uid. */ | ||||
|  | @ -1906,7 +1904,7 @@ void DerivationGoal::startBuilder() | |||
|            environment using bind-mounts.  We put it in the Nix store | ||||
|            to ensure that we can create hard-links to non-directory | ||||
|            inputs in the fake Nix store in the chroot (see below). */ | ||||
|         chrootRootDir = drvPath + ".chroot"; | ||||
|         chrootRootDir = worker.store.toRealPath(drvPath) + ".chroot"; | ||||
|         deletePath(chrootRootDir); | ||||
| 
 | ||||
|         /* Clean up the chroot directory automatically. */ | ||||
|  | @ -1917,7 +1915,7 @@ void DerivationGoal::startBuilder() | |||
|         if (mkdir(chrootRootDir.c_str(), 0750) == -1) | ||||
|             throw SysError(format("cannot create ‘%1%’") % chrootRootDir); | ||||
| 
 | ||||
|         if (chown(chrootRootDir.c_str(), 0, buildUser.getGID()) == -1) | ||||
|         if (buildUser.enabled() && chown(chrootRootDir.c_str(), 0, buildUser.getGID()) == -1) | ||||
|             throw SysError(format("cannot change ownership of ‘%1%’") % chrootRootDir); | ||||
| 
 | ||||
|         /* Create a writable /tmp in the chroot.  Many builders need
 | ||||
|  | @ -1960,18 +1958,19 @@ void DerivationGoal::startBuilder() | |||
|         createDirs(chrootStoreDir); | ||||
|         chmod_(chrootStoreDir, 01775); | ||||
| 
 | ||||
|         if (chown(chrootStoreDir.c_str(), 0, buildUser.getGID()) == -1) | ||||
|         if (buildUser.enabled() && chown(chrootStoreDir.c_str(), 0, buildUser.getGID()) == -1) | ||||
|             throw SysError(format("cannot change ownership of ‘%1%’") % chrootStoreDir); | ||||
| 
 | ||||
|         for (auto & i : inputPaths) { | ||||
|             Path r = worker.store.toRealPath(i); | ||||
|             struct stat st; | ||||
|             if (lstat(i.c_str(), &st)) | ||||
|             if (lstat(r.c_str(), &st)) | ||||
|                 throw SysError(format("getting attributes of path ‘%1%’") % i); | ||||
|             if (S_ISDIR(st.st_mode)) | ||||
|                 dirsInChroot[i] = i; | ||||
|                 dirsInChroot[i] = r; | ||||
|             else { | ||||
|                 Path p = chrootRootDir + i; | ||||
|                 if (link(i.c_str(), p.c_str()) == -1) { | ||||
|                 if (link(r.c_str(), p.c_str()) == -1) { | ||||
|                     /* Hard-linking fails if we exceed the maximum
 | ||||
|                        link count on a file (e.g. 32000 of ext3), | ||||
|                        which is quite possible after a `nix-store | ||||
|  | @ -1979,7 +1978,7 @@ void DerivationGoal::startBuilder() | |||
|                     if (errno != EMLINK) | ||||
|                         throw SysError(format("linking ‘%1%’ to ‘%2%’") % p % i); | ||||
|                     StringSink sink; | ||||
|                     dumpPath(i, sink); | ||||
|                     dumpPath(r, sink); | ||||
|                     StringSource source(*sink.s); | ||||
|                     restorePath(p, source); | ||||
|                 } | ||||
|  | @ -2112,6 +2111,10 @@ void DerivationGoal::startBuilder() | |||
|            CLONE_PARENT to ensure that the real builder is parented to | ||||
|            us. | ||||
|         */ | ||||
| 
 | ||||
|         if (!fixedOutput) | ||||
|             privateNetwork = true; | ||||
| 
 | ||||
|         ProcessOptions options; | ||||
|         options.allowVfork = false; | ||||
|         Pid helper = startProcess([&]() { | ||||
|  | @ -2120,7 +2123,10 @@ void DerivationGoal::startBuilder() | |||
|                 PROT_WRITE | PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0); | ||||
|             if (stack == MAP_FAILED) throw SysError("allocating stack"); | ||||
|             int flags = CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWIPC | CLONE_NEWUTS | CLONE_PARENT | SIGCHLD; | ||||
|             if (!fixedOutput) flags |= CLONE_NEWNET; | ||||
|             if (getuid() != 0) | ||||
|                 flags |= CLONE_NEWUSER; | ||||
|             if (privateNetwork) | ||||
|                 flags |= CLONE_NEWNET; | ||||
|             pid_t child = clone(childEntry, stack + stackSize, flags, this); | ||||
|             if (child == -1 && errno == EINVAL) | ||||
|                 /* Fallback for Linux < 2.13 where CLONE_NEWPID and
 | ||||
|  | @ -2174,6 +2180,8 @@ void DerivationGoal::runChild() | |||
| #if __linux__ | ||||
|         if (useChroot) { | ||||
| 
 | ||||
|             if (privateNetwork) { | ||||
| 
 | ||||
|                 /* Initialise the loopback interface. */ | ||||
|                 AutoCloseFD fd(socket(PF_INET, SOCK_DGRAM, IPPROTO_IP)); | ||||
|                 if (fd == -1) throw SysError("cannot open IP socket"); | ||||
|  | @ -2185,6 +2193,7 @@ void DerivationGoal::runChild() | |||
|                     throw SysError("cannot set loopback interface flags"); | ||||
| 
 | ||||
|                 fd.close(); | ||||
|             } | ||||
| 
 | ||||
|             /* Set the hostname etc. to fixed values. */ | ||||
|             char hostname[] = "localhost"; | ||||
|  | @ -2266,7 +2275,7 @@ void DerivationGoal::runChild() | |||
|                     createDirs(dirOf(target)); | ||||
|                     writeFile(target, ""); | ||||
|                 } | ||||
|                 if (mount(source.c_str(), target.c_str(), "", MS_BIND, 0) == -1) | ||||
|                 if (mount(source.c_str(), target.c_str(), "", MS_BIND | MS_REC, 0) == -1) | ||||
|                     throw SysError(format("bind mount from ‘%1%’ to ‘%2%’ failed") % source % target); | ||||
|             } | ||||
| 
 | ||||
|  | @ -2284,7 +2293,8 @@ void DerivationGoal::runChild() | |||
|                requires the kernel to be compiled with | ||||
|                CONFIG_DEVPTS_MULTIPLE_INSTANCES=y (which is the case | ||||
|                if /dev/ptx/ptmx exists). */ | ||||
|             if (pathExists("/dev/pts/ptmx") && | ||||
|             if (getuid() == 0 && | ||||
|                 pathExists("/dev/pts/ptmx") && | ||||
|                 !pathExists(chrootRootDir + "/dev/ptmx") | ||||
|                 && dirsInChroot.find("/dev/pts") == dirsInChroot.end()) | ||||
|             { | ||||
|  | @ -2587,10 +2597,10 @@ void DerivationGoal::registerOutputs() | |||
|                 if (buildMode == bmRepair) | ||||
|                     replaceValidPath(path, actualPath); | ||||
|                 else | ||||
|                     if (buildMode != bmCheck && rename(actualPath.c_str(), path.c_str()) == -1) | ||||
|                     if (buildMode != bmCheck && rename(actualPath.c_str(), worker.store.toRealPath(path).c_str()) == -1) | ||||
|                         throw SysError(format("moving build output ‘%1%’ from the sandbox to the Nix store") % path); | ||||
|             } | ||||
|             if (buildMode != bmCheck) actualPath = path; | ||||
|             if (buildMode != bmCheck) actualPath = worker.store.toRealPath(path); | ||||
|         } else { | ||||
|             Path redirected = redirectedOutputs[path]; | ||||
|             if (buildMode == bmRepair | ||||
|  | @ -2641,8 +2651,6 @@ void DerivationGoal::registerOutputs() | |||
|             rewritten = true; | ||||
|         } | ||||
| 
 | ||||
|         Activity act(*logger, lvlTalkative, format("scanning for references inside ‘%1%’") % path); | ||||
| 
 | ||||
|         /* Check that fixed-output derivations produced the right
 | ||||
|            outputs (i.e., the content hash should match the specified | ||||
|            hash). */ | ||||
|  | @ -2668,13 +2676,15 @@ void DerivationGoal::registerOutputs() | |||
|                     % dest % printHashType(ht) % printHash16or32(h2)); | ||||
|                 if (worker.store.isValidPath(dest)) | ||||
|                     return; | ||||
|                 if (actualPath != dest) { | ||||
|                     PathLocks outputLocks({dest}); | ||||
|                     deletePath(dest); | ||||
|                     if (rename(actualPath.c_str(), dest.c_str()) == -1) | ||||
|                 Path actualDest = worker.store.toRealPath(dest); | ||||
|                 if (actualPath != actualDest) { | ||||
|                     PathLocks outputLocks({actualDest}); | ||||
|                     deletePath(actualDest); | ||||
|                     if (rename(actualPath.c_str(), actualDest.c_str()) == -1) | ||||
|                         throw SysError(format("moving ‘%1%’ to ‘%2%’") % actualPath % dest); | ||||
|                 } | ||||
|                 path = actualPath = dest; | ||||
|                 path = dest; | ||||
|                 actualPath = actualDest; | ||||
|             } else { | ||||
|                 if (h != h2) | ||||
|                     throw BuildError( | ||||
|  | @ -2692,6 +2702,7 @@ void DerivationGoal::registerOutputs() | |||
|            contained in it.  Compute the SHA-256 NAR hash at the same | ||||
|            time.  The hash is stored in the database so that we can | ||||
|            verify later on whether nobody has messed with the store. */ | ||||
|         Activity act(*logger, lvlTalkative, format("scanning for references inside ‘%1%’") % path); | ||||
|         HashResult hash; | ||||
|         PathSet references = scanForReferences(actualPath, allPaths, hash); | ||||
| 
 | ||||
|  | @ -2700,7 +2711,7 @@ void DerivationGoal::registerOutputs() | |||
|             auto info = *worker.store.queryPathInfo(path); | ||||
|             if (hash.first != info.narHash) { | ||||
|                 if (settings.keepFailed) { | ||||
|                     Path dst = path + checkSuffix; | ||||
|                     Path dst = worker.store.toRealPath(path + checkSuffix); | ||||
|                     deletePath(dst); | ||||
|                     if (rename(actualPath.c_str(), dst.c_str())) | ||||
|                         throw SysError(format("renaming ‘%1%’ to ‘%2%’") % actualPath % dst); | ||||
|  | @ -2743,7 +2754,7 @@ void DerivationGoal::registerOutputs() | |||
|                 /* Our requisites are the union of the closures of our references. */ | ||||
|                 for (auto & i : references) | ||||
|                     /* Don't call computeFSClosure on ourselves. */ | ||||
|                     if (actualPath != i) | ||||
|                     if (path != i) | ||||
|                         worker.store.computeFSClosure(i, used); | ||||
|             } else | ||||
|                 used = references; | ||||
|  | @ -2775,8 +2786,7 @@ void DerivationGoal::registerOutputs() | |||
|         checkRefs("disallowedRequisites", false, true); | ||||
| 
 | ||||
|         if (curRound == nrRounds) { | ||||
|             worker.store.optimisePath(path); // FIXME: combine with scanForReferences()
 | ||||
| 
 | ||||
|             worker.store.optimisePath(actualPath); // FIXME: combine with scanForReferences()
 | ||||
|             worker.markContentsGood(path); | ||||
|         } | ||||
| 
 | ||||
|  |  | |||
|  | @ -39,6 +39,7 @@ public: | |||
| }; | ||||
| 
 | ||||
| 
 | ||||
| // FIXME: not thread-safe!
 | ||||
| bool pathIsLockedByMe(const Path & path); | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -503,6 +503,11 @@ public: | |||
|         const Path & gcRoot, bool indirect, bool allowOutsideRootsDir = false); | ||||
| 
 | ||||
|     virtual Path getRealStoreDir() { return storeDir; } | ||||
| 
 | ||||
|     Path toRealPath(const Path & storePath) | ||||
|     { | ||||
|         return getRealStoreDir() + "/" + baseNameOf(storePath); | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue