Polaris-Obfuscator: hiding control flow at the LLVM IR and MIR levels
Polaris: An LLVM-based obfuscator that protects software at various levels
At a glance
- What is it?
- Polaris is an LLVM 16.0.6 based obfuscator written in C++ that rewrites a program in two places, the IR that LLVM optimises and the MIR that the backend lowers, so that both a compiler front end and a decompiler have a harder time seeing the original logic. It is a research scale project with one tagged release, and the build asks you to compile LLVM yourself with assertions disabled.
- Who is it for?
- Polaris suits a team that already owns an LLVM based toolchain, can rebuild LLVM 16.0.6 with assertions off, and wants to raise the cost of static analysis against a shipped binary without changing what the program does.
- Can I use it commercially?
- Yes. Apache-2.0 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 161 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 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Two obfuscation layers, IR and MIR, in one compiler
Polaris is a compiler level obfuscator built on LLVM 16.0.6 and written in C++. Its stated goal is to transform code into a more complex form at the IR level or the assembly level while preserving the original semantics, and the stated purpose of that is to make the code harder to crack. The project is described as protecting software at various levels, and the top level tree is small: src/ for the passes, examples/ with three numbered example directories, doc/, imgs/ and a build.sh.
What separates it from the OLLVM framework, according to the project documentation, is the second layer. OLLVM offers obfuscation based on LLVM IR. Polaris adds obfuscation based on MIR inside the LLVM backend, which is a materially different target because MIR is the machine level intermediate representation used for instruction selection and scheduling, below the level where the IR passes operate.
That two layer arrangement is the core idea of the project. IR level obfuscation makes the source level structure hard to reason about, and a decompiler that has learned to undo common IR patterns can often recover something readable. Adding backend transformations on top makes the disassembly phase of the decompiler fail, so the function boundaries it relies on are wrong, and the IR level obfuscation has no obvious layer to strip. The two layers protect each other in a way that a single layer cannot.
Nine IR passes and the problem each one attacks
The IR level catalogue is nine named passes, and each one targets a specific weakness in a reader's analysis. Flattening, named fla, replaces the structured control flow of a function with a dispatcher and a state variable, so the original branches no longer appear as branches. Bogus control flow, bcf, inserts additional conditional jumps guarded by opaque predicates, which complicate the control flow graph without changing what the program computes. Alias access, alias, reaches local variables through pointer aliases instead of directly, which defeats a reader that assumes a variable is accessed where it was declared.
Indirection covers the call and branch edges. Indirect branch, indbr, computes the target address of a jump at runtime, and indirect call, indcall, does the same for a call target, so a static reader cannot tell which function is invoked without following the computation. String encryption, gvenc, encrypts global constants and decrypts them on the function stack rather than in a constructor section, which removes the visible plaintext from the data section and moves the decryption into the function that uses the value.
The remaining three work on the shape of the code. Instruction substitution, sub, replaces an arithmetic instruction with an equivalent set of arithmetic instructions, so a reader looking for a single multiply does not find one. Merge function, merge, folds several functions into one, which destroys the per-function boundaries a decompiler relies on. Linear MBA, mba, replaces bitwise operators using linear mixed boolean arithmetic, so the operator that the source used is not the operator the machine code contains.
Passes are combined by name, comma separated, and the set is deliberately composable.
Four MIR techniques aimed at the decompiler rather than the compiler
The backend techniques are described as making decompilers ineffective and preventing IR based obfuscation from being removed, and they work on the disassembly phase rather than on the source structure.
Dirty bytes insertion places meaningless data between machine instructions and uses opaque predicates and conditional jump instructions to prevent that data from ever being executed, so a linear disassembler has to follow a path through data that is not code. Function splitting changes the block structure so that a decompiler misidentifies a basic block as a function and tries to decompile it, which produces a wrong call graph rather than a merely noisy one. Junk instruction insertion adds numerous junk instructions that have side effects but no impact on program semantics, and the stated effect is that the data flow analysis decompilers conduct is impeded significantly. Instruction substitution at the MIR level replaces given instructions with several instructions with the same semantics, and it reduces the disparity between junk instructions and original instructions so that a reader cannot simply filter the junk out by appearance.
That last point is the subtle one. If junk instructions were trivially distinguishable, a decompiler would drop them and the technique would buy nothing. The backend substitution exists to make the injected code look like the code around it, and that is also why it reduces the visual difference between the two instruction classes.
The four techniques are stated to be the complete set used at the backend level, and the documentation says the backend data flow obfuscation can render decompilation ineffective and generate nonsensical pseudocode.
Building LLVM yourself with assertions switched off
There is a build.sh that builds the obfuscator, and the documentation also lays out the manual path for people who need to configure the build themselves. Either way you are compiling LLVM and Clang from the llvm-project directory, and one instruction is not optional: the note says to make sure -DLLVM_ENABLE_ASSERTIONS=Off. That is a hard requirement rather than a preference, and it is the kind of detail that will stop a first build if it is missed.
The configuration is a standard CMake configure of the LLVM tree with a generator and a set of options. The listed generator choices are Ninja, Unix Makefiles, Visual Studio and Xcode. The project options cover -DLLVM_ENABLE_PROJECTS and -DLLVM_ENABLE_RUNTIMES, with the documented example for LLVM, Clang, libcxx and libcxxabi given as -DLLVM_ENABLE_PROJECTS="clang" -DLLVM_ENABLE_RUNTIMES="libcxx;libcxxabi". Other options cover the install prefix, with a warning not to overwrite system copies of the runtime libraries LLVM provides, such as libc++ or libc++abi, the build type among Debug, Release, RelWithDebInfo and MinSizeRel, and the assertions switch itself.
The build then follows the usual LLVM pattern, with cmake --build build or the generator's own equivalent, a default target that builds all of LLVM, and a check-all target that runs the regression tests. The documentation warns that a serial build is slow and recommends a parallel one, with -j NNN for make and -j$(nproc) on Unix systems. The practical cost here is large: this is a full LLVM and Clang build before the obfuscator itself is compiled, and the check-all regression run is part of validating that setup.
Compiling a single file with the -mllvm pass list
Using the tool is a clang invocation rather than a separate command. The form given in the documentation is clang -mllvm -passes=<obfuscation passes> <source file> -o <target>, which passes the obfuscation passes through the -mllvm option so the custom pass plugin is picked up by the compiler you just built.
The worked example in the documentation enables two passes at once:
clang -mllvm -passes=fla,indcall test.cpp -o testThat command enables control flow flattening and indirect call obfuscation in one compile, and multiple passes are supported with names separated by commas. The available names are fla for flattening, gvenc for global variable and string encryption, indcall for indirect call obfuscation, indbr for indirect branch obfuscation, alias for alias access, bcf for bogus control flow, sub for instruction substitution, merge for functions merging, and mba for linear MBA.
The important property of the design is that obfuscation is opt-in per compilation. The compiler is still clang, the source language and the build flags are unchanged, and the passes are selected by name, which means you can enable the set that matters for a given artefact and leave it off everywhere else. What the documentation does not cover is how to select passes per file or per function rather than per compilation unit, and the pass list applies to every translation unit in the invocation.
One tagged release, a last push in May 2026 and no project home page
The repository is not archived and the last push was on 2026-05-15. The release history is one entry: Polaris 1.0, tagged v1.0, published on 2024-11-09. There is no project home page listed beyond the repository itself, so the README, the doc/ directory and the three example directories are the documentation, and the examples are the closest thing to a test corpus in the tree.
The gap between the last release and the last push is worth noticing. There is work in the tree that is newer than the only tagged release, so if you plan to deploy this, you have a choice between a tagged version that predates those changes and a moving commit, and the repository gives no compatibility statement either way. For a tool whose output is a compiler that other people build their code with, that is an uncomfortable position, because an unannounced change in a pass can change the binary your product ships without changing your source.
The build requirement sharpens that point. Because the tool is an LLVM plugin rather than a standalone binary, adopting it means owning an LLVM build that matches the plugin, and any change to that build has to be revalidated against your own code. The maintenance cost of this project is therefore mostly the cost of the toolchain around it, not the cost of the passes themselves.
Where obfuscation is the wrong answer, and the Apache-2.0 terms
Obfuscation raises the cost of static analysis. It does not change what a program does when it runs, and it does not protect a secret that the program has to use. Those two facts define where the tool is the wrong choice. If your risk is a determined reader with the binary and unlimited time, obfuscation slows them without stopping them. If your risk is a runtime attack, a memory disclosure bug, an exposed API, an over permissive service account or a licence check that happens in the client, then none of the nine IR passes or four backend techniques touch the actual exposure, and shipping an obfuscated build adds cost and debuggability problems to a product that is still insecure.
There is a second wrong-tool case, which is when the build and release cost is larger than the benefit. A full LLVM and Clang build with assertions off, plus a check-all regression run, plus a per-release validation that the obfuscated binary still passes your own test suite, is a pipeline cost that only pays off if the artefact you protect is worth more than the engineering time.
The licence is Apache-2.0, which is permissive and carries an explicit patent grant. For a compiler plugin, the relevant question is what you may do with the binaries and libraries you produce, and the terms cover the code you distribute; a note for anyone shipping obfuscated output is that the Apache-2.0 obligations attach to the components you redistribute, and you should check how the terms interact with the licence of the LLVM and Clang code you build from.
Editorial conclusion
Polaris suits a team that already owns an LLVM based toolchain, can rebuild LLVM 16.0.6 with assertions off, and wants to raise the cost of static analysis against a shipped binary without changing what the program does. It is the wrong choice for a project that needs a real protection boundary, for a team that cannot carry a multi hour LLVM build in its release pipeline, and for anything where build reproducibility matters more than obfuscation, since the last push was on 2026-05-15 and the only tagged release is Polaris 1.0 from 2024-11-09. Before adopting it, compile LLVM with -DLLVM_ENABLE_ASSERTIONS=Off as the build notes require, run your own test suite against a binary produced with every pass you plan to enable, and confirm under Apache-2.0 that publishing obfuscated output does not conflict with your own licence obligations.
Frequently asked questions
What is Polaris-Obfuscator and which LLVM version does it use?
Polaris is a code obfuscator written in C++ and based on LLVM 16.0.6. It applies obfuscation both at the LLVM IR level and at the MIR level in the backend, transforming the code into a more complex form while preserving the original semantics.
How do I use the Polaris obfuscation passes?
You invoke clang with the pass names passed through the -mllvm option, for example clang -mllvm -passes=fla,indcall test.cpp -o test. Multiple passes are enabled by separating their names with commas, and the supported names are fla, gvenc, indcall, indbr, alias, bcf, sub, merge and mba.
What is the difference between Polaris and the OLLVM framework?
The project documentation states that OLLVM provides obfuscation based on LLVM IR, while Polaris also provides obfuscation based on MIR inside the LLVM backend. That backend layer is designed to disrupt the disassembly phase of a decompiler so IR level obfuscation cannot simply be removed.
What has to be true before Polaris can be built?
You need to build LLVM and Clang from the llvm-project directory, and the build notes insist that -DLLVM_ENABLE_ASSERTIONS=Off is set. A build.sh is provided, and the CMake step accepts the usual generator and project options before the compiler is built with a parallel job count.
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/za233-polaris-obfuscator)Was this useful?