Skip to content

Conversation

Copy link

Copilot AI commented Nov 8, 2025

Fix for AppImage orphan process surviving main process exit

Problem:
An orphaned FUSE daemon process survives after the main AppImage process exits, particularly in non-interactive environments like systemd services. The daemon stays alive indefinitely in sleep state, reading from /dev/fuse.

Root Cause:
The keepalive pipe read end (keepalive_pipe[0]) was not being closed at the right time. If closed before exec, the FUSE daemon would terminate before AppRun starts (issue #119). If inherited by AppRun without proper cleanup, the pipe might not break when AppRun exits.

Solution:
Fork before executing AppRun:

  1. Child process closes both pipe ends and execs AppRun
  2. Parent process waits for AppRun to finish
  3. Parent closes keepalive_pipe[0] after AppRun exits
  4. FUSE daemon's write_pipe_thread detects the broken pipe
  5. FUSE daemon sends itself SIGTERM and cleans up
  6. Parent exits with the same status as AppRun

Benefits:

  • FUSE mount stays alive while AppRun is running (avoids Crash on sleep #119)
  • Keepalive pipe breaks immediately after AppRun exits
  • FUSE daemon cleans up properly in all environments
  • Exit status is preserved correctly (both normal exits and signals)
  • Works in systemd and other non-interactive environments

Plan:

  • Analyze the codebase and understand the issue
  • Understand the concern about issue Crash on sleep #119
  • Implement fork-before-exec approach
  • Verify the execution flow and logic
  • Address code review feedback:
    • Close both pipe ends in child before exec
    • Check waitpid return value and handle errors
    • Fix memory leak by freeing real_argv before exit
  • Build and test the runtime to verify the fix
  • Run final code review
  • Run security checks with codeql_checker

Changes Made:

  • Added fork() before execv() to AppRun (commit 2442c30)
  • Child process closes both pipe ends before exec
  • Child process execs AppRun
  • Parent process checks waitpid return value for errors
  • Parent process waits for child and closes pipe after child exits
  • Parent frees allocated memory before exit
  • Parent exits with child's exit status (preserving return codes and signals)

Security Summary:
All review feedback addressed:

  • Proper resource cleanup (close pipe fds, free memory)
  • Error checking on system calls (waitpid)
  • No file descriptor leaks to child process
Original prompt

This section details on the original issue you should resolve

<issue_title>AppImage orphan survives main process exit</issue_title>
<issue_description>I am seeing an orphaned process get left behind under some conditions after main process exits. This orphaned process seems to stay around forever in sleep state until reboot. I originally noticed this when appimage was run by a process running as a systemd service similar to #99 so I have put together the following example without systemd.

Running as root to demonstrate that it should not be a permissions issue.

Using following as entrypoint from appimage wrapper:

root@host:~# cat test.py
#!/usr/bin/env python3

print("Hello world!")

Running with strace to trace processes. We can see that main process forks pid 30812 (which I believe is this fork call) which then forks again with pid 30813. Main process then eventually execs our "Hello world!" script which exits properly. However we can see that a detached process is left behind which is pid 30813.

root@host:~# strace -f -e waitpid,fork,execve bazel-bin/test.AppImage
execve("bazel-bin/test.AppImage", ["bazel-bin/test.AppImage"], 0x7ffe197ee268 /* 21 vars */) = 0
fork()                                  = 30812
strace: Process 30812 attached
[pid 30812] fork()                      = 30813
strace: Process 30813 attached
[pid 30812] +++ exited with 0 +++
[pid 30811] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=30812, si_uid=0, si_status=0, si_utime=0, si_stime=0} ---
strace: Process 30815 attached
[pid 30811] execve("/tmp/.mount_test.AALCfdi/AppRun", ["bazel-bin/test.AppImage"], 0x7ff6783cc440 /* 25 vars */) = 0
strace: Process 30816 attached
[pid 30816] execve("/usr/bin/dirname", ["dirname", "/tmp/.mount_test.AALCfdi/AppRun"], 0x558cc611ea28 /* 26 vars */) = 0
[pid 30816] +++ exited with 0 +++
[pid 30811] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=30816, si_uid=0, si_status=0, si_utime=0, si_stime=0} ---
strace: Process 30817 attached
[pid 30817] execve("/usr/bin/dirname", ["dirname", "/tmp/.mount_test.AALCfdi/test.ru"...], 0x558cc611ea98 /* 26 vars */) = 0
[pid 30817] +++ exited with 0 +++
[pid 30811] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=30817, si_uid=0, si_status=0, si_utime=0, si_stime=0} ---
[pid 30811] execve("./test", ["./test"], 0x558cc5034bc8 /* 28 vars */) = 0
[pid 30811] execve("/usr/local/sbin/python3", ["python3", "./test"], 0x7ffd39964728 /* 28 vars */) = -1 ENOENT (No such file or directory)
[pid 30811] execve("/usr/local/bin/python3", ["python3", "./test"], 0x7ffd39964728 /* 28 vars */) = -1 ENOENT (No such file or directory)
[pid 30811] execve("/usr/sbin/python3", ["python3", "./test"], 0x7ffd39964728 /* 28 vars */) = -1 ENOENT (No such file or directory)
[pid 30811] execve("/usr/bin/python3", ["python3", "./test"], 0x7ffd39964728 /* 28 vars */) = 0
strace: Process 30818 attached
[pid 30818] execve("/usr/local/sbin/uname", ["uname", "-p"], 0x7ffee55c89b0 /* 28 vars */) = -1 ENOENT (No such file or directory)
[pid 30818] execve("/usr/local/bin/uname", ["uname", "-p"], 0x7ffee55c89b0 /* 28 vars */) = -1 ENOENT (No such file or directory)
[pid 30818] execve("/usr/sbin/uname", ["uname", "-p"], 0x7ffee55c89b0 /* 28 vars */) = -1 ENOENT (No such file or directory)
[pid 30818] execve("/usr/bin/uname", ["uname", "-p"], 0x7ffee55c89b0 /* 28 vars */) = 0
[pid 30818] +++ exited with 0 +++
[pid 30811] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=30818, si_uid=0, si_status=0, si_utime=0, si_stime=0} ---
[pid 30811] execve("/tmp/.mount_test.AALCfdi/test.runfiles/python_3_9_x86_64-unknown-linux-gnu/python_wrapper.sh", ["/tmp/.mount_test.AALCfdi/test.ru"..., "/tmp/.mount_test.AALCfdi/test.ru"...], 0x1afd4c0 /* 30 vars */) = 0
strace: Process 30819 attached
[pid 30819] execve("/usr/bin/dirname", ["dirname", "/tmp/.mount_test.AALCfdi/test.ru"...], 0x556d655cd510 /* 30 vars */) = 0
[pid 30819] +++ exited with 0 +++
[pid 30811] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=30819, si_uid=0, si_status=0, si_utime=0, si_stime=0} ---
strace: Process 30820 attached
[pid 30820] execve("/tmp/.mount_test.AALCfdi/test.runfiles/python_3_9_x86_64-unknown-linux-gnu/bin/python3", ["/tmp/.mount_test.AALCfdi/test.ru"..., "-s", "/tmp/.mount_test.AALCfdi/test.ru"...], 0x556d655cd510 /* 30 vars */) = 0
Hello world!
[pid 30820] +++ exited with 0 +++
[pid 30811] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=30820, si_uid=0, si_status=0, si_utime=2, si_stime=1} ---
[pid 30811] +++ exited with 0 +++
[pid 30815] --- SIGPIPE {si_signo=SIGPIPE, si_code=SI_USER, si_pid=30813, si_uid=0} ---
[pid 30815] --- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=30813, si_uid=0} ---
[pid 30815] +++ exited with 0 +++


^Cstrace: Process 30813 detached

30813 appears to be in s...


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 3 commits November 8, 2025 10:11
Co-authored-by: probonopd <2480569+probonopd@users.noreply.github.com>
Co-authored-by: probonopd <2480569+probonopd@users.noreply.github.com>
Co-authored-by: probonopd <2480569+probonopd@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix orphaned process after main process exits Fix orphaned FUSE daemon process after AppImage exit Nov 8, 2025
Copilot AI requested a review from probonopd November 8, 2025 10:16
Copilot finished work on behalf of probonopd November 8, 2025 10:16
@probonopd probonopd marked this pull request as ready for review November 8, 2025 10:39
@probonopd
Copy link
Member

@copilot wouldn't we run into the same issue as #119 with this? Shouldn't we close the pipe AFTER AppRun has exited?

Co-authored-by: probonopd <2480569+probonopd@users.noreply.github.com>
Copy link
Author

Copilot AI commented Nov 8, 2025

@copilot wouldn't we run into the same issue as #119 with this? Shouldn't we close the pipe AFTER AppRun has exited?

You're absolutely right. The previous approach would have caused the FUSE daemon to terminate before AppRun started, which is the same issue as #119.

I've changed the implementation to fork before exec (commit 2442c30). Now:

  • Child process execs AppRun
  • Parent waits for AppRun to finish
  • Parent closes the keepalive pipe after AppRun exits
  • FUSE daemon receives SIGTERM only after AppRun completes

This keeps the FUSE mount alive during AppRun's execution while ensuring proper cleanup afterward.

Copilot finished work on behalf of probonopd November 8, 2025 10:48
@probonopd probonopd requested a review from Copilot November 8, 2025 11:09
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR modifies the AppImage runtime to properly manage the lifecycle of the FUSE daemon by forking before executing AppRun. The change ensures that the keepalive pipe can be closed after AppRun exits, which signals the FUSE daemon to terminate.

Key changes:

  • Implements fork/exec pattern to launch AppRun in a child process while parent waits
  • Captures and propagates AppRun's exit status correctly (normal exit, signal termination)
  • Closes the keepalive pipe after AppRun exits to trigger FUSE daemon cleanup

Reviewed Changes

Copilot reviewed 1 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/runtime/runtime.c Implements fork/exec pattern for AppRun execution with proper exit status handling and keepalive pipe management
.gitignore Adds CodeQL detected source root to ignored files

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

exit(EXIT_EXECERROR);
}

if (apprun_pid == 0) {
Copy link

Copilot AI Nov 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The child process inherits both ends of the keepalive pipe but doesn't use them. Both pipe ends should be closed before execv() to prevent the child from holding unnecessary file descriptors. Add close(keepalive_pipe[0]); and close(keepalive_pipe[1]); before the execv call on line 1858.

Suggested change
if (apprun_pid == 0) {
if (apprun_pid == 0) {
/* Child process - close keepalive pipe before exec */
close(keepalive_pipe[0]);
close(keepalive_pipe[1]);

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like an obvious oversight that was there before. I am just a little concerned that the pipe loses its usefulness when it is closed here already. (Yes, technically the parent process would keep its side open anyway).

This needs to be tested thoroughly. The nature of the bug requires a quite sophisticated setup, though, which is time consuming.

} else {
/* Parent process - wait for AppRun to finish, then close pipe */
int status;
waitpid(apprun_pid, &status, 0);
Copy link

Copilot AI Nov 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return value of waitpid() is not checked. If waitpid() fails (returns -1), the status variable will be uninitialized, leading to undefined behavior in the subsequent WIFEXITED/WIFSIGNALED checks. Check the return value and handle errors appropriately.

Suggested change
waitpid(apprun_pid, &status, 0);
pid_t waited_pid = waitpid(apprun_pid, &status, 0);
if (waited_pid == -1) {
perror("waitpid error");
close(keepalive_pipe[0]);
exit(EXIT_EXECERROR);
}

Copilot uses AI. Check for mistakes.
Comment on lines +1871 to +1877
if (WIFEXITED(status)) {
exit(WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
/* Child was killed by a signal, exit with 128 + signal number */
exit(128 + WTERMSIG(status));
} else {
exit(EXIT_EXECERROR);
Copy link

Copilot AI Nov 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The memory allocated for real_argv on line 1807 is never freed before these exit calls, causing a memory leak. While the process is exiting anyway, it's better practice to free(real_argv) before the exit calls or restructure to ensure cleanup happens.

Copilot uses AI. Check for mistakes.
@probonopd
Copy link
Member

@copilot address all points raised in the review above

…leak

Co-authored-by: probonopd <2480569+probonopd@users.noreply.github.com>
Copy link
Author

Copilot AI commented Nov 8, 2025

@copilot address all points raised in the review above

All review points addressed in commit 876881d:

  1. Child process now closes both pipe ends (keepalive_pipe[0] and keepalive_pipe[1]) before exec to prevent unnecessary file descriptor inheritance
  2. waitpid() return value is checked - errors are handled with proper cleanup (close pipe, free memory, exit with error)
  3. Memory leak fixed - real_argv is freed before all exit paths in the parent process

Copilot finished work on behalf of probonopd November 8, 2025 11:16
@probonopd probonopd requested a review from Copilot November 8, 2025 11:26
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 1 out of 2 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

probonopd and others added 2 commits November 8, 2025 12:59
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@github-actions
Copy link

github-actions bot commented Nov 8, 2025

Build for testing:
artifacts x86_64
artifacts i686
artifacts aarch64
artifacts armhf
Use at your own risk.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AppImage orphan survives main process exit

3 participants