Skip to content

Latest commit

 

History

96 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 

Repository files navigation

MemoryGuard

A dynamic analysis tool that detects memory leaks and dangling pointer accesses in C programs.

Built as a course project for CPSC 410 (Advanced Software Engineering) at UBC, MemoryGuard instruments C source code at the AST level using LLVM/Clang, tracks heap memory events at runtime, and produces a syntax-highlighted visual report pinpointing the exact lines where errors occur.


What It Does

C programs are highly prone to subtle memory bugs - allocating memory that's never freed, or dereferencing a pointer after the memory it points to has been freed. These bugs are silent at compile time, often only surfacing as crashes or corruption in production.

MemoryGuard catches two categories of bug dynamically, by wrapping your program's execution in a lightweight analysis layer:

  • Memory Leaks — heap allocations that are never freed before program exit
  • Dangling Pointer Accesses — reads or writes through a pointer after its underlying allocation has been freed

Because the analyzer tracks heap addresses rather than variable names, the dangling pointer class also covers calling free() on memory that was already freed through a different alias. That case is reported as a dangling pointer, not as a separately named bug class. See src/tests/test_dangling_pointer.c line 36: ptr was assigned from hi, hi was freed, and the later free(ptr) is flagged because it reaches a block the analyzer already knows is dead.


How It Works

MemoryGuard is a three-stage pipeline:

Your C File
    │
    ▼
┌─────────────────────────────────────────────────────┐
│  Stage 1 — AST Instrumentation (C++ / LLVM / Clang) │
│  Traverses the AST and injects analysis hooks       │
│  around malloc, free, dereferences, and aliasing    │
└─────────────────────────────────────────────────────┘
    │   instrumented.c
    ▼
┌─────────────────────────────────────────────────────┐
│  Stage 2 — Runtime Analysis (C++ shared library)    │
│  Tracks pointer state at runtime, builds a shadow   │
│  map of allocations, and records errors to JSON     │
└─────────────────────────────────────────────────────┘
    │   output_report.json
    ▼
┌─────────────────────────────────────────────────────┐
│  Stage 3 — Visual Report (Python / Pygments)        │
│  Annotates the original source with error messages  │
│  and renders a syntax-highlighted PNG               │
└─────────────────────────────────────────────────────┘
    │   test.png
    ▼
 Annotated source image showing exact error locations

Stage 1 — AST Instrumentation

A custom Clang AST plugin (PointerCallInsertionAST) walks the abstract syntax tree of your C file and injects instrumentation hooks at every relevant memory operation:

Event Hook
malloc call _ANALYSIS_INJECTION_call_before/after_malloc
free call _ANALYSIS_INJECTION_call_before_free
Pointer dereference (*ptr) _ANALYSIS_INJECTION_call_before_dereference
Arrow access (ptr->field) _ANALYSIS_INJECTION_call_before_arrow_access
Pointer aliasing (p = q) _ANALYSIS_INJECTION_call_before/after_pointer_aliasing
Function entry/exit _ANALYSIS_INJECTION_call_before/after_function

Stage 2 — Runtime Analyzer

The analyzer (a C++ shared library) maintains a shadow memory map — a runtime model of which heap addresses are currently live, which are freed, and which pointers alias them. At program exit, it serializes all detected errors (with source location metadata) to a JSON report.

Stage 3 — Visual Report

A Python script reads the JSON report and the original source file, annotates each flagged line with its error type and severity inline as a code comment, then renders the result to a PNG image using Pygments with Monokai syntax highlighting.


Tech Stack

Layer Technology
AST Instrumentation C++, LLVM, Clang LibTooling
Build system CMake
Runtime Analyzer C++ (compiled as a shared library via Clang)
Visualization Python 3, Pygments, Pillow

Prerequisites

macOS only. LLVM toolchain must be installed via Homebrew.

brew install llvm@16 cmake

Python 3 with:

pip install pygments pillow
# If pillow pip install fails:
brew install Pillow

Pointing the build at your LLVM

LLVM 16 specifically. The instrumenter is built on Clang's LibTooling, whose API has changed since this project was written in 2024. On LLVM 17 and later it no longer compiles — FileManager::getFile, SourceManager::createFileID, and CompilerInstance::createSourceManager have all changed signature. Porting it is not a packaging problem; it is a rewrite of the frontend setup code, and has not been done.

Apple's bundled toolchain does not ship the Clang development headers at all, and Homebrew keeps LLVM keg-only, so llvm-config is usually not on your PATH. Set LLVM_CONFIG before running anything:

export LLVM_CONFIG="$(brew --prefix llvm@16)/bin/llvm-config"

Sanity check it before you build — Homebrew leaves llvm@N symlinks behind that point at whatever LLVM version you actually have:

$LLVM_CONFIG --version   # must print 16.x

Two more optional overrides, for when clang is not on your PATH:

export CC="$(brew --prefix llvm@16)/bin/clang"       # default: clang
export CXX="$(brew --prefix llvm@16)/bin/clang++"    # default: clang++

These compile the runtime analyzer and the instrumented program, and are unrelated to the LibTooling constraint above — the system clang is fine for both. Most people only need LLVM_CONFIG.

CMake caches the LLVM it found on the first configure. If you change LLVM_CONFIG after a failed build, delete src/backend/instrumenter/build/ before retrying, or the stale cache wins.


Running MemoryGuard

cd src/
./run_project.sh <path_to_your_c_file>

Example — using a bundled test file:

./run_project.sh tests/test_dangling_pointer.c
./run_project.sh tests/ML_test1.c

The script will:

  1. Build the LLVM instrumenter
  2. Instrument your source file and compile it with the runtime analyzer
  3. Execute the instrumented binary to collect the error report
  4. Run the Python frontend to produce an annotated PNG

Output: frontend/test.png — an annotated, syntax-highlighted image of your source code with errors marked inline.

Note: If CMake fails with Could not find a package configuration file provided by "Clang", LLVM_CONFIG is unset or pointing at an LLVM without the Clang development headers. See Pointing the build at your LLVM.


Example Output

Running MemoryGuard on src/tests/DP_test2.c, which frees a pointer, reads through a surviving alias, and leaks a second allocation:

Annotated output for DP_test2.c

Each flagged line is annotated inline with its error type, severity, and the supporting locations the analyzer used to reach that conclusion — where the memory was aliased, and where it was freed.


Test Suite

The src/tests/ directory holds small C programs used during development, named by the bug they exercise:

Prefix Bug Type
ML_test*.c Memory leaks
DP_test*.c Dangling pointers, including aliasing
test_dangling_pointer.c Combined leak and aliasing cases
dynamic_test.c Sanity check: bugs in functions that are never called, which a dynamic tool should not report

src/tests/outputs/ holds the JSON report each program produced when it was last run, recorded as-is. An empty report means the analyzer found nothing, which is the correct result for dynamic_test.c and a miss for DP_test5.c and DP_test6.c.

Known gaps

  • Frees performed inside a called function are not attributed back to the caller's aliases. DP_test5.c and DP_test6.c both free a linked list inside freeList and then read through a pointer the caller still holds. The read is instrumented and the analyzer inspects the correct heap address, but concludes the block is live, so both report nothing. DP_test6.c is commented in detail and is the clearest illustration.
  • The analyzer cannot report a bug that kills the process first. DP_test3.c writes through a freed pointer and takes a bus error before the report is serialized at exit, so it produces no report at all.
  • Repeated allocations at one site are reported once per allocation. ML_test4.c leaks in a loop and produces ten identical entries for the same line; ML_test5.c produces five from a recursive call.

Project Structure

docs/                       # Example output image used by this README
src/
├── backend/
│   ├── instrumenter/       # Clang AST plugin (C++)
│   ├── analyzer/           # Runtime analysis library (C++)
│   └── run_analysis.sh     # Backend orchestration script
├── frontend/
│   └── frontend.py         # Visualization script
├── tests/                  # Example C programs
│   └── outputs/            # JSON report each program last produced
└── run_project.sh          # Main entry point

About

Dynamic analysis tool for C. Instruments source at the Clang AST level, tracks heap state at runtime, and renders an annotated report of memory leaks and dangling pointer accesses.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages