/* * libsandbox.c * * Main libsandbox related functions. * * Copyright 1999-2008 Gentoo Foundation * Licensed under the GPL-2 * * Partly Copyright (C) 1998-9 Pancrazio `Ezio' de Mauro , * as some of the InstallWatch code was used. */ #include "headers.h" #include "sbutil.h" #include "libsandbox.h" #include "wrappers.h" #include "sb_nr.h" #define LOG_VERSION "1.0" #define LOG_STRING "VERSION " LOG_VERSION "\n" #define LOG_FMT_FUNC "FORMAT: F - Function called\n" #define LOG_FMT_ACCESS "FORMAT: S - Access Status\n" #define LOG_FMT_PATH "FORMAT: P - Path as passed to function\n" #define LOG_FMT_APATH "FORMAT: A - Absolute Path (not canonical)\n" #define LOG_FMT_RPATH "FORMAT: R - Canonical Path\n" #define LOG_FMT_CMDLINE "FORMAT: C - Command Line\n" char sandbox_lib[SB_PATH_MAX]; typedef struct { bool show_access_violation, on, active, testing, verbose, debug; sandbox_method_t method; char *ld_library_path; char **prefixes[5]; int num_prefixes[5]; #define deny_prefixes prefixes[0] #define num_deny_prefixes num_prefixes[0] #define read_prefixes prefixes[1] #define num_read_prefixes num_prefixes[1] #define write_prefixes prefixes[2] #define num_write_prefixes num_prefixes[2] #define predict_prefixes prefixes[3] #define num_predict_prefixes num_prefixes[3] #define write_denied_prefixes prefixes[4] #define num_write_denied_prefixes num_prefixes[4] #define MAX_DYN_PREFIXES 4 /* the first 4 are dynamic */ } sbcontext_t; static sbcontext_t sbcontext; static char *cached_env_vars[MAX_DYN_PREFIXES]; static char log_path[SB_PATH_MAX]; static char debug_log_path[SB_PATH_MAX]; static char message_path[SB_PATH_MAX]; bool sandbox_on = true; static bool sb_init = false; static bool sb_env_init = false; int (*sbio_open)(const char *, int, mode_t) = sb_unwrapped_open; FILE *(*sbio_popen)(const char *, const char *) = sb_unwrapped_popen; static char *resolve_path(const char *, int); static int check_prefixes(char **, int, const char *); static void clean_env_entries(char ***, int *); static void sb_process_env_settings(void); const char *sbio_message_path; const char sbio_fallback_path[] = "/dev/tty"; /* We need to initialize these vars before main(). This is to handle programs * (like `env`) that will clear the environment before making any syscalls * other than execve(). At that point, trying to get the settings is too late. * However, we might still need to init the env vars in the syscall wrapper for * programs that have their own constructors. #404013 */ __attribute__((constructor)) void libsb_init(void) { if (sb_env_init) /* Ah, we already saw a syscall */ return; sb_env_init = true; /* Get the path and name to this library */ get_sandbox_lib(sandbox_lib); get_sandbox_log(log_path, NULL); get_sandbox_debug_log(debug_log_path, NULL); get_sandbox_message_path(message_path); sbio_message_path = message_path; memset(&sbcontext, 0x00, sizeof(sbcontext)); sbcontext.show_access_violation = true; sb_process_env_settings(); is_sandbox_on(); sbcontext.verbose = is_env_on(ENV_SANDBOX_VERBOSE); sbcontext.debug = is_env_on(ENV_SANDBOX_DEBUG); sbcontext.testing = is_env_on(ENV_SANDBOX_TESTING); sbcontext.method = get_sandbox_method(); if (sbcontext.testing) { const char *ldpath = getenv("LD_LIBRARY_PATH"); if (ldpath) sbcontext.ld_library_path = xstrdup(ldpath); } } sandbox_method_t get_sandbox_method(void) { return parse_sandbox_method(getenv(ENV_SANDBOX_METHOD)); } /* resolve_dirfd_path - get the path relative to a dirfd * * return value: * -1 - error! * 0 - path is in @resolved_path * 1 - path is in @path (no resolution necessary) * 2 - errno issues -- ignore this path */ int resolve_dirfd_path(int dirfd, const char *path, char *resolved_path, size_t resolved_path_len) { /* The *at style functions have the following semantics: * - dirfd = AT_FDCWD: same as non-at func: file is based on CWD * - file is absolute: dirfd is ignored * - otherwise: file is relative to dirfd * Since maintaining fd state based on open's is real messy, we'll * just rely on the kernel doing it for us with /proc//fd/ ... */ if (dirfd == AT_FDCWD || (path && path[0] == '/')) return 1; save_errno(); size_t at_len = resolved_path_len - 1 - 1 - (path ? strlen(path) : 0); if (trace_pid) { sprintf(resolved_path, "/proc/%i/fd/%i", trace_pid, dirfd); } else { /* If /proc was mounted by a process in a different pid namespace, * getpid cannot be used to create a valid /proc/ path. Instead * use sb_get_fd_dir() which works in any case. */ sprintf(resolved_path, "%s/%i", sb_get_fd_dir(), dirfd); } ssize_t ret = readlink(resolved_path, resolved_path, at_len); if (ret == -1) { /* see comments at end of check_syscall() */ if (errno_is_too_long()) { restore_errno(); return 2; } sb_debug_dyn("AT_FD LOOKUP fail: %s: %s\n", resolved_path, strerror(errno)); /* If the fd isn't found, some guys (glibc) expect errno */ if (errno == ENOENT) errno = EBADF; return -1; } resolved_path[ret] = '/'; resolved_path[ret + 1] = '\0'; if (path) strcat(resolved_path, path); restore_errno(); return 0; } int canonicalize(const char *path, char *resolved_path) { int old_errno = errno; char *retval; *resolved_path = '\0'; /* If path == NULL, return or we get a segfault */ if (NULL == path) { errno = EINVAL; return -1; } /* Do not try to resolve an empty path */ if ('\0' == path[0]) { errno = old_errno; return 0; } /* We can't handle resolving a buffer inline (erealpath), * so demand separate read and write strings. */ sb_assert(path != resolved_path); retval = erealpath(path, resolved_path); if ((NULL == retval) && (path[0] != '/')) { /* The path could not be canonicalized, append it * to the current working directory if it was not * an absolute path */ if (errno_is_too_long()) return -1; if (NULL == egetcwd(resolved_path, SB_PATH_MAX - 2)) return -1; size_t len = strlen(resolved_path); snprintf(resolved_path + len, SB_PATH_MAX - len, "/%s", path); char *copy = xstrdup(resolved_path); char *ret = erealpath(copy, resolved_path); free(copy); if (ret == NULL) { if (errno_is_too_long()) { /* The resolved path is too long for the buffer to hold */ return -1; } else { /* Whatever it resolved, is not a valid path */ errno = ENOENT; return -1; } } } else if ((NULL == retval) && (path[0] == '/')) { /* Whatever it resolved, is not a valid path */ errno = ENOENT; return -1; } errno = old_errno; return 0; } static char *resolve_path(const char *path, int follow_link) { char *dname, *bname; char *filtered_path; if (NULL == path) return NULL; save_errno(); filtered_path = xmalloc(SB_PATH_MAX * sizeof(char)); if (0 == follow_link) { if (-1 == canonicalize(path, filtered_path)) { free(filtered_path); filtered_path = NULL; } } else { /* Basically we get the realpath which should resolve symlinks, * etc. If that fails (might not exist), we try to get the * realpath of the parent directory, as that should hopefully * exist. If all else fails, just go with canonicalize */ char *ret; if (trace_pid) ret = erealpath(path, filtered_path); else ret = realpath(path, filtered_path); /* Handle broken symlinks. This can come up for a variety of reasons, * but we need to make sure that we resolve the path all the way to the * final target, and not just where the current link happens to start. * Latest discussion is in #540828. * * Maybe we failed because of funky anonymous fd symlinks. * You can see this by doing something like: * $ echo | ls -l /proc/self/fd/ * ....... 0 -> pipe:[9422999] * So any syntax like this we should allow as there isn't any * actual file paths for us to check against. #288863 * Don't look for any particular string as these are dynamic * according to the kernel. You can see pipe:, socket:, etc... * * Maybe we failed because it's a symlink to a path in /proc/ that * is a symlink to a path that longer exists -- readlink will set * ENOENT even in that case and the file ends in (deleted). This * can come up in cases like: * /dev/stderr -> fd/2 -> /proc/self/fd/2 -> /removed/file (deleted) */ if (!ret && errno == ENOENT) { ret = canonicalize_filename_mode(path, CAN_ALL_BUT_LAST); if (ret) { free(filtered_path); filtered_path = ret; } } if (!ret) { char tmp_str1[SB_PATH_MAX]; snprintf(tmp_str1, SB_PATH_MAX, "%s", path); dname = dirname(tmp_str1); /* If not, then check if we can resolve the * parent directory */ if (trace_pid) ret = erealpath(dname, filtered_path); else ret = realpath(dname, filtered_path); if (!ret) { /* Fall back to canonicalize */ if (-1 == canonicalize(path, filtered_path)) { free(filtered_path); filtered_path = NULL; } } else { char tmp_str2[SB_PATH_MAX]; /* OK, now add the basename to keep our access * checking happy (don't want '/usr/lib' if we * tried to do something with non-existing * file '/usr/lib/cf*' ...) */ snprintf(tmp_str2, SB_PATH_MAX, "%s", path); bname = basename(tmp_str2); size_t len = strlen(filtered_path); snprintf(filtered_path + len, SB_PATH_MAX - len, "%s%s", (filtered_path[len - 1] != '/') ? "/" : "", bname); } } } /* If things failed, don't restore errno. More info at comment at * end of check_syscall() function. */ if (filtered_path) restore_errno(); return filtered_path; } /* * Internal Functions */ char *egetcwd(char *buf, size_t size) { struct stat64 st; char *tmpbuf; /* We can't let the C lib allocate memory for us since we have our * own local routines to handle things. */ bool allocated = (buf == NULL); if (allocated) { size = SB_PATH_MAX; buf = xmalloc(size); } /* If tracing a child, our cwd may not be the same as the child's */ if (trace_pid) { char proc[20]; sprintf(proc, "/proc/%i/cwd", trace_pid); ssize_t ret = readlink(proc, buf, size); if (ret == -1) { errno = ESRCH; return NULL; } buf[ret] = '\0'; return buf; } /* Need to disable sandbox, as on non-linux libc's, opendir() is * used by some getcwd() implementations and resolves to the sandbox * opendir() wrapper, causing infinit recursion and finially crashes. */ sandbox_on = false; errno = 0; tmpbuf = sb_unwrapped_getcwd(buf, size); sandbox_on = true; /* We basically try to figure out if we can trust what getcwd() * returned. If one of the following happens kernel/libc side, * bad things will happen, but not much we can do about it: * - Invalid pointer with errno = 0 * - Truncated path with errno = 0 * - Whatever I forgot about */ if ((tmpbuf) && (errno == 0)) { save_errno(); if (!lstat64(buf, &st)) /* errno is set only on failure */ errno = 0; if (errno == ENOENT) /* If lstat failed with eerror = ENOENT, then its * possible that we are running on an older kernel * which had issues with returning invalid paths if * they got too long. Return with errno = ENAMETOOLONG, * so that canonicalize() and check_syscall() know * what the issue is. */ errno = ENAMETOOLONG; if (errno && errno != EACCES) { /* If getcwd() allocated the buffer, free it. */ if (allocated) free(buf); /* Not sure if we should quit here, but I guess if * lstat fails, getcwd could have messed up. Not * sure what to do about errno - use lstat's for * now. */ return NULL; } restore_errno(); } else if (errno != 0) { /* If getcwd() allocated the buffer, free it. */ if (allocated) free(buf); /* Make sure we do not return garbage if the current libc or * kernel's getcwd() is buggy. */ return NULL; } return tmpbuf; } void __sb_dump_backtrace(void) { const char *cmdline = sb_get_cmdline(trace_pid); sb_printf("%s: ", cmdline); sb_copy_file_to_fd(cmdline, STDERR_FILENO); sb_printf("\n\n"); } #define _SB_WRITE_STR(str) \ do { \ size_t _len = strlen(str); \ if (sb_write(logfd, str, _len) != _len) \ goto error; \ } while (0) static bool write_logfile(const char *logfile, const char *func, const char *path, const char *apath, const char *rpath, bool access) { struct stat64 log_stat; int stat_ret; int logfd; bool ret = false; stat_ret = lstat64(logfile, &log_stat); /* Do not care about failure */ errno = 0; if (stat_ret == 0 && S_ISREG(log_stat.st_mode) == 0) sb_ebort("SECURITY BREACH: '%s' %s\n", logfile, "already exists and is not a regular file!"); logfd = sb_open(logfile, O_APPEND | O_WRONLY | O_CREAT | O_CLOEXEC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); if (logfd == -1) { sb_eerror("ISE:%s: unable to append logfile: %s\n", __func__, logfile); goto error; } if (0 != stat_ret) _SB_WRITE_STR( LOG_STRING LOG_FMT_FUNC LOG_FMT_ACCESS LOG_FMT_PATH LOG_FMT_APATH LOG_FMT_RPATH LOG_FMT_CMDLINE ); /* Already have data in the log, so add a newline to space the * log entries. */ _SB_WRITE_STR("\nF: "); _SB_WRITE_STR(func); _SB_WRITE_STR("\nS: "); if (access) _SB_WRITE_STR("allow"); else _SB_WRITE_STR("deny"); _SB_WRITE_STR("\nP: "); _SB_WRITE_STR(path); _SB_WRITE_STR("\nA: "); _SB_WRITE_STR(apath); _SB_WRITE_STR("\nR: "); _SB_WRITE_STR(rpath); _SB_WRITE_STR("\nC: "); const char *cmdline = sb_get_cmdline(trace_pid); if (sb_copy_file_to_fd(cmdline, logfd)) { _SB_WRITE_STR("unable to read "); _SB_WRITE_STR(cmdline); } _SB_WRITE_STR("\n"); ret = true; error: sb_close(logfd); return ret; } static void clean_env_entries(char ***prefixes_array, int *prefixes_num) { if (*prefixes_array == NULL) return; size_t i; save_errno(); for (i = 0; i < *prefixes_num; ++i) { if (NULL != (*prefixes_array)[i]) { free((*prefixes_array)[i]); (*prefixes_array)[i] = NULL; } } if (NULL != *prefixes_array) free(*prefixes_array); *prefixes_array = NULL; *prefixes_num = 0; restore_errno(); } #define pfx_num (*prefixes_num) #define pfx_array (*prefixes_array) #define pfx_item ((*prefixes_array)[(*prefixes_num)]) static void init_env_entries(char ***prefixes_array, int *prefixes_num, const char *env, const char *prefixes_env, int warn) { char *token = NULL; char *rpath = NULL; char *buffer = NULL; char *buffer_ptr = NULL; int prefixes_env_length = strlen(prefixes_env); int num_delimiters = 0; int i = 0; int old_errno = errno; if (NULL == prefixes_env) { /* Do not warn if this is in init stage, as we might get * issues due to LD_PRELOAD already set (bug #91431). */ if (sb_init) fprintf(stderr, "libsandbox: The '%s' env variable is not defined!\n", env); if (pfx_array) { for (i = 0; i < pfx_num; i++) free(pfx_item); free(pfx_array); } pfx_num = 0; goto done; } for (i = 0; i < prefixes_env_length; i++) { if (':' == prefixes_env[i]) num_delimiters++; } /* num_delimiters might be 0, and we need 2 entries at least */ pfx_array = xmalloc(((num_delimiters * 2) + 2) * sizeof(char *)); buffer = xstrdup(prefixes_env); buffer_ptr = buffer; #ifdef HAVE_STRTOK_R token = strtok_r(buffer_ptr, ":", &buffer_ptr); #else token = strtok(buffer_ptr, ":"); #endif while ((NULL != token) && (strlen(token) > 0)) { pfx_item = resolve_path(token, 0); /* We do not care about errno here */ errno = 0; if (NULL != pfx_item) { pfx_num++; /* Now add the realpath if it exists and * are not a duplicate */ rpath = xmalloc(SB_PATH_MAX * sizeof(char)); pfx_item = realpath(*(&(pfx_item) - 1), rpath); if ((NULL != pfx_item) && (0 != strcmp(*(&(pfx_item) - 1), pfx_item))) { pfx_num++; } else { free(rpath); pfx_item = NULL; } } #ifdef HAVE_STRTOK_R token = strtok_r(NULL, ":", &buffer_ptr); #else token = strtok(NULL, ":"); #endif } free(buffer); done: errno = old_errno; return; } static void sb_process_env_settings(void) { static const char * const sb_env_names[4] = { ENV_SANDBOX_DENY, ENV_SANDBOX_READ, ENV_SANDBOX_WRITE, ENV_SANDBOX_PREDICT, }; size_t i; for (i = 0; i < ARRAY_SIZE(sb_env_names); ++i) { char *sb_env = getenv(sb_env_names[i]); /* Allow the vars to change values, but not be unset. * See sb_check_envp() for more details. */ if (!sb_env) continue; if (/*(!sb_env && cached_env_vars[i]) || -- see above */ !cached_env_vars[i] || strcmp(cached_env_vars[i], sb_env) != 0) { clean_env_entries(&sbcontext.prefixes[i], &sbcontext.num_prefixes[i]); if (cached_env_vars[i]) free(cached_env_vars[i]); if (sb_env) { init_env_entries(&sbcontext.prefixes[i], &sbcontext.num_prefixes[i], sb_env_names[i], sb_env, 1); cached_env_vars[i] = xstrdup(sb_env); } else cached_env_vars[i] = NULL; } } } static int check_prefixes(char **prefixes, int num_prefixes, const char *path) { if (!prefixes) return 0; size_t i; for (i = 0; i < num_prefixes; ++i) { if (unlikely(!prefixes[i])) continue; size_t prefix_len = strlen(prefixes[i]); /* Start with a regular prefix match for speed */ if (strncmp(path, prefixes[i], prefix_len)) continue; /* Now, if prefix did not end with a slash, we need to make sure * we are not matching in the middle of a filename. So check * whether the match is followed by a slash, or NUL. */ if (prefixes[i][prefix_len-1] != '/' && path[prefix_len] != '/' && path[prefix_len] != '\0') continue; return 1; } return 0; } /* Is this a func that works on symlinks, and is the file a symlink ? */ static bool symlink_func(int sb_nr, int flags) { /* These funcs always operate on symlinks */ if (sb_nr == SB_NR_UNLINK || sb_nr == SB_NR_UNLINKAT || sb_nr == SB_NR_LCHOWN || sb_nr == SB_NR_LREMOVEXATTR || sb_nr == SB_NR_LSETXATTR || sb_nr == SB_NR_LUTIMES || sb_nr == SB_NR_REMOVE || sb_nr == SB_NR_RENAME || sb_nr == SB_NR_RENAMEAT || sb_nr == SB_NR_RENAMEAT2 || sb_nr == SB_NR_RMDIR || sb_nr == SB_NR_SYMLINK || sb_nr == SB_NR_SYMLINKAT) return true; /* These funcs sometimes operate on symlinks */ if ((sb_nr == SB_NR_FCHOWNAT || sb_nr == SB_NR_FCHMODAT || sb_nr == SB_NR_UTIMENSAT) && (flags & AT_SYMLINK_NOFOLLOW)) return true; return false; } static int check_access(sbcontext_t *sbcontext, int sb_nr, const char *func, int flags, const char *abs_path, const char *resolv_path) { int old_errno = errno; int result = 0; int retval; bool sym_func = symlink_func(sb_nr, flags); retval = check_prefixes(sbcontext->deny_prefixes, sbcontext->num_deny_prefixes, abs_path); if (1 == retval) /* Fall in a read/write denied path, Deny Access */ goto out; if (!sym_func) { retval = check_prefixes(sbcontext->deny_prefixes, sbcontext->num_deny_prefixes, resolv_path); if (1 == retval) /* Fall in a read/write denied path, Deny Access */ goto out; } if (sbcontext->read_prefixes && (sb_nr == SB_NR_ACCESS_RD || sb_nr == SB_NR_OPEN_RD || sb_nr == SB_NR_OPENDIR || sb_nr == SB_NR_POPEN || sb_nr == SB_NR_SYSTEM || /*sb_nr == SB_NR_EXECL || sb_nr == SB_NR_EXECLP || sb_nr == SB_NR_EXECLE ||*/ sb_nr == SB_NR_EXECV || sb_nr == SB_NR_EXECVP || sb_nr == SB_NR_EXECVE || sb_nr == SB_NR_EXECVPE || sb_nr == SB_NR_FEXECVE)) { retval = check_prefixes(sbcontext->read_prefixes, sbcontext->num_read_prefixes, resolv_path); if (1 == retval) { /* Fall in a readable path, Grant Access */ result = 1; goto out; } /* If we are here, and still no joy, and its the access() call, * do not log it, but just return -1 */ if (sb_nr == SB_NR_ACCESS_RD) { sbcontext->show_access_violation = false; goto out; } } /* Hardcode denying write to the whole log dir. While this is a * parial match and so rejects paths that also start with this * string, that isn't going to happen in real life so live with * it. We can't append a slash to this path either as that would * allow people to open the dir itself for writing. */ if (!strncmp(resolv_path, SANDBOX_LOG_LOCATION, strlen(SANDBOX_LOG_LOCATION))) goto out; if (sb_nr == SB_NR_ACCESS_WR || sb_nr == SB_NR_CHMOD || sb_nr == SB_NR_CHOWN || sb_nr == SB_NR_CREAT || sb_nr == SB_NR_CREAT64 || sb_nr == SB_NR_FCHMODAT || sb_nr == SB_NR_FCHOWNAT || /*sb_nr == SB_NR_FTRUNCATE || sb_nr == SB_NR_FTRUNCATE64 ||*/ sb_nr == SB_NR_FUTIMESAT || sb_nr == SB_NR_LCHOWN || sb_nr == SB_NR_LINK || sb_nr == SB_NR_LINKAT || sb_nr == SB_NR_LREMOVEXATTR|| sb_nr == SB_NR_LSETXATTR || sb_nr == SB_NR_LUTIMES || sb_nr == SB_NR_MKDIR || sb_nr == SB_NR_MKDIRAT || sb_nr == SB_NR_MKDTEMP || sb_nr == SB_NR_MKFIFO || sb_nr == SB_NR_MKFIFOAT || sb_nr == SB_NR_MKNOD || sb_nr == SB_NR_MKNODAT || sb_nr == SB_NR_MKOSTEMP || sb_nr == SB_NR_MKOSTEMP64 || sb_nr == SB_NR_MKOSTEMPS || sb_nr == SB_NR_MKOSTEMPS64 || sb_nr == SB_NR_MKSTEMP || sb_nr == SB_NR_MKSTEMP64 || sb_nr == SB_NR_MKSTEMPS || sb_nr == SB_NR_MKSTEMPS64 || sb_nr == SB_NR_OPEN_WR || sb_nr == SB_NR_REMOVE || sb_nr == SB_NR_REMOVEXATTR || sb_nr == SB_NR_RENAME || sb_nr == SB_NR_RENAMEAT || sb_nr == SB_NR_RENAMEAT2 || sb_nr == SB_NR_RMDIR || sb_nr == SB_NR_SETXATTR || sb_nr == SB_NR_SYMLINK || sb_nr == SB_NR_SYMLINKAT || sb_nr == SB_NR_TRUNCATE || sb_nr == SB_NR_TRUNCATE64 || sb_nr == SB_NR_UNLINK || sb_nr == SB_NR_UNLINKAT || sb_nr == SB_NR_UTIME || sb_nr == SB_NR_UTIMENSAT || sb_nr == SB_NR_UTIMES || sb_nr == SB_NR__XMKNOD || sb_nr == SB_NR___XMKNOD || sb_nr == SB_NR___XMKNODAT) { retval = check_prefixes(sbcontext->write_denied_prefixes, sbcontext->num_write_denied_prefixes, resolv_path); if (1 == retval) /* Falls in a write denied path, Deny Access */ goto out; retval = check_prefixes(sbcontext->write_prefixes, sbcontext->num_write_prefixes, resolv_path); if (1 == retval) { /* Falls in a writable path, Grant Access */ result = 1; goto out; } /* Hack to allow writing to '/proc/self/fd' #91516. It needs * to be here as for each process, the '/proc/self' symlink * will differ ... */ char proc_self_fd[SB_PATH_MAX]; if (realpath(sb_get_fd_dir(), proc_self_fd) && !strncmp(resolv_path, proc_self_fd, strlen(proc_self_fd))) { result = 1; goto out; } /* If operating on a location those parent dirs do not exist, * then let it through as the OS itself will trigger a fail. * This is like fopen("/foo/bar", "w") and /foo/ does not * exist. All the functions filtered thus far fall into that * behavior category, so no need to check the syscall. */ char *dname_buf = xstrdup(resolv_path); int aret = sb_unwrapped_access(dirname(dname_buf), F_OK); free(dname_buf); if (aret) { result = 1; goto out; } retval = check_prefixes(sbcontext->predict_prefixes, sbcontext->num_predict_prefixes, resolv_path); if (1 == retval) { /* Is a known access violation, so deny access, * and do not log it */ sbcontext->show_access_violation = false; goto out; } /* If we are here, and still no joy, and its the access() call, * do not log it, but just return -1 */ if (sb_nr == SB_NR_ACCESS_WR) { sbcontext->show_access_violation = false; goto out; } } out: errno = old_errno; return result; } /* Return values: * 0: failure, caller should abort * 1: things worked out fine * 2: things worked out fine, but the errno should not be restored */ static int check_syscall(sbcontext_t *sbcontext, int sb_nr, const char *func, const char *file, int flags) { char *absolute_path = NULL; char *resolved_path = NULL; int old_errno = errno; int result; bool access, debug, verbose, set; absolute_path = resolve_path(file, 0); if (!absolute_path) goto error; /* Do not bother dereferencing symlinks when we are using a function that * itself does not dereference. This speeds things up and avoids updating * the atime implicitly. #415475 */ if (symlink_func(sb_nr, flags)) resolved_path = absolute_path; else resolved_path = resolve_path(file, 1); if (!absolute_path || !resolved_path) goto error; sb_debug_dyn("absolute_path: %s\n", absolute_path); sb_debug_dyn("resolved_path: %s\n", resolved_path); verbose = is_env_set_on(ENV_SANDBOX_VERBOSE, &set); if (set) sbcontext->verbose = verbose; debug = is_env_set_on(ENV_SANDBOX_DEBUG, &set); if (set) sbcontext->debug = debug; result = check_access(sbcontext, sb_nr, func, flags, absolute_path, resolved_path); if (unlikely(verbose)) { int sym_len = SB_MAX_STRING_LEN + 1 - strlen(func); if (!result && sbcontext->show_access_violation) sb_eerror("%sACCESS DENIED%s: %s:%*s%s\n", COLOR_RED, COLOR_NORMAL, func, sym_len, "", absolute_path); else if (debug && sbcontext->show_access_violation) sb_einfo("%sACCESS ALLOWED%s: %s:%*s%s\n", COLOR_GREEN, COLOR_NORMAL, func, sym_len, "", absolute_path); else if (debug && !sbcontext->show_access_violation) sb_ewarn("%sACCESS PREDICTED%s: %s:%*s%s\n", COLOR_YELLOW, COLOR_NORMAL, func, sym_len, "", absolute_path); } if ((0 == result) && sbcontext->show_access_violation) access = false; else access = true; if (unlikely(!access)) { bool worked = write_logfile(log_path, func, file, absolute_path, resolved_path, access); if (!worked && errno) goto error; } if (unlikely(debug)) { bool worked = write_logfile(debug_log_path, func, file, absolute_path, resolved_path, access); if (!worked && errno) goto error; } free(absolute_path); if (absolute_path != resolved_path) free(resolved_path); errno = old_errno; return result; error: /* The path is too long to be canonicalized, so just warn and let the * function handle it (see bugs #21766 #94630 #101728 #227947) */ if (errno_is_too_long()) { free(absolute_path); if (absolute_path != resolved_path) free(resolved_path); return 2; } /* Process went away while we were tracing it ... #264478 */ if (trace_pid && errno == ESRCH) return 2; /* Underlying directory we operate on went away: #590084 */ if (!absolute_path && !resolved_path && errno == ENOENT) { int sym_len = SB_MAX_STRING_LEN + 1 - strlen(func); if (sbcontext->show_access_violation) sb_eerror("%sACCESS DENIED%s: %s:%*s'%s' (from deleted directory, see https://bugs.gentoo.org/590084)\n", COLOR_RED, COLOR_NORMAL, func, sym_len, "", file); return 0; } /* If we get here, something bad happened */ sb_ebort("ISE: %s('%s')\n" "\tabs_path: %s\n" "\tres_path: %s\n" "\terrno=%i: %s\n", func, file, absolute_path, resolved_path, errno, strerror(errno)); } bool is_sandbox_on(void) { bool result = false; save_errno(); /* $SANDBOX_ACTIVE is an env variable that should ONLY * be used internal by sandbox.c and libsanbox.c. External * sources should NEVER set it, else the sandbox is enabled * in some cases when run in parallel with another sandbox, * but not even in the sandbox shell. */ if (sandbox_on) { if (!sbcontext.active) { /* Once you go active, you never go back */ char *sb_env_active = getenv(ENV_SANDBOX_ACTIVE); sbcontext.active = (sb_env_active && !strcmp(sb_env_active, SANDBOX_ACTIVE)); } if (sbcontext.active) { bool on, set; on = is_env_set_on(ENV_SANDBOX_ON, &set); if (set) sbcontext.on = on; if (sbcontext.on) result = true; } } restore_errno(); return result; } bool before_syscall(int dirfd, int sb_nr, const char *func, const char *file, int flags) { int result; char at_file_buf[SB_PATH_MAX]; /* Some funcs operate on a fd directly and so filename is NULL, but * the rest should get rejected as "file/directory does not exist". */ if (file == NULL || file[0] == '\0') { if (file == NULL && dirfd != AT_FDCWD && (sb_nr == SB_NR_UTIMENSAT || sb_nr == SB_NR_FUTIMESAT)) { /* let it slide -- the func is magic and changes behavior * from "file relative to dirfd" to "dirfd is actually file * fd" whenever file is NULL. */ } else { errno = ENOENT; return false; } } switch (resolve_dirfd_path(dirfd, file, at_file_buf, sizeof(at_file_buf))) { case -1: return false; case 0: file = at_file_buf; break; case 2: return true; } save_errno(); /* Need to protect the global sbcontext structure */ sb_lock(); if (!sb_init) { libsb_init(); sb_init = true; } sb_process_env_settings(); /* Might have been reset in check_access() */ sbcontext.show_access_violation = true; result = check_syscall(&sbcontext, sb_nr, func, file, flags); sb_unlock(); if (0 == result) { /* FIXME: Should probably audit errno, and enable some other * error to be returned (EINVAL for invalid mode for * fopen() and co, ETOOLONG, etc). */ errno = EACCES; } else if (result == 1) restore_errno(); return result ? true : false; } bool before_syscall_access(int dirfd, int sb_nr, const char *func, const char *file, int flags) { const char *ext_func; if (flags & W_OK) sb_nr = SB_NR_ACCESS_WR, ext_func = "access_wr"; else sb_nr = SB_NR_ACCESS_RD, ext_func = "access_rd"; return before_syscall(dirfd, sb_nr, ext_func, file, flags); } bool before_syscall_open_int(int dirfd, int sb_nr, const char *func, const char *file, int flags) { const char *ext_func; if ((flags & O_WRONLY) || (flags & O_RDWR)) sb_nr = SB_NR_OPEN_WR, ext_func = "open_wr"; else sb_nr = SB_NR_OPEN_RD, ext_func = "open_rd"; return before_syscall(dirfd, sb_nr, ext_func, file, flags); } bool before_syscall_open_char(int dirfd, int sb_nr, const char *func, const char *file, const char *mode) { if (NULL == mode) return false; const char *ext_func; if ((*mode == 'r') && ((0 == (strcmp(mode, "r"))) || /* The strspn accept args are known non-writable modifiers */ (strlen(mode+1) == strspn(mode+1, "xbtmce")))) sb_nr = SB_NR_OPEN_RD, ext_func = "fopen_rd"; else sb_nr = SB_NR_OPEN_WR, ext_func = "fopen_wr"; return before_syscall(dirfd, sb_nr, ext_func, file, 0); } typedef struct { const char *name; size_t len; const char *value; } env_pair; #define ENV_PAIR(x, n, v) [x] = { .name = n, .len = sizeof(n) - 1, .value = v, } #define str_list_add_item_env(_string_list, _var, _item, _error) \ do { \ char *str = xmalloc(strlen(_var) + strlen(_item) + 2); \ sprintf(str, "%s=%s", _var, _item); \ str_list_add_item(_string_list, str, _error); \ } while (0) /* We need to make sure we pass along sandbox env vars. If we don't, programs * (like scons) will inadvertently disable us. While we allow modification * (e.g. export SANDBOX_WRITE=""), we disallow clearing (e.g. unset SANDBOX_WRITE). * The former is clear in the user's intention, but the latter is indicative * of a bad program. * * XXX: Might be much nicer if we could serialize these vars behind the back of * the program. Might be hard to handle LD_PRELOAD though ... * * execv*() must never modify environment inplace with * setenv/putenv/unsetenv as it can relocate 'environ' and break * vfork()/execv() users: https://bugs.gentoo.org/669702 */ struct sb_envp_ctx sb_new_envp(char **envp, bool insert) { struct sb_envp_ctx r = { .sb_envp = envp, .orig_envp = envp, .__mod_cnt = 0, }; char *entry; size_t count, i; env_pair vars[] = { /* Indices matter -- see init below */ ENV_PAIR( 0, ENV_LD_PRELOAD, sandbox_lib), ENV_PAIR( 1, ENV_SANDBOX_LOG, log_path), ENV_PAIR( 2, ENV_SANDBOX_DEBUG_LOG, debug_log_path), ENV_PAIR( 3, ENV_SANDBOX_MESSAGE_PATH, message_path), ENV_PAIR( 4, ENV_SANDBOX_DENY, cached_env_vars[0]), ENV_PAIR( 5, ENV_SANDBOX_READ, cached_env_vars[1]), ENV_PAIR( 6, ENV_SANDBOX_WRITE, cached_env_vars[2]), ENV_PAIR( 7, ENV_SANDBOX_PREDICT, cached_env_vars[3]), ENV_PAIR( 8, ENV_SANDBOX_ON, NULL), ENV_PAIR( 9, ENV_SANDBOX_ACTIVE, NULL), ENV_PAIR(10, ENV_SANDBOX_VERBOSE, NULL), ENV_PAIR(11, ENV_SANDBOX_DEBUG, NULL), ENV_PAIR(12, "LD_LIBRARY_PATH", NULL), ENV_PAIR(13, ENV_SANDBOX_TESTING, NULL), ENV_PAIR(14, ENV_SANDBOX_METHOD, NULL), }; size_t num_vars = ARRAY_SIZE(vars); char *found_vars[num_vars]; size_t found_var_cnt; /* If sandbox is explicitly disabled, do not propagate the vars * and just return user's envp */ if (!sbcontext.on) return r; /* First figure out how many vars are already in the env */ found_var_cnt = 0; memset(found_vars, 0, sizeof(found_vars)); /* Iterate through user's environment and check against expected. */ str_list_for_each_item(envp, entry, count) { for (i = 0; i < num_vars; ++i) { if (found_vars[i]) continue; if (unlikely(!is_env_var(entry, vars[i].name, vars[i].len))) continue; found_vars[i] = entry; ++found_var_cnt; } } /* Treat unset and expected-unset variables as found. This will allow us * to keep existing environment. */ for (i = 0; i < num_vars; ++i) { if (vars[i].value == NULL && found_vars[i] == NULL) { ++found_var_cnt; } } /* Now specially handle merging of LD_PRELOAD */ char *ld_preload; bool merge_ld_preload = found_vars[0] && !strstr(found_vars[0], sandbox_lib); if (unlikely(merge_ld_preload)) { /* Ok, there's an existing LD_PRELOAD value that we need to merge * with. Handle this specially. */ size_t ld_preload_len = strlen(ENV_LD_PRELOAD); count = ld_preload_len + 1 + strlen(sandbox_lib) + 1 + strlen(found_vars[0] + ld_preload_len + 1); ld_preload = xmalloc(count * sizeof(char)); sprintf(ld_preload, "%s=%s %s", ENV_LD_PRELOAD, sandbox_lib, found_vars[0] + ld_preload_len + 1); goto mod_env; } /* If we found everything, there's nothing to do! */ if ((insert && num_vars == found_var_cnt) || (!insert && found_var_cnt == 0)) /* Use the user's envp */ return r; /* Ok, we need to create our own envp, as we need to restore stuff * and we should not touch the user's envp. First we add our vars, * and just all the rest. */ mod_env: /* Indices matter -- see vars[] setup above */ if (sbcontext.on) vars[8].value = "1"; if (sbcontext.active) vars[9].value = SANDBOX_ACTIVE; if (sbcontext.verbose) vars[10].value = "1"; if (sbcontext.debug) vars[11].value = "1"; if (sbcontext.testing) { vars[12].value = sbcontext.ld_library_path; vars[13].value = "1"; } if (sbcontext.method != SANDBOX_METHOD_ANY) vars[14].value = str_sandbox_method(sbcontext.method); char ** my_env = NULL; if (!insert) { str_list_for_each_item(envp, entry, count) { for (i = 0; i < num_vars; ++i) if (i != 12 /* LD_LIBRARY_PATH index */ && is_env_var(entry, vars[i].name, vars[i].len)) { r.__mod_cnt++; goto skip; } str_list_add_item(my_env, entry, error); skip: ; } } else { /* Count directly due to variability with LD_PRELOAD and the value * logic below. Getting out of sync can mean memory corruption. */ r.__mod_cnt = 0; if (unlikely(merge_ld_preload)) { str_list_add_item(my_env, ld_preload, error); r.__mod_cnt++; } for (i = 0; i < num_vars; ++i) { if (found_vars[i] || !vars[i].value) continue; str_list_add_item_env(my_env, vars[i].name, vars[i].value, error); r.__mod_cnt++; } str_list_for_each_item(envp, entry, count) { if (unlikely(merge_ld_preload && is_env_var(entry, vars[0].name, vars[0].len))) continue; str_list_add_item(my_env, entry, error); } } error: r.sb_envp = my_env; return r; } void sb_free_envp(struct sb_envp_ctx * envp_ctx) { /* We assume all the stuffed vars are at the start */ size_t mod_cnt = envp_ctx->__mod_cnt; char ** envp = envp_ctx->sb_envp; size_t i; for (i = 0; i < mod_cnt; ++i) free(envp[i]); /* We do not use str_list_free(), as we did not allocate the * entries except for LD_PRELOAD. All the other entries are * pointers to existing envp memory. */ if (envp != envp_ctx->orig_envp) free(envp); }