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_dirNoneUnset means default_report_dir(): ~/.verum/crashes/ when HOME (or USERPROFILE) resolves, and $TMPDIR/verum-crashes when it does not — a headless CI container hits the second. 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​