Argus.Analyses.CallbackReceive (Panoptes v0.13.0)

Copy Markdown View Source

Bare receive inside an OTP callback.

An OTP process is already sitting in a receive loop owned by its behaviour module. A receive written inside a callback runs inside that loop and selectively consumes from the same mailbox, which breaks three things at once:

  1. It steals messages the behaviour needs. {:system, _, _} is how :sys.get_state/1, :sys.suspend/1, :sys.replace_state/2 and the whole debug and trace surface work; {:EXIT, _, _} is how a trapping process learns a link died; {:DOWN, ...} is every monitor in flight. A receive with a catch-all clause eats them.
  2. It reorders delivery. Messages the callback does not match stay in the queue and are re-scanned on every later receive — the selective receive cliff, quadratic in queue length.
  3. Without an after, it can block forever. The process stops answering its supervisor, so shutdown waits out the child's timeout and then brutal-kills it, turning a graceful stop into a lost buffer.

There is no compiler or dialyzer diagnostic for this, and it survives review because the receive usually looks locally reasonable.

Precision

call_edge deliberately treats closure construction as a call so that reachability follows execution into lambdas passed to Enum.map, Task.async and friends. That is right in general and wrong here: a receive inside spawn(fn -> ... end) runs in the spawned process, not the callback's. Closure edges are subtracted, so what remains is control that stays on this process's stack.