hughesjr / rpms / docker

Forked from rpms/docker 4 years ago
Clone

Blame SOURCES/0001-nsenter-clone-proc-self-exe-to-avoid-exposing-host-b.patch

8eb443
From 2a03a0c265d5bd3e77bc22e7d1fbb57388984727 Mon Sep 17 00:00:00 2001
8eb443
From: Aleksa Sarai <asarai@suse.de>
8eb443
Date: Wed, 9 Jan 2019 13:40:01 +1100
8eb443
Subject: [PATCH 1/1] nsenter: clone /proc/self/exe to avoid exposing host
8eb443
 binary to container
8eb443
8eb443
There are quite a few circumstances where /proc/self/exe pointing to a
8eb443
pretty important container binary is a _bad_ thing, so to avoid this we
8eb443
have to make a copy (preferably doing self-clean-up and not being
8eb443
writeable).
8eb443
8eb443
As a hotfix we require memfd_create(2), but we can always extend this to
8eb443
use a scratch MNT_DETACH overlayfs or tmpfs. The main downside to this
8eb443
approach is no page-cache sharing for the runc binary (which overlayfs
8eb443
would give us) but this is far less complicated.
8eb443
8eb443
This is only done during nsenter so that it happens transparently to the
8eb443
Go code, and any libcontainer users benefit from it. This also makes
8eb443
ExtraFiles and --preserve-fds handling trivial (because we don't need to
8eb443
worry about it).
8eb443
8eb443
Fixes: CVE-2019-5736
8eb443
Co-developed-by: Christian Brauner <christian.brauner@ubuntu.com>
8eb443
Signed-off-by: Aleksa Sarai <asarai@suse.de>
8eb443
Signed-off-by: Mrunal Patel <mrunalp@gmail.com>
8eb443
---
8eb443
 libcontainer/nsenter/cloned_binary.c | 221 +++++++++++++++++++++++++++
8eb443
 libcontainer/nsenter/nsexec.c        |  11 ++
8eb443
 2 files changed, 232 insertions(+)
8eb443
 create mode 100644 libcontainer/nsenter/cloned_binary.c
8eb443
8eb443
diff --git a/libcontainer/nsenter/cloned_binary.c b/libcontainer/nsenter/cloned_binary.c
8eb443
new file mode 100644
8eb443
index 00000000..d9f6093a
8eb443
--- /dev/null
8eb443
+++ b/libcontainer/nsenter/cloned_binary.c
8eb443
@@ -0,0 +1,221 @@
8eb443
+#define _GNU_SOURCE
8eb443
+#include <unistd.h>
8eb443
+#include <stdio.h>
8eb443
+#include <stdlib.h>
8eb443
+#include <stdbool.h>
8eb443
+#include <string.h>
8eb443
+#include <limits.h>
8eb443
+#include <fcntl.h>
8eb443
+#include <errno.h>
8eb443
+
8eb443
+#include <sys/types.h>
8eb443
+#include <sys/stat.h>
8eb443
+#include <sys/vfs.h>
8eb443
+#include <sys/mman.h>
8eb443
+#include <sys/sendfile.h>
8eb443
+#include <sys/syscall.h>
8eb443
+
8eb443
+#include <linux/magic.h>
8eb443
+#include <linux/memfd.h>
8eb443
+
8eb443
+/* Use our own wrapper for memfd_create. */
8eb443
+#if !defined(SYS_memfd_create) && defined(__NR_memfd_create)
8eb443
+#  define SYS_memfd_create __NR_memfd_create
8eb443
+#endif
8eb443
+#ifndef SYS_memfd_create
8eb443
+#  error "memfd_create(2) syscall not supported by this glibc version"
8eb443
+#endif
8eb443
+int memfd_create(const char *name, unsigned int flags)
8eb443
+{
8eb443
+	return syscall(SYS_memfd_create, name, flags);
8eb443
+}
8eb443
+
8eb443
+/* This comes directly from <linux/fcntl.h>. */
8eb443
+#ifndef F_LINUX_SPECIFIC_BASE
8eb443
+#  define F_LINUX_SPECIFIC_BASE 1024
8eb443
+#endif
8eb443
+#ifndef F_ADD_SEALS
8eb443
+#  define F_ADD_SEALS (F_LINUX_SPECIFIC_BASE + 9)
8eb443
+#  define F_GET_SEALS (F_LINUX_SPECIFIC_BASE + 10)
8eb443
+#endif
8eb443
+#ifndef F_SEAL_SEAL
8eb443
+#  define F_SEAL_SEAL   0x0001	/* prevent further seals from being set */
8eb443
+#  define F_SEAL_SHRINK 0x0002	/* prevent file from shrinking */
8eb443
+#  define F_SEAL_GROW   0x0004	/* prevent file from growing */
8eb443
+#  define F_SEAL_WRITE  0x0008	/* prevent writes */
8eb443
+#endif
8eb443
+
8eb443
+
8eb443
+#define OUR_MEMFD_COMMENT "runc_cloned:/proc/self/exe"
8eb443
+#define OUR_MEMFD_SEALS \
8eb443
+	(F_SEAL_SEAL | F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE)
8eb443
+
8eb443
+static void *must_realloc(void *ptr, size_t size)
8eb443
+{
8eb443
+	void *old = ptr;
8eb443
+	do {
8eb443
+		ptr = realloc(old, size);
8eb443
+	} while(!ptr);
8eb443
+	return ptr;
8eb443
+}
8eb443
+
8eb443
+/*
8eb443
+ * Verify whether we are currently in a self-cloned program (namely, is
8eb443
+ * /proc/self/exe a memfd). F_GET_SEALS will only succeed for memfds (or rather
8eb443
+ * for shmem files), and we want to be sure it's actually sealed.
8eb443
+ */
8eb443
+static int is_self_cloned(void)
8eb443
+{
8eb443
+	int fd, seals;
8eb443
+
8eb443
+	fd = open("/proc/self/exe", O_RDONLY|O_CLOEXEC);
8eb443
+	if (fd < 0)
8eb443
+		return -ENOTRECOVERABLE;
8eb443
+
8eb443
+	seals = fcntl(fd, F_GET_SEALS);
8eb443
+	close(fd);
8eb443
+	return seals == OUR_MEMFD_SEALS;
8eb443
+}
8eb443
+
8eb443
+/*
8eb443
+ * Basic wrapper around mmap(2) that gives you the file length so you can
8eb443
+ * safely treat it as an ordinary buffer. Only gives you read access.
8eb443
+ */
8eb443
+static char *read_file(char *path, size_t *length)
8eb443
+{
8eb443
+	int fd;
8eb443
+	char buf[4096], *copy = NULL;
8eb443
+
8eb443
+	if (!length)
8eb443
+		return NULL;
8eb443
+
8eb443
+	fd = open(path, O_RDONLY | O_CLOEXEC);
8eb443
+	if (fd < 0)
8eb443
+		return NULL;
8eb443
+
8eb443
+	*length = 0;
8eb443
+	for (;;) {
8eb443
+		int n;
8eb443
+
8eb443
+		n = read(fd, buf, sizeof(buf));
8eb443
+		if (n < 0)
8eb443
+			goto error;
8eb443
+		if (!n)
8eb443
+			break;
8eb443
+
8eb443
+		copy = must_realloc(copy, (*length + n) * sizeof(*copy));
8eb443
+		memcpy(copy + *length, buf, n);
8eb443
+		*length += n;
8eb443
+	}
8eb443
+	close(fd);
8eb443
+	return copy;
8eb443
+
8eb443
+error:
8eb443
+	close(fd);
8eb443
+	free(copy);
8eb443
+	return NULL;
8eb443
+}
8eb443
+
8eb443
+/*
8eb443
+ * A poor-man's version of "xargs -0". Basically parses a given block of
8eb443
+ * NUL-delimited data, within the given length and adds a pointer to each entry
8eb443
+ * to the array of pointers.
8eb443
+ */
8eb443
+static int parse_xargs(char *data, int data_length, char ***output)
8eb443
+{
8eb443
+	int num = 0;
8eb443
+	char *cur = data;
8eb443
+
8eb443
+	if (!data || *output != NULL)
8eb443
+		return -1;
8eb443
+
8eb443
+	while (cur < data + data_length) {
8eb443
+		num++;
8eb443
+		*output = must_realloc(*output, (num + 1) * sizeof(**output));
8eb443
+		(*output)[num - 1] = cur;
8eb443
+		cur += strlen(cur) + 1;
8eb443
+	}
8eb443
+	(*output)[num] = NULL;
8eb443
+	return num;
8eb443
+}
8eb443
+
8eb443
+/*
8eb443
+ * "Parse" out argv and envp from /proc/self/cmdline and /proc/self/environ.
8eb443
+ * This is necessary because we are running in a context where we don't have a
8eb443
+ * main() that we can just get the arguments from.
8eb443
+ */
8eb443
+static int fetchve(char ***argv, char ***envp)
8eb443
+{
8eb443
+	char *cmdline = NULL, *environ = NULL;
8eb443
+	size_t cmdline_size, environ_size;
8eb443
+
8eb443
+	cmdline = read_file("/proc/self/cmdline", &cmdline_size);
8eb443
+	if (!cmdline)
8eb443
+		goto error;
8eb443
+	environ = read_file("/proc/self/environ", &environ_size);
8eb443
+	if (!environ)
8eb443
+		goto error;
8eb443
+
8eb443
+	if (parse_xargs(cmdline, cmdline_size, argv) <= 0)
8eb443
+		goto error;
8eb443
+	if (parse_xargs(environ, environ_size, envp) <= 0)
8eb443
+		goto error;
8eb443
+
8eb443
+	return 0;
8eb443
+
8eb443
+error:
8eb443
+	free(environ);
8eb443
+	free(cmdline);
8eb443
+	return -EINVAL;
8eb443
+}
8eb443
+
8eb443
+#define SENDFILE_MAX 0x7FFFF000 /* sendfile(2) is limited to 2GB. */
8eb443
+static int clone_binary(void)
8eb443
+{
8eb443
+	int binfd, memfd, err;
8eb443
+	ssize_t sent = 0;
8eb443
+
8eb443
+	memfd = memfd_create(OUR_MEMFD_COMMENT, MFD_CLOEXEC | MFD_ALLOW_SEALING);
8eb443
+	if (memfd < 0)
8eb443
+		return -ENOTRECOVERABLE;
8eb443
+
8eb443
+	binfd = open("/proc/self/exe", O_RDONLY | O_CLOEXEC);
8eb443
+	if (binfd < 0)
8eb443
+		goto error;
8eb443
+
8eb443
+	sent = sendfile(memfd, binfd, NULL, SENDFILE_MAX);
8eb443
+	close(binfd);
8eb443
+	if (sent < 0)
8eb443
+		goto error;
8eb443
+
8eb443
+	err = fcntl(memfd, F_ADD_SEALS, OUR_MEMFD_SEALS);
8eb443
+	if (err < 0)
8eb443
+		goto error;
8eb443
+
8eb443
+	return memfd;
8eb443
+
8eb443
+error:
8eb443
+	close(memfd);
8eb443
+	return -EIO;
8eb443
+}
8eb443
+
8eb443
+int ensure_cloned_binary(void)
8eb443
+{
8eb443
+	int execfd;
8eb443
+	char **argv = NULL, **envp = NULL;
8eb443
+
8eb443
+	/* Check that we're not self-cloned, and if we are then bail. */
8eb443
+	int cloned = is_self_cloned();
8eb443
+	if (cloned > 0 || cloned == -ENOTRECOVERABLE)
8eb443
+		return cloned;
8eb443
+
8eb443
+	if (fetchve(&argv, &envp) < 0)
8eb443
+		return -EINVAL;
8eb443
+
8eb443
+	execfd = clone_binary();
8eb443
+	if (execfd < 0)
8eb443
+		return -EIO;
8eb443
+
8eb443
+	fexecve(execfd, argv, envp);
8eb443
+	return -ENOEXEC;
8eb443
+}
8eb443
diff --git a/libcontainer/nsenter/nsexec.c b/libcontainer/nsenter/nsexec.c
8eb443
index 0a10f802..814c738d 100644
8eb443
--- a/libcontainer/nsenter/nsexec.c
8eb443
+++ b/libcontainer/nsenter/nsexec.c
8eb443
@@ -420,6 +420,9 @@ void join_namespaces(char *nslist)
8eb443
 	free(namespaces);
8eb443
 }
8eb443
 
8eb443
+/* Defined in cloned_binary.c. */
8eb443
+int ensure_cloned_binary(void);
8eb443
+
8eb443
 void nsexec(void)
8eb443
 {
8eb443
 	int pipenum;
8eb443
@@ -435,6 +438,14 @@ void nsexec(void)
8eb443
 	if (pipenum == -1)
8eb443
 		return;
8eb443
 
8eb443
+	/*
8eb443
+	 * We need to re-exec if we are not in a cloned binary. This is necessary
8eb443
+	 * to ensure that containers won't be able to access the host binary
8eb443
+	 * through /proc/self/exe. See CVE-2019-5736.
8eb443
+	 */
8eb443
+	if (ensure_cloned_binary() < 0)
8eb443
+		bail("could not ensure we are a cloned binary");
8eb443
+
8eb443
 	/* make the process non-dumpable */
8eb443
 	if (prctl(PR_SET_DUMPABLE, 0, 0, 0, 0) != 0) {
8eb443
 		bail("failed to set process as non-dumpable");
8eb443
-- 
8eb443
2.20.1
8eb443