fj auth login: Ctrl+C prints error: interrupted but the process hangs until the OIDC callback deadline #257

Closed
opened 2026-09-06 15:30:58 +00:00 by stephen · 0 comments
Owner

fj auth login on an OIDC deployment prints error: interrupted on Ctrl+C but the process keeps running for up to five minutes before it exits.

→ Opening your browser to sign in to https://fjord.sh
  ...
  Waiting for the sign-in to complete…
^C
error: interrupted
   (hangs; further ^C does nothing)

Cause

cli::run races every command against SIGINT with tokio::select!, so the interrupt is handled correctly: the command future is dropped and main returns.

But LoopbackServer::wait_for_code drives its accept loop from tokio::task::spawn_blocking. Dropping the JoinHandle only detaches the task, it does not cancel it, and dropping the tokio runtime waits for blocking-pool tasks to finish. accept_code polls in 100 ms sleeps until CALLBACK_TIMEOUT (300 s), so the process sits in runtime shutdown for whatever is left of those five minutes after the error line is already on screen.

Reproduced on main (ba0f96a):

sending SIGINT to 95811
RESULT: STILL ALIVE 10s after SIGINT

The --device path is unaffected: poll_for_device_token is ordinary async, so the select cancels it and the process exits immediately. Only the loopback path is affected.

Connections are handled one at a time and the request-line read is unbounded. A socket that connects and then sends nothing (a browser speculative preconnect, a port scanner) blocks read_line forever, so the real redirect queued behind it is never accepted and the sign-in silently fails until the 300 s deadline.

`fj auth login` on an OIDC deployment prints `error: interrupted` on Ctrl+C but the process keeps running for up to five minutes before it exits. ``` → Opening your browser to sign in to https://fjord.sh ... Waiting for the sign-in to complete… ^C error: interrupted (hangs; further ^C does nothing) ``` ## Cause `cli::run` races every command against SIGINT with `tokio::select!`, so the interrupt is handled correctly: the command future is dropped and `main` returns. But `LoopbackServer::wait_for_code` drives its accept loop from `tokio::task::spawn_blocking`. Dropping the `JoinHandle` only detaches the task, it does not cancel it, and dropping the tokio runtime waits for blocking-pool tasks to finish. `accept_code` polls in 100 ms sleeps until `CALLBACK_TIMEOUT` (300 s), so the process sits in runtime shutdown for whatever is left of those five minutes after the error line is already on screen. Reproduced on `main` (ba0f96a): ``` sending SIGINT to 95811 RESULT: STILL ALIVE 10s after SIGINT ``` The `--device` path is unaffected: `poll_for_device_token` is ordinary async, so the select cancels it and the process exits immediately. Only the loopback path is affected. ## Second, related defect in the same loop Connections are handled one at a time and the request-line read is unbounded. A socket that connects and then sends nothing (a browser speculative preconnect, a port scanner) blocks `read_line` forever, so the real redirect queued behind it is never accepted and the sign-in silently fails until the 300 s deadline.
Sign in to join this conversation.
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rasterstate/fj#257
No description provided.