Skip to main content

Crash diagnostics

The verum binary installs a crash reporter at process start. Every panic and every fatal signal (SIGSEGV, SIGBUS, SIGILL, SIGFPE, SIGABRT on Unix; SetUnhandledExceptionFilter on Windows) produces a structured report on disk with:

  • the command the user ran, its cwd, and a filtered view of the process environment (secret-looking keys redacted);
  • the frozen build identity — verum version, git SHA, build profile, target triple, rustc --version;
  • the compiler phase where the fault occurred, via an RAII breadcrumb trail maintained by the pipeline;
  • a Rust backtrace, which resolves to file:line when the binary ships with DWARF line tables (see profiles).

Reports live under ~/.verum/crashes/ as a matching .log (human) and .json (schema-versioned) pair. The reporter keeps the last 50 by default and rotates older ones out.

The verum diagnose subcommand is the user-facing interface for the report store — list, show, sanitise, bundle, and submit.

Report layout

~/.verum/crashes/
verum-2026-04-19T05-08-22-d6b3-abcdef-0.log ← human-readable
verum-2026-04-19T05-08-22-d6b3-abcdef-0.json ← structured (schema v1)

Filenames sort chronologically (ISO-8601 date with - in place of : for filesystem portability).

.log example

=== Verum crash report ===========================================
Report ID: 1538d-19da2a45011-0
Timestamp: 1776550170641 (unix-ms)
Kind: fatal signal SIGSEGV (11)
Thread: verum-main
Message: received fatal signal SIGSEGV (11)

Build: verum 0.1.0 (release, aarch64-apple-darwin, rustc 1.93.0, git abc1234 clean)
Host: macos aarch64 (16 cores)
PID: 86925
Cwd: /Users/me/projects/demo
Args: verum build ./src/main.vr

Context:
command: build
input: ./src/main.vr

Breadcrumbs (most recent last):
[ 1611ms] compiler.run_native_compilation ./src/main.vr [thread=verum-main]
[ 313ms] compiler.phase.generate_native ./src/main.vr [thread=verum-main]
[ 12ms] compiler.codegen.vbc_to_llvm project [thread=verum-main]

Backtrace:


Environment (filtered):
HOME=/Users/me
LANG=en_US.UTF-8
TMPDIR=/Users/me/.tmp
RUST_BACKTRACE=1
===================================================================

.json example

{
"schema_version": 1,
"report_id": "1538d-19da2a45011-0",
"timestamp_ms": 1776550170641,
"kind": { "type": "signal", "name": "SIGSEGV", "signo": 11 },
"message": "received fatal signal SIGSEGV (11)",
"location": null,
"backtrace": "…",
"thread_name": "verum-main",
"breadcrumbs": [
{ "phase": "compiler.run_native_compilation", "detail": "./src/main.vr", "thread": "verum-main", "age_ms": 1611 },
{ "phase": "compiler.phase.generate_native", "detail": "./src/main.vr", "thread": "verum-main", "age_ms": 313 }
],
"context": { "command": "build", "input_file": "./src/main.vr" },
"environment": {
"verum_version": "0.1.0",
"build_profile": "release",
"build_target": "aarch64-apple-darwin",
"build_rustc": "rustc 1.93.0",
"build_git_sha": "abc1234",
"build_git_dirty": "clean",
"os": "macos",
"arch": "aarch64",
"cpu_cores": 16,
"pid": 86925,
"cwd": "/Users/me/projects/demo",
"argv": ["verum", "build", "./src/main.vr"],
"env": { "HOME": "/Users/me", "RUST_BACKTRACE": "1",}
}
}

The pipeline instruments its major phases with RAII breadcrumbs — the trail of phase names and per-phase details leading up to the crash. Typical phases:

PhaseWhen
compiler.run_native_compilationAOT build driver
compiler.phase.stdlib_loadingembedded stdlib
compiler.phase.project_modulessibling modules
compiler.phase.load_source / .parsefront-end
compiler.phase.type_checktype inference
compiler.phase.verifyrefinement / SMT
compiler.phase.cbgr_analysisCBGR tier analysis
compiler.phase.ffi_validationFFI boundary checks
compiler.phase.rayon_fencewait for rayon workers before LLVM
compiler.phase.generate_nativeLLVM codegen
compiler.codegen.vbc_to_llvminner VBC → LLVM lowering
compiler.phase.interpretTier 0 (VBC interpreter)

Third-party code that embeds verum_compiler / verum_vbc can push its own breadcrumbs by acquiring a scoped guard from verum_error.breadcrumb.enter("mytool.stage", file_path). The guard is automatically popped on scope exit, so a breadcrumb cannot outlive its surrounding work.

The trail is bounded (64 entries) and mirrored to a cross-thread snapshot so the signal handler can include it even when the offending thread's TLS is unreachable.

Sensitive data

The reporter is conservative about what it captures:

  • argv, cwd, and the breadcrumb details are preserved verbatim in the on-disk report. They are intended for the developer who ran the build — the report is local, not uploaded.
  • Environment variables are filtered. Only VERUM_*, RUST*, CARGO*, and a curated whitelist (HOME, USER, LANG, TERM, TMPDIR, LLVM_*, etc.) survive. Anything whose name contains PASSWORD, SECRET, TOKEN, APIKEY, PRIVATE, SESSION, COOKIE, CREDENTIAL, AUTH, or PASSPHRASE is replaced with <redacted> even if the name itself is whitelisted.

When sharing a report externally, use --scrub-paths to replace $HOME with ~ and the current username with <user> in the emitted output. The originals on disk are not modified.

The verum diagnose command

verum diagnose <subcommand> [options]

list

List recent reports in ~/.verum/crashes/, newest first, with a one-line summary of each (kind, message, build, last known phase).

verum diagnose list # last 20
verum diagnose list --limit 50 # widen the window

show

Print the full report to stdout. Defaults to the most recent.

verum diagnose show # newest .log
verum diagnose show path/to/report.log
verum diagnose show --json # structured form
verum diagnose show --scrub-paths # safe-to-share render

bundle

Pack recent reports (both the .log and the .json) into a single .tar.gz suitable for attaching to an issue. A README inside the archive explains where to upload it.

verum diagnose bundle # last 5 → ./verum-crash-bundle-<ts>.tar.gz
verum diagnose bundle --recent 3 -o report.tgz
verum diagnose bundle --scrub-paths # sanitise every file in the archive

--scrub-paths rewrites each bundled file — the originals under ~/.verum/crashes are untouched.

submit

Open a new GitHub issue via the gh CLI. Paths are always scrubbed before upload; the .tar.gz path is printed for the user to attach manually (the gh CLI does not accept attachments at issue creation time).

verum diagnose submit # verum-lang/verum
verum diagnose submit --repo my/fork --recent 3
verum diagnose submit --dry-run # print the gh invocation

Requires gh auth login.

env

Print the build/host environment snapshot that the reporter captured at install time — useful when diagnosing "which verum am I running" questions without needing a crash.

verum diagnose env
verum diagnose env --json

clean

Delete every report in ~/.verum/crashes/.

verum diagnose clean # prompts for confirmation
verum diagnose clean --yes # unattended

Debug-info profile

The primary [profile.release] stays stripped for binary size and runtime stability (keeping DWARF in release re-introduces an LLVM pass-registration race on macOS — see the note below). A dedicated profile keeps line tables so crash-report backtraces resolve to file:line:

cargo build --profile release-debug-tables --bin verum

This produces target/release-debug-tables/verum plus an external .dSYM (macOS) or .dwp (Linux) bundle next to the binary. The main binary size is unchanged; the extra data lives in the bundle, which the backtrace crate consults automatically when resolving frames.

Ship the debug-tables build to users who are triaging a reported bug; keep the primary release build on production paths. Do not fold debug = "line-tables-only" into [profile.release] — the extra DWARF-emitter passes expand the lazy-init surface that races rayon worker wake paths, re-introducing a ~70 % SIGSEGV rate in the phase_generate_native codegen step.

Chaining your own panic hook

crash.install chains into whatever hook was set before it. If you need custom panic metrics in addition to the crash report, install your own hook first and then call verum_error.crash.install(...) — the crash reporter will defer to the previously installed hook before doing its own work.

The Verum CLI itself does not install the stock PanicLogger from verum_error.panic_handler by default. Benchmarking showed the extra hook measurably destabilised release builds on the codegen race path; the structured report produced by the crash reporter already contains everything PanicLogger would record, plus the breadcrumb trail and environment snapshot.

Signal-safety caveats

The reporter is best-effort async-signal-safe, not strictly so. It:

  • installs on an alternate signal stack (sigaltstack) so a stack overflow still reaches the handler;
  • pre-creates the report directory at install time;
  • uses the backtrace crate to capture frames from the signal path (pragmatic choice — not strictly sig-safe but works in practice for dev tools);
  • re-raises the original signal after writing the report so the kernel still produces a core dump if ulimit -c allows.

A hard fault may leave the global allocator poisoned; in that case the JSON write may fail and only the short stderr notice survives. That notice still includes the report ID so a subsequent run can correlate the two events.

Configuration

All of the above is controlled by verum_error.crash.CrashReporterConfig:

FieldDefaultWhat it controls
app_name"verum"Verbatim brand string surfaced in five places: the === {Titlecased} crash report === log header (first letter title-cased automatically), the Build: {app_name} {ver} (...) line of the human report, the app_name field of the JSON envelope's environment block, the {app_name}: internal compiler error... stderr prefix, and the run \{app_name} diagnose bundle`` hint shown after a crash. Embedders override this to rebrand every surface in one place.
app_versionenv!("CARGO_PKG_VERSION")Mirrored into EnvSnapshot.verum_version (kept under that name for schema stability) and rendered in the human report's Build line.
report_dir~/.verum/crashes/$HOME-relative; created at install time so the signal handler doesn't have to.
retention50Older reports rotated off after every successful write — the rotator deletes oldest first by mtime.
capture_backtracetrueAlso forces RUST_BACKTRACE=1 so the symbolizer captures frames even if the user hasn't set the env var.
install_signal_handlerstrueUnix: SIGSEGV / SIGBUS / SIGILL / SIGFPE / SIGABRT on an alternate signal stack via sigaltstack. Windows: SetUnhandledExceptionFilter. Set to false if a host process owns its own handler.
redact_sensitive_envtrueWhen true, env vars whose name matches PASSWORD/SECRET/TOKEN/APIKEY/PRIVATE/SESSION/COOKIE/CREDENTIAL/AUTH/PASSPHRASE (case-insensitive substring) render as <redacted> even when they pass the keep-list. Set to false only in dev environments where you need the raw env.
issue_tracker_urlhttps://github.com/verum-lang/verum/issues/newRendered verbatim under the "Please file an issue at:" line on crash. Embedders point this at their own tracker.

Downstream tools that embed the compiler should install with an app_name + issue_tracker_url appropriate to them so their crash surfaces point users at the right bug tracker. The app_name flows through every user-facing rendering surface — log header, build line, JSON envelope, stderr branding, diagnose-bundle hint — so a single override rebrands the whole reporter without touching any rendering code:

In practice an embedder constructs a CrashReporterConfig, overrides app_name and issue_tracker_url to its own identifiers, leaves the remaining fields at their defaults, and calls verum_error.crash.install(cfg).

The header title-cases the first letter, so myapp renders as Myapp. The lowercased app_name is used verbatim in the stderr prefix and the diagnose bundle hint, matching the shell convention of lowercase tool names.

Render configuration knobs

Embedders that drive verum_diagnostics::Renderer directly control output shape via RenderConfig. Every documented field is honoured by the consumer (no inert defenses):

FieldDefaultWhat it gates
colorstrueANSI escape codes around severity / spans.
context_lines2Lines shown above / below each labeled line.
show_line_numberstrueWhen false, the gutter pads with spaces of the same width so the pipe alignment stays consistent for downstream parsers.
max_line_width120Caps source-line rendering. Lines longer than the cap render with an ellipsis suffix (UTF-8-safe char-count slicing). 0 disables truncation. Bounds the diagnostic size on minified-source workloads.
show_sourcetrueMaster switch on the file-snippet block.
show_suggestionstrueToggles the inline fix-it suggestions.
show_doc_urlstrueToggles verum-lang.org/errors/<code> URL footers.
unicode_outputtrueBox-drawing + arrow glyphs vs ASCII fallback.
terminal_width80Read mirror via Renderer::terminal_width(). The renderer itself doesn't soft-wrap (terminals do that for free); the value is exposed so external composers (LSP servers framing into JSON-RPC, CI pretty-printers) can consult the configured stance.
relative_pathsfalseWhen true, the file-snippet header strips the CWD prefix from absolute paths. Falls back to the original path on any failure (CWD lookup error, different filesystem mount, path is not a child of CWD).

See also