Packages

Whole-program BEAM analysis via incremental Datalog (FlowLog): supervision, GenServer and OTP bug detectors over compiled beams (the Argus modules).

Current section

Files

Jump to
argus_beam README.md
Raw

README.md

# Argus

[![CI](https://github.com/QuinnWilton/argus/actions/workflows/ci.yml/badge.svg)](https://github.com/QuinnWilton/argus/actions/workflows/ci.yml)
[![Hex.pm](https://img.shields.io/hexpm/v/argus_beam.svg)](https://hex.pm/packages/argus_beam)
[![Docs](https://img.shields.io/badge/docs-hexdocs-blue.svg)](https://hexdocs.pm/argus_beam)

Argus finds OTP and concurrency bugs in Elixir, Erlang and Gleam programs:

- supervisor children that depend on each other but restart independently;
- GenServers that deadlock calling each other;
- races on a registered name or an ETS key.

It analyzes compiled `.beam` files and reports findings at the source lines
involved.

![Argus reports a restart dependency between two children of a one_for_one supervisor](https://raw.githubusercontent.com/QuinnWilton/argus/main/images/coupling.png)

## Install and run

Argus runs its analyses on [FlowLog](https://github.com/flowlog-rs/flowlog)
engines: incremental Datalog that keeps its results up to date as your code changes,
so a recompile re-derives only what an edit reached. The Mix integration requires
Elixir 1.19 or later; prebuilt escripts require Erlang/OTP 28.

On macOS and Linux (arm64 or x86_64), a release ships its engines prebuilt: the
first run downloads them once (about 70 MB, checked against the SHA-256 the package
names) and needs no Rust, for argus's own analyses and for rules files of your own,
which run in the generic engine the download carries. Elsewhere, argus builds its
toolchain with [Rust](https://rustup.rs) 1.88 or later, once: a few minutes. Every
program then runs at once in the generic engine, and `mix argus.flowlog build`
compiles engines for argus's own, which solve a large project up to twice as fast.
`ARGUS_FLOWLOG_PREBUILT=0` turns the download off, so argus builds everything itself.
On Linux the prebuilt engines are linked static, so they run on any distribution:
glibc or musl (Alpine), old or new, and NixOS. Where argus builds its own, it needs
Rust and a C compiler (on NixOS, `nix-shell -p cargo rustc gcc`). A download that
fails is tried again an hour later (or at once by `mix argus.flowlog build`), not
at every compile.

`ARGUS_OFFLINE=1` (or `--offline` on `mix argus`, `mix argus.flowlog`, the escript
and `rebar3 argus`) makes no network call: argus uses prebuilt engines only once
they are downloaded, Cargo builds from the crates `~/.cargo` already holds, and
`priors: :live` answers from the cache alone.

Build or fetch the engines ahead of time, in CI for instance, with
`mix argus.flowlog build`, and see what is installed with `mix argus.flowlog status`.
`ARGUS_FLOWLOG_ENGINE=generic` runs every program in the generic engine, and
`compiled` builds an engine for every program, rules files of your own included.
They live in `~/.cache/argus/flowlog` (`ARGUS_FLOWLOG_DIR` names another directory,
`ARGUS_CARGO` another `cargo`); `mix argus.flowlog clean` removes those of earlier
releases.

### Mix

Add Argus to your dependencies:

```elixir
def deps do
  [
    {:argus_beam, "~> 0.23", only: [:dev, :test], runtime: false}
  ]
end
```

`only:` and `runtime: false` keep Argus out of production builds and releases.

Then fetch dependencies and run an analysis. `mix argus` compiles the project first.

```sh
mix deps.get
mix argus                 # configured analyses, or the default set
mix argus coupling ets    # selected analyses
mix argus --all           # all bug-finding analyses
mix argus --list          # available analyses and sets
mix argus --format json   # findings as JSON
mix argus --fail-above 0  # fail CI on any finding
```

To report findings on every compile, append the `:argus` compiler. Editors can
show the findings as compiler diagnostics.

```elixir
def project do
  [
    compilers: Mix.compilers() ++ if(Mix.env() in [:dev, :test], do: [:argus], else: []),
    argus: [analyses: [:default, :ets], severity: [mailbox: :error]]
  ]
end
```

List `:argus` only in the environments that have the dependency: in any other,
such as a `MIX_ENV=prod` release build, Mix stops with "The task compile.argus
could not be found".

By default, error findings fail the compiler run. `mix argus` uses `--fail-above`
to decide whether findings fail the run.

### rebar3

Add this to `rebar.config`, then run `rebar3 argus`. The plugin compiles the
project and downloads the specified Argus escript.

```erlang
{plugins, [rebar3_argus]}.
{argus_plugin, [{version, "0.23.0"}]}.
{argus, [{analyses, [default, ets]}]}.           % optional
{provider_hooks, [{post, [{compile, argus}]}]}.  % optional: run after compilation
```

### Gleam, erlang.mk, or a directory of BEAM files

Install the escript with Mix:

```sh
mix escript.install hex argus_beam
```

Or download a prebuilt escript and its SHA-256 checksum from
[GitHub releases](https://github.com/QuinnWilton/argus/releases). Put `argus` on
your `PATH`.

Build your project, then run:

```sh
argus                                 # current project
argus path/to/project --all
argus --project beams --ebin path/to/ebin
```

The escript reads existing BEAM files and warns about sources newer than their
compiled files. Configuration goes in `argus.config` as Erlang terms. Gleam
findings point to the generated Erlang source.

## Choose analyses

The analyses marked ✓ run by default.

| Analysis | Finds | Default |
|---|---|:---:|
| `startup` | blocking work, deadlocks and races during initialization | ✓ |
| `shutdown` | skipped cleanup and teardown that disrupts a peer | ✓ |
| `coupling` | processes that depend on each other but restart independently | ✓ |
| `structure` | invalid child specs, conflicting registrations and supervision mistakes | ✓ |
| `mailbox` | unhandled messages, missing replies and repeated resource acquisitions | ✓ |
| `failure` | swallowed errors, unchecked results and dropped resources | ✓ |
| `races` | check-then-act races on registered names, ETS keys or Mnesia records | ✓ |
| `blocking` | call cycles, nested waits, bottlenecks and unsafe distributed calls | |
| `state_machine` | unreachable `gen_statem` states and terminal states that never stop | |
| `ets` | table ownership, lifecycle and access-pattern problems | |
| `effects` | violated `@pure` contracts and effects a transaction cannot undo | |
| `unsafe_input` | atom exhaustion, unsafe deserialization and code execution | |
| `exposure` | inspect-visible secrets and TLS connections without peer verification | |

Use analysis names or these sets in `analyses:`:

- `:default`: the analyses marked ✓;
- `:all`: every analysis in the table;
- `:security`: `unsafe_input` and `exposure`;
- `:effects`: `effects`;
- `:otp`: `:all` except `:security` and `:effects`.

The separate `coverage` analysis reports missing analysis information. Request it
explicitly; `--all` excludes it.

`severity:` overrides the level for an analysis or set. See the
[configuration reference](https://hexdocs.pm/argus_beam/Argus.Config.html) for
severity, ignore rules and compiler settings.

## Interpret findings

Argus analyzes your project's code by default. Add `--include-deps` to analyze
dependencies too. Dynamic calls and runtime configuration can leave gaps or
produce findings that do not apply to a particular deployment.

Use the [bug-class catalog](docs/bug-classes.md) to understand each finding's
evidence and limits. Start with the [contributor guide](CONTRIBUTING.md) to capture
a finding, inspect its rules and facts, or run a small extension. The
[analysis model](docs/design/analysis-model.md) and
[rule guide](docs/design/rule-style.md) explain the implementation contracts.

## License

MIT