Whole-program BEAM analysis for subtle OTP and supervision bugs. The
package is panoptes — Argus Panoptes, the hundred-eyed watchman — and
its modules are Argus.*.
Argus disassembles compiled .beam files, extracts facts from the bytecode,
and evaluates Souffle Datalog rules to
detect BEAM/OTP-specific anti-patterns: supervision-tree coupling, GenServer
deadlocks, leaked tasks, ETS misuse, atom-table exhaustion, and more.
Installation
def deps do
[
{:panoptes, "~> 0.13"}
]
endSouffle must be installed and
available on your PATH.
To run the analyses over a project, use scry, the Mix compiler built on this library: it runs them incrementally after every compile and reports findings as compiler diagnostics. This package is the engine — the extraction pipeline, the rules, and the in-VM API below.
Approach
Argus is inspired by Doop (JVM), cclyzer++ (LLVM IR), and Gigahorse (EVM), but takes advantage of the BEAM's register-based instruction set to skip the expensive IR-lifting step those frameworks need. BEAM instructions map to Datalog facts directly, with no intermediate representation.
.beam → disassemble → facts (emitter + extractors) → stage 0 call graph → Souffle rules → findingsThe emitter walks every instruction and records the generic facts — instructions, registers, control flow, calls, literals. The extractors read the same bytecode for what the analyses reason about: behaviours, supervision trees, process calls, monitors, ETS, return shapes. A shared call graph is derived once per run, and each analysis is one Souffle program over the facts and a common rule library, producing findings with a severity, a source anchor and a remediation hint.
Analyses
Argus ships 27 BEAM/OTP-specific bug detectors (mix scry --list prints
the same table):
| Analysis | Detects |
|---|---|
atom_safety | atom table exhaustion, unsafe deserialization, and code injection |
call_cycle | module-level synchronous call cycles (deadlocks) |
callback_receive | receive inside an OTP callback, which consumes the behaviour's own mailbox |
coverage | extractor coverage and imprecision (meta-analysis) |
deferred_startup_deadlock | handle_continue deadlocks and crash loops |
distributed | RPC without timeouts, :global races, init blocking on nodes |
error_handling | swallowed errors, ignored results, exit misuse |
ets | ETS table ownership, concurrency options, and lifecycle |
gen_statem | unreachable states and terminal states that never stop |
message_contract | messages a module sends itself but cannot handle |
monitor_leak | monitors left live after a timed wait gave up |
one_for_one_coupling | cross-branch coupling under one_for_one supervisors |
process_bottleneck | synchronous call fan-in (serialization bottlenecks) |
process_registry | duplicate names, whereis races, registry collisions |
purity | @pure contracts checked against the call graph and an effect model |
reply_contract | handle_call clauses that defer a reply they cannot send |
request_surface | dangerous operations reachable from request-handling callbacks |
secret_exposure | schema fields holding secrets that inspect/1 will print |
shutdown_safety | cleanup in terminate/2 that a supervisor shutdown will skip |
supervision | supervision tree structure and anti-patterns |
sync_call_in_init | synchronous calls in init/1 (startup deadlocks) |
timeout_chain | GenServer timeout chains and blocking cast handlers |
tls_verification | TLS connections that do not verify the peer |
transaction_safety | side effects inside a DB transaction that a rollback cannot undo |
unbounded_dynamic_children | unbounded process creation reachable from a request |
unlinked_spawn | unlinked (orphan) process spawns |
unsafe_task | leaked async tasks and unchecked Task.Supervisor.start_child |
Programmatic API
# Every analysis, one extraction: structured findings with severity,
# source anchors and remediation hints.
{:ok, %{findings: findings}} = Argus.run_analyses([MyApp.Supervisor, MyApp.WorkerA])
# One analysis, raw output relations.
{:ok, results} = Argus.analyze([MyApp.Supervisor, MyApp.WorkerA], :supervision)
# Ad-hoc Datalog rules over the same facts.
{:ok, results} = Argus.analyze([MyApp.Worker], {:custom, "path/to/rules.dl"})License
MIT