From 81fddcf55534d085d6f9f294e7bec52152df6d6f Mon Sep 17 00:00:00 2001 From: Ingo Budde Date: Fri, 7 Aug 2026 13:56:09 +0200 Subject: [PATCH 1/8] Create docker tooling that simplifies installation and usage, create phasar-analyze.sh script that orchestrates several steps required for program analysis. --- Dockerfile | 39 ++- USAGE.docker.md | 233 +++++++++++++++ .../01-uninitialized-variables/main.c | 7 + .../01-uninitialized-variables/util.c | 8 + .../01-uninitialized-variables/util.h | 4 + .../02-linear-constant/main.c | 10 + .../docker-whole-program/03-taint-leak/main.c | 9 + .../03-taint-leak/taint-config.json | 8 + .../04-double-free/double-free-config.json | 10 + .../04-double-free/main.c | 7 + .../05-file-io-typestate/main.c | 8 + .../06-type-hierarchy/main.cpp | 8 + .../07-call-graph/main.cpp | 8 + .../docker-whole-program/08-points-to/main.c | 8 + .../09-instruction-interaction/main.c | 9 + .../docker-whole-program/10-statistics/main.c | 24 ++ .../11-library/CMakeLists.txt | 3 + .../docker-whole-program/11-library/mylib.c | 11 + examples/docker-whole-program/README.md | 125 ++++++++ examples/docker-whole-program/run-all.sh | 48 ++++ utils/phasar-analyze.sh | 269 ++++++++++++++++++ 21 files changed, 851 insertions(+), 5 deletions(-) create mode 100644 USAGE.docker.md create mode 100644 examples/docker-whole-program/01-uninitialized-variables/main.c create mode 100644 examples/docker-whole-program/01-uninitialized-variables/util.c create mode 100644 examples/docker-whole-program/01-uninitialized-variables/util.h create mode 100644 examples/docker-whole-program/02-linear-constant/main.c create mode 100644 examples/docker-whole-program/03-taint-leak/main.c create mode 100644 examples/docker-whole-program/03-taint-leak/taint-config.json create mode 100644 examples/docker-whole-program/04-double-free/double-free-config.json create mode 100644 examples/docker-whole-program/04-double-free/main.c create mode 100644 examples/docker-whole-program/05-file-io-typestate/main.c create mode 100644 examples/docker-whole-program/06-type-hierarchy/main.cpp create mode 100644 examples/docker-whole-program/07-call-graph/main.cpp create mode 100644 examples/docker-whole-program/08-points-to/main.c create mode 100644 examples/docker-whole-program/09-instruction-interaction/main.c create mode 100644 examples/docker-whole-program/10-statistics/main.c create mode 100644 examples/docker-whole-program/11-library/CMakeLists.txt create mode 100644 examples/docker-whole-program/11-library/mylib.c create mode 100644 examples/docker-whole-program/README.md create mode 100755 examples/docker-whole-program/run-all.sh create mode 100755 utils/phasar-analyze.sh diff --git a/Dockerfile b/Dockerfile index 844e78ea7f..58b69b5948 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,32 @@ ARG baseimage="ubuntu:24.04" +# LLVM/clang major version used throughout the image. This is the version PhASAR +# links against, so it is also the LLVM IR version phasar-cli can parse and the +# version WLLVM must emit. Bump both ARGs together to change it. +ARG llvm_version=22 +# PhASAR's cmake version string. LLVM 22 releases as 22.1.x (new scheme), whereas +# LLVM <=20 use NN.0.x — so this is "22.1" for llvm 22, but e.g. "16" for llvm 16. +ARG phasar_llvm_version=22.1 + FROM "$baseimage" AS build +ARG llvm_version + +# Install the LLVM/clang toolchain plus the pieces the whole-program wrapper needs: +# `llvm-` provides llvm-link/llvm-dis used by WLLVM, `python3-pip` installs wllvm. RUN --mount=type=bind,source=./utils/InstallAptDependencies.sh,target=/InstallAptDependencies.sh \ set -eux; \ - ./InstallAptDependencies.sh --noninteractive tzdata clang-20 libclang-rt-20-dev clang-tools-20 + ./InstallAptDependencies.sh --noninteractive --llvm-version "${llvm_version}" \ + tzdata "clang-tools-${llvm_version}" "llvm-${llvm_version}" python3-pip file; \ + pip3 install --no-cache-dir --break-system-packages wllvm -ENV CC=/usr/bin/clang-20 \ - CXX=/usr/bin/clang++-20 +ENV CC=/usr/bin/clang-${llvm_version} \ + CXX=/usr/bin/clang++-${llvm_version} FROM build +ARG llvm_version +ARG phasar_llvm_version + ARG RUN_TESTS=OFF RUN --mount=type=bind,source=.,target=/usr/src/phasar,rw \ set -eux; \ @@ -17,9 +34,10 @@ RUN --mount=type=bind,source=.,target=/usr/src/phasar,rw \ git submodule update --init; \ cmake -S . -B cmake-build/Release \ -DCMAKE_BUILD_TYPE=Release \ + -DPHASAR_LLVM_VERSION="${phasar_llvm_version}" \ -DPHASAR_TARGET_ARCH="" \ -DPHASAR_ENABLE_SANITIZERS=ON \ - -DBUILD_PHASAR_CLANG=ON \ + -DPHASAR_ALLOW_LTO_IN_RELEASE_BUILD=OFF \ -DPHASAR_USE_Z3=ON \ -DPHASAR_BUILD_UNITTESTS=$RUN_TESTS \ -DPHASAR_BUILD_IR=$RUN_TESTS \ @@ -29,4 +47,15 @@ RUN --mount=type=bind,source=.,target=/usr/src/phasar,rw \ [ "${RUN_TESTS}" = "ON" ] && ctest --test-dir cmake-build/Release --output-on-failure || true; \ phasar-cli --version -ENTRYPOINT [ "phasar-cli" ] +# Install the end-to-end wrapper: build a C/C++ project to whole-program IR via +# WLLVM and run a PhASAR analysis in one command. `phasar-analyze cli ...` still +# exposes the raw phasar-cli. +COPY utils/phasar-analyze.sh /usr/local/bin/phasar-analyze +RUN chmod +x /usr/local/bin/phasar-analyze + +# LLVM version WLLVM must emit for phasar-cli to accept the bitcode. Kept in sync +# with the toolchain above via the llvm_version ARG. Override at runtime with +# `-e PHASAR_IR_LLVM_VERSION=NN`. +ENV PHASAR_IR_LLVM_VERSION=${llvm_version} + +ENTRYPOINT [ "phasar-analyze" ] diff --git a/USAGE.docker.md b/USAGE.docker.md new file mode 100644 index 0000000000..c0700c37e6 --- /dev/null +++ b/USAGE.docker.md @@ -0,0 +1,233 @@ +# Analyzing a C/C++ Program with the PhASAR Docker Container + +This guide shows how to analyze a whole C/C++ program end-to-end using the +PhASAR Docker image. You do **not** need to install LLVM, WLLVM, or PhASAR +yourself — the image bundles everything and orchestrates the required steps for +you. + +## Contents +- [How it works](#how-it-works) +- [Build the image](#build-the-image) +- [Quick start](#quick-start) +- [Providing input](#providing-input) +- [Choosing an analysis](#choosing-an-analysis) +- [Alias analysis & call graph](#alias-analysis--call-graph) +- [Getting results out](#getting-results-out) +- [Taint / typestate configuration](#taint--typestate-configuration) +- [Raw phasar-cli access](#raw-phasar-cli-access) +- [Configuration (environment variables)](#configuration-environment-variables) +- [Troubleshooting](#troubleshooting) + +## How it works + +PhASAR analyzes **LLVM IR**, not C/C++ source directly. To analyze a real +program it must first be compiled to a single whole-program LLVM module. The +image's entry point, `phasar-analyze`, automates the whole pipeline: + +``` +your C/C++ project ──► build with WLLVM (clang) ──► extract-bc ──► whole-program.bc ──► phasar-cli ──► results +``` + +1. **Build with WLLVM** — compiles your project with clang while preserving each + translation unit's bitcode (with `-g` for source-level reporting). +2. **`extract-bc`** — links all the preserved bitcode into one whole-program + module. +3. **`phasar-cli`** — runs the requested analysis on that module. + +All three steps run inside the container; you just provide the program and pick +an analysis. + +## Build the image + +From the repository root: + +```bash +docker build -t phasar . +``` + +This builds PhASAR against LLVM 22 by default. To use a different LLVM major +version, pass build args (the value must match a version PhASAR supports, 16–22): + +```bash +docker build -t phasar --build-arg llvm_version=16 --build-arg phasar_llvm_version=16 . +``` + +> `phasar_llvm_version` is PhASAR's CMake version string — `22.1` for LLVM 22 +> (new release scheme), but `16` for LLVM 16. + +## Quick start + +Analyze a project mounted at `/work` for uses of uninitialized variables: + +```bash +docker run --rm --user "$(id -u):$(id -g)" \ + -v "$PWD:/work" phasar --project /work -a ifds-uninit +``` + +- `-v "$PWD:/work"` mounts your current directory into the container. +- `--user "$(id -u):$(id -g)"` keeps generated files owned by you, not root. +- `--project /work` autodetects the build system (cmake / `./configure` / make). +- `-a ifds-uninit` selects the analysis. + +See runnable, self-contained examples in +[`examples/docker-whole-program/`](examples/docker-whole-program/) — run them all +with `examples/docker-whole-program/run-all.sh`. + +## Providing input + +`phasar-analyze` accepts several input modes — pick one: + +| Mode | Use when | Example | +|------|----------|---------| +| `--project DIR` | You have a project with a build system (autodetects cmake / `./configure` / make) | `phasar --project /work -a ifds-uninit` | +| `--build-cmd "CMD"` | The project needs a specific build command | `phasar --project /work --build-cmd "make -j4" -a ifds-uninit` | +| `--sources "A.c B.cpp"` | You just have a few loose source files | `phasar --sources "main.c util.c" -a ifds-uninit` | +| `--binary PATH` | You want to target a specific build artifact (exe, `.so`, `.a`) | `phasar --project /work --binary /work/build/libfoo.a -a ifds-uninit -E __ALL__` | +| `--module FILE.ll\|.bc` | You already have LLVM IR | `phasar --module /work/prog.bc -a ifds-uninit` | + +**Entry points.** By default analysis starts at `main`. Use `-E NAME` to set a +different entry point, or `-E __ALL__` for code without a `main` (e.g. a library): + +```bash +docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/work" phasar \ + --project /work -a ifds-uninit -E __ALL__ +``` + +In `--project` mode without `--binary`, the wrapper autodetects the newest build +artifact that carries embedded bitcode. If detection picks the wrong one, name it +with `--binary`. + +## Choosing an analysis + +Select a data-flow analysis with `-a` (repeatable). Available analyses: + +| Flag (`-a`) | Analysis | Needs `--analysis-config` | +|-------------|----------|:---:| +| `ifds-uninit` | Uses of uninitialized variables | | +| `ifds-const` | Variables actually mutated through the program | | +| `ifds-type` | Simple type analysis | | +| `ide-lca` | Linear constant propagation | | +| `ide-iia` | Instruction-interaction (what influences what) | | +| `ide-stdio-ts` | libc file-I/O typestate (invalid usages) | | +| `ide-openssl-ts` | OpenSSL EVP typestate | | +| `ifds-taint` | Alias-aware taint analysis | ✔ | +| `ide-xtaint` | Taint analysis with limited field-sensitivity | ✔ | +| `ifds-fieldsens-taint` | Field-sensitive taint (CFL) | ✔ | +| `monoifds-taint` | Taint on the MonoIFDS solver | ✔ | +| `sparse-ifds-taint` | Taint on the SparseIFDS solver | ✔ | +| `inter-mono-taint` | Taint via inter-procedural Monotone Framework | ✔ | +| `intra-mono-fca` | Intra-procedural full constant propagation (Monotone) | | + +If you omit `-a` (and don't request an emitter), the wrapper just builds the +module and emits its IR (`--emit-ir`), which is handy to confirm the build. + +## Alias analysis & call graph + +These affect precision/performance of most analyses: + +- **Alias analysis** — `-P` / `--alias-analysis`: + `CFLAnders` (default, precise), `CFLSteens` (faster), `union-find` + (add `--union-find-aa=ctx-sens` or `ctx-ind-sens` after `--`). +- **Call-graph algorithm** — `-C` / `--call-graph`: + `otf` (default), `cha`, `rta`, `vta`, `nores`. + +```bash +docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/work" phasar \ + --sources main.c -P CFLSteens -C cha -a ide-xtaint --analysis-config /work/tc.json +``` + +## Getting results out + +By default results print to **stdout**. You can also emit specific artifacts — +pass emitter flags after `--` (everything after `--` goes straight to +`phasar-cli`): + +| After `--` | Emits | +|------------|-------| +| `--emit-ir` | Preprocessed/annotated IR of the target | +| `--emit-stats` / `--emit-statistics-as-json` | Module statistics | +| `--emit-th-as-text` / `--emit-th-as-dot` / `--emit-th-as-json` | Type hierarchy | +| `--emit-cg-as-dot` / `--emit-cg-as-json` | Call graph | +| `--emit-pta-as-text` / `--emit-pta-as-dot` / `--emit-pta-as-json` | Points-to / alias info | +| `--emit-esg-as-dot` | Exploded super-graph | +| `--emit-raw-results` | Unprocessed solver results | + +```bash +# Type hierarchy + call graph, as text/DOT, to stdout: +docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/work" phasar \ + --sources main.cpp -- --emit-th-as-text --emit-cg-as-dot +``` + +**Write to files instead of stdout** with `-o DIR` (the directory must exist and +be mounted). Results land in a timestamped subdirectory: + +```bash +mkdir -p results +docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/work" -w /work phasar \ + --sources main.cpp -o /work/results -- --emit-cg-as-json --emit-th-as-json +# -> results/-/psr-cg.json, psr-th.json +``` + +## Taint / typestate configuration + +Taint analyses (`ifds-taint`, `ide-xtaint`, `ifds-fieldsens-taint`, +`monoifds-taint`, …) require a JSON config that declares sources, sinks and +sanitizers, passed with `--analysis-config`. The config path must be inside the +mounted directory. Example (a double-free config — `free`'s argument is both a +source and a sink): + +```json +{ + "name": "double-free", + "version": 1.0, + "functions": [ + { "name": "free", "params": { "source": [0], "sink": [0] } } + ] +} +``` + +```bash +docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/work" -w /work phasar \ + --sources main.c -a ifds-taint --analysis-config double-free-config.json +``` + +`ret` tags a function's return value (`"ret": "source"`); `params` tags argument +indices. Full schema: [`config/TaintConfigSchema.json`](config/TaintConfigSchema.json). + +## Raw phasar-cli access + +`phasar-analyze` is a convenience wrapper. To call `phasar-cli` directly (e.g. on +an existing module, or to see all options), use the `cli` escape hatch: + +```bash +docker run --rm phasar cli --help +docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/work" phasar \ + cli -m /work/prog.ll -D ifds-uninit --emit-text-report +``` + +## Configuration (environment variables) + +Set with `docker run -e NAME=VALUE`: + +| Variable | Purpose | Default | +|----------|---------|---------| +| `PHASAR_IR_LLVM_VERSION` | LLVM major version WLLVM must emit (must match the version PhASAR was built with) | baked in at build (22) | +| `PHASAR_LLVM_BIN_DIR` | Directory holding the matching `clang`/`llvm-link` | `/usr/lib/llvm-/bin` | +| `PHASAR_WORKDIR` | Scratch dir for the generated whole-program bitcode | `/tmp/phasar-analyze` | + +## Troubleshooting + +- **`... does not exist` / module not loaded** — the LLVM version of the bitcode + must match the version PhASAR was built with. If you rebuilt the image for a + different `llvm_version`, the wrapper picks it up automatically; if you feed a + prebuilt `--module`, make sure it was produced by the same LLVM major version. +- **`could not detect a build system`** — pass `--build-cmd "..."` (project mode) + or use `--sources`. +- **`no artifact with embedded bitcode found`** — the build produced no linkable + bitcode (e.g. it failed). Check the build output, or point at the artifact with + `--binary`. For libraries, remember `-E __ALL__`. +- **Files owned by root** — add `--user "$(id -u):$(id -g)"` to `docker run`. +- **Missing source lines in results** — keep debug info (`-g`); the wrapper adds + it automatically for `--sources`/`--project` builds. +- **See every phasar-cli option** — `docker run --rm phasar cli --help`, or + `docker run --rm phasar --help` for the wrapper's own options. diff --git a/examples/docker-whole-program/01-uninitialized-variables/main.c b/examples/docker-whole-program/01-uninitialized-variables/main.c new file mode 100644 index 0000000000..75dd0bbb1b --- /dev/null +++ b/examples/docker-whole-program/01-uninitialized-variables/main.c @@ -0,0 +1,7 @@ +#include +#include "util.h" +int main(int argc, char **argv) { + int x = compute(argc - 1); + printf("%d\n", x); + return 0; +} diff --git a/examples/docker-whole-program/01-uninitialized-variables/util.c b/examples/docker-whole-program/01-uninitialized-variables/util.c new file mode 100644 index 0000000000..eb609828ba --- /dev/null +++ b/examples/docker-whole-program/01-uninitialized-variables/util.c @@ -0,0 +1,8 @@ +#include "util.h" +int compute(int seed) { + int result; /* BUG: not initialized on the seed<=0 path */ + if (seed > 0) { + result = seed * 2; + } + return result; /* uninitialized use when seed <= 0 */ +} diff --git a/examples/docker-whole-program/01-uninitialized-variables/util.h b/examples/docker-whole-program/01-uninitialized-variables/util.h new file mode 100644 index 0000000000..b744f7d139 --- /dev/null +++ b/examples/docker-whole-program/01-uninitialized-variables/util.h @@ -0,0 +1,4 @@ +#ifndef UTIL_H +#define UTIL_H +int compute(int seed); /* defined in util.c -> forces whole-program linking */ +#endif diff --git a/examples/docker-whole-program/02-linear-constant/main.c b/examples/docker-whole-program/02-linear-constant/main.c new file mode 100644 index 0000000000..74b87e0bce --- /dev/null +++ b/examples/docker-whole-program/02-linear-constant/main.c @@ -0,0 +1,10 @@ +#include +/* IDE linear constant analysis tracks integer constants through linear ops. */ +int main() { + int a = 6; + int b = a + 1; /* 7 */ + int c = a * 7; /* 42 */ + int d = b - 3; /* 4 */ + printf("%d %d %d\n", b, c, d); + return 0; +} diff --git a/examples/docker-whole-program/03-taint-leak/main.c b/examples/docker-whole-program/03-taint-leak/main.c new file mode 100644 index 0000000000..e1bb3e7074 --- /dev/null +++ b/examples/docker-whole-program/03-taint-leak/main.c @@ -0,0 +1,9 @@ +#include +/* 'source' returns tainted data; 'sink' must never receive tainted data. */ +extern char *source(void); +extern void sink(const char *data); +int main() { + char *tainted = source(); + sink(tainted); /* LEAK: tainted value reaches the sink */ + return 0; +} diff --git a/examples/docker-whole-program/03-taint-leak/taint-config.json b/examples/docker-whole-program/03-taint-leak/taint-config.json new file mode 100644 index 0000000000..b5c9c2aeb4 --- /dev/null +++ b/examples/docker-whole-program/03-taint-leak/taint-config.json @@ -0,0 +1,8 @@ +{ + "name": "demo-source-sink", + "version": 1.0, + "functions": [ + { "name": "source", "ret": "source", "params": {} }, + { "name": "sink", "params": { "sink": [0] } } + ] +} diff --git a/examples/docker-whole-program/04-double-free/double-free-config.json b/examples/docker-whole-program/04-double-free/double-free-config.json new file mode 100644 index 0000000000..1ada7f62b4 --- /dev/null +++ b/examples/docker-whole-program/04-double-free/double-free-config.json @@ -0,0 +1,10 @@ +{ + "name": "double-free", + "version": 1.0, + "functions": [ + { + "name": "free", + "params": { "source": [0], "sink": [0] } + } + ] +} diff --git a/examples/docker-whole-program/04-double-free/main.c b/examples/docker-whole-program/04-double-free/main.c new file mode 100644 index 0000000000..cf3ec429d6 --- /dev/null +++ b/examples/docker-whole-program/04-double-free/main.c @@ -0,0 +1,7 @@ +#include +int main() { + int *p = (int *)malloc(sizeof(int) * 4); + free(p); + free(p); /* BUG: double free (p flows from one free() to another) */ + return 0; +} diff --git a/examples/docker-whole-program/05-file-io-typestate/main.c b/examples/docker-whole-program/05-file-io-typestate/main.c new file mode 100644 index 0000000000..7df14c25e1 --- /dev/null +++ b/examples/docker-whole-program/05-file-io-typestate/main.c @@ -0,0 +1,8 @@ +#include +int main() { + FILE *f = fopen("data.txt", "r"); + fclose(f); + int c = fgetc(f); /* BUG: use of FILE* after fclose */ + printf("%d\n", c); + return 0; +} diff --git a/examples/docker-whole-program/06-type-hierarchy/main.cpp b/examples/docker-whole-program/06-type-hierarchy/main.cpp new file mode 100644 index 0000000000..bcc0513c32 --- /dev/null +++ b/examples/docker-whole-program/06-type-hierarchy/main.cpp @@ -0,0 +1,8 @@ +struct Animal { virtual ~Animal() = default; virtual void speak() const = 0; }; +struct Dog : Animal { void speak() const override; }; +struct Cat : Animal { void speak() const override; }; +struct Puppy : Dog { void speak() const override; }; +void Dog::speak() const {} +void Cat::speak() const {} +void Puppy::speak() const {} +int main() { return 0; } diff --git a/examples/docker-whole-program/07-call-graph/main.cpp b/examples/docker-whole-program/07-call-graph/main.cpp new file mode 100644 index 0000000000..9deb88e3ca --- /dev/null +++ b/examples/docker-whole-program/07-call-graph/main.cpp @@ -0,0 +1,8 @@ +struct Shape { virtual double area() const { return 0.0; } virtual ~Shape() = default; }; +struct Circle : Shape { double area() const override { return 3.14; } }; +struct Square : Shape { double area() const override { return 4.0; } }; +static double total(const Shape *s) { return s->area(); } /* virtual call site */ +int main() { + Circle c; Square s; + return (int)(total(&c) + total(&s)); +} diff --git a/examples/docker-whole-program/08-points-to/main.c b/examples/docker-whole-program/08-points-to/main.c new file mode 100644 index 0000000000..ce7caeb306 --- /dev/null +++ b/examples/docker-whole-program/08-points-to/main.c @@ -0,0 +1,8 @@ +int main() { + int x = 0, y = 0; + int *p = &x; + int *q = p; /* q aliases p (both point to x) */ + int *r = &y; /* r points to y (must-not-alias p/q) */ + *q = 5; + return *p + *r; +} diff --git a/examples/docker-whole-program/09-instruction-interaction/main.c b/examples/docker-whole-program/09-instruction-interaction/main.c new file mode 100644 index 0000000000..b4b8b78ee3 --- /dev/null +++ b/examples/docker-whole-program/09-instruction-interaction/main.c @@ -0,0 +1,9 @@ +#include +/* IDE instruction-interaction: which instructions influence which others. */ +int main() { + int secret = 42; + int derived = secret + 1; /* influenced by secret */ + int unrelated = 7; /* independent */ + printf("%d %d\n", derived, unrelated); + return 0; +} diff --git a/examples/docker-whole-program/10-statistics/main.c b/examples/docker-whole-program/10-statistics/main.c new file mode 100644 index 0000000000..08c4b79137 --- /dev/null +++ b/examples/docker-whole-program/10-statistics/main.c @@ -0,0 +1,24 @@ +#include +#include +/* A small program with a mix of constructs so the IR statistics are non-trivial: + globals, a heap allocation, a loop with loads/stores, and two functions. */ +int global_counter = 0; + +static int accumulate(int *arr, int n) { + int sum = 0; + for (int i = 0; i < n; ++i) { + sum += arr[i]; + global_counter++; + } + return sum; +} + +int main(void) { + int *data = (int *)malloc(sizeof(int) * 4); + for (int i = 0; i < 4; ++i) { + data[i] = i * i; + } + printf("%d (counter=%d)\n", accumulate(data, 4), global_counter); + free(data); + return 0; +} diff --git a/examples/docker-whole-program/11-library/CMakeLists.txt b/examples/docker-whole-program/11-library/CMakeLists.txt new file mode 100644 index 0000000000..56c3cedaf3 --- /dev/null +++ b/examples/docker-whole-program/11-library/CMakeLists.txt @@ -0,0 +1,3 @@ +cmake_minimum_required(VERSION 3.10) +project(mylib C) +add_library(mylib STATIC mylib.c) # builds libmylib.a (no executable) diff --git a/examples/docker-whole-program/11-library/mylib.c b/examples/docker-whole-program/11-library/mylib.c new file mode 100644 index 0000000000..0ac0519c3d --- /dev/null +++ b/examples/docker-whole-program/11-library/mylib.c @@ -0,0 +1,11 @@ +/* A LIBRARY with no main(). Analyzed with -E __ALL__ so every function + definition is treated as an entry point. 'unsafe' has an uninit-use bug. */ +int add(int a, int b) { return a + b; } + +int unsafe(int flag) { + int v; + if (flag) { + v = 1; + } + return v; /* uninitialized when flag == 0 */ +} diff --git a/examples/docker-whole-program/README.md b/examples/docker-whole-program/README.md new file mode 100644 index 0000000000..755dd45191 --- /dev/null +++ b/examples/docker-whole-program/README.md @@ -0,0 +1,125 @@ +# Whole-Program Analysis Examples (Docker) + +A set of tiny, self-contained C/C++ programs — each with a known finding — that +demonstrate PhASAR's main capabilities **end-to-end through the Docker image**: +build the program to whole-program LLVM IR with [WLLVM](https://github.com/travitch/whole-program-llvm), +then run a PhASAR analysis on it. All of this is handled by the image's +`phasar-analyze` entry point. + +## Prerequisites + +Build the image once from the repository root: + +```bash +docker build -t phasar . +``` + +The image bundles clang/LLVM (22 by default), WLLVM, and `phasar-cli`, and uses +`phasar-analyze` as its entry point. See the top-level `Dockerfile` and +`utils/phasar-analyze.sh`. + +## Running + +Run the whole suite: + +```bash +./run-all.sh # uses image name "phasar" +./run-all.sh my-image # or a custom image name +``` + +Run a single example (mount its directory at `/work`): + +```bash +docker run --rm --user "$(id -u):$(id -g)" \ + -v "$PWD/01-uninitialized-variables:/work" -w /work \ + phasar --sources "main.c util.c" -a ifds-uninit +``` + +Running as `--user "$(id -u):$(id -g)"` keeps any generated files owned by you +instead of root. + +## The examples + +| # | Directory | PhASAR feature | Command (args to `phasar-analyze`) | +|---|-----------|----------------|------------------------------------| +| 01 | `01-uninitialized-variables` | **IFDS** data-flow: uninitialized-variable uses (whole-program across 2 files) | `--sources "main.c util.c" -a ifds-uninit` | +| 02 | `02-linear-constant` | **IDE** data-flow: linear constant propagation | `--sources main.c -a ide-lca` | +| 03 | `03-taint-leak` | **Taint** (IDE): source → sink leak with a custom config | `--sources main.c -a ide-xtaint --analysis-config taint-config.json` | +| 04 | `04-double-free` | **Taint** (IFDS): double-free (`free` tagged as source *and* sink) | `--sources main.c -a ifds-taint --analysis-config double-free-config.json` | +| 05 | `05-file-io-typestate` | **Typestate** (IDE): libc file-I/O use-after-close | `--sources main.c -a ide-stdio-ts` | +| 06 | `06-type-hierarchy` | **Type hierarchy** + vtables reconstructed from C++ | `--sources main.cpp -- --emit-th-as-text` | +| 07 | `07-call-graph` | **Call graph** incl. virtual dispatch (CHA) | `--sources main.cpp -C cha -- --emit-cg-as-dot` | +| 08 | `08-points-to` | **Alias / points-to** information (CFLAnders) | `--sources main.c -- --emit-pta-as-text` | +| 09 | `09-instruction-interaction` | **IDE** data-flow: instruction-interaction analysis | `--sources main.c -a ide-iia` | +| 10 | `10-statistics` | **LLVM IR statistics** of the module | `--sources main.c -- --emit-stats` | +| 11 | `11-library` | **Library** (no `main`) analyzed with **all** functions as entry points | `--project /work -a ifds-uninit -E __ALL__` | + +Anything after `--` is forwarded verbatim to `phasar-cli` (used above for the +`--emit-*` reporting flags). + +## Ways to provide input + +The examples above use `--sources` and `--project`, but `phasar-analyze` accepts +several input modes (see `phasar-analyze --help`). All of the following are +supported and tested: + +| Mode | When to use | Example | +|------|-------------|---------| +| `--sources "a.c b.cpp"` | A few loose source files | `phasar --sources "main.c util.c" -a ifds-uninit` | +| `--project DIR` | A real project; autodetects cmake / `./configure` / make | `phasar --project /work -a ifds-uninit` | +| `--build-cmd "CMD"` | A project with a non-standard build | `phasar --project /work --build-cmd "make -j4" -a ifds-uninit` | +| `--binary PATH` | Point at a specific build artifact (exe, `.so`, `.a`) | `phasar --project /work --binary /work/build/libfoo.a -a ifds-uninit -E __ALL__` | +| `--module FILE.ll\|.bc` | You already have LLVM IR | `phasar --module /work/prog.bc -a ifds-uninit` | + +For code without a `main` (libraries), pass `-E __ALL__` to use every function +definition as an entry point. + +### Writing results to files + +By default results go to stdout. Pass `-o DIR` (mount it, and it must exist) to +write results into a timestamped subdirectory instead — useful with the JSON +emitters for machine-readable output: + +```bash +mkdir -p results +docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/work" -w /work phasar \ + --sources main.cpp -o /work/results -- --emit-cg-as-json --emit-th-as-json +# -> results/-/psr-cg.json, psr-th.json +``` + +## Beyond these examples + +`phasar-analyze` is a thin wrapper over `phasar-cli`; the examples above are a +representative slice, not the full set. To see everything available: + +```bash +docker run --rm phasar cli --help +``` + +Some notable options not exercised here: + +- **More data-flow analyses** (`-a`/`-D`): `ifds-taint`, `ifds-fieldsens-taint`, + `monoifds-taint`, `sparse-ifds-taint`, `inter-mono-taint`, `ifds-const`, + `ifds-type`, `ide-openssl-ts`, `ide-fiia`, `intra-mono-fca`, and the solver + self-tests (`ifds-solvertest`, `ide-solvertest`, …). These cover the different + solver families: IFDS, IDE, MonoIFDS, SparseIFDS, and intra-/inter-procedural + Monotone Frameworks. +- **Alias analyses** (`--alias-analysis`): `CFLAnders` (default, more precise), + `CFLSteens` (faster), `union-find` (with `--union-find-aa=ctx-sens` / + `ctx-ind-sens`). +- **Call-graph algorithms** (`-C`): `cha`, `rta`, `vta`, `otf` (default), `nores`. +- **Emitters**: `--emit-ir`, `--emit-cg-as-{dot,json}`, + `--emit-th-as-{text,dot,json}`, `--emit-pta-as-{text,dot,json}`, + `--emit-statistics-as-json`, `--emit-esg-as-dot`, `--emit-raw-results`. +- **Results to files** instead of stdout: add `-o ` (mount it, too). + +## Notes + +- The wrapper compiles with `-g`, so findings carry **source-level** file/line + information (see the `ifds-uninit` output). Keep debug info in your own builds + for the same benefit. +- For real, multi-file projects use `--project ` (autodetects + cmake/`./configure`/make and builds with WLLVM) or `--build-cmd "..."` instead + of `--sources`. See `phasar-analyze --help`. +- Taint/typestate configs use PhASAR's taint-config JSON + (see `config/TaintConfigSchema.json`). diff --git a/examples/docker-whole-program/run-all.sh b/examples/docker-whole-program/run-all.sh new file mode 100755 index 0000000000..79a0d676f3 --- /dev/null +++ b/examples/docker-whole-program/run-all.sh @@ -0,0 +1,48 @@ +#!/bin/bash +# run-all.sh — run every example in this directory through the PhASAR Docker +# image (the `phasar-analyze` end-to-end wrapper). Each example is a tiny, +# self-contained C/C++ program with a known finding. +# +# Usage: +# ./run-all.sh [IMAGE] # IMAGE defaults to "phasar" +# +# Build the image first (from the repository root): +# docker build -t phasar . + +set -euo pipefail + +IMAGE="${1:-phasar}" +HERE="$(cd "$(dirname "$0")" && pwd)" +# run the container as the current user so generated files aren't owned by root +USER_ARGS=(--user "$(id -u):$(id -g)") + +# Each entry: "||" +# Anything after `--` in the args is forwarded verbatim to phasar-cli. +EXAMPLES=( + "01-uninitialized-variables|IFDS: use of uninitialized variables (whole-program, 2 files)|--sources 'main.c util.c' -a ifds-uninit" + "02-linear-constant|IDE: linear constant propagation|--sources main.c -a ide-lca" + "03-taint-leak|IDE: taint analysis, source->sink leak (custom config)|--sources main.c -a ide-xtaint --analysis-config taint-config.json" + "04-double-free|IFDS: double-free via taint (free = source & sink)|--sources main.c -a ifds-taint --analysis-config double-free-config.json" + "05-file-io-typestate|IDE: libc file-I/O typestate (use-after-close)|--sources main.c -a ide-stdio-ts" + "06-type-hierarchy|Type hierarchy + vtables from C++ (emit)|--sources main.cpp -- --emit-th-as-text" + "07-call-graph|Call graph incl. virtual dispatch, CHA (emit)|--sources main.cpp -C cha -- --emit-cg-as-dot" + "08-points-to|Alias/points-to information, CFLAnders (emit)|--sources main.c -- --emit-pta-as-text" + "09-instruction-interaction|IDE: which instructions influence which|--sources main.c -a ide-iia" + "10-statistics|LLVM IR statistics of the module (emit)|--sources main.c -- --emit-stats" + "11-library|Analyze a static library (no main) with all functions as entry points|--project /work -a ifds-uninit -E __ALL__" +) + +for entry in "${EXAMPLES[@]}"; do + IFS='|' read -r dir desc args <<< "$entry" + echo "############################################################" + echo "# $dir" + echo "# $desc" + echo "# phasar-analyze $args" + echo "############################################################" + # `eval` so the quoted 'main.c util.c' in args is word-split correctly + eval docker run --rm "${USER_ARGS[@]}" \ + -v "\"$HERE/$dir:/work\"" -w /work "\"$IMAGE\"" "$args" + echo +done + +echo "All examples finished." diff --git a/utils/phasar-analyze.sh b/utils/phasar-analyze.sh new file mode 100755 index 0000000000..34ad931638 --- /dev/null +++ b/utils/phasar-analyze.sh @@ -0,0 +1,269 @@ +#!/bin/bash +# phasar-analyze — build a C/C++ project to whole-program LLVM IR (via WLLVM) and +# run a PhASAR analysis on it, end-to-end. +# +# This is the default entry point of the PhASAR Docker image. It wires together +# three steps that are otherwise manual: +# +# 1. build the target project with WLLVM so every translation unit's bitcode is +# preserved and can be linked into a single whole-program module, +# 2. extract that whole-program module (extract-bc), +# 3. hand the module to phasar-cli with the requested analysis. +# +# The LLVM version used for (1) and (2) MUST match the LLVM version PhASAR was +# linked against (see PHASAR_LLVM_VERSION at build time), otherwise phasar-cli +# will refuse to load the bitcode. In the image that version is baked into +# PHASAR_IR_LLVM_VERSION; override it at runtime with `-e PHASAR_IR_LLVM_VERSION=NN`. + +set -euo pipefail + +# --------------------------------------------------------------------------- +# escape hatch: `phasar-analyze cli ...` runs the raw phasar-cli unchanged +# --------------------------------------------------------------------------- +if [ "${1:-}" = "cli" ]; then + shift + exec phasar-cli "$@" +fi + +LLVM_VERSION="${PHASAR_IR_LLVM_VERSION:-22}" +LLVM_BIN_DIR="${PHASAR_LLVM_BIN_DIR:-/usr/lib/llvm-${LLVM_VERSION}/bin}" + +# --------------------------------------------------------------------------- +# defaults +# --------------------------------------------------------------------------- +MODE="" # project | sources | module +PROJECT_DIR="" +BUILD_CMD="" +SOURCES="" +MODULE="" +BINARY="" # artifact to extract bitcode from (autodetected if empty) +KEEP_GOING="false" +WORKDIR="${PHASAR_WORKDIR:-/tmp/phasar-analyze}" + +# forwarded to phasar-cli +ANALYSES=() +ENTRY_POINTS=() +CALL_GRAPH="" +ALIAS_ANALYSIS="" +ANALYSIS_CONFIG="" +OUT_DIR="" +PASSTHROUGH=() # everything after `--` + +usage() { + cat <<'EOF' +phasar-analyze — build a C/C++ project to whole-program LLVM IR (WLLVM) and run PhASAR. + +USAGE: + phasar-analyze [OPTIONS] [-- EXTRA_PHASAR_CLI_ARGS...] + phasar-analyze cli # bypass; call phasar-cli directly + +MODE (choose one; defaults to `--project .`): + --project DIR Build the project rooted at DIR, then analyze it. + Autodetects cmake / ./configure / Makefile. + --sources "A.c B.cpp" Compile the given source file(s) directly (no build system) + and link them into one module. + --module FILE.bc|.ll Skip building; analyze an existing LLVM IR module. + +BUILD OPTIONS (project mode): + --build-cmd "CMD" Use CMD as the build command instead of autodetection. + Run from inside PROJECT_DIR with the WLLVM compilers set. + --binary PATH Artifact (executable, .so or .a) to extract bitcode from. + Default: autodetect the newest artifact holding bitcode. + --keep-going Do not abort if the build returns a non-zero status + (analyze whatever bitcode was produced). + +ANALYSIS OPTIONS (forwarded to phasar-cli): + -a, --analysis FLAG Data-flow analysis, e.g. ifds-uninit, ifds-taint, ide-xtaint, + ide-lca, ifds-const. Repeatable. Omit to only build+emit IR. + -E, --entry NAME Entry point(s). Default: main. Use __ALL__ for libraries. + -C, --call-graph ALG cha | rta | vta | otf | nores (phasar default: otf) + -P, --alias-analysis A e.g. union-find, CFLAnders, CFLSteens + --analysis-config F Taint/typestate config JSON (sources/sinks/sanitizers). + -o, --out DIR Write results into DIR instead of stdout. + -- Pass all following args verbatim to phasar-cli + (e.g. --emit-pta-as-text, --union-find-aa=ctx-sens). + +ENV: + PHASAR_IR_LLVM_VERSION LLVM major version for WLLVM (must match PhASAR). Default: 22. + PHASAR_WORKDIR Scratch dir for generated bitcode. Default: /tmp/phasar-analyze. + +EXAMPLES: + # Uninitialized-variable analysis on a CMake project mounted at /work: + phasar-analyze --project /work -a ifds-uninit + + # Double-free taint analysis on two source files: + phasar-analyze --sources "a.c b.c" -a ide-xtaint \ + --analysis-config /work/double-free-config.json --emit-text-report + + # Points-to as text on an existing module: + phasar-analyze --module prog.ll -- --emit-pta-as-text \ + --alias-analysis=union-find --union-find-aa=ctx-sens +EOF +} + +# --------------------------------------------------------------------------- +# argument parsing +# --------------------------------------------------------------------------- +while [[ $# -gt 0 ]]; do + case "$1" in + --project) MODE="project"; PROJECT_DIR="$2"; shift 2 ;; + --sources) MODE="sources"; SOURCES="$2"; shift 2 ;; + --module) MODE="module"; MODULE="$2"; shift 2 ;; + --build-cmd) BUILD_CMD="$2"; shift 2 ;; + --binary) BINARY="$2"; shift 2 ;; + --keep-going) KEEP_GOING="true"; shift ;; + -a|--analysis) ANALYSES+=("$2"); shift 2 ;; + -E|--entry) ENTRY_POINTS+=("$2"); shift 2 ;; + -C|--call-graph) CALL_GRAPH="$2"; shift 2 ;; + -P|--alias-analysis) ALIAS_ANALYSIS="$2"; shift 2 ;; + --analysis-config) ANALYSIS_CONFIG="$2"; shift 2 ;; + -o|--out) OUT_DIR="$2"; shift 2 ;; + -h|--help) usage; exit 0 ;; + --) shift; PASSTHROUGH=("$@"); break ;; + *) echo "phasar-analyze: unknown option '$1' (try --help)" >&2; exit 2 ;; + esac +done + +# default mode: analyze the current directory as a project +if [ -z "$MODE" ]; then + MODE="project" + PROJECT_DIR="." +fi + +# --------------------------------------------------------------------------- +# WLLVM environment — pinned to the LLVM version PhASAR understands +# --------------------------------------------------------------------------- +if [ ! -d "$LLVM_BIN_DIR" ]; then + echo "phasar-analyze: LLVM toolchain not found at '$LLVM_BIN_DIR'." >&2 + echo " Set PHASAR_IR_LLVM_VERSION to the version PhASAR was built with," >&2 + echo " or PHASAR_LLVM_BIN_DIR to the directory holding clang/llvm-link." >&2 + exit 1 +fi +export PATH="$LLVM_BIN_DIR:$PATH" +export LLVM_COMPILER="clang" +export LLVM_COMPILER_PATH="$LLVM_BIN_DIR" +# embed debug info so PhASAR's type hierarchy and source-level reporting work +export LLVM_BITCODE_GENERATION_FLAGS="-g ${LLVM_BITCODE_GENERATION_FLAGS:-}" + +mkdir -p "$WORKDIR" +WHOLE_PROGRAM_BC="$WORKDIR/whole-program.bc" + +# newest file under $1 that carries an embedded WLLVM bitcode section (.llvm_bc). +# A raw grep for the section marker works for executables, shared objects and +# static archives alike, without depending on readelf or a specific tool flag. +find_bc_artifact() { + local root="$1" + find "$root" -type f \( -perm -u+x -o -name '*.so' -o -name '*.so.*' -o -name '*.a' \) \ + -printf '%T@ %p\n' 2>/dev/null | sort -nr | while read -r _ path; do + if LC_ALL=C grep -qa '\.llvm_bc' "$path" 2>/dev/null; then + echo "$path"; return 0 + fi + done +} + +case "$MODE" in + # ----------------------------------------------------------------------- + module) + [ -f "$MODULE" ] || { echo "phasar-analyze: module '$MODULE' not found" >&2; exit 1; } + WHOLE_PROGRAM_BC="$MODULE" + ;; + + # ----------------------------------------------------------------------- + sources) + [ -n "$SOURCES" ] || { echo "phasar-analyze: --sources is empty" >&2; exit 1; } + echo ">> Compiling sources to LLVM IR with clang-${LLVM_VERSION} ..." + objs=() + i=0 + for src in $SOURCES; do + [ -f "$src" ] || { echo "phasar-analyze: source '$src' not found" >&2; exit 1; } + case "$src" in + *.c) cc="clang" ;; + *.cc|*.cpp|*.cxx|*.C) cc="clang++" ;; + *) echo "phasar-analyze: unsupported source '$src'" >&2; exit 1 ;; + esac + obj="$WORKDIR/unit_${i}.bc" + "$cc" -g -O0 -Xclang -disable-O0-optnone -emit-llvm -c "$src" -o "$obj" + objs+=("$obj") + i=$((i + 1)) + done + echo ">> Linking $i module(s) into $WHOLE_PROGRAM_BC ..." + llvm-link "${objs[@]}" -o "$WHOLE_PROGRAM_BC" + ;; + + # ----------------------------------------------------------------------- + project) + [ -d "$PROJECT_DIR" ] || { echo "phasar-analyze: project dir '$PROJECT_DIR' not found" >&2; exit 1; } + export CC="wllvm" + export CXX="wllvm++" + + echo ">> Building project in '$PROJECT_DIR' with WLLVM (clang-${LLVM_VERSION}) ..." + ( + cd "$PROJECT_DIR" + set +e + if [ -n "$BUILD_CMD" ]; then + echo ">> Using custom build command: $BUILD_CMD" + bash -c "$BUILD_CMD" + elif [ -f "CMakeLists.txt" ]; then + echo ">> Detected CMake project" + cmake -S . -B build-wllvm -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_C_COMPILER=wllvm -DCMAKE_CXX_COMPILER=wllvm++ + cmake --build build-wllvm -j"$(nproc)" + elif [ -x "configure" ]; then + echo ">> Detected autotools project" + ./configure && make -j"$(nproc)" + elif [ -f "Makefile" ] || [ -f "makefile" ]; then + echo ">> Detected Makefile" + make -j"$(nproc)" + else + echo "phasar-analyze: could not detect a build system in '$PROJECT_DIR'." >&2 + echo " Provide one explicitly with --build-cmd \"...\"." >&2 + exit 1 + fi + rc=$? + if [ "$rc" -ne 0 ] && [ "$KEEP_GOING" != "true" ]; then + echo "phasar-analyze: build failed (exit $rc). Use --keep-going to analyze anyway." >&2 + exit "$rc" + fi + ) + + # locate the artifact holding bitcode + if [ -z "$BINARY" ]; then + echo ">> Autodetecting a build artifact that carries bitcode ..." + BINARY="$(find_bc_artifact "$PROJECT_DIR" || true)" + [ -n "$BINARY" ] || { + echo "phasar-analyze: no artifact with embedded bitcode found under '$PROJECT_DIR'." >&2 + echo " Point at it explicitly with --binary PATH." >&2 + exit 1 + } + echo ">> Using artifact: $BINARY" + fi + + echo ">> Extracting whole-program bitcode ..." + case "$BINARY" in + *.a) extract-bc -b "$BINARY" -o "$WHOLE_PROGRAM_BC" ;; + *) extract-bc "$BINARY" -o "$WHOLE_PROGRAM_BC" ;; + esac + ;; +esac + +[ -f "$WHOLE_PROGRAM_BC" ] || { echo "phasar-analyze: no bitcode produced at '$WHOLE_PROGRAM_BC'" >&2; exit 1; } +echo ">> Whole-program module ready: $WHOLE_PROGRAM_BC" + +# --------------------------------------------------------------------------- +# assemble and run phasar-cli +# --------------------------------------------------------------------------- +cli_args=(-m "$WHOLE_PROGRAM_BC") +for a in "${ANALYSES[@]}"; do cli_args+=(-D "$a"); done +for e in "${ENTRY_POINTS[@]}"; do cli_args+=(-E "$e"); done +[ -n "$CALL_GRAPH" ] && cli_args+=(-C "$CALL_GRAPH") +[ -n "$ALIAS_ANALYSIS" ] && cli_args+=(--alias-analysis="$ALIAS_ANALYSIS") +[ -n "$ANALYSIS_CONFIG" ] && cli_args+=(--analysis-config="$ANALYSIS_CONFIG") +[ -n "$OUT_DIR" ] && cli_args+=(-O "$OUT_DIR") +# if no analysis and no explicit emit was requested, at least emit the IR +if [ "${#ANALYSES[@]}" -eq 0 ] && [ "${#PASSTHROUGH[@]}" -eq 0 ]; then + cli_args+=(--emit-ir) +fi +[ "${#PASSTHROUGH[@]}" -gt 0 ] && cli_args+=("${PASSTHROUGH[@]}") + +echo ">> Running: phasar-cli ${cli_args[*]}" +exec phasar-cli "${cli_args[@]}" From 1c3ceb207d10345feb253652a73039b0b8027135 Mon Sep 17 00:00:00 2001 From: Ingo Budde Date: Fri, 7 Aug 2026 14:20:54 +0200 Subject: [PATCH 2/8] Use lld linker in docker tooling --- Dockerfile | 9 +++++++-- .../04-double-free/double-free-config.json | 20 ++++++++++++++++++- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 58b69b5948..8199ad550c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,10 +13,13 @@ ARG llvm_version # Install the LLVM/clang toolchain plus the pieces the whole-program wrapper needs: # `llvm-` provides llvm-link/llvm-dis used by WLLVM, `python3-pip` installs wllvm. +# `lld-` is LLVM's linker: PhASAR's Release build uses ThinLTO, whose bitcode +# objects the default GNU ld (gold plugin) fails to link on LLVM 22 — lld links +# them natively, so we keep LTO enabled and link with it (see -fuse-ld=lld below). RUN --mount=type=bind,source=./utils/InstallAptDependencies.sh,target=/InstallAptDependencies.sh \ set -eux; \ ./InstallAptDependencies.sh --noninteractive --llvm-version "${llvm_version}" \ - tzdata "clang-tools-${llvm_version}" "llvm-${llvm_version}" python3-pip file; \ + tzdata "clang-tools-${llvm_version}" "llvm-${llvm_version}" "lld-${llvm_version}" python3-pip file; \ pip3 install --no-cache-dir --break-system-packages wllvm ENV CC=/usr/bin/clang-${llvm_version} \ @@ -37,7 +40,9 @@ RUN --mount=type=bind,source=.,target=/usr/src/phasar,rw \ -DPHASAR_LLVM_VERSION="${phasar_llvm_version}" \ -DPHASAR_TARGET_ARCH="" \ -DPHASAR_ENABLE_SANITIZERS=ON \ - -DPHASAR_ALLOW_LTO_IN_RELEASE_BUILD=OFF \ + -DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=lld" \ + -DCMAKE_SHARED_LINKER_FLAGS="-fuse-ld=lld" \ + -DCMAKE_MODULE_LINKER_FLAGS="-fuse-ld=lld" \ -DPHASAR_USE_Z3=ON \ -DPHASAR_BUILD_UNITTESTS=$RUN_TESTS \ -DPHASAR_BUILD_IR=$RUN_TESTS \ diff --git a/examples/docker-whole-program/04-double-free/double-free-config.json b/examples/docker-whole-program/04-double-free/double-free-config.json index 1ada7f62b4..043b392288 100644 --- a/examples/docker-whole-program/04-double-free/double-free-config.json +++ b/examples/docker-whole-program/04-double-free/double-free-config.json @@ -4,7 +4,25 @@ "functions": [ { "name": "free", - "params": { "source": [0], "sink": [0] } + "params": { + "source": [ + 0 + ], + "sink": [ + 0 + ] + } + }, + { + "name": "_ZdlPv", + "params": { + "source": [ + 0 + ], + "sink": [ + 0 + ] + } } ] } From 6a3c43eb4b0a4305310ec3b68338b890a22cc5a0 Mon Sep 17 00:00:00 2001 From: Ingo Budde Date: Fri, 7 Aug 2026 15:32:01 +0200 Subject: [PATCH 3/8] Make phasar-analyze.sh more robust when expected arguments are missing --- utils/phasar-analyze.sh | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/utils/phasar-analyze.sh b/utils/phasar-analyze.sh index 34ad931638..72d1025083 100755 --- a/utils/phasar-analyze.sh +++ b/utils/phasar-analyze.sh @@ -104,20 +104,30 @@ EOF # --------------------------------------------------------------------------- # argument parsing # --------------------------------------------------------------------------- +# Fail with a friendly message (never a bash 'unbound variable' crash) when an +# option that expects a value is given without one. $1 = flag, $2 = the value +# (may be unset), $3 = remaining arg count ($#). +require_value() { + if [ "$3" -lt 2 ]; then + echo "phasar-analyze: option '$1' requires an argument (try --help)" >&2 + exit 2 + fi +} + while [[ $# -gt 0 ]]; do case "$1" in - --project) MODE="project"; PROJECT_DIR="$2"; shift 2 ;; - --sources) MODE="sources"; SOURCES="$2"; shift 2 ;; - --module) MODE="module"; MODULE="$2"; shift 2 ;; - --build-cmd) BUILD_CMD="$2"; shift 2 ;; - --binary) BINARY="$2"; shift 2 ;; + --project) require_value "$1" "${2:-}" "$#"; MODE="project"; PROJECT_DIR="$2"; shift 2 ;; + --sources) require_value "$1" "${2:-}" "$#"; MODE="sources"; SOURCES="$2"; shift 2 ;; + --module) require_value "$1" "${2:-}" "$#"; MODE="module"; MODULE="$2"; shift 2 ;; + --build-cmd) require_value "$1" "${2:-}" "$#"; BUILD_CMD="$2"; shift 2 ;; + --binary) require_value "$1" "${2:-}" "$#"; BINARY="$2"; shift 2 ;; --keep-going) KEEP_GOING="true"; shift ;; - -a|--analysis) ANALYSES+=("$2"); shift 2 ;; - -E|--entry) ENTRY_POINTS+=("$2"); shift 2 ;; - -C|--call-graph) CALL_GRAPH="$2"; shift 2 ;; - -P|--alias-analysis) ALIAS_ANALYSIS="$2"; shift 2 ;; - --analysis-config) ANALYSIS_CONFIG="$2"; shift 2 ;; - -o|--out) OUT_DIR="$2"; shift 2 ;; + -a|--analysis) require_value "$1" "${2:-}" "$#"; ANALYSES+=("$2"); shift 2 ;; + -E|--entry) require_value "$1" "${2:-}" "$#"; ENTRY_POINTS+=("$2"); shift 2 ;; + -C|--call-graph) require_value "$1" "${2:-}" "$#"; CALL_GRAPH="$2"; shift 2 ;; + -P|--alias-analysis) require_value "$1" "${2:-}" "$#"; ALIAS_ANALYSIS="$2"; shift 2 ;; + --analysis-config) require_value "$1" "${2:-}" "$#"; ANALYSIS_CONFIG="$2"; shift 2 ;; + -o|--out) require_value "$1" "${2:-}" "$#"; OUT_DIR="$2"; shift 2 ;; -h|--help) usage; exit 0 ;; --) shift; PASSTHROUGH=("$@"); break ;; *) echo "phasar-analyze: unknown option '$1' (try --help)" >&2; exit 2 ;; From 5d357da134a4a0a39435169bb0a8a4370ed4811d Mon Sep 17 00:00:00 2001 From: Ingo Budde Date: Fri, 7 Aug 2026 15:57:29 +0200 Subject: [PATCH 4/8] Set /work as workdir in Dockerfile --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index 8199ad550c..b4be90d18f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -63,4 +63,6 @@ RUN chmod +x /usr/local/bin/phasar-analyze # `-e PHASAR_IR_LLVM_VERSION=NN`. ENV PHASAR_IR_LLVM_VERSION=${llvm_version} +WORKDIR /work + ENTRYPOINT [ "phasar-analyze" ] From 3b061b4ddd2409698535f8cecb5dab0dcea7e9de Mon Sep 17 00:00:00 2001 From: Ingo Budde Date: Fri, 7 Aug 2026 16:17:22 +0200 Subject: [PATCH 5/8] Mark experimental analyses in USAGE.docker.md --- USAGE.docker.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/USAGE.docker.md b/USAGE.docker.md index c0700c37e6..71af16ebeb 100644 --- a/USAGE.docker.md +++ b/USAGE.docker.md @@ -104,19 +104,19 @@ Select a data-flow analysis with `-a` (repeatable). Available analyses: | Flag (`-a`) | Analysis | Needs `--analysis-config` | |-------------|----------|:---:| | `ifds-uninit` | Uses of uninitialized variables | | -| `ifds-const` | Variables actually mutated through the program | | -| `ifds-type` | Simple type analysis | | +| `ifds-const` | **EXPERIMENTAL:** Variables actually mutated through the program | | +| `ifds-type` | **EXPERIMENTAL:** Simple type analysis | | | `ide-lca` | Linear constant propagation | | | `ide-iia` | Instruction-interaction (what influences what) | | -| `ide-stdio-ts` | libc file-I/O typestate (invalid usages) | | -| `ide-openssl-ts` | OpenSSL EVP typestate | | +| `ide-stdio-ts` | **EXPERIMENTAL:** libc file-I/O typestate (invalid usages) | | +| `ide-openssl-ts` | **EXPERIMENTAL:** OpenSSL EVP typestate | | | `ifds-taint` | Alias-aware taint analysis | ✔ | | `ide-xtaint` | Taint analysis with limited field-sensitivity | ✔ | | `ifds-fieldsens-taint` | Field-sensitive taint (CFL) | ✔ | | `monoifds-taint` | Taint on the MonoIFDS solver | ✔ | | `sparse-ifds-taint` | Taint on the SparseIFDS solver | ✔ | -| `inter-mono-taint` | Taint via inter-procedural Monotone Framework | ✔ | -| `intra-mono-fca` | Intra-procedural full constant propagation (Monotone) | | +| `inter-mono-taint` | **EXPERIMENTAL:** Taint via inter-procedural Monotone Framework | ✔ | +| `intra-mono-fca` | **EXPERIMENTAL:** Intra-procedural full constant propagation (Monotone) | | If you omit `-a` (and don't request an emitter), the wrapper just builds the module and emits its IR (`--emit-ir`), which is handy to confirm the build. From a00dac5db35d2c14b5e9db749cbcb405b5e1fbdb Mon Sep 17 00:00:00 2001 From: Fabian Schiebel Date: Tue, 11 Aug 2026 18:05:32 +0200 Subject: [PATCH 6/8] pre-commit --- docs/regression-testing-lit.md | 182 ++++++++++++++++++ .../01-uninitialized-variables/main.c | 9 +- .../01-uninitialized-variables/util.c | 10 +- .../02-linear-constant/main.c | 12 +- .../docker-whole-program/03-taint-leak/main.c | 6 +- .../03-taint-leak/taint-config.json | 15 +- .../04-double-free/main.c | 8 +- .../05-file-io-typestate/main.c | 10 +- .../06-type-hierarchy/main.cpp | 21 +- .../07-call-graph/main.cpp | 22 ++- .../docker-whole-program/08-points-to/main.c | 12 +- .../09-instruction-interaction/main.c | 10 +- .../docker-whole-program/10-statistics/main.c | 31 +-- .../docker-whole-program/11-library/mylib.c | 10 +- 14 files changed, 286 insertions(+), 72 deletions(-) create mode 100644 docs/regression-testing-lit.md diff --git a/docs/regression-testing-lit.md b/docs/regression-testing-lit.md new file mode 100644 index 0000000000..b5a733dd2b --- /dev/null +++ b/docs/regression-testing-lit.md @@ -0,0 +1,182 @@ +# Task: Add LIT/FileCheck Regression Testing (Hybrid with gtest) + +## Goal + +Add an LLVM-style LIT + FileCheck regression-test suite that drives +`phasar-cli` directly on IR/source fixtures, checking its textual output +(JSON export, dot export, raw results, diagnostics). This complements the +existing gtest suite, which stays as-is for API/library-level unit tests. + +## Why + +- PhASAR already builds on LLVM/Clang; LIT/FileCheck is the tool the LLVM + ecosystem uses for exactly this kind of test, so the pattern is familiar + to contributors. +- Today, a bug reported as "run `phasar-cli` on this `.ll` file, output is + wrong" gets translated into a `TEST_F` with an inline C++ ground-truth + map. That's boilerplate-heavy and requires a full unittest rebuild for a + one-line fixture change. +- With LIT, the same bug becomes a `.ll`/`.c` file with `// RUN:` and + `// CHECK:` lines next to it. Lower friction for contributors and + reviewers (CONTRIBUTING.md already asks bug reports to include IR files). + +## Non-goals + +- Do not migrate existing gtest tests to LIT. gtest keeps testing internal + APIs and data structures; LIT only tests CLI-observable behavior. +- Do not add fuzzing or property-based testing (separate effort). + +## Decision rule (add to CONTRIBUTING.md) + +- Bug is only observable/reproducible via `phasar-cli` output (JSON/dot + export, diagnostics, exit code) -> LIT test. +- Bug is in an internal API/data structure not exposed via the CLI -> gtest. +- When in doubt, prefer LIT for anything that started life as "here is an + `.ll` file that produces the wrong result." + +## Directory layout + +``` +test/lit/ + lit.cfg.py # lit configuration (Python) + lit.site.cfg.py.in # configured by CMake, generates build-tree lit.site.cfg.py + README.md # how to write/run a LIT test + DataFlow/ + ifds-uninitialized/ + basic.ll + ... + ControlFlow/ + icfg-cha/ + ... + Export/ + json-export/ + ... +``` + +Mirror the `unittests/PhasarLLVM/...` module layout so contributors can +find the LIT equivalent of a gtest directory by analogy. + +## CMake work + +1. `find_package` / `find_program(LLVM_LIT ...)`: + - Prefer `llvm-lit` shipped alongside the LLVM install phasar already + depends on (same `LLVM_TOOLS_BINARY_DIR` used by `generate_ll_file` + in `cmake/phasar_macros.cmake`). + - Fall back to a `lit` found via `find_package(Python3 COMPONENTS + Interpreter)` + `pip`-installed `lit` package if `llvm-lit` is not + found. Emit a clear warning and skip the target (do not hard-fail + the whole build) if neither is available. +2. New `cmake/add_lit_tests.cmake` (or extend `phasar_macros.cmake`) with + a `add_phasar_lit_testsuite()` function that: + - Configures `test/lit/lit.site.cfg.py.in` -> build dir, substituting + `@PHASAR_CLI_PATH@`, `@LLVM_TOOLS_BINARY_DIR@`, `@FILECHECK_PATH@`, + `@PHASAR_SOURCE_DIR@`. + - Registers a `check-phasar-lit` custom target running + `${LLVM_LIT} -sv test/lit` from the build directory, depending on + `phasar-cli` and `FileCheck` (the latter usually ships with the LLVM + dev package; `find_program` it the same way `LLVM_COV_PATH` is + found today). +3. Top-level `check-phasar-tests` (or reuse an existing umbrella target if + one exists) depends on both `check-phasar-unittests` and + `check-phasar-lit`, so CI can run one command for everything. +4. Gate the whole feature behind a `PHASAR_BUILD_LIT_TESTS` option + (default ON, matching the `PHASAR_BUILD_UNITTESTS` precedent in + `BUILD.md`), so environments without `lit`/`FileCheck` available can + configure it OFF cleanly instead of failing configure. + +## `lit.cfg.py` content + +- `config.name = 'PhASAR'` +- `config.suffixes = ['.ll', '.c', '.cpp']` +- `config.test_source_root` = `test/lit` +- `config.test_exec_root` = build-tree equivalent +- Substitutions: `%phasar-cli` -> path to built `phasar-cli`, `%clang`, + `%clangxx`, `%opt`, `%FileCheck` -> resolved via the same LLVM tool + search phasar already does in `generate_ll_file` (reuse + `LLVM_TOOLS_BINARY_DIR` / `PHASAR_LLVM_VERSION`, do not re-implement + version discovery). +- No special LLVM-style feature flags needed initially (no target + triples, no shared-lib-only exclusions) -- keep the config minimal and + add exclusions later only if a real portability issue shows up. + +## Fixture format (example) + +`test/lit/DataFlow/ifds-uninitialized/basic.c`: + +```c +// RUN: %clang -S -emit-llvm -Xclang -disable-O0-optnone %s -o - \ +// RUN: | opt -passes=mem2reg -S \ +// RUN: | %phasar-cli -D ifds-uninit --emit-raw-results - \ +// RUN: | FileCheck %s + +int main() { + int x; + return x; // CHECK: UndefUse at {{.*}}basic.c:{{[0-9]+}} +} +``` + +Prefer piping through `%clang`/`opt` inline (as above) over checking in +pre-generated `.ll` files, consistent with how `generate_ll_file` already +regenerates IR at build time rather than committing `.ll` fixtures -- keeps +IR fixtures in sync with the LLVM version actually in use. + +## CI work (`.github/workflows/ci.yml`) + +- Add `check-phasar-lit` (or the umbrella target) to the existing build + step for every matrix leg, not just one -- LIT tests are cheap per-test, + no need to restrict to a single leg the way `run_sample_programs` is + restricted to `DebugLibdeps`. +- Ensure `FileCheck` is present in the CI image / LLVM install used by the + matrix (verify for both LLVM 16 and 22.1 legs). +- `DebugCov` leg: confirm `ccov-all` filtering excludes `test/lit/` the + same way it excludes `external/` and `unittests/` today + (`CMakeLists.txt:283-303`). + +## Migration / first tests + +Do not do a big-bang migration. Seed the suite with: + +1. 3-5 new tests covering CLI-observable behavior not currently tested at + all (e.g. JSON/dot export format, CLI error handling on malformed + input, exit codes). +2. Port the two known IR-dependent/disabled cases surfaced during the + investigation as good LIT candidates: + - `unittests/PhasarLLVM/ControlFlow/LLVMBasedICFGExportTest.cpp` + (`DISABLED_` test referencing issue #741) -- re-express as a LIT test + checking export output directly, since the original problem was + IR-dependent output instability, which FileCheck's `{{regex}}` + patterns handle more gracefully than exact `EXPECT_EQ`. + - One of the flaky tests currently excluded on Ubuntu > 22 in + `unittests/CMakeLists.txt:16-27` (e.g. `IDEGeneralizedLCATest`) -- + evaluate whether the flakiness is about exact-match brittleness that + a FileCheck-based regex check would fix. Do not blindly port all of + them; some flakiness may be a real solver bug, not a test-format + issue. + +## Documentation + +- `test/lit/README.md`: how to write a test, available substitutions, how + to run just the LIT suite locally (`ninja check-phasar-lit` or direct + `llvm-lit -sv build/test/lit`). +- `CONTRIBUTING.md`: add the decision rule above; update the "please + provide unit tests" bug-fix guidance to mention LIT as the default for + CLI-reproducible bugs. +- `BUILD.md`: document `PHASAR_BUILD_LIT_TESTS` next to + `PHASAR_BUILD_UNITTESTS`. + +## Open questions to resolve during implementation + +- Minimum `lit` package version / whether to pin it in a + `requirements.txt` for reproducibility across CI images. +- Whether `FileCheck` needs to be built from source in any CI leg (LLVM + dev packages sometimes omit test-only tools) or is reliably present + wherever `clang`/`opt` already are. +- Whether ARM (`ubuntu-24.04-arm` matrix leg) needs any LIT-specific + exclusions. + +## Rough effort estimate + +- CMake/lit plumbing + CI wiring: 1-2 days. +- Seed test suite (5-10 tests) + docs: 1 day. +- Total: ~2-3 days for a working, documented, CI-gated suite; further + tests added incrementally afterward at low marginal cost. diff --git a/examples/docker-whole-program/01-uninitialized-variables/main.c b/examples/docker-whole-program/01-uninitialized-variables/main.c index 75dd0bbb1b..ab672aa311 100644 --- a/examples/docker-whole-program/01-uninitialized-variables/main.c +++ b/examples/docker-whole-program/01-uninitialized-variables/main.c @@ -1,7 +1,8 @@ -#include #include "util.h" + +#include int main(int argc, char **argv) { - int x = compute(argc - 1); - printf("%d\n", x); - return 0; + int x = compute(argc - 1); + printf("%d\n", x); + return 0; } diff --git a/examples/docker-whole-program/01-uninitialized-variables/util.c b/examples/docker-whole-program/01-uninitialized-variables/util.c index eb609828ba..1ac2e8e712 100644 --- a/examples/docker-whole-program/01-uninitialized-variables/util.c +++ b/examples/docker-whole-program/01-uninitialized-variables/util.c @@ -1,8 +1,8 @@ #include "util.h" int compute(int seed) { - int result; /* BUG: not initialized on the seed<=0 path */ - if (seed > 0) { - result = seed * 2; - } - return result; /* uninitialized use when seed <= 0 */ + int result; /* BUG: not initialized on the seed<=0 path */ + if (seed > 0) { + result = seed * 2; + } + return result; /* uninitialized use when seed <= 0 */ } diff --git a/examples/docker-whole-program/02-linear-constant/main.c b/examples/docker-whole-program/02-linear-constant/main.c index 74b87e0bce..a37ebb027a 100644 --- a/examples/docker-whole-program/02-linear-constant/main.c +++ b/examples/docker-whole-program/02-linear-constant/main.c @@ -1,10 +1,10 @@ #include /* IDE linear constant analysis tracks integer constants through linear ops. */ int main() { - int a = 6; - int b = a + 1; /* 7 */ - int c = a * 7; /* 42 */ - int d = b - 3; /* 4 */ - printf("%d %d %d\n", b, c, d); - return 0; + int a = 6; + int b = a + 1; /* 7 */ + int c = a * 7; /* 42 */ + int d = b - 3; /* 4 */ + printf("%d %d %d\n", b, c, d); + return 0; } diff --git a/examples/docker-whole-program/03-taint-leak/main.c b/examples/docker-whole-program/03-taint-leak/main.c index e1bb3e7074..a8f9f4a74f 100644 --- a/examples/docker-whole-program/03-taint-leak/main.c +++ b/examples/docker-whole-program/03-taint-leak/main.c @@ -3,7 +3,7 @@ extern char *source(void); extern void sink(const char *data); int main() { - char *tainted = source(); - sink(tainted); /* LEAK: tainted value reaches the sink */ - return 0; + char *tainted = source(); + sink(tainted); /* LEAK: tainted value reaches the sink */ + return 0; } diff --git a/examples/docker-whole-program/03-taint-leak/taint-config.json b/examples/docker-whole-program/03-taint-leak/taint-config.json index b5c9c2aeb4..8b925ef7d9 100644 --- a/examples/docker-whole-program/03-taint-leak/taint-config.json +++ b/examples/docker-whole-program/03-taint-leak/taint-config.json @@ -2,7 +2,18 @@ "name": "demo-source-sink", "version": 1.0, "functions": [ - { "name": "source", "ret": "source", "params": {} }, - { "name": "sink", "params": { "sink": [0] } } + { + "name": "source", + "ret": "source", + "params": {} + }, + { + "name": "sink", + "params": { + "sink": [ + 0 + ] + } + } ] } diff --git a/examples/docker-whole-program/04-double-free/main.c b/examples/docker-whole-program/04-double-free/main.c index cf3ec429d6..a67d4e3baa 100644 --- a/examples/docker-whole-program/04-double-free/main.c +++ b/examples/docker-whole-program/04-double-free/main.c @@ -1,7 +1,7 @@ #include int main() { - int *p = (int *)malloc(sizeof(int) * 4); - free(p); - free(p); /* BUG: double free (p flows from one free() to another) */ - return 0; + int *p = (int *)malloc(sizeof(int) * 4); + free(p); + free(p); /* BUG: double free (p flows from one free() to another) */ + return 0; } diff --git a/examples/docker-whole-program/05-file-io-typestate/main.c b/examples/docker-whole-program/05-file-io-typestate/main.c index 7df14c25e1..60701c184b 100644 --- a/examples/docker-whole-program/05-file-io-typestate/main.c +++ b/examples/docker-whole-program/05-file-io-typestate/main.c @@ -1,8 +1,8 @@ #include int main() { - FILE *f = fopen("data.txt", "r"); - fclose(f); - int c = fgetc(f); /* BUG: use of FILE* after fclose */ - printf("%d\n", c); - return 0; + FILE *f = fopen("data.txt", "r"); + fclose(f); + int c = fgetc(f); /* BUG: use of FILE* after fclose */ + printf("%d\n", c); + return 0; } diff --git a/examples/docker-whole-program/06-type-hierarchy/main.cpp b/examples/docker-whole-program/06-type-hierarchy/main.cpp index bcc0513c32..160a8401b2 100644 --- a/examples/docker-whole-program/06-type-hierarchy/main.cpp +++ b/examples/docker-whole-program/06-type-hierarchy/main.cpp @@ -1,8 +1,17 @@ -struct Animal { virtual ~Animal() = default; virtual void speak() const = 0; }; -struct Dog : Animal { void speak() const override; }; -struct Cat : Animal { void speak() const override; }; -struct Puppy : Dog { void speak() const override; }; -void Dog::speak() const {} -void Cat::speak() const {} +struct Animal { + virtual ~Animal() = default; + virtual void speak() const = 0; +}; +struct Dog : Animal { + void speak() const override; +}; +struct Cat : Animal { + void speak() const override; +}; +struct Puppy : Dog { + void speak() const override; +}; +void Dog::speak() const {} +void Cat::speak() const {} void Puppy::speak() const {} int main() { return 0; } diff --git a/examples/docker-whole-program/07-call-graph/main.cpp b/examples/docker-whole-program/07-call-graph/main.cpp index 9deb88e3ca..14616bb82b 100644 --- a/examples/docker-whole-program/07-call-graph/main.cpp +++ b/examples/docker-whole-program/07-call-graph/main.cpp @@ -1,8 +1,18 @@ -struct Shape { virtual double area() const { return 0.0; } virtual ~Shape() = default; }; -struct Circle : Shape { double area() const override { return 3.14; } }; -struct Square : Shape { double area() const override { return 4.0; } }; -static double total(const Shape *s) { return s->area(); } /* virtual call site */ +struct Shape { + virtual double area() const { return 0.0; } + virtual ~Shape() = default; +}; +struct Circle : Shape { + double area() const override { return 3.14; } +}; +struct Square : Shape { + double area() const override { return 4.0; } +}; +static double total(const Shape *s) { + return s->area(); +} /* virtual call site */ int main() { - Circle c; Square s; - return (int)(total(&c) + total(&s)); + Circle c; + Square s; + return (int)(total(&c) + total(&s)); } diff --git a/examples/docker-whole-program/08-points-to/main.c b/examples/docker-whole-program/08-points-to/main.c index ce7caeb306..12d470089e 100644 --- a/examples/docker-whole-program/08-points-to/main.c +++ b/examples/docker-whole-program/08-points-to/main.c @@ -1,8 +1,8 @@ int main() { - int x = 0, y = 0; - int *p = &x; - int *q = p; /* q aliases p (both point to x) */ - int *r = &y; /* r points to y (must-not-alias p/q) */ - *q = 5; - return *p + *r; + int x = 0, y = 0; + int *p = &x; + int *q = p; /* q aliases p (both point to x) */ + int *r = &y; /* r points to y (must-not-alias p/q) */ + *q = 5; + return *p + *r; } diff --git a/examples/docker-whole-program/09-instruction-interaction/main.c b/examples/docker-whole-program/09-instruction-interaction/main.c index b4b8b78ee3..aa09f43d98 100644 --- a/examples/docker-whole-program/09-instruction-interaction/main.c +++ b/examples/docker-whole-program/09-instruction-interaction/main.c @@ -1,9 +1,9 @@ #include /* IDE instruction-interaction: which instructions influence which others. */ int main() { - int secret = 42; - int derived = secret + 1; /* influenced by secret */ - int unrelated = 7; /* independent */ - printf("%d %d\n", derived, unrelated); - return 0; + int secret = 42; + int derived = secret + 1; /* influenced by secret */ + int unrelated = 7; /* independent */ + printf("%d %d\n", derived, unrelated); + return 0; } diff --git a/examples/docker-whole-program/10-statistics/main.c b/examples/docker-whole-program/10-statistics/main.c index 08c4b79137..decb005f95 100644 --- a/examples/docker-whole-program/10-statistics/main.c +++ b/examples/docker-whole-program/10-statistics/main.c @@ -1,24 +1,25 @@ #include #include -/* A small program with a mix of constructs so the IR statistics are non-trivial: - globals, a heap allocation, a loop with loads/stores, and two functions. */ +/* A small program with a mix of constructs so the IR statistics are + non-trivial: globals, a heap allocation, a loop with loads/stores, and two + functions. */ int global_counter = 0; static int accumulate(int *arr, int n) { - int sum = 0; - for (int i = 0; i < n; ++i) { - sum += arr[i]; - global_counter++; - } - return sum; + int sum = 0; + for (int i = 0; i < n; ++i) { + sum += arr[i]; + global_counter++; + } + return sum; } int main(void) { - int *data = (int *)malloc(sizeof(int) * 4); - for (int i = 0; i < 4; ++i) { - data[i] = i * i; - } - printf("%d (counter=%d)\n", accumulate(data, 4), global_counter); - free(data); - return 0; + int *data = (int *)malloc(sizeof(int) * 4); + for (int i = 0; i < 4; ++i) { + data[i] = i * i; + } + printf("%d (counter=%d)\n", accumulate(data, 4), global_counter); + free(data); + return 0; } diff --git a/examples/docker-whole-program/11-library/mylib.c b/examples/docker-whole-program/11-library/mylib.c index 0ac0519c3d..d47e9e5eaf 100644 --- a/examples/docker-whole-program/11-library/mylib.c +++ b/examples/docker-whole-program/11-library/mylib.c @@ -3,9 +3,9 @@ int add(int a, int b) { return a + b; } int unsafe(int flag) { - int v; - if (flag) { - v = 1; - } - return v; /* uninitialized when flag == 0 */ + int v; + if (flag) { + v = 1; + } + return v; /* uninitialized when flag == 0 */ } From 6dc3338bd5932340c9bd4c06f558ef8ff9266ad6 Mon Sep 17 00:00:00 2001 From: Fabian Schiebel Date: Tue, 11 Aug 2026 18:42:23 +0200 Subject: [PATCH 7/8] Some refactoring on the documentation --- USAGE.docker.md | 118 ++++++++++-------------- examples/docker-whole-program/README.md | 63 ++++--------- 2 files changed, 66 insertions(+), 115 deletions(-) diff --git a/USAGE.docker.md b/USAGE.docker.md index 71af16ebeb..94fe075608 100644 --- a/USAGE.docker.md +++ b/USAGE.docker.md @@ -1,9 +1,6 @@ # Analyzing a C/C++ Program with the PhASAR Docker Container -This guide shows how to analyze a whole C/C++ program end-to-end using the -PhASAR Docker image. You do **not** need to install LLVM, WLLVM, or PhASAR -yourself — the image bundles everything and orchestrates the required steps for -you. +This guide shows how to analyze a whole C/C++ program end-to-end using the PhASAR Docker image. You do **not** need to install LLVM, WLLVM, or PhASAR yourself — the image bundles everything and orchestrates the required steps for you. ## Contents - [How it works](#how-it-works) @@ -18,20 +15,23 @@ you. - [Configuration (environment variables)](#configuration-environment-variables) - [Troubleshooting](#troubleshooting) -## How it works +## How it Works -PhASAR analyzes **LLVM IR**, not C/C++ source directly. To analyze a real -program it must first be compiled to a single whole-program LLVM module. The -image's entry point, `phasar-analyze`, automates the whole pipeline: +PhASAR analyzes **LLVM IR**, not C/C++ source directly. To analyze a real program it must first be compiled to a single whole-program LLVM module. The image's entry point, `phasar-analyze`, automates the whole pipeline: -``` -your C/C++ project ──► build with WLLVM (clang) ──► extract-bc ──► whole-program.bc ──► phasar-cli ──► results +```mermaid +flowchart LR + A[/"your C/C++ project"/] --> B["build with WLLVM (clang)"] + B --> C(["extract-bc"]) + C --> D[/"whole-program.bc"/] + D --> E(["phasar-cli"]) + E --> F[/"results"/] + + linkStyle default stroke-width:3px ``` -1. **Build with WLLVM** — compiles your project with clang while preserving each - translation unit's bitcode (with `-g` for source-level reporting). -2. **`extract-bc`** — links all the preserved bitcode into one whole-program - module. +1. **Build with WLLVM** — compiles your project with clang while preserving each translation unit's bitcode (with `-g` for source-level reporting). +2. **`extract-bc`** — links all the preserved bitcode into one whole-program module. 3. **`phasar-cli`** — runs the requested analysis on that module. All three steps run inside the container; you just provide the program and pick @@ -45,17 +45,17 @@ From the repository root: docker build -t phasar . ``` -This builds PhASAR against LLVM 22 by default. To use a different LLVM major -version, pass build args (the value must match a version PhASAR supports, 16–22): +This builds PhASAR against LLVM 22 by default. To use a different LLVM major version, pass build args (the value must match a version PhASAR supports, currently 16–22): ```bash docker build -t phasar --build-arg llvm_version=16 --build-arg phasar_llvm_version=16 . ``` -> `phasar_llvm_version` is PhASAR's CMake version string — `22.1` for LLVM 22 -> (new release scheme), but `16` for LLVM 16. +> [!NOTE]- llvm_version vs phasar_llvm_version +> `phasar_llvm_version` is PhASAR's CMake version string — `22.1` for LLVM 22 (new release scheme), but `16` for LLVM 16. + -## Quick start +## Quick Start Analyze a project mounted at `/work` for uses of uninitialized variables: @@ -69,11 +69,9 @@ docker run --rm --user "$(id -u):$(id -g)" \ - `--project /work` autodetects the build system (cmake / `./configure` / make). - `-a ifds-uninit` selects the analysis. -See runnable, self-contained examples in -[`examples/docker-whole-program/`](examples/docker-whole-program/) — run them all -with `examples/docker-whole-program/run-all.sh`. +See runnable, self-contained examples in [`examples/docker-whole-program/`](examples/docker-whole-program/) — run them all with `examples/docker-whole-program/run-all.sh`. -## Providing input +## Providing Input `phasar-analyze` accepts several input modes — pick one: @@ -85,62 +83,55 @@ with `examples/docker-whole-program/run-all.sh`. | `--binary PATH` | You want to target a specific build artifact (exe, `.so`, `.a`) | `phasar --project /work --binary /work/build/libfoo.a -a ifds-uninit -E __ALL__` | | `--module FILE.ll\|.bc` | You already have LLVM IR | `phasar --module /work/prog.bc -a ifds-uninit` | -**Entry points.** By default analysis starts at `main`. Use `-E NAME` to set a -different entry point, or `-E __ALL__` for code without a `main` (e.g. a library): +**Entry points.** By default analysis starts at `main`. Use `-E NAME` to set a different entry point, or `-E __ALL__` for code without a `main` (e.g. a library): ```bash docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/work" phasar \ --project /work -a ifds-uninit -E __ALL__ ``` -In `--project` mode without `--binary`, the wrapper autodetects the newest build -artifact that carries embedded bitcode. If detection picks the wrong one, name it -with `--binary`. +In `--project` mode without `--binary`, the wrapper autodetects the newest build artifact that carries embedded bitcode. If detection picks the wrong one, name it with `--binary`. -## Choosing an analysis +## Choosing an Analysis Select a data-flow analysis with `-a` (repeatable). Available analyses: | Flag (`-a`) | Analysis | Needs `--analysis-config` | |-------------|----------|:---:| -| `ifds-uninit` | Uses of uninitialized variables | | -| `ifds-const` | **EXPERIMENTAL:** Variables actually mutated through the program | | -| `ifds-type` | **EXPERIMENTAL:** Simple type analysis | | -| `ide-lca` | Linear constant propagation | | -| `ide-iia` | Instruction-interaction (what influences what) | | -| `ide-stdio-ts` | **EXPERIMENTAL:** libc file-I/O typestate (invalid usages) | | -| `ide-openssl-ts` | **EXPERIMENTAL:** OpenSSL EVP typestate | | | `ifds-taint` | Alias-aware taint analysis | ✔ | | `ide-xtaint` | Taint analysis with limited field-sensitivity | ✔ | | `ifds-fieldsens-taint` | Field-sensitive taint (CFL) | ✔ | | `monoifds-taint` | Taint on the MonoIFDS solver | ✔ | | `sparse-ifds-taint` | Taint on the SparseIFDS solver | ✔ | +| `ifds-uninit` | Uses of uninitialized variables | | +| `ide-lca` | Linear constant propagation | | +| `ide-iia` | Instruction-interaction (what influences what) | | +| `ifds-const` | **EXPERIMENTAL:** Variables actually mutated through the program | | +| `ifds-type` | **EXPERIMENTAL:** Simple type analysis | | +| `ide-stdio-ts` | **EXPERIMENTAL:** libc file-I/O typestate (invalid usages) | | +| `ide-openssl-ts` | **EXPERIMENTAL:** OpenSSL EVP typestate | | | `inter-mono-taint` | **EXPERIMENTAL:** Taint via inter-procedural Monotone Framework | ✔ | | `intra-mono-fca` | **EXPERIMENTAL:** Intra-procedural full constant propagation (Monotone) | | If you omit `-a` (and don't request an emitter), the wrapper just builds the module and emits its IR (`--emit-ir`), which is handy to confirm the build. -## Alias analysis & call graph +## Alias Analysis & Call Graph These affect precision/performance of most analyses: - **Alias analysis** — `-P` / `--alias-analysis`: - `CFLAnders` (default, precise), `CFLSteens` (faster), `union-find` - (add `--union-find-aa=ctx-sens` or `ctx-ind-sens` after `--`). -- **Call-graph algorithm** — `-C` / `--call-graph`: - `otf` (default), `cha`, `rta`, `vta`, `nores`. + `CFLAnders` (default, legacy), `CFLSteens` (faster), `union-find` (add `--union-find-aa=ctx-sens` or `ctx-ind-sens` after `--`). +- **Call-graph algorithm** — `-C` / `--call-graph`: `otf` (default), `cha`, `rta`, `vta`, `nores`. ```bash docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/work" phasar \ --sources main.c -P CFLSteens -C cha -a ide-xtaint --analysis-config /work/tc.json ``` -## Getting results out +## Extracting Results -By default results print to **stdout**. You can also emit specific artifacts — -pass emitter flags after `--` (everything after `--` goes straight to -`phasar-cli`): +By default results print to **stdout**. You can also emit specific artifacts — pass emitter flags after `--` (everything after `--` goes straight to `phasar-cli`): | After `--` | Emits | |------------|-------| @@ -158,8 +149,7 @@ docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/work" phasar \ --sources main.cpp -- --emit-th-as-text --emit-cg-as-dot ``` -**Write to files instead of stdout** with `-o DIR` (the directory must exist and -be mounted). Results land in a timestamped subdirectory: +**Write to files instead of stdout** with `-o DIR` (the directory must exist and be mounted). Results land in a timestamped subdirectory: ```bash mkdir -p results @@ -168,13 +158,9 @@ docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/work" -w /work phasar \ # -> results/-/psr-cg.json, psr-th.json ``` -## Taint / typestate configuration +## Taint Configuration -Taint analyses (`ifds-taint`, `ide-xtaint`, `ifds-fieldsens-taint`, -`monoifds-taint`, …) require a JSON config that declares sources, sinks and -sanitizers, passed with `--analysis-config`. The config path must be inside the -mounted directory. Example (a double-free config — `free`'s argument is both a -source and a sink): +Taint analyses (`ifds-taint`, `ide-xtaint`, `ifds-fieldsens-taint`, `monoifds-taint`, ...) require a JSON config that declares sources, sinks and sanitizers, passed with `--analysis-config`. The config path must be inside the mounted directory. Example (a double-free config — `free`'s argument is both a source and a sink): ```json { @@ -191,13 +177,11 @@ docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/work" -w /work phasar \ --sources main.c -a ifds-taint --analysis-config double-free-config.json ``` -`ret` tags a function's return value (`"ret": "source"`); `params` tags argument -indices. Full schema: [`config/TaintConfigSchema.json`](config/TaintConfigSchema.json). +`ret` tags a function's return value (`"ret": "source"`); `params` tags argument indices. Full schema: [`config/TaintConfigSchema.json`](./config/TaintConfigSchema.json). -## Raw phasar-cli access +## Raw phasar-cli Access -`phasar-analyze` is a convenience wrapper. To call `phasar-cli` directly (e.g. on -an existing module, or to see all options), use the `cli` escape hatch: +`phasar-analyze` is a convenience wrapper. To call `phasar-cli` directly (e.g. on an existing module, or to see all options), use the `cli` escape hatch: ```bash docker run --rm phasar cli --help @@ -217,17 +201,9 @@ Set with `docker run -e NAME=VALUE`: ## Troubleshooting -- **`... does not exist` / module not loaded** — the LLVM version of the bitcode - must match the version PhASAR was built with. If you rebuilt the image for a - different `llvm_version`, the wrapper picks it up automatically; if you feed a - prebuilt `--module`, make sure it was produced by the same LLVM major version. -- **`could not detect a build system`** — pass `--build-cmd "..."` (project mode) - or use `--sources`. -- **`no artifact with embedded bitcode found`** — the build produced no linkable - bitcode (e.g. it failed). Check the build output, or point at the artifact with - `--binary`. For libraries, remember `-E __ALL__`. +- **`... does not exist` / module not loaded** — the LLVM version of the bitcode must match the version PhASAR was built with. If you rebuilt the image for a different `llvm_version`, the wrapper picks it up automatically; if you feed a prebuilt `--module`, make sure it was produced by the same LLVM major version. +- **`could not detect a build system`** — pass `--build-cmd "..."` (project mode) or use `--sources`. +- **`no artifact with embedded bitcode found`** — the build produced no linkable bitcode (e.g. it failed). Check the build output, or point at the artifact with `--binary`. For libraries, remember `-E __ALL__`. - **Files owned by root** — add `--user "$(id -u):$(id -g)"` to `docker run`. -- **Missing source lines in results** — keep debug info (`-g`); the wrapper adds - it automatically for `--sources`/`--project` builds. -- **See every phasar-cli option** — `docker run --rm phasar cli --help`, or - `docker run --rm phasar --help` for the wrapper's own options. +- **Missing source lines in results** — keep debug info (`-g`); the wrapper adds it automatically for `--sources`/`--project` builds. +- **See every phasar-cli option** — `docker run --rm phasar cli --help`, or `docker run --rm phasar --help` for the wrapper's own options. diff --git a/examples/docker-whole-program/README.md b/examples/docker-whole-program/README.md index 755dd45191..a67c937283 100644 --- a/examples/docker-whole-program/README.md +++ b/examples/docker-whole-program/README.md @@ -1,9 +1,7 @@ # Whole-Program Analysis Examples (Docker) -A set of tiny, self-contained C/C++ programs — each with a known finding — that -demonstrate PhASAR's main capabilities **end-to-end through the Docker image**: -build the program to whole-program LLVM IR with [WLLVM](https://github.com/travitch/whole-program-llvm), -then run a PhASAR analysis on it. All of this is handled by the image's +A set of tiny, self-contained C/C++ programs — each with a known finding — that demonstrate PhASAR's main capabilities **end-to-end through the Docker image**: +Build the program to whole-program LLVM IR with [WLLVM](https://github.com/travitch/whole-program-llvm), then run a PhASAR analysis on it. All of this is handled by the image's `phasar-analyze` entry point. ## Prerequisites @@ -14,9 +12,7 @@ Build the image once from the repository root: docker build -t phasar . ``` -The image bundles clang/LLVM (22 by default), WLLVM, and `phasar-cli`, and uses -`phasar-analyze` as its entry point. See the top-level `Dockerfile` and -`utils/phasar-analyze.sh`. +The image bundles clang/LLVM (22 by default), WLLVM, and `phasar-cli`, and uses `phasar-analyze` as its entry point. See the top-level `Dockerfile` and `utils/phasar-analyze.sh`. ## Running @@ -35,8 +31,7 @@ docker run --rm --user "$(id -u):$(id -g)" \ phasar --sources "main.c util.c" -a ifds-uninit ``` -Running as `--user "$(id -u):$(id -g)"` keeps any generated files owned by you -instead of root. +Running as `--user "$(id -u):$(id -g)"` keeps any generated files owned by you instead of root. ## The examples @@ -54,14 +49,12 @@ instead of root. | 10 | `10-statistics` | **LLVM IR statistics** of the module | `--sources main.c -- --emit-stats` | | 11 | `11-library` | **Library** (no `main`) analyzed with **all** functions as entry points | `--project /work -a ifds-uninit -E __ALL__` | -Anything after `--` is forwarded verbatim to `phasar-cli` (used above for the -`--emit-*` reporting flags). +Anything after `--` is forwarded verbatim to `phasar-cli` (used above for the `--emit-*` reporting flags). -## Ways to provide input +## Ways to Provide Input -The examples above use `--sources` and `--project`, but `phasar-analyze` accepts -several input modes (see `phasar-analyze --help`). All of the following are -supported and tested: +The examples above use `--sources` and `--project`, but `phasar-analyze` accepts several input modes (see `phasar-analyze --help`). +We support all of the following: | Mode | When to use | Example | |------|-------------|---------| @@ -71,14 +64,11 @@ supported and tested: | `--binary PATH` | Point at a specific build artifact (exe, `.so`, `.a`) | `phasar --project /work --binary /work/build/libfoo.a -a ifds-uninit -E __ALL__` | | `--module FILE.ll\|.bc` | You already have LLVM IR | `phasar --module /work/prog.bc -a ifds-uninit` | -For code without a `main` (libraries), pass `-E __ALL__` to use every function -definition as an entry point. +For code without a `main` (libraries), pass `-E __ALL__` to use every function definition as an entry point. -### Writing results to files +### Writing Results to Files -By default results go to stdout. Pass `-o DIR` (mount it, and it must exist) to -write results into a timestamped subdirectory instead — useful with the JSON -emitters for machine-readable output: +By default results go to stdout. Pass `-o DIR` (mount it, and it must exist) to write results into a timestamped subdirectory instead — useful with the JSON emitters for machine-readable output: ```bash mkdir -p results @@ -87,10 +77,9 @@ docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/work" -w /work phasar \ # -> results/-/psr-cg.json, psr-th.json ``` -## Beyond these examples +## Beyond these Examples -`phasar-analyze` is a thin wrapper over `phasar-cli`; the examples above are a -representative slice, not the full set. To see everything available: +`phasar-analyze` is a thin wrapper over `phasar-cli`; the examples above are a representative slice, not the full set. To see everything available: ```bash docker run --rm phasar cli --help @@ -98,28 +87,14 @@ docker run --rm phasar cli --help Some notable options not exercised here: -- **More data-flow analyses** (`-a`/`-D`): `ifds-taint`, `ifds-fieldsens-taint`, - `monoifds-taint`, `sparse-ifds-taint`, `inter-mono-taint`, `ifds-const`, - `ifds-type`, `ide-openssl-ts`, `ide-fiia`, `intra-mono-fca`, and the solver - self-tests (`ifds-solvertest`, `ide-solvertest`, …). These cover the different - solver families: IFDS, IDE, MonoIFDS, SparseIFDS, and intra-/inter-procedural - Monotone Frameworks. -- **Alias analyses** (`--alias-analysis`): `CFLAnders` (default, more precise), - `CFLSteens` (faster), `union-find` (with `--union-find-aa=ctx-sens` / - `ctx-ind-sens`). +- **More data-flow analyses** (`-a`/`-D`): `ifds-taint`, `ifds-fieldsens-taint`, `monoifds-taint`, `sparse-ifds-taint`, `inter-mono-taint`, `ifds-const`, `ifds-type`, `ide-openssl-ts`, `ide-fiia`, `intra-mono-fca`, ... . These cover the different solver families: IFDS, IDE, MonoIFDS, SparseIFDS, and intra-/inter-procedural Monotone Frameworks. +- **Alias analyses** (`--alias-analysis`): `CFLAnders` (default, legacy), `CFLSteens` (faster), `union-find` (with `--union-find-aa=ctx-sens` / `ctx-ind-sens`). - **Call-graph algorithms** (`-C`): `cha`, `rta`, `vta`, `otf` (default), `nores`. -- **Emitters**: `--emit-ir`, `--emit-cg-as-{dot,json}`, - `--emit-th-as-{text,dot,json}`, `--emit-pta-as-{text,dot,json}`, - `--emit-statistics-as-json`, `--emit-esg-as-dot`, `--emit-raw-results`. +- **Emitters**: `--emit-ir`, `--emit-cg-as-{dot,json}`, `--emit-th-as-{text,dot,json}`, `--emit-pta-as-{text,dot,json}`, `--emit-statistics-as-json`, `--emit-esg-as-dot`, `--emit-raw-results`. - **Results to files** instead of stdout: add `-o ` (mount it, too). ## Notes -- The wrapper compiles with `-g`, so findings carry **source-level** file/line - information (see the `ifds-uninit` output). Keep debug info in your own builds - for the same benefit. -- For real, multi-file projects use `--project ` (autodetects - cmake/`./configure`/make and builds with WLLVM) or `--build-cmd "..."` instead - of `--sources`. See `phasar-analyze --help`. -- Taint/typestate configs use PhASAR's taint-config JSON - (see `config/TaintConfigSchema.json`). +- The wrapper compiles with `-g`, so findings carry **source-level** file/line information (see the `ifds-uninit` output). Keep debug info in your own builds for the same benefit. +- For real, multi-file projects use `--project ` (autodetects cmake/`./configure`/make and builds with WLLVM) or `--build-cmd "..."` instead of `--sources`. See `phasar-analyze --help`. +- Taint/typestate configs use PhASAR's taint-config JSON (see `config/TaintConfigSchema.json`). From c896857a9809f89c5d94e434685b565d2241900d Mon Sep 17 00:00:00 2001 From: Fabian Schiebel Date: Tue, 11 Aug 2026 18:49:12 +0200 Subject: [PATCH 8/8] minor --- USAGE.docker.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/USAGE.docker.md b/USAGE.docker.md index 94fe075608..d4149f44ac 100644 --- a/USAGE.docker.md +++ b/USAGE.docker.md @@ -51,7 +51,7 @@ This builds PhASAR against LLVM 22 by default. To use a different LLVM major ver docker build -t phasar --build-arg llvm_version=16 --build-arg phasar_llvm_version=16 . ``` -> [!NOTE]- llvm_version vs phasar_llvm_version +> [!NOTE] > `phasar_llvm_version` is PhASAR's CMake version string — `22.1` for LLVM 22 (new release scheme), but `16` for LLVM 16.