processownership: Detect unjoined child processes in GoSkip to content
Blog
Resources and lifecycle

processownership

Checks that started os/exec commands are waited on or transferred to a wait owner.

Tiercore
Kinddefect

What it detects

Tracks commands successfully started with os/exec and reports return paths that neither wait for the process nor transfer the command to code that owns waiting for it.

Launching a process without waiting is not reported on its own: opening a browser or relaunching a daemon may intentionally outlive the caller. The check reports a command that is waited on or handed on along some return paths but not others. Commands built by helpers or reached through callbacks are not checked.

The full list of precision boundaries, with the fixtures that pin each one, is in the processownership design notes.

Checks

Check Tier Kind What it detects
missing-wait
core defect Reports successfully started commands that are neither waited on nor transferred.

Why this is flagged

Starting a process without waiting for it can leave operating-system resources unreleased and loses the process’s final error or exit status. Waiting gives the child process a complete, observable lifecycle.

Further reading: exec.Cmd.Wait.

How to fix it

Use Run when the program can wait immediately. If it must use Start, make sure every successful start is followed by Wait, and handle the resulting exit status or error.

Examples

Flagged code

func run(ctx context.Context, wait bool) error {
command := exec.CommandContext(ctx, "worker")
// gohawk: started command is not waited on every successful return path
if err := command.Start(); err != nil {
return err
}
if wait {
return command.Wait()
}
return nil
}

Accepted code

func runSafely(ctx context.Context) error {
command := exec.CommandContext(ctx, "worker")
if err := command.Start(); err != nil {
return err
}
return command.Wait()
}