summaryrefslogtreecommitdiff
path: root/nix
diff options
context:
space:
mode:
authorReepca Russelstein <reepca@russelstein.xyz>2025-04-18 01:35:31 -0500
committerJohn Kehayias <john.kehayias@protonmail.com>2025-06-24 10:07:57 -0400
commitfb42611b8f27960304db5a1c0d33b8371dcde2a8 (patch)
treee4331b4b340c3304684914044d543ecd0e653fb7 /nix
parentbe8aca065118aa4485c02f991c51bea89034defa (diff)
daemon: Use slirp4netns to provide networking to fixed-output derivations.
Previously, the builder of a fixed-output derivation could communicate with an external process via an abstract Unix-domain socket. In particular, it could send an open file descriptor to the store, granting write access to some of its output files in the store provided the derivation build fails—the fix for CVE-2024-27297 did not address this specific case. It could also send an open file descriptor to a setuid program, which could then be executed using execveat to gain the privileges of the build user. With this change, fixed-output derivations other than “builtin:download” and “builtin:git-download” always run in a separate network namespace and have network access provided by a TAP device backed by slirp4netns, thereby closing the abstract Unix-domain socket channel. * nix/libstore/globals.hh (Settings)[useHostLoopback, slirp4netns]: new fields. * config-daemon.ac (SLIRP4NETNS): new C preprocessor definition. * nix/libstore/globals.cc (Settings::Settings): initialize them to defaults. * nix/nix-daemon/guix-daemon.cc (options): add --isolate-host-loopback option. * doc/guix.texi: document it. * nix/libstore/build.cc (DerivationGoal)[slirp]: New field. (setupTap, setupTapAction, waitForSlirpReadyAction, enableRouteLocalnetAction, prepareSlirpChrootAction, spawnSlirp4netns, haveGlobalIPv6Address, remapIdsTo0Action): New functions. (initializeUserNamespace): allow the guest UID and GID to be specified. (DerivationGoal::killChild): When ‘slirp’ is not -1, call ‘kill’. (DerivationGoal::startBuilder): Unconditionally add CLONE_NEWNET to FLAGS. When ‘fixedOutput’ is true, spawn ‘slirp4netns’. When ‘fixedOutput’ and ‘useChroot’ are true, add setupTapAction, waitForSlirpReadyAction, and enableRouteLocalnetAction to builder setup phases. Create a /etc/resolv.conf for fixed-output derivations that directs them to slirp4netns's dns address. When settings.useHostLoopback is true, supply fixed-output derivations with a /etc/hosts that resolves "localhost" to slirp4netns's address for accessing the host loopback. * nix/libutil/util.cc (keepOnExec, decodeOctalEscaped, sendFD, receiveFD, findProgram): New functions. * nix/libutil/util.hh (keepOnExec, decodeOctalEscaped, sendFD, receiveFD, findProgram): New declarations. * gnu/packages/package-management.scm (guix): add slirp4netns input for linux targets. * tests/derivations.scm (builder-network-isolated?): new variable. ("fixed-output derivation, network access, localhost", "fixed-output derivation, network access, external host"): skip test case if fixed output derivations are isolated from the network. Change-Id: Ia3fea2ab7add56df66800071cf15cdafe7bfab96 Signed-off-by: John Kehayias <john.kehayias@protonmail.com>
Diffstat (limited to 'nix')
-rw-r--r--nix/libstore/build.cc551
-rw-r--r--nix/libstore/globals.cc2
-rw-r--r--nix/libstore/globals.hh9
-rw-r--r--nix/libutil/util.cc101
-rw-r--r--nix/libutil/util.hh16
-rw-r--r--nix/nix-daemon/guix-daemon.cc6
6 files changed, 666 insertions, 19 deletions
diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index 51f5aed1068..1a688f3b56c 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -14,6 +14,7 @@
14#include <map> 14#include <map>
15#include <sstream> 15#include <sstream>
16#include <algorithm> 16#include <algorithm>
17#include <regex>
17 18
18#include <limits.h> 19#include <limits.h>
19#include <time.h> 20#include <time.h>
@@ -73,10 +74,18 @@
73#endif 74#endif
74 75
75#if CHROOT_ENABLED 76#if CHROOT_ENABLED
76#include <sys/socket.h>
77#include <sys/ioctl.h> 77#include <sys/ioctl.h>
78#include <net/if.h> 78#include <net/if.h>
79#include <netinet/ip.h> 79#include <sys/socket.h>
80#include <netinet/in.h>
81#include <net/route.h>
82#include <arpa/inet.h>
83#if __linux__
84#include <linux/if_tun.h>
85/* This header isn't documented in 'man netdevice', but there doesn't seem to
86 be any other way to get 'struct in6_ifreq'... */
87#include <linux/ipv6.h>
88#endif
80#endif 89#endif
81 90
82#if __linux__ 91#if __linux__
@@ -661,6 +670,10 @@ private:
661 /* Whether this is a fixed-output derivation. */ 670 /* Whether this is a fixed-output derivation. */
662 bool fixedOutput; 671 bool fixedOutput;
663 672
673 /* PID of the 'slirp4netns' process in case of a fixed-output
674 derivation. */
675 Pid slirp;
676
664 typedef void (DerivationGoal::*GoalState)(); 677 typedef void (DerivationGoal::*GoalState)();
665 GoalState state; 678 GoalState state;
666 679
@@ -831,6 +844,10 @@ void DerivationGoal::killChild()
831 worker.childTerminated(hook->pid); 844 worker.childTerminated(hook->pid);
832 } 845 }
833 hook.reset(); 846 hook.reset();
847
848 if (slirp != -1)
849 /* Terminate the 'slirp4netns' process. */
850 slirp.kill();
834} 851}
835 852
836 853
@@ -1611,7 +1628,9 @@ static const gid_t guestGID = 30000;
1611/* Initialize the user namespace of CHILD. */ 1628/* Initialize the user namespace of CHILD. */
1612static void initializeUserNamespace(pid_t child, 1629static void initializeUserNamespace(pid_t child,
1613 uid_t hostUID = getuid(), 1630 uid_t hostUID = getuid(),
1614 gid_t hostGID = getgid()) 1631 gid_t hostGID = getgid(),
1632 uid_t guestUID = guestUID,
1633 gid_t guestGID = guestGID)
1615{ 1634{
1616 writeFile("/proc/" + std::to_string(child) + "/uid_map", 1635 writeFile("/proc/" + std::to_string(child) + "/uid_map",
1617 (format("%d %d 1") % guestUID % hostUID).str()); 1636 (format("%d %d 1") % guestUID % hostUID).str());
@@ -1624,12 +1643,427 @@ static void initializeUserNamespace(pid_t child,
1624 1643
1625#if CHROOT_ENABLED 1644#if CHROOT_ENABLED
1626 1645
1627void clearRootWritePermsAction(SpawnContext & sctx) 1646/* Creating TAP device for the fixed-output derivation build environment,
1647 based on how slirp4netns does it. send_fd_socket is a unix-domain socket
1648 that a file descriptor for the TAP device will be sent on along with a
1649 single null byte of regular data. */
1650static void setupTap(int send_fd_socket, bool ipv6Enabled)
1651{
1652 AutoCloseFD tapfd;
1653 struct ifreq ifr;
1654 struct in6_ifreq ifr6;
1655 char tapname[] = "tap0";
1656 int ifindex;
1657
1658 tapfd = open("/dev/net/tun", O_RDWR);
1659 if(tapfd < 0)
1660 throw SysError("opening `/dev/net/tun'");
1661
1662 memset(&ifr, 0, sizeof(ifr));
1663 ifr.ifr_flags = IFF_TAP | IFF_NO_PI;
1664 strncpy(ifr.ifr_name, tapname, sizeof(ifr.ifr_name) - 1);
1665 if(ioctl(tapfd, TUNSETIFF, (void*)&ifr) < 0)
1666 throw SysError("TUNSETIFF");
1667
1668 /* DAD is "duplicate address detection". By default the kernel will put
1669 any ipv6 addresses that we add into the "tentative" state, and only
1670 after several seconds have been spent trying to chat with network
1671 neighbors about whether anyone is already using the address will it
1672 allow it to be bound to, whether for listening or for connecting.
1673
1674 This causes tcp connections initiated before then to bind to ::1, which
1675 obviously is not a valid address for communication between hosts. Even
1676 after the real addresses leave the "tentative" state, the source address
1677 used for the already-started connection attempt does not change.
1678
1679 In our situation we know for a fact nobody else is using the addresses
1680 we give, so there's no point in waiting the extra several seconds to
1681 perform DAD; disable it entirely instead.
1682
1683 Note: this needs to use conf/tap0/ instead of conf/all/ */
1684 writeFile("/proc/sys/net/ipv6/conf/tap0/accept_dad", "0");
1685
1686 /* By default tap0 will solicit and receive router advertisements, and
1687 * thereby obtain an ipv6 address from slirp4netns. But if the host
1688 * doesn't have a working ipv6 connection, this could mess things up for
1689 * guest programs (and really the guest network stack itself), as they
1690 * have no way of knowing that, and will therefore likely try connecting
1691 * to addresses found in AAAA records, which will fail. To prevent this,
1692 * ignore router advertisements. */
1693 writeFile("/proc/sys/net/ipv6/conf/tap0/accept_ra", "0");
1694
1695 /* Now set up:
1696 1. tap0's active flags (so it's running, up, etc)
1697 2. tap0's MTU
1698 3. tap0's ip address
1699 4. tap0's network mask
1700 5. A default route to tap0 */
1701 AutoCloseFD sockfd = socket(AF_INET, SOCK_DGRAM, 0);
1702
1703 if(sockfd < 0)
1704 throw SysError("creating socket");
1705
1706 AutoCloseFD sockfd6 = socket(AF_INET6, SOCK_DGRAM, 0);
1707
1708 if(sockfd6 < 0)
1709 throw SysError("creating ipv6 socket");
1710
1711 if(ioctl(sockfd, SIOCGIFINDEX, &ifr) < 0)
1712 throw SysError("getting tap0 ifindex");
1713
1714 ifindex = ifr.ifr_ifindex;
1715
1716 ifr.ifr_flags = IFF_UP | IFF_RUNNING;
1717 if(ioctl(sockfd, SIOCSIFFLAGS, &ifr) < 0)
1718 throw SysError("setting flags for tap0");
1719
1720 /* slirp4netns default */
1721 ifr.ifr_mtu = 1500;
1722 if(ioctl(sockfd, SIOCSIFMTU, &ifr) < 0)
1723 throw SysError("setting MTU for tap0");
1724
1725 /* default network CIDR: 10.0.2.0/24, fd00::/64 */
1726 /* default recommended_vguest: 10.0.2.100, fd00::??? (we choose to use
1727 fd00::80 and fe80::80) */
1728 /* default gateway: 10.0.2.2, fd00::2 */
1729 struct sockaddr_in *sai = (struct sockaddr_in *) &ifr.ifr_addr;
1730 sai->sin_family = AF_INET;
1731 sai->sin_port = htonl(0);
1732 if(inet_pton(AF_INET, "10.0.2.100", &sai->sin_addr) != 1)
1733 throw Error("inet_pton failed");
1734
1735 if(ioctl(sockfd, SIOCSIFADDR, &ifr) < 0)
1736 throw SysError("setting tap0 address");
1737
1738 if(ipv6Enabled) {
1739 if(inet_pton(AF_INET6, "fd00::80", &ifr6.ifr6_addr) != 1)
1740 throw Error("inet_pton failed");
1741 ifr6.ifr6_prefixlen = 64;
1742 ifr6.ifr6_ifindex = ifindex;
1743
1744 if(ioctl(sockfd6, SIOCSIFADDR, &ifr6) < 0)
1745 throw SysError("setting tap0 ipv6 address");
1746 }
1747
1748 /* Always set up the link-local address so that communication with the
1749 * host loopback over ipv6 can be possible. */
1750 if(inet_pton(AF_INET6, "fe80::80", &ifr6.ifr6_addr) != 1)
1751 throw Error("inet_pton failed");
1752 ifr6.ifr6_prefixlen = 64;
1753 ifr6.ifr6_ifindex = ifindex;
1754
1755 if(ioctl(sockfd6, SIOCSIFADDR, &ifr6) < 0)
1756 throw SysError("setting tap0 link-local ipv6 address");
1757
1758 if(inet_pton(AF_INET, "255.255.255.0", &sai->sin_addr) != 1)
1759 throw Error("inet_pton failed");
1760
1761 if(ioctl(sockfd, SIOCSIFNETMASK, &ifr) < 0)
1762 throw SysError("setting tap0 network mask");
1763
1764 /* To my knowledge there is no official documentation of SIOCADDRT and
1765 struct rtentry for Linux aside from the Linux kernel source code as of
1766 the year 2025. This is therefore fully cargo-culted from
1767 slirp4netns. */
1768
1769 struct rtentry route;
1770 memset(&route, 0, sizeof(route));
1771 sai = (struct sockaddr_in *)&route.rt_gateway;
1772 sai->sin_family = AF_INET;
1773 if(inet_pton(AF_INET, "10.0.2.2", &sai->sin_addr) != 1)
1774 throw Error("inet_pton failed");
1775 sai = (struct sockaddr_in *)&route.rt_dst;
1776 sai->sin_family = AF_INET;
1777 sai->sin_addr.s_addr = htonl(INADDR_ANY);
1778 sai = (struct sockaddr_in *)&route.rt_genmask;
1779 sai->sin_family = AF_INET;
1780 sai->sin_addr.s_addr = htonl(INADDR_ANY);
1781
1782 route.rt_flags = RTF_UP | RTF_GATEWAY;
1783 route.rt_metric = 0;
1784 route.rt_dev = tapname;
1785
1786 if(ioctl(sockfd, SIOCADDRT, &route) < 0)
1787 throw SysError("setting tap0 as default route");
1788
1789 struct in6_rtmsg route6;
1790 memset(&route6, 0, sizeof(route6));
1791 if(inet_pton(AF_INET6, "fd00::2", &route6.rtmsg_gateway) != 1)
1792 throw Error("inet_pton failed");
1793
1794 if(ipv6Enabled) {
1795 /* Set up a default gateway via slirp4netns */
1796 route6.rtmsg_dst = IN6ADDR_ANY_INIT;
1797 route6.rtmsg_dst_len = 0;
1798 route6.rtmsg_flags = RTF_UP | RTF_GATEWAY;
1799 } else {
1800 /* Set up a route to slirp4netns, but only for talking to the host
1801 * loopback */
1802 if(inet_pton(AF_INET6, "fd00::2", &route6.rtmsg_dst) != 1)
1803 throw Error("inet_pton failed");
1804 route6.rtmsg_dst_len = 128;
1805 route6.rtmsg_flags = RTF_UP;
1806 }
1807 route6.rtmsg_src = IN6ADDR_ANY_INIT;
1808 route6.rtmsg_src_len = 0;
1809 route6.rtmsg_ifindex = ifindex;
1810 route6.rtmsg_metric = 1;
1811
1812 if(ioctl(sockfd6, SIOCADDRT, &route6) < 0)
1813 throw SysError("setting tap0 as default ipv6 route");
1814
1815 sendFD(send_fd_socket, tapfd);
1816}
1817
1818struct ChrootBuildSpawnContext : CloneSpawnContext {
1819 bool ipv6Enabled = false;
1820};
1821
1822static void setupTapAction(SpawnContext & sctx)
1823{
1824 ChrootBuildSpawnContext & ctx = (ChrootBuildSpawnContext &) sctx;
1825 setupTap(ctx.setupFD, ctx.ipv6Enabled);
1826}
1827
1828
1829static void waitForSlirpReadyAction(SpawnContext & sctx)
1830{
1831 CloneSpawnContext & ctx = (CloneSpawnContext &) sctx;
1832 /* Wait for the parent process to get slirp4netns running */
1833 waitForMessage(ctx.setupFD, "1");
1834}
1835
1836
1837static void enableRouteLocalnetAction(SpawnContext & sctx)
1838{
1839 /* Don't treat as invalid packets received with loopback source addresses.
1840 This allows for packets to be received from the host loopback using its
1841 real address, so for example proxy settings referencing 127.0.0.1 will
1842 work both for builtin and regular fixed-output derivations. */
1843
1844 /* Note: this file is treated relative to the network namespace of the
1845 process that opens it. We aren't modifying any host settings here,
1846 provided we are in a new network namespace. */
1847 Path route_localnet4 = "/proc/sys/net/ipv4/conf/all/route_localnet";
1848 /* XXX: no such toggle exists for ipv6 */
1849 if(pathExists(route_localnet4))
1850 writeFile(route_localnet4, "1");
1851}
1852
1853
1854static void prepareSlirpChrootAction(SpawnContext & sctx)
1855{
1856 CloneSpawnContext & ctx = (CloneSpawnContext &) sctx;
1857 auto mounts = tokenizeString<Strings>(readFile("/proc/self/mountinfo", true), "\n");
1858 set<string> seen;
1859 for(auto & i : mounts) {
1860 auto fields = tokenizeString<vector<string> >(i, " ");
1861 auto fs = decodeOctalEscaped(fields.at(4));
1862 if(seen.find(fs) == seen.end()) {
1863 /* slirp4netns only does a single umount of the old root ("/old")
1864 after pivot_root. Because of this, if there are multiple
1865 mounts stacked on top of each other, only the topmost one (the
1866 read-only bind mount) will be unmounted, leaving the real root
1867 in place and causing the subsequent rmdir to fail. The best we
1868 can do is to make everything immediately underneath "/" be
1869 read-only, which we do after mounting every non-/ filesystem
1870 read-only. */
1871 if(fs == "/") continue;
1872 /* Don't mount /etc or any of its subdirectories, we're only interested
1873 in mounting network stuff from it */
1874 if(fs.compare(0, 4, "/etc") == 0) continue;
1875 /* We want /run to be empty */
1876 if(fs.compare(0, 4, "/run") == 0) continue;
1877 /* Don't mount anything from under our chroot directory */
1878 if(fs.compare(0, ctx.chrootRootDir.length(), ctx.chrootRootDir) == 0) continue;
1879 struct stat st;
1880 if(stat(fs.c_str(), &st) != 0) {
1881 if(errno == EACCES) continue; /* Not accessible anyway */
1882 else throw SysError(format("stat of `%1%'") % fs);
1883 }
1884
1885 ctx.readOnlyFilesInChroot.insert(fs);
1886 ctx.filesInChroot[fs] = fs;
1887 seen.insert(fs);
1888 }
1889 }
1890
1891 /* Limit /etc to containing just /etc/resolv.conf and /etc/hosts, and
1892 read-only at that */
1893 Strings etcFiles = { "/etc/resolv.conf", "/etc/hosts" };
1894 for(auto & i : etcFiles) {
1895 if(pathExists(i)) {
1896 ctx.filesInChroot[i] = i;
1897 ctx.readOnlyFilesInChroot.insert(i);
1898 }
1899 }
1900
1901 /* Make everything immediately under "/" read-only, since we can't make /
1902 itself read-only. */
1903 DirEntries dirs = readDirectory("/");
1904 for (auto & i : dirs) {
1905 string fs = "/" + i.name;
1906 if(fs == "/etc") continue;
1907 if(fs == "/run") continue;
1908 ctx.filesInChroot[fs] = fs;
1909 ctx.readOnlyFilesInChroot.insert(fs);
1910 }
1911
1912 if(mkdir((ctx.chrootRootDir + "/run").c_str(), 0700) == -1)
1913 throw SysError("mkdir /run in chroot");
1914}
1915
1916
1917static void remapIdsTo0Action(SpawnContext & sctx)
1918{
1919 CloneSpawnContext & ctx = (CloneSpawnContext &) sctx;
1920 string uid = std::to_string(ctx.setuid ? ctx.user : getuid());
1921 string gid = std::to_string(ctx.setgid ? ctx.group : getgid());
1922
1923 /* If uid != getuid(), then the process that writes to uid_map needs
1924 * capabilities in the parent user namespace. Fork a child to stay in
1925 * the parent namespace and do the write for us. */
1926 unshareAndInitUserns(CLONE_NEWUSER,
1927 "0 " + uid + " 1",
1928 "0 " + gid + " 1",
1929 ctx.lockMountsAllowSetgroups);
1930
1931 ctx.user = 0;
1932 ctx.group = 0;
1933}
1934
1935
1936/* Spawn 'slirp4netns' in separate namespaces as the given user and group;
1937 'tapfd' must correspond to a /dev/net/tun connection. Configure it to
1938 write to 'notifyReadyFD' once it's up and running. */
1939static pid_t spawnSlirp4netns(int tapfd, int notifyReadyFD,
1940 uid_t slirpUser, gid_t slirpGroup)
1941{
1942 Pipe slirpSetupPipe;
1943 CloneSpawnContext slirpCtx;
1944 AutoCloseFD devNullFd;
1945 bool amRoot = geteuid() == 0;
1946 bool newUserNS = !amRoot;
1947 slirpCtx.phases = getCloneSpawnPhases();
1948 slirpCtx.cloneFlags =
1949 /* slirp4netns will handle the chroot and pivot_root on its own, but
1950 we should ensure that whatever filesystem holds the slirp4netns
1951 executable is read-only, since otherwise it might be possible for a
1952 compromised slirp4netns to overwrite itself using /proc/self/exe,
1953 depending on who owns what. */
1954 CLONE_NEWNS |
1955 /* ptrace disregards user namespaces when the would-be tracing process
1956 and the would-be traced process have the same real, effective, and
1957 saved user ids. The only way to protect them is to make it
1958 impossible to reference them. */
1959 CLONE_NEWPID |
1960 /* need this when we're not running as root so that we have the
1961 * capabilities to create the other namespaces. */
1962 (newUserNS ? CLONE_NEWUSER : 0) |
1963 /* For good measure */
1964 CLONE_NEWIPC |
1965 CLONE_NEWUTS |
1966 /* Of course, a new network namespace would defeat the
1967 purpose. */
1968 SIGCHLD;
1969 slirpCtx.program = settings.slirp4netns;
1970 slirpCtx.args =
1971 { "slirp4netns", "--netns-type=tapfd",
1972 "--enable-sandbox",
1973 "--enable-ipv6",
1974 "--ready-fd=" + std::to_string(notifyReadyFD) };
1975 if(!settings.useHostLoopback)
1976 slirpCtx.args.push_back("--disable-host-loopback");
1977 slirpCtx.args.push_back(std::to_string(tapfd));
1978 slirpCtx.inheritEnv = true;
1979 if(newUserNS) {
1980 slirpSetupPipe.create();
1981 slirpCtx.setupFD = slirpSetupPipe.readSide;
1982 slirpCtx.earlyCloseFDs.insert(slirpSetupPipe.writeSide);
1983 }
1984 slirpCtx.closeMostFDs = true;
1985 slirpCtx.preserveFDs.insert(notifyReadyFD);
1986 slirpCtx.preserveFDs.insert(tapfd);
1987 slirpCtx.setStdin = true;
1988 slirpCtx.stdinFile = "/dev/null";
1989 slirpCtx.setsid = true;
1990 slirpCtx.dropAmbientCapabilities = true;
1991 slirpCtx.doChroot = true;
1992 slirpCtx.mountTmpfsOnChroot = true;
1993 slirpCtx.chrootRootDir = getEnv("TMPDIR", "/tmp");
1994 slirpCtx.lockMounts = true;
1995 slirpCtx.lockMountsMapAll = true; /* So that later setuid will work */
1996 slirpCtx.lockMountsAllowSetgroups = amRoot;
1997 slirpCtx.mountProc = true;
1998 slirpCtx.setuid = true;
1999 slirpCtx.user = slirpUser;
2000 slirpCtx.setgid = true;
2001 slirpCtx.group = slirpGroup;
2002 /* Dropping supplementary groups requires capabilities in current user
2003 * namespace */
2004 if(amRoot) {
2005 slirpCtx.supplementaryGroups = {};
2006 slirpCtx.setSupplementaryGroups = true;
2007 }
2008 slirpCtx.seccompFilter = slirpSeccompFilter();
2009 slirpCtx.addSeccompFilter = true;
2010
2011 /* Silence slirp4netns output unless requested */
2012 if(verbosity <= lvlInfo) {
2013 devNullFd = open("/dev/null", O_WRONLY);
2014 if(devNullFd == -1)
2015 throw SysError("cannot open `/dev/null'");
2016 slirpCtx.logFD = devNullFd;
2017 }
2018
2019 addPhaseAfter(slirpCtx.phases,
2020 "makeChrootSeparateFilesystem",
2021 "prepareSlirpChroot",
2022 prepareSlirpChrootAction);
2023
2024 /* slirp behaves differently when uid != 0 */
2025 addPhaseAfter(slirpCtx.phases,
2026 "lockMounts",
2027 "remapIdsTo0",
2028 remapIdsTo0Action);
2029
2030#if 0 /* For debugging networking issues */
2031 slirpCtx.env["SLIRP_DEBUG"] = "call,misc,error,tftp,verbose_call";
2032 slirpCtx.env["G_MESSAGES_DEBUG"] = "all";
2033#endif
2034
2035 pid_t slirpPid = cloneChild(slirpCtx);
2036
2037 if(newUserNS) {
2038 slirpSetupPipe.readSide.close();
2039 initializeUserNamespace(slirpPid, getuid(), getgid(), getuid(), getgid());
2040 writeFull(slirpSetupPipe.writeSide, (unsigned char*)"go\n", 3);
2041 }
2042 return slirpPid;
2043}
2044
2045static void clearRootWritePermsAction(SpawnContext & sctx)
1628{ 2046{
1629 if(chmod("/", 0555) == -1) 2047 if(chmod("/", 0555) == -1)
1630 throw SysError("changing mode of chroot root directory"); 2048 throw SysError("changing mode of chroot root directory");
1631} 2049}
1632 2050
2051
2052/* Note: linux-only */
2053bool haveGlobalIPv6Address()
2054{
2055 if(!pathExists("/proc/net/if_inet6")) return false;
2056
2057 auto addresses = tokenizeString<Strings>(readFile("/proc/net/if_inet6", true), "\n");
2058 for(auto & i : addresses) {
2059 auto fields = tokenizeString<vector<string> >(i, " ");
2060 auto scopeHex = fields.at(3);
2061 /* 0x0 means "Global scope" */
2062 if(scopeHex == "00" || scopeHex == "40") return true;
2063 }
2064 return false;
2065}
2066
1633#endif /* CHROOT_ENABLED */ 2067#endif /* CHROOT_ENABLED */
1634 2068
1635/* Return true if the operating system kernel part of SYSTEM1 and SYSTEM2 (the 2069/* Return true if the operating system kernel part of SYSTEM1 and SYSTEM2 (the
@@ -1731,10 +2165,10 @@ void DerivationGoal::startBuilder()
1731 f.exceptions(boost::io::all_error_bits ^ boost::io::too_many_args_bit); 2165 f.exceptions(boost::io::all_error_bits ^ boost::io::too_many_args_bit);
1732 startNest(nest, lvlInfo, f % showPaths(missingPaths) % curRound % nrRounds); 2166 startNest(nest, lvlInfo, f % showPaths(missingPaths) % curRound % nrRounds);
1733 2167
1734 /* A CloneSpawnContext reference can be passed to procedures expecting a 2168 /* A ChrootBuildSpawnContext reference can be passed to procedures
1735 SpawnContext reference */ 2169 expecting a SpawnContext reference */
1736#if CHROOT_ENABLED 2170#if CHROOT_ENABLED
1737 CloneSpawnContext ctx; 2171 ChrootBuildSpawnContext ctx;
1738#else 2172#else
1739 SpawnContext ctx; 2173 SpawnContext ctx;
1740#endif 2174#endif
@@ -1945,6 +2379,10 @@ void DerivationGoal::startBuilder()
1945 ctx.supplementaryGroups = buildUser.getSupplementaryGIDs(); 2379 ctx.supplementaryGroups = buildUser.getSupplementaryGIDs();
1946 } 2380 }
1947 2381
2382#if CHROOT_ENABLED
2383 bool useSlirp4netns = false;
2384#endif
2385
1948 if (useChroot) { 2386 if (useChroot) {
1949#if CHROOT_ENABLED 2387#if CHROOT_ENABLED
1950 ctx.phases = getCloneSpawnPhases(); 2388 ctx.phases = getCloneSpawnPhases();
@@ -1960,14 +2398,26 @@ void DerivationGoal::startBuilder()
1960 /* Clean up the chroot directory automatically. */ 2398 /* Clean up the chroot directory automatically. */
1961 autoDelChroot = std::shared_ptr<AutoDelete>(new AutoDelete(chrootRootTop)); 2399 autoDelChroot = std::shared_ptr<AutoDelete>(new AutoDelete(chrootRootTop));
1962 2400
2401 if(fixedOutput) {
2402 if(findProgram(settings.slirp4netns) == "")
2403 printMsg(lvlError, format("`%1%' can't be found in PATH, network access disabled") % settings.slirp4netns);
2404 else {
2405 if(!pathExists("/dev/net/tun"))
2406 printMsg(lvlError, "`/dev/net/tun' is missing, network access disabled");
2407 else {
2408 useSlirp4netns = true;
2409 ctx.ipv6Enabled = haveGlobalIPv6Address();
2410 }
2411 }
2412 }
2413
1963 ctx.doChroot = true; 2414 ctx.doChroot = true;
1964 ctx.chrootRootDir = chrootRootDir; 2415 ctx.chrootRootDir = chrootRootDir;
1965 ctx.cloneFlags = CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWIPC | CLONE_NEWUTS | SIGCHLD; 2416 ctx.cloneFlags = CLONE_NEWNS | CLONE_NEWNET | CLONE_NEWPID | CLONE_NEWIPC | CLONE_NEWUTS | SIGCHLD;
1966 2417
1967 if(!fixedOutput) { 2418 if(!fixedOutput || /* redundant but shows the cases clearly */
2419 (fixedOutput && !settings.useHostLoopback))
1968 ctx.initLoopback = true; 2420 ctx.initLoopback = true;
1969 ctx.cloneFlags |= CLONE_NEWNET;
1970 }
1971 2421
1972 if(!buildUser.enabled()) 2422 if(!buildUser.enabled())
1973 ctx.cloneFlags |= CLONE_NEWUSER; 2423 ctx.cloneFlags |= CLONE_NEWUSER;
@@ -2014,18 +2464,42 @@ void DerivationGoal::startBuilder()
2014 if (fixedOutput) { 2464 if (fixedOutput) {
2015 /* Fixed-output derivations typically need to access the network, 2465 /* Fixed-output derivations typically need to access the network,
2016 so give them access to /etc/resolv.conf and so on. */ 2466 so give them access to /etc/resolv.conf and so on. */
2017 auto files = { "/etc/resolv.conf", "/etc/nsswitch.conf", 2467 std::vector<Path> files = { "/etc/services", "/etc/nsswitch.conf" };
2018 "/etc/services", "/etc/hosts" }; 2468 if (useSlirp4netns) {
2019 for (auto & file: files) { 2469 if (settings.useHostLoopback) {
2470 string hosts;
2471 if(pathExists("/etc/hosts")) {
2472 hosts = readFile("/etc/hosts");
2473 hosts = std::regex_replace(hosts, std::regex("127\\.0\\.0\\.1"), "10.0.2.2");
2474 hosts = std::regex_replace(hosts, std::regex("::1"), "fd00::2");
2475 } else {
2476 hosts =
2477 "10.0.2.2 localhost\n"
2478 "fd00::2 localhost\n";
2479 }
2480 writeFile(chrootRootDir + "/etc/hosts", hosts);
2481 }
2482 else {
2483 files.push_back("/etc/hosts");
2484 }
2485 writeFile(chrootRootDir + "/etc/resolv.conf", "nameserver 10.0.2.3");
2486 }
2487 else {
2488 files.push_back("/etc/hosts");
2489 files.push_back("/etc/resolv.conf");
2490 }
2491 for (auto & file : files) {
2020 if (pathExists(file)) { 2492 if (pathExists(file)) {
2021 ctx.filesInChroot[file] = file; 2493 ctx.filesInChroot[file] = file;
2022 ctx.readOnlyFilesInChroot.insert(file); 2494 ctx.readOnlyFilesInChroot.insert(file);
2023 } 2495 }
2024 } 2496 }
2025 } else {
2026 /* Create /etc/hosts with localhost entry. */
2027 writeFile(chrootRootDir + "/etc/hosts", "127.0.0.1 localhost\n");
2028 } 2497 }
2498 else
2499 /* Create /etc/hosts with localhost entry. */
2500 writeFile(chrootRootDir + "/etc/hosts",
2501 "127.0.0.1 localhost\n"
2502 "::1 localhost\n");
2029 2503
2030 /* Bind-mount a user-configurable set of directories from the 2504 /* Bind-mount a user-configurable set of directories from the
2031 host file system. */ 2505 host file system. */
@@ -2175,7 +2649,9 @@ void DerivationGoal::startBuilder()
2175 2649
2176 - The private network namespace ensures that the builder cannot 2650 - The private network namespace ensures that the builder cannot
2177 talk to the outside world (or vice versa). It only has a 2651 talk to the outside world (or vice versa). It only has a
2178 private loopback interface. 2652 private loopback interface. As an exception, fixed-output
2653 derivations may talk to the outside world through slirp4netns, but
2654 still in a separate network namespace.
2179 2655
2180 - The IPC namespace prevents the builder from communicating 2656 - The IPC namespace prevents the builder from communicating
2181 with outside processes using SysV IPC mechanisms (shared 2657 with outside processes using SysV IPC mechanisms (shared
@@ -2191,7 +2667,7 @@ void DerivationGoal::startBuilder()
2191 AutoCloseFD parentSetupSocket; 2667 AutoCloseFD parentSetupSocket;
2192 AutoCloseFD childSetupSocket; 2668 AutoCloseFD childSetupSocket;
2193 2669
2194 if(((ctx.cloneFlags & CLONE_NEWUSER) != 0)) { 2670 if(((ctx.cloneFlags & CLONE_NEWUSER) != 0) || useSlirp4netns) {
2195 if (socketpair(AF_LOCAL, SOCK_STREAM, 0, fds)) 2671 if (socketpair(AF_LOCAL, SOCK_STREAM, 0, fds))
2196 throw SysError("creating setup socket"); 2672 throw SysError("creating setup socket");
2197 parentSetupSocket = fds[0]; 2673 parentSetupSocket = fds[0];
@@ -2202,6 +2678,15 @@ void DerivationGoal::startBuilder()
2202 ctx.setupFD = childSetupSocket; 2678 ctx.setupFD = childSetupSocket;
2203 } 2679 }
2204 2680
2681 if(useSlirp4netns) {
2682 addPhaseAfter(ctx.phases, "initLoopback", "setupTap", setupTapAction);
2683 addPhaseAfter(ctx.phases, "setupTap", "waitForSlirpReady",
2684 waitForSlirpReadyAction);
2685 if(settings.useHostLoopback)
2686 addPhaseAfter(ctx.phases, "waitForSlirpReady", "enableRouteLocalnet",
2687 enableRouteLocalnetAction);
2688 }
2689
2205 pid = cloneChild(ctx); 2690 pid = cloneChild(ctx);
2206 2691
2207 if(childSetupSocket >= 0) childSetupSocket.close(); 2692 if(childSetupSocket >= 0) childSetupSocket.close();
@@ -2211,6 +2696,34 @@ void DerivationGoal::startBuilder()
2211 initializeUserNamespace(pid); 2696 initializeUserNamespace(pid);
2212 writeFull(parentSetupSocket, (unsigned char*)"go\n", 3); 2697 writeFull(parentSetupSocket, (unsigned char*)"go\n", 3);
2213 } 2698 }
2699
2700 try {
2701 if(useSlirp4netns) {
2702 AutoCloseFD tapfd = receiveFD(parentSetupSocket);
2703 /* Start 'slirp4netns' to provide networking in the child process;
2704 running the builder in the global network namespace would give
2705 it access to the global namespace of abstract sockets, which
2706 could be used to grant write access to the store to an external
2707 process. */
2708 slirp = spawnSlirp4netns(
2709 tapfd,
2710 parentSetupSocket,
2711 /* Do whatever we can to run slirp4netns as some user
2712 other than root - run it as the build user if
2713 necessary */
2714 buildUser.enabled() ? buildUser.getUID() : getuid(),
2715 buildUser.enabled() ? buildUser.getGID() : getgid());
2716 }
2717 } catch(std::exception & e) {
2718 if(slirp != -1) {
2719 slirp.kill(true);
2720 }
2721 if(pid != -1) {
2722 pid.kill(true);
2723 }
2724 throw e;
2725 }
2726
2214 } else 2727 } else
2215#endif 2728#endif
2216 { 2729 {
diff --git a/nix/libstore/globals.cc b/nix/libstore/globals.cc
index 10c60f6106d..31da8d4769d 100644
--- a/nix/libstore/globals.cc
+++ b/nix/libstore/globals.cc
@@ -56,6 +56,8 @@ Settings::Settings()
56 envKeepDerivations = false; 56 envKeepDerivations = false;
57 lockCPU = getEnv("NIX_AFFINITY_HACK", "1") == "1"; 57 lockCPU = getEnv("NIX_AFFINITY_HACK", "1") == "1";
58 showTrace = false; 58 showTrace = false;
59 useHostLoopback = true;
60 slirp4netns = SLIRP4NETNS;
59} 61}
60 62
61 63
diff --git a/nix/libstore/globals.hh b/nix/libstore/globals.hh
index 27616a22834..7cfa06e76c1 100644
--- a/nix/libstore/globals.hh
+++ b/nix/libstore/globals.hh
@@ -206,6 +206,15 @@ struct Settings {
206 /* Whether to show a stack trace if Nix evaluation fails. */ 206 /* Whether to show a stack trace if Nix evaluation fails. */
207 bool showTrace; 207 bool showTrace;
208 208
209 /* Whether fixed-output chroot builds should be able to use the host
210 loopback, for example to access a socks proxy. Note that while using
211 "localhost" and 127.0.0.1 to access the host loopback will work, using
212 ::1 will not, due to a limitation in Linux. */
213 bool useHostLoopback;
214
215 /* The filename to use for executing slirp4netns when it is needed. */
216 Path slirp4netns;
217
209private: 218private:
210 SettingsMap settings, overrides; 219 SettingsMap settings, overrides;
211 220
diff --git a/nix/libutil/util.cc b/nix/libutil/util.cc
index e71e6c170ad..327edf471f0 100644
--- a/nix/libutil/util.cc
+++ b/nix/libutil/util.cc
@@ -14,6 +14,7 @@
14#include <unistd.h> 14#include <unistd.h>
15#include <fcntl.h> 15#include <fcntl.h>
16#include <limits.h> 16#include <limits.h>
17#include <sys/socket.h>
17 18
18#ifdef __APPLE__ 19#ifdef __APPLE__
19#include <sys/syscall.h> 20#include <sys/syscall.h>
@@ -62,6 +63,27 @@ string getEnv(const string & key, const string & def)
62} 63}
63 64
64 65
66string findProgram(const string & program)
67{
68 if(program.empty()) return "";
69
70 if(program[0] == '/') return pathExists(program) ? program : "";
71
72 char *path_ = getenv("PATH");
73 if(path_ == NULL) return "";
74 string path = path_;
75
76 Strings dirs = tokenizeString<Strings>(path, ":");
77 for (const auto& i : dirs) {
78 if(i == "") continue;
79 string f = i + "/" + program;
80 if(pathExists(f)) return f;
81 }
82
83 return "";
84}
85
86
65Path absPath(Path path, Path dir) 87Path absPath(Path path, Path dir)
66{ 88{
67 if (path[0] != '/') { 89 if (path[0] != '/') {
@@ -857,6 +879,67 @@ void Pipe::create()
857} 879}
858 880
859 881
882void sendFD(int sock, int fd)
883{
884 ssize_t rc;
885 struct msghdr msg;
886 struct cmsghdr *cmsg;
887 char cmsgbuf[CMSG_SPACE(sizeof(fd))];
888 struct iovec iov;
889 char dummy = '\0';
890 memset(&msg, 0, sizeof(msg));
891 iov.iov_base = &dummy;
892 iov.iov_len = 1;
893 msg.msg_iov = &iov;
894 msg.msg_iovlen = 1;
895 msg.msg_control = cmsgbuf;
896 msg.msg_controllen = sizeof(cmsgbuf);
897 cmsg = CMSG_FIRSTHDR(&msg);
898 cmsg->cmsg_level = SOL_SOCKET;
899 cmsg->cmsg_type = SCM_RIGHTS;
900 cmsg->cmsg_len = CMSG_LEN(sizeof(fd));
901 memcpy(CMSG_DATA(cmsg), &fd, sizeof(fd));
902 msg.msg_controllen = cmsg->cmsg_len;
903 do
904 {
905 rc = sendmsg(sock, &msg, 0);
906 } while(rc < 0 && errno == EINTR);
907 if(rc < 0)
908 throw SysError("sending fd");
909}
910
911
912int receiveFD(int sock)
913{
914 int fd;
915 ssize_t rc;
916 struct msghdr msg;
917 struct cmsghdr *cmsg;
918 char cmsgbuf[CMSG_SPACE(sizeof(fd))];
919 struct iovec iov;
920 char dummy = '\0';
921 memset(&msg, 0, sizeof(msg));
922 iov.iov_base = &dummy;
923 iov.iov_len = 1;
924 msg.msg_iov = &iov;
925 msg.msg_iovlen = 1;
926 msg.msg_control = cmsgbuf;
927 msg.msg_controllen = sizeof(cmsgbuf);
928 do
929 {
930 rc = recvmsg(sock, &msg, 0);
931 } while(rc < 0 && errno == EINTR);
932 if (rc < 0)
933 throw SysError("receiving fd");
934 if (rc == 0)
935 throw Error("received EOF (empty message) while receiving fd");
936
937 cmsg = CMSG_FIRSTHDR(&msg);
938 if (cmsg == NULL || cmsg->cmsg_type != SCM_RIGHTS)
939 throw Error("received message without an fd");
940 memcpy(&fd, CMSG_DATA(cmsg), sizeof(fd));
941 return fd;
942}
860 943
861////////////////////////////////////////////////////////////////////// 944//////////////////////////////////////////////////////////////////////
862 945
@@ -1301,6 +1384,24 @@ bool endOfList(std::istream & str)
1301 return false; 1384 return false;
1302} 1385}
1303 1386
1387string decodeOctalEscaped(const string & s)
1388{
1389 string r;
1390 for (string::const_iterator i = s.begin(); i != s.end(); ) {
1391 if (*i != '\\') { r += *(i++); continue; }
1392 unsigned char c = 0;
1393 ++i;
1394 for(int j = 0; j < 3; j++) {
1395 if(i == s.end() || *i < '0' || *i >= '8')
1396 throw Error("malformed octal escape");
1397 c = c * 8 + (*i - '0');
1398 ++i;
1399 }
1400 r += c;
1401 }
1402 return r;
1403}
1404
1304 1405
1305void ignoreException() 1406void ignoreException()
1306{ 1407{
diff --git a/nix/libutil/util.hh b/nix/libutil/util.hh
index ab2395e959f..648d6f19a4c 100644
--- a/nix/libutil/util.hh
+++ b/nix/libutil/util.hh
@@ -19,6 +19,12 @@ namespace nix {
19/* Return an environment variable. */ 19/* Return an environment variable. */
20string getEnv(const string & key, const string & def = ""); 20string getEnv(const string & key, const string & def = "");
21 21
22/* Find the absolute filename corresponding to PROGRAM, searching PATH if
23 PROGRAM is a relative filename. If PROGRAM is an absolute filename for a
24 file that doesn't exist, or it can't be found in PATH, then return the
25 empty string. */
26string findProgram(const string & program);
27
22/* Return an absolutized path, resolving paths relative to the 28/* Return an absolutized path, resolving paths relative to the
23 specified directory, or the current directory otherwise. The path 29 specified directory, or the current directory otherwise. The path
24 is also canonicalised. */ 30 is also canonicalised. */
@@ -207,6 +213,10 @@ public:
207 int borrow(); 213 int borrow();
208}; 214};
209 215
216/* Send and receive an FD on a unix-domain socket, along with a single null
217 byte of regular data. */
218void sendFD(int sock, int fd);
219int receiveFD(int sock);
210 220
211class Pipe 221class Pipe
212{ 222{
@@ -370,6 +380,12 @@ string parseString(std::istream & str);
370bool endOfList(std::istream & str); 380bool endOfList(std::istream & str);
371 381
372 382
383/* Escape a string that contains octal-encoded escape codes such as
384 used in /etc/fstab and /proc/mounts (e.g. "foo\040bar" decodes to
385 "foo bar"). */
386string decodeOctalEscaped(const string & s);
387
388
373/* Exception handling in destructors: print an error message, then 389/* Exception handling in destructors: print an error message, then
374 ignore the exception. */ 390 ignore the exception. */
375void ignoreException(); 391void ignoreException();
diff --git a/nix/nix-daemon/guix-daemon.cc b/nix/nix-daemon/guix-daemon.cc
index d7ab9c5e649..30727d55593 100644
--- a/nix/nix-daemon/guix-daemon.cc
+++ b/nix/nix-daemon/guix-daemon.cc
@@ -90,6 +90,7 @@ builds derivations on behalf of its clients.");
90#define GUIX_OPT_MAX_SILENT_TIME 19 90#define GUIX_OPT_MAX_SILENT_TIME 19
91#define GUIX_OPT_LOG_COMPRESSION 20 91#define GUIX_OPT_LOG_COMPRESSION 20
92#define GUIX_OPT_DISCOVER 21 92#define GUIX_OPT_DISCOVER 21
93#define GUIX_OPT_ISOLATE_HOST_LOOPBACK 22
93 94
94static const struct argp_option options[] = 95static const struct argp_option options[] =
95 { 96 {
@@ -160,6 +161,8 @@ to live outputs") },
160 n_("listen for connections on SOCKET") }, 161 n_("listen for connections on SOCKET") },
161 { "debug", GUIX_OPT_DEBUG, 0, 0, 162 { "debug", GUIX_OPT_DEBUG, 0, 0,
162 n_("produce debugging output") }, 163 n_("produce debugging output") },
164 { "isolate-host-loopback", GUIX_OPT_ISOLATE_HOST_LOOPBACK, 0, 0,
165 n_("do not allow fixed-output chroot builds to access the host loopback") },
163 { 0, 0, 0, 0, 0 } 166 { 0, 0, 0, 0, 0 }
164 }; 167 };
165 168
@@ -294,6 +297,9 @@ parse_opt (int key, char *arg, struct argp_state *state)
294 case GUIX_OPT_SYSTEM: 297 case GUIX_OPT_SYSTEM:
295 settings.thisSystem = arg; 298 settings.thisSystem = arg;
296 break; 299 break;
300 case GUIX_OPT_ISOLATE_HOST_LOOPBACK:
301 settings.useHostLoopback = false;
302 break;
297 default: 303 default:
298 return (error_t) ARGP_ERR_UNKNOWN; 304 return (error_t) ARGP_ERR_UNKNOWN;
299 } 305 }