Open-source project
sefcom/oxidizer avatar
sefcom/oxidizer

Oxidizer: a Rust decompiler that tries to recover Rust abstractions

A Rust decompiler built on top of angr

545 stars19 forksPythonBSD-2-Clause

At a glance

What is it?
Oxidizer is a decompiler built on angr that targets stripped Rust binaries, with a six-stage pipeline that identifies the compiler version, strips compiler-inserted cleanup and bounds checks, infers struct and enum types including Option and Result, and restructures control flow back into match, if let and the ? operator. It has been merged into angr's master branch.
Who is it for?
Oxidizer suits a reverse engineer who needs to read a stripped Rust binary rather than a C one, since the pipeline is built around Rust's compilation patterns rather than bolting Rust names onto C-shaped output. It does not suit a general binary analysis workflow, and it does not suit anyone who cannot build a Rust toolchain, since the package mixes Python, Rust and native code.
Can I use it commercially?
Yes. BSD-2-Clause 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 45 days ago.
What is it written in?
Mainly Python, 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

C-oriented decompilers produce the wrong shape for Rust

Oxidizer is a Rust decompiler built on top of angr that generates concise and high-fidelity Rust pseudocode from stripped Rust binaries. Unlike existing C-oriented decompilers, it is specifically designed to handle Rust's unique compilation patterns and recover high-level Rust abstractions such as enums, pattern matching, error propagation with the ? operator, and macros including println!, format! and panic!.

The problem it addresses is stated bluntly in the Overview. Modern C decompilers, naming Hex-Rays, Ghidra, Binary Ninja and angr, produce verbose and inaccurate output when decompiling Rust binaries because they fail to recover high-level Rust abstractions from low-level implementations.

That failure is not cosmetic. A Rust binary compiled with optimisations has its enum matching lowered into jump tables and comparisons on discriminant values, its ownership and drop machinery turned into calls to drop_in_place and __rust_dealloc, its bounds checks left in as explicit branches, and its macros expanded into call sequences. A decompiler that treats all of that as ordinary machine control flow will faithfully reproduce it and produce something that reads like C with a strange calling convention.

So the goal is not to produce prettier C-shaped output with Rust variable names. It is to reconstruct the Rust program that was compiled, which is a much harder problem and a much more useful answer for someone auditing a binary.

The project is brought to you by SEFCOM at Arizona State University, and the licence is BSD-2-Clause. The primary language is recorded as Python, with a Rust component in the repository, and the last push was on 2026-09-08 with no GitHub releases.

One administrative point comes first, because it changes where you should send your work. The README states that Oxidizer has been merged into the master branch of angr, and that if you would like to contribute to Oxidizer, please submit pull requests and issues to angr. The evaluation pipeline and scripts live separately at oxidizer-eval.

Six stages, from binary analysis to Rust source

The Overview lists the pipeline as six numbered stages, and the ordering is the argument.

The first is binary-level analysis. It identifies the Rust compiler version via FLIRT-based function match rate analysis, recovers standard library functions, and loads version-specific struct, enum and function type databases, supporting Rust 1.39.0 to 1.93.0.

That version detection is the hinge of the whole design. Rust's monomorphisation, layout and standard library change between releases, so the type database you need for 1.70 is not the one you need for 1.90. Identifying the compiler version from how closely the binary's functions match the known patterns is what lets the later stages use the right tables at all.

The second stage is fCFG simplification without types. It removes extraneous code introduced by Rust's automatic resource management, naming drop_in_place and __rust_dealloc, and compiler-inserted security checks such as bounds checking.

The third stage is Rust type inference. It performs inter-procedural function prototype inference to recover struct and enum return types and argument types including Option<T> and Result<T, E>, enhanced with a constraint-based type inference algorithm called Retypd, extended with enum type support.

The fourth stage is fCFG simplification with types, and this is where the payoff appears. It recovers struct and enum initialisations, outlines common macros such as println!, format!, write! and panic!, and simplifies deref coercions.

The fifth stage is structuring: converting the simplified fCFG into high-level Rust control flow constructs, including match and if let pattern matching, and ? error propagation.

The sixth is Rust pseudocode generation, which outputs structured, human-readable Rust pseudocode.

The two simplification stages around the type inference are the clever part of the arrangement. You cannot outline a println! expansion until you know which types are involved, and you cannot recover an enum initialisation until you know the enum, but you cannot fully recover the types until the noise of drop calls and bounds checks is gone. So the pipeline simplifies once without type information to make inference tractable, infers, then simplifies again with types to recover the language constructs.

What the output actually looks like

The README includes a sample of generated Rust pseudocode, and it is worth reading closely because it shows both the recovery and the limits in one block.

rust
fn sub_455300(a0: i64, a1: i64, a2: i64, a3: i64) -> u32 {
    let v0: struct4;  // [bp-0x818]
    let v2: Result<struct4, struct8>;  // [bp-0x810]
    let v3: struct24;  // [bp-0x800], Other Possible Types: struct8
    let v4: u64;  // [bp-0x7f8]

Two things stand out. The first is that the types are recovered: v2 is a Result<struct4, struct8> and v15 is an Option<struct16>. Those are the Rust generic abstractions that a C-oriented decompiler has no way to express, and having them present changes how the control flow below reads.

The second is that the struct names are still anonymous. struct4, struct8 and struct24 are type placeholders, with the stack offset and, where the inference was ambiguous, a note of the other possible types. So the recovery is partial in an important way: the tool knows a value is an enum or a Result with particular type parameters, but it does not necessarily know the struct's name and field layout.

The control flow, though, is recognisably Rust.

rust
        match v15 {
            Some(_) => {
                v8 = sub_455f30(v6, a1);
                match v8 {
                    Err(_) => {
                        sub_454c90(&v8);
                    },
                    Ok(v7) => {
                        sub_454c70(sub_4560d0(&v7, v15, v14));
                    },
                }
            },
            None => {
                eprintln!("[!] Encryption failed for {:?}: {:?}", &v6, &v1);
            },
        }

That is a nested match on an Option and then on a Result, with an if let on an Ok binding earlier in the function, and the panic-style macro call recovered as an eprintln! rather than a call to a formatting function. The function names are still sub_ prefixed and unresolved, since the binary is stripped, but the shape of the program is Rust.

So the honest summary is that Oxidizer recovers the language, not the names. For an analyst who needs to understand what a binary does, that is the valuable part, because the control flow of a match on a Result is where the program's real branching lives, and reading it as a chain of integer comparisons is much harder.

Three ways to install, and the pip route installs angr from git

The Quick Start gives three routes, and the differences between them are about reproducibility rather than about features.

Docker is the shortest path to a working binary.

bash
git clone https://github.com/sefcom/oxidizer.git
cd oxidizer
docker build -t oxidizer .
bash
docker run -it --rm oxidizer python demo.py

The Dockerfile is a python:3.12-slim base with git, build-essential and cmake installed, uv copied in from a pinned image, and Rust installed through rustup because setuptools-rust needs it. The install step is uv sync --frozen --no-default-groups, with a comment that it is a reproducible install from uv.lock excluding dev and extras groups.

The uv local setup is the recommended one on the host, and the README explains why: uv installs the exact versions pinned in uv.lock, so every machine gets a byte-identical environment.

bash
git clone https://github.com/sefcom/oxidizer.git
cd oxidizer
uv sync --frozen --no-default-groups
bash
uv run python demo.py

The pip route is the one to read carefully, because it installs angr's own components from pinned git commits rather than from PyPI.

bash
pip install setuptools setuptools-rust
pip install rust_demangler==1.0
pip install git+https://github.com/angr/archinfo.git@84ad167543028b32e170d3659650707b3866185c
pip install git+https://github.com/angr/claripy.git@8b890bb13fe743bfdbaae119062631db4f10047b
pip install git+https://github.com/angr/pyvex.git@3f92fece7147e91cea401e14a3936f20860a402e
pip install git+https://github.com/angr/cle.git@ce3333d0e1e72936fdbb75eefd70299ead4fb998
cd oxidizer
pip install --no-build-isolation -e .

So the pip route installs a development version of angr itself, from specific commits, with setuptools-rust for the Rust extension and rust_demangler for symbol name demangling. The --no-build-isolation flag matters because the build needs the angr components already present.

The demo is then run directly.

bash
python demo.py

One detail about the dependency pinning: the pyproject.toml requires exact versions for the angr internals, with archinfo, claripy, cle and pyvex all at 9.2.197.dev0, and pins capstone at 5.0.3 and unicorn at 2.0.1.post1. These are development versions, which tells you the whole stack is version-locked as a set rather than released independently.

Where Oxidizer is the wrong tool

The first wrong-tool case is a C or C++ binary. The whole pipeline is Rust specific. The version detection looks for Rust compiler patterns, the type databases are Rust standard library and struct definitions, and the structuring stage emits match and the ? operator. Running it on a C binary would at best produce noise.

The second case is a workflow that needs more than decompilation. The repository is an angr checkout with an angr/ directory inside it, and angr is a binary analysis toolkit with dynamic symbolic execution and static analyses. If your need is to explore a binary's behaviour rather than read it, that is angr's job and the angr project is the place to work, particularly since Oxidizer now lives inside it.

The third case is an environment without a Rust toolchain or a compiler. The package mixes Python, Rust and native code. The pyproject build system requires setuptools-rust, the setup.py explicitly raises an exception saying angr requires setuptools-rust to build, and the Dockerfile installs build-essential, cmake and Rust for that reason. On a machine with only a Python interpreter and no compiler, this does not install.

The fourth case is a Rust binary outside the supported version range. The type databases cover Rust 1.39.0 to 1.93.0, and the version detection is a match rate against known patterns. A binary built with a much newer compiler than that range, or with a custom target or unusual feature set, will either detect poorly or load the wrong tables, and there is no documented fallback behaviour for that case.

There is also a question about the names, which the sample output makes plain. struct4 and struct24 are type placeholders, not recovered struct names. If your analysis question is about what fields a particular structure holds, the decompiled output will not answer it; you would need the type databases to be loaded for the right version and the inference to have found the layout, and even then the output shows the anonymous form.

On the repository state, the merge into angr matters for anyone planning to depend on it. Oxidizer is now in angr's master branch, and the angr project has its own release cadence, so Oxidizer ships with angr rather than on its own. The evaluation pipeline lives in a separate oxidizer-eval repository, and the project has a SECURITY.md, a COPYRIGHT file and a CLAUDE.md in the tree.

The publication is worth noting for anyone citing this work. Oxidizer: Toward Concise and High-fidelity Rust Decompilation appeared at the IEEE Symposium on Security and Privacy in 2026, with a PDF and an IEEE link in the README, and the licence is BSD-2-Clause with a LICENSE file at the root.

Editorial conclusion

Oxidizer suits a reverse engineer who needs to read a stripped Rust binary rather than a C one, since the pipeline is built around Rust's compilation patterns rather than bolting Rust names onto C-shaped output. It does not suit a general binary analysis workflow, and it does not suit anyone who cannot build a Rust toolchain, since the package mixes Python, Rust and native code. Before adopting, note that the project has been merged into the angr master branch and that contributions should go to angr rather than to this repository, and plan for the Rust version range it supports, which the README puts at 1.39.0 to 1.93.0.

Frequently asked questions

What is Oxidizer?

Oxidizer is a Rust decompiler built on top of angr that generates concise and high-fidelity Rust pseudocode from stripped Rust binaries. It is specifically designed to handle Rust's compilation patterns and recover high-level abstractions such as enums, pattern matching, error propagation with the ? operator and macros.

What are the six stages of the Oxidizer decompilation pipeline?

Binary-level analysis to identify the Rust compiler version and load type databases, fCFG simplification without types to remove drop and bounds-check code, Rust type inference to recover struct, enum, Option and Result types, fCFG simplification with types to recover initialisations and outline macros, structuring into match, if let and ? error propagation, and finally Rust pseudocode generation.

Which Rust versions does Oxidizer support?

The version-specific struct, enum and function type databases cover Rust 1.39.0 to 1.93.0, and the compiler version is identified from FLIRT-based function match rate analysis so the right database can be loaded.

How do I run Oxidizer on a stripped Rust binary?

With Docker, build the image with docker build -t oxidizer . and run docker run -it --rm oxidizer python demo.py. Locally with uv, run uv sync --frozen --no-default-groups after cloning, then uv run python demo.py. The pip route installs angr's internals from pinned git commits before pip install --no-build-isolation -e .

Where should I send contributions for Oxidizer?

The README states that Oxidizer has been merged into the master branch of angr, and that pull requests and issues should be submitted to angr rather than to the Oxidizer repository. The evaluation pipeline and scripts are in the separate oxidizer-eval repository.

What license is Oxidizer released under?

Oxidizer is released under the BSD-2-Clause license, with the LICENSE file at the root of the repository. The work was published at the IEEE Symposium on Security and Privacy in 2026 and is brought to you by SEFCOM at Arizona State University.

Official sources

  1. Issues
  2. License: BSD-2-Clause
  3. README
  4. sefcom/oxidizer on GitHub
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.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/sefcom-oxidizer.svg)](https://hysenlabs.com/projects/sefcom-oxidizer)

Was this useful?