diff --git a/ci/build.sh b/ci/build.sh index dbd972ce..0a5f4a42 100755 --- a/ci/build.sh +++ b/ci/build.sh @@ -45,6 +45,8 @@ cmake_args=( "-DCMAKE_BUILD_TYPE=RelWithDebInfo" "-DBUILD_TESTING=OFF" "-DCMAKE_BUILD_TYPE=RelWithDebInfo" + # TODO: we don't necessarily want to hardcode this here + "-DBINFMT_INTERPRETER_PATH_PREPEND_LD_P_NATIVE_PACKAGES_PREFIX=/opt/appimagelauncher.AppDir/" ) if [[ "${BUILD_LITE:-}" == "" ]]; then diff --git a/cmake/install.cmake b/cmake/install.cmake index 520f3b5e..ee3de23e 100644 --- a/cmake/install.cmake +++ b/cmake/install.cmake @@ -43,11 +43,18 @@ if(ENABLE_UPDATE_HELPER) ) endif() +option(BINFMT_INTERPRETER_PATH_PREPEND_LD_P_NATIVE_PACKAGES_PREFIX "") + if(NOT BUILD_LITE) # unfortunately, due to a cyclic dependency, we need to hardcode parts of this variable, which is included in the # install scripts and the binfmt.d config set(BINFMT_INTERPRETER_PATH ${CMAKE_INSTALL_PREFIX}/${_private_libdir}/binfmt-interpreter) + if(BINFMT_INTERPRETER_PATH_PREPEND_LD_P_NATIVE_PACKAGES_PREFIX) + message(STATUS "Prepending prefix ${BINFMT_INTERPRETER_PATH_PREPEND_LD_P_NATIVE_PACKAGES_PREFIX} to binfmt interpreter path") + set(BINFMT_INTERPRETER_PATH "${BINFMT_INTERPRETER_PATH_PREPEND_LD_P_NATIVE_PACKAGES_PREFIX}${BINFMT_INTERPRETER_PATH}") + endif() + # according to https://www.kernel.org/doc/html/latest/admin-guide/binfmt-misc.html, we must make sure the # interpreter string does not exceed 127 characters set(BINFMT_INTERPRETER_PATH_LENGTH_MAX 127) diff --git a/resources/install-scripts/post-install.in b/resources/install-scripts/post-install.in index 3940cc88..65fc8cfd 100755 --- a/resources/install-scripts/post-install.in +++ b/resources/install-scripts/post-install.in @@ -1,6 +1,6 @@ #! /bin/bash -set -eo pipefail +set -euo pipefail echo "Installing AppImageLauncher as interpreter for AppImages" diff --git a/resources/install-scripts/post-uninstall.in b/resources/install-scripts/post-uninstall.in index d27e46ea..54caea26 100755 --- a/resources/install-scripts/post-uninstall.in +++ b/resources/install-scripts/post-uninstall.in @@ -1,6 +1,6 @@ #! /bin/bash -set -eo pipefail +set -euo pipefail echo "Removing AppImageLauncher as interpreter for AppImages" (set -x; systemctl restart systemd-binfmt) diff --git a/src/binfmt-bypass/CMakeLists.txt b/src/binfmt-bypass/CMakeLists.txt index a4f7f39b..387e9b83 100644 --- a/src/binfmt-bypass/CMakeLists.txt +++ b/src/binfmt-bypass/CMakeLists.txt @@ -120,14 +120,14 @@ target_link_libraries(${bypass_lib} PUBLIC dl) # we need to include the preload lib headers (see below) from the binary dir target_include_directories(${bypass_lib} PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) target_compile_options(${bypass_lib} - PRIVATE -DPRELOAD_LIB_PATH="${CMAKE_INSTALL_PREFIX}/${_private_libdir}/$" + PRIVATE -DPRELOAD_LIB_NAME="$" PUBLIC -D_GNU_SOURCE # obviously needs to be private, otherwise the value leaks into users of this lib PRIVATE -DCOMPONENT_NAME="lib" ) if(build_32bit_preload_library) target_compile_options(${bypass_lib} - PRIVATE -DPRELOAD_LIB_PATH_32BIT="${CMAKE_INSTALL_PREFIX}/${_private_libdir}/$" + PRIVATE -DPRELOAD_LIB_NAME_32BIT="$" ) target_sources(${bypass_lib} PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/${preload_lib_32bit}.h) endif() diff --git a/src/binfmt-bypass/elf.cpp b/src/binfmt-bypass/elf.cpp index 7d7998ee..8478aa6d 100644 --- a/src/binfmt-bypass/elf.cpp +++ b/src/binfmt-bypass/elf.cpp @@ -92,6 +92,49 @@ off_t get_elf_size(std::ifstream& ifs) return sht_end > last_section_end ? sht_end : last_section_end; } +template +ssize_t get_pt_dynamic_offset(std::ifstream& ifs) +{ + static_assert(std::is_same::value || std::is_same::value, + "must be Elf{32,64}_Ehdr"); + static_assert(std::is_same::value || std::is_same::value, + "must be Elf{32,64}_Shdr"); + + EhdrT elf_header{}; + + ifs.seekg(0, std::ifstream::beg); + ifs.read(reinterpret_cast(&elf_header), sizeof(elf_header)); + + if (!ifs) { + log_error("failed to read ELF header\n"); + return -1; + } + + swap_data_if_necessary(elf_header, elf_header.e_shoff); + swap_data_if_necessary(elf_header, elf_header.e_shentsize); + swap_data_if_necessary(elf_header, elf_header.e_shnum); + swap_data_if_necessary(elf_header, elf_header.e_shnum); + + off_t last_shdr_offset = elf_header.e_shoff + (elf_header.e_shentsize * (elf_header.e_shnum - 1)); + ShdrT section_header{}; + + ifs.seekg(last_shdr_offset, std::ifstream::beg); + ifs.read(reinterpret_cast(§ion_header), sizeof(elf_header)); + + if (!ifs) { + log_error("failed to read ELF section header\n"); + return -1; + } + + swap_data_if_necessary(elf_header, section_header.sh_offset); + swap_data_if_necessary(elf_header, section_header.sh_size); + + /* ELF ends either with the table of section headers (SHT) or with a section. */ + off_t sht_end = elf_header.e_shoff + (elf_header.e_shentsize * elf_header.e_shnum); + off_t last_section_end = section_header.sh_offset + section_header.sh_size; + return sht_end > last_section_end ? sht_end : last_section_end; +} + bool is_32bit_elf(std::ifstream& ifs) { if (!ifs) { log_error("failed to read e_ident from ELF file\n"); @@ -121,13 +164,41 @@ bool is_32bit_elf(const std::string& filename) { std::ifstream ifs(filename); if (!ifs) { - log_error("could not open file\n"); + log_error("could not open file: %s\n", filename.c_str()); return -1; } return is_32bit_elf(ifs); } +bool is_statically_linked_elf(std::ifstream& ifs) { + if (!ifs) { + log_error("failed to read e_ident from ELF file\n"); + return -1; + } + + ssize_t pt_dynamic_offset; + + if (is_32bit_elf(ifs)) { + pt_dynamic_offset = get_pt_dynamic_offset(ifs); + } else { + pt_dynamic_offset = get_pt_dynamic_offset(ifs); + } + + return pt_dynamic_offset != -1; +} + +bool is_statically_linked_elf(const std::string& filename) { + std::ifstream ifs(filename); + + if (!ifs) { + log_error("could not open file: %s\n", filename.c_str()); + return -1; + } + + return is_statically_linked_elf(ifs); +} + ssize_t elf_binary_size(const std::string& filename) { std::ifstream ifs(filename); diff --git a/src/binfmt-bypass/elf.h b/src/binfmt-bypass/elf.h index bd25a3fa..50e484b8 100644 --- a/src/binfmt-bypass/elf.h +++ b/src/binfmt-bypass/elf.h @@ -2,6 +2,13 @@ #include +/** + * Check whether file is linked staticallly. + * @param filename path to ELF file + * @return true if file is statically linked, false otherwise + */ +bool is_statically_linked_elf(const std::string& filename); + /** * Calculate size of ELF binary. Useful e.g., to estimate the size of the runtime in an AppImage. * @param filename path to ELF file diff --git a/src/binfmt-bypass/interpreter_main.cpp b/src/binfmt-bypass/interpreter_main.cpp index 3bdcffb9..fa055649 100644 --- a/src/binfmt-bypass/interpreter_main.cpp +++ b/src/binfmt-bypass/interpreter_main.cpp @@ -33,7 +33,6 @@ int main(int argc, char** argv) { if (!executableExists(APPIMAGELAUNCHER_PATH)) { log_message( "AppImageLauncher not found at %s, launching AppImage directly: %s\n", - APPIMAGELAUNCHER_PATH, appImagePath.c_str() ); useAppImageLauncher = false; @@ -42,7 +41,6 @@ int main(int argc, char** argv) { if (getenv("APPIMAGELAUNCHER_DISABLE") != nullptr) { log_message( "APPIMAGELAUNCHER_DISABLE set, launching AppImage directly: %s\n", - APPIMAGELAUNCHER_PATH, appImagePath.c_str() ); useAppImageLauncher = false; diff --git a/src/binfmt-bypass/lib.cpp b/src/binfmt-bypass/lib.cpp index d2d07304..852c6cad 100644 --- a/src/binfmt-bypass/lib.cpp +++ b/src/binfmt-bypass/lib.cpp @@ -9,6 +9,7 @@ #include #include #include +#include // own headers #include "elf.h" @@ -16,7 +17,7 @@ #include "lib.h" #include "binfmt-bypass-preload.h" -#ifdef PRELOAD_LIB_PATH_32BIT +#ifdef PRELOAD_LIB_NAME_32BIT #include "binfmt-bypass-preload_32bit.h" #endif @@ -122,23 +123,27 @@ int create_shm_fd_with_patched_runtime(const char* const appimage_filename, cons #endif -std::string find_preload_library(bool is_32bit) { +std::filesystem::path find_preload_library(bool is_32bit) { + // packaging is now done using ld-p-native_packages which does not make guarantees about the install path + // therefore, we need to look up the path of the preload library in relation to the current binary's path // since we use the F (fix binary) binfmt mode nowadays to enable the use of the interpreter in different cgroups, - // namespaces or changeroots, we can no longer rely on the path to the interpreter to find the preload libs - // also, we compile in the absolute path to AppImageLauncher anyway - // therefore, we can just compile in the paths to the libs as well - // since we also compile in the contents of the preload libraries, this is merely just an optimization so we do not - // have to write and maintain the library files in the majority of all cases (that is, using AppImages regularly - // on some desktop or generally a host system) - // our workflow is: check whether the (right) preload lib exists on the system, otherwise return an empty string - // the creation of a library file needs to be handled by the caller then -#ifdef PRELOAD_LIB_PATH_32BIT + // namespaces or changeroots, we may not find the library there, but we'll at least try + + // we expect the library to be placed next to this binary + const auto own_binary_path = std::filesystem::read_symlink("/proc/self/exe"); + const auto dir_path = own_binary_path.parent_path(); + + std::filesystem::path rv = dir_path; + +#ifdef PRELOAD_LIB_NAME_32BIT if (is_32bit) { - return PRELOAD_LIB_PATH_32BIT; + rv /= PRELOAD_LIB_NAME_32BIT; + return rv; } #endif - return PRELOAD_LIB_PATH; + rv /= PRELOAD_LIB_NAME; + return rv; } /** @@ -174,7 +179,7 @@ class TemporaryPreloadLibFile { private: int _fd; - std::string _path; + std::filesystem::path _path; }; // need to keep track of the subprocess pid in a global variable, as signal handlers in C(++) are simple interrupt @@ -231,15 +236,17 @@ int bypassBinfmtAndRunAppImage(const std::string& appimage_path, const std::vect new_argv.push_back(nullptr); // preload our library - std::string preload_lib_path = find_preload_library(is_32bit_elf(appimage_path)); + auto preload_lib_path = find_preload_library(is_32bit_elf(appimage_path)); + + log_debug("preload lib path: %s\n", preload_lib_path.string().c_str()); // may or may not be used, but must survive until this application terminates std::unique_ptr temporaryPreloadLibFile; - if (access(preload_lib_path.c_str(), F_OK) != 0) { - log_warning("could not find preload library path, creating new temporary file for it\n"); + if (!std::filesystem::exists(preload_lib_path)) { + log_warning("could not find preload library, creating new temporary file for it\n"); -#ifdef PRELOAD_LIB_PATH_32BIT +#ifdef PRELOAD_LIB_NAME_32BIT if (is_32bit_elf(appimage_path)) { temporaryPreloadLibFile = std::make_unique( libbinfmt_bypass_preload_32bit_so, @@ -260,14 +267,17 @@ int bypassBinfmtAndRunAppImage(const std::string& appimage_path, const std::vect preload_lib_path = temporaryPreloadLibFile->path(); } - log_debug("library to preload: %s\n", preload_lib_path.c_str()); - - setenv("LD_PRELOAD", preload_lib_path.c_str(), true); + if (!is_statically_linked_elf(appimage_path)) { + log_debug("library to preload: %s\n", preload_lib_path.string().c_str()); + setenv("LD_PRELOAD", preload_lib_path.c_str(), true); + } // calculate absolute path to AppImage, for use in the preloaded lib char* abs_appimage_path = realpath(appimage_path.c_str(), nullptr); log_debug("absolute AppImage path: %s\n", abs_appimage_path); + // TARGET_APPIMAGE is further needed for static runtimes which do not make any use of LD_PRELOAD setenv("REDIRECT_APPIMAGE", abs_appimage_path, true); + setenv("TARGET_APPIMAGE", abs_appimage_path, true); // launch memfd directly, no path needed log_debug("fexecve(...)\n"); diff --git a/src/binfmt-bypass/preload.c b/src/binfmt-bypass/preload.c index d4cfeac0..2ae61e72 100644 --- a/src/binfmt-bypass/preload.c +++ b/src/binfmt-bypass/preload.c @@ -61,7 +61,7 @@ void __init() { char* __abs_appimage_path() { __init(); - static const char env_var_name[] = "REDIRECT_APPIMAGE"; + static const char env_var_name[] = "TARGET_APPIMAGE"; char* appimage_var = getenv(env_var_name);