diff --git a/src/runtime/runtime.c b/src/runtime/runtime.c index a073f60..5ec8f3e 100644 --- a/src/runtime/runtime.c +++ b/src/runtime/runtime.c @@ -56,6 +56,8 @@ extern int sqfs_opt_proc(void* data, const char* arg, int key, struct fuse_args* #include #include #include +#include +#include #include #include #include @@ -588,6 +590,148 @@ extern int fusefs_main(int argc, char* argv[], void (* mounted)(void)); static pid_t fuse_pid; static int keepalive_pipe[2]; +static const char* fuse_mount_path = NULL; + +/* Returns true if path is the mount point or lies below it. */ +static bool +path_in_mount(const char* path, const char* mount) { + size_t len = strlen(mount); + return strncmp(path, mount, len) == 0 && (path[len] == '\0' || path[len] == '/'); +} + +/* Returns true if the process referenced by the /proc/ dir fd uses the mount through its cwd, root, exe, + * open files or memory mappings. */ +static bool +process_uses_mount(int proc_fd, const char* mount) { + char buf[PATH_MAX]; + const char* links[] = {"cwd", "root", "exe"}; + + for (size_t i = 0; i < sizeof(links) / sizeof(links[0]); i++) { + ssize_t n = readlinkat(proc_fd, links[i], buf, sizeof(buf) - 1); + if (n > 0) { + buf[n] = '\0'; + if (path_in_mount(buf, mount)) + return true; + } + } + + int fd_dir_fd = openat(proc_fd, "fd", O_RDONLY | O_DIRECTORY); + if (fd_dir_fd != -1) { + DIR* fds = fdopendir(fd_dir_fd); + if (fds != NULL) { + struct dirent* e; + while ((e = readdir(fds)) != NULL) { + if (e->d_name[0] == '.') + continue; + ssize_t n = readlinkat(fd_dir_fd, e->d_name, buf, sizeof(buf) - 1); + if (n > 0) { + buf[n] = '\0'; + if (path_in_mount(buf, mount)) { + closedir(fds); + return true; + } + } + } + closedir(fds); + } else { + close(fd_dir_fd); + } + } + + int maps_fd = openat(proc_fd, "maps", O_RDONLY); + if (maps_fd != -1) { + FILE* maps = fdopen(maps_fd, "r"); + if (maps != NULL) { + char line[PATH_MAX + 128]; + size_t mlen = strlen(mount); + while (fgets(line, sizeof(line), maps) != NULL) { + char* hit = strstr(line, mount); + if (hit != NULL && (hit[mlen] == '/' || hit[mlen] == '\n' || hit[mlen] == ' ')) { + fclose(maps); + return true; + } + } + fclose(maps); + } else { + close(maps_fd); + } + } + + return false; +} + +/* Returns true if any process other than ourselves still uses the mount. */ +static bool +mount_in_use(const char* mount) { + DIR* proc = opendir("/proc"); + if (proc == NULL) + return false; + + bool in_use = false; + struct dirent* e; + while (!in_use && (e = readdir(proc)) != NULL) { + if (e->d_name[0] < '0' || e->d_name[0] > '9') + continue; + if ((pid_t) atoi(e->d_name) == getpid()) + continue; + int proc_fd = openat(dirfd(proc), e->d_name, O_RDONLY | O_DIRECTORY); + if (proc_fd == -1) + continue; + in_use = process_uses_mount(proc_fd, mount); + close(proc_fd); + } + closedir(proc); + return in_use; +} + +/* While the mount is in use, poll with exponential backoff between these bounds: a running app keeps the mount + * busy for a long time, so there is no point in scanning /proc often, but the cap keeps the reaction time to the + * app's exit short. */ +#define MOUNT_POLL_MIN_USEC (100 * 1000) +#define MOUNT_POLL_MAX_USEC (2 * 1000 * 1000) +/* The mount must stay unused for this long before it is torn down. This bridges the gap in which a launcher + * (e.g. a shell script or sandbox wrapper) has exited and the next process that uses the mount has not started + * yet, and avoids tearing the mount down in the middle of such a handover. */ +#define MOUNT_QUIET_USEC (1000 * 1000) + +static long +elapsed_usec(const struct timespec* since) { + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + return (now.tv_sec - since->tv_sec) * 1000000L + (now.tv_nsec - since->tv_nsec) / 1000L; +} + +/* Blocks until no other process uses the mount for MOUNT_QUIET_USEC. Returns immediately if the mount path cannot + * be resolved, as there is nothing to watch then. */ +static void +wait_until_mount_unused(void) { + char* real_mount = fuse_mount_path != NULL ? realpath(fuse_mount_path, NULL) : NULL; + if (real_mount == NULL) + return; + + useconds_t poll_usec = MOUNT_POLL_MIN_USEC; + struct timespec idle_since; + bool idle = false; + + while (1) { + if (mount_in_use(real_mount)) { + idle = false; + usleep(poll_usec); + poll_usec = poll_usec * 2 > MOUNT_POLL_MAX_USEC ? MOUNT_POLL_MAX_USEC : poll_usec * 2; + } else { + if (!idle) { + clock_gettime(CLOCK_MONOTONIC, &idle_since); + idle = true; + } + if (elapsed_usec(&idle_since) >= MOUNT_QUIET_USEC) + break; + /* Probe quickly while idle so the quiet period is checked with fine granularity. */ + poll_usec = MOUNT_POLL_MIN_USEC; + usleep(poll_usec); + } + } + free(real_mount); +} static void* write_pipe_thread(void* arg) { @@ -599,6 +743,11 @@ write_pipe_thread(void* arg) { /* Write until we block, on broken pipe, exit */ res = write(keepalive_pipe[1], c, sizeof(c)); if (res == -1) { + /* All holders of the read end are gone. Some launchers close inherited file descriptors (e.g. when + * they sandbox the app) while the app is still running from the mount, so only shut down once nothing + * uses the mount any more. Otherwise the app sees "Transport endpoint is not connected". + * See https://github.com/AppImage/type2-runtime/issues/144 */ + wait_until_mount_unused(); kill(fuse_pid, SIGTERM); break; } @@ -1723,6 +1872,8 @@ int main(int argc, char* argv[]) { exit(EXIT_EXECERROR); } + fuse_mount_path = mount_dir; + if (pipe(keepalive_pipe) == -1) { perror("pipe error"); exit(EXIT_EXECERROR);