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) | #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 | #if CHROOT_ENABLED | ||||||
| #include <sys/socket.h> | #include <sys/socket.h> | ||||||
| #include <sys/ioctl.h> | #include <sys/ioctl.h> | ||||||
|  | @ -1752,6 +1761,44 @@ void DerivationGoal::startBuilder() | ||||||
|     if (get(drv.env, "__noChroot") == "1") useChroot = false; |     if (get(drv.env, "__noChroot") == "1") useChroot = false; | ||||||
| 
 | 
 | ||||||
|     if (useChroot) { |     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 | #if CHROOT_ENABLED | ||||||
|         /* Create a temporary directory in which we set up the chroot
 |         /* Create a temporary directory in which we set up the chroot
 | ||||||
|            environment using bind-mounts.  We put it in the Nix store |            environment using bind-mounts.  We put it in the Nix store | ||||||
|  | @ -1793,20 +1840,6 @@ void DerivationGoal::startBuilder() | ||||||
|         /* Create /etc/hosts with localhost entry. */ |         /* Create /etc/hosts with localhost entry. */ | ||||||
|         writeFile(chrootRootDir + "/etc/hosts", "127.0.0.1 localhost\n"); |         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,
 |         /* Make the closure of the inputs available in the chroot,
 | ||||||
|            rather than the whole Nix store.  This prevents any access |            rather than the whole Nix store.  This prevents any access | ||||||
|            to undeclared dependencies.  Directories are bind-mounted, |            to undeclared dependencies.  Directories are bind-mounted, | ||||||
|  | @ -1850,6 +1883,9 @@ void DerivationGoal::startBuilder() | ||||||
|             foreach (DerivationOutputs::iterator, i, drv.outputs) |             foreach (DerivationOutputs::iterator, i, drv.outputs) | ||||||
|                 dirsInChroot.erase(i->second.path); |                 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 | #else | ||||||
|         throw Error("chroot builds are not supported on this platform"); |         throw Error("chroot builds are not supported on this platform"); | ||||||
| #endif | #endif | ||||||
|  | @ -2156,8 +2192,118 @@ void DerivationGoal::runChild() | ||||||
| 
 | 
 | ||||||
|         /* Fill in the arguments. */ |         /* Fill in the arguments. */ | ||||||
|         Strings args; |         Strings args; | ||||||
|  | 
 | ||||||
|  |         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); |             string builderBasename = baseNameOf(drv.builder); | ||||||
|             args.push_back(builderBasename); |             args.push_back(builderBasename); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         foreach (Strings::iterator, i, drv.args) |         foreach (Strings::iterator, i, drv.args) | ||||||
|             args.push_back(rewriteHashes(*i, rewritesToTmp)); |             args.push_back(rewriteHashes(*i, rewritesToTmp)); | ||||||
|         auto argArr = stringsToCharPtrs(args); |         auto argArr = stringsToCharPtrs(args); | ||||||
|  | @ -2167,8 +2313,14 @@ void DerivationGoal::runChild() | ||||||
|         /* Indicate that we managed to set up the build environment. */ |         /* Indicate that we managed to set up the build environment. */ | ||||||
|         writeFull(STDERR_FILENO, "\n"); |         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. */ |         /* 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); |         throw SysError(format("executing ‘%1%’") % drv.builder); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue