A Clang/LLVM plugin that implements Rust-style borrow checking for C, catching memory safety bugs at compile time.
pagurus performs comprehensive memory safety analysis covering:
- Memory safety: use-after-free, double-free, null-deref, memory leaks
- Borrow conflicts: double mutable borrow, simultaneous mutable and shared borrow
- Lifetime violations: returning pointers to local variables, use-while-borrowed
- Array bounds: constant index out of bounds
- Dangerous functions: unsafe functions like
gets,strcpy, format string vulnerabilities - Move semantics: use-after-move for resource-owning types
- Drop semantics: missing destructor calls, manual memory management
| Rule | Name | Description |
|---|---|---|
| E001 | use-after-free |
Dereference or call after free() |
| E002 | double-free |
free() called twice on the same pointer |
| E004 | return-of-local |
return &local — dangling reference |
| E005 | null-deref |
Dereference without null check after malloc |
| E006 | uninit-use |
Variable read before initialisation |
| E007 | memory-leak |
Heap allocation not freed on return path |
| E009 | format-string |
Non-literal format arg to printf/scanf family |
| E010 | unsafe-fn |
gets, strcpy, sprintf, strtok, mktemp, … |
| E011 | array-oob |
Constant array index out of declared bounds |
| E012 | double-mut-borrow |
&x passed as mutable argument twice |
| E013 | mut-shared-conflict |
Mutable and shared borrow simultaneously active |
| E015 | assign-while-borrowed |
Assignment to a live-borrowed variable |
| E016 | use-while-mut-borrowed |
Read of a mutably-borrowed variable |
| E017 | two-phase-conflict |
&x and x in the same call (C §6.5.2.2p10) |
| E018 | volatile-borrow / atomic-borrow |
Mutable borrow of volatile or _Atomic variable |
| E019 | use-after-move |
Drop-annotated struct used after its value was moved |
| E020 | missing-drop |
Drop-annotated struct goes out of scope without drop function call |
| E021 | drop-necessary |
Struct with pointer fields but no drop annotation |
| Rule | Name | Analysis method |
|---|---|---|
| IR-E001 | use-after-free |
AliasAnalysis::isMustAlias + DominatorTree |
| IR-E001b | use-after-free (GEP) |
GEP element of freed object |
| IR-E002 | double-free |
Two free() calls that MustAlias |
| IR-E015 | borrow-conflict |
MemorySSA def-def pairs + AliasAnalysis |
| IR-E015b | borrow-conflict (GEP) |
GEP pointer arithmetic alias |
| IR-E015c | borrow-conflict (bitcast) |
Union type-pun / bitcast alias |
| IR-E015 (loop) | loop-carried borrow |
MemorySSA MemPhi nodes across back-edges |
| IR-E018 | atomic-borrow-conflict |
AtomicRMWInst/AtomicCmpXchgInst on borrowed alloca |
| IR-Drop | drop-injection |
Automatically inserts drop calls at scope-exit |
IR checks catch patterns invisible at AST level: bit-cast/type-pun aliases, GEP pointer arithmetic, loop-carried borrows (SSA φ-nodes), and atomic instruction races on borrowed memory.
# Ubuntu 24.04
sudo apt install clang-18 llvm-18-dev libclang-18-dev cmake
mkdir build && cd build
cmake .. \
-DLLVM_DIR=$(llvm-config-18 --cmakedir) \
-DClang_DIR=/usr/lib/llvm-18/lib/cmake/clang
make -j$(nproc)See BUILDING.md for detailed build instructions, MLIR integration, and runtime dependencies.
# AST checks only (E001–E021):
clang -fplugin=./build/pagurus_plugin.so -c your_file.c
# AST + LLVM IR analysis (adds GEP/bitcast/loop-carried checks + drop injection):
clang -fplugin=./build/pagurus_plugin.so \
-fpass-plugin=./build/pagurus_plugin.so \
-g -O0 -c your_file.cEvery invocation runs in compile mode by default: the plugin rewrites the source to produce <input>.pagurus.c that:
- Has every
#pragma pagurus …line removed (plain C, no pagurus-isms) - Has drop-function calls injected at scope-exit / early-return points
All other errors (E001–E019, E021) still fire and cause build failure. E020 is suppressed because the transformation fixes it automatically.
# Writes your_file.pagurus.c — pagurus annotations stripped,
# missing drop calls injected. E020 is not reported (fixed by injection).
clang -fplugin=./build/pagurus_plugin.so -c your_file.cPass dry-run as a plugin argument to enable inspector mode. All diagnostics (E001–E021, including E020) are reported, a textual report of what changes would be made is printed to stderr, and no output file is written.
clang -fplugin=./build/pagurus_plugin.so \
-Xclang -plugin-arg-pagurus -Xclang dry-run \
-c your_file.cExample dry-run output:
pagurus [dry-run]: changes that would be applied:
remove line 5: #pragma pagurus drop(Buf)
inject before line 12: buf_free(b);
warning: E020[missing-drop]: `b` goes out of scope without calling `buf_free`
pagurus supports pragma-based annotations for lifetime constraints and drop functions:
// Explicitly release a borrow (mirrors Rust `drop(borrow)`):
#pragma pagurus borrow_end(p)
// Function lifetime annotation — return tied to first parameter:
#pragma pagurus 'a 'b -> 'a
int *get_x(int *x, int *y);
// Drop function annotation — required destructor:
#pragma pagurus drop(StrStruct)
void str_free(struct StrStruct s);
// Struct lifetime parameters:
#pragma pagurus 'a 'b
struct Buf {
#pragma pagurus 'a
const char *data;
#pragma pagurus 'b
char *scratch;
};See ANNOTATIONS.md for the complete reference.
Run pagurus across an entire codebase using the included tools:
# Check all files under src/ with 4 parallel jobs
./pagurus-check --plugin=./build/pagurus_plugin.so \
--cflags="-Iinclude" \
--jobs=4 --dir=src
# Or use a compilation database
bear make
./pagurus-check --plugin=./build/pagurus_plugin.so \
--compile-db=compile_commands.jsonIntegrate into Makefiles:
# myproject/Makefile
PAGURUS_PLUGIN = /path/to/build/pagurus_plugin.so
include /path/to/pagurus.mk
# Then run:
# make pagurus-checkSee INTEGRATION.md for complete integration guide.
- Non-lexical lifetimes (NLL): Precise borrow tracking with loan release at last use
- Control flow analysis: Conditional and loop-aware borrow propagation
- Inter-procedural: Function summaries for return-alias and parameter effects
- Move semantics: Rust-style ownership transfer for drop-annotated types
- Drop injection: Automatic RAII-style cleanup at IR level with
-fpass-plugin= - Source transformation: Produces plain C code without pagurus annotations
- Two-tier analysis: AST for precision + IR for patterns invisible at source level
- BUILDING.md — Build instructions, MLIR integration, runtime dependencies
- ANNOTATIONS.md — Complete
#pragma pagurusreference - INTEGRATION.md — Multi-file project integration with
pagurus-checkandpagurus.mk - ARCHITECTURE.md — Technical architecture and implementation details
MIT