Self-calling executables

Lobsters Hottest Tools

Summary

This article explains the concept of self-calling executables, where a program starts another instance of itself, and demonstrates its use in Go testing (running the main function in a subprocess) and in TUI tools (e.g., jjui using SSH_ASKPASS to prompt for passwords via a child process).

<p><a href="https://lobste.rs/s/o78n3y/self_calling_executables">Comments</a></p>
Original Article
View Cached Full Text

Cached at: 06/02/26, 05:36 PM

# Self-calling executables | Olivier's log Source: [https://log.pfad.fr/2026/self-calling-executable/](https://log.pfad.fr/2026/self-calling-executable/) 2 June 2026, inI call the "self\-calling executable" an inception technique, by which a currently running executable starts another version of itself \(directly or indirectly\)\. This technique can be quite useful in testing scenarios as well as in command\-line tools \(such as TUIs\)\. ## Testing the main of an executable Ideally, an executable is correctly structured to allow for isolated testing\. However, it is quite easy \(and common\) to have a less\-than\-optimal architecture \(such as relying on global state\) which can interfere when multiple tests are running in parallel \(or even sequentially\)\. To circumvent this issue in Go, we can start a subprocess\. This subprocess is the currently running test executable itself, but with a specific environment variable set \(e\.g\., TEST\_MAIN\)\. In \`TestMain\(m \*testing\.M\)\` \(a special Go test function\), if this environment variable is detected, no standard tests are run\. Instead, the main command is executed directly\. ``` func TestMain(m *testing.M) { switch os.Getenv("TEST_MAIN") { case "": os.Exit(m.Run()) // standard tests default: os.Exit(cmd.Main()) // run your main } } // runMain can be called inside tests to run the executable as a black-box. func runMain(args ...string) ([]byte, error) { // os.Args[0] points to the current executable cmd := exec.Command(os.Args[0], args...) cmd.Env = append(os.Environ(), "TEST_MAIN=main", ) return cmd.Output() } ``` For other variations of this technique, the following blog post is worth a read: [Mock programs to test os/exec in Go \| Abhinav Gupta](https://abhinavg.net/2022/05/15/hijack-testmain/) ## Interacting back with a main process Another use case for self\-calling executables is interacting with third\-party tools\. For example, the ssh command considers the SSH\_ASKPASS environment variable to locate an executable to run when the user needs to be prompted for a password \(like an SSH\-Key passphrase or a TPM PIN\)\. When a TUI needs to perform an SSH operation, it can set the SSH\_ASKPASS environment variable to point to itself and run ssh as a child process\. When ssh triggers a password prompt, a grand\-child process is started \(the same binary as the TUI\), which connects back to the main TUI process\. The TUI prompts the user for the password and forwards it to the grand\-child process\. Finally, the grand\-child outputs the password to stdout, as expected by ssh\. Let's consider jjui as an example; a TUI for the Jujutsu VCS \(which delegates operations to \`jj git\`, which may interact with ssh\)\. [Pull Request to hijack ssh askpass to jjui](https://github.com/idursun/jjui/pull/423) ### Self\-determination Just like in the test case, the executable must detect at startup whether it was launched as the main process or as a descendant\. Using environment variables is a sensible choice since they easily propagate to grand\-children\. On startup, jjui checks for JJUI\_SSH\_ASKPASS\_ADDR\. If this variable is not set, we are the main process and start the TUI normally\. If the variable is set, we are running as a descendant and switch to the "askpass" behavior: - Connect back to the main process \(see "Family communication" below\)\. - Forward the prompt from ssh to the TUI \(e\.g\., "Enter PIN for 'ssh': "\)\. - Wait for the password from the TUI\. - Output the password to stdout\. - Exit \(preventing the normal TUI startup\)\. ### Family communication Descendants must be able to connect back to the main process\. In jjui, the main process listens on a Unix socket\. To allow multiple instances of jjui to run at the same time, the socket path includes the main process's PID\. The main instance then passes this socket path down via the JJUI\_SSH\_ASKPASS\_ADDR environment variable\. The descendant can then directly connect to this socket upon startup\. In the case of jjui, however, asking the user for a password is a sensitive operation\. To prevent malicious processes with socket access from triggering a prompt, I added a couple of safeguards: - A random JJUI\_SSH\_ASKPASS\_KEY environment variable is generated for each subprocess\. When the descendant connects to the socket, it must send this key\. The main process verifies that the key is attached to a known subprocess\. - After a successful key check, the main process walks up the ancestor \(PID\) chain of the connecting descendant\. If the subprocess PID associated with the key is found in the chain, the prompt is triggered\. Otherwise, the connection is dropped\. Bookkeeping the subprocess PIDs and keys is not trivial, since the key must be generated before launching the subprocess\. Once the subprocess is started, we can then save its PID while accounting for the fact, that the grand\-child might have already dialed the main process in the meantime \(see the subprocess struct in the pull request linked above for a Go example, using channels\)\. Getting the PID of the process on the other side of the Unix socket is done via OS\-specific calls \(e\.g\., in Go using the github\.com/tailscale/peercred package\)\. ## Gotchas While this technique is quite powerful, it is not without drawbacks\. First, it can be quite cumbersome to debug\. If environment variables are not correctly passed around \(or cleared\), you can easily trigger an infinite recursion of processes\. Moreover, if you are using this technique in Go tests with the race detector enabled, note that by default all executions will sleep for 1 second before exiting \(presumably to detect late race conditions\)\. This can be mitigated by setting yet another environment variable: GORACE="atexit\_sleep\_ms=0"\. [Data Race Detector \- go\.dev](https://go.dev/doc/articles/race_detector) In Forgejo's integration tests, the executable is called many times by various Git hooks\. The race detector's default sleep caused the integration tests to time out, after more than 2 hours \(instead of the usual 45 minutes\)\. [Pull Request to self\-execute the Forgejo test binary](https://codeberg.org/forgejo/forgejo/pulls/12855)

Similar Articles

jj jj jj jj jj

Lobsters Hottest

A blog post demonstrates how to fix the error when accidentally typing 'jj jj' in the jj version control system by creating a recursive alias in the jj config file that passes arguments through using 'util exec --'.

What is BusyBox?

Lobsters Hottest

An explanatory article detailing how BusyBox functions as a multi-call binary in Alpine Linux, providing a single executable for various command-line utilities through symlinks and applet configuration.

Adding Go to a browser code runner

Lobsters Hottest

The author details the challenges and solution for adding Go as a language to a browser-based code runner (dailyprog), explaining why the standard GOOS=js approach fails in a V8 isolate and how using GOOS=wasip1 with a minimal WASI host shim succeeds.

Tips to debug hanging Go programs

Michael Stapelberg

A practical guide covering three methods to debug hanging Go programs: using SIGQUIT to print stack traces, attaching the delve debugger, and saving core dumps for later analysis.

Go Experiments Explained

Lobsters Hottest

This article explains how Go handles experimental features, their lifecycle, and examples of recent experiments.