mirror of
https://github.com/opencontainers/runc.git
synced 2026-07-11 06:03:57 +08:00
614ce12f0e
(This is a cherry-pick of b999376fb237195265081a8b8ba3fd3bd6ef8c2c.)
While the ro-bind-mount trick did eliminate the memory overhead of
copying the runc binary for each "runc init" invocation, on machines
with very significant container churn, creating a temporary mount
namespace on every container invocation can trigger severe lock
contention on namespace_sem that makes containers fail to spawn.
The only reason we added bindfd in commit 16612d74de ("nsenter:
cloned_binary: try to ro-bind /proc/self/exe before copying") was due to
a Kubernetes e2e test failure where they had a ridiculously small memory
limit. It seems incredibly unlikely that real workloads are running
without 10MB to spare for the very short time that runc is interacting
with the container.
In addition, since the original cloned_binary implementation, cgroupv2
is now almost universally used on modern systems. Unlike cgroupv1, the
cgroupv2 memcg implementation does not migrate memory usage when
processes change cgroups (even cgroupv1 only did this if you had
memory.move_charge_at_immigrate enabled). In addition, because we do the
/proc/self/exe clone before synchronising the bootstrap data read, we
are guaranteed to do the clone before "runc init" is moved into the
container cgroup -- meaning that the memory used by the /proc/self/exe
clone is charged against the root cgroup, and thus container workloads
should not be affected at all with memfd cloning.
The long-term fix for this problem is to block the /proc/self/exe
re-opening attack entirely in-kernel, which is something I'm working
on[1]. Though it should also be noted that because the memfd is
completely separate to the host binary, even attacks like Dirty COW
against the runc binary can be defended against with the memfd approach.
Of course, once we have in-kernel protection against the /proc/self/exe
re-opening attack, we won't have that protection anymore...
[1]: https://lwn.net/Articles/934460/
Signed-off-by: Aleksa Sarai <cyphar@cyphar.com>
498 lines
12 KiB
C
498 lines
12 KiB
C
// SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later
|
|
/*
|
|
* Copyright (C) 2019 Aleksa Sarai <cyphar@cyphar.com>
|
|
* Copyright (C) 2019 SUSE LLC
|
|
*
|
|
* This work is dual licensed under the following licenses. You may use,
|
|
* redistribute, and/or modify the work under the conditions of either (or
|
|
* both) licenses.
|
|
*
|
|
* === Apache-2.0 ===
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*
|
|
* === LGPL-2.1-or-later ===
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2.1 of the License, or (at your option) any later version.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with this library. If not, see
|
|
* <https://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
#define _GNU_SOURCE
|
|
#include <unistd.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <stdbool.h>
|
|
#include <string.h>
|
|
#include <limits.h>
|
|
#include <fcntl.h>
|
|
#include <errno.h>
|
|
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/statfs.h>
|
|
#include <sys/vfs.h>
|
|
#include <sys/mman.h>
|
|
#include <sys/mount.h>
|
|
#include <sys/sendfile.h>
|
|
#include <sys/syscall.h>
|
|
|
|
/* Use our own wrapper for memfd_create. */
|
|
#ifndef SYS_memfd_create
|
|
# ifdef __NR_memfd_create
|
|
# define SYS_memfd_create __NR_memfd_create
|
|
# else
|
|
/* These values come from <https://fedora.juszkiewicz.com.pl/syscalls.html>. */
|
|
# warning "libc is outdated -- using hard-coded SYS_memfd_create"
|
|
# if defined(__x86_64__)
|
|
# define SYS_memfd_create 319
|
|
# elif defined(__i386__)
|
|
# define SYS_memfd_create 356
|
|
# elif defined(__ia64__)
|
|
# define SYS_memfd_create 1340
|
|
# elif defined(__arm__)
|
|
# define SYS_memfd_create 385
|
|
# elif defined(__aarch64__)
|
|
# define SYS_memfd_create 279
|
|
# elif defined(__ppc__) || defined(__PPC64__) || defined(__powerpc64__)
|
|
# define SYS_memfd_create 360
|
|
# elif defined(__s390__) || defined(__s390x__)
|
|
# define SYS_memfd_create 350
|
|
# else
|
|
# warning "unknown architecture -- cannot hard-code SYS_memfd_create"
|
|
# endif
|
|
# endif
|
|
#endif
|
|
|
|
/* memfd_create(2) flags -- copied from <linux/memfd.h>. */
|
|
#ifndef MFD_CLOEXEC
|
|
# define MFD_CLOEXEC 0x0001U
|
|
# define MFD_ALLOW_SEALING 0x0002U
|
|
#endif
|
|
|
|
int memfd_create(const char *name, unsigned int flags)
|
|
{
|
|
#ifdef SYS_memfd_create
|
|
return syscall(SYS_memfd_create, name, flags);
|
|
#else
|
|
errno = ENOSYS;
|
|
return -1;
|
|
#endif
|
|
}
|
|
|
|
/* This comes directly from <linux/fcntl.h>. */
|
|
#ifndef F_LINUX_SPECIFIC_BASE
|
|
# define F_LINUX_SPECIFIC_BASE 1024
|
|
#endif
|
|
#ifndef F_ADD_SEALS
|
|
# define F_ADD_SEALS (F_LINUX_SPECIFIC_BASE + 9)
|
|
# define F_GET_SEALS (F_LINUX_SPECIFIC_BASE + 10)
|
|
#endif
|
|
#ifndef F_SEAL_SEAL
|
|
# define F_SEAL_SEAL 0x0001 /* prevent further seals from being set */
|
|
# define F_SEAL_SHRINK 0x0002 /* prevent file from shrinking */
|
|
# define F_SEAL_GROW 0x0004 /* prevent file from growing */
|
|
# define F_SEAL_WRITE 0x0008 /* prevent writes */
|
|
#endif
|
|
|
|
#define CLONED_BINARY_ENV "_LIBCONTAINER_CLONED_BINARY"
|
|
#define RUNC_MEMFD_COMMENT "runc_cloned:/proc/self/exe"
|
|
#define RUNC_MEMFD_SEALS \
|
|
(F_SEAL_SEAL | F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE)
|
|
|
|
static void *must_realloc(void *ptr, size_t size)
|
|
{
|
|
void *old = ptr;
|
|
do {
|
|
ptr = realloc(old, size);
|
|
} while (!ptr);
|
|
return ptr;
|
|
}
|
|
|
|
/*
|
|
* Verify whether we are currently in a self-cloned program (namely, is
|
|
* /proc/self/exe a memfd). F_GET_SEALS will only succeed for memfds (or rather
|
|
* for shmem files), and we want to be sure it's actually sealed.
|
|
*/
|
|
static int is_self_cloned(void)
|
|
{
|
|
int fd, ret, is_cloned = 0;
|
|
struct stat statbuf = { };
|
|
struct statfs fsbuf = { };
|
|
|
|
fd = open("/proc/self/exe", O_RDONLY | O_CLOEXEC);
|
|
if (fd < 0) {
|
|
fprintf(stderr, "you have no read access to runc binary file\n");
|
|
return -ENOTRECOVERABLE;
|
|
}
|
|
|
|
/*
|
|
* Is the binary a fully-sealed memfd? We don't need CLONED_BINARY_ENV for
|
|
* this, because you cannot write to a sealed memfd no matter what (so
|
|
* sharing it isn't a bad thing -- and an admin could bind-mount a sealed
|
|
* memfd to /usr/bin/runc to allow reuse).
|
|
*/
|
|
ret = fcntl(fd, F_GET_SEALS);
|
|
if (ret >= 0) {
|
|
is_cloned = (ret == RUNC_MEMFD_SEALS);
|
|
goto out;
|
|
}
|
|
|
|
/*
|
|
* All other forms require CLONED_BINARY_ENV, since they are potentially
|
|
* writeable (or we can't tell if they're fully safe) and thus we must
|
|
* check the environment as an extra layer of defence.
|
|
*/
|
|
if (!getenv(CLONED_BINARY_ENV)) {
|
|
is_cloned = false;
|
|
goto out;
|
|
}
|
|
|
|
/*
|
|
* Is the binary on a read-only filesystem? We can't detect bind-mounts in
|
|
* particular (in-kernel they are identical to regular mounts) but we can
|
|
* at least be sure that it's read-only. In addition, to make sure that
|
|
* it's *our* bind-mount we check CLONED_BINARY_ENV.
|
|
*/
|
|
if (fstatfs(fd, &fsbuf) >= 0)
|
|
is_cloned |= (fsbuf.f_flags & MS_RDONLY);
|
|
|
|
/*
|
|
* Okay, we're a tmpfile -- or we're currently running on RHEL <=7.6
|
|
* which appears to have a borked backport of F_GET_SEALS. Either way,
|
|
* having a file which has no hardlinks indicates that we aren't using
|
|
* a host-side "runc" binary and this is something that a container
|
|
* cannot fake (because unlinking requires being able to resolve the
|
|
* path that you want to unlink).
|
|
*/
|
|
if (fstat(fd, &statbuf) >= 0)
|
|
is_cloned |= (statbuf.st_nlink == 0);
|
|
|
|
out:
|
|
close(fd);
|
|
return is_cloned;
|
|
}
|
|
|
|
/* Read a given file into a new buffer, and providing the length. */
|
|
static char *read_file(char *path, size_t *length)
|
|
{
|
|
int fd;
|
|
char buf[4096], *copy = NULL;
|
|
|
|
if (!length)
|
|
return NULL;
|
|
|
|
fd = open(path, O_RDONLY | O_CLOEXEC);
|
|
if (fd < 0)
|
|
return NULL;
|
|
|
|
*length = 0;
|
|
for (;;) {
|
|
ssize_t n;
|
|
|
|
n = read(fd, buf, sizeof(buf));
|
|
if (n < 0)
|
|
goto error;
|
|
if (!n)
|
|
break;
|
|
|
|
copy = must_realloc(copy, (*length + n) * sizeof(*copy));
|
|
memcpy(copy + *length, buf, n);
|
|
*length += n;
|
|
}
|
|
close(fd);
|
|
return copy;
|
|
|
|
error:
|
|
close(fd);
|
|
free(copy);
|
|
return NULL;
|
|
}
|
|
|
|
/*
|
|
* A poor-man's version of "xargs -0". Basically parses a given block of
|
|
* NUL-delimited data, within the given length and adds a pointer to each entry
|
|
* to the array of pointers.
|
|
*/
|
|
static int parse_xargs(char *data, int data_length, char ***output)
|
|
{
|
|
int num = 0;
|
|
char *cur = data;
|
|
|
|
if (!data || *output != NULL)
|
|
return -1;
|
|
|
|
while (cur < data + data_length) {
|
|
num++;
|
|
*output = must_realloc(*output, (num + 1) * sizeof(**output));
|
|
(*output)[num - 1] = cur;
|
|
cur += strlen(cur) + 1;
|
|
}
|
|
(*output)[num] = NULL;
|
|
return num;
|
|
}
|
|
|
|
/*
|
|
* "Parse" out argv from /proc/self/cmdline.
|
|
* This is necessary because we are running in a context where we don't have a
|
|
* main() that we can just get the arguments from.
|
|
*/
|
|
static int fetchve(char ***argv)
|
|
{
|
|
char *cmdline = NULL;
|
|
size_t cmdline_size;
|
|
|
|
cmdline = read_file("/proc/self/cmdline", &cmdline_size);
|
|
if (!cmdline)
|
|
goto error;
|
|
|
|
if (parse_xargs(cmdline, cmdline_size, argv) <= 0)
|
|
goto error;
|
|
|
|
return 0;
|
|
|
|
error:
|
|
free(cmdline);
|
|
return -EINVAL;
|
|
}
|
|
|
|
enum {
|
|
EFD_NONE = 0,
|
|
EFD_MEMFD,
|
|
EFD_FILE,
|
|
};
|
|
|
|
/*
|
|
* This comes from <linux/fcntl.h>. We can't hard-code __O_TMPFILE because it
|
|
* changes depending on the architecture. If we don't have O_TMPFILE we always
|
|
* have the mkostemp(3) fallback.
|
|
*/
|
|
#ifndef O_TMPFILE
|
|
# if defined(__O_TMPFILE) && defined(O_DIRECTORY)
|
|
# define O_TMPFILE (__O_TMPFILE | O_DIRECTORY)
|
|
# endif
|
|
#endif
|
|
|
|
static int make_execfd(int *fdtype)
|
|
{
|
|
int fd = -1;
|
|
char template[PATH_MAX] = { 0 };
|
|
char *prefix = getenv("_LIBCONTAINER_STATEDIR");
|
|
|
|
if (!prefix || *prefix != '/')
|
|
prefix = "/tmp";
|
|
if (snprintf(template, sizeof(template), "%s/runc.XXXXXX", prefix) < 0)
|
|
return -1;
|
|
|
|
/*
|
|
* Now try memfd, it's much nicer than actually creating a file in STATEDIR
|
|
* since it's easily detected thanks to sealing and also doesn't require
|
|
* assumptions about STATEDIR.
|
|
*/
|
|
*fdtype = EFD_MEMFD;
|
|
fd = memfd_create(RUNC_MEMFD_COMMENT, MFD_CLOEXEC | MFD_ALLOW_SEALING);
|
|
if (fd >= 0)
|
|
return fd;
|
|
if (errno != ENOSYS && errno != EINVAL)
|
|
goto error;
|
|
|
|
#ifdef O_TMPFILE
|
|
/*
|
|
* Try O_TMPFILE to avoid races where someone might snatch our file. Note
|
|
* that O_EXCL isn't actually a security measure here (since you can just
|
|
* fd re-open it and clear O_EXCL).
|
|
*/
|
|
*fdtype = EFD_FILE;
|
|
fd = open(prefix, O_TMPFILE | O_EXCL | O_RDWR | O_CLOEXEC, 0700);
|
|
if (fd >= 0) {
|
|
struct stat statbuf = { };
|
|
bool working_otmpfile = false;
|
|
|
|
/*
|
|
* open(2) ignores unknown O_* flags -- yeah, I was surprised when I
|
|
* found this out too. As a result we can't check for EINVAL. However,
|
|
* if we get nlink != 0 (or EISDIR) then we know that this kernel
|
|
* doesn't support O_TMPFILE.
|
|
*/
|
|
if (fstat(fd, &statbuf) >= 0)
|
|
working_otmpfile = (statbuf.st_nlink == 0);
|
|
|
|
if (working_otmpfile)
|
|
return fd;
|
|
|
|
/* Pretend that we got EISDIR since O_TMPFILE failed. */
|
|
close(fd);
|
|
errno = EISDIR;
|
|
}
|
|
if (errno != EISDIR)
|
|
goto error;
|
|
#endif /* defined(O_TMPFILE) */
|
|
|
|
/*
|
|
* Our final option is to create a temporary file the old-school way, and
|
|
* then unlink it so that nothing else sees it by accident.
|
|
*/
|
|
*fdtype = EFD_FILE;
|
|
fd = mkostemp(template, O_CLOEXEC);
|
|
if (fd >= 0) {
|
|
if (unlink(template) >= 0)
|
|
return fd;
|
|
close(fd);
|
|
}
|
|
|
|
error:
|
|
*fdtype = EFD_NONE;
|
|
return -1;
|
|
}
|
|
|
|
static int seal_execfd(int *fd, int fdtype)
|
|
{
|
|
switch (fdtype) {
|
|
case EFD_MEMFD:
|
|
return fcntl(*fd, F_ADD_SEALS, RUNC_MEMFD_SEALS);
|
|
case EFD_FILE:{
|
|
/* Need to re-open our pseudo-memfd as an O_PATH to avoid execve(2) giving -ETXTBSY. */
|
|
int newfd;
|
|
char fdpath[PATH_MAX] = { 0 };
|
|
|
|
if (fchmod(*fd, 0100) < 0)
|
|
return -1;
|
|
|
|
if (snprintf(fdpath, sizeof(fdpath), "/proc/self/fd/%d", *fd) < 0)
|
|
return -1;
|
|
|
|
newfd = open(fdpath, O_PATH | O_CLOEXEC);
|
|
if (newfd < 0)
|
|
return -1;
|
|
|
|
close(*fd);
|
|
*fd = newfd;
|
|
return 0;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static ssize_t fd_to_fd(int outfd, int infd)
|
|
{
|
|
ssize_t total = 0;
|
|
char buffer[4096];
|
|
|
|
for (;;) {
|
|
ssize_t nread, nwritten = 0;
|
|
|
|
nread = read(infd, buffer, sizeof(buffer));
|
|
if (nread < 0)
|
|
return -1;
|
|
if (!nread)
|
|
break;
|
|
|
|
do {
|
|
ssize_t n = write(outfd, buffer + nwritten, nread - nwritten);
|
|
if (n < 0)
|
|
return -1;
|
|
nwritten += n;
|
|
} while (nwritten < nread);
|
|
|
|
total += nwritten;
|
|
}
|
|
|
|
return total;
|
|
}
|
|
|
|
static int clone_binary(void)
|
|
{
|
|
int binfd, execfd;
|
|
struct stat statbuf = { };
|
|
size_t sent = 0;
|
|
int fdtype = EFD_NONE;
|
|
|
|
execfd = make_execfd(&fdtype);
|
|
if (execfd < 0 || fdtype == EFD_NONE)
|
|
return -ENOTRECOVERABLE;
|
|
|
|
binfd = open("/proc/self/exe", O_RDONLY | O_CLOEXEC);
|
|
if (binfd < 0)
|
|
goto error;
|
|
|
|
if (fstat(binfd, &statbuf) < 0)
|
|
goto error_binfd;
|
|
|
|
while (sent < statbuf.st_size) {
|
|
int n = sendfile(execfd, binfd, NULL, statbuf.st_size - sent);
|
|
if (n < 0) {
|
|
/* sendfile can fail so we fallback to a dumb user-space copy. */
|
|
n = fd_to_fd(execfd, binfd);
|
|
if (n < 0)
|
|
goto error_binfd;
|
|
}
|
|
sent += n;
|
|
}
|
|
close(binfd);
|
|
if (sent != statbuf.st_size)
|
|
goto error;
|
|
|
|
if (seal_execfd(&execfd, fdtype) < 0)
|
|
goto error;
|
|
|
|
return execfd;
|
|
|
|
error_binfd:
|
|
close(binfd);
|
|
error:
|
|
close(execfd);
|
|
return -EIO;
|
|
}
|
|
|
|
/* Get cheap access to the environment. */
|
|
extern char **environ;
|
|
|
|
int ensure_cloned_binary(void)
|
|
{
|
|
int execfd;
|
|
char **argv = NULL;
|
|
|
|
/* Check that we're not self-cloned, and if we are then bail. */
|
|
int cloned = is_self_cloned();
|
|
if (cloned > 0 || cloned == -ENOTRECOVERABLE)
|
|
return cloned;
|
|
|
|
if (fetchve(&argv) < 0)
|
|
return -EINVAL;
|
|
|
|
execfd = clone_binary();
|
|
if (execfd < 0)
|
|
return -EIO;
|
|
|
|
if (putenv(CLONED_BINARY_ENV "=1"))
|
|
goto error;
|
|
|
|
fexecve(execfd, argv, environ);
|
|
error:
|
|
close(execfd);
|
|
return -ENOEXEC;
|
|
}
|