Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 151 additions & 0 deletions src/runtime/runtime.c
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ extern int sqfs_opt_proc(void* data, const char* arg, int key, struct fuse_args*
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <stdbool.h>
#include <time.h>
#include <errno.h>
#include <sys/wait.h>
#include <fnmatch.h>
Expand Down Expand Up @@ -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/<pid> 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) {
Expand All @@ -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;
}
Expand Down Expand Up @@ -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);
Expand Down
Loading