Skip to content

functions serve: SIGTERM to a stale process kills unrelated processes via kill(-pid) on reused child PIDs #6858

Description

@rofreytag

Affected area

functions serve

Supabase CLI version

2.111.0 (darwin-arm64, npm package @supabase/cli-darwin-arm64)

Operating system

macOS 15 (arm64), Docker via OrbStack

Command

supabase functions serve my-function (left running in the background)

Actual behavior

Sending SIGTERM to a long-running functions serve process killed unrelated processes on the machine: Postgres.app, an IDE, Dropbox and Grammarly helpers, a system widget, several terminal sessions, and every Chrome helper process (all tabs died). The unified log attributes each one to the CLI:

launchd: application.com.postgresapp.Postgres2 [13084] exited due to SIGTERM | sent by supabase[77586]
launchd: com.grammarly.ProjectLlama.Shepherd [11055]  exited due to SIGTERM | sent by supabase[77586]
launchd: com.getdropbox.dropbox.dbkextd [36026]       exited due to SIGTERM | sent by supabase[77586]
launchd: com.apple.weather.widget [9741]              exited due to SIGTERM | sent by supabase[77586]

All kills happened within 100 ms of the SIGTERM.

What I think is happening

  • serve.ts follows container logs in an endless for (;;) loop: docker logs -f is spawned, and when it fails with a retriable error (no such container, conflict, ...) it sleeps 400 ms and spawns again. Every spawn happens in the same Effect scope, so the child handles are never released until the whole scope closes.
  • The Node/Bun child-process spawner starts each child with detached: true (own process group). Its scope release for a child that already exited non-zero calls process.kill(-pid, "SIGTERM").
  • In my case another terminal ran supabase stop && supabase start while functions serve was running. The serve kept looping on the old container ID for hours (about one docker spawn per second, all exiting non-zero).
  • The serve command itself finished a day later (the trace shows the functions.serve span ending) but the process never exited. It sat for five more days.
  • On SIGTERM the scope finally closed and thousands of accumulated releases each ran kill(-pid) against a PID number that had long since been reused. Every process-group leader started after the loop began that happened to land on one of those PIDs was signalled.

Only processes that predated the serve survived, which matches PID reuse exactly.

Expected behavior

  • A release for a child that has already exited must not send any signal. Killing by -pid after exit can only ever hit a stranger.
  • Each docker logs -f retry should be scoped per iteration so handles are released as they exit.
  • The process should exit when the command completes instead of hanging.

Steps to reproduce

  1. supabase start, then supabase functions serve my-function &.
  2. In another shell: supabase stop && supabase start.
  3. Leave it for a long time (hours to days) so the PID counter wraps.
  4. kill -TERM <serve pid>.
  5. Check log show --last 5m --predicate 'eventMessage CONTAINS "sent by supabase"'.

Related

Activity

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions