From cc17f977761b8caf31b81e6fc84f4889b7c34b4b Mon Sep 17 00:00:00 2001 From: probonopd Date: Sat, 19 Sep 2026 22:57:46 +0200 Subject: [PATCH] Make AppImages mount on systems that use defused instead of setuid fusermount3 Distributions that run with NoNewPrivs replace the setuid fusermount3 with defused, which works without the setuid bit. AppImages failed to mount there because the runtime only accepted setuid root fusermount binaries. Now, if no setuid root fusermount is found on the PATH, the runtime falls back to a non-setuid fusermount that identifies itself as defused in its --version output, so AppImages work on such systems while setuid binaries are still preferred and arbitrary non-setuid binaries are still rejected. --- src/runtime/runtime.c | 216 ++++++++++++++++++++++++++++++------------ 1 file changed, 156 insertions(+), 60 deletions(-) diff --git a/src/runtime/runtime.c b/src/runtime/runtime.c index 2ebb3c8..8f6fa26 100644 --- a/src/runtime/runtime.c +++ b/src/runtime/runtime.c @@ -414,7 +414,97 @@ int appimage_print_binary(char* fname, unsigned long offset, unsigned long lengt return 0; } -char* find_fusermount(bool verbose) { +/* Runs " --version" and returns true if it exits successfully. + * If require_defused is set, the output must also identify the binary as defused + * (https://github.com/Skyb0rg007/defused), a fusermount3 replacement which works + * without the setuid bit and hence cannot be recognized by its permissions. */ +static bool fusermount_version_ok(const char* path, bool verbose, bool require_defused) { + int out_pipe[2]; + if (require_defused && pipe(out_pipe) == -1) { + perror("pipe"); + return false; + } + + pid_t pid = fork(); + if (pid == -1) { + perror("fork"); + if (require_defused) { + close(out_pipe[0]); + close(out_pipe[1]); + } + return false; + } + + if (pid == 0) { + // Child process + if (require_defused) { + close(out_pipe[0]); + dup2(out_pipe[1], 1); + close(out_pipe[1]); + } else if (!verbose) { + close(1); + } + + // close stderr if not in verbose mode + if (!verbose) { + close(2); + } + + char* args[] = {(char*) path, "--version", NULL}; + execv(path, args); + // If execv returns, the executable could not be run + _exit(1); + } + + // Parent process + bool is_defused = false; + if (require_defused) { + close(out_pipe[1]); + + char output[256]; + size_t len = 0; + // Read until EOF even when the buffer is full so that the child never blocks on a full pipe + for (;;) { + char chunk[256]; + ssize_t n = read(out_pipe[0], chunk, sizeof(chunk)); + if (n == -1 && errno == EINTR) { + continue; + } + if (n <= 0) { + break; + } + size_t room = sizeof(output) - 1 - len; + size_t take = (size_t) n < room ? (size_t) n : room; + memcpy(output + len, chunk, take); + len += take; + } + output[len] = '\0'; + close(out_pipe[0]); + + if (verbose) { + printf("%s", output); + } + + is_defused = strstr(output, "(defused)") != NULL; + } + + int status; + if (waitpid(pid, &status, 0) == -1) { + perror("waitpid"); + return false; + } + + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + return false; + } + + return !require_defused || is_defused; +} + +/* Searches $PATH for fusermount, fusermount3, ... binaries. + * If want_setuid is set, only setuid root binaries are accepted, otherwise + * only executables which identify themselves as defused. */ +static char* find_fusermount_in_path(bool verbose, bool want_setuid) { char* fusermount_base = "fusermount"; char* fusermount_path = getenv("PATH"); @@ -435,75 +525,68 @@ char* find_fusermount(bool verbose) { struct dirent* entry; while ((entry = readdir(dir_ptr)) != NULL) { // Check if the entry starts with "fusermount" - if (strncmp(entry->d_name, fusermount_base, 10) == 0) { - // Check if the rest of the entry is a digit - char* suffix = entry->d_name + 10; - int j = 0; - while (suffix[j] != '\0' && isdigit(suffix[j])) { - j++; - } + if (strncmp(entry->d_name, fusermount_base, 10) != 0) { + continue; + } - if (suffix[j] == '\0') { - // Construct the full path of the entry - char* fusermount_full_path = malloc(strlen(dir) + strlen(entry->d_name) + 2); - sprintf(fusermount_full_path, "%s/%s", dir, entry->d_name); - - // Check if the binary is setuid root - struct stat sb; - if (stat(fusermount_full_path, &sb) == -1) { - perror("stat"); - free(fusermount_full_path); - continue; - } + // Check if the rest of the entry is a digit + char* suffix = entry->d_name + 10; + int j = 0; + while (suffix[j] != '\0' && isdigit(suffix[j])) { + j++; + } - if (sb.st_uid != 0 || (sb.st_mode & S_ISUID) == 0) { - if (verbose) { - printf("Not setuid root, skipping...\n"); - } - free(fusermount_full_path); - continue; - } + if (suffix[j] != '\0') { + continue; + } - if (verbose) { - printf("Found setuid root executable: %s\n", fusermount_full_path); - } + // Construct the full path of the entry + char* fusermount_full_path = malloc(strlen(dir) + strlen(entry->d_name) + 2); + sprintf(fusermount_full_path, "%s/%s", dir, entry->d_name); - pid_t pid = fork(); - if (pid == -1) { - perror("fork"); - free(fusermount_full_path); - continue; - } + struct stat sb; + if (stat(fusermount_full_path, &sb) == -1) { + perror("stat"); + free(fusermount_full_path); + continue; + } - if (pid == 0) { - // Child process + bool is_setuid_root = sb.st_uid == 0 && (sb.st_mode & S_ISUID) != 0; - // close stdout and stderr if not in verbose mode - if (!verbose) { - close(1); - close(2); - } + if (want_setuid) { + if (!is_setuid_root) { + if (verbose) { + printf("Not setuid root, skipping %s...\n", fusermount_full_path); + } + free(fusermount_full_path); + continue; + } - char* args[] = {fusermount_full_path, "--version", NULL}; - execvp(fusermount_full_path, args); - // If execvp returns, it means the executable was not found - exit(1); - } else { - // Parent process - int status; - waitpid(pid, &status, 0); - - if (WIFEXITED(status) && WEXITSTATUS(status) == 0) { - // The executable was found and executed successfully - closedir(dir_ptr); - free(path_copy); - return fusermount_full_path; - } + if (verbose) { + printf("Found setuid root executable: %s\n", fusermount_full_path); + } + } else { + // setuid root binaries were already rejected by the first pass + if (is_setuid_root || !S_ISREG(sb.st_mode) || access(fusermount_full_path, X_OK) != 0) { + free(fusermount_full_path); + continue; + } - free(fusermount_full_path); - } + if (verbose) { + printf("Checking whether %s is defused...\n", fusermount_full_path); + } + } + + if (fusermount_version_ok(fusermount_full_path, verbose, !want_setuid)) { + if (verbose && !want_setuid) { + printf("Found defused executable: %s\n", fusermount_full_path); } + closedir(dir_ptr); + free(path_copy); + return fusermount_full_path; } + + free(fusermount_full_path); } closedir(dir_ptr); @@ -514,6 +597,19 @@ char* find_fusermount(bool verbose) { return NULL; } +char* find_fusermount(bool verbose) { + char* result = find_fusermount_in_path(verbose, true); + if (result != NULL) { + return result; + } + + // Distributions running with NoNewPrivs replace the setuid fusermount3 by defused + if (verbose) { + printf("No setuid root fusermount found, looking for defused...\n"); + } + return find_fusermount_in_path(verbose, false); +} + /* Exit status to use when launching an AppImage fails. * For applications that assign meanings to exit status codes (e.g. rsync), * we avoid "cluttering" pre-defined exit status codes by using 127 which