about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorDaniel Peebles <pumpkin@me.com>2015-01-06T06·27-0500
committerEelco Dolstra <eelco.dolstra@logicblox.com>2015-01-12T11·00+0100
commitf1151a3373c1df77255cb63a01daf29fd093d690 (patch)
tree9ae1f2e946028e2af602d85ff51983f4a07fd796 /src
parentc23d67920ec1d96b930106b256e37b75e6151a19 (diff)
Add basic Apple sandbox support
Diffstat (limited to 'src')
-rw-r--r--src/libstore/build.cc186
1 files changed, 169 insertions, 17 deletions
diff --git a/src/libstore/build.cc b/src/libstore/build.cc
index 5817611d4e0c..2bd0d2030689 100644
--- a/src/libstore/build.cc
+++ b/src/libstore/build.cc
@@ -50,6 +50,15 @@
 
 #define CHROOT_ENABLED HAVE_CHROOT && HAVE_UNSHARE && HAVE_SYS_MOUNT_H && defined(MS_BIND) && defined(MS_PRIVATE) && defined(CLONE_NEWNS)
 
+/* chroot-like behavior from Apple's sandbox */
+#if __APPLE__
+    #define SANDBOX_ENABLED 1
+    #define DEFAULT_ALLOWED_IMPURE_PREFIXES "/System/Library/Frameworks /usr/lib /dev /bin/sh"
+#else
+    #define SANDBOX_ENABLED 0
+    #define DEFAULT_ALLOWED_IMPURE_PREFIXES ""
+#endif
+
 #if CHROOT_ENABLED
 #include <sys/socket.h>
 #include <sys/ioctl.h>
@@ -1752,6 +1761,44 @@ void DerivationGoal::startBuilder()
     if (get(drv.env, "__noChroot") == "1") useChroot = false;
 
     if (useChroot) {
+        /* Allow a user-configurable set of directories from the
+           host file system. */
+        PathSet dirs = tokenizeString<StringSet>(settings.get("build-chroot-dirs", string(DEFAULT_CHROOT_DIRS)));
+        PathSet dirs2 = tokenizeString<StringSet>(settings.get("build-extra-chroot-dirs", string("")));
+        dirs.insert(dirs2.begin(), dirs2.end());
+
+        for (auto & i : dirs) {
+            size_t p = i.find('=');
+            if (p == string::npos)
+                dirsInChroot[i] = i;
+            else
+                dirsInChroot[string(i, 0, p)] = string(i, p + 1);
+        }
+        dirsInChroot[tmpDir] = tmpDir;
+
+        string allowed = settings.get("allowed-impure-host-deps", string(DEFAULT_ALLOWED_IMPURE_PREFIXES));
+        PathSet allowedPaths = tokenizeString<StringSet>(allowed);
+
+        /* This works like the above, except on a per-derivation level */
+        Strings impurePaths = tokenizeString<Strings>(get(drv.env, "__impureHostDeps"));
+
+        for (auto & i : impurePaths) {
+            bool found = false;
+            Path canonI = canonPath(i, true);
+            /* If only we had a trie to do this more efficiently :) luckily, these are generally going to be pretty small */
+            for (auto & a : allowedPaths) {
+                Path canonA = canonPath(a, true);
+                if (canonI == canonA || isInDir(canonI, canonA)) {
+                    found = true;
+                    break;
+                }
+            }
+            if (!found)
+                throw SysError(format("derivation '%1%' requested impure path ‘%2%’, but it was not in allowed-impure-host-deps (‘%3%’)") % drvPath % i % allowed);
+
+            dirsInChroot[i] = i;
+        }
+
 #if CHROOT_ENABLED
         /* Create a temporary directory in which we set up the chroot
            environment using bind-mounts.  We put it in the Nix store
@@ -1793,20 +1840,6 @@ void DerivationGoal::startBuilder()
         /* Create /etc/hosts with localhost entry. */
         writeFile(chrootRootDir + "/etc/hosts", "127.0.0.1 localhost\n");
 
-        /* Bind-mount a user-configurable set of directories from the
-           host file system. */
-        PathSet dirs = tokenizeString<StringSet>(settings.get("build-chroot-dirs", string(DEFAULT_CHROOT_DIRS)));
-        PathSet dirs2 = tokenizeString<StringSet>(settings.get("build-extra-chroot-dirs", string("")));
-        dirs.insert(dirs2.begin(), dirs2.end());
-        for (auto & i : dirs) {
-            size_t p = i.find('=');
-            if (p == string::npos)
-                dirsInChroot[i] = i;
-            else
-                dirsInChroot[string(i, 0, p)] = string(i, p + 1);
-        }
-        dirsInChroot[tmpDir] = tmpDir;
-
         /* Make the closure of the inputs available in the chroot,
            rather than the whole Nix store.  This prevents any access
            to undeclared dependencies.  Directories are bind-mounted,
@@ -1850,6 +1883,9 @@ void DerivationGoal::startBuilder()
             foreach (DerivationOutputs::iterator, i, drv.outputs)
                 dirsInChroot.erase(i->second.path);
 
+#elif SANDBOX_ENABLED
+        /* We don't really have any parent prep work to do (yet?)
+           All work happens in the child, instead. */
 #else
         throw Error("chroot builds are not supported on this platform");
 #endif
@@ -2156,8 +2192,118 @@ void DerivationGoal::runChild()
 
         /* Fill in the arguments. */
         Strings args;
-        string builderBasename = baseNameOf(drv.builder);
-        args.push_back(builderBasename);
+
+        const char *builder = "invalid";
+
+        string sandboxProfile;
+        if (useChroot && SANDBOX_ENABLED) {
+            /* Lots and lots and lots of file functions freak out if they can't stat their full ancestry */
+            PathSet ancestry;
+
+            /* We build the ancestry before adding all inputPaths to the store because we know they'll
+               all have the same parents (the store), and there might be lots of inputs. This isn't
+               particularly efficient... I doubt it'll be a bottleneck in practice */
+            for (auto & i : dirsInChroot) {
+                Path cur = i.first;
+                while (cur.compare("/") != 0) {
+                    cur = dirOf(cur);
+                    ancestry.insert(cur);
+                }
+            }
+
+            /* And we want the store in there regardless of how empty dirsInChroot. We include the innermost
+               path component this time, since it's typically /nix/store and we care about that. */
+            Path cur = settings.nixStore;
+            while (cur.compare("/") != 0) {
+                ancestry.insert(cur);
+                cur = dirOf(cur);
+            }
+
+            /* Add all our input paths to the chroot */
+            for (auto & i : inputPaths)
+                dirsInChroot[i] = i;
+
+
+            /* TODO: we should factor out the policy cleanly, so we don't have to repeat the constants every time... */
+            sandboxProfile += "(version 1)\n";
+
+            /* Violations will go to the syslog if you set this. Unfortunately the destination does not appear to be configurable */
+            if (settings.get("darwin-log-sandbox-violations", false)) {
+                sandboxProfile += "(deny default)\n";
+            } else {
+                sandboxProfile += "(deny default (with no-log))\n";
+            }
+
+            sandboxProfile += "(allow file-read* file-write-data (literal \"/dev/null\"))\n";
+
+            sandboxProfile += "(allow file-read-metadata\n"
+                "\t(literal \"/var\")\n"
+                "\t(literal \"/tmp\")\n"
+                "\t(literal \"/etc\")\n"
+                "\t(literal \"/etc/nix\")\n"
+                "\t(literal \"/etc/nix/nix.conf\"))\n";
+
+            /* The tmpDir in scope points at the temporary build directory for our derivation. Some packages try different mechanisms
+               to find temporary directories, so we want to open up a broader place for them to dump their files, if needed. */
+            Path globalTmpDir = canonPath(getEnv("TMPDIR", "/tmp"), true);
+
+            /* They don't like trailing slashes on subpath directives */
+            if (globalTmpDir.back() == '/') globalTmpDir.pop_back();
+
+            /* This is where our temp folders are and where most of the building will happen, so we want rwx on it. */
+            sandboxProfile += (format("(allow file-read* file-write* process-exec (subpath \"%1%\") (subpath \"/private/tmp\"))\n") % globalTmpDir).str();
+
+            sandboxProfile += "(allow process-fork)\n";
+            sandboxProfile += "(allow sysctl-read)\n";
+            sandboxProfile += "(allow signal (target same-sandbox))\n";
+
+            /* Enables getpwuid (used by git and others) */
+            sandboxProfile += "(allow mach-lookup (global-name \"com.apple.system.notification_center\") (global-name \"com.apple.system.opendirectoryd.libinfo\"))\n";
+
+
+            /* Our rwx outputs */
+            sandboxProfile += "(allow file-read* file-write* process-exec\n";
+            for (auto & i : missingPaths) {
+                sandboxProfile += (format("\t(subpath \"%1%\")\n") % i.c_str()).str();
+            }
+            sandboxProfile += ")\n";
+
+            /* Our inputs (transitive dependencies and any impurities computed above) */
+            sandboxProfile += "(allow file-read* process-exec\n";
+            for (auto & i : dirsInChroot) {
+                if (i.first != i.second)
+                    throw SysError(format("can't map '%1%' to '%2%': mismatched impure paths not supported on darwin"));
+
+                string path = i.first;
+                struct stat st;
+                if (lstat(path.c_str(), &st))
+                    throw SysError(format("getting attributes of path ‘%1%’") % path);
+                if (S_ISDIR(st.st_mode))
+                    sandboxProfile += (format("\t(subpath \"%1%\")\n") % path).str();
+                else
+                    sandboxProfile += (format("\t(literal \"%1%\")\n") % path).str();
+            }
+            sandboxProfile += ")\n";
+
+            /* Our ancestry. N.B: this uses literal on folders, instead of subpath. Without that,
+               you open up the entire filesystem because you end up with (subpath "/") */
+            sandboxProfile += "(allow file-read-metadata\n";
+            for (auto & i : ancestry) {
+                sandboxProfile += (format("\t(literal \"%1%\")\n") % i.c_str()).str();
+            }
+            sandboxProfile += ")\n";
+
+            builder = "/usr/bin/sandbox-exec";
+            args.push_back("sandbox-exec");
+            args.push_back("-p");
+            args.push_back(sandboxProfile);
+            args.push_back(drv.builder);
+        } else {
+            builder = drv.builder.c_str();
+            string builderBasename = baseNameOf(drv.builder);
+            args.push_back(builderBasename);
+        }
+
         foreach (Strings::iterator, i, drv.args)
             args.push_back(rewriteHashes(*i, rewritesToTmp));
         auto argArr = stringsToCharPtrs(args);
@@ -2167,8 +2313,14 @@ void DerivationGoal::runChild()
         /* Indicate that we managed to set up the build environment. */
         writeFull(STDERR_FILENO, "\n");
 
+        /* This needs to be after that fateful '\n', and I didn't want to duplicate code */
+        if (useChroot && SANDBOX_ENABLED) {
+            printMsg(lvlDebug, "Generated sandbox profile:");
+            printMsg(lvlDebug, sandboxProfile);
+        }
+
         /* Execute the program.  This should not return. */
-        execve(drv.builder.c_str(), (char * *) &argArr[0], (char * *) &envArr[0]);
+        execve(builder, (char * *) &argArr[0], (char * *) &envArr[0]);
 
         throw SysError(format("executing ‘%1%’") % drv.builder);