the whitelist of dynamic linker paths comes from clang --- src/nspawn/elf.c | 174 ++++++++++++++++++++++++++++++++++++++++++----- src/nspawn/elf.h | 14 +++- src/nspawn/nspawn.c | 191 +++++++++++++++++++++++++++++++++++++++++++++++++--- src/shared/util.c | 80 ++++++++++++++++++++++ src/shared/util.h | 2 + 5 files changed, 435 insertions(+), 26 deletions(-)
diff --git a/src/nspawn/elf.c b/src/nspawn/elf.c index c399d59..2f0b7ef 100644 --- a/src/nspawn/elf.c +++ b/src/nspawn/elf.c @@ -26,23 +26,30 @@ #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) { @@ -50,18 +57,59 @@ int analyze_executable(const char *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() on %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 (shebang) - *shebang = NULL; + 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 = (char *)&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 read interpreter %s: %m", t); + return -errno; + } + + r = read(shebang_fd, shebang_e_ident, sizeof(Elf64_Ehdr)); + if (r < SELFMAG) { + log_error("read() on %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: @@ -79,10 +127,16 @@ int analyze_executable(const char *path, Elf64_Ehdr *ehdr = (Elf64_Ehdr *)&e_ident; e_type = ehdr->e_type; + e_phoff = ehdr->e_phoff; + e_phentsize = ehdr->e_phentsize; + e_phnum = ehdr->e_phnum; } else { Elf32_Ehdr *ehdr = (Elf32_Ehdr *)&e_ident; e_type = ehdr->e_type; + e_phoff = ehdr->e_phoff; + e_phentsize = ehdr->e_phentsize; + e_phnum = ehdr->e_phnum; } /* Not checking e_ident[E_DATA], file is assumed to be of host endianness. */ @@ -113,15 +167,103 @@ int analyze_executable(const char *path, } if (!have_interp) { - if (_elf64) - *_elf64 = elf64; - if (_linker) - *_linker = NULL; - if (!have_shebang) + *_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 5082d72..55b9c47 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -113,6 +113,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" @@ -193,7 +199,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"); @@ -1037,6 +1042,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; @@ -1152,21 +1275,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; + + 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 = 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); + 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"); @@ -1489,6 +1660,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 3a4d196..4def0ed 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; @@ -3327,6 +3328,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; @@ -4955,6 +4974,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 e46438c..d536316 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 _______________________________________________ systemd-devel mailing list systemd-devel@lists.freedesktop.org http://lists.freedesktop.org/mailman/listinfo/systemd-devel