Add basic Apple sandbox support
This commit is contained in:
parent
c23d67920e
commit
f1151a3373
1 changed files with 169 additions and 17 deletions
|
@ -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);
|
||||
|
||||
|
|
Loading…
Reference in a new issue