diff --git a/Dockerfile b/Dockerfile index 844e78ea7f..b4be90d18f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,35 @@ 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. +# `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 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}" "lld-${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 +37,12 @@ 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 \ + -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 \ @@ -29,4 +52,17 @@ 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} + +WORKDIR /work + +ENTRYPOINT [ "phasar-analyze" ] diff --git a/USAGE.docker.md b/USAGE.docker.md new file mode 100644 index 0000000000..d4149f44ac --- /dev/null +++ b/USAGE.docker.md @@ -0,0 +1,209 @@ +# 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: + +```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. +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, currently 16–22): + +```bash +docker build -t phasar --build-arg llvm_version=16 --build-arg phasar_llvm_version=16 . +``` + +> [!NOTE] +> `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-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 + +These affect precision/performance of most analyses: + +- **Alias analysis** — `-P` / `--alias-analysis`: + `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 +``` + +## 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`): + +| 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 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/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 new file mode 100644 index 0000000000..ab672aa311 --- /dev/null +++ b/examples/docker-whole-program/01-uninitialized-variables/main.c @@ -0,0 +1,8 @@ +#include "util.h" + +#include +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..1ac2e8e712 --- /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..a37ebb027a --- /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..a8f9f4a74f --- /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..8b925ef7d9 --- /dev/null +++ b/examples/docker-whole-program/03-taint-leak/taint-config.json @@ -0,0 +1,19 @@ +{ + "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..043b392288 --- /dev/null +++ b/examples/docker-whole-program/04-double-free/double-free-config.json @@ -0,0 +1,28 @@ +{ + "name": "double-free", + "version": 1.0, + "functions": [ + { + "name": "free", + "params": { + "source": [ + 0 + ], + "sink": [ + 0 + ] + } + }, + { + "name": "_ZdlPv", + "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..a67d4e3baa --- /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..60701c184b --- /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..160a8401b2 --- /dev/null +++ b/examples/docker-whole-program/06-type-hierarchy/main.cpp @@ -0,0 +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 {} +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..14616bb82b --- /dev/null +++ b/examples/docker-whole-program/07-call-graph/main.cpp @@ -0,0 +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 */ +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..12d470089e --- /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..aa09f43d98 --- /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..decb005f95 --- /dev/null +++ b/examples/docker-whole-program/10-statistics/main.c @@ -0,0 +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. */ +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..d47e9e5eaf --- /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..a67c937283 --- /dev/null +++ b/examples/docker-whole-program/README.md @@ -0,0 +1,100 @@ +# 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`). +We support all of the following: + +| 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`, ... . 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`. +- **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..72d1025083 --- /dev/null +++ b/utils/phasar-analyze.sh @@ -0,0 +1,279 @@ +#!/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 +# --------------------------------------------------------------------------- +# 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) 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) 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 ;; + 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[@]}"