Panoptes
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.11"}
]
end
Souffle 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 → findings
The 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