diff options
Diffstat (limited to 'nix')
| -rw-r--r-- | nix/libstore/build.cc | 788 | ||||
| -rw-r--r-- | nix/libutil/spawn.cc | 829 | ||||
| -rw-r--r-- | nix/libutil/spawn.hh | 164 | ||||
| -rw-r--r-- | nix/libutil/util.cc | 19 | ||||
| -rw-r--r-- | nix/libutil/util.hh | 5 | ||||
| -rw-r--r-- | nix/local.mk | 6 |
6 files changed, 1303 insertions, 508 deletions
diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc index afdcd9518b3..51f5aed1068 100644 --- a/nix/libstore/build.cc +++ b/nix/libstore/build.cc | |||
| @@ -9,6 +9,7 @@ | |||
| 9 | #include "archive.hh" | 9 | #include "archive.hh" |
| 10 | #include "affinity.hh" | 10 | #include "affinity.hh" |
| 11 | #include "builtins.hh" | 11 | #include "builtins.hh" |
| 12 | #include "spawn.hh" | ||
| 12 | 13 | ||
| 13 | #include <map> | 14 | #include <map> |
| 14 | #include <sstream> | 15 | #include <sstream> |
| @@ -402,22 +403,6 @@ void Goal::trace(const format & f) | |||
| 402 | } | 403 | } |
| 403 | 404 | ||
| 404 | 405 | ||
| 405 | |||
| 406 | ////////////////////////////////////////////////////////////////////// | ||
| 407 | |||
| 408 | |||
| 409 | /* Restore default handling of SIGPIPE, otherwise some programs will | ||
| 410 | randomly say "Broken pipe". */ | ||
| 411 | static void restoreSIGPIPE() | ||
| 412 | { | ||
| 413 | struct sigaction act, oact; | ||
| 414 | act.sa_handler = SIG_DFL; | ||
| 415 | act.sa_flags = 0; | ||
| 416 | sigemptyset(&act.sa_mask); | ||
| 417 | if (sigaction(SIGPIPE, &act, &oact)) throw SysError("resetting SIGPIPE"); | ||
| 418 | } | ||
| 419 | |||
| 420 | |||
| 421 | ////////////////////////////////////////////////////////////////////// | 406 | ////////////////////////////////////////////////////////////////////// |
| 422 | 407 | ||
| 423 | 408 | ||
| @@ -679,12 +664,6 @@ private: | |||
| 679 | typedef void (DerivationGoal::*GoalState)(); | 664 | typedef void (DerivationGoal::*GoalState)(); |
| 680 | GoalState state; | 665 | GoalState state; |
| 681 | 666 | ||
| 682 | /* Stuff we need to pass to runChild(). */ | ||
| 683 | typedef map<Path, Path> DirsInChroot; // maps target path to source path | ||
| 684 | DirsInChroot dirsInChroot; | ||
| 685 | typedef map<string, string> Environment; | ||
| 686 | Environment env; | ||
| 687 | |||
| 688 | /* Hash rewriting. */ | 667 | /* Hash rewriting. */ |
| 689 | HashRewrites rewritesToTmp, rewritesFromTmp; | 668 | HashRewrites rewritesToTmp, rewritesFromTmp; |
| 690 | typedef map<Path, Path> RedirectedOutputs; | 669 | typedef map<Path, Path> RedirectedOutputs; |
| @@ -753,14 +732,9 @@ private: | |||
| 753 | /* Start building a derivation. */ | 732 | /* Start building a derivation. */ |
| 754 | void startBuilder(); | 733 | void startBuilder(); |
| 755 | 734 | ||
| 756 | /* Run the builder's process. */ | 735 | void execBuilderOrBuiltin(SpawnContext &); |
| 757 | void runChild(); | ||
| 758 | 736 | ||
| 759 | friend int childEntry(void *); | 737 | friend void execBuilderOrBuiltinAction(SpawnContext &); |
| 760 | |||
| 761 | /* Pipe to notify readiness to the child process when using unprivileged | ||
| 762 | user namespaces. */ | ||
| 763 | Pipe readiness; | ||
| 764 | 738 | ||
| 765 | /* Check that the derivation outputs all exist and register them | 739 | /* Check that the derivation outputs all exist and register them |
| 766 | as valid. */ | 740 | as valid. */ |
| @@ -1630,13 +1604,6 @@ void chmod_(const Path & path, mode_t mode) | |||
| 1630 | } | 1604 | } |
| 1631 | 1605 | ||
| 1632 | 1606 | ||
| 1633 | int childEntry(void * arg) | ||
| 1634 | { | ||
| 1635 | ((DerivationGoal *) arg)->runChild(); | ||
| 1636 | return 1; | ||
| 1637 | } | ||
| 1638 | |||
| 1639 | |||
| 1640 | /* UID and GID of the build user inside its own user namespace. */ | 1607 | /* UID and GID of the build user inside its own user namespace. */ |
| 1641 | static const uid_t guestUID = 30001; | 1608 | static const uid_t guestUID = 30001; |
| 1642 | static const gid_t guestGID = 30000; | 1609 | static const gid_t guestGID = 30000; |
| @@ -1655,6 +1622,105 @@ static void initializeUserNamespace(pid_t child, | |||
| 1655 | (format("%d %d 1") % guestGID % hostGID).str()); | 1622 | (format("%d %d 1") % guestGID % hostGID).str()); |
| 1656 | } | 1623 | } |
| 1657 | 1624 | ||
| 1625 | #if CHROOT_ENABLED | ||
| 1626 | |||
| 1627 | void clearRootWritePermsAction(SpawnContext & sctx) | ||
| 1628 | { | ||
| 1629 | if(chmod("/", 0555) == -1) | ||
| 1630 | throw SysError("changing mode of chroot root directory"); | ||
| 1631 | } | ||
| 1632 | |||
| 1633 | #endif /* CHROOT_ENABLED */ | ||
| 1634 | |||
| 1635 | /* Return true if the operating system kernel part of SYSTEM1 and SYSTEM2 (the | ||
| 1636 | bit that comes after the hyphen in system types such as "i686-linux") is | ||
| 1637 | the same. */ | ||
| 1638 | static bool sameOperatingSystemKernel(const std::string& system1, const std::string& system2) | ||
| 1639 | { | ||
| 1640 | auto os1 = system1.substr(system1.find("-")); | ||
| 1641 | auto os2 = system2.substr(system2.find("-")); | ||
| 1642 | return os1 == os2; | ||
| 1643 | } | ||
| 1644 | |||
| 1645 | |||
| 1646 | void DerivationGoal::execBuilderOrBuiltin(SpawnContext & ctx) | ||
| 1647 | { | ||
| 1648 | if(isBuiltin(drv)) { | ||
| 1649 | /* Note: must not return from this block */ | ||
| 1650 | try { | ||
| 1651 | logType = ltFlat; | ||
| 1652 | |||
| 1653 | auto buildDrv = lookupBuiltinBuilder(drv.builder); | ||
| 1654 | if (buildDrv != NULL) { | ||
| 1655 | /* Check what the output file name is. When doing a 'bmCheck' | ||
| 1656 | build, the output file name is different from that | ||
| 1657 | specified in DRV due to hash rewriting. */ | ||
| 1658 | Path output = drv.outputs["out"].path; | ||
| 1659 | auto redirected = redirectedOutputs.find(output); | ||
| 1660 | if (redirected != redirectedOutputs.end()) | ||
| 1661 | output = redirected->second; | ||
| 1662 | |||
| 1663 | buildDrv(drv, drvPath, output); | ||
| 1664 | } | ||
| 1665 | else | ||
| 1666 | throw Error(format("unsupported builtin function '%1%'") % string(drv.builder, 8)); | ||
| 1667 | _exit(0); | ||
| 1668 | } catch (std::exception & e) { | ||
| 1669 | writeFull(STDERR_FILENO, "error: " + string(e.what()) + "\n"); | ||
| 1670 | _exit(1); | ||
| 1671 | } | ||
| 1672 | } | ||
| 1673 | /* Ensure that the builder is within the store. This prevents users from | ||
| 1674 | using /proc/self/exe (or a symlink to it) as their builder, which could | ||
| 1675 | allow them to overwrite the guix-daemon binary (CVE-2019-5736). | ||
| 1676 | |||
| 1677 | This attack is possible even if the target of /proc/self/exe is outside | ||
| 1678 | the chroot (it's as if it were a hard link), though it requires that | ||
| 1679 | its ELF interpreter and dependencies be in the chroot. | ||
| 1680 | |||
| 1681 | Note: 'canonPath' throws if 'ctx.program' cannot be resolved within the | ||
| 1682 | chroot. */ | ||
| 1683 | ctx.program = canonPath(ctx.program, true); | ||
| 1684 | if(!isInStore(ctx.program)) | ||
| 1685 | throw Error(format("derivation builder `%1' is outside the store") % ctx.program); | ||
| 1686 | /* If DRV targets the same operating system kernel, try to execute it: | ||
| 1687 | there might be binfmt_misc set up for user-land emulation of other | ||
| 1688 | architectures. However, if it targets a different operating | ||
| 1689 | system--e.g., "i586-gnu" vs. "x86_64-linux"--do not try executing it: | ||
| 1690 | the ELF file for that OS is likely indistinguishable from a native ELF | ||
| 1691 | binary and it would just crash at run time. */ | ||
| 1692 | int error; | ||
| 1693 | if (sameOperatingSystemKernel(drv.platform, settings.thisSystem)) { | ||
| 1694 | try { | ||
| 1695 | execAction(ctx); | ||
| 1696 | error = errno; | ||
| 1697 | } catch(SysError & e) { | ||
| 1698 | error = e.errNo; | ||
| 1699 | } | ||
| 1700 | } else { | ||
| 1701 | error = ENOEXEC; | ||
| 1702 | } | ||
| 1703 | /* Right platform? Check this after we've tried 'execve' to allow for | ||
| 1704 | transparent emulation of different platforms with binfmt_misc handlers | ||
| 1705 | that invoke QEMU. */ | ||
| 1706 | if (error == ENOEXEC && !canBuildLocally(drv.platform)) { | ||
| 1707 | if (settings.printBuildTrace) | ||
| 1708 | printMsg(lvlError, format("@ unsupported-platform %1% %2%") % drvPath % drv.platform); | ||
| 1709 | throw Error(format("a `%1%' is required to build `%3%', but I am a `%2%'") | ||
| 1710 | % drv.platform % settings.thisSystem % drvPath); | ||
| 1711 | } | ||
| 1712 | |||
| 1713 | errno = error; | ||
| 1714 | throw SysError(format("executing `%1%'") % drv.builder); | ||
| 1715 | } | ||
| 1716 | |||
| 1717 | |||
| 1718 | void execBuilderOrBuiltinAction(SpawnContext & ctx) | ||
| 1719 | { | ||
| 1720 | ((DerivationGoal *)ctx.extraData)->execBuilderOrBuiltin(ctx); | ||
| 1721 | } | ||
| 1722 | |||
| 1723 | |||
| 1658 | void DerivationGoal::startBuilder() | 1724 | void DerivationGoal::startBuilder() |
| 1659 | { | 1725 | { |
| 1660 | auto f = format( | 1726 | auto f = format( |
| @@ -1665,6 +1731,57 @@ void DerivationGoal::startBuilder() | |||
| 1665 | f.exceptions(boost::io::all_error_bits ^ boost::io::too_many_args_bit); | 1731 | f.exceptions(boost::io::all_error_bits ^ boost::io::too_many_args_bit); |
| 1666 | startNest(nest, lvlInfo, f % showPaths(missingPaths) % curRound % nrRounds); | 1732 | startNest(nest, lvlInfo, f % showPaths(missingPaths) % curRound % nrRounds); |
| 1667 | 1733 | ||
| 1734 | /* A CloneSpawnContext reference can be passed to procedures expecting a | ||
| 1735 | SpawnContext reference */ | ||
| 1736 | #if CHROOT_ENABLED | ||
| 1737 | CloneSpawnContext ctx; | ||
| 1738 | #else | ||
| 1739 | SpawnContext ctx; | ||
| 1740 | #endif | ||
| 1741 | |||
| 1742 | ctx.extraData = (void *) this; | ||
| 1743 | ctx.setsid = true; | ||
| 1744 | ctx.oomSacrifice = true; | ||
| 1745 | ctx.signalSetupSuccess = true; | ||
| 1746 | ctx.setStdin = true; | ||
| 1747 | ctx.stdinFile = "/dev/null"; | ||
| 1748 | ctx.closeMostFDs = true; | ||
| 1749 | ctx.program = drv.builder; | ||
| 1750 | ctx.args = drv.args; | ||
| 1751 | if(!isBuiltin(drv)) | ||
| 1752 | ctx.args.insert(ctx.args.begin(), baseNameOf(drv.builder)); | ||
| 1753 | |||
| 1754 | #if __linux__ | ||
| 1755 | ctx.dropAmbientCapabilities = true; | ||
| 1756 | ctx.persona = PER_LINUX; /* default */ | ||
| 1757 | ctx.setPersona = true; | ||
| 1758 | /* Change the personality to 32-bit if we're doing an | ||
| 1759 | i686-linux build on an x86_64-linux machine. */ | ||
| 1760 | struct utsname utsbuf; | ||
| 1761 | uname(&utsbuf); | ||
| 1762 | if (drv.platform == "i686-linux" && | ||
| 1763 | (settings.thisSystem == "x86_64-linux" || | ||
| 1764 | (!strcmp(utsbuf.sysname, "Linux") && !strcmp(utsbuf.machine, "x86_64")))) { | ||
| 1765 | ctx.persona = PER_LINUX32; | ||
| 1766 | } | ||
| 1767 | |||
| 1768 | if (drv.platform == "armhf-linux" && | ||
| 1769 | (settings.thisSystem == "aarch64-linux" || | ||
| 1770 | (!strcmp(utsbuf.sysname, "Linux") && !strcmp(utsbuf.machine, "aarch64")))) { | ||
| 1771 | ctx.persona = PER_LINUX32; | ||
| 1772 | } | ||
| 1773 | |||
| 1774 | /* Impersonate a Linux 2.6 machine to get some determinism in | ||
| 1775 | builds that depend on the kernel version. */ | ||
| 1776 | if ((drv.platform == "i686-linux" || drv.platform == "x86_64-linux") && settings.impersonateLinux26) { | ||
| 1777 | ctx.persona |= 0x0020000; /* == UNAME26 */ | ||
| 1778 | } | ||
| 1779 | |||
| 1780 | /* Disable address space randomization for improved determinism. */ | ||
| 1781 | ctx.persona |= ADDR_NO_RANDOMIZE; | ||
| 1782 | |||
| 1783 | #endif | ||
| 1784 | |||
| 1668 | /* Note: built-in builders are *not* running in a chroot environment so | 1785 | /* Note: built-in builders are *not* running in a chroot environment so |
| 1669 | that we can easily implement them in Guile without having it as a | 1786 | that we can easily implement them in Guile without having it as a |
| 1670 | derivation input (they are running under a separate build user, | 1787 | derivation input (they are running under a separate build user, |
| @@ -1672,12 +1789,13 @@ void DerivationGoal::startBuilder() | |||
| 1672 | useChroot = settings.useChroot && !isBuiltin(drv); | 1789 | useChroot = settings.useChroot && !isBuiltin(drv); |
| 1673 | 1790 | ||
| 1674 | /* Construct the environment passed to the builder. */ | 1791 | /* Construct the environment passed to the builder. */ |
| 1675 | env.clear(); | 1792 | ctx.env.clear(); |
| 1793 | ctx.inheritEnv = false; | ||
| 1676 | 1794 | ||
| 1677 | /* Most shells initialise PATH to some default (/bin:/usr/bin:...) when | 1795 | /* Most shells initialise PATH to some default (/bin:/usr/bin:...) when |
| 1678 | PATH is not set. We don't want this, so we fill it in with some dummy | 1796 | PATH is not set. We don't want this, so we fill it in with some dummy |
| 1679 | value. */ | 1797 | value. */ |
| 1680 | env["PATH"] = "/path-not-set"; | 1798 | ctx.env["PATH"] = "/path-not-set"; |
| 1681 | 1799 | ||
| 1682 | /* Set HOME to a non-existing path to prevent certain programs from using | 1800 | /* Set HOME to a non-existing path to prevent certain programs from using |
| 1683 | /etc/passwd (or NIS, or whatever) to locate the home directory (for | 1801 | /etc/passwd (or NIS, or whatever) to locate the home directory (for |
| @@ -1686,20 +1804,20 @@ void DerivationGoal::startBuilder() | |||
| 1686 | they are looking for does not exist if HOME is set but points to some | 1804 | they are looking for does not exist if HOME is set but points to some |
| 1687 | non-existing path. */ | 1805 | non-existing path. */ |
| 1688 | Path homeDir = "/homeless-shelter"; | 1806 | Path homeDir = "/homeless-shelter"; |
| 1689 | env["HOME"] = homeDir; | 1807 | ctx.env["HOME"] = homeDir; |
| 1690 | 1808 | ||
| 1691 | /* Tell the builder where the store is. Usually they | 1809 | /* Tell the builder where the store is. Usually they |
| 1692 | shouldn't care, but this is useful for purity checking (e.g., | 1810 | shouldn't care, but this is useful for purity checking (e.g., |
| 1693 | the compiler or linker might only want to accept paths to files | 1811 | the compiler or linker might only want to accept paths to files |
| 1694 | in the store or in the build directory). */ | 1812 | in the store or in the build directory). */ |
| 1695 | env["NIX_STORE"] = settings.nixStore; | 1813 | ctx.env["NIX_STORE"] = settings.nixStore; |
| 1696 | 1814 | ||
| 1697 | /* The maximum number of cores to utilize for parallel building. */ | 1815 | /* The maximum number of cores to utilize for parallel building. */ |
| 1698 | env["NIX_BUILD_CORES"] = (format("%d") % settings.buildCores).str(); | 1816 | ctx.env["NIX_BUILD_CORES"] = (format("%d") % settings.buildCores).str(); |
| 1699 | 1817 | ||
| 1700 | /* Add all bindings specified in the derivation. */ | 1818 | /* Add all bindings specified in the derivation. */ |
| 1701 | for (auto& i : drv.env) | 1819 | for (auto& i : drv.env) |
| 1702 | env[i.first] = i.second; | 1820 | ctx.env[i.first] = i.second; |
| 1703 | 1821 | ||
| 1704 | /* Create a temporary directory where the build will take | 1822 | /* Create a temporary directory where the build will take |
| 1705 | place. */ | 1823 | place. */ |
| @@ -1728,18 +1846,20 @@ void DerivationGoal::startBuilder() | |||
| 1728 | directory. */ | 1846 | directory. */ |
| 1729 | tmpDirInSandbox = useChroot ? canonPath("/tmp", true) + "/guix-build-" + drvName + "-0" : tmpDir; | 1847 | tmpDirInSandbox = useChroot ? canonPath("/tmp", true) + "/guix-build-" + drvName + "-0" : tmpDir; |
| 1730 | 1848 | ||
| 1849 | ctx.setcwd = true; | ||
| 1850 | ctx.cwd = tmpDirInSandbox; | ||
| 1731 | /* For convenience, set an environment pointing to the top build | 1851 | /* For convenience, set an environment pointing to the top build |
| 1732 | directory. */ | 1852 | directory. */ |
| 1733 | env["NIX_BUILD_TOP"] = tmpDirInSandbox; | 1853 | ctx.env["NIX_BUILD_TOP"] = tmpDirInSandbox; |
| 1734 | 1854 | ||
| 1735 | /* Also set TMPDIR and variants to point to this directory. */ | 1855 | /* Also set TMPDIR and variants to point to this directory. */ |
| 1736 | env["TMPDIR"] = env["TEMPDIR"] = env["TMP"] = env["TEMP"] = tmpDirInSandbox; | 1856 | ctx.env["TMPDIR"] = ctx.env["TEMPDIR"] = ctx.env["TMP"] = ctx.env["TEMP"] = tmpDirInSandbox; |
| 1737 | 1857 | ||
| 1738 | /* Explicitly set PWD to prevent problems with chroot builds. In | 1858 | /* Explicitly set PWD to prevent problems with chroot builds. In |
| 1739 | particular, dietlibc cannot figure out the cwd because the | 1859 | particular, dietlibc cannot figure out the cwd because the |
| 1740 | inode of the current directory doesn't appear in .. (because | 1860 | inode of the current directory doesn't appear in .. (because |
| 1741 | getdents returns the inode of the mount point). */ | 1861 | getdents returns the inode of the mount point). */ |
| 1742 | env["PWD"] = tmpDirInSandbox; | 1862 | ctx.env["PWD"] = tmpDirInSandbox; |
| 1743 | 1863 | ||
| 1744 | /* *Only* if this is a fixed-output derivation, propagate the | 1864 | /* *Only* if this is a fixed-output derivation, propagate the |
| 1745 | values of the environment variables specified in the | 1865 | values of the environment variables specified in the |
| @@ -1752,7 +1872,7 @@ void DerivationGoal::startBuilder() | |||
| 1752 | already know the cryptographic hash of the output). */ | 1872 | already know the cryptographic hash of the output). */ |
| 1753 | if (fixedOutput) { | 1873 | if (fixedOutput) { |
| 1754 | Strings varNames = tokenizeString<Strings>(get(drv.env, "impureEnvVars")); | 1874 | Strings varNames = tokenizeString<Strings>(get(drv.env, "impureEnvVars")); |
| 1755 | for (auto& i : varNames) env[i] = getEnv(i); | 1875 | for (auto& i : varNames) ctx.env[i] = getEnv(i); |
| 1756 | } | 1876 | } |
| 1757 | 1877 | ||
| 1758 | /* The `exportReferencesGraph' feature allows the references graph | 1878 | /* The `exportReferencesGraph' feature allows the references graph |
| @@ -1816,10 +1936,20 @@ void DerivationGoal::startBuilder() | |||
| 1816 | /* Change ownership of the temporary build directory. */ | 1936 | /* Change ownership of the temporary build directory. */ |
| 1817 | if (chown(tmpDir.c_str(), buildUser.getUID(), buildUser.getGID()) == -1) | 1937 | if (chown(tmpDir.c_str(), buildUser.getUID(), buildUser.getGID()) == -1) |
| 1818 | throw SysError(format("cannot change ownership of '%1%'") % tmpDir); | 1938 | throw SysError(format("cannot change ownership of '%1%'") % tmpDir); |
| 1939 | |||
| 1940 | ctx.setuid = true; | ||
| 1941 | ctx.user = buildUser.getUID(); | ||
| 1942 | ctx.setgid = true; | ||
| 1943 | ctx.group = buildUser.getGID(); | ||
| 1944 | ctx.setSupplementaryGroups = true; | ||
| 1945 | ctx.supplementaryGroups = buildUser.getSupplementaryGIDs(); | ||
| 1819 | } | 1946 | } |
| 1820 | 1947 | ||
| 1821 | if (useChroot) { | 1948 | if (useChroot) { |
| 1822 | #if CHROOT_ENABLED | 1949 | #if CHROOT_ENABLED |
| 1950 | ctx.phases = getCloneSpawnPhases(); | ||
| 1951 | addPhaseAfter(ctx.phases, "chroot", "clearRootWritePerms", | ||
| 1952 | clearRootWritePermsAction); | ||
| 1823 | /* Create a temporary directory in which we set up the chroot | 1953 | /* Create a temporary directory in which we set up the chroot |
| 1824 | environment using bind-mounts. Put it in the store to ensure it | 1954 | environment using bind-mounts. Put it in the store to ensure it |
| 1825 | can be atomically moved to the store. */ | 1955 | can be atomically moved to the store. */ |
| @@ -1830,6 +1960,22 @@ void DerivationGoal::startBuilder() | |||
| 1830 | /* Clean up the chroot directory automatically. */ | 1960 | /* Clean up the chroot directory automatically. */ |
| 1831 | autoDelChroot = std::shared_ptr<AutoDelete>(new AutoDelete(chrootRootTop)); | 1961 | autoDelChroot = std::shared_ptr<AutoDelete>(new AutoDelete(chrootRootTop)); |
| 1832 | 1962 | ||
| 1963 | ctx.doChroot = true; | ||
| 1964 | ctx.chrootRootDir = chrootRootDir; | ||
| 1965 | ctx.cloneFlags = CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWIPC | CLONE_NEWUTS | SIGCHLD; | ||
| 1966 | |||
| 1967 | if(!fixedOutput) { | ||
| 1968 | ctx.initLoopback = true; | ||
| 1969 | ctx.cloneFlags |= CLONE_NEWNET; | ||
| 1970 | } | ||
| 1971 | |||
| 1972 | if(!buildUser.enabled()) | ||
| 1973 | ctx.cloneFlags |= CLONE_NEWUSER; | ||
| 1974 | |||
| 1975 | /* Set the hostname etc. to fixed values. */ | ||
| 1976 | ctx.hostname = "localhost"; | ||
| 1977 | ctx.domainname = "(none)"; /* kernel default */ | ||
| 1978 | |||
| 1833 | printMsg(lvlChatty, format("setting up chroot environment in `%1%'") % chrootRootDir); | 1979 | printMsg(lvlChatty, format("setting up chroot environment in `%1%'") % chrootRootDir); |
| 1834 | 1980 | ||
| 1835 | if (mkdir(chrootRootTop.c_str(), 0750) == -1) | 1981 | if (mkdir(chrootRootTop.c_str(), 0750) == -1) |
| @@ -1865,9 +2011,21 @@ void DerivationGoal::startBuilder() | |||
| 1865 | (format("nixbld:!:%1%:\n") | 2011 | (format("nixbld:!:%1%:\n") |
| 1866 | % (buildUser.enabled() ? buildUser.getGID() : guestGID)).str()); | 2012 | % (buildUser.enabled() ? buildUser.getGID() : guestGID)).str()); |
| 1867 | 2013 | ||
| 1868 | /* Create /etc/hosts with localhost entry. */ | 2014 | if (fixedOutput) { |
| 1869 | if (!fixedOutput) | 2015 | /* Fixed-output derivations typically need to access the network, |
| 2016 | so give them access to /etc/resolv.conf and so on. */ | ||
| 2017 | auto files = { "/etc/resolv.conf", "/etc/nsswitch.conf", | ||
| 2018 | "/etc/services", "/etc/hosts" }; | ||
| 2019 | for (auto & file: files) { | ||
| 2020 | if (pathExists(file)) { | ||
| 2021 | ctx.filesInChroot[file] = file; | ||
| 2022 | ctx.readOnlyFilesInChroot.insert(file); | ||
| 2023 | } | ||
| 2024 | } | ||
| 2025 | } else { | ||
| 2026 | /* Create /etc/hosts with localhost entry. */ | ||
| 1870 | writeFile(chrootRootDir + "/etc/hosts", "127.0.0.1 localhost\n"); | 2027 | writeFile(chrootRootDir + "/etc/hosts", "127.0.0.1 localhost\n"); |
| 2028 | } | ||
| 1871 | 2029 | ||
| 1872 | /* Bind-mount a user-configurable set of directories from the | 2030 | /* Bind-mount a user-configurable set of directories from the |
| 1873 | host file system. */ | 2031 | host file system. */ |
| @@ -1877,11 +2035,11 @@ void DerivationGoal::startBuilder() | |||
| 1877 | for (auto & i : dirs) { | 2035 | for (auto & i : dirs) { |
| 1878 | size_t p = i.find('='); | 2036 | size_t p = i.find('='); |
| 1879 | if (p == string::npos) | 2037 | if (p == string::npos) |
| 1880 | dirsInChroot[i] = i; | 2038 | ctx.filesInChroot[i] = i; |
| 1881 | else | 2039 | else |
| 1882 | dirsInChroot[string(i, 0, p)] = string(i, p + 1); | 2040 | ctx.filesInChroot[string(i, 0, p)] = string(i, p + 1); |
| 1883 | } | 2041 | } |
| 1884 | dirsInChroot[tmpDirInSandbox] = tmpDir; | 2042 | ctx.filesInChroot[tmpDirInSandbox] = tmpDir; |
| 1885 | 2043 | ||
| 1886 | /* Create the fake store. */ | 2044 | /* Create the fake store. */ |
| 1887 | Path chrootStoreDir = chrootRootDir + settings.nixStore; | 2045 | Path chrootStoreDir = chrootRootDir + settings.nixStore; |
| @@ -1897,22 +2055,8 @@ void DerivationGoal::startBuilder() | |||
| 1897 | the whole store. This prevents any access to undeclared | 2055 | the whole store. This prevents any access to undeclared |
| 1898 | dependencies. */ | 2056 | dependencies. */ |
| 1899 | for (auto& i : inputPaths) { | 2057 | for (auto& i : inputPaths) { |
| 1900 | struct stat st; | 2058 | ctx.filesInChroot[i] = i; |
| 1901 | if (lstat(i.c_str(), &st)) | 2059 | ctx.readOnlyFilesInChroot.insert(i); |
| 1902 | throw SysError(format("getting attributes of path `%1%'") % i); | ||
| 1903 | |||
| 1904 | if (S_ISLNK(st.st_mode)) { | ||
| 1905 | /* Since bind-mounts follow symlinks, thus representing their | ||
| 1906 | target and not the symlink itself, special-case | ||
| 1907 | symlinks. XXX: When running unprivileged, TARGET can be | ||
| 1908 | deleted by the build process. Use 'open_tree' & co. when | ||
| 1909 | it's more widely available. */ | ||
| 1910 | Path target = chrootRootDir + i; | ||
| 1911 | if (symlink(readLink(i).c_str(), target.c_str()) == -1) | ||
| 1912 | throw SysError(format("failed to create symlink '%1%' to '%2%'") % target % readLink(i)); | ||
| 1913 | } | ||
| 1914 | else | ||
| 1915 | dirsInChroot[i] = i; | ||
| 1916 | } | 2060 | } |
| 1917 | 2061 | ||
| 1918 | /* If we're repairing, checking or rebuilding part of a | 2062 | /* If we're repairing, checking or rebuilding part of a |
| @@ -1921,14 +2065,56 @@ void DerivationGoal::startBuilder() | |||
| 1921 | (typically the dependencies of /bin/sh). Throw them | 2065 | (typically the dependencies of /bin/sh). Throw them |
| 1922 | out. */ | 2066 | out. */ |
| 1923 | for (auto & i : drv.outputs) | 2067 | for (auto & i : drv.outputs) |
| 1924 | dirsInChroot.erase(i.second.path); | 2068 | ctx.filesInChroot.erase(i.second.path); |
| 2069 | |||
| 2070 | /* Set up a nearly empty /dev, unless the user asked to bind-mount the | ||
| 2071 | host /dev. */ | ||
| 2072 | Strings ss; | ||
| 2073 | if(ctx.filesInChroot.find("/dev") == ctx.filesInChroot.end()) { | ||
| 2074 | createDirs(chrootRootDir + "/dev/shm"); | ||
| 2075 | createDirs(chrootRootDir + "/dev/pts"); | ||
| 2076 | ss.push_back("/dev/full"); | ||
| 2077 | #ifdef __linux__ | ||
| 2078 | if (pathExists("/dev/kvm")) | ||
| 2079 | ss.push_back("/dev/kvm"); | ||
| 2080 | #endif | ||
| 2081 | ss.push_back("/dev/null"); | ||
| 2082 | ss.push_back("/dev/random"); | ||
| 2083 | ss.push_back("/dev/tty"); | ||
| 2084 | ss.push_back("/dev/urandom"); | ||
| 2085 | ss.push_back("/dev/zero"); | ||
| 2086 | createSymlink("/proc/self/fd", chrootRootDir + "/dev/fd"); | ||
| 2087 | createSymlink("/proc/self/fd/0", chrootRootDir + "/dev/stdin"); | ||
| 2088 | createSymlink("/proc/self/fd/1", chrootRootDir + "/dev/stdout"); | ||
| 2089 | createSymlink("/proc/self/fd/2", chrootRootDir + "/dev/stderr"); | ||
| 2090 | } | ||
| 1925 | 2091 | ||
| 2092 | for (auto & i : ss) ctx.filesInChroot[i] = i; | ||
| 2093 | |||
| 2094 | ctx.mountProc = true; | ||
| 2095 | ctx.mountDevshm = true; | ||
| 2096 | /* Mount a new devpts on /dev/pts. Note that this requires the kernel | ||
| 2097 | to be compiled with CONFIG_DEVPTS_MULTIPLE_INSTANCES=y (which is | ||
| 2098 | the case if /dev/ptx/ptmx exists). */ | ||
| 2099 | ctx.maybeMountDevpts = | ||
| 2100 | pathExists("/dev/pts/ptmx") && | ||
| 2101 | ctx.filesInChroot.find("/dev/pts") == ctx.filesInChroot.end(); | ||
| 2102 | ctx.lockMounts = !buildUser.enabled(); | ||
| 2103 | |||
| 2104 | for (auto & i : ctx.filesInChroot) { | ||
| 2105 | /* Failsafe: If the source is in the store, it should be | ||
| 2106 | read-only */ | ||
| 2107 | if(i.second.compare(0, settings.nixStore.length(), settings.nixStore) == 0) { | ||
| 2108 | ctx.readOnlyFilesInChroot.insert(i.first); | ||
| 2109 | } | ||
| 2110 | } | ||
| 1926 | #else | 2111 | #else |
| 1927 | throw Error("chroot builds are not supported on this platform"); | 2112 | throw Error("chroot builds are not supported on this platform"); |
| 1928 | #endif | 2113 | #endif |
| 1929 | } | 2114 | } |
| 1930 | 2115 | ||
| 1931 | else { | 2116 | else { |
| 2117 | ctx.phases = getBasicSpawnPhases(); | ||
| 1932 | 2118 | ||
| 1933 | if (pathExists(homeDir)) | 2119 | if (pathExists(homeDir)) |
| 1934 | throw Error(format("directory `%1%' exists; please remove it") % homeDir); | 2120 | throw Error(format("directory `%1%' exists; please remove it") % homeDir); |
| @@ -1956,6 +2142,7 @@ void DerivationGoal::startBuilder() | |||
| 1956 | } | 2142 | } |
| 1957 | } | 2143 | } |
| 1958 | 2144 | ||
| 2145 | replacePhase(ctx.phases, "exec", execBuilderOrBuiltinAction); | ||
| 1959 | 2146 | ||
| 1960 | /* Run the builder. */ | 2147 | /* Run the builder. */ |
| 1961 | printMsg(lvlChatty, format("executing builder `%1%'") % drv.builder); | 2148 | printMsg(lvlChatty, format("executing builder `%1%'") % drv.builder); |
| @@ -1966,6 +2153,9 @@ void DerivationGoal::startBuilder() | |||
| 1966 | /* Create a pipe to get the output of the builder. */ | 2153 | /* Create a pipe to get the output of the builder. */ |
| 1967 | builderOut.create(); | 2154 | builderOut.create(); |
| 1968 | 2155 | ||
| 2156 | ctx.logFD = builderOut.writeSide; | ||
| 2157 | ctx.earlyCloseFDs.insert(builderOut.readSide); | ||
| 2158 | |||
| 1969 | /* Fork a child to build the package. Note that while we | 2159 | /* Fork a child to build the package. Note that while we |
| 1970 | currently use forks to run and wait for the children, it | 2160 | currently use forks to run and wait for the children, it |
| 1971 | shouldn't be hard to use threads for this on systems where | 2161 | shouldn't be hard to use threads for this on systems where |
| @@ -1997,43 +2187,35 @@ void DerivationGoal::startBuilder() | |||
| 1997 | */ | 2187 | */ |
| 1998 | #if __linux__ | 2188 | #if __linux__ |
| 1999 | if (useChroot) { | 2189 | if (useChroot) { |
| 2000 | char stack[32 * 1024]; | 2190 | int fds[2]; |
| 2001 | int flags = CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWIPC | CLONE_NEWUTS | SIGCHLD; | 2191 | AutoCloseFD parentSetupSocket; |
| 2002 | if (!fixedOutput) { | 2192 | AutoCloseFD childSetupSocket; |
| 2003 | flags |= CLONE_NEWNET; | 2193 | |
| 2004 | } | 2194 | if(((ctx.cloneFlags & CLONE_NEWUSER) != 0)) { |
| 2005 | if (!buildUser.enabled() || getuid() != 0) { | 2195 | if (socketpair(AF_LOCAL, SOCK_STREAM, 0, fds)) |
| 2006 | flags |= CLONE_NEWUSER; | 2196 | throw SysError("creating setup socket"); |
| 2007 | readiness.create(); | 2197 | parentSetupSocket = fds[0]; |
| 2008 | } | 2198 | closeOnExec(parentSetupSocket); |
| 2199 | ctx.earlyCloseFDs.insert(parentSetupSocket); | ||
| 2200 | childSetupSocket = fds[1]; | ||
| 2201 | closeOnExec(childSetupSocket); | ||
| 2202 | ctx.setupFD = childSetupSocket; | ||
| 2203 | } | ||
| 2009 | 2204 | ||
| 2010 | /* Ensure proper alignment on the stack. On aarch64, it has to be 16 | 2205 | pid = cloneChild(ctx); |
| 2011 | bytes. */ | ||
| 2012 | pid = clone(childEntry, | ||
| 2013 | (char *)(((uintptr_t)stack + sizeof(stack) - 8) & ~(uintptr_t)0xf), | ||
| 2014 | flags, this); | ||
| 2015 | if (pid == -1) { | ||
| 2016 | if ((flags & CLONE_NEWUSER) != 0 && getuid() != 0) | ||
| 2017 | /* 'clone' fails with EPERM on distros where unprivileged user | ||
| 2018 | namespaces are disabled. Error out instead of giving up on | ||
| 2019 | isolation. */ | ||
| 2020 | throw SysError("cannot create process in unprivileged user namespace"); | ||
| 2021 | else | ||
| 2022 | throw SysError("cloning builder process"); | ||
| 2023 | } | ||
| 2024 | 2206 | ||
| 2025 | readiness.readSide.close(); | 2207 | if(childSetupSocket >= 0) childSetupSocket.close(); |
| 2026 | if ((flags & CLONE_NEWUSER) != 0) { | 2208 | |
| 2027 | /* Initialize the UID/GID mapping of the child process. */ | 2209 | if ((ctx.cloneFlags & CLONE_NEWUSER) != 0) { |
| 2028 | initializeUserNamespace(pid); | 2210 | /* Initialize the UID/GID mapping of the builder. */ |
| 2029 | writeFull(readiness.writeSide, (unsigned char*)"go\n", 3); | 2211 | initializeUserNamespace(pid); |
| 2030 | } | 2212 | writeFull(parentSetupSocket, (unsigned char*)"go\n", 3); |
| 2031 | readiness.writeSide.close(); | 2213 | } |
| 2032 | } else | 2214 | } else |
| 2033 | #endif | 2215 | #endif |
| 2034 | { | 2216 | { |
| 2035 | pid = fork(); | 2217 | pid = fork(); |
| 2036 | if (pid == 0) runChild(); | 2218 | if (pid == 0) runChildSetup(ctx); |
| 2037 | } | 2219 | } |
| 2038 | 2220 | ||
| 2039 | if (pid == -1) throw SysError("unable to fork"); | 2221 | if (pid == -1) throw SysError("unable to fork"); |
| @@ -2049,415 +2231,9 @@ void DerivationGoal::startBuilder() | |||
| 2049 | 2231 | ||
| 2050 | if (settings.printBuildTrace) { | 2232 | if (settings.printBuildTrace) { |
| 2051 | printMsg(lvlError, format("@ build-started %1% - %2% %3% %4%") | 2233 | printMsg(lvlError, format("@ build-started %1% - %2% %3% %4%") |
| 2052 | % drvPath % drv.platform % logFile % pid); | 2234 | % drvPath % drv.platform % logFile % pid); |
| 2053 | } | ||
| 2054 | |||
| 2055 | } | ||
| 2056 | |||
| 2057 | /* Return true if the operating system kernel part of SYSTEM1 and SYSTEM2 (the | ||
| 2058 | bit that comes after the hyphen in system types such as "i686-linux") is | ||
| 2059 | the same. */ | ||
| 2060 | static bool sameOperatingSystemKernel(const std::string& system1, const std::string& system2) | ||
| 2061 | { | ||
| 2062 | auto os1 = system1.substr(system1.find("-")); | ||
| 2063 | auto os2 = system2.substr(system2.find("-")); | ||
| 2064 | return os1 == os2; | ||
| 2065 | } | ||
| 2066 | |||
| 2067 | void DerivationGoal::runChild() | ||
| 2068 | { | ||
| 2069 | /* Warning: in the child we should absolutely not make any SQLite | ||
| 2070 | calls! */ | ||
| 2071 | |||
| 2072 | try { /* child */ | ||
| 2073 | |||
| 2074 | _writeToStderr = 0; | ||
| 2075 | |||
| 2076 | if (readiness.writeSide >= 0) readiness.writeSide.close(); | ||
| 2077 | |||
| 2078 | if (readiness.readSide >= 0) { | ||
| 2079 | /* Wait for the parent process to initialize the UID/GID mapping | ||
| 2080 | of our user namespace. */ | ||
| 2081 | char str[20] = { '\0' }; | ||
| 2082 | readFull(readiness.readSide, (unsigned char*)str, 3); | ||
| 2083 | readiness.readSide.close(); | ||
| 2084 | if (strcmp(str, "go\n") != 0) | ||
| 2085 | throw Error("failed to initialize process in unprivileged user namespace"); | ||
| 2086 | } | ||
| 2087 | |||
| 2088 | restoreAffinity(); | ||
| 2089 | |||
| 2090 | commonChildInit(builderOut); | ||
| 2091 | |||
| 2092 | #if CHROOT_ENABLED | ||
| 2093 | if (useChroot) { | ||
| 2094 | # if HAVE_SYS_PRCTL_H | ||
| 2095 | /* Drop ambient capabilities such as CAP_CHOWN that might have | ||
| 2096 | been granted when starting guix-daemon. */ | ||
| 2097 | prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0); | ||
| 2098 | # endif | ||
| 2099 | |||
| 2100 | if (!fixedOutput) { | ||
| 2101 | /* Initialise the loopback interface. */ | ||
| 2102 | AutoCloseFD fd(socket(PF_INET, SOCK_DGRAM, IPPROTO_IP)); | ||
| 2103 | if (fd == -1) throw SysError("cannot open IP socket"); | ||
| 2104 | |||
| 2105 | struct ifreq ifr; | ||
| 2106 | strcpy(ifr.ifr_name, "lo"); | ||
| 2107 | ifr.ifr_flags = IFF_UP | IFF_LOOPBACK | IFF_RUNNING; | ||
| 2108 | if (ioctl(fd, SIOCSIFFLAGS, &ifr) == -1) | ||
| 2109 | throw SysError("cannot set loopback interface flags"); | ||
| 2110 | |||
| 2111 | fd.close(); | ||
| 2112 | } | ||
| 2113 | |||
| 2114 | /* Set the hostname etc. to fixed values. */ | ||
| 2115 | char hostname[] = "localhost"; | ||
| 2116 | if (sethostname(hostname, sizeof(hostname)) == -1) | ||
| 2117 | throw SysError("cannot set host name"); | ||
| 2118 | char domainname[] = "(none)"; // kernel default | ||
| 2119 | if (setdomainname(domainname, sizeof(domainname)) == -1) | ||
| 2120 | throw SysError("cannot set domain name"); | ||
| 2121 | |||
| 2122 | /* Make all filesystems private. This is necessary | ||
| 2123 | because subtrees may have been mounted as "shared" | ||
| 2124 | (MS_SHARED). (Systemd does this, for instance.) Even | ||
| 2125 | though we have a private mount namespace, mounting | ||
| 2126 | filesystems on top of a shared subtree still propagates | ||
| 2127 | outside of the namespace. Making a subtree private is | ||
| 2128 | local to the namespace, though, so setting MS_PRIVATE | ||
| 2129 | does not affect the outside world. */ | ||
| 2130 | if (mount(0, "/", 0, MS_REC|MS_PRIVATE, 0) == -1) { | ||
| 2131 | throw SysError("unable to make ‘/’ private mount"); | ||
| 2132 | } | ||
| 2133 | |||
| 2134 | /* Bind-mount chroot directory to itself, to treat it as a | ||
| 2135 | different filesystem from /, as needed for pivot_root. */ | ||
| 2136 | if (mount(chrootRootDir.c_str(), chrootRootDir.c_str(), 0, MS_BIND, 0) == -1) | ||
| 2137 | throw SysError(format("unable to bind mount ‘%1%’") % chrootRootDir); | ||
| 2138 | |||
| 2139 | /* Set up a nearly empty /dev, unless the user asked to | ||
| 2140 | bind-mount the host /dev. */ | ||
| 2141 | Strings ss; | ||
| 2142 | if (dirsInChroot.find("/dev") == dirsInChroot.end()) { | ||
| 2143 | createDirs(chrootRootDir + "/dev/shm"); | ||
| 2144 | createDirs(chrootRootDir + "/dev/pts"); | ||
| 2145 | ss.push_back("/dev/full"); | ||
| 2146 | #ifdef __linux__ | ||
| 2147 | if (pathExists("/dev/kvm")) | ||
| 2148 | ss.push_back("/dev/kvm"); | ||
| 2149 | #endif | ||
| 2150 | ss.push_back("/dev/null"); | ||
| 2151 | ss.push_back("/dev/random"); | ||
| 2152 | ss.push_back("/dev/tty"); | ||
| 2153 | ss.push_back("/dev/urandom"); | ||
| 2154 | ss.push_back("/dev/zero"); | ||
| 2155 | createSymlink("/proc/self/fd", chrootRootDir + "/dev/fd"); | ||
| 2156 | createSymlink("/proc/self/fd/0", chrootRootDir + "/dev/stdin"); | ||
| 2157 | createSymlink("/proc/self/fd/1", chrootRootDir + "/dev/stdout"); | ||
| 2158 | createSymlink("/proc/self/fd/2", chrootRootDir + "/dev/stderr"); | ||
| 2159 | } | ||
| 2160 | |||
| 2161 | /* Fixed-output derivations typically need to access the | ||
| 2162 | network, so give them access to /etc/resolv.conf and so | ||
| 2163 | on. */ | ||
| 2164 | if (fixedOutput) { | ||
| 2165 | auto files = { "/etc/resolv.conf", "/etc/nsswitch.conf", | ||
| 2166 | "/etc/services", "/etc/hosts" }; | ||
| 2167 | for (auto & file: files) { | ||
| 2168 | if (pathExists(file)) ss.push_back(file); | ||
| 2169 | } | ||
| 2170 | } | ||
| 2171 | |||
| 2172 | for (auto & i : ss) dirsInChroot[i] = i; | ||
| 2173 | |||
| 2174 | /* Make new mounts for the store and for /tmp. That way, when | ||
| 2175 | 'chrootRootDir' is made read-only below, these two mounts will | ||
| 2176 | remain writable (the store needs to be writable so derivation | ||
| 2177 | outputs can be written to it, and /tmp is writable by | ||
| 2178 | convention). */ | ||
| 2179 | auto chrootStoreDir = chrootRootDir + settings.nixStore; | ||
| 2180 | if (mount(chrootStoreDir.c_str(), chrootStoreDir.c_str(), 0, MS_BIND, 0) == -1) | ||
| 2181 | throw SysError(format("read-write mount of store '%1%' failed") % chrootStoreDir); | ||
| 2182 | auto chrootTmpDir = chrootRootDir + "/tmp"; | ||
| 2183 | if (mount(chrootTmpDir.c_str(), chrootTmpDir.c_str(), 0, MS_BIND, 0) == -1) | ||
| 2184 | throw SysError(format("read-write mount of temporary directory '%1%' failed") % chrootTmpDir); | ||
| 2185 | |||
| 2186 | /* Bind-mount all the directories from the "host" | ||
| 2187 | filesystem that we want in the chroot | ||
| 2188 | environment. */ | ||
| 2189 | for (auto& i : dirsInChroot) { | ||
| 2190 | struct stat st; | ||
| 2191 | Path source = i.second; | ||
| 2192 | Path target = chrootRootDir + i.first; | ||
| 2193 | if (source == "/proc") continue; // backwards compatibility | ||
| 2194 | if (stat(source.c_str(), &st) == -1) | ||
| 2195 | throw SysError(format("getting attributes of path `%1%'") % source); | ||
| 2196 | if (S_ISDIR(st.st_mode)) | ||
| 2197 | createDirs(target); | ||
| 2198 | else { | ||
| 2199 | createDirs(dirOf(target)); | ||
| 2200 | writeFile(target, ""); | ||
| 2201 | } | ||
| 2202 | |||
| 2203 | /* Extra flags passed with MS_BIND are ignored, hence the | ||
| 2204 | extra MS_REMOUNT. */ | ||
| 2205 | if (mount(source.c_str(), target.c_str(), "", MS_BIND, 0) == -1) | ||
| 2206 | throw SysError(format("bind mount from `%1%' to `%2%' failed") % source % target); | ||
| 2207 | if (source.compare(0, settings.nixStore.length(), settings.nixStore) == 0) { | ||
| 2208 | if (mount(source.c_str(), target.c_str(), "", MS_BIND | MS_REMOUNT | MS_RDONLY, 0) == -1) | ||
| 2209 | throw SysError(format("read-only remount of `%1%' failed") % target); | ||
| 2210 | } | ||
| 2211 | } | ||
| 2212 | |||
| 2213 | /* Bind a new instance of procfs on /proc to reflect our | ||
| 2214 | private PID namespace. */ | ||
| 2215 | createDirs(chrootRootDir + "/proc"); | ||
| 2216 | if (mount("none", (chrootRootDir + "/proc").c_str(), "proc", 0, 0) == -1) | ||
| 2217 | throw SysError("mounting /proc"); | ||
| 2218 | |||
| 2219 | /* Mount a new tmpfs on /dev/shm to ensure that whatever | ||
| 2220 | the builder puts in /dev/shm is cleaned up automatically. */ | ||
| 2221 | if (pathExists("/dev/shm") && mount("none", (chrootRootDir + "/dev/shm").c_str(), "tmpfs", 0, 0) == -1) | ||
| 2222 | throw SysError("mounting /dev/shm"); | ||
| 2223 | |||
| 2224 | /* Mount a new devpts on /dev/pts. Note that this | ||
| 2225 | requires the kernel to be compiled with | ||
| 2226 | CONFIG_DEVPTS_MULTIPLE_INSTANCES=y (which is the case | ||
| 2227 | if /dev/ptx/ptmx exists). */ | ||
| 2228 | if (pathExists("/dev/pts/ptmx") && | ||
| 2229 | !pathExists(chrootRootDir + "/dev/ptmx") | ||
| 2230 | && dirsInChroot.find("/dev/pts") == dirsInChroot.end()) | ||
| 2231 | { | ||
| 2232 | if (mount("none", (chrootRootDir + "/dev/pts").c_str(), "devpts", 0, "newinstance,mode=0620") == -1) | ||
| 2233 | throw SysError("mounting /dev/pts"); | ||
| 2234 | createSymlink("/dev/pts/ptmx", chrootRootDir + "/dev/ptmx"); | ||
| 2235 | |||
| 2236 | /* Make sure /dev/pts/ptmx is world-writable. With some | ||
| 2237 | Linux versions, it is created with permissions 0. */ | ||
| 2238 | chmod_(chrootRootDir + "/dev/pts/ptmx", 0666); | ||
| 2239 | } | ||
| 2240 | |||
| 2241 | /* Do the chroot(). */ | ||
| 2242 | if (chdir(chrootRootDir.c_str()) == -1) | ||
| 2243 | throw SysError(format("cannot change directory to '%1%'") % chrootRootDir); | ||
| 2244 | |||
| 2245 | if (mkdir("real-root", 0) == -1) | ||
| 2246 | throw SysError("cannot create real-root directory"); | ||
| 2247 | |||
| 2248 | if (pivot_root(".", "real-root") == -1) | ||
| 2249 | throw SysError(format("cannot pivot old root directory onto '%1%'") % (chrootRootDir + "/real-root")); | ||
| 2250 | |||
| 2251 | if (chroot(".") == -1) | ||
| 2252 | throw SysError(format("cannot change root directory to '%1%'") % chrootRootDir); | ||
| 2253 | |||
| 2254 | if (umount2("real-root", MNT_DETACH) == -1) | ||
| 2255 | throw SysError("cannot unmount real root filesystem"); | ||
| 2256 | |||
| 2257 | if (rmdir("real-root") == -1) | ||
| 2258 | throw SysError("cannot remove real-root directory"); | ||
| 2259 | |||
| 2260 | /* Make the root read-only. | ||
| 2261 | |||
| 2262 | When build users are disabled, the build process could make it | ||
| 2263 | world-accessible, but that's OK: since 'chrootRootTop' is *not* | ||
| 2264 | world-accessible, a world-accessible 'chrootRootDir' cannot be | ||
| 2265 | used to grant access to the build environment to external | ||
| 2266 | processes. | ||
| 2267 | |||
| 2268 | Remounting the root as read-only was rejected because it makes | ||
| 2269 | write access fail with EROFS instead of EACCES, which goes | ||
| 2270 | against what some test suites expect (Go, Ruby, SCons, | ||
| 2271 | Shepherd, to name a few). */ | ||
| 2272 | chmod_("/", 0555); | ||
| 2273 | |||
| 2274 | if (getuid() != 0) { | ||
| 2275 | /* Create a new mount namespace to "lock" previous mounts. | ||
| 2276 | See mount_namespaces(7). */ | ||
| 2277 | auto uid = getuid(); | ||
| 2278 | auto gid = getgid(); | ||
| 2279 | |||
| 2280 | if (unshare(CLONE_NEWNS | CLONE_NEWUSER) == -1) | ||
| 2281 | throw SysError(format("creating new user and mount namespaces")); | ||
| 2282 | |||
| 2283 | initializeUserNamespace(getpid(), uid, gid); | ||
| 2284 | |||
| 2285 | /* Check that mounts within the build environment are "locked" | ||
| 2286 | together and cannot be separated from within the build | ||
| 2287 | environment namespace. Since | ||
| 2288 | umount(2) is documented to fail with EINVAL when attempting | ||
| 2289 | to unmount one of the mounts that are locked together, | ||
| 2290 | check that this is what we get. */ | ||
| 2291 | int ret = umount(tmpDirInSandbox.c_str()); | ||
| 2292 | assert(ret == -1 && errno == EINVAL); | ||
| 2293 | } | ||
| 2294 | } | ||
| 2295 | #endif | ||
| 2296 | |||
| 2297 | if (chdir(tmpDirInSandbox.c_str()) == -1) | ||
| 2298 | throw SysError(format("changing into `%1%'") % tmpDir); | ||
| 2299 | |||
| 2300 | /* Close all other file descriptors. */ | ||
| 2301 | closeMostFDs(set<int>()); | ||
| 2302 | |||
| 2303 | #if __linux__ | ||
| 2304 | /* Change the personality to 32-bit if we're doing an | ||
| 2305 | i686-linux build on an x86_64-linux machine. */ | ||
| 2306 | struct utsname utsbuf; | ||
| 2307 | uname(&utsbuf); | ||
| 2308 | if (drv.platform == "i686-linux" && | ||
| 2309 | (settings.thisSystem == "x86_64-linux" || | ||
| 2310 | (!strcmp(utsbuf.sysname, "Linux") && !strcmp(utsbuf.machine, "x86_64")))) { | ||
| 2311 | if (personality(PER_LINUX32) == -1) | ||
| 2312 | throw SysError("cannot set i686-linux personality"); | ||
| 2313 | } | ||
| 2314 | |||
| 2315 | if (drv.platform == "armhf-linux" && | ||
| 2316 | (settings.thisSystem == "aarch64-linux" || | ||
| 2317 | (!strcmp(utsbuf.sysname, "Linux") && !strcmp(utsbuf.machine, "aarch64")))) { | ||
| 2318 | if (personality(PER_LINUX32) == -1) | ||
| 2319 | throw SysError("cannot set armhf-linux personality"); | ||
| 2320 | } | ||
| 2321 | |||
| 2322 | /* Impersonate a Linux 2.6 machine to get some determinism in | ||
| 2323 | builds that depend on the kernel version. */ | ||
| 2324 | if ((drv.platform == "i686-linux" || drv.platform == "x86_64-linux") && settings.impersonateLinux26) { | ||
| 2325 | int cur = personality(0xffffffff); | ||
| 2326 | if (cur != -1) personality(cur | 0x0020000 /* == UNAME26 */); | ||
| 2327 | } | ||
| 2328 | |||
| 2329 | /* Disable address space randomization for improved | ||
| 2330 | determinism. */ | ||
| 2331 | int cur = personality(0xffffffff); | ||
| 2332 | if (cur != -1) personality(cur | ADDR_NO_RANDOMIZE); | ||
| 2333 | |||
| 2334 | /* Ask the kernel to eagerly kill us & our children if it runs out of | ||
| 2335 | memory, regardless of blame, to preserve ‘real’ user data & state. */ | ||
| 2336 | try { | ||
| 2337 | writeFile("/proc/self/oom_score_adj", "1000"); // 100% | ||
| 2338 | } catch (...) { ignoreException(); } | ||
| 2339 | #endif | ||
| 2340 | |||
| 2341 | /* Fill in the environment. */ | ||
| 2342 | Strings envStrs; | ||
| 2343 | for (const auto& i : env) | ||
| 2344 | envStrs.push_back(rewriteHashes(i.first + "=" + i.second, rewritesToTmp)); | ||
| 2345 | |||
| 2346 | /* If we are running in `build-users' mode, then switch to the | ||
| 2347 | user we allocated above. Make sure that we drop all root | ||
| 2348 | privileges. Note that above we have closed all file | ||
| 2349 | descriptors except std*, so that's safe. Also note that | ||
| 2350 | setuid() when run as root sets the real, effective and | ||
| 2351 | saved UIDs. */ | ||
| 2352 | if (buildUser.enabled()) { | ||
| 2353 | /* Preserve supplementary groups of the build user, to allow | ||
| 2354 | admins to specify groups such as "kvm". */ | ||
| 2355 | if (setgroups(buildUser.getSupplementaryGIDs().size(), | ||
| 2356 | buildUser.getSupplementaryGIDs().data()) == -1) | ||
| 2357 | throw SysError("cannot set supplementary groups of build user"); | ||
| 2358 | |||
| 2359 | if (setgid(buildUser.getGID()) == -1 || | ||
| 2360 | getgid() != buildUser.getGID() || | ||
| 2361 | getegid() != buildUser.getGID()) | ||
| 2362 | throw SysError("setgid failed"); | ||
| 2363 | |||
| 2364 | if (setuid(buildUser.getUID()) == -1 || | ||
| 2365 | getuid() != buildUser.getUID() || | ||
| 2366 | geteuid() != buildUser.getUID()) | ||
| 2367 | throw SysError("setuid failed"); | ||
| 2368 | } | ||
| 2369 | |||
| 2370 | restoreSIGPIPE(); | ||
| 2371 | |||
| 2372 | /* Indicate that we managed to set up the build environment. */ | ||
| 2373 | writeFull(STDERR_FILENO, "\n"); | ||
| 2374 | |||
| 2375 | /* Execute the program. This should not return. */ | ||
| 2376 | string builderBasename; | ||
| 2377 | if (isBuiltin(drv)) { | ||
| 2378 | try { | ||
| 2379 | logType = ltFlat; | ||
| 2380 | |||
| 2381 | auto buildDrv = lookupBuiltinBuilder(drv.builder); | ||
| 2382 | if (buildDrv != NULL) { | ||
| 2383 | /* Check what the output file name is. When doing a | ||
| 2384 | 'bmCheck' build, the output file name is different from | ||
| 2385 | that specified in DRV due to hash rewriting. */ | ||
| 2386 | Path output = drv.outputs["out"].path; | ||
| 2387 | auto redirected = redirectedOutputs.find(output); | ||
| 2388 | if (redirected != redirectedOutputs.end()) | ||
| 2389 | output = redirected->second; | ||
| 2390 | |||
| 2391 | buildDrv(drv, drvPath, output); | ||
| 2392 | } | ||
| 2393 | else | ||
| 2394 | throw Error(format("unsupported builtin function '%1%'") % string(drv.builder, 8)); | ||
| 2395 | _exit(0); | ||
| 2396 | } catch (std::exception & e) { | ||
| 2397 | writeFull(STDERR_FILENO, "error: " + string(e.what()) + "\n"); | ||
| 2398 | _exit(1); | ||
| 2399 | } | ||
| 2400 | } else { | ||
| 2401 | /* Ensure that the builder is within the store. This prevents | ||
| 2402 | users from using /proc/self/exe (or a symlink to it) as their | ||
| 2403 | builder, which could allow them to overwrite the guix-daemon | ||
| 2404 | binary (CVE-2019-5736). | ||
| 2405 | |||
| 2406 | This attack is possible even if the target of /proc/self/exe is | ||
| 2407 | outside the chroot (it's as if it were a hard link), though it | ||
| 2408 | requires that its ELF interpreter and dependencies be in the | ||
| 2409 | chroot. | ||
| 2410 | |||
| 2411 | Note: 'canonPath' throws if 'drv.builder' cannot be resolved | ||
| 2412 | within the chroot. */ | ||
| 2413 | builderBasename = baseNameOf(drv.builder); | ||
| 2414 | drv.builder = canonPath(drv.builder, true); | ||
| 2415 | |||
| 2416 | if (!isInStore(drv.builder)) | ||
| 2417 | throw Error(format("derivation builder '%1%' is outside the store") % drv.builder); | ||
| 2418 | } | ||
| 2419 | |||
| 2420 | /* Fill in the arguments. */ | ||
| 2421 | Strings args; | ||
| 2422 | args.push_back(builderBasename); | ||
| 2423 | for (auto& i : drv.args) | ||
| 2424 | args.push_back(rewriteHashes(i, rewritesToTmp)); | ||
| 2425 | |||
| 2426 | /* If DRV targets the same operating system kernel, try to execute it: | ||
| 2427 | there might be binfmt_misc set up for user-land emulation of other | ||
| 2428 | architectures. However, if it targets a different operating | ||
| 2429 | system--e.g., "i586-gnu" vs. "x86_64-linux"--do not try executing | ||
| 2430 | it: the ELF file for that OS is likely indistinguishable from a | ||
| 2431 | native ELF binary and it would just crash at run time. */ | ||
| 2432 | int error; | ||
| 2433 | if (sameOperatingSystemKernel(drv.platform, settings.thisSystem)) { | ||
| 2434 | execve(drv.builder.c_str(), stringsToCharPtrs(args).data(), | ||
| 2435 | stringsToCharPtrs(envStrs).data()); | ||
| 2436 | error = errno; | ||
| 2437 | } else { | ||
| 2438 | error = ENOEXEC; | ||
| 2439 | } | ||
| 2440 | |||
| 2441 | /* Right platform? Check this after we've tried 'execve' to allow for | ||
| 2442 | transparent emulation of different platforms with binfmt_misc | ||
| 2443 | handlers that invoke QEMU. */ | ||
| 2444 | if (error == ENOEXEC && !canBuildLocally(drv.platform)) { | ||
| 2445 | if (settings.printBuildTrace) | ||
| 2446 | printMsg(lvlError, format("@ unsupported-platform %1% %2%") % drvPath % drv.platform); | ||
| 2447 | throw Error( | ||
| 2448 | format("a `%1%' is required to build `%3%', but I am a `%2%'") | ||
| 2449 | % drv.platform % settings.thisSystem % drvPath); | ||
| 2450 | } | ||
| 2451 | |||
| 2452 | errno = error; | ||
| 2453 | throw SysError(format("executing `%1%'") % drv.builder); | ||
| 2454 | |||
| 2455 | } catch (std::exception & e) { | ||
| 2456 | writeFull(STDERR_FILENO, "while setting up the build environment: " + string(e.what()) + "\n"); | ||
| 2457 | _exit(1); | ||
| 2458 | } | 2235 | } |
| 2459 | 2236 | ||
| 2460 | abort(); /* never reached */ | ||
| 2461 | } | 2237 | } |
| 2462 | 2238 | ||
| 2463 | 2239 | ||
diff --git a/nix/libutil/spawn.cc b/nix/libutil/spawn.cc new file mode 100644 index 00000000000..93bab9f59e4 --- /dev/null +++ b/nix/libutil/spawn.cc | |||
| @@ -0,0 +1,829 @@ | |||
| 1 | /* GNU Guix --- Functional package management for GNU | ||
| 2 | Copyright (C) 2025 Caleb Ristvedt <reepca@russelstein.xyz> | ||
| 3 | |||
| 4 | This file is part of GNU Guix. | ||
| 5 | |||
| 6 | GNU Guix is free software; you can redistribute it and/or modify it | ||
| 7 | under the terms of the GNU General Public License as published by | ||
| 8 | the Free Software Foundation; either version 3 of the License, or (at | ||
| 9 | your option) any later version. | ||
| 10 | |||
| 11 | GNU Guix is distributed in the hope that it will be useful, but | ||
| 12 | WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 14 | GNU General Public License for more details. | ||
| 15 | |||
| 16 | You should have received a copy of the GNU General Public License | ||
| 17 | along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. */ | ||
| 18 | |||
| 19 | /* Process spawning and setup code. */ | ||
| 20 | |||
| 21 | #include <spawn.hh> | ||
| 22 | #include <util.hh> | ||
| 23 | #include <affinity.hh> | ||
| 24 | #include <stddef.h> | ||
| 25 | #include <unistd.h> | ||
| 26 | #include <grp.h> | ||
| 27 | #include <limits.h> | ||
| 28 | #include <sys/wait.h> | ||
| 29 | #include <cstring> | ||
| 30 | #include <cstdlib> | ||
| 31 | |||
| 32 | #if HAVE_SYS_MOUNT_H | ||
| 33 | #include <sys/mount.h> | ||
| 34 | #endif | ||
| 35 | |||
| 36 | #if HAVE_SCHED_H | ||
| 37 | #include <sched.h> | ||
| 38 | #endif | ||
| 39 | |||
| 40 | #if HAVE_STATVFS | ||
| 41 | #include <sys/statvfs.h> | ||
| 42 | #endif | ||
| 43 | |||
| 44 | #if HAVE_SYS_SYSCALL_H | ||
| 45 | #include <sys/syscall.h> | ||
| 46 | #endif | ||
| 47 | |||
| 48 | #if HAVE_SYS_PRCTL_H | ||
| 49 | #include <sys/prctl.h> | ||
| 50 | #endif | ||
| 51 | |||
| 52 | #ifdef __linux__ | ||
| 53 | #include <sys/personality.h> | ||
| 54 | #endif | ||
| 55 | |||
| 56 | #if defined(SYS_pivot_root) | ||
| 57 | #define pivot_root(new_root, put_old) (syscall(SYS_pivot_root, new_root,put_old)) | ||
| 58 | #endif | ||
| 59 | |||
| 60 | |||
| 61 | #define CLONE_ENABLED defined(CLONE_NEWNS) | ||
| 62 | |||
| 63 | #if CLONE_ENABLED | ||
| 64 | #include <sys/ioctl.h> | ||
| 65 | #include <net/if.h> | ||
| 66 | #include <netinet/in.h> | ||
| 67 | #endif | ||
| 68 | |||
| 69 | namespace nix { | ||
| 70 | |||
| 71 | |||
| 72 | void addPhaseAfter(Phases & phases, string afterLabel, string addLabel, Action addAction) | ||
| 73 | { | ||
| 74 | for(auto i = phases.begin(); i != phases.end(); i++) | ||
| 75 | if((*i).label == afterLabel) { | ||
| 76 | i++; /* std::vector::insert inserts before, not after */ | ||
| 77 | Phase p; | ||
| 78 | p.label = addLabel; | ||
| 79 | p.action = addAction; | ||
| 80 | phases.insert(i, p); | ||
| 81 | return; | ||
| 82 | } | ||
| 83 | throw Error(format("label `%1%' not found in phases") % afterLabel); | ||
| 84 | } | ||
| 85 | |||
| 86 | |||
| 87 | void addPhaseBefore(Phases & phases, string beforeLabel, string addLabel, Action addAction) | ||
| 88 | { | ||
| 89 | for(auto i = phases.begin(); i != phases.end(); i++) | ||
| 90 | if((*i).label == beforeLabel) { | ||
| 91 | Phase p; | ||
| 92 | p.label = addLabel; | ||
| 93 | p.action = addAction; | ||
| 94 | phases.insert(i, p); | ||
| 95 | return; | ||
| 96 | } | ||
| 97 | throw Error(format("label `%1%' not found in phases") % beforeLabel); | ||
| 98 | } | ||
| 99 | |||
| 100 | |||
| 101 | void prependPhase(Phases & phases, string addLabel, Action addAction) | ||
| 102 | { | ||
| 103 | Phase p; | ||
| 104 | p.label = addLabel; | ||
| 105 | p.action = addAction; | ||
| 106 | phases.insert(phases.begin(), p); | ||
| 107 | } | ||
| 108 | |||
| 109 | |||
| 110 | void appendPhase(Phases & phases, string addLabel, Action addAction) | ||
| 111 | { | ||
| 112 | Phase p; | ||
| 113 | p.label = addLabel; | ||
| 114 | p.action = addAction; | ||
| 115 | phases.push_back(p); | ||
| 116 | } | ||
| 117 | |||
| 118 | |||
| 119 | void deletePhase(Phases & phases, string delLabel) | ||
| 120 | { | ||
| 121 | for(auto i = phases.begin(); i != phases.end(); i++) | ||
| 122 | if((*i).label == delLabel) { | ||
| 123 | phases.erase(i); | ||
| 124 | return; | ||
| 125 | } | ||
| 126 | throw Error(format("label `%1%' not found in phases") % delLabel); | ||
| 127 | } | ||
| 128 | |||
| 129 | |||
| 130 | void replacePhase(Phases & phases, string replaceLabel, Action newAction) | ||
| 131 | { | ||
| 132 | for(auto i = phases.begin(); i != phases.end(); i++) | ||
| 133 | if((*i).label == replaceLabel) { | ||
| 134 | (*i).action = newAction; | ||
| 135 | return; | ||
| 136 | } | ||
| 137 | throw Error(format("label `%1' not found in phases") % replaceLabel); | ||
| 138 | } | ||
| 139 | |||
| 140 | |||
| 141 | /* A curated selection of predefined actions */ | ||
| 142 | |||
| 143 | void reset_writeToStderrAction(SpawnContext & ctx) | ||
| 144 | { | ||
| 145 | _writeToStderr = 0; | ||
| 146 | } | ||
| 147 | |||
| 148 | |||
| 149 | void restoreAffinityAction(SpawnContext & ctx) | ||
| 150 | { | ||
| 151 | restoreAffinity(); | ||
| 152 | } | ||
| 153 | |||
| 154 | |||
| 155 | void setsidAction(SpawnContext & ctx) | ||
| 156 | { | ||
| 157 | /* Puts the current process in a separate session, which implies a | ||
| 158 | separate process group, so it doesn't receive group-directed signals | ||
| 159 | sent at the parent. The new session initially has no controlling | ||
| 160 | terminal, so it also doesn't receive terminal signals and can't open | ||
| 161 | /dev/tty. */ | ||
| 162 | if(ctx.setsid && setsid() == (pid_t)-1) | ||
| 163 | throw SysError("creating a new session"); | ||
| 164 | } | ||
| 165 | |||
| 166 | |||
| 167 | void earlyIOSetupAction(SpawnContext & ctx) | ||
| 168 | { | ||
| 169 | for(auto i = ctx.earlyCloseFDs.begin(); i != ctx.earlyCloseFDs.end(); i++) | ||
| 170 | if(close(*i) == -1) | ||
| 171 | throw SysError("closing fd"); | ||
| 172 | |||
| 173 | if(ctx.logFD != -1) { | ||
| 174 | if(dup2(ctx.logFD, STDOUT_FILENO) == -1) | ||
| 175 | throw SysError("cannot dup2 log fd into stdout fd"); | ||
| 176 | if(dup2(ctx.logFD, STDERR_FILENO) == -1) | ||
| 177 | throw SysError("cannot dup2 log fd into stderr fd"); | ||
| 178 | } | ||
| 179 | |||
| 180 | if(ctx.setStdin) { | ||
| 181 | if(ctx.stdinFD != -1) { | ||
| 182 | if(dup2(ctx.stdinFD, STDIN_FILENO) == -1) | ||
| 183 | throw SysError("cannot dup2 fd into stdin fd"); | ||
| 184 | } | ||
| 185 | else { | ||
| 186 | /* Doesn't make sense for it to be writable, but compatibility... */ | ||
| 187 | AutoCloseFD fd = open(ctx.stdinFile.c_str(), O_RDWR); | ||
| 188 | if(fd == -1) | ||
| 189 | throw SysError(format("cannot open `%1%'") % ctx.stdinFile); | ||
| 190 | if(dup2(fd, STDIN_FILENO) == -1) | ||
| 191 | throw SysError("cannot dup2 fd into stdin fd"); | ||
| 192 | } | ||
| 193 | } | ||
| 194 | } | ||
| 195 | |||
| 196 | |||
| 197 | void dropAmbientCapabilitiesAction(SpawnContext & ctx) | ||
| 198 | { | ||
| 199 | /* Drop ambient capabilities such as CAP_CHOWN that might have been granted | ||
| 200 | when starting guix-daemon. */ | ||
| 201 | if(ctx.dropAmbientCapabilities) | ||
| 202 | #if HAVE_SYS_PRCTL_H | ||
| 203 | prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0); | ||
| 204 | #else | ||
| 205 | throw Error("dropping ambient capabilities is not supported on this system"); | ||
| 206 | #endif | ||
| 207 | } | ||
| 208 | |||
| 209 | |||
| 210 | void chrootAction(SpawnContext & ctx) | ||
| 211 | { | ||
| 212 | if(ctx.doChroot) | ||
| 213 | #if HAVE_CHROOT | ||
| 214 | if(chroot(ctx.chrootRootDir.c_str()) == -1) | ||
| 215 | throw SysError(format("cannot change root directory to '%1%'") % ctx.chrootRootDir); | ||
| 216 | #else | ||
| 217 | throw Error("chroot is not supported on this system"); | ||
| 218 | #endif | ||
| 219 | } | ||
| 220 | |||
| 221 | |||
| 222 | void chdirAction(SpawnContext & ctx) | ||
| 223 | { | ||
| 224 | if(ctx.setcwd) | ||
| 225 | if(chdir(ctx.cwd.c_str()) == -1) | ||
| 226 | throw SysError(format("changing into `%1%'") % ctx.cwd); | ||
| 227 | } | ||
| 228 | |||
| 229 | |||
| 230 | void closeMostFDsAction(SpawnContext & ctx) | ||
| 231 | { | ||
| 232 | if(ctx.closeMostFDs) closeMostFDs(ctx.preserveFDs); | ||
| 233 | for(auto i = ctx.preserveFDs.begin(); i != ctx.preserveFDs.end(); i++) | ||
| 234 | keepOnExec(*i); | ||
| 235 | } | ||
| 236 | |||
| 237 | |||
| 238 | void setPersonalityAction(SpawnContext & ctx) | ||
| 239 | { | ||
| 240 | if(ctx.setPersona) | ||
| 241 | #ifdef __linux__ | ||
| 242 | if(personality(ctx.persona) == -1) | ||
| 243 | throw SysError("cannot set personality"); | ||
| 244 | #else | ||
| 245 | throw Error("setting the personality is not supported on this system"); | ||
| 246 | #endif | ||
| 247 | } | ||
| 248 | |||
| 249 | |||
| 250 | void oomSacrificeAction(SpawnContext & ctx) | ||
| 251 | { | ||
| 252 | #ifdef __linux__ | ||
| 253 | if(ctx.oomSacrifice) | ||
| 254 | /* Ask the kernel to eagerly kill us & our children if it runs out of | ||
| 255 | memory, regardless of blame, to preserve ‘real’ user data & | ||
| 256 | state. */ | ||
| 257 | try { | ||
| 258 | writeFile("/proc/self/oom_score_adj", "1000"); // 100% | ||
| 259 | } catch(...) { ignoreException(); } | ||
| 260 | #endif | ||
| 261 | } | ||
| 262 | |||
| 263 | |||
| 264 | void setIDsAction(SpawnContext & ctx) | ||
| 265 | { | ||
| 266 | if(ctx.setSupplementaryGroups) | ||
| 267 | if(setgroups(ctx.supplementaryGroups.size(), | ||
| 268 | ctx.supplementaryGroups.data()) == -1) | ||
| 269 | throw SysError("cannot set supplementary groups"); | ||
| 270 | |||
| 271 | if(ctx.setgid) | ||
| 272 | if(setgid(ctx.group) == -1 || | ||
| 273 | getgid() != ctx.group || | ||
| 274 | getegid() != ctx.group) | ||
| 275 | throw SysError("setgid failed"); | ||
| 276 | |||
| 277 | if(ctx.setuid) | ||
| 278 | if(setuid(ctx.user) == -1 || | ||
| 279 | getuid() != ctx.user || | ||
| 280 | geteuid() != ctx.user) | ||
| 281 | throw SysError("setuid failed"); | ||
| 282 | } | ||
| 283 | |||
| 284 | |||
| 285 | void restoreSIGPIPEAction(SpawnContext & ctx) | ||
| 286 | { | ||
| 287 | /* Restore default handling of SIGPIPE, otherwise some programs will | ||
| 288 | randomly say "Broken pipe". */ | ||
| 289 | struct sigaction act, oact; | ||
| 290 | act.sa_handler = SIG_DFL; | ||
| 291 | act.sa_flags = 0; | ||
| 292 | sigemptyset(&act.sa_mask); | ||
| 293 | if (sigaction(SIGPIPE, &act, &oact)) throw SysError("resetting SIGPIPE"); | ||
| 294 | } | ||
| 295 | |||
| 296 | |||
| 297 | void setupSuccessAction(SpawnContext & ctx) | ||
| 298 | { | ||
| 299 | if(ctx.signalSetupSuccess) | ||
| 300 | writeFull(STDERR_FILENO, "\n"); | ||
| 301 | } | ||
| 302 | |||
| 303 | |||
| 304 | void execAction(SpawnContext & ctx) | ||
| 305 | { | ||
| 306 | Strings envStrs; | ||
| 307 | std::vector<char *> envPtrs; | ||
| 308 | char **env; | ||
| 309 | if(ctx.inheritEnv) { | ||
| 310 | for(auto i = ctx.env.begin(); i != ctx.env.end(); i++) | ||
| 311 | if(setenv(i->first.c_str(), i->second.c_str(), 1) == -1) | ||
| 312 | throw SysError("setenv"); | ||
| 313 | env = environ; | ||
| 314 | } else { | ||
| 315 | for(auto i = ctx.env.begin(); i != ctx.env.end(); i++) | ||
| 316 | envStrs.push_back(i->first + "=" + i->second); | ||
| 317 | /* Need to keep the envPtrs vector alive as long as its .data()! */ | ||
| 318 | envPtrs = stringsToCharPtrs(envStrs); | ||
| 319 | env = envPtrs.data(); | ||
| 320 | } | ||
| 321 | if(execvpe(ctx.program.c_str(), stringsToCharPtrs(ctx.args).data(), env) == -1) | ||
| 322 | throw SysError(format("executing `%1%'") % ctx.program); | ||
| 323 | } | ||
| 324 | |||
| 325 | |||
| 326 | Phases getBasicSpawnPhases() | ||
| 327 | { | ||
| 328 | return { { "reset_writeToStderr", reset_writeToStderrAction }, | ||
| 329 | { "restoreAffinity", restoreAffinityAction }, | ||
| 330 | { "setsid", setsidAction }, | ||
| 331 | { "earlyIOSetup", earlyIOSetupAction }, | ||
| 332 | { "dropAmbientCapabilities", dropAmbientCapabilitiesAction }, | ||
| 333 | { "chroot", chrootAction }, | ||
| 334 | { "chdir", chdirAction }, | ||
| 335 | { "closeMostFDs", closeMostFDsAction }, | ||
| 336 | { "setPersonality", setPersonalityAction }, | ||
| 337 | { "oomSacrifice", oomSacrificeAction }, | ||
| 338 | { "setIDs", setIDsAction }, | ||
| 339 | { "restoreSIGPIPE", restoreSIGPIPEAction }, | ||
| 340 | { "setupSuccess", setupSuccessAction }, | ||
| 341 | { "exec", execAction } }; | ||
| 342 | } | ||
| 343 | |||
| 344 | |||
| 345 | void usernsInitSyncAction(SpawnContext & sctx) | ||
| 346 | { | ||
| 347 | #if CLONE_ENABLED | ||
| 348 | CloneSpawnContext & ctx = (CloneSpawnContext &) sctx; | ||
| 349 | if((ctx.cloneFlags & CLONE_NEWUSER) != 0) { | ||
| 350 | /* Close the earlyCloseFDs before we try reading anything */ | ||
| 351 | for(auto i = ctx.earlyCloseFDs.begin(); i != ctx.earlyCloseFDs.end(); i++) | ||
| 352 | if(close(*i) == -1) | ||
| 353 | throw SysError("closing fd"); | ||
| 354 | /* Don't try closing them again later */ | ||
| 355 | ctx.earlyCloseFDs.clear(); | ||
| 356 | /* Wait for the parent process to initialize the UID/GID mapping of | ||
| 357 | our user namespace. */ | ||
| 358 | waitForMessage(ctx.setupFD, "go\n"); | ||
| 359 | } | ||
| 360 | #endif | ||
| 361 | } | ||
| 362 | |||
| 363 | |||
| 364 | void usernsSetIDsAction(SpawnContext & sctx) | ||
| 365 | { | ||
| 366 | #if CLONE_ENABLED | ||
| 367 | CloneSpawnContext & ctx = (CloneSpawnContext &) sctx; | ||
| 368 | if((ctx.cloneFlags & CLONE_NEWUSER) != 0) { | ||
| 369 | /* Note: 'man capabilities' says that a transition from zero to | ||
| 370 | nonzero uids causes capabilities to be lost, but doesn't say what | ||
| 371 | happens when a transition from an unmapped (possibly zero) uid to a | ||
| 372 | nonzero uid happens. */ | ||
| 373 | if(ctx.usernsSetuid) | ||
| 374 | /* Since we presumably have CAP_SETUID, this sets the real, | ||
| 375 | effective, saved, and filesystem uids */ | ||
| 376 | if(setuid(ctx.usernsUser) != 0) | ||
| 377 | throw SysError("setuid"); | ||
| 378 | if(ctx.usernsSetgid) | ||
| 379 | /* Ditto but with gids */ | ||
| 380 | if(setgid(ctx.usernsGroup) != 0) | ||
| 381 | throw SysError("setgid"); | ||
| 382 | } | ||
| 383 | #endif | ||
| 384 | } | ||
| 385 | |||
| 386 | |||
| 387 | void initLoopbackAction(SpawnContext & sctx) | ||
| 388 | { | ||
| 389 | #if CLONE_ENABLED | ||
| 390 | CloneSpawnContext & ctx = (CloneSpawnContext &) sctx; | ||
| 391 | if(((ctx.cloneFlags & CLONE_NEWNET) != 0) && ctx.initLoopback) { | ||
| 392 | AutoCloseFD fd(socket(PF_INET, SOCK_DGRAM, IPPROTO_IP)); | ||
| 393 | if (fd == -1) throw SysError("cannot open IP socket"); | ||
| 394 | |||
| 395 | struct ifreq ifr; | ||
| 396 | strcpy(ifr.ifr_name, "lo"); | ||
| 397 | ifr.ifr_flags = IFF_UP | IFF_LOOPBACK | IFF_RUNNING; | ||
| 398 | if (ioctl(fd, SIOCSIFFLAGS, &ifr) == -1) | ||
| 399 | throw SysError("cannot set loopback interface flags"); | ||
| 400 | |||
| 401 | fd.close(); | ||
| 402 | } | ||
| 403 | #endif | ||
| 404 | } | ||
| 405 | |||
| 406 | |||
| 407 | void setHostAndDomainAction(SpawnContext & sctx) | ||
| 408 | { | ||
| 409 | #if CLONE_ENABLED | ||
| 410 | CloneSpawnContext & ctx = (CloneSpawnContext &) sctx; | ||
| 411 | if((ctx.cloneFlags & CLONE_NEWUTS) != 0) { | ||
| 412 | if (sethostname(ctx.hostname.c_str(), | ||
| 413 | strlen(ctx.hostname.c_str())) == -1) | ||
| 414 | throw SysError("cannot set host name"); | ||
| 415 | if (setdomainname(ctx.domainname.c_str(), | ||
| 416 | strlen(ctx.domainname.c_str())) == -1) | ||
| 417 | throw SysError("cannot set domain name"); | ||
| 418 | } | ||
| 419 | #endif | ||
| 420 | } | ||
| 421 | |||
| 422 | |||
| 423 | void makeFilesystemsPrivateAction(SpawnContext & sctx) | ||
| 424 | { | ||
| 425 | #if CLONE_ENABLED && HAVE_SYS_MOUNT_H && defined(MS_REC) && defined(MS_PRIVATE) | ||
| 426 | CloneSpawnContext & ctx = (CloneSpawnContext &) sctx; | ||
| 427 | if((ctx.cloneFlags & CLONE_NEWNS) != 0) { | ||
| 428 | if(mount(0, "/", 0, MS_REC|MS_PRIVATE, 0) == -1) | ||
| 429 | throw SysError("unable to make `/' private mount"); | ||
| 430 | } | ||
| 431 | #endif | ||
| 432 | } | ||
| 433 | |||
| 434 | |||
| 435 | void makeChrootSeparateFilesystemAction(SpawnContext & sctx) | ||
| 436 | { | ||
| 437 | #if CLONE_ENABLED && HAVE_SYS_MOUNT_H && defined(MS_BIND) | ||
| 438 | CloneSpawnContext & ctx = (CloneSpawnContext &) sctx; | ||
| 439 | if(((ctx.cloneFlags & CLONE_NEWNS) != 0) && ctx.doChroot) { | ||
| 440 | /* Bind-mount chroot directory to itself, to treat it as a different | ||
| 441 | filesystem from /, as needed for pivot_root. Alternatively, mount | ||
| 442 | a tmpfs on it. */ | ||
| 443 | if(ctx.mountTmpfsOnChroot) { | ||
| 444 | if(mount("none", ctx.chrootRootDir.c_str(), "tmpfs", 0, 0) == -1) | ||
| 445 | throw SysError(format("unable to mount tmpfs on `%1%'") % ctx.chrootRootDir); | ||
| 446 | } | ||
| 447 | else { | ||
| 448 | if(mount(ctx.chrootRootDir.c_str(), ctx.chrootRootDir.c_str(), 0, MS_BIND, 0) == -1) | ||
| 449 | throw SysError(format("unable to bind mount ‘%1%’") % ctx.chrootRootDir); | ||
| 450 | } | ||
| 451 | } | ||
| 452 | #endif | ||
| 453 | } | ||
| 454 | |||
| 455 | |||
| 456 | static int statfsToMountFlags(int f_flags) | ||
| 457 | { | ||
| 458 | #if HAVE_SYS_MOUNT_H && HAVE_STATVFS | ||
| 459 | int ret = 0; | ||
| 460 | #if defined(ST_RDONLY) && defined(MS_RDONLY) | ||
| 461 | if((f_flags & ST_RDONLY) != 0) ret |= MS_RDONLY; | ||
| 462 | #endif | ||
| 463 | #if defined(ST_NOSUID) && defined(MS_NOSUID) | ||
| 464 | if((f_flags & ST_NOSUID) != 0) ret |= MS_NOSUID; | ||
| 465 | #endif | ||
| 466 | #if defined(ST_NODEV) && defined(MS_NODEV) | ||
| 467 | if((f_flags & ST_NODEV) != 0) ret |= MS_NODEV; | ||
| 468 | #endif | ||
| 469 | #if defined(ST_NOEXEC) && defined(MS_NOEXEC) | ||
| 470 | if((f_flags & ST_NOEXEC) != 0) ret |= MS_NOEXEC; | ||
| 471 | #endif | ||
| 472 | #if defined(ST_NOATIME) && defined(MS_NOATIME) | ||
| 473 | if((f_flags & ST_NOATIME) != 0) ret |= MS_NOATIME; | ||
| 474 | #endif | ||
| 475 | #if defined(ST_NODIRATIME) && defined(MS_NODIRATIME) | ||
| 476 | if((f_flags & ST_NODIRATIME) != 0) ret |= MS_NODIRATIME; | ||
| 477 | #endif | ||
| 478 | #if defined(ST_RELATIME) && defined(MS_RELATIME) | ||
| 479 | if((f_flags & ST_RELATIME) != 0) ret |= MS_RELATIME; | ||
| 480 | #endif | ||
| 481 | return ret; | ||
| 482 | #else | ||
| 483 | throw Error("statfsToMountFlags not supported on this platform"); | ||
| 484 | #endif | ||
| 485 | } | ||
| 486 | |||
| 487 | |||
| 488 | void bindMount(Path source, Path target, bool readOnly) | ||
| 489 | { | ||
| 490 | #if HAVE_SYS_MOUNT_H && defined(MS_BIND) | ||
| 491 | struct stat st; | ||
| 492 | if (lstat(source.c_str(), &st) == -1) | ||
| 493 | throw SysError(format("getting attributes of path `%1%'") % source); | ||
| 494 | |||
| 495 | if(S_ISDIR(st.st_mode)) | ||
| 496 | createDirs(target); | ||
| 497 | else if(S_ISLNK(st.st_mode)) { | ||
| 498 | /* bind-mounts follow symlinks, thus representing their target and not | ||
| 499 | the symlink itself. Create a copy of the symlink instead.*/ | ||
| 500 | createDirs(dirOf(target)); | ||
| 501 | createSymlink(readLink(source), target); | ||
| 502 | return; | ||
| 503 | } | ||
| 504 | else { | ||
| 505 | createDirs(dirOf(target)); | ||
| 506 | writeFile(target, ""); | ||
| 507 | } | ||
| 508 | |||
| 509 | /* This may fail with EINVAL unless we specify MS_REC, specifically if we | ||
| 510 | are in an unprivileged mount namespace and not specifying MS_REC would | ||
| 511 | reveal subtrees that had been covered up. */ | ||
| 512 | if (mount(source.c_str(), target.c_str(), 0, MS_BIND|MS_REC, 0) == -1) | ||
| 513 | throw SysError(format("bind mount from `%1%' to `%2%' failed") % source % target); | ||
| 514 | if(readOnly) { | ||
| 515 | #if defined(MS_REMOUNT) && defined(MS_RDONLY) | ||
| 516 | /* Extra flags passed with MS_BIND are ignored, hence the extra | ||
| 517 | MS_REMOUNT. */ | ||
| 518 | unsigned long mount_flags = MS_BIND | MS_REMOUNT | MS_RDONLY; | ||
| 519 | /* MS_BIND | MS_REMOUNT sets all mountpoint flags, so we may get EPERM | ||
| 520 | unless we preserve the other flags (for example because it would | ||
| 521 | result in trying to clear the nosuid flag). */ | ||
| 522 | #if HAVE_STATVFS | ||
| 523 | struct statvfs stvfs; | ||
| 524 | if(statvfs(target.c_str(), &stvfs) == -1) | ||
| 525 | throw SysError(format("statvfs of `%1%'") % target); | ||
| 526 | mount_flags |= statfsToMountFlags(stvfs.f_flag); | ||
| 527 | #endif | ||
| 528 | |||
| 529 | if (mount(source.c_str(), target.c_str(), 0, mount_flags, 0) == -1) | ||
| 530 | throw SysError(format("read-only remount of `%1%' failed") % target); | ||
| 531 | #else | ||
| 532 | throw Error("remounting read-only is not supported on this platform"); | ||
| 533 | #endif | ||
| 534 | } | ||
| 535 | #endif | ||
| 536 | } | ||
| 537 | |||
| 538 | |||
| 539 | void mountIntoChroot(std::map<Path, Path> filesInChroot, | ||
| 540 | set<Path> readOnlyFiles, | ||
| 541 | Path chrootRootDir) | ||
| 542 | { | ||
| 543 | #if HAVE_SYS_MOUNT_H && defined(MS_BIND) | ||
| 544 | for(auto i = filesInChroot.begin(); i != filesInChroot.end(); i++) { | ||
| 545 | Path source = i->second; | ||
| 546 | Path target = chrootRootDir + i->first; | ||
| 547 | bool readOnly = readOnlyFiles.find(i->first) != readOnlyFiles.end(); | ||
| 548 | bindMount(source, target, readOnly); | ||
| 549 | } | ||
| 550 | #else | ||
| 551 | throw Error("bind mounting not supported on this platform"); | ||
| 552 | #endif | ||
| 553 | } | ||
| 554 | |||
| 555 | |||
| 556 | void mountIntoChrootAction(SpawnContext & sctx) | ||
| 557 | { | ||
| 558 | #if CLONE_ENABLED && HAVE_SYS_MOUNT_H && defined(MS_BIND) | ||
| 559 | CloneSpawnContext & ctx = (CloneSpawnContext &) sctx; | ||
| 560 | if((ctx.cloneFlags & CLONE_NEWNS) != 0 && ctx.doChroot) { | ||
| 561 | mountIntoChroot(ctx.filesInChroot, ctx.readOnlyFilesInChroot, ctx.chrootRootDir); | ||
| 562 | } | ||
| 563 | #endif | ||
| 564 | } | ||
| 565 | |||
| 566 | |||
| 567 | void mountProcAction(SpawnContext & sctx) | ||
| 568 | { | ||
| 569 | #if CLONE_ENABLED && HAVE_SYS_MOUNT_H | ||
| 570 | CloneSpawnContext & ctx = (CloneSpawnContext &) sctx; | ||
| 571 | if((ctx.cloneFlags & CLONE_NEWNS) != 0 && ctx.mountProc) { | ||
| 572 | Path target = (ctx.doChroot ? ctx.chrootRootDir : "") + "/proc"; | ||
| 573 | createDirs(target); | ||
| 574 | if(mount("none", target.c_str(), "proc", 0, 0) == -1) | ||
| 575 | throw SysError(format("mounting `%1%'") % target); | ||
| 576 | } | ||
| 577 | #endif | ||
| 578 | } | ||
| 579 | |||
| 580 | |||
| 581 | void mountDevshmAction(SpawnContext & sctx) | ||
| 582 | { | ||
| 583 | #if CLONE_ENABLED && HAVE_SYS_MOUNT_H | ||
| 584 | CloneSpawnContext & ctx = (CloneSpawnContext &) sctx; | ||
| 585 | if((ctx.cloneFlags & CLONE_NEWNS) != 0 && ctx.mountDevshm) { | ||
| 586 | Path target = (ctx.doChroot ? ctx.chrootRootDir : "") + "/dev/shm"; | ||
| 587 | createDirs(target); | ||
| 588 | if(mount("none", target.c_str(), "tmpfs", 0, 0) == -1) | ||
| 589 | throw SysError(format("mounting `%1%'") % target); | ||
| 590 | } | ||
| 591 | #endif | ||
| 592 | } | ||
| 593 | |||
| 594 | |||
| 595 | void mountDevptsAction(SpawnContext & sctx) | ||
| 596 | { | ||
| 597 | #if CLONE_ENABLED && HAVE_SYS_MOUNT_H | ||
| 598 | CloneSpawnContext & ctx = (CloneSpawnContext &) sctx; | ||
| 599 | if((ctx.cloneFlags & CLONE_NEWNS) != 0 && ctx.maybeMountDevpts) { | ||
| 600 | Path chroot = (ctx.doChroot ? ctx.chrootRootDir : ""); | ||
| 601 | Path target = chroot + "/dev/pts"; | ||
| 602 | if(pathExists(chroot + "/dev/ptmx")) return; | ||
| 603 | createDirs(target); | ||
| 604 | if(mount("none", target.c_str(), "devpts", 0, "newinstance,mode=0620") == -1) | ||
| 605 | throw SysError(format("mounting `%1%'") % target); | ||
| 606 | createSymlink("/dev/pts/ptmx", chroot + "/dev/ptmx"); | ||
| 607 | /* Make sure /dev/pts/ptmx is world-writable. With some Linux | ||
| 608 | versions, it is created with permissions 0. */ | ||
| 609 | Path targetPtmx = chroot + "/dev/pts/ptmx"; | ||
| 610 | if (chmod(targetPtmx.c_str(), 0666) == -1) | ||
| 611 | throw SysError(format("setting permissions on `%1%'") % targetPtmx); | ||
| 612 | } | ||
| 613 | #endif | ||
| 614 | } | ||
| 615 | |||
| 616 | |||
| 617 | void pivotRootAction(SpawnContext & sctx) | ||
| 618 | { | ||
| 619 | #if CLONE_ENABLED && HAVE_SYS_MOUNT_H | ||
| 620 | CloneSpawnContext & ctx = (CloneSpawnContext &) sctx; | ||
| 621 | if((ctx.cloneFlags & CLONE_NEWNS) != 0 && ctx.doChroot) { | ||
| 622 | if (chdir(ctx.chrootRootDir.c_str()) == -1) | ||
| 623 | throw SysError(format("cannot change directory to '%1%'") % ctx.chrootRootDir); | ||
| 624 | |||
| 625 | if (mkdir("real-root", 0) == -1) | ||
| 626 | throw SysError("cannot create real-root directory"); | ||
| 627 | |||
| 628 | if (pivot_root(".", "real-root") == -1) | ||
| 629 | throw SysError(format("cannot pivot old root directory onto '%1%'") % (ctx.chrootRootDir + "/real-root")); | ||
| 630 | |||
| 631 | if (chroot(".") == -1) | ||
| 632 | throw SysError(format("cannot change root directory to '%1%'") % ctx.chrootRootDir); | ||
| 633 | |||
| 634 | if (umount2("real-root", MNT_DETACH) == -1) | ||
| 635 | throw SysError("cannot unmount real root filesystem"); | ||
| 636 | |||
| 637 | if (rmdir("real-root") == -1) | ||
| 638 | throw SysError("cannot remove real-root directory"); | ||
| 639 | } | ||
| 640 | #endif | ||
| 641 | } | ||
| 642 | |||
| 643 | |||
| 644 | string idMapToIdentityMap(const string & map) | ||
| 645 | { | ||
| 646 | std::vector<string> mapLines = | ||
| 647 | tokenizeString<std::vector<string> >(map, "\n"); | ||
| 648 | string out; | ||
| 649 | |||
| 650 | for(auto & i : mapLines) { | ||
| 651 | std::vector<string> elements = | ||
| 652 | tokenizeString<std::vector<string> >(i, " "); | ||
| 653 | out.append(elements.at(0) + " " + elements.at(0) + " " + elements.at(2) + "\n"); | ||
| 654 | } | ||
| 655 | return out; | ||
| 656 | } | ||
| 657 | |||
| 658 | |||
| 659 | /* Initializing a user namespace with more than one id mapped requires | ||
| 660 | * capabilities in the *parent* user namespace, which may not even have any | ||
| 661 | * processes in it after unshare is called. So fork a child and have it do | ||
| 662 | * the initialization. */ | ||
| 663 | void unshareAndInitUserns(int flags, const string & uidMap, | ||
| 664 | const string & gidMap, bool allowSetgroups) | ||
| 665 | { | ||
| 666 | #if CLONE_ENABLED | ||
| 667 | pid_t pid_ = getpid(); | ||
| 668 | string pid = std::to_string(pid_); | ||
| 669 | Pipe toChild; | ||
| 670 | Pipe fromChild; | ||
| 671 | toChild.create(); | ||
| 672 | fromChild.create(); | ||
| 673 | pid_t child = fork(); | ||
| 674 | if(child == -1) | ||
| 675 | throw SysError("creating child process"); | ||
| 676 | if(child == 0) { | ||
| 677 | try { | ||
| 678 | toChild.writeSide.close(); | ||
| 679 | fromChild.readSide.close(); | ||
| 680 | waitForMessage(toChild.readSide, "ready\n"); | ||
| 681 | writeFile("/proc/" + pid + "/uid_map", uidMap); | ||
| 682 | writeFile("/proc/" + pid + "/setgroups", | ||
| 683 | allowSetgroups ? "allow" : "deny"); | ||
| 684 | writeFile("/proc/" + pid + "/gid_map", gidMap); | ||
| 685 | writeFull(fromChild.writeSide, (unsigned char*)"go\n", 3); | ||
| 686 | } catch(...) { | ||
| 687 | /* Don't unwind the stack in case of exception, halt | ||
| 688 | * immediately. */ | ||
| 689 | _exit(1); | ||
| 690 | } | ||
| 691 | _exit(EXIT_SUCCESS); | ||
| 692 | } else { | ||
| 693 | toChild.readSide.close(); | ||
| 694 | fromChild.writeSide.close(); | ||
| 695 | if(unshare(flags) == -1) | ||
| 696 | throw SysError("unshare"); | ||
| 697 | writeFull(toChild.writeSide, (unsigned char*)"ready\n", 6); | ||
| 698 | waitForMessage(fromChild.readSide, "go\n"); | ||
| 699 | int status; | ||
| 700 | while(waitpid(child, &status, 0) == -1) { | ||
| 701 | if(errno != EINTR) | ||
| 702 | throw SysError("reaping userns init process"); | ||
| 703 | } | ||
| 704 | if(!(WIFEXITED(status) != 0 && WEXITSTATUS(status) == EXIT_SUCCESS)) | ||
| 705 | throw Error(format("userns init child exited with status %1%") % WEXITSTATUS(status)); | ||
| 706 | } | ||
| 707 | #endif | ||
| 708 | } | ||
| 709 | |||
| 710 | |||
| 711 | void lockMountsAction(SpawnContext & sctx) | ||
| 712 | { | ||
| 713 | #if CLONE_ENABLED && HAVE_SYS_MOUNT_H | ||
| 714 | CloneSpawnContext & ctx = (CloneSpawnContext &) sctx; | ||
| 715 | if(ctx.lockMounts) { | ||
| 716 | string uidMap; | ||
| 717 | string gidMap; | ||
| 718 | if(ctx.lockMountsMapAll) { | ||
| 719 | string oldUidMap = readFile("/proc/self/uid_map", true); | ||
| 720 | string oldGidMap = readFile("/proc/self/gid_map", true); | ||
| 721 | uidMap = idMapToIdentityMap(oldUidMap); | ||
| 722 | gidMap = idMapToIdentityMap(oldGidMap); | ||
| 723 | } else { | ||
| 724 | string uid = std::to_string(getuid()); | ||
| 725 | string gid = std::to_string(getgid()); | ||
| 726 | uidMap = uid + " " + uid + " 1"; | ||
| 727 | gidMap = gid + " " + gid + " 1"; | ||
| 728 | } | ||
| 729 | unshareAndInitUserns(CLONE_NEWNS | CLONE_NEWUSER, | ||
| 730 | uidMap, gidMap, ctx.lockMountsAllowSetgroups); | ||
| 731 | /* Check that mounts inherited in our new mount namespace are "locked" | ||
| 732 | together and cannot be separated from within our mount namespace. | ||
| 733 | Since umount(2) is documented to fail with EINVAL when attempting | ||
| 734 | to unmount one of the mounts that are locked together, check that | ||
| 735 | this is what we get. */ | ||
| 736 | int ret = umount("/proc"); | ||
| 737 | assert(ret == -1 && errno == EINVAL); | ||
| 738 | } | ||
| 739 | #endif | ||
| 740 | } | ||
| 741 | |||
| 742 | |||
| 743 | Phases getCloneSpawnPhases() | ||
| 744 | { | ||
| 745 | #if CLONE_ENABLED | ||
| 746 | return { { "reset_writeToStderr", reset_writeToStderrAction }, | ||
| 747 | { "usernsInitSync", usernsInitSyncAction }, | ||
| 748 | { "usernsSetIDs", usernsSetIDsAction }, | ||
| 749 | { "restoreAffinity", restoreAffinityAction }, | ||
| 750 | { "setsid", setsidAction }, | ||
| 751 | { "earlyIOSetup", earlyIOSetupAction }, | ||
| 752 | { "dropAmbientCapabilities", dropAmbientCapabilitiesAction }, | ||
| 753 | { "initLoopback", initLoopbackAction }, | ||
| 754 | { "setHostAndDomain", setHostAndDomainAction }, | ||
| 755 | { "makeFilesystemsPrivate", makeFilesystemsPrivateAction }, | ||
| 756 | { "makeChrootSeparateFilesystem", makeChrootSeparateFilesystemAction }, | ||
| 757 | { "mountIntoChroot", mountIntoChrootAction }, | ||
| 758 | { "mountProc", mountProcAction }, | ||
| 759 | { "mountDevshm", mountDevshmAction }, | ||
| 760 | { "mountDevpts", mountDevptsAction }, | ||
| 761 | { "chroot", pivotRootAction }, | ||
| 762 | { "chdir", chdirAction }, | ||
| 763 | { "closeMostFDs", closeMostFDsAction }, | ||
| 764 | { "setPersonality", setPersonalityAction }, | ||
| 765 | { "oomSacrifice", oomSacrificeAction }, | ||
| 766 | /* Being put in a user namespace with only the current ids mapped | ||
| 767 | would tend to prevent switching to other ones, but if this | ||
| 768 | comes after setIDs then the per-process "dumpable" flag may be | ||
| 769 | reset, which will cause /proc/self to become root-owned, | ||
| 770 | making /proc/self/uid_map inaccessible. If you need | ||
| 771 | lockMounts to preserve the id mappings, and you have the | ||
| 772 | necessary capabilities in the parent user namespace, set | ||
| 773 | CloneSpawnContext.lockMountsMapAll = true. */ | ||
| 774 | { "lockMounts", lockMountsAction }, | ||
| 775 | { "setIDs", setIDsAction }, | ||
| 776 | { "restoreSIGPIPE", restoreSIGPIPEAction }, | ||
| 777 | { "setupSuccess", setupSuccessAction }, | ||
| 778 | { "exec", execAction }}; | ||
| 779 | #else | ||
| 780 | throw Error("clone not supported on this platform"); | ||
| 781 | #endif | ||
| 782 | } | ||
| 783 | |||
| 784 | |||
| 785 | void runChildSetup(SpawnContext & ctx) | ||
| 786 | { | ||
| 787 | ctx.currentPhase = 0; | ||
| 788 | try { | ||
| 789 | /* Should not return regularly from this */ | ||
| 790 | while(true) { | ||
| 791 | ctx.phases.at(ctx.currentPhase).action(ctx); | ||
| 792 | ctx.currentPhase++; | ||
| 793 | } | ||
| 794 | } catch (std::exception & e) { | ||
| 795 | try { | ||
| 796 | writeFull(STDERR_FILENO, | ||
| 797 | "while setting up the child process: " + | ||
| 798 | (ctx.currentPhase < (ssize_t)ctx.phases.size() ? | ||
| 799 | "in phase " + ctx.phases[ctx.currentPhase].label + ": " : "") + | ||
| 800 | string(e.what()) + "\n"); | ||
| 801 | } catch (std::exception & e2) { | ||
| 802 | _exit(1); | ||
| 803 | } | ||
| 804 | _exit(1); | ||
| 805 | } | ||
| 806 | abort(); /* Should never be reached */ | ||
| 807 | } | ||
| 808 | |||
| 809 | |||
| 810 | int runChildSetupEntry(void *data) | ||
| 811 | { | ||
| 812 | runChildSetup(* (SpawnContext *)data); | ||
| 813 | return 1; | ||
| 814 | } | ||
| 815 | |||
| 816 | |||
| 817 | int cloneChild(CloneSpawnContext & ctx) | ||
| 818 | { | ||
| 819 | char stack[32 * 1024]; | ||
| 820 | /* Ensure proper alignment on the stack. On aarch64, it has to be 16 | ||
| 821 | bytes. */ | ||
| 822 | char *alignedStack = (char *)(((uintptr_t)stack + sizeof(stack) - 8) & ~(uintptr_t)0xf); | ||
| 823 | int ret = clone(runChildSetupEntry, alignedStack, ctx.cloneFlags, (void *) &ctx); | ||
| 824 | if(ret == -1) | ||
| 825 | throw SysError("clone"); | ||
| 826 | return ret; | ||
| 827 | } | ||
| 828 | |||
| 829 | } | ||
diff --git a/nix/libutil/spawn.hh b/nix/libutil/spawn.hh new file mode 100644 index 00000000000..edc528312db --- /dev/null +++ b/nix/libutil/spawn.hh | |||
| @@ -0,0 +1,164 @@ | |||
| 1 | #pragma once | ||
| 2 | |||
| 3 | #include <util.hh> | ||
| 4 | #include <map> | ||
| 5 | #include <stddef.h> | ||
| 6 | |||
| 7 | namespace nix { | ||
| 8 | struct SpawnContext; /* Forward declaration */ | ||
| 9 | typedef void (Action)(SpawnContext & ctx); | ||
| 10 | |||
| 11 | struct Phase { | ||
| 12 | string label; | ||
| 13 | Action * action; | ||
| 14 | }; | ||
| 15 | |||
| 16 | typedef std::vector<Phase> Phases; | ||
| 17 | |||
| 18 | /* Common structure read from / written to by setup phases in a newly-spawned | ||
| 19 | child process. Configure this to determine which per-process or | ||
| 20 | per-thread attributes should be set. */ | ||
| 21 | struct SpawnContext { | ||
| 22 | ssize_t currentPhase = 0; | ||
| 23 | Phases phases; | ||
| 24 | Strings args; /* Will be passed as-is to execve, does not implicitly add | ||
| 25 | * program basename as argv[0]! */ | ||
| 26 | Path program; | ||
| 27 | bool inheritEnv = true; /* True to use the current environment after env | ||
| 28 | * has been applied to it, false to use strictly | ||
| 29 | * env. */ | ||
| 30 | std::map<string, string> env; | ||
| 31 | bool setPersona = false; | ||
| 32 | int persona; | ||
| 33 | int logFD = -1; /* -1 to keep stdout and stderr */ | ||
| 34 | set<int> earlyCloseFDs; /* Typically for closing inherited unused pipe or | ||
| 35 | * socket ends to prevent hangs when reading or | ||
| 36 | * writing. */ | ||
| 37 | bool closeMostFDs = false; | ||
| 38 | set<int> preserveFDs; /* 0, 1, and 2 are always implicitly preserved. */ | ||
| 39 | bool setStdin = false; | ||
| 40 | int stdinFD = -1; /* fd or -1 */ | ||
| 41 | Path stdinFile; /* used if stdinFD == -1 */ | ||
| 42 | bool setuid = false; | ||
| 43 | uid_t user; | ||
| 44 | bool setgid = false; | ||
| 45 | gid_t group; | ||
| 46 | bool setSupplementaryGroups = false; | ||
| 47 | std::vector<gid_t> supplementaryGroups; | ||
| 48 | bool setsid = false; | ||
| 49 | bool oomSacrifice = false; /* Whether to attempt to offer the child | ||
| 50 | * process to the OOM killer if possible. */ | ||
| 51 | bool setcwd = false; | ||
| 52 | Path cwd; | ||
| 53 | bool signalSetupSuccess = false; /* Whether the parent is waiting for a | ||
| 54 | * message that setup succeeded. By | ||
| 55 | * default success is signaled by | ||
| 56 | * writing a single newline to stderr. */ | ||
| 57 | bool dropAmbientCapabilities = false; /* Whether to drop ambient | ||
| 58 | * capabilities if on a system that | ||
| 59 | * supports them. */ | ||
| 60 | bool doChroot = false; | ||
| 61 | Path chrootRootDir; | ||
| 62 | void * extraData; /* Extra user data */ | ||
| 63 | }; | ||
| 64 | |||
| 65 | /* Like SpawnContext, but with extra fields for setting up Linux namespaces, | ||
| 66 | as created by clone or unshare. */ | ||
| 67 | struct CloneSpawnContext : SpawnContext { | ||
| 68 | int cloneFlags = 0; | ||
| 69 | std::map<Path, Path> filesInChroot; /* map from path inside chroot to | ||
| 70 | * path outside of chroot */ | ||
| 71 | set<Path> readOnlyFilesInChroot; | ||
| 72 | bool mountTmpfsOnChroot = false; /* req. CLONE_NEWNS and doChroot */ | ||
| 73 | bool mountProc = false; | ||
| 74 | bool mountDevshm = false; | ||
| 75 | bool maybeMountDevpts = false; /* Only mounted if /dev/ptmx doesn't exist | ||
| 76 | * after any chroot, if applicable. */ | ||
| 77 | bool lockMounts = false; /* Whether to lock mounts by creating a fresh | ||
| 78 | * user and mount namespace, see | ||
| 79 | * mount_namespaces(7). */ | ||
| 80 | bool lockMountsMapAll = false; /* Whether to map all currently-mapped | ||
| 81 | users and groups when locking mounts or | ||
| 82 | only the current ones. */ | ||
| 83 | bool lockMountsAllowSetgroups = false; | ||
| 84 | int setupFD = -1; /* Used for userns init sync and other stuff */ | ||
| 85 | string hostname; /* Requires CLONE_NEWUTS */ | ||
| 86 | string domainname; /* Same */ | ||
| 87 | bool initLoopback = false; /* Also requires CLONE_NEWNET in cloneFlags */ | ||
| 88 | /* These may be used if CLONE_NEWUSER in cloneFlags. These are to be | ||
| 89 | used when an id other than the current uid/gid has been mapped into the | ||
| 90 | child's user namespace, and it now needs to setuid/setgid to an id | ||
| 91 | that is mapped. */ | ||
| 92 | bool usernsSetuid = false; | ||
| 93 | uid_t usernsUser; | ||
| 94 | bool usernsSetgid = false; | ||
| 95 | gid_t usernsGroup; | ||
| 96 | }; | ||
| 97 | |||
| 98 | void addPhaseAfter(Phases & phases, string afterLabel, string addLabel, Action addAction); | ||
| 99 | |||
| 100 | void addPhaseBefore(Phases & phases, string beforeLabel, string addLabel, Action addAction); | ||
| 101 | |||
| 102 | void prependPhase(Phases & phases, string addLabel, Action addAction); | ||
| 103 | |||
| 104 | void appendPhase(Phases & phases, string addLabel, Action addAction); | ||
| 105 | |||
| 106 | void deletePhase(Phases & phases, string delLabel); | ||
| 107 | |||
| 108 | void replacePhase(Phases & phases, string replaceLabel, Action newAction); | ||
| 109 | |||
| 110 | Action reset_writeToStderrAction; | ||
| 111 | Action restoreAffinityAction; | ||
| 112 | Action setsidAction; | ||
| 113 | Action earlyIOSetupAction; | ||
| 114 | Action dropAmbientCapabilitiesAction; | ||
| 115 | Action chrootAction; | ||
| 116 | Action chdirAction; | ||
| 117 | Action closeMostFDsAction; | ||
| 118 | Action setPersonalityAction; | ||
| 119 | Action oomSacrificeAction; | ||
| 120 | Action setIDsAction; | ||
| 121 | Action restoreSIGPIPEAction; | ||
| 122 | Action setupSuccessAction; | ||
| 123 | Action execAction; | ||
| 124 | |||
| 125 | Phases getBasicSpawnPhases(); | ||
| 126 | |||
| 127 | void bindMount(Path source, Path target, bool readOnly); | ||
| 128 | |||
| 129 | void mountIntoChroot(std::map<Path, Path> filesInChroot, | ||
| 130 | set<Path> readOnlyFiles, | ||
| 131 | Path chrootRootDir); | ||
| 132 | |||
| 133 | Action usernsInitSyncAction; | ||
| 134 | Action usernsSetIDsAction; | ||
| 135 | Action initLoopbackAction; | ||
| 136 | Action setHostAndDomainAction; | ||
| 137 | Action makeFilesystemsPrivateAction; | ||
| 138 | Action makeChrootSeparateFilesystemAction; | ||
| 139 | Action mountIntoChrootAction; | ||
| 140 | Action mountProcAction; | ||
| 141 | Action mountDevshmAction; | ||
| 142 | Action mountDevptsAction; | ||
| 143 | Action pivotRootAction; | ||
| 144 | Action lockMountsAction; | ||
| 145 | |||
| 146 | Phases getCloneSpawnPhases(); | ||
| 147 | |||
| 148 | /* Helpers */ | ||
| 149 | string idMapToIdentityMap(const string & map); | ||
| 150 | void unshareAndInitUserns(int flags, const string & uidMap, | ||
| 151 | const string & gidMap, bool allowSetgroups); | ||
| 152 | |||
| 153 | /* Run the phases of ctx in order, catching and reporting any exception, and | ||
| 154 | * exiting in all cases. */ | ||
| 155 | void runChildSetup(SpawnContext & ctx); | ||
| 156 | |||
| 157 | /* Helper to call runChildSetup that can be passed to the variant of clone | ||
| 158 | * that expects a callback. */ | ||
| 159 | int runChildSetupEntry(void *data); | ||
| 160 | |||
| 161 | /* Create a new process using clone that will immediately call runChildSetup | ||
| 162 | * with the provided CloneSpawnContext. Return the pid of the new process. */ | ||
| 163 | int cloneChild(CloneSpawnContext & ctx); | ||
| 164 | } | ||
diff --git a/nix/libutil/util.cc b/nix/libutil/util.cc index c406325cdcb..e71e6c170ad 100644 --- a/nix/libutil/util.cc +++ b/nix/libutil/util.cc | |||
| @@ -724,6 +724,18 @@ string drainFD(int fd) | |||
| 724 | } | 724 | } |
| 725 | 725 | ||
| 726 | 726 | ||
| 727 | /* Wait on FD until MESSAGE has been read. */ | ||
| 728 | void waitForMessage(int fd, const char *message) | ||
| 729 | { | ||
| 730 | size_t size = strlen(message); | ||
| 731 | char str[size] = { '\0' }; | ||
| 732 | readFull(fd, (unsigned char*)str, size); | ||
| 733 | if (strncmp(str, message, size) != 0) | ||
| 734 | throw Error(format("did not receive message '%1%' on file descriptor %2%") | ||
| 735 | % message % fd); | ||
| 736 | } | ||
| 737 | |||
| 738 | |||
| 727 | 739 | ||
| 728 | ////////////////////////////////////////////////////////////////////// | 740 | ////////////////////////////////////////////////////////////////////// |
| 729 | 741 | ||
| @@ -1140,6 +1152,13 @@ void closeOnExec(int fd) | |||
| 1140 | throw SysError("setting close-on-exec flag"); | 1152 | throw SysError("setting close-on-exec flag"); |
| 1141 | } | 1153 | } |
| 1142 | 1154 | ||
| 1155 | void keepOnExec(int fd) | ||
| 1156 | { | ||
| 1157 | int prev; | ||
| 1158 | if ((prev = fcntl(fd, F_GETFD, 0)) == -1 || | ||
| 1159 | fcntl(fd, F_SETFD, prev & ~FD_CLOEXEC) == -1) | ||
| 1160 | throw SysError("clearing close-on-exec flag"); | ||
| 1161 | } | ||
| 1143 | 1162 | ||
| 1144 | ////////////////////////////////////////////////////////////////////// | 1163 | ////////////////////////////////////////////////////////////////////// |
| 1145 | 1164 | ||
diff --git a/nix/libutil/util.hh b/nix/libutil/util.hh index a07c3be6eb6..ab2395e959f 100644 --- a/nix/libutil/util.hh +++ b/nix/libutil/util.hh | |||
| @@ -173,6 +173,8 @@ MakeError(EndOfFile, Error) | |||
| 173 | /* Read a file descriptor until EOF occurs. */ | 173 | /* Read a file descriptor until EOF occurs. */ |
| 174 | string drainFD(int fd); | 174 | string drainFD(int fd); |
| 175 | 175 | ||
| 176 | void waitForMessage(int fd, const char *message); | ||
| 177 | |||
| 176 | 178 | ||
| 177 | 179 | ||
| 178 | /* Automatic cleanup of resources. */ | 180 | /* Automatic cleanup of resources. */ |
| @@ -300,6 +302,9 @@ void closeMostFDs(const set<int> & exceptions); | |||
| 300 | /* Set the close-on-exec flag for the given file descriptor. */ | 302 | /* Set the close-on-exec flag for the given file descriptor. */ |
| 301 | void closeOnExec(int fd); | 303 | void closeOnExec(int fd); |
| 302 | 304 | ||
| 305 | /* Clear the close-on-exec flag for the given file descriptor. */ | ||
| 306 | void keepOnExec(int fd); | ||
| 307 | |||
| 303 | /* Common initialisation performed in child processes. */ | 308 | /* Common initialisation performed in child processes. */ |
| 304 | void commonChildInit(Pipe & logPipe); | 309 | void commonChildInit(Pipe & logPipe); |
| 305 | 310 | ||
diff --git a/nix/local.mk b/nix/local.mk index 54976a5741a..9f21550af2d 100644 --- a/nix/local.mk +++ b/nix/local.mk | |||
| @@ -56,7 +56,8 @@ libutil_a_SOURCES = \ | |||
| 56 | %D%/libutil/affinity.cc \ | 56 | %D%/libutil/affinity.cc \ |
| 57 | %D%/libutil/serialise.cc \ | 57 | %D%/libutil/serialise.cc \ |
| 58 | %D%/libutil/util.cc \ | 58 | %D%/libutil/util.cc \ |
| 59 | %D%/libutil/hash.cc | 59 | %D%/libutil/hash.cc \ |
| 60 | %D%/libutil/spawn.cc | ||
| 60 | 61 | ||
| 61 | libutil_headers = \ | 62 | libutil_headers = \ |
| 62 | %D%/libutil/affinity.hh \ | 63 | %D%/libutil/affinity.hh \ |
| @@ -64,7 +65,8 @@ libutil_headers = \ | |||
| 64 | %D%/libutil/serialise.hh \ | 65 | %D%/libutil/serialise.hh \ |
| 65 | %D%/libutil/util.hh \ | 66 | %D%/libutil/util.hh \ |
| 66 | %D%/libutil/archive.hh \ | 67 | %D%/libutil/archive.hh \ |
| 67 | %D%/libutil/types.hh | 68 | %D%/libutil/types.hh \ |
| 69 | %D%/libutil/spawn.hh | ||
| 68 | 70 | ||
| 69 | libutil_a_CPPFLAGS = \ | 71 | libutil_a_CPPFLAGS = \ |
| 70 | -I$(top_builddir)/nix \ | 72 | -I$(top_builddir)/nix \ |
