diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/download-via-ssh/download-via-ssh.cc | 1 | ||||
-rw-r--r-- | src/libexpr/eval.cc | 3 | ||||
-rw-r--r-- | src/libexpr/eval.hh | 10 | ||||
-rw-r--r-- | src/libexpr/lexer.l | 2 | ||||
-rw-r--r-- | src/libexpr/nixexpr.cc | 2 | ||||
-rw-r--r-- | src/libexpr/parser.y | 16 | ||||
-rw-r--r-- | src/libexpr/primops.cc | 8 | ||||
-rw-r--r-- | src/libmain/shared.cc | 10 | ||||
-rw-r--r-- | src/libstore/build.cc | 361 | ||||
-rw-r--r-- | src/libstore/local-store.cc | 25 | ||||
-rw-r--r-- | src/libstore/optimise-store.cc | 1 | ||||
-rw-r--r-- | src/libstore/remote-store.cc | 1 | ||||
-rw-r--r-- | src/libutil/hash.cc | 99 | ||||
-rw-r--r-- | src/libutil/monitor-fd.hh | 1 | ||||
-rw-r--r-- | src/libutil/util.cc | 90 | ||||
-rw-r--r-- | src/libutil/util.hh | 7 | ||||
-rw-r--r-- | src/nix-env/nix-env.cc | 4 | ||||
-rw-r--r-- | src/nix-store/local.mk | 2 | ||||
-rw-r--r-- | src/nix-store/nix-store.cc | 38 |
19 files changed, 491 insertions, 190 deletions
diff --git a/src/download-via-ssh/download-via-ssh.cc b/src/download-via-ssh/download-via-ssh.cc index b64455eb1724..f71cf56507b8 100644 --- a/src/download-via-ssh/download-via-ssh.cc +++ b/src/download-via-ssh/download-via-ssh.cc @@ -9,6 +9,7 @@ #include "store-api.hh" #include <iostream> +#include <cstdlib> #include <unistd.h> using namespace nix; diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index 298f6a3a60e3..95b56e84d89a 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -1174,7 +1174,8 @@ void ExprConcatStrings::eval(EvalState & state, Env & env, Value & v) else if (firstType == tPath) { if (!context.empty()) throwEvalError("a string that refers to a store path cannot be appended to a path, at %1%", pos); - mkPath(v, s.str().c_str()); + auto path = canonPath(s.str()); + mkPath(v, path.c_str()); } else mkString(v, s.str(), context); } diff --git a/src/libexpr/eval.hh b/src/libexpr/eval.hh index 78942927fd24..f7415fb78dfd 100644 --- a/src/libexpr/eval.hh +++ b/src/libexpr/eval.hh @@ -39,10 +39,10 @@ public: typedef uint32_t size_t; private: - size_t size_, capacity; + size_t size_, capacity_; Attr attrs[0]; - Bindings(uint32_t capacity) : size_(0), capacity(capacity) { } + Bindings(size_t capacity) : size_(0), capacity_(capacity) { } Bindings(const Bindings & bindings) = delete; public: @@ -54,7 +54,7 @@ public: void push_back(const Attr & attr) { - assert(size_ < capacity); + assert(size_ < capacity_); attrs[size_++] = attr; } @@ -76,6 +76,8 @@ public: void sort(); + size_t capacity() { return capacity_; } + friend class EvalState; }; @@ -169,7 +171,7 @@ public: /* Look up a file in the search path. */ Path findFile(const string & path); - Path findFile(SearchPath & searchPath, const string & path); + Path findFile(SearchPath & searchPath, const string & path, const Pos & pos = noPos); /* Evaluate an expression to normal form, storing the result in value `v'. */ diff --git a/src/libexpr/lexer.l b/src/libexpr/lexer.l index 82520ee7a59a..7051909008d1 100644 --- a/src/libexpr/lexer.l +++ b/src/libexpr/lexer.l @@ -80,6 +80,7 @@ static Expr * unescapeStr(SymbolTable & symbols, const char * s) ID [a-zA-Z\_][a-zA-Z0-9\_\'\-]* INT [0-9]+ PATH [a-zA-Z0-9\.\_\-\+]*(\/[a-zA-Z0-9\.\_\-\+]+)+ +HPATH \~(\/[a-zA-Z0-9\.\_\-\+]+)+ SPATH \<[a-zA-Z0-9\.\_\-\+]+(\/[a-zA-Z0-9\.\_\-\+]+)*\> URI [a-zA-Z][a-zA-Z0-9\+\-\.]*\:[a-zA-Z0-9\%\/\?\:\@\&\=\+\$\,\-\_\.\!\~\*\']+ @@ -159,6 +160,7 @@ or { return OR_KW; } <IND_STRING>. return yytext[0]; /* just in case: shouldn't be reached */ {PATH} { yylval->path = strdup(yytext); return PATH; } +{HPATH} { yylval->path = strdup(yytext); return HPATH; } {SPATH} { yylval->path = strdup(yytext); return SPATH; } {URI} { yylval->uri = strdup(yytext); return URI; } diff --git a/src/libexpr/nixexpr.cc b/src/libexpr/nixexpr.cc index 6945e4daed80..50997e096fd1 100644 --- a/src/libexpr/nixexpr.cc +++ b/src/libexpr/nixexpr.cc @@ -191,7 +191,7 @@ std::ostream & operator << (std::ostream & str, const Pos & pos) if (!pos) str << "undefined position"; else - str << (format(ANSI_BOLD "%1%" ANSI_NORMAL ":%2%:%3%") % pos.file % pos.line % pos.column).str(); + str << (format(ANSI_BOLD "%1%" ANSI_NORMAL ":%2%:%3%") % (string) pos.file % pos.line % pos.column).str(); return str; } diff --git a/src/libexpr/parser.y b/src/libexpr/parser.y index dcb270b862a3..d70d29be8ba7 100644 --- a/src/libexpr/parser.y +++ b/src/libexpr/parser.y @@ -53,10 +53,6 @@ namespace nix { #include "parser-tab.hh" #include "lexer-tab.hh" -#include <stdio.h> -#include <stdlib.h> -#include <string.h> - YY_DECL; using namespace nix; @@ -272,7 +268,7 @@ void yyerror(YYLTYPE * loc, yyscan_t scanner, ParseData * data, const char * err %token <id> ID ATTRPATH %token <e> STR IND_STR %token <n> INT -%token <path> PATH SPATH +%token <path> PATH HPATH SPATH %token <uri> URI %token IF THEN ELSE ASSERT WITH LET IN REC INHERIT EQ NEQ AND OR IMPL OR_KW %token DOLLAR_CURLY /* == ${ */ @@ -290,7 +286,6 @@ void yyerror(YYLTYPE * loc, yyscan_t scanner, ParseData * data, const char * err %left '*' '/' %right CONCAT %nonassoc '?' -%nonassoc '~' %nonassoc NEGATE %% @@ -380,6 +375,7 @@ expr_simple $$ = stripIndentation(CUR_POS, data->symbols, *$2); } | PATH { $$ = new ExprPath(absPath($1, data->basePath)); } + | HPATH { $$ = new ExprPath(getEnv("HOME", "") + string{$1 + 1}); } | SPATH { string path($1 + 1, strlen($1) - 2); $$ = new ExprApp(CUR_POS, @@ -630,7 +626,7 @@ Path EvalState::findFile(const string & path) } -Path EvalState::findFile(SearchPath & searchPath, const string & path) +Path EvalState::findFile(SearchPath & searchPath, const string & path, const Pos & pos) { foreach (SearchPath::iterator, i, searchPath) { Path res; @@ -645,7 +641,11 @@ Path EvalState::findFile(SearchPath & searchPath, const string & path) } if (pathExists(res)) return canonPath(res); } - throw ThrownError(format("file ‘%1%’ was not found in the Nix search path (add it using $NIX_PATH or -I)") % path); + format f = format( + "file ‘%1%’ was not found in the Nix search path (add it using $NIX_PATH or -I)" + + string(pos ? ", at %2%" : "")); + f.exceptions(boost::io::all_error_bits ^ boost::io::too_many_args_bit); + throw ThrownError(f % path % pos); } diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index cd7b287e29c3..a4efd397ec7a 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -187,7 +187,7 @@ static void prim_typeOf(EvalState & state, const Pos & pos, Value * * args, Valu case tPrimOpApp: t = "lambda"; break; - case tExternal: + case tExternal: t = args[0]->external->typeOf(); break; default: abort(); @@ -710,7 +710,7 @@ static void prim_pathExists(EvalState & state, const Pos & pos, Value * * args, static void prim_baseNameOf(EvalState & state, const Pos & pos, Value * * args, Value & v) { PathSet context; - mkString(v, baseNameOf(state.coerceToString(pos, *args[0], context)), context); + mkString(v, baseNameOf(state.coerceToString(pos, *args[0], context, false, false)), context); } @@ -775,7 +775,7 @@ static void prim_findFile(EvalState & state, const Pos & pos, Value * * args, Va % path % e.path % pos); } - mkPath(v, state.findFile(searchPath, path).c_str()); + mkPath(v, state.findFile(searchPath, path, pos).c_str()); } /* Read a directory (without . or ..) */ @@ -796,7 +796,7 @@ static void prim_readDir(EvalState & state, const Pos & pos, Value * * args, Val for (auto & ent : entries) { Value * ent_val = state.allocAttr(v, state.symbols.create(ent.name)); if (ent.type == DT_UNKNOWN) - ent.type = getFileType(path); + ent.type = getFileType(path + "/" + ent.name); mkStringNoCopy(*ent_val, ent.type == DT_REG ? "regular" : ent.type == DT_DIR ? "directory" : diff --git a/src/libmain/shared.cc b/src/libmain/shared.cc index 7b50bfa9bb32..c83e997b2307 100644 --- a/src/libmain/shared.cc +++ b/src/libmain/shared.cc @@ -11,6 +11,7 @@ #include <exception> #include <algorithm> +#include <cstdlib> #include <sys/time.h> #include <sys/stat.h> #include <unistd.h> @@ -292,8 +293,9 @@ int handleExceptions(const string & programName, std::function<void()> fun) RunPager::RunPager() { if (!isatty(STDOUT_FILENO)) return; - string pager = getEnv("PAGER", "default"); - if (pager == "" || pager == "cat") return; + char * pager = getenv("NIX_PAGER"); + if (!pager) pager = getenv("PAGER"); + if (pager && ((string) pager == "" || (string) pager == "cat")) return; /* Ignore SIGINT. The pager will handle it (and we'll get SIGPIPE). */ @@ -313,8 +315,8 @@ RunPager::RunPager() throw SysError("dupping stdin"); if (!getenv("LESS")) setenv("LESS", "FRSXMK", 1); - if (pager != "default") - execl("/bin/sh", "sh", "-c", pager.c_str(), NULL); + if (pager) + execl("/bin/sh", "sh", "-c", pager, NULL); execlp("pager", "pager", NULL); execlp("less", "less", NULL); execlp("more", "more", NULL); diff --git a/src/libstore/build.cc b/src/libstore/build.cc index 08f44b392b00..e64bd3fef587 100644 --- a/src/libstore/build.cc +++ b/src/libstore/build.cc @@ -38,6 +38,9 @@ #if HAVE_SYS_MOUNT_H #include <sys/mount.h> #endif +#if HAVE_SYS_SYSCALL_H +#include <sys/syscall.h> +#endif #if HAVE_SCHED_H #include <sched.h> #endif @@ -48,7 +51,16 @@ #include <linux/fs.h> #endif -#define CHROOT_ENABLED HAVE_CHROOT && HAVE_UNSHARE && HAVE_SYS_MOUNT_H && defined(MS_BIND) && defined(MS_PRIVATE) && defined(CLONE_NEWNS) +#define CHROOT_ENABLED HAVE_CHROOT && HAVE_UNSHARE && HAVE_SYS_MOUNT_H && defined(MS_BIND) && defined(MS_PRIVATE) && defined(CLONE_NEWNS) && defined(SYS_pivot_root) + +/* 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 "/bin" "/usr/bin" +#endif #if CHROOT_ENABLED #include <sys/socket.h> @@ -1586,6 +1598,13 @@ void chmod_(const Path & path, mode_t mode) } +int childEntry(void * arg) +{ + ((DerivationGoal *) arg)->runChild(); + return 1; +} + + void DerivationGoal::startBuilder() { startNest(nest, lvlInfo, format( @@ -1627,14 +1646,26 @@ void DerivationGoal::startBuilder() /* The maximum number of cores to utilize for parallel building. */ env["NIX_BUILD_CORES"] = (format("%d") % settings.buildCores).str(); - /* Add all bindings specified in the derivation. */ - foreach (StringPairs::iterator, i, drv.env) - env[i->first] = i->second; - /* Create a temporary directory where the build will take place. */ tmpDir = createTempDir("", "nix-build-" + storePathToName(drvPath), false, false, 0700); + /* Add all bindings specified in the derivation via the + environments, except those listed in the passAsFile + attribute. Those are passed as file names pointing to + temporary files containing the contents. */ + StringSet passAsFile = tokenizeString<StringSet>(get(drv.env, "passAsFile")); + int fileNr = 0; + for (auto & i : drv.env) { + if (passAsFile.find(i.first) == passAsFile.end()) { + env[i.first] = i.second; + } else { + Path p = tmpDir + "/.attr-" + int2String(fileNr++); + writeFile(p, i.second); + env[i.first + "Path"] = p; + } + } + /* For convenience, set an environment pointing to the top build directory. */ env["NIX_BUILD_TOP"] = tmpDir; @@ -1729,21 +1760,6 @@ void DerivationGoal::startBuilder() /* Change ownership of the temporary build directory. */ if (chown(tmpDir.c_str(), buildUser.getUID(), buildUser.getGID()) == -1) throw SysError(format("cannot change ownership of ‘%1%’") % tmpDir); - - /* Check that the Nix store has the appropriate permissions, - i.e., owned by root and mode 1775 (sticky bit on so that - the builder can create its output but not mess with the - outputs of other processes). */ - struct stat st; - if (stat(settings.nixStore.c_str(), &st) == -1) - throw SysError(format("cannot stat ‘%1%’") % settings.nixStore); - if (!(st.st_mode & S_ISVTX) || - ((st.st_mode & S_IRWXG) != S_IRWXG) || - (st.st_gid != buildUser.getGID())) - throw Error(format( - "builder does not have write permission to ‘%2%’; " - "try ‘chgrp %1% %2%; chmod 1775 %2%’") - % buildUser.getGID() % settings.nixStore); } @@ -1760,6 +1776,47 @@ 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; + /* Note: we're not resolving symlinks here to prevent + giving a non-root user info about inaccessible + files. */ + Path canonI = canonPath(i); + /* 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); + if (canonI == canonA || isInDir(canonI, canonA)) { + found = true; + break; + } + } + if (!found) + throw Error(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 @@ -1801,20 +1858,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, @@ -1858,6 +1901,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 @@ -1902,11 +1948,66 @@ void DerivationGoal::startBuilder() builderOut.create(); /* Fork a child to build the package. */ - ProcessOptions options; - options.allowVfork = !buildUser.enabled(); - pid = startProcess([&]() { - runChild(); - }, options); +#if CHROOT_ENABLED + if (useChroot) { + /* Set up private namespaces for the build: + + - The PID namespace causes the build to start as PID 1. + Processes outside of the chroot are not visible to those + on the inside, but processes inside the chroot are + visible from the outside (though with different PIDs). + + - The private mount namespace ensures that all the bind + mounts we do will only show up in this process and its + children, and will disappear automatically when we're + done. + + - The private network namespace ensures that the builder + cannot talk to the outside world (or vice versa). It + only has a private loopback interface. + + - The IPC namespace prevents the builder from communicating + with outside processes using SysV IPC mechanisms (shared + memory, message queues, semaphores). It also ensures + that all IPC objects are destroyed when the builder + exits. + + - The UTS namespace ensures that builders see a hostname of + localhost rather than the actual hostname. + + We use a helper process to do the clone() to work around + clone() being broken in multi-threaded programs due to + at-fork handlers not being run. Note that we use + CLONE_PARENT to ensure that the real builder is parented to + us. + */ + Pid helper = startProcess([&]() { + char stack[32 * 1024]; + pid_t child = clone(childEntry, stack + sizeof(stack) - 8, + CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWNET | CLONE_NEWIPC | CLONE_NEWUTS | CLONE_PARENT | SIGCHLD, this); + if (child == -1) { + if (errno == EINVAL) + throw SysError("cloning builder process (Linux chroot builds require 3.13 or later)"); + else + throw SysError("cloning builder process"); + } + writeFull(builderOut.writeSide, int2String(child) + "\n"); + _exit(0); + }); + if (helper.wait(true) != 0) + throw Error("unable to start build process"); + pid_t tmp; + if (!string2Int<pid_t>(readLine(builderOut.readSide), tmp)) abort(); + pid = tmp; + } else +#endif + { + ProcessOptions options; + options.allowVfork = !buildUser.enabled(); + pid = startProcess([&]() { + runChild(); + }, options); + } /* parent */ pid.setSeparatePG(true); @@ -1922,7 +2023,6 @@ void DerivationGoal::startBuilder() printMsg(lvlError, format("@ build-started %1% - %2% %3%") % drvPath % drv.platform % logFile); } - } @@ -1938,30 +2038,6 @@ void DerivationGoal::runChild() #if CHROOT_ENABLED if (useChroot) { - /* Set up private namespaces for the build: - - - The private mount namespace ensures that all the bind - mounts we do will only show up in this process and - its children, and will disappear automatically when - we're done. - - - The private network namespace ensures that the - builder cannot talk to the outside world (or vice - versa). It only has a private loopback interface. - - - The IPC namespace prevents the builder from - communicating with outside processes using SysV IPC - mechanisms (shared memory, message queues, - semaphores). It also ensures that all IPC objects - are destroyed when the builder exits. - - - The UTS namespace ensures that builders see a - hostname of localhost rather than the actual - hostname. - */ - if (unshare(CLONE_NEWNS | CLONE_NEWNET | CLONE_NEWIPC | CLONE_NEWUTS) == -1) - throw SysError("setting up private namespaces"); - /* Initialise the loopback interface. */ AutoCloseFD fd(socket(PF_INET, SOCK_DGRAM, IPPROTO_IP)); if (fd == -1) throw SysError("cannot open IP socket"); @@ -1998,6 +2074,11 @@ void DerivationGoal::runChild() throw SysError(format("unable to make filesystem ‘%1%’ private") % fs); } + /* Bind-mount chroot directory to itself, to treat it as a + different filesystem from /, as needed for pivot_root. */ + if (mount(chrootRootDir.c_str(), chrootRootDir.c_str(), 0, MS_BIND, 0) == -1) + throw SysError(format("unable to bind mount ‘%1%’") % chrootRootDir); + /* Set up a nearly empty /dev, unless the user asked to bind-mount the host /dev. */ if (dirsInChroot.find("/dev") == dirsInChroot.end()) { @@ -2069,13 +2150,26 @@ void DerivationGoal::runChild() chmod_(chrootRootDir + "/dev/pts/ptmx", 0666); } - /* Do the chroot(). Below we do a chdir() to the - temporary build directory to make sure the current - directory is in the chroot. (Actually the order - doesn't matter, since due to the bind mount tmpDir and - tmpRootDit/tmpDir are the same directories.) */ - if (chroot(chrootRootDir.c_str()) == -1) + /* Do the chroot(). */ + if (chdir(chrootRootDir.c_str()) == -1) + throw SysError(format("cannot change directory to ‘%1%’") % chrootRootDir); + + if (mkdir("real-root", 0) == -1) + throw SysError("cannot create real-root directory"); + +#define pivot_root(new_root, put_old) (syscall(SYS_pivot_root, new_root, put_old)) + if (pivot_root(".", "real-root") == -1) + throw SysError(format("cannot pivot old root directory onto ‘%1%’") % (chrootRootDir + "/real-root")); +#undef pivot_root + + if (chroot(".") == -1) throw SysError(format("cannot change root directory to ‘%1%’") % chrootRootDir); + + if (umount2("real-root", MNT_DETACH) == -1) + throw SysError("cannot unmount real root filesystem"); + + if (rmdir("real-root") == -1) + throw SysError("cannot remove real-root directory"); } #endif @@ -2139,8 +2233,123 @@ 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) + Note that the sandbox profile allows file-write* even though it isn't seemingly necessary. First of all, nix's standard user permissioning + mechanism still prevents builders from writing to input directories, so no security/purity is lost. The reason we allow file-write* is that + denying it means the `access` syscall will return EPERM instead of EACCESS, which confuses a few programs that assume (understandably, since + it appears to be a violation of the POSIX spec) that `access` won't do that, and don't deal with it nicely if it does. The most notable of + these is the entire GHC Haskell ecosystem. */ + sandboxProfile += "(allow file-read* file-write* 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); @@ -2150,8 +2359,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); diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc index 3ad80bc4e6f4..bc792baf296b 100644 --- a/src/libstore/local-store.cc +++ b/src/libstore/local-store.cc @@ -256,20 +256,23 @@ LocalStore::LocalStore(bool reserveSpace) if (chmod(perUserDir.c_str(), 01777) == -1) throw SysError(format("could not set permissions on ‘%1%’ to 1777") % perUserDir); + mode_t perm = 01735; + struct group * gr = getgrnam(settings.buildUsersGroup.c_str()); if (!gr) - throw Error(format("the group ‘%1%’ specified in ‘build-users-group’ does not exist") + printMsg(lvlError, format("warning: the group ‘%1%’ specified in ‘build-users-group’ does not exist") % settings.buildUsersGroup); - - struct stat st; - if (stat(settings.nixStore.c_str(), &st)) - throw SysError(format("getting attributes of path ‘%1%’") % settings.nixStore); - - if (st.st_uid != 0 || st.st_gid != gr->gr_gid || (st.st_mode & ~S_IFMT) != 01775) { - if (chown(settings.nixStore.c_str(), 0, gr->gr_gid) == -1) - throw SysError(format("changing ownership of path ‘%1%’") % settings.nixStore); - if (chmod(settings.nixStore.c_str(), 01775) == -1) - throw SysError(format("changing permissions on path ‘%1%’") % settings.nixStore); + else { + struct stat st; + if (stat(settings.nixStore.c_str(), &st)) + throw SysError(format("getting attributes of path ‘%1%’") % settings.nixStore); + + if (st.st_uid != 0 || st.st_gid != gr->gr_gid || (st.st_mode & ~S_IFMT) != perm) { + if (chown(settings.nixStore.c_str(), 0, gr->gr_gid) == -1) + throw SysError(format("changing ownership of path ‘%1%’") % settings.nixStore); + if (chmod(settings.nixStore.c_str(), perm) == -1) + throw SysError(format("changing permissions on path ‘%1%’") % settings.nixStore); + } } } diff --git a/src/libstore/optimise-store.cc b/src/libstore/optimise-store.cc index dd18d66fa008..55c252b9b2e3 100644 --- a/src/libstore/optimise-store.cc +++ b/src/libstore/optimise-store.cc @@ -4,6 +4,7 @@ #include "local-store.hh" #include "globals.hh" +#include <cstdlib> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> diff --git a/src/libstore/remote-store.cc b/src/libstore/remote-store.cc index acc895409eba..d08913246321 100644 --- a/src/libstore/remote-store.cc +++ b/src/libstore/remote-store.cc @@ -10,6 +10,7 @@ #include <sys/stat.h> #include <sys/socket.h> #include <sys/un.h> +#include <errno.h> #include <fcntl.h> #include <iostream> diff --git a/src/libutil/hash.cc b/src/libutil/hash.cc index 965f3ed47701..a83ba0a81817 100644 --- a/src/libutil/hash.cc +++ b/src/libutil/hash.cc @@ -84,7 +84,7 @@ string printHash(const Hash & hash) return string(buf, hash.hashSize * 2); } - + Hash parseHash(HashType ht, const string & s) { Hash hash(ht); @@ -92,7 +92,7 @@ Hash parseHash(HashType ht, const string & s) throw Error(format("invalid hash ‘%1%’") % s); for (unsigned int i = 0; i < hash.hashSize; i++) { string s2(s, i * 2, 2); - if (!isxdigit(s2[0]) || !isxdigit(s2[1])) + if (!isxdigit(s2[0]) || !isxdigit(s2[1])) throw Error(format("invalid hash ‘%1%’") % s); std::istringstream str(s2); int n; @@ -103,24 +103,6 @@ Hash parseHash(HashType ht, const string & s) } -static unsigned char divMod(unsigned char * bytes, unsigned char y) -{ - unsigned int borrow = 0; - - int pos = Hash::maxHashSize - 1; - while (pos >= 0 && !bytes[pos]) --pos; - - for ( ; pos >= 0; --pos) { - unsigned int s = bytes[pos] + (borrow << 8); - unsigned int d = s / y; - borrow = s % y; - bytes[pos] = d; - } - - return borrow; -} - - unsigned int hashLength32(const Hash & hash) { return (hash.hashSize * 8 - 1) / 5 + 1; @@ -136,19 +118,19 @@ string printHash32(const Hash & hash) Hash hash2(hash); unsigned int len = hashLength32(hash); - const char * chars = base32Chars.data(); - - string s(len, '0'); - - int pos = len - 1; - while (pos >= 0) { - unsigned char digit = divMod(hash2.hash, 32); - s[pos--] = chars[digit]; + string s; + s.reserve(len); + + for (int n = len - 1; n >= 0; n--) { + unsigned int b = n * 5; + unsigned int i = b / 8; + unsigned int j = b % 8; + unsigned char c = + (hash.hash[i] >> j) + | (i >= hash.hashSize - 1 ? 0 : hash.hash[i + 1] << (8 - j)); + s.push_back(base32Chars[c & 0x1f]); } - for (unsigned int i = 0; i < hash2.maxHashSize; ++i) - assert(hash2.hash[i] == 0); - return s; } @@ -159,51 +141,24 @@ string printHash16or32(const Hash & hash) } -static bool mul(unsigned char * bytes, unsigned char y, int maxSize) -{ - unsigned char carry = 0; - - for (int pos = 0; pos < maxSize; ++pos) { - unsigned int m = bytes[pos] * y + carry; - bytes[pos] = m & 0xff; - carry = m >> 8; - } - - return carry; -} - - -static bool add(unsigned char * bytes, unsigned char y, int maxSize) -{ - unsigned char carry = y; - - for (int pos = 0; pos < maxSize; ++pos) { - unsigned int m = bytes[pos] + carry; - bytes[pos] = m & 0xff; - carry = m >> 8; - if (carry == 0) break; - } - - return carry; -} - - Hash parseHash32(HashType ht, const string & s) { Hash hash(ht); + unsigned int len = hashLength32(ht); + assert(s.size() == len); - const char * chars = base32Chars.data(); - - for (unsigned int i = 0; i < s.length(); ++i) { - char c = s[i]; + for (unsigned int n = 0; n < len; ++n) { + char c = s[len - n - 1]; unsigned char digit; for (digit = 0; digit < base32Chars.size(); ++digit) /* !!! slow */ - if (chars[digit] == c) break; + if (base32Chars[digit] == c) break; if (digit >= 32) throw Error(format("invalid base-32 hash ‘%1%’") % s); - if (mul(hash.hash, 32, hash.hashSize) || - add(hash.hash, digit, hash.hashSize)) - throw Error(format("base-32 hash ‘%1%’ is too large") % s); + unsigned int b = n * 5; + unsigned int i = b / 8; + unsigned int j = b % 8; + hash.hash[i] |= digit << j; + if (i < hash.hashSize - 1) hash.hash[i + 1] |= digit >> (8 - j); } return hash; @@ -299,7 +254,7 @@ Hash hashFile(HashType ht, const Path & path) if (n == -1) throw SysError(format("reading file ‘%1%’") % path); update(ht, ctx, buf, n); } - + finish(ht, ctx, hash.hash); return hash; } @@ -311,7 +266,7 @@ HashSink::HashSink(HashType ht) : ht(ht) bytes = 0; start(ht, *ctx); } - + HashSink::~HashSink() { bufPos = 0; @@ -369,7 +324,7 @@ HashType parseHashType(const string & s) else return htUnknown; } - + string printHashType(HashType ht) { if (ht == htMD5) return "md5"; @@ -378,5 +333,5 @@ string printHashType(HashType ht) else throw Error("cannot print unknown hash type"); } - + } diff --git a/src/libutil/monitor-fd.hh b/src/libutil/monitor-fd.hh index 72d23fb6934c..6f01ccd91a43 100644 --- a/src/libutil/monitor-fd.hh +++ b/src/libutil/monitor-fd.hh @@ -3,6 +3,7 @@ #include <thread> #include <atomic> +#include <cstdlib> #include <poll.h> #include <sys/types.h> #include <unistd.h> diff --git a/src/libutil/util.cc b/src/libutil/util.cc index dcdb438e03b2..be0a9bf317d1 100644 --- a/src/libutil/util.cc +++ b/src/libutil/util.cc @@ -193,8 +193,12 @@ Path readLink(const Path & path) if (!S_ISLNK(st.st_mode)) throw Error(format("‘%1%’ is not a symlink") % path); char buf[st.st_size]; - if (readlink(path.c_str(), buf, st.st_size) != st.st_size) + ssize_t rlsize = readlink(path.c_str(), buf, st.st_size); + if (rlsize == -1) throw SysError(format("reading symbolic link ‘%1%’") % path); + else if (rlsize > st.st_size) + throw Error(format("symbolic link ‘%1%’ size overflow %2% > %3%") + % path % rlsize % st.st_size); return string(buf, st.st_size); } @@ -921,18 +925,24 @@ std::vector<const char *> stringsToCharPtrs(const Strings & ss) } -string runProgram(Path program, bool searchPath, const Strings & args) +string runProgram(Path program, bool searchPath, const Strings & args, + const string & input) { checkInterrupt(); /* Create a pipe. */ - Pipe pipe; - pipe.create(); + Pipe stdout, stdin; + stdout.create(); + if (!input.empty()) stdin.create(); /* Fork. */ Pid pid = startProcess([&]() { - if (dup2(pipe.writeSide, STDOUT_FILENO) == -1) + if (dup2(stdout.writeSide, STDOUT_FILENO) == -1) throw SysError("dupping stdout"); + if (!input.empty()) { + if (dup2(stdin.readSide, STDIN_FILENO) == -1) + throw SysError("dupping stdin"); + } Strings args_(args); args_.push_front(program); @@ -946,9 +956,16 @@ string runProgram(Path program, bool searchPath, const Strings & args) throw SysError(format("executing ‘%1%’") % program); }); - pipe.writeSide.close(); + stdout.writeSide.close(); + + /* FIXME: This can deadlock if the input is too long. */ + if (!input.empty()) { + stdin.readSide.close(); + writeFull(stdin.writeSide, input); + stdin.writeSide.close(); + } - string result = drainFD(pipe.readSide); + string result = drainFD(stdout.readSide); /* Wait for the child to finish. */ int status = pid.wait(true); @@ -1191,4 +1208,63 @@ string filterANSIEscapes(const string & s, bool nixOnly) } +static char base64Chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + +string base64Encode(const string & s) +{ + string res; + int data = 0, nbits = 0; + + for (char c : s) { + data = data << 8 | (unsigned char) c; + nbits += 8; + while (nbits >= 6) { + nbits -= 6; + res.push_back(base64Chars[data >> nbits & 0x3f]); + } + } + + if (nbits) res.push_back(base64Chars[data << (6 - nbits) & 0x3f]); + while (res.size() % 4) res.push_back('='); + + return res; +} + + +string base64Decode(const string & s) +{ + bool init = false; + char decode[256]; + if (!init) { + // FIXME: not thread-safe. + memset(decode, -1, sizeof(decode)); + for (int i = 0; i < 64; i++) + decode[(int) base64Chars[i]] = i; + init = true; + } + + string res; + unsigned int d = 0, bits = 0; + + for (char c : s) { + if (c == '=') break; + if (c == '\n') continue; + + char digit = decode[(unsigned char) c]; + if (digit == -1) + throw Error("invalid character in Base64 string"); + + bits += 6; + d = d << 6 | digit; + if (bits >= 8) { + res.push_back(d >> (bits - 8) & 0xff); + bits -= 8; + } + } + + return res; +} + + } diff --git a/src/libutil/util.hh b/src/libutil/util.hh index 186ee71f45d0..20330fb7699e 100644 --- a/src/libutil/util.hh +++ b/src/libutil/util.hh @@ -285,7 +285,7 @@ pid_t startProcess(std::function<void()> fun, const ProcessOptions & options = P /* Run a program and return its stdout in a string (i.e., like the shell backtick operator). */ string runProgram(Path program, bool searchPath = false, - const Strings & args = Strings()); + const Strings & args = Strings(), const string & input = ""); MakeError(ExecError, Error) @@ -398,4 +398,9 @@ void ignoreException(); string filterANSIEscapes(const string & s, bool nixOnly = false); +/* Base64 encoding/decoding. */ +string base64Encode(const string & s); +string base64Decode(const string & s); + + } diff --git a/src/nix-env/nix-env.cc b/src/nix-env/nix-env.cc index f5e8ee08c42f..f3c8d3ba8953 100644 --- a/src/nix-env/nix-env.cc +++ b/src/nix-env/nix-env.cc @@ -132,6 +132,8 @@ static void getAllExprs(EvalState & state, Value & vArg(*state.allocValue()); state.getBuiltin("import", vFun); mkString(vArg, path2); + if (v.attrs->size() == v.attrs->capacity()) + throw Error(format("too many Nix expressions in directory ‘%1%’") % path); mkApp(*state.allocAttr(v, state.symbols.create(attrName)), vFun, vArg); } else if (S_ISDIR(st.st_mode)) @@ -160,7 +162,7 @@ static void loadSourceExpr(EvalState & state, const Path & path, Value & v) ~/.nix-defexpr directory that includes some system-wide directory). */ if (S_ISDIR(st.st_mode)) { - state.mkAttrs(v, 16); + state.mkAttrs(v, 1024); state.mkList(*state.allocAttr(v, state.symbols.create("_combineChannels")), 0); StringSet attrs; getAllExprs(state, path, attrs, v); diff --git a/src/nix-store/local.mk b/src/nix-store/local.mk index b887fe03389b..84ff15b241f3 100644 --- a/src/nix-store/local.mk +++ b/src/nix-store/local.mk @@ -6,6 +6,6 @@ nix-store_SOURCES := $(wildcard $(d)/*.cc) nix-store_LIBS = libmain libstore libutil libformat -nix-store_LDFLAGS = -lbz2 -pthread +nix-store_LDFLAGS = -lbz2 -pthread $(SODIUM_LIBS) nix-store_CXXFLAGS = -DCURL=\"$(curl)\" diff --git a/src/nix-store/nix-store.cc b/src/nix-store/nix-store.cc index 87bc8c379de5..7ce5f63c2d2f 100644 --- a/src/nix-store/nix-store.cc +++ b/src/nix-store/nix-store.cc @@ -20,6 +20,10 @@ #include <bzlib.h> +#if HAVE_SODIUM +#include <sodium.h> +#endif + using namespace nix; using std::cin; @@ -1006,6 +1010,34 @@ static void opServe(Strings opFlags, Strings opArgs) } +static void opGenerateBinaryCacheKey(Strings opFlags, Strings opArgs) +{ + foreach (Strings::iterator, i, opFlags) + throw UsageError(format("unknown flag ‘%1%’") % *i); + + if (opArgs.size() != 3) throw UsageError("three arguments expected"); + auto i = opArgs.begin(); + string keyName = *i++; + string secretKeyFile = *i++; + string publicKeyFile = *i++; + +#if HAVE_SODIUM + sodium_init(); + + unsigned char pk[crypto_sign_PUBLICKEYBYTES]; + unsigned char sk[crypto_sign_SECRETKEYBYTES]; + if (crypto_sign_keypair(pk, sk) != 0) + throw Error("key generation failed"); + + writeFile(publicKeyFile, keyName + ":" + base64Encode(string((char *) pk, crypto_sign_PUBLICKEYBYTES))); + umask(0077); + writeFile(secretKeyFile, keyName + ":" + base64Encode(string((char *) sk, crypto_sign_SECRETKEYBYTES))); +#else + throw Error("Nix was not compiled with libsodium, required for signed binary cache support"); +#endif +} + + /* Scan the arguments; find the operation, set global flags, put all other flags in a list, and put all other arguments in another list. */ @@ -1072,14 +1104,16 @@ int main(int argc, char * * argv) op = opQueryFailedPaths; else if (*arg == "--clear-failed-paths") op = opClearFailedPaths; + else if (*arg == "--serve") + op = opServe; + else if (*arg == "--generate-binary-cache-key") + op = opGenerateBinaryCacheKey; else if (*arg == "--add-root") gcRoot = absPath(getArg(*arg, arg, end)); else if (*arg == "--indirect") indirectRoot = true; else if (*arg == "--no-output") noOutput = true; - else if (*arg == "--serve") - op = opServe; else if (*arg != "" && arg->at(0) == '-') { opFlags.push_back(*arg); if (*arg == "--max-freed" || *arg == "--max-links" || *arg == "--max-atime") /* !!! hack */ |