pe-parse reads PE files through a bounded buffer so malformed headers cannot walk off the end
Principled, lightweight C/C++ PE parser
At a glance
- What is it?
- Trail of Bits' pe-parse is a C and C++ parser for Windows portable executables, built for analysing programs of unknown origin, where a hostile file is the normal case rather than the exception. It reaches data through a bounded buffer, sanitises what it reads into C++ containers, and warns that its error handling is not thread safe.
- Who is it for?
- Adopt pe-parse if you analyse Windows executables of unknown origin and want a parser built around hostile input rather than one that assumes well formed files, especially if you already work in C or C++.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 79 days ago.
- What is it written in?
- Mainly C++, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The threat model is the design brief
pe-parse is described as a principled, lightweight parser for Windows portable executable files. It was created to assist in compiled program analysis, potentially of programs of unknown origins.
That phrase, potentially of programs of unknown origins, is the whole specification. Everything else in the project follows from it.
The README draws the consequence directly. This means that it should be resistant to malformed or maliciously crafted PE files, and it should support questions that analysis software would ask of an executable program container. The examples given are listing relocations, describing imports and exports, and supporting byte reads from virtual addresses as well as file offsets.
So the scope is deliberately narrow. This is not a general binary analysis framework and not a disassembler. It answers structural questions about the container, and it does so in a way that assumes the container is hostile.
That framing also tells you who it is for. The consumers are people writing analysis tooling: sandboxing infrastructure, triage pipelines, scanners that need to know what an executable imports before deciding what to do with it, and reverse engineers who want a parsing primitive they can trust with a file that came off the internet.
The listed use cases map to a small API, which is described as minimal. Opening and closing a PE file. Iterating over the imported functions, the relocations, the exported functions, the sections and the resources. Reading bytes from specified virtual addresses. Retrieving the program entry point.
Eight operations, and the interface is defined in a single header, parser-library/parse.h.
The bounded buffer is the mechanism that makes it defensible
Internally, the parser-library uses a bounded buffer abstraction to access information stored in the PE file. The README says this should help in constructing a sane parser that allows for detection of the use of bogus values in the PE that would result in out of bounds accesses of the input buffer.
That is the interesting sentence in the whole document, because it names the classic failure mode. A PE file contains offsets and sizes that the file itself supplies. A directory entry claims a region of the file is at offset X with length Y. If the parser trusts those values and reads Y bytes from X, a crafted file can make the parser read past the end of the buffer. The bounded buffer makes that read return an error instead.
The second half of the design follows from the first. Once data is read from the file it is sanitized and placed in C++ STL containers of internal types. So the values that came out of an untrusted file do not stay in the shape the file gave them. They are copied into containers the parser owns, after being checked.
Together those two properties are what a parser of hostile input needs. The buffer refuses to read out of range, and the values that do get read are not left in attacker-controlled form.
The API surface reflects the same discipline. Every accessor is an iteration over a validated collection, not a pointer into the original file, and the read-by-virtual-address operation is offered as a first-class feature because analysis tools genuinely need to map a virtual address back to bytes, which is where a naive implementation goes wrong.
The example program in dump-pe/main.cpp exists to demonstrate this API in use, and the README points at it as the reference for how the library is meant to be called.
The thread safety warning is stated before anything else
Before the installation instructions, before the API list, the README puts a warning block, and it is the only warning block in the document.
It says that pe-parse's error handling is not thread safe, and that if you want to perform PE writes or parses in multiple threads, you must independently synchronize your operations.
That placement is deliberate and it deserves to be read as the project telling you its single most important operational fact first. Everything else in the README is documentation; this is a precondition.
The reason it matters is specific to this kind of library. A parser that reports errors has to store somewhere what went wrong: a last error message, an error code, a buffer for the description. That state is global or shared in a parser of this shape, so two threads parsing two files at once can overwrite each other's error state. One thread sees a parse failure that belongs to the other's file, or sees success where the other thread failed.
For a single-threaded tool, a batch job, or a service that parses one file per request in its own process, this costs nothing. For a service that parses many files in parallel threads inside one process, it means either a lock around the parse calls or one process per worker.
The repository also carries a CHANGELOG.md and a VERSION file, plus a CODEOWNERS file, which is the kind of housekeeping a project keeps when it has more than one active maintainer. The README names Andrew Ruef as the designer and implementer, with significant contributions from Wesley Shields, and says the project is currently maintained by Eric Kilmer and Graham Sutherland.
Building it, and the ICU dependency that only exists off Windows
The generic build is clone, configure, build.
git clone https://github.com/trailofbits/pe-parse.git
cd pe-parse
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Release ..
cmake --build .And the optional install step.
cmake --build . --target installThe dependency list has one entry with a wrinkle. ICU is required on non-Windows platforms, Linux and macOS, and you do not need ICU on Windows.
On Debian and Ubuntu that is `sudo apt-get install libicu-dev`, on RedHat and Fedora `sudo dnf install libicu-devel`, on macOS `brew install icu4c`, and through vcpkg `vcpkg install icu`. CMake itself has the same list of package manager invocations.
The macOS wrinkle is the ICU_ROOT environment variable. When ICU is installed via brew it will not be in the usual library path, so you have to point cmake at it.
ICU_ROOT=/opt/homebrew/opt/icu4c@123 cmake -DCMAKE_BUILD_TYPE=Release ..
ICU_ROOT=/opt/homebrew/opt/icu4c@123 cmake --build .The path is usually /opt/homebrew/opt/icu4c@version, where version is the version number of ICU you have installed.
Windows has its own generator names, and the supported list is long: VS 2017, VS 2019, VS 2022 and VS 2026.
cmake -G "Visual Studio 17 2022" -A Win64 ..
cmake --build . --config ReleaseNotice the Windows build passes the build type at build time rather than at configure time, which is a consequence of how the Visual Studio generators work.
Testing with sanitizers and the Corkami corpus
Two switches decide how much of the project you actually build.
Tests are Catch2 based and are enabled with a configure flag, -DPEPARSE_ENABLE_TESTING=ON.
Once built, you run them with ctest or with cmake --build . --target test.
But the full test suite needs more than that. To run the full test suite with the Corkami test suite you must clone the submodule with git submodule update --init. The Corkami PE corpus is a collection of deliberately malformed and unusual PE files, and it is the test set that matters for a parser built to resist hostile input. Without the submodule you are running the project's own unit tests, which is not the same thing.
Examples are behind their own flag, -DPEPARSE_ENABLE_EXAMPLES=ON, and the examples/peaddrconv folder is the one the README points to as a complete worked example of consuming the installed library.
The sanitizers are the third switch, and they are described with unusual specificity. You can compile with any of Address, HWAddress, Undefined, Memory, MemoryWithOrigins, Leak, or the combined Address,Undefined. The combined build is the one recommended for development and testing, and the README says it is tested in CI.
mkdir build-san
cd build-san
cmake -DCMAKE_BUILD_TYPE=Debug -DPEPARSE_ENABLE_TESTING=ON -DPEPARSE_USE_SANITIZER=Address,Undefined ..
cmake --build .For a parser whose whole job is not reading out of bounds, running the suite under AddressSanitizer is not optional diligence, it is the point. The README notes that you need the compiler and an instrumented standard library for this, which is a fair caveat because a sanitizer build against a non-instrumented standard library reports fewer useful things.
pepy, vcpkg, and a Docker image that dumps rather than analyses
There are three ways to get pe-parse into your hands, and they suit different situations.
The vcpkg route is one command.
$ vcpkg install pe-parseThe Python bindings route installs pepy, which the README describes as pe-parse includes Python bindings via pepy, installable via pip.
$ pip3 install pepyMore detail is in the pepy README inside the repository. The packaging metadata for pepy is worth a look, because it explains how the binding works. The project name is pepy, the description is Python bindings for pe-parse, and the licence is BSD-2-Clause while the C library itself is MIT. It requires Python 3.10 or newer and classifies itself as Development Status :: 5 - Production/Stable.
The most telling detail is in the tool.setuptools section, with a comment that reads: pepy is a C extension module, not a Python package. There are no Python packages to discover, only the compiled extension from setup.py. And the extension is built from three source files: pepy/pepy.cpp plus pe-parser-library/src/parse.cpp and pe-parser-library/src/buffer.cpp.
That tells you the binding compiles the C++ parser into your Python process rather than shipping a separate binary and calling across a boundary. It also means a pip install of pepy needs a C++ toolchain, which is the practical constraint people hit.
The Docker image is the third option, and it is shaped for the demo rather than for integration. The Dockerfile is built on alpine:latest with cmake, icu-dev, clang and build-base, copies the source to /app/pe-parse, configures with the build type from an argument, and installs. Its entrypoint is /usr/bin/dump-pe, with the default command --help.
So the container runs the example dumper, not a general analysis service. It is useful for inspecting a file quickly, and the README does not present it as a way to embed pe-parse in a pipeline.
Where pe-parse is the wrong tool
Three cases rule it out, and only one of them is about the library itself.
The first is thread safety, and it is not a small gap. The README's warning is explicit that error handling is not thread safe and that you must independently synchronise your operations. If you need to parse PE files concurrently inside one process, pe-parse as shipped is not the answer, and the fix is on your side, in your locking discipline.
The second is anything beyond the container format. The API answers structural questions about the executable: imports, exports, relocations, sections, resources, the entry point, and byte reads. It does not disassemble, it does not follow the import table to find the actual code, it does not reconstruct data structures, and it does not emulate. If your goal is understanding what the program does rather than what it contains, pe-parse is the first step and not the answer.
The third is a pure Python workflow. The pepy bindings compile C++ into your process, so pip install pepy needs a working toolchain, and the resulting extension inherits the same not-thread-safe error handling. If you want something you can drop into a Python environment without a compiler, that is a different requirement and a different tool.
There is also a version story to check. The listed releases are v3.0.0-rc.1 from 2026-05-22, and then v2.1.1 and v2.1.0, both from 2022-11-14. So the current line is a release candidate of a new major, sitting three and a half years after the last 2.x release. The repository is not archived and the last push was on 2026-08-05.
A release candidate means the API may still move, and the two-year gap between the 2.x tags and the 3.0 candidate means the churn may be larger than a version number suggests. Read the CHANGELOG.md before pinning.
The alternative, and the honest one to name, is the platform-native parsing you would otherwise reach for: the Windows loader APIs and the debug interface read PE headers directly, and other PE parsing libraries exist for C and Python. The difference pe-parse offers is not new capability but a stance, in that a parser built for unknown-origin binaries is a different engineering problem from one built for known-good files, and the bounded buffer is the concrete expression of that stance.
Editorial conclusion
Adopt pe-parse if you analyse Windows executables of unknown origin and want a parser built around hostile input rather than one that assumes well formed files, especially if you already work in C or C++. Do not adopt it if you need a thread-safe parser, because the README warns in bold that its error handling is not thread safe and requires independent synchronisation, and do not adopt it if you need a pure Python tool without a C toolchain, since the pepy bindings compile the C++ source. Before you build on it, run the test suite with the sanitizers enabled and clone the Corkami submodule, because the malformed PE corpus is where the parser's value actually lives.
Frequently asked questions
What is pe-parse and what is it for?
pe-parse is a principled, lightweight C and C++ parser for Windows portable executable files, created to assist in compiled program analysis of programs of unknown origins. It supports listing relocations, describing imports and exports, and reading bytes from virtual addresses as well as file offsets.
Is pe-parse thread safe?
No. The README carries a warning that pe-parse's error handling is not thread safe, and that if you perform PE writes or parses in multiple threads you must independently synchronize your operations.
How do I install pe-parse?
Through vcpkg with vcpkg install pe-parse, or through the Python bindings with pip3 install pepy. To build from source, clone the repository, create a build directory, run cmake with CMAKE_BUILD_TYPE=Release and then cmake --build .
What dependencies does pe-parse need?
ICU is required on non-Windows platforms, Linux and macOS, and is not needed on Windows. The README lists libicu-dev for Debian and Ubuntu, libicu-devel for RedHat and Fedora, and icu4c for macOS. CMake is also required.
How do I build and run the pe-parse tests?
Configure with -DPEPARSE_ENABLE_TESTING=ON and then run ctest or cmake --build . --target test. Running the full test suite with the Corkami test suite also requires cloning the submodule with git submodule update --init.
How does pe-parse stay safe on malformed PE files?
Internally the parser-library uses a bounded buffer abstraction to access information stored in the PE file, so bogus values in the file cannot cause out of bounds accesses of the input buffer. Once read, the data is sanitized and placed in C++ STL containers of internal types.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/trailofbits-pe-parse)Was this useful?