diff --git a/Makefile b/Makefile index 7af5a9b..cd8fe34 100644 --- a/Makefile +++ b/Makefile @@ -2,20 +2,37 @@ CFLAGS ?= -O2 -Wall -Wextra LDFLAGS += -s BIN = AppRun_patched LIB = exec.so +EXEC_TEST = exec_test +ENV_TEST = env_test +checkrt: $(BIN) $(LIB) -all: $(BIN) $(LIB) +test: $(EXEC_TEST) $(ENV_TEST) + +all: checkrt test clean: - -rm -f $(BIN) $(LIB) *.o AppRun.c AppRun_patched.c + -rm -f $(BIN) $(LIB) $(EXEC_TEST) $(ENV_TEST) *.o AppRun.c AppRun_patched.c -$(BIN): AppRun_patched.o checkrt.o +$(BIN): AppRun_patched.o checkrt.o env.o -$(LIB): exec.o +$(LIB): exec.o env.o $(CC) -shared $(LDFLAGS) -o $@ $^ -ldl AppRun_patched.o checkrt.o: CFLAGS += -include checkrt.h -exec.o: CFLAGS += -fPIC -std=c99 +exec.o env.o: CFLAGS += -fPIC + +$(EXEC_TEST): CFLAGS += -DEXEC_TEST +$(EXEC_TEST): exec.c env.c + $(CC) -o $@ $(CFLAGS) $^ -ldl + +$(ENV_TEST): CFLAGS += -DENV_TEST +$(ENV_TEST): env.c + $(CC) -o $@ $(CFLAGS) $^ + +run_tests: $(EXEC_TEST) $(ENV_TEST) + ./$(ENV_TEST) + ./$(EXEC_TEST) AppRun_patched.c: AppRun.c patch -p1 --output $@ < AppRun.c.patch @@ -23,3 +40,4 @@ AppRun_patched.c: AppRun.c AppRun.c: wget -c "https://raw.githubusercontent.com/AppImage/AppImageKit/appimagetool/master/src/AppRun.c" +.PHONY: checkrt test run_tests all clean diff --git a/README.md b/README.md index 23571f8..fcec416 100644 --- a/README.md +++ b/README.md @@ -16,4 +16,20 @@ You would have to know the library version of the host system and decide whether application is started. This is exactly what the patched AppRun binary does. It will search for `usr/optional/libstdc++/libstdc++.so.6` and `usr/optional/libgcc_s/libgcc_s.so.1` inside the AppImage or AppDir. If found it will compare their internal versions with the ones found on the system and prepend their paths to `LD_LIBRARY_PATH` if necessary. -You should also put `exec.so` into `usr/optional`. + +You should also put `exec.so` into `usr/optional`. This exec.so library is intended to restore the environment of the AppImage to its parent. +This is done to avoid library clashing of bundled libraries with external processes. e.g when running the web browser + +The intended usage is as follows: + +1. This library is injected to the dynamic loader through LD_PRELOAD + automatically in AppRun **only** if `usr/optional/exec.so` exists: + e.g `LD_PRELOAD=$APPDIR/usr/optional/exec.so My.AppImage` + +2. This library will intercept calls to new processes and will detect whether + those calls are for binaries within the AppImage bundle or external ones. + +3. In case it's an internal process, it will not change anything. + In case it's an external process, it will restore the environment of + the AppImage parent by reading `/proc/[pid]/environ`. + This is the conservative approach taken. diff --git a/checkrt.c b/checkrt.c index 1cde83b..e62327b 100644 --- a/checkrt.c +++ b/checkrt.c @@ -103,12 +103,18 @@ void checkrt(char *usr_in_appdir) if (gcc_bundle_ver > gcc_sys_ver) bundle_gcc = 1; - if (bundle_cxx == 1 || bundle_gcc == 1) { + char *exec_file = malloc(strlen(usr_in_appdir) + 1 + strlen(EXEC_SO) + 1); + sprintf(exec_file, "%s/%s", usr_in_appdir, EXEC_SO); + f = fopen(exec_file, "r"); + if (f) { char *old_ld_preload = getenv("LD_PRELOAD"); optional_ld_preload = malloc(strlen(EXEC_SO) + (old_ld_preload ? 1+strlen(old_ld_preload) : 0) + 13 + len); - sprintf(optional_ld_preload, "LD_PRELOAD=%s/" EXEC_SO "%s%s", usr_in_appdir, + sprintf(optional_ld_preload, "LD_PRELOAD=%s%s%s", exec_file, old_ld_preload ? ":" : "", old_ld_preload ? old_ld_preload : ""); + DEBUG("optional_ld_preload: %s\n", optional_ld_preload); + fclose(f); } + free(exec_file); if (bundle_cxx == 1 && bundle_gcc == 0) { optional_ld_library_path = malloc(strlen(CXXDIR) + 3 + len); @@ -124,6 +130,6 @@ void checkrt(char *usr_in_appdir) sprintf(optional_ld_library_path, "%s", ""); } - DEBUG("optional_ld_library_path: %s\noptional_ld_preload: %s\n", optional_ld_library_path, optional_ld_preload); + DEBUG("optional_ld_library_path: %s\n", optional_ld_library_path); } diff --git a/debug.h b/debug.h index 0eb7fcb..706d043 100644 --- a/debug.h +++ b/debug.h @@ -2,6 +2,7 @@ #define DEBUG_H #include +#include #define DEBUG(...) do { \ if (getenv("APPIMAGE_CHECKRT_DEBUG")) \ diff --git a/env.c b/env.c new file mode 100755 index 0000000..6776bff --- /dev/null +++ b/env.c @@ -0,0 +1,120 @@ +/* Copyright (c) 2018 Pablo Marcos Oltra + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#define _GNU_SOURCE + +#include "env.h" +#include "debug.h" + +#include +#include +#include +#include +#include + +static char** env_allocate(size_t size) { + return calloc(size + 1, sizeof(char*)); +} + +void env_free(char* const *env) { + size_t len = 0; + while (env[len] != 0) { + free(env[len]); + len++; + } + free((char**)env); +} + +static size_t get_number_of_variables(FILE *file, char **buffer, size_t *len) { + size_t number = 0; + + if (getline(buffer, len, file) < 0) + return -1; + + char *ptr = *buffer; + while (ptr < *buffer + *len) { + size_t var_len = strlen(ptr); + ptr += var_len + 1; + if (var_len == 0) + break; + number++; + } + + return number != 0 ? (ssize_t)number : -1; +} + +static char* const* env_from_buffer(FILE *file) { + char *buffer = NULL; + size_t len = 0; + size_t num_vars = get_number_of_variables(file, &buffer, &len); + char** env = env_allocate(num_vars); + + size_t n = 0; + char *ptr = buffer; + while (ptr < buffer + len && n < num_vars) { + size_t var_len = strlen(ptr); + if (var_len == 0) + break; + + env[n] = calloc(sizeof(char*), var_len + 1); + strncpy(env[n], ptr, var_len + 1); + DEBUG("\tenv var copied: %s\n", env[n]); + ptr += var_len + 1; + n++; + } + free(buffer); + + return env; +} + +static char* const* read_env_from_process(pid_t pid) { + char buffer[256] = {0}; + + snprintf(buffer, sizeof(buffer), "/proc/%d/environ", pid); + DEBUG("Reading env from parent process: %s\n", buffer); + FILE *env_file = fopen(buffer, "r"); + if (!env_file) { + DEBUG("Error reading file: %s (%s)\n", buffer, strerror(errno)); + return NULL; + } + + char* const* env = env_from_buffer(env_file); + fclose(env_file); + + return env; +} + +char* const* read_parent_env() { + pid_t ppid = getppid(); + return read_env_from_process(ppid); +} + +#ifdef ENV_TEST +int main() { + putenv("APPIMAGE_CHECKRT_DEBUG=1"); + DEBUG("ENV TEST\n"); + char **env = NULL; + read_parent_env(&env); + + return 0; +} +#endif + diff --git a/env.h b/env.h new file mode 100755 index 0000000..affc2d4 --- /dev/null +++ b/env.h @@ -0,0 +1,9 @@ +#ifndef ENV_H +#define END_H + +#include + +char* const* read_parent_env(); +void env_free(char* const *env); + +#endif diff --git a/exec.c b/exec.c index 0e32ef3..ad0f489 100644 --- a/exec.c +++ b/exec.c @@ -1,317 +1,135 @@ -/* - * Copyright (C) 2016 Sven Brauch +/* Copyright (c) 2018 Pablo Marcos Oltra * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Library General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: * - * 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 - * Library General Public License for more details. + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. * - * You should have received a copy of the GNU Library General Public License - * along with this library; see the file COPYING.LIB. If not, write to - * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - * Boston, MA 02110-1301, USA. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. */ -/** -This library is intended to be used together with the AppImage distribution mechanism. -Place the library somewhere in your AppImage and point LD_PRELOAD to it -before launching your application. - -Whenever your application invokes a child process through execv() or execve(), -this wrapper will intercept the call and see if the child process lies -outside of the bundled appdir. If it does, the wrapper will attempt to undo -any changes done to environment variables before launching the process, -since you probably did not intend to launch it with e.g. the LD_LIBRARY_PATH -you previously set for your application. - -To perform this operation, you have to set the following environment variables: - $APPDIR -- path of the AppDir you are launching your application from. If this - is not present, the wrapper will do nothing. - -For each environment variable you want restored, where {VAR} is the name of the environment -variable (e.g. "PATH"): - $APPIMAGE_ORIGINAL_{VAR} -- original value of the environment variable - $APPIMAGE_STARTUP_{VAR} -- value of the variable when you were starting up - your application -*/ +/* + * This exec.so library is intended to restore the environment of the AppImage's + * parent process. This is done to avoid library clashing of bundled libraries + * with external processes. e.g when running the web browser + * + * The intended usage is as follows: + * + * 1. This library is injected to the dynamic loader through LD_PRELOAD + * automatically in AppRun **only** if `usr/optional/exec.so` exists: + * e.g `LD_PRELOAD=$APPDIR/usr/optional/exec.so My.AppImage` + * + * 2. This library will intercept calls to new processes and will detect whether + * those calls are for binaries within the AppImage bundle or external ones. + * + * 3. In case it's an internal process, it will not change anything. + * In case it's an external process, it will restore the environment of + * the AppImage parent by reading `/proc/[pid]/environ`. + * This is the conservative approach taken. + */ #define _GNU_SOURCE +#include "env.h" #include "debug.h" +#include #include #include -#include #include -#include -#include +#include -typedef ssize_t (*execve_func_t)(const char* filename, char* const argv[], char* const envp[]); -static execve_func_t old_execve = NULL; +typedef int (*execve_func_t)(const char *filename, char *const argv[], char *const envp[]); -typedef ssize_t (*execvp_func_t)(const char* filename, char* const argv[]); -//static execvp_func_t old_execvp = NULL; +#define MIN(X,Y) ((X) < (Y) ? (X) : (Y)) -// TODO implement me: execl, execlp, execle; but it's annoying work and nothing seems to use them -// typedef int (*execl_func_t)(const char *path, const char *arg); -// static execl_func_t old_execl = NULL; -// -// typedef int (*execlp_func_t)(const char *file, const char *arg); -// static execlp_func_t old_execlp = NULL; -// -// typedef int (*execle_func_t)(const char *path, const char *arg, char * const envp[]); -// static execle_func_t old_execle = NULL; +static const char* get_fullpath(const char *filename) { + // Try to get the canonical path in case it's a relative path or symbolic + // link. Otherwise, use which to get the fullpath of the binary + char *fullpath = canonicalize_file_name(filename); + DEBUG("filename %s, canonical path %s\n", filename, fullpath); + if (fullpath) + return fullpath; -typedef int (*execv_func_t)(const char *path, char *const argv[]); -//static execv_func_t old_execv = NULL; - -typedef int (*execvpe_func_t)(const char *file, char *const argv[], char *const envp[]); -static execvpe_func_t old_execvpe = NULL; - -char* APPIMAGE_ORIG_PREFIX = "APPIMAGE_ORIGINAL_"; -char* APPIMAGE_STARTUP_PREFIX = "APPIMAGE_STARTUP_"; -char* APPDIR = "APPDIR"; - -typedef struct { - char** names; - char** values; -} environment; - -environment environment_alloc(size_t envc) { - environment env; - env.names = calloc(envc+1, sizeof(char*)); - env.values = calloc(envc+1, sizeof(char*)); - return env; + return filename; } -int arr_len(char* const x[]) { - int len = 0; - while ( x[len] != 0 ) { - len++; +static int is_external_process(const char *filename) { + const char *appdir = getenv("APPDIR"); + if (!appdir) + return 0; + DEBUG("APPDIR = %s\n", appdir); + + return strncmp(filename, appdir, MIN(strlen(filename), strlen(appdir))); +} + +static int exec_common(execve_func_t function, const char *filename, char* const argv[], char* const envp[]) { + const char *fullpath = get_fullpath(filename); + DEBUG("filename %s, fullpath %s\n", filename, fullpath); + char* const *env = envp; + if (is_external_process(fullpath)) { + DEBUG("External process detected. Restoring env vars from parent %d\n", getppid()); + env = read_parent_env(); + if (!env) + env = envp; + else + DEBUG("Error restoring env vars from parent\n"); } - return len; -} + int ret = function(filename, argv, env); -void stringlist_free(char* const envp[]) { - if ( envp ) { - for ( int i = 0; i < arr_len(envp); i++ ) { - free(envp[i]); - } - } -} + if (fullpath != filename) + free((char*)fullpath); + if (env != envp) + env_free(env); -char** stringlist_alloc(int size) { - char** ret = calloc(size, sizeof(char*)); return ret; } -int environment_len(const environment env) { - return arr_len(env.names); +int execve(const char *filename, char *const argv[], char *const envp[]) { + DEBUG("execve call hijacked: %s\n", filename); + execve_func_t execve_orig = dlsym(RTLD_NEXT, "execve"); + if (!execve_orig) + DEBUG("Error getting execve original symbol: %s\n", strerror(errno)); + + return exec_common(execve_orig, filename, argv, envp); } -void environment_free(environment env) { - stringlist_free(env.names); - stringlist_free(env.values); +int execv(const char *filename, char *const argv[]) { + DEBUG("execv call hijacked: %s\n", filename); + return execve(filename, argv, environ); } -void environment_append_item(environment env, char* name, int name_size, char* val, int val_size) { - int count = environment_len(env); - env.names[count] = calloc(name_size+1, sizeof(char)); - env.values[count] = calloc(val_size+1, sizeof(char)); - strncpy(env.names[count], name, name_size); - strncpy(env.values[count], val, val_size); +int execvpe(const char *filename, char *const argv[], char *const envp[]) { + DEBUG("execvpe call hijacked: %s\n", filename); + execve_func_t execve_orig = dlsym(RTLD_NEXT, "execvpe"); + if (!execve_orig) + DEBUG("Error getting execvpe original symbol: %s\n", strerror(errno)); + + return exec_common(execve_orig, filename, argv, envp); } -int environment_find_name(environment env, char* name, int name_size) { - int count = environment_len(env); - for ( int i = 0; i < count; i++ ) { - if ( !strncmp(env.names[i], name, name_size) ) { - return i; - } - } - return -1; +int execvp(const char *filename, char *const argv[]) { + DEBUG("execvp call hijacked: %s\n", filename); + return execvpe(filename, argv, environ); } -char** environment_to_stringlist(environment env) { - int len = environment_len(env); - char** ret = stringlist_alloc(len+1); - for ( int i = 0; i < len; i++ ) { - char* name = env.names[i]; - char* value = env.values[i]; - int result_len = strlen(name) + strlen(value) + 1; - ret[i] = calloc(result_len+1, sizeof(char)); - strcat(ret[i], name); - strcat(ret[i], "="); - strcat(ret[i], value); - } - return ret; -} - -char** adjusted_environment(const char* filename, char* const envp[]) { - if ( !envp ) { - return NULL; - } - - int envc = arr_len(envp); - - char* appdir = NULL; - - environment orig = environment_alloc(envc); - environment startup = environment_alloc(envc); - int orig_prefix_len = strlen(APPIMAGE_ORIG_PREFIX); - int startup_prefix_len = strlen(APPIMAGE_STARTUP_PREFIX); - for ( int i = 0; i < envc; i++ ) { - char* line = envp[i]; - int name_size = strchr(line, '=')-line; - int val_size = strlen(line)-name_size-1; - - if ( !strncmp(line, APPIMAGE_ORIG_PREFIX, orig_prefix_len) ) { - environment_append_item(orig, line+orig_prefix_len, name_size-orig_prefix_len, - line+name_size+1, val_size); - } - if ( !strncmp(line, APPIMAGE_STARTUP_PREFIX, startup_prefix_len) ) { - environment_append_item(startup, line+startup_prefix_len, name_size-startup_prefix_len, - line+name_size+1, val_size); - } - if ( !strncmp(line, APPDIR, strlen(APPDIR)) ) { - appdir = calloc(val_size+1, sizeof(char)); - strncpy(appdir, line+name_size+1, val_size); - } - } - - environment new_env = environment_alloc(envc); - if ( appdir && strncmp(filename, appdir, strlen(appdir)) ) { - // we have a value for $APPDIR and are leaving it -- perform replacement - for ( int i = 0; i < envc; i++ ) { - char* line = envp[i]; - if ( !strncmp(line, APPIMAGE_ORIG_PREFIX, strlen(APPIMAGE_ORIG_PREFIX)) || - !strncmp(line, APPIMAGE_STARTUP_PREFIX, strlen(APPIMAGE_STARTUP_PREFIX)) ) - { - // we are not interested in the backup vars here, don't copy them over - continue; - } - - int name_size = strchr(line, '=')-line; - int val_size = strlen(line)-name_size-1; - char* value = line+name_size+1; - int value_len = strlen(value); - - int at_startup = environment_find_name(startup, line, name_size); - int at_original = environment_find_name(orig, line, name_size); - if ( at_startup == -1 || at_original == -1 ) { - // no information, just keep it - environment_append_item(new_env, line, name_size, value, value_len); - continue; - } - - char* at_start = startup.values[at_startup]; - int at_start_len = strlen(at_start); - char* at_orig = orig.values[at_original]; - int at_orig_len = strlen(at_orig); - - // TODO HACK: do not copy over empty vars - if ( strlen(at_orig) == 0 ) { - continue; - } - - if ( !strncmp(line+name_size+1, startup.values[at_startup], val_size) ) { - // nothing changed since startup, restore old value - environment_append_item(new_env, line, name_size, at_orig, at_orig_len); - continue; - } - - int chars_added = value_len > at_start_len; - char* use_value = NULL; - if ( chars_added > 0 ) { - // something was added to the current value - // take _original_ value of the env var and append/prepend the same thing - use_value = calloc(strlen(at_orig) + chars_added + 1, sizeof(char)); - if ( !strncmp(value, at_start, at_start_len) ) { - // append case - strcat(use_value, value); - strcat(use_value, at_orig + strlen(value)); - } - else if ( !strncmp(value+(value_len-at_start_len), at_start, at_start_len) ) { - // prepend case - strcat(use_value, at_orig + strlen(value)); - strcat(use_value, value); - } - else { - // none of the above methods matched - // assume the value changed completely and simply keep what the application set - free(use_value); - use_value = NULL; - } - } - if ( !use_value ) { - environment_append_item(new_env, line, name_size, value, value_len); - } - else { - environment_append_item(new_env, line, name_size, use_value, strlen(use_value)); - free(use_value); - } - } - } - - char** ret = NULL; - if ( environment_len(new_env) > 0 ) { - ret = environment_to_stringlist(new_env); - } - else { - // nothing changed - ret = stringlist_alloc(envc+1); - for ( int i = 0; i < envc; i++ ) { - int len = strlen(envp[i]); - ret[i] = calloc(len+1, sizeof(char)); - strncpy(ret[i], envp[i], len); - } - } - environment_free(orig); - environment_free(startup); - environment_free(new_env); - free(appdir); - return ret; -} - -int execve(const char* filename, char* const argv[], char* const envp[]) { - char** new_envp = adjusted_environment(filename, envp); - old_execve = dlsym(RTLD_NEXT, "execve"); - int ret = old_execve(filename, argv, new_envp); - stringlist_free(new_envp); - DEBUG("custom execve()!\n"); - return ret; -} - -int execv(const char* filename, char* const argv[]) { - char** new_envp = adjusted_environment(filename, environ); - old_execve = dlsym(RTLD_NEXT, "execve"); - int ret = old_execve(filename, argv, new_envp); - stringlist_free(new_envp); - DEBUG("custom execv()!\n"); - return ret; -} - -int execvpe(const char* filename, char* const argv[], char* const envp[]) { - // TODO: might not be full path - char** new_envp = adjusted_environment(filename, envp); - old_execvpe = dlsym(RTLD_NEXT, "execvpe"); - int ret = old_execvpe(filename, argv, new_envp); - stringlist_free(new_envp); - DEBUG("custom execvpe()!\n"); - return ret; -} - -int execvp(const char* filename, char* const argv[]) { - // TODO: might not be full path - char** new_envp = adjusted_environment(filename, environ); - old_execvpe = dlsym(RTLD_NEXT, "execvpe"); - int ret = old_execvpe(filename, argv, new_envp); - stringlist_free(new_envp); - DEBUG("custom execvp()!\n"); - return ret; +#ifdef EXEC_TEST +int main(int argc, char *argv[]) { + putenv("APPIMAGE_CHECKRT_DEBUG=1"); + DEBUG("EXEC TEST\n"); + execv("./env_test", argv); + + return 0; } +#endif