| /* -*- c-set-style: "K&R"; c-basic-offset: 8 -*- |
| * |
| * This file is part of PRoot. |
| * |
| * Copyright (C) 2014 STMicroelectronics |
| * |
| * This program is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU General Public License as |
| * published by the Free Software Foundation; either version 2 of the |
| * License, or (at your option) any later version. |
| * |
| * This program 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 |
| * General Public License for more details. |
| * |
| * You should have received a copy of the GNU General Public License |
| * along with this program; if not, write to the Free Software |
| * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
| * 02110-1301 USA. |
| */ |
| |
| #include <string.h> /* string(3), */ |
| #include <stdarg.h> /* va_*(3), */ |
| #include <assert.h> /* assert(3), */ |
| #include <fcntl.h> /* AT_*, */ |
| #include <unistd.h> /* readlink*(2), *stat(2), getpid(2), */ |
| #include <sys/types.h> /* pid_t, */ |
| #include <sys/stat.h> /* S_ISDIR, */ |
| #include <dirent.h> /* opendir(3), readdir(3), */ |
| #include <stdio.h> /* snprintf(3), */ |
| #include <errno.h> /* E*, */ |
| #include <stddef.h> /* ptrdiff_t, */ |
| |
| #include "path/path.h" |
| #include "path/binding.h" |
| #include "path/canon.h" |
| #include "path/proc.h" |
| #include "extension/extension.h" |
| #include "cli/note.h" |
| #include "build.h" |
| |
| #include "compat.h" |
| |
| /** |
| * Copy in @result the concatenation of several paths (@number_paths) |
| * and adds a path separator ('/') in between when needed. This |
| * function returns -errno if an error occured, otherwise it returns 0. |
| */ |
| int join_paths(int number_paths, char result[PATH_MAX], ...) |
| { |
| va_list paths; |
| size_t length; |
| int status; |
| int i; |
| |
| result[0] = '\0'; |
| length = 0; |
| status = 0; |
| |
| /* Parse the list of variadic arguments. */ |
| va_start(paths, result); |
| for (i = 0; i < number_paths; i++) { |
| const char *path; |
| size_t path_length; |
| size_t new_length; |
| |
| path = va_arg(paths, const char *); |
| if (path == NULL) |
| continue; |
| path_length = strlen(path); |
| |
| /* A new path separator is needed. */ |
| if (length > 0 && result[length - 1] != '/' && path[0] != '/') { |
| new_length = length + path_length + 1; |
| if (new_length + 1 >= PATH_MAX) { |
| status = -ENAMETOOLONG; |
| break; |
| } |
| strcat(result + length, "/"); |
| strcat(result + length, path); |
| length = new_length; |
| } |
| /* There are already two path separators. */ |
| else if (length > 0 && result[length - 1] == '/' && path[0] == '/') { |
| new_length = length + path_length - 1; |
| if (new_length + 1 >= PATH_MAX) { |
| status = -ENAMETOOLONG; |
| break; |
| } |
| strcat(result + length, path + 1); |
| length += path_length - 1; |
| } |
| /* There's already one path separator or result[] is empty. */ |
| else { |
| new_length = length + path_length; |
| if (new_length + 1 >= PATH_MAX) { |
| status = -ENAMETOOLONG; |
| break; |
| } |
| strcat(result + length, path); |
| length += path_length; |
| } |
| |
| status = 0; |
| } |
| va_end(paths); |
| |
| return status; |
| } |
| |
| /** |
| * Put in @host_path the full path to the given shell @command. The |
| * @command is searched in @paths if not null, otherwise in $PATH |
| * (relatively to the @tracee's file-system name-space). This |
| * function always returns -1 on error, otherwise 0. |
| */ |
| int which(Tracee *tracee, const char *paths, char host_path[PATH_MAX], const char *command) |
| { |
| char path[PATH_MAX]; |
| const char *cursor; |
| struct stat statr; |
| int status; |
| |
| bool is_explicit; |
| bool found; |
| |
| assert(command != NULL); |
| is_explicit = (strchr(command, '/') != NULL); |
| |
| /* Is the command available without any $PATH look-up? */ |
| status = realpath2(tracee, host_path, command, true); |
| if (status == 0 && stat(host_path, &statr) == 0) { |
| if (is_explicit && !S_ISREG(statr.st_mode)) { |
| note(tracee, ERROR, USER, "'%s' is not a regular file", command); |
| return -EACCES; |
| } |
| |
| if (is_explicit && (statr.st_mode & S_IXUSR) == 0) { |
| note(tracee, ERROR, USER, "'%s' is not executable", command); |
| return -EACCES; |
| } |
| |
| found = true; |
| |
| /* Don't dereference the final component to preserve |
| * argv0 in case it is a symlink to script. */ |
| (void) realpath2(tracee, host_path, command, false); |
| } |
| else |
| found = false; |
| |
| /* Is the the explicit command was found? */ |
| if (is_explicit) { |
| if (found) |
| return 0; |
| else |
| goto not_found; |
| } |
| |
| /* Otherwise search the command in $PATH. */ |
| paths = paths ?: getenv("PATH"); |
| if (paths == NULL || strcmp(paths, "") == 0) |
| goto not_found; |
| |
| cursor = paths; |
| do { |
| size_t length; |
| |
| length = strcspn(cursor, ":"); |
| cursor += length + 1; |
| |
| if (length >= PATH_MAX) |
| continue; |
| else if (length == 0) |
| strcpy(path, "."); |
| else { |
| strncpy(path, cursor - length - 1, length); |
| path[length] = '\0'; |
| } |
| |
| /* Avoid buffer-overflow. */ |
| if (length + strlen(command) + 2 >= PATH_MAX) |
| continue; |
| |
| strcat(path, "/"); |
| strcat(path, command); |
| |
| status = realpath2(tracee, host_path, path, true); |
| if (status == 0 |
| && stat(host_path, &statr) == 0 |
| && S_ISREG(statr.st_mode) |
| && (statr.st_mode & S_IXUSR) != 0) { |
| /* Don't dereference the final component to preserve |
| * argv0 in case it is a symlink to script. */ |
| (void) realpath2(tracee, host_path, path, false); |
| return 0; |
| } |
| } while (*(cursor - 1) != '\0'); |
| |
| not_found: |
| status = getcwd2(tracee, path); |
| if (status < 0) |
| strcpy(path, "<unknown>"); |
| |
| note(tracee, ERROR, USER, "'%s' not found (root = %s, cwd = %s, $PATH=%s)", |
| command, get_root(tracee), path, paths); |
| |
| /* Check if the command was found without any $PATH look-up |
| * but it didn't contain "/". */ |
| if (found && !is_explicit) |
| note(tracee, ERROR, USER, |
| "to execute a local program, use the './' prefix, for example: ./%s", command); |
| |
| return -1; |
| } |
| |
| /** |
| * Put in @host_path the canonicalized form of @path. In the nominal |
| * case (@tracee == NULL), this function is barely equivalent to |
| * realpath(), but when doing sub-reconfiguration, the path is |
| * canonicalized relatively to the current @tracee's file-system |
| * name-space. This function returns -errno on error, otherwise 0. |
| */ |
| int realpath2(Tracee *tracee, char host_path[PATH_MAX], const char *path, bool deref_final) |
| { |
| int status; |
| |
| if (tracee == NULL) |
| status = (realpath(path, host_path) == NULL ? -errno : 0); |
| else |
| status = translate_path(tracee, host_path, AT_FDCWD, path, deref_final); |
| return status; |
| } |
| |
| /** |
| * Put in @guest_path the canonicalized current working directory. In |
| * the nominal case (@tracee == NULL), this function is barely |
| * equivalent to realpath(), but when doing sub-reconfiguration, the |
| * path is canonicalized relatively to the current @tracee's |
| * file-system name-space. This function returns -errno on error, |
| * otherwise 0. |
| */ |
| int getcwd2(Tracee *tracee, char guest_path[PATH_MAX]) |
| { |
| if (tracee == NULL) { |
| if (getcwd(guest_path, PATH_MAX) == NULL) |
| return -errno; |
| } |
| else { |
| if (strlen(tracee->fs->cwd) >= PATH_MAX) |
| return -ENAMETOOLONG; |
| |
| strcpy(guest_path, tracee->fs->cwd); |
| } |
| |
| return 0; |
| } |
| |
| /** |
| * Remove the trailing "/" or "/.". |
| */ |
| void chop_finality(char *path) |
| { |
| size_t length = strlen(path); |
| |
| if (path[length - 1] == '.') { |
| assert(length >= 2); |
| /* Special case for "/." */ |
| if (length == 2) |
| path[length - 1] = '\0'; |
| else |
| path[length - 2] = '\0'; |
| } |
| else if (path[length - 1] == '/') { |
| /* Special case for "/" */ |
| if (length > 1) |
| path[length - 1] = '\0'; |
| } |
| } |
| |
| /** |
| * Put in @path the result of readlink(/proc/@pid/fd/@fd). This |
| * function returns -errno if an error occured, othrwise 0. |
| */ |
| int readlink_proc_pid_fd(pid_t pid, int fd, char path[PATH_MAX]) |
| { |
| char link[32]; /* 32 > sizeof("/proc//cwd") + sizeof(#ULONG_MAX) */ |
| int status; |
| |
| /* Format the path to the "virtual" link. */ |
| status = snprintf(link, sizeof(link), "/proc/%d/fd/%d", pid, fd); |
| if (status < 0) |
| return -EBADF; |
| if ((size_t) status >= sizeof(link)) |
| return -EBADF; |
| |
| /* Read the value of this "virtual" link. */ |
| status = readlink(link, path, PATH_MAX); |
| if (status < 0) |
| return -EBADF; |
| if (status >= PATH_MAX) |
| return -ENAMETOOLONG; |
| path[status] = '\0'; |
| |
| return 0; |
| } |
| |
| /** |
| * Copy in @result the equivalent of "@tracee->root + canon(@dir_fd + |
| * @user_path)". If @user_path is not absolute then it is relative to |
| * the directory referred by the descriptor @dir_fd (AT_FDCWD is for |
| * the current working directory). See the documentation of |
| * canonicalize() for the meaning of @deref_final. This function |
| * returns -errno if an error occured, otherwise 0. |
| */ |
| int translate_path(Tracee *tracee, char result[PATH_MAX], int dir_fd, |
| const char *user_path, bool deref_final) |
| { |
| char guest_path[PATH_MAX]; |
| int status; |
| |
| /* Use "/" as the base if it is an absolute guest path. */ |
| if (user_path[0] == '/') { |
| strcpy(result, "/"); |
| } |
| /* It is relative to a directory referred by a descriptor, see |
| * openat(2) for details. */ |
| else if (dir_fd != AT_FDCWD) { |
| /* /proc/@tracee->pid/fd/@dir_fd -> result. */ |
| status = readlink_proc_pid_fd(tracee->pid, dir_fd, result); |
| if (status < 0) |
| return status; |
| |
| /* Named file descriptors may reference special |
| * objects like pipes, sockets, inodes, ... Such |
| * objects do not belong to the file-system. */ |
| if (result[0] != '/') |
| return -ENOTDIR; |
| |
| /* Remove the leading "root" part of the base |
| * (required!). */ |
| status = detranslate_path(tracee, result, NULL); |
| if (status < 0) |
| return status; |
| } |
| /* It is relative to the current working directory. */ |
| else { |
| status = getcwd2(tracee, result); |
| if (status < 0) |
| return status; |
| } |
| |
| VERBOSE(tracee, 2, "pid %d: translate(\"%s\" + \"%s\")", |
| tracee != NULL ? tracee->pid : 0, result, user_path); |
| |
| status = notify_extensions(tracee, GUEST_PATH, (intptr_t) result, (intptr_t) user_path); |
| if (status < 0) |
| return status; |
| if (status > 0) |
| goto skip; |
| |
| /* So far "result" was used as a base path, it's time to join |
| * it to the user path. */ |
| assert(result[0] == '/'); |
| status = join_paths(2, guest_path, result, user_path); |
| if (status < 0) |
| return status; |
| strcpy(result, "/"); |
| |
| /* Canonicalize regarding the new root. */ |
| status = canonicalize(tracee, guest_path, deref_final, result, 0); |
| if (status < 0) |
| return status; |
| |
| /* Final binding substitution to convert "result" into a host |
| * path, since canonicalize() works from the guest |
| * point-of-view. */ |
| status = substitute_binding(tracee, GUEST, result); |
| if (status < 0) |
| return status; |
| |
| skip: |
| VERBOSE(tracee, 2, "pid %d: -> \"%s\"", |
| tracee != NULL ? tracee->pid : 0, result); |
| return 0; |
| } |
| |
| /** |
| * Remove/substitute the leading part of a "translated" @path. It |
| * returns 0 if no transformation is required (ie. symmetric binding), |
| * otherwise it returns the size in bytes of the updated @path, |
| * including the end-of-string terminator. On error it returns |
| * -errno. |
| */ |
| int detranslate_path(Tracee *tracee, char path[PATH_MAX], const char t_referrer[PATH_MAX]) |
| { |
| size_t prefix_length; |
| ssize_t new_length; |
| |
| bool sanity_check; |
| bool follow_binding; |
| |
| /* Sanity check. */ |
| if (strnlen(path, PATH_MAX) >= PATH_MAX) |
| return -ENAMETOOLONG; |
| |
| /* Don't try to detranslate relative paths (typically the |
| * target of a relative symbolic link). */ |
| if (path[0] != '/') |
| return 0; |
| |
| /* Is it a symlink? */ |
| if (t_referrer != NULL) { |
| Comparison comparison; |
| |
| sanity_check = false; |
| follow_binding = false; |
| |
| /* In some cases bindings have to be resolved. */ |
| comparison = compare_paths("/proc", t_referrer); |
| if (comparison == PATH1_IS_PREFIX) { |
| /* Some links in "/proc" are generated |
| * dynamically by the kernel. PRoot has to |
| * emulate some of them. */ |
| char proc_path[PATH_MAX]; |
| strcpy(proc_path, path); |
| new_length = readlink_proc2(tracee, proc_path, t_referrer); |
| if (new_length < 0) |
| return new_length; |
| if (new_length != 0) { |
| strcpy(path, proc_path); |
| return new_length + 1; |
| } |
| |
| /* Always resolve bindings for symlinks in |
| * "/proc", they always point to the emulated |
| * file-system namespace by design. */ |
| follow_binding = true; |
| } |
| else if (!belongs_to_guestfs(tracee, t_referrer)) { |
| const char *binding_referree; |
| const char *binding_referrer; |
| |
| binding_referree = get_path_binding(tracee, HOST, path); |
| binding_referrer = get_path_binding(tracee, HOST, t_referrer); |
| assert(binding_referrer != NULL); |
| |
| /* Resolve bindings for symlinks that belong |
| * to a binding and point to the same binding. |
| * For example, if "-b /lib:/foo" is specified |
| * and the symlink "/lib/a -> /lib/b" exists |
| * in the host rootfs namespace, then it |
| * should appear as "/foo/a -> /foo/b" in the |
| * guest rootfs namespace for consistency |
| * reasons. */ |
| if (binding_referree != NULL) { |
| comparison = compare_paths(binding_referree, binding_referrer); |
| follow_binding = (comparison == PATHS_ARE_EQUAL); |
| } |
| } |
| } |
| else { |
| sanity_check = true; |
| follow_binding = true; |
| } |
| |
| if (follow_binding) { |
| switch (substitute_binding(tracee, HOST, path)) { |
| case 0: |
| return 0; |
| case 1: |
| return strlen(path) + 1; |
| default: |
| break; |
| } |
| } |
| |
| switch (compare_paths(get_root(tracee), path)) { |
| case PATH1_IS_PREFIX: |
| /* Remove the leading part, that is, the "root". */ |
| prefix_length = strlen(get_root(tracee)); |
| |
| /* Special case when path to the guest rootfs == "/". */ |
| if (prefix_length == 1) |
| prefix_length = 0; |
| |
| new_length = strlen(path) - prefix_length; |
| memmove(path, path + prefix_length, new_length); |
| |
| path[new_length] = '\0'; |
| break; |
| |
| case PATHS_ARE_EQUAL: |
| /* Special case when path == root. */ |
| new_length = 1; |
| strcpy(path, "/"); |
| break; |
| |
| default: |
| /* Ensure the path is within the new root. */ |
| if (sanity_check) |
| return -EPERM; |
| else |
| return 0; |
| } |
| |
| return new_length + 1; |
| } |
| |
| /** |
| * Check if the translated @host_path belongs to the guest rootfs, |
| * that is, isn't from a binding. |
| */ |
| bool belongs_to_guestfs(const Tracee *tracee, const char *host_path) |
| { |
| Comparison comparison; |
| |
| comparison = compare_paths(get_root(tracee), host_path); |
| return (comparison == PATHS_ARE_EQUAL || comparison == PATH1_IS_PREFIX); |
| } |
| |
| /** |
| * Compare @path1 with @path2, which are respectively @length1 and |
| * @length2 long. |
| * |
| * This function works only with paths canonicalized in the same |
| * namespace (host/guest)! |
| */ |
| Comparison compare_paths2(const char *path1, size_t length1, const char *path2, size_t length2) |
| { |
| size_t length_min; |
| bool is_prefix; |
| char sentinel; |
| |
| #if defined DEBUG_OPATH |
| assert(length(path1) == length1); |
| assert(length(path2) == length2); |
| #endif |
| assert(length1 > 0); |
| assert(length2 > 0); |
| |
| if (!length1 || !length2) { |
| return PATHS_ARE_NOT_COMPARABLE; |
| } |
| |
| /* Remove potential trailing '/' for the comparison. */ |
| if (path1[length1 - 1] == '/') |
| length1--; |
| |
| if (path2[length2 - 1] == '/') |
| length2--; |
| |
| if (length1 < length2) { |
| length_min = length1; |
| sentinel = path2[length_min]; |
| } |
| else { |
| length_min = length2; |
| sentinel = path1[length_min]; |
| } |
| |
| /* Optimize obvious cases. */ |
| if (sentinel != '/' && sentinel != '\0') |
| return PATHS_ARE_NOT_COMPARABLE; |
| |
| is_prefix = (strncmp(path1, path2, length_min) == 0); |
| |
| if (!is_prefix) |
| return PATHS_ARE_NOT_COMPARABLE; |
| |
| if (length1 == length2) |
| return PATHS_ARE_EQUAL; |
| else if (length1 < length2) |
| return PATH1_IS_PREFIX; |
| else if (length1 > length2) |
| return PATH2_IS_PREFIX; |
| |
| assert(0); |
| return PATHS_ARE_NOT_COMPARABLE; |
| } |
| |
| Comparison compare_paths(const char *path1, const char *path2) |
| { |
| return compare_paths2(path1, strlen(path1), path2, strlen(path2)); |
| } |
| |
| typedef int (*foreach_fd_t)(const Tracee *tracee, int fd, char path[PATH_MAX]); |
| |
| /** |
| * Call @callback on each open file descriptors of @pid. It returns |
| * the status of the first failure, that is, if @callback returned |
| * seomthing lesser than 0, otherwise 0. |
| */ |
| static int foreach_fd(const Tracee *tracee, foreach_fd_t callback) |
| { |
| struct dirent *dirent; |
| char path[PATH_MAX]; |
| char proc_fd[32]; /* 32 > sizeof("/proc//fd") + sizeof(#ULONG_MAX) */ |
| int status; |
| DIR *dirp; |
| |
| /* Format the path to the "virtual" directory. */ |
| status = snprintf(proc_fd, sizeof(proc_fd), "/proc/%d/fd", tracee->pid); |
| if (status < 0 || (size_t) status >= sizeof(proc_fd)) |
| return 0; |
| |
| /* Open the virtual directory "/proc/$pid/fd". */ |
| dirp = opendir(proc_fd); |
| if (dirp == NULL) |
| return 0; |
| |
| while ((dirent = readdir(dirp)) != NULL) { |
| /* Read the value of this "virtual" link. Don't use |
| * readlinkat(2) here since it would require Linux >= |
| * 2.6.16 and Glibc >= 2.4, whereas PRoot is supposed |
| * to work on any Linux 2.6 systems. */ |
| |
| char tmp[PATH_MAX]; |
| if (strlen(proc_fd) + strlen(dirent->d_name) + 1 >= PATH_MAX) |
| continue; |
| |
| strcpy(tmp, proc_fd); |
| strcat(tmp, "/"); |
| strcat(tmp, dirent->d_name); |
| |
| status = readlink(tmp, path, PATH_MAX); |
| if (status < 0 || status >= PATH_MAX) |
| continue; |
| path[status] = '\0'; |
| |
| /* Ensure it points to a path (not a socket or somethink like that). */ |
| if (path[0] != '/') |
| continue; |
| |
| status = callback(tracee, atoi(dirent->d_name), path); |
| if (status < 0) |
| goto end; |
| } |
| status = 0; |
| |
| end: |
| closedir(dirp); |
| return status; |
| } |
| |
| /** |
| * Helper for list_open_fd(). |
| */ |
| static int list_open_fd_callback(const Tracee *tracee, int fd, char path[PATH_MAX]) |
| { |
| VERBOSE(tracee, 1, "pid %d: access to \"%s\" (fd %d) won't be translated until closed", |
| tracee->pid, path, fd); |
| return 0; |
| } |
| |
| /** |
| * Warn for files that are open. It is useful right after PRoot has |
| * attached a process. |
| */ |
| int list_open_fd(const Tracee *tracee) |
| { |
| return foreach_fd(tracee, list_open_fd_callback); |
| } |
| |
| /** |
| * Substitute the first @old_prefix_length bytes of @path with |
| * @new_prefix (the caller has to provides a correct |
| * @new_prefix_length). This function returns the new length of |
| * @path. Note: this function takes care about special cases (like |
| * "/"). |
| */ |
| size_t substitute_path_prefix(char path[PATH_MAX], size_t old_prefix_length, |
| const char *new_prefix, size_t new_prefix_length) |
| { |
| size_t path_length; |
| size_t new_length; |
| |
| path_length = strlen(path); |
| |
| assert(old_prefix_length < PATH_MAX); |
| assert(new_prefix_length < PATH_MAX); |
| |
| if (new_prefix_length == 1) { |
| /* Special case: "/foo" -> "/". Substitute "/foo/bin" |
| * with "/bin" not "//bin". */ |
| |
| new_length = path_length - old_prefix_length; |
| if (new_length != 0) |
| memmove(path, path + old_prefix_length, new_length); |
| else { |
| /* Special case: "/". */ |
| path[0] = '/'; |
| new_length = 1; |
| } |
| } |
| else if (old_prefix_length == 1) { |
| /* Special case: "/" -> "/foo". Substitute "/bin" with |
| * "/foo/bin" not "/foobin". */ |
| |
| new_length = new_prefix_length + path_length; |
| if (new_length >= PATH_MAX) |
| return -ENAMETOOLONG; |
| |
| if (path_length > 1) { |
| memmove(path + new_prefix_length, path, path_length); |
| memcpy(path, new_prefix, new_prefix_length); |
| } |
| else { |
| /* Special case: "/". */ |
| memcpy(path, new_prefix, new_prefix_length); |
| new_length = new_prefix_length; |
| } |
| } |
| else { |
| /* Generic case. */ |
| |
| new_length = path_length - old_prefix_length + new_prefix_length; |
| if (new_length >= PATH_MAX) |
| return -ENAMETOOLONG; |
| |
| memmove(path + new_prefix_length, |
| path + old_prefix_length, |
| path_length - old_prefix_length); |
| memcpy(path, new_prefix, new_prefix_length); |
| } |
| |
| assert(new_length < PATH_MAX); |
| path[new_length] = '\0'; |
| |
| return new_length; |
| } |