Runs a C or C++ program with CoreTrace instrumentation and reports the memory errors it hits as findings with a source location, as text or as a SARIF log.
runtime-analyzer compiles the program through
coretrace-compiler's library API with
instrumentation enabled, runs the instrumented binary with a timeout and its stdin on /dev/null,
captures its output, and turns each report of the CoreTrace runtime into a finding.
| Rule | CWE | Locations |
|---|---|---|
heap-buffer-overflow |
CWE-122 (write), CWE-125 (read) | faulting access, allocation site |
stack-buffer-overflow |
CWE-121 (write), CWE-125 (read) | faulting access, the object's function |
heap-use-after-free |
CWE-416 | faulting access, allocation site |
double-free |
CWE-415 | second free, allocation site |
memory-leak |
CWE-401, warning | allocation site |
vtable-null-this |
CWE-476 | virtual call site |
vtable-use-after-free |
CWE-416 | virtual call site |
vtable-corrupted |
CWE-843 | virtual call site |
vcall-invalid-target |
CWE-843 | virtual call site |
vtable-type-mismatch |
CWE-843, warning | virtual call site |
vtable-diagnostic |
CWE-843, warning | virtual call site |
The memory rules come from the alloc and bounds modules, which the default module set
enables. The vtable rules come from the vtable module's diagnostics
(--ct-modules=alloc,vtable --ct-vtable-diag): each box the runtime logs at WARN level is one
finding, its message listing the box's warnings and its rule the most specific one. A box with
no more specific warning is vtable-diagnostic: missing typeinfo alone, which a valid program
built with -fno-rtti produces, is one. Boxes logged at INFO level are tracing and produce no
finding.
Sites carry the path the source was compiled from, so run the analyzer from the directory the static analyzers report paths from.
Each release carries a
self-contained Linux archive per architecture, runtime-analyzer-<version>-linux-amd64.tar.gz
and runtime-analyzer-<version>-linux-arm64.tar.gz, each with its .sha256:
sha256sum -c runtime-analyzer-v0.1.0-linux-amd64.tar.gz.sha256
tar -xzf runtime-analyzer-v0.1.0-linux-amd64.tar.gz
./runtime-analyzer-v0.1.0-linux-amd64/bin/runtime-analyzer --versionThe tree holds bin/runtime-analyzer, the LLVM libraries it links and the instrumentation
runtime archives in lib/, Clang's own headers in lib/clang/<version>/include, and the
library with its headers. Everything is found relative to the CLI's own location, so the tree can
be moved. No clang and no LLVM have to be installed: the CLI points coretrace-compiler at the
shipped headers unless CT_CLANG names another clang.
The archive is built on Ubuntu 22.04 and runs on it and on every newer distribution (Debian 12,
RHEL 9 and rebuilds, Ubuntu 24.04...). What stays the system's is a C++ build environment to
link each instrumented program against the runtime, which is C++: g++ on Debian and Ubuntu,
gcc-c++ on RHEL, as for building any C++ program.
Needs CMake 3.21 or later, a C++20 compiler, LLVM and Clang 19 or later with their CMake
packages, and Python 3, which the test suite runs (configuration fails without it).
coretrace-compiler is fetched at configure time, at the release tag pinned in CMakeLists.txt.
# macOS: brew install llvm@20. Debian/Ubuntu: /usr/lib/llvm-20/lib/cmake/{llvm,clang}.
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \
-DLLVM_DIR=$(brew --prefix llvm@20)/lib/cmake/llvm \
-DClang_DIR=$(brew --prefix llvm@20)/lib/cmake/clang
cmake --build build --parallel
cmake --install build --prefix /opt/coretrace # optionalConfiguration fails against an LLVM older than LLVM_MIN_REQUIRED_VERSION (19).
runtime-analyzer [options] -- <compiler args>Arguments before -- belong to runtime-analyzer. Arguments after -- are forwarded to
coretrace-compiler and then to Clang. The analyzer enables instrumentation through the
compilerlib::compile(..., instrument=true) API, so --instrument is optional and ignored if it
is present after --.
test/findings/heap_overflow_write.c writes one element past a malloc'd array of four:
int main(void)
{
int* values = malloc(4 * sizeof(int));
values[4] = 1;
free(values);
return 0;
}$ cd test
$ runtime-analyzer -- --ct-modules=alloc,bounds findings/heap_overflow_write.c
runtime-analyzer: binary=runtime-analyzer.out
runtime-analyzer: exit_code=134
runtime-analyzer: timed_out=0
runtime-analyzer: findings=1
findings/heap_overflow_write.c:7:15: error: heap-buffer-overflow WRITE of size 4 [heap-buffer-overflow, CWE-122]
findings/heap_overflow_write.c:6:19: note: allocated here
runtime-analyzer: collection
coretrace_lines=8
...
$ echo $?
1--format sarif prints a SARIF 2.1.0 log instead of the text summary: one result per finding
with ruleId, level, message.text, properties.cwe and properties.confidence, the faulting
access as locations[0] and the allocation site as relatedLocations[0] (id 0, "allocated
here"). The confidence is high for a fault observed while the program ran, medium for a vtable
type mismatch, which may be a legitimate cast, and low for a vtable report without a more
specific rule. tool.driver.rules lists each rule reported, with a title (shortDescription) and
what it detects (fullDescription), for a consumer such as GitHub code scanning to explain the
finding. In --test-dir mode, the log holds the findings of every program in one run.
$ runtime-analyzer --format sarif -- --ct-modules=alloc,bounds findings/heap_overflow_write.c
{
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
"runs": [
{
"results": [
{
"level": "error",
"locations": [ { "physicalLocation": { "artifactLocation": { "uri": "findings/heap_overflow_write.c" },
"region": { "startColumn": 15, "startLine": 7 } } } ],
"message": { "text": "heap-buffer-overflow WRITE of size 4" },
"properties": { "confidence": "high", "cwe": "CWE-122" },
"relatedLocations": [ { "id": 0, "message": { "text": "allocated here" }, ... } ],
"ruleId": "heap-buffer-overflow"
}
],
"tool": { "driver": { "name": "coretrace-runtime-analyzer",
"rules": [ { "id": "heap-buffer-overflow",
"shortDescription": { "text": "Heap buffer overflow" }, ... } ], ... } }
}
],
"version": "2.1.0"
}| Option | Effect |
|---|---|
-o, --output <path> |
instrumented binary path when the compiler args carry no -o |
--run-arg <value> |
argument passed to the instrumented binary (repeatable) |
--env NAME=VALUE |
environment variable set for the instrumented binary (repeatable) |
--cwd <path> |
working directory of the instrumented binary |
--timeout <seconds> |
kill the binary after this long; 60 by default, 0 disables the limit |
--format <text|sarif> |
text summary (default) or SARIF log on stdout |
--show-output |
print the captured stdout and stderr after the summary |
--show-events |
print the collected CoreTrace event lines |
--no-run |
compile only |
--test-dir <path> |
run every C/C++ source under a directory (repeatable, see below) |
--output-dir <path> |
artifact directory used by --test-dir (runtime-analyzer-artifacts by default) |
--strict-test-exit |
in batch mode, a non-zero program exit raises the status to at least 1 |
--version |
print the version and exit |
The exit status is the analyzer's verdict. The program's own status is reported as exit_code=.
| Status | Meaning |
|---|---|
0 |
the program was built and ran, and reported no finding |
1 |
at least one finding was reported |
2 |
the program could not be built or run, timed out without findings, or the arguments are invalid |
In batch mode the status is the worst of all the tests, and --strict-test-exit raises it to at
least 1 when a program exits non-zero.
Each run is bounded by --timeout. The program starts in its own process group with its stdin on
/dev/null, so a program waiting for input ends at EOF. When the deadline passes, the whole group
is killed, the output captured so far is still parsed for findings, the summary reports
timed_out=1, and the diagnostics say program timed out after N s.
The CLI is a thin main() over coretrace::runtime_analyzer::Run(), declared in
include/runtime_analyzer.hpp. A CMake project can fetch this repository and link
coretrace::runtime-analyzer_lib, which requires C++20 of its consumers;
AnalyzerResult::findings then holds the findings of a run,
AnalyzerOptions::timeout bounds it, and AnalyzerResult::timed_out reports a killed run. The
version is coretrace::runtime_analyzer::kVersion, from the generated
runtime_analyzer_version.hpp. The CTest suite is registered by the top-level project only, so
consumers do not inherit it. unittests/consumer/ is a complete consumer, built and run by the
consumer test.
Use --test-dir <path> to run every .c, .cc, .cpp, and .cxx source under a test directory.
Each source is compiled and executed as a separate instrumented binary, which avoids linker
collisions between test files that each define main.
Arguments after -- are shared compiler/CoreTrace flags for every test file. Batch mode keeps going
after compile failures, runtime failures and timeouts, marks them [COMPILE], [RUNTIME] and
[TIMEOUT] in the per-test lines, prints each test's findings under its line, and counts them in
the batch summary. Add --strict-test-exit if a non-zero test binary exit should make the analyzer
return a non-zero exit code.
runtime-analyzer --test-dir test --timeout 10 --output-dir /tmp/runtime-analyzer-tests -- --ct-modules=allctest --test-dir build --output-on-failure| Test | Checks |
|---|---|
findings_unit, process_unit |
the report parser and the process runner (unittests/) |
cli_args_unit |
argument parsing, every option and every invalid spelling, the compiler's -o, the compile-only and -x flags, the C-as-C++ and DEBUG heuristics, the compiler arguments of a run, the artifact names of a batch (src/cli_args.hpp) |
events_unit |
the CoreTrace event lines of a run and their summary, ANSI colours stripped, and the verdict drawn from a result (src/events.hpp) |
sarif_unit |
the SARIF writer against the exact log a consumer reads: empty, an error with its locations, a warning without one; every rule listed with a title and a description, and each finding's confidence |
api_unit |
the library API: Run and RunBatch on real programs, a heap overflow, a compile-only run, a hang against the timeout, a batch, the refused and empty cases |
consumer |
a project fetching this repository the way ctrace does (unittests/consumer/) configures, builds, links coretrace::runtime-analyzer_lib and analyzes a program |
fixtures |
every source under test/ states what it proves in its header (// args:, // expect:), and the analyzer proves it: findings with rule, CWE, level, faulting line and allocation site, or none; the program's exit status and output when stated. A fixture without an expectation fails (test/check_fixtures.py) |
cli |
every option as the table above states it, the text output, the SARIF log of a batch, and exit 2 with a message for invalid arguments and for what cannot be analyzed (test/cli/check_cli.py) |
timeout |
hung, stdin-reading and forking programs, alone and in a batch |
compile_failure |
a source that fails code generation fails its own analysis only |
hello_runs |
test/examples/fixtures/hello.c runs and its entry/exit events are collected |
sweep_runs |
batch mode over the whole tree: every fixture builds, with the per-source heuristics, and runs without a timeout (--test-dir test --timeout 10 -- --ct-modules=alloc) |
version |
--version prints the project() version |
llvm_floor |
configuration fails against an LLVM below the floor |
shipped_toolchain_unit |
the CLI points coretrace-compiler at the Clang headers shipped next to it, unless CT_CLANG is set |
install_layout |
cmake --install lays out the CLI, library, headers and Clang headers, and the installed CLI builds and runs a program |
A new fixture is a source under test/ whose header states its compiler arguments and what the
analyzer must report, for example:
// args: --ct-modules=alloc,vtable --ct-vtable-diag
// expect: rule=vtable-null-this cwe=CWE-476 level=error line=11expect: none states that nothing is reported; expect: program-exit=3 and
expect: output="..." check the program itself; expect[linux]: and expect[darwin]: keep a
line to one platform. test/check_fixtures.py documents the keys.
The Build workflow runs this suite on Ubuntu 24.04 and macOS against LLVM 20 for every push and
pull request. Its coverage job measures how much of src/, include/ and main.cpp the
suite runs, with llvm-cov on a Clang build configured with -DCORETRACE_COVERAGE=ON: the
totals go to the job summary, the report to an artifact, and the job fails when the line
coverage drops below the floor set in the workflow. The same measure runs locally with
scripts/ci/coverage.sh <build dir> <output dir> [<floor>], through xcrun on macOS.
clang-format (version 17) is checked by the clang-format workflow.
- Set the version in
project(... VERSION x.y.z ...)inCMakeLists.txtand merge it. - Tag the commit
vx.y.zand push the tag.
The Release workflow checks that the tag matches the project version, then builds the
archives with Dockerfile.release for amd64 and arm64: an Ubuntu 22.04 base with GCC 13 and
LLVM 20, the test suite, cmake --install with the LLVM libraries and Clang headers shipped,
patchelf for the rpaths, and a run of test/release/check-analysis.sh on bare Ubuntu 22.04,
Debian 12, Rocky Linux 9 and Ubuntu 24.04 images with only g++ installed. The archives and
their .sha256 are attached to the GitHub Release. The same build runs on every pull request
that touches the release files, without publishing.
Apache License 2.0, see LICENSE and NOTICE. To report a vulnerability, see SECURITY.md.