Restore environment from parent for external processes

This commit is contained in:
Pablo Marcos Oltra 2018-01-11 13:17:08 +01:00
parent 5f97d55211
commit 6059df77c3
7 changed files with 280 additions and 292 deletions

View File

@ -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

View File

@ -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.

View File

@ -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);
}

View File

@ -2,6 +2,7 @@
#define DEBUG_H
#include <stdlib.h>
#include <stdio.h>
#define DEBUG(...) do { \
if (getenv("APPIMAGE_CHECKRT_DEBUG")) \

120
env.c Executable file
View File

@ -0,0 +1,120 @@
/* Copyright (c) 2018 Pablo Marcos Oltra <pablo.marcos.oltra@gmail.com>
*
* 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 <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
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

9
env.h Executable file
View File

@ -0,0 +1,9 @@
#ifndef ENV_H
#define END_H
#include <unistd.h>
char* const* read_parent_env();
void env_free(char* const *env);
#endif

384
exec.c
View File

@ -1,317 +1,135 @@
/*
* Copyright (C) 2016 Sven Brauch <mail@svenbrauch.de>
/* Copyright (c) 2018 Pablo Marcos Oltra <pablo.marcos.oltra@gmail.com>
*
* 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 <stdlib.h>
#include <unistd.h>
#include <dlfcn.h>
#include <stdio.h>
#include <string.h>
#include <malloc.h>
#include <stdlib.h>
#include <errno.h>
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