# PolyTracker tracks every byte of input through a program, and then hands you the taint graph to query

> trailofbits/polytracker is an LLVM pass that instruments a program to record which bytes of an input file are operated on by which functions, writing a taint database and a runtime trace that a Python library and REPL can query. It began as a research project for parser analysis, it needs a container to run, and its newest tagged release is v4.0.0 from 2022.

**trailofbits/polytracker** — An LLVM-based instrumentation tool for universal taint tracking, dataflow analysis, and tracing.

- Repository: https://github.com/trailofbits/polytracker
- Stars: 600 · Forks: 54
- Language: C++
- License: Apache-2.0
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/trailofbits-polytracker

## An LLVM pass that records byte level provenance

PolyTracker is an LLVM based instrumentation tool for universal taint tracking, dataflow analysis and tracing, and the description of what it does at the byte level is what makes it different from a conventional taint tool.

The README says it is an LLVM pass that instruments programs to track which bytes of an input file are operated on by which functions. The output is a database containing the dataflow information, as well as a runtime trace, and PolyTracker also provides a Python library for interacting with and analysing that output, plus an interactive Python REPL.

The origin story is worth knowing because it explains the shape of the tool. PolyTracker was originally created for the Automated Lexical Annotation and Navigation of Parsers, a backronym devised solely for the purpose of referring to it as The ALAN Parsers Project. It has since evolved into a general purpose tool for efficiently performing dataflow and control-flow analysis of programs.

That origin is still visible in what the tool can do with the result. PolyTracker can be used in conjunction with PolyFile to automatically determine the semantic purpose of the functions in a parser, and it has an experimental feature capable of generating a context free grammar representing the language accepted by a parser. Those two features are what turn a taint graph into a parser description, and they are the reason the project is interesting beyond taint analysis.

The audience is security researchers and anyone doing the kind of work where understanding a parser's structure is the goal, whether the subject is a file format, a protocol, or an input validation path in a program you are auditing.

## Provenance for every byte, against the sixteen taints DataFlowSanitizer can track

The comparison with dynamic instrumentation alternatives is made in the README, and it is the sharpest claim the project makes.

Unlike dynamic instrumentation alternatives such as Taintgrind, PolyTracker is said to impose negligible performance overhead for almost all inputs, and it is capable of tracking every byte of input at once. The mechanism behind that is that PolyTracker started as a fork of the LLVM DataFlowSanitizer, so the taint is propagated by the compiler rather than by shadow memory updated at every load and store.

The provenance claim is the sharper one. In February 2021, LLVM DataFlowSanitizer added a feature for tracking taint provenance called origin tracking. The README notes that DataFlowSanitizer is only able to track at most 16 taints at once, while PolyTracker can track up to 2 to the power of 31 minus 1. The project also takes much inspiration from the Angora Fuzzer, but unlike the Angora system, it is able to track the entire provenance of a taint.

The difference between tracking a taint and tracking the provenance of a taint is the difference between knowing that byte 40 reached a function and knowing that byte 40 was originally the length field at offset 12 of the header, which was itself derived from a byte at offset 4, and that the function that consumed it is the one at a particular address. For parser work, where the interesting question is which field drove which branch, that distinction is the whole value.

The counterpoint is equally real. Shadow memory based approaches are simpler to reason about and do not require recompiling the target with a specific LLVM. PolyTracker needs an instrumented build, and the instrumented binary is a different artefact from the one you ship, which is a constraint for anyone trying to analyse a binary they cannot rebuild.

## A container is not optional, and the README says so plainly

The installation path has two halves, and the documentation is unusually direct about needing both.

The package is on PyPI, so it installs with:

```shell-script
pip3 install polytracker
```

But then it says that PolyTracker requires a very particular system environment to run, so almost all users are likely to run it in a containerized environment, and that polytracker makes this easy. All you need is Docker installed, then:

```shell-script
polytracker docker pull
```

and:

```shell-script
polytracker docker run
```

The second command mounts the current working directory into the PolyTracker Docker container and lets you build and run instrumented programs from there. So the host needs Python and the package, the container carries the LLVM toolchain, and your project directory is the bridge between them.

The control script itself runs from either side, on the host or inside the container, and it has commands for instrumenting programs and for analysing the resulting artefacts. The documentation suggests exploring them with:

```shell-script
polytracker --help
```

and notes that the script is also a REPL when run with no arguments at all:

```python
$ polytracker
PolyTracker (4.0.0)
https://github.com/trailofbits/polytracker
Type "help" or "commands"
>>> commands
```

One warning at the end of the documentation is worth planning around. PolyTracker can be very slow if it is running in a virtualized environment and either the input file or, especially, the output database is located in a directory mapped or mounted from the host operating system. So the container model is not just packaging, it is also where the performance trap is.

The build side of the container is visible in the Dockerfile, which is based on ubuntu:jammy. It installs clang-12 rather than a newer clang, and the comment explains why: the clang-13 package contains a version with behaviour that is odd with respect to -Werror, the flag is raised even when the user does not specify it, and MuPDF does not build with clang-13 installed from apt, for example. The image also installs gllvm, which is used for builds with bitcode references embedded in the build targets.

## Blight records the build, then instrument-targets rebuilds with the pass

The instrumentation workflow has two halves, and separating them is what lets PolyTracker handle real build systems.

The first half is polytracker build, which runs any build command in a Blight instrumented environment. Blight is a build instrumentation tool, and the point of using it is that the build itself is recorded: the run produces a blight_journal.jsonl file that records all commands run during the build. For a single C or C++ target, you pass your build command directly:

```shell-script
polytracker build gcc -g -o my_binary my_source.c
```

The second half is polytracker instrument-targets. By default it uses a blight_journal.jsonl in the current working directory to build an instrumented version of your build target, and the instrumented target is built using the same flags as the original. So the journal is the bridge: the build is recorded once, and any target from it can then be rebuilt with the LLVM pass applied.

```shell-script
polytracker build cmake .. -DCMAKE_BUILD_TYPE=Release
polytracker build ninja
# or
polytracker build ./configure
polytracker build make
```

The result is a new file next to the original, with a .instrumented. suffix before the extension, so a.binstrumented.bin and b.instrumented.so in the example in the documentation. The originals are untouched, which is what you want when the point is analysis rather than deployment.

The build command is not limited to a single compiler invocation. It also supports programs that use a build system:

```shell-script
polytracker build cmake .. -DCMAKE_BUILD_TYPE=Release
polytracker build ninja
# or
polytracker build ./configure
polytracker build make
```

Then instrument-targets can be run on any targets of the build, for example a.binstrumented.bin and b.instrumented.so will be the instrumented versions.

The examples directory makes the ambition concrete. It holds a Dockerfile for each target program, among them ffmpeg, poppler, qpdf, xpdf, mupdf, pdfium, openjpeg, libjpeg, png, jq, libgen, acropalypse, and the daedalus and nitro NITF parsers, plus an analysis/ directory, a build.sh, an http/ directory and a jpeg/ directory. Instrumenting ffmpeg or poppler is not something a small taint tool can do, so the example set is also a statement about scale.

## The output is a taint DAG, and you query it from Python

The instrumented software writes its output to the path specified in POLYDB, or to polytracker.tdag if that is omitted. The file is binary, and the documented way to work with it is the Python API.

The trace object gives you a taint DAG file and a taint forest:

```python
from polytracker import PolyTrackerTrace, taint_dag

trace = PolyTrackerTrace.load("polytracker.tdag")
tdfile = trace.tdfile

first_node = list(tdfile.nodes)[0]
print(f"First node affects control flow: {first_node.affects_control_flow}")
```

The nodes come in several types, and the example iterates over them checking for range nodes, which carry the first and last byte of a range:

```python
# Operate on all Range nodes
for index, node in enumerate(tdfile.nodes):
  if isinstance(node, taint_dag.TDRangeNode):
    print(f"Node {index}: first {node.first}, last {node.last}")
```

The taint forest is the hierarchy of how one taint derived from another, and the API for it is the part that answers the provenance question:

```python
# Access taint forest
tdforest = trace.taint_forest
n1 = tdforest.get_node(1)
print(
  f"Forest node {n1.label}. Parent labels: {n1.parent_labels}, "
  f"source: {n1.source.path if n1.source is not None else None}, "
  f"affects control flow: {n1.affected_control_flow}"
)
```

So a node has a label, the labels of its parents, an optional source with a path, and a flag for whether it affected control flow. That is the shape of the answer to the question PolyTracker was built for: which input bytes reached which functions, and through what chain.

You can also run an instrumented binary directly from the REPL, with run_trace taking a binary path and an input file path, and the documentation notes that this will automatically run the instrumented binary in a Docker container if necessary.

## From taint graph to grammar, with PolyFile alongside

The two features that turn PolyTracker into a parser analysis tool are the PolyFile integration and the experimental grammar generation.

PolyFile is a separate project from the same organisation, and PolyTracker can be used in conjunction with it to automatically determine the semantic purpose of the functions in a parser. The logic of that is straightforward once you see the taint graph: if a function's output taint derives from a specific input offset, and that offset is the same one every other function treats specially, the field has a name and a role, and PolyFile works out which one.

The grammar generation is the more ambitious piece, described as an experimental feature capable of generating a context free grammar representing the language accepted by a parser. Given that the tool tracks every byte and can see which bytes reach which functions, it is in a position to infer the structure of the language a parser accepts, which is a capability that is very hard to get from a fuzzer and a lot easier from an instrumented parse of a representative input.

The word experimental is doing real work in that sentence, and the README does not describe the limits, the success rate, or the class of grammars it can produce. If grammar extraction is the reason you are looking at this project, that gap is the first thing to test.

The repository layout supports the claim that this is a research project with a real tool attached. Alongside the polytracker/ package there is an eval/ directory, which suggests an evaluation harness for measuring what the instrumentation recovers, an examples/ directory with real targets, a docs/ directory, a build_in_docker/ directory, third_party/ and unittests/ directories, and a mutool-draw entry, which is a MuPDF based tool for drawing documents, useful for turning a PDF into something a parser test corpus can be built from. The presence of CODEOWNERS, a .trunk/ directory with trunk used for linting and formatting, .mypy.ini, .ruff.toml and a .clang-format file shows the project is held to a house style, and a CLAUDE.md file at the root suggests the same instruction files convention that has spread across the ecosystem.

## Apache 2.0, a 2022 release, and a 2026 push

The licence is Apache-2.0, which is a permissive choice that lets the tool be used inside commercial security products, and the repository is published on PyPI as polytracker with a Slack badge for the Empire Hacking community.

The release history is where an adopter should look carefully. The tagged versions are v4.0.0 from 2022-09-15, v3.1.0 from 2021-11-09 and v3.0.1 from 2021-04-16. The newest release is therefore from 2022, and the REPL banner in the documentation still reads PolyTracker (4.0.0).

The repository itself is not archived, and the last push was on 2026-06-10, so work is continuing in the tree even though no new tag has been cut in a long time. That combination, an active repository and a stale release line, is the pattern you get when a research tool keeps being developed for internal or evaluation use without a maintainer cutting releases for public consumption.

The Python packaging is a small but real compatibility constraint. setup.py requires Python 3.7 or newer, and on macOS 20 or later the requirement tightens to Python 3.9.1 or newer, because the shared cache on Big Sur breaks ctypes.util.find_library, which the cxxfilt dependency uses, and the fix was not backported to CPython before that version. The version number itself is parsed out of the C header at polytracker/include/polytracker/polytracker.h, and the build refuses to proceed if that header is missing, which tells you the C library and the Python package are one artefact rather than two.

The Makefile reinforces that the Docker image is the supported workflow. The docker target builds the image with DOCKER_BUILDKIT=1, the test target runs the test suite inside the image with pytest, and lint and format are delegated to trunk. There is no bare metal build target, which is consistent with the documentation's advice to run everything inside the container.

## Conclusion

Use PolyTracker if you are reverse engineering a parser, auditing how a program consumes untrusted input, or building a taint graph for a large program where a dynamic taint tool would drown you in one tag at a time. Do not expect it to be a general purpose profiler or a drop in fuzzing harness, and budget for the containerised workflow, since the documentation says it needs a particular system environment. Before starting, run polytracker docker pull and polytracker docker run, instrument one small target with polytracker build and polytracker instrument-targets, and check the version of the tool you are reading about, because the last tagged release is v4.0.0 from 2022 while the repository itself was pushed on 2026-06-10.

## FAQ

### What is PolyTracker and what does it track?

It is an LLVM based instrumentation tool for universal taint tracking, dataflow analysis and tracing. It is an LLVM pass that instruments a program to track which bytes of an input file are operated on by which functions, and it writes the dataflow information to a database along with a runtime trace.

### How do I install and run PolyTracker?

Install the package with pip3 install polytracker, then use polytracker docker pull and polytracker docker run, which mounts the current working directory into the container. The documentation says PolyTracker needs a particular system environment, so almost all users run it containerised.

### How do I instrument a program with PolyTracker?

Run polytracker build with your build command, for example polytracker build gcc -g -o my_binary my_source.c, which records the build in a blight_journal.jsonl. Then run polytracker instrument-targets with the target names to build instrumented copies such as a.binstrumented.bin using the same flags.

### What file does PolyTracker write and how do I read it?

The instrumented program writes to the path in POLYDB, or to polytracker.tdag when that is not set. The Python API loads it with PolyTrackerTrace.load, and the trace exposes a taint DAG file and a taint forest whose nodes carry labels, parent labels, a source path and a control flow flag.

### How does PolyTracker compare with Taintgrind and with LLVM DataFlowSanitizer?

The README says that unlike dynamic instrumentation alternatives such as Taintgrind, PolyTracker imposes negligible performance overhead for almost all inputs and can track every byte of input at once, and that it can track the entire provenance of a taint where DataFlowSanitizer's origin tracking handles at most 16 taints at once.

### Can PolyTracker extract a grammar from a parser?

It has an experimental feature capable of generating a context free grammar representing the language accepted by a parser, and it can be used with PolyFile to automatically determine the semantic purpose of the functions in a parser. The documentation describes the grammar generation as experimental.

## Sources

- [Issues](https://github.com/trailofbits/polytracker/issues)
- [License: Apache-2.0](https://github.com/trailofbits/polytracker/blob/master/LICENSE)
- [README](https://github.com/trailofbits/polytracker/blob/master/README.md)
- [Releases](https://github.com/trailofbits/polytracker/releases)
- [trailofbits/polytracker on GitHub](https://github.com/trailofbits/polytracker)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/trailofbits-polytracker
