[systemd-devel] [PATCH 4/4] nspawn: --populate with dynamic libs and one-file scripts
Shawn Landden
shawn at churchofgit.com
Sun Dec 1 14:50:17 PST 2013
the whitelist of dynamic linker paths comes from clang
---
man/systemd-nspawn.xml | 8 +--
src/nspawn/elf.c | 162 +++++++++++++++++++++++++++++++++++++----
src/nspawn/elf.h | 14 +++-
src/nspawn/nspawn.c | 191 ++++++++++++++++++++++++++++++++++++++++++++++---
src/shared/util.c | 80 +++++++++++++++++++++
src/shared/util.h | 2 +
6 files changed, 431 insertions(+), 26 deletions(-)
diff --git a/man/systemd-nspawn.xml b/man/systemd-nspawn.xml
index 24bc0d7..723ec09 100644
--- a/man/systemd-nspawn.xml
+++ b/man/systemd-nspawn.xml
@@ -214,11 +214,11 @@
<term><option>-p</option></term>
<term><option>--populate</option></term>
- <listitem><para>If COMMAND does not exist in
- target root directory, launch host COMMAND.</para>
+ <listitem><para>Use COMMAND from host.</para>
- <para>Can be used on empty target directories
- (if COMMAND a static executable).</para></listitem>
+ <para>Can be used on empty target directories,
+ if COMMAND an ELF executable, or
+ one-file script.</para></listitem>
</varlistentry>
<varlistentry>
diff --git a/src/nspawn/elf.c b/src/nspawn/elf.c
index f91b374..62d0fda 100644
--- a/src/nspawn/elf.c
+++ b/src/nspawn/elf.c
@@ -26,44 +26,90 @@
#include "elf.h"
#include "util.h"
#include "log.h"
+#include "strv.h"
int analyze_executable(const char *path,
int *_fd,
bool *_elf64,
char **_linker,
- char **shebang) {
+ char **shebang,
+ char ***_libs) {
- char e_ident[sizeof(Elf64_Ehdr)];
+ char e_ident[MAX(2u + PATH_MAX, sizeof(Elf64_Ehdr))];
uint16_t e_type;
off_t e_phoff;
uint16_t e_phentsize, e_phnum;
bool elf64;
- int fd = -1;
+ _cleanup_close_ int fd = -1;
bool have_interp = false;
int r;
assert(path);
+ assert(_fd);
assert(_elf64);
assert(_linker);
assert(shebang);
+ assert(_libs);
fd = open(path, O_RDONLY | O_CLOEXEC);
if (fd < 0) {
- log_error("open(\"%s\") failed: %m", path);
+ log_error("open(%s) failed: %m", path);
return -errno;
}
- r = read(fd, e_ident, sizeof(Elf64_Ehdr));
+ r = read(fd, e_ident, MAX(2u + PATH_MAX, sizeof(Elf64_Ehdr)));
if (r < 0) {
- log_error("read() on %s failed: %m", path);
+ log_error("read(%s) failed: %s", path, strerror(errno));
return -errno;
}
if (memcmp(e_ident, ELFMAG, SELFMAG) != 0) {
- log_error("%s is not an ELF executable.", path);
- return -ENOSYS;
- } else
+ if (startswith(e_ident, "#!")) {
+ _cleanup_close_ int shebang_fd = -1;
+ char shebang_e_ident[sizeof(Elf64_Ehdr)];
+ char *t;
+
+ /* from fs/binfmt_script.c:42 */
+ t = e_ident + strcspn(e_ident, " \t\n");
+ t[0] = '\0';
+
+ t = e_ident[2];
+
+ shebang_fd = open(t, O_RDONLY|O_CLOEXEC);
+ if (shebang_fd < 0) {
+ log_error("Cannot open interpreter %s: %m", t);
+ return -errno;
+ }
+
+ r = read(shebang_fd, shebang_e_ident, sizeof(Elf64_Ehdr));
+ if (r < SELFMAG) {
+ log_error("read(%s) failed: %s", t, strerror(r < 0 ? errno : EIO));
+ return -errno;
+ }
+
+ /* The kernel actually supports interpreters of interpreters
+ * but we don't support that here. */
+ if (memcmp(shebang_e_ident, ELFMAG, SELFMAG) != 0) {
+ log_error("Interpreter %s is not an ELF executable.", t);
+ return -EINVAL;
+ }
+
+ *_fd = fd;
+ fd = shebang_fd; /* analyze and */
+ shebang_fd = -1; /* don't close ELF */
+ *shebang = strdup(t);
+ if (!*shebang)
+ return log_oom();
+
+ memcpy(e_ident, shebang_e_ident, sizeof(Elf64_Ehdr));
+ } else {
+ log_error("%s is not an ELF executable or script starting with #!.", path);
+ return -ENOSYS;
+ }
+ } else {
+ *_fd = fd;
*shebang = NULL;
+ }
switch (e_ident[EI_CLASS]) {
case ELFCLASS32:
@@ -103,14 +149,14 @@ int analyze_executable(const char *path,
char *phdr;
lseek(fd, e_phoff, SEEK_SET);
- r = read(fd, &phdrs, e_phentsize * e_phnum);
+ r = read(fd, phdrs, e_phentsize * e_phnum);
if (r < 0) {
log_error("read() on %s failed: %m", path);
return -errno;
}
for (int i = 0; i < e_phnum; i++) {
- phdr = ((char *)&phdrs + i * e_phentsize);
+ phdr = (phdrs + i * e_phentsize);
/* p_type is the same offset and size regardless
* of 32 or 64-bit, so this works for 64-bit too */
@@ -123,9 +169,101 @@ int analyze_executable(const char *path,
if (!have_interp) {
*_elf64 = elf64;
*_linker = NULL;
+ if (!*shebang)
+ fd = -1; /* don't close */
return 0;
}
/* is dynamic executable */
- return -ENOSYS;
+ _cleanup_free_ char *loput = NULL;
+ _cleanup_strv_free_ char **libs = NULL;
+ char linker[PATH_MAX], *t, *q, *w;
+ const char *c;
+ int n;
+
+ if (elf64)
+ lseek(fd, ((Elf64_Phdr *)phdr)->p_offset, SEEK_SET);
+ else
+ lseek(fd, ((Elf32_Phdr *)phdr)->p_offset, SEEK_SET);
+
+ r = read(fd, linker, PATH_MAX);
+ if (r < 0)
+ return -errno;
+ /* linker name is null terminated, but just in case */
+ linker[PATH_MAX - 1] = '\0';
+
+ /* Get the path of the dynamic linker from PT_INTERP of the executable.
+ * Check if it's a path well-known to be the GNU dynamic linker,
+ * that responds to our LD_TRACE_LOADED_OBJECTS=1.
+ *
+ * The other way to extract this information is "objdump -p", but no
+ * need to depend on that when ld.so does just fine and the target
+ * binary already depends on it. */
+ have_interp = false;
+ NULSTR_FOREACH(c, ld_whitelist)
+ if (strcmp(c, linker) == 0) {
+ have_interp = true;
+ break;
+ }
+
+ if (!have_interp) {
+ log_error("%s, ELF interpreter for %s, not a whitelisted path of the GNU dynamic linker.", linker, *shebang ? *shebang : path);
+ return -EPERM;
+ }
+
+ loput = popenve(linker, (char *const[]){linker, path, NULL}, (char *const[]){"LD_TRACE_LOADED_OBJECTS=1", NULL});
+ if (!loput)
+ return log_oom();
+
+ t = loput; /* This number is too high, but that is OK. */
+ for (n = 0; t = strchr(t, '\n'); n++)
+ t++;
+
+ libs = malloc((n + 1) * sizeof(char *));
+ if (!libs)
+ return log_oom();
+ memzero(libs, (n + 1) * sizeof(char *));
+
+ q = t = loput;
+ for (n = 0; t = strchr(q, '\n'); ) {
+ t++;
+ if (*t != '\t') {
+ if (*t == '\0')
+ break;
+ else {
+ log_error("Failed to parse linker output.");
+ return -EINVAL;
+ }
+ }
+
+ q = strstr(t, " (0x");
+ if (!q) {
+ log_error("Failed to parse linker output.");
+ return -EINVAL;
+ }
+ *q = '\0'; q++;
+
+ w = strstr(t, " => /");
+ if (!w) /* if library is an absolute path or otherwise
+ * does not resolve, like linux-vdso.so.1, ignore it */
+ continue;
+ w += strlen(" => /") - 1;
+
+ libs[n] = strdup(w);
+ if (!libs[n])
+ return log_oom();
+
+ n++;
+ }
+
+ *_linker = strdup(linker);
+ if (!*_linker)
+ return log_oom();
+
+ *_libs = libs;
+ libs = NULL; /* do not free */
+
+ if (!*shebang)
+ fd = -1; /* do not close */
+ return 0;
}
diff --git a/src/nspawn/elf.h b/src/nspawn/elf.h
index 55f8566..1ec54f3 100644
--- a/src/nspawn/elf.h
+++ b/src/nspawn/elf.h
@@ -23,8 +23,20 @@
#include <stdbool.h>
+static const char ld_whitelist[] = "/lib64/ld-linux-x86-64.so.2\0"
+ "/lib/ld-linux.so.2\0"
+ "/lib/ld-linux-armhf.so.3\0"
+ "/lib/ld-linux-aarch64.so.1\0"
+ "/lib64/ld64.so.1\0"
+ "/lib/ld.so.1\0"
+ "/lib64/ld.so.1\0"
+ "/lib32/ld.so.1\0"
+ "/lib/ld.so.1\0"
+ "/lib/ld-linux.so.3\0";
+
int analyze_executable(const char *path,
int *_fd,
bool *_elf64,
char **linker,
- char **shebang);
+ char **shebang,
+ char ***_libs);
diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c
index 24a48e8..6a30df7 100644
--- a/src/nspawn/nspawn.c
+++ b/src/nspawn/nspawn.c
@@ -114,6 +114,12 @@ static uint64_t arg_retain =
static char **arg_bind = NULL;
static char **arg_bind_ro = NULL;
+/* we set atime to this when we created the file only to bind mount onto it */
+#define BIND_FILE_MAGIC_TIME -0x6ddddddd
+static char *populate_shebang = NULL;
+static char *populate_linker = NULL;
+static char **populate_libraries = NULL;
+
static int help(void) {
printf("%s [OPTIONS...] [COMMAND [ARGS...]]\n\n"
@@ -194,7 +200,6 @@ static int parse_argv(int argc, char *argv[]) {
return 0;
case 'D':
- free(arg_directory);
arg_directory = canonicalize_file_name(optarg);
if (!arg_directory) {
log_error("Invalid root directory: %m");
@@ -1058,6 +1063,124 @@ static bool audit_enabled(void) {
return false;
}
+static int mount_file_bind(const char *src, const char *dest, const char *dest_root) {
+ char *q, *j, p[PATH_MAX], t[PATH_MAX];
+ struct stat st;
+ int r;
+
+ if (strlen(dest_root) + strlen(dest) + 1 > PATH_MAX)
+ return -ENAMETOOLONG;
+
+ *t = '\0';
+ strcat(t, dest_root);
+ strcat(t, dest);
+
+ q = strchr(dest, '/');
+ if (q) {
+ q = strrchr(t, '/');
+ *q = '\0';
+
+ j = realpath(t, p);
+ if (!j)
+ return -errno;
+
+ j += strlen(p);
+ *j = '/';
+ strcpy(j + 1, q + 1);
+ } else
+ strcpy(p, t);
+
+ r = touch_time_create_executable(p, BIND_FILE_MAGIC_TIME);
+ if (r < 0) {
+ if (errno == EEXIST) {
+ log_warning("%s already exists in %s", dest, dest_root);
+ return 0;
+ } else
+ return -errno;
+ }
+
+
+ r = mount(src, p, NULL, MS_BIND|MS_RDONLY, NULL);
+ if (r < 0) {
+ if (stat(t, &st) == 0 && st.st_atime == BIND_FILE_MAGIC_TIME)
+ (void)unlink(t);
+
+ return -errno;
+ }
+
+ return 0;
+}
+
+static int mount_file_bind_mkdir(const char *path) {
+ char t[PATH_MAX];
+ int z;
+
+ if (strlen(arg_directory) + strlen(path) + 1 > PATH_MAX)
+ return -ENAMETOOLONG;
+
+ *t = '\0';
+ strcat(t, arg_directory);
+ strcat(t, path);
+
+ z = mkdir_parents(t, 0755);
+ if (z < 0) {
+ log_error("Failed to create directories for %s: %s", path, strerror(-z));
+ return z;
+ }
+
+ z = mount_file_bind(path, path, arg_directory);
+ if (z < 0) {
+ log_error("Failed to bind mount %s: %s", path, strerror(-z));
+ return z;
+ }
+
+ return 0;
+}
+
+static void umount_populate(void) {
+ char t[PATH_MAX];
+ struct stat st;
+ char **lib;
+
+ *t = '\0';
+ strcat(t, arg_directory);
+
+ if (populate_shebang) {
+ assert(strlen(arg_directory) + strlen(populate_shebang) + 1 <= PATH_MAX);
+
+ memcpy(t + strlen(arg_directory), populate_shebang, strlen(populate_shebang) + 1);
+
+ (void)umount(t);
+
+ if (stat(t, &st) == 0 && st.st_atime == BIND_FILE_MAGIC_TIME)
+ (void)unlink(t);
+ }
+
+ if (populate_linker) {
+ assert(strlen(arg_directory) + strlen(populate_linker) + 1 <= PATH_MAX);
+
+ memcpy(t + strlen(arg_directory), populate_linker, strlen(populate_linker) + 1);
+
+ (void)umount(t);
+
+ if (stat(t, &st) == 0 && st.st_atime == BIND_FILE_MAGIC_TIME)
+ (void)unlink(t);
+ }
+
+ STRV_FOREACH(lib, populate_libraries) {
+ assert(strrchr(*lib, '/'));
+ assert(strlen(arg_directory) + strlen("/usr/lib") + strlen(strrchr(*lib, '/')) + 1 <= PATH_MAX);
+
+ memcpy(t + strlen(arg_directory), "/usr/lib", strlen("/usr/lib") + 1);
+ strcat(t, strrchr(*lib, '/'));
+
+ (void)umount(t);
+
+ if (stat(t, &st) == 0 && st.st_atime == BIND_FILE_MAGIC_TIME)
+ (void)unlink(t);
+ }
+}
+
int main(int argc, char *argv[]) {
pid_t pid = 0;
int r = EXIT_FAILURE, k;
@@ -1179,21 +1302,69 @@ int main(int argc, char *argv[]) {
goto finish;
}
- if (arg_populate && argc > optind && find_binary_path_root(argv[optind], DEFAULT_PATH_SPLIT_USR, arg_directory, NULL) == -ENOENT) {
- _cleanup_free_ char *t = NULL, *linker = NULL, *shebang = NULL;
+ if (arg_populate) {
+ _cleanup_free_ char *t = NULL, *linker = NULL, *libroot = NULL;
bool elf64;
+ char **lib;
k = find_binary(argv[optind], &t);
if (k < 0) {
- log_error("COMMAND %s not available in host or target: %s", argv[optind], strerror(-r));
- goto finish;
+ if (argc <= optind) {
+ t = strdup("/bin/bash");
+ if (!t) {
+ k = log_oom();
+ goto finish;
+ }
+ } else {
+ log_error("COMMAND %s not available in host: %s", argv[optind], strerror(-k));
+ goto finish;
+ }
}
- k = analyze_executable(t, &exec_fd, &elf64, &linker, &shebang);
+ if (find_binary_path_root(argv[optind], DEFAULT_PATH_SPLIT_USR, arg_directory, NULL) != -ENOENT)
+ log_warning("%s already exists in %s.", argv[optind], arg_directory);
+
+ k = analyze_executable(t, &exec_fd, &elf64, &populate_linker, &populate_shebang, &populate_libraries);
if (k < 0) {
- printf("analyze failed: %m");/* nothing */
- } else
- asprintf(&proc_exec, "/proc/self/fd/%d", exec_fd);
+ log_error("--populate failed: %s", strerror(-k));
+ goto finish;
+ }
+
+ asprintf(&proc_exec, "/proc/self/fd/%d", exec_fd);
+ if (populate_shebang)
+ if ((k = mount_file_bind_mkdir(populate_shebang)) < 0)
+ goto finish;
+
+ if (populate_linker)
+ if ((k = mount_file_bind_mkdir(populate_linker)) < 0)
+ goto finish;
+
+ if (populate_libraries) {
+ libroot = strappend(arg_directory, "/usr/lib/");
+ if (!libroot) {
+ log_oom();
+ goto finish;
+ }
+
+ k = mkdir_p(libroot, 0755);
+ if (k < 0) {
+ log_error("Failed to create directory %s: %s", libroot, strerror(-k));
+ goto finish;
+ }
+ }
+
+ STRV_FOREACH(lib, populate_libraries) {
+ if (!strrchr(*lib, '/')) {
+ log_error("Failed to parse linker output.");
+ goto finish;
+ }
+
+ k = mount_file_bind(*lib, strrchr(*lib, '/') + 1, libroot);
+ if (k < 0) {
+ log_error("Failed to bind mount %s: %s", *lib, strerror(-k));
+ goto finish;
+ }
+ }
}
sd_notify(0, "READY=1");
@@ -1519,6 +1690,8 @@ int main(int argc, char *argv[]) {
break;
}
+ umount_populate();
+
if (status.si_code == CLD_EXITED) {
r = status.si_status;
if (status.si_status != 0) {
diff --git a/src/shared/util.c b/src/shared/util.c
index 305a6c2..f2acb76 100644
--- a/src/shared/util.c
+++ b/src/shared/util.c
@@ -77,6 +77,7 @@
#include "gunicode.h"
#include "virt.h"
#include "def.h"
+#include "mkdir.h"
int saved_argc = 0;
char **saved_argv = NULL;
@@ -3338,6 +3339,24 @@ char *ellipsize(const char *s, size_t length, unsigned percent) {
return ellipsize_mem(s, strlen(s), length, percent);
}
+int touch_time_create_executable(const char *path, time_t t) {
+ const struct timespec times[2] = { {t, 0}, {t, 0} };
+ int fd, r;
+
+ assert(path);
+
+ fd = open(path, O_WRONLY|O_CREAT|O_EXCL|O_CLOEXEC|O_NOCTTY, 0755);
+ if (fd < 0)
+ return -errno;
+
+ r = futimens(fd, times);
+ if (r < 0)
+ return r;
+
+ close_nointr_nofail(fd);
+ return 0;
+}
+
int touch(const char *path) {
int fd;
@@ -4966,6 +4985,67 @@ int fd_inc_rcvbuf(int fd, size_t n) {
return 1;
}
+/* popen() is insecure due to shell, reimplement w/o shell
+ * be careful about deadlocks where process is waiting for us to read()
+ * when we are blocked on waitpid() (do not use in PID 1) */
+#define POPENV_READ_SIZE 4096
+char *popenve(const char *path, char *const argv[], char *const envp[]) {
+ int fd[2];
+ pid_t child_pid;
+ int r;
+
+ char *s = NULL, *i;
+ int j;
+
+ r = pipe2(fd, O_CLOEXEC);
+ if(r < 0)
+ return NULL;
+
+ child_pid = fork();
+ if (child_pid == 0) {
+ int d;
+ /* child */
+ if (fd[1] != STDOUT_FILENO)
+ dup2(fd[1], STDOUT_FILENO);
+
+ d = open("/dev/null", O_APPEND | O_CLOEXEC);
+ if (d < 0)
+ _exit(1);
+
+ dup2(d, STDERR_FILENO);
+ fcntl(STDERR_FILENO, F_SETFD, 0);
+ fcntl(STDOUT_FILENO, F_SETFD, 0);
+
+ execve(path, argv, envp);
+ /* execve() failed */
+ _exit(127);
+ } else if (child_pid == -1) {
+ /* fork() failed */
+ close(fd[1]);
+ close(fd[0]);
+ return NULL;
+ }
+ close(fd[1]);
+
+ waitpid(child_pid, NULL, 0);
+ for (j=0;;j++) {
+ s = realloc(s, (j+1)*POPENV_READ_SIZE);
+ i = j*POPENV_READ_SIZE + s;
+ r = read(fd[0], i, POPENV_READ_SIZE);
+ if (r < 0) {
+ free(s);
+ kill(child_pid, SIGKILL);
+ close(fd[0]);
+ return NULL;
+ } if (r < POPENV_READ_SIZE) { // read to end
+ close(fd[0]);
+ /* read() does not NULL terminate */
+ i[r] = '\0';
+ return s;
+ }
+ }
+}
+
int fork_agent(pid_t *pid, const int except[], unsigned n_except, const char *path, ...) {
pid_t parent_pid, agent_pid;
int fd;
diff --git a/src/shared/util.h b/src/shared/util.h
index 1662bd4..392e47a 100644
--- a/src/shared/util.h
+++ b/src/shared/util.h
@@ -409,6 +409,7 @@ char *ellipsize(const char *s, size_t length, unsigned percent);
/* bytes columns */
char *ellipsize_mem(const char *s, size_t old_length, size_t new_length, unsigned percent);
+int touch_time_create_executable(const char *path, time_t t);
int touch(const char *path);
char *unquote(const char *s, const char *quotes);
@@ -539,6 +540,7 @@ int is_kernel_thread(pid_t pid);
int fd_inc_sndbuf(int fd, size_t n);
int fd_inc_rcvbuf(int fd, size_t n);
+char *popenve(const char *path, char *const argv[], char *const envp[]);
int fork_agent(pid_t *pid, const int except[], unsigned n_except, const char *path, ...);
int setrlimit_closest(int resource, const struct rlimit *rlim);
--
1.8.4.4
More information about the systemd-devel
mailing list