Browse documentation
Documentation / Protection passes / Control-flow flattening

Control-flow flattening

ConditionalBranches, jumps, and fall-through are rewritten through a single dispatcher table per method.

What it does

Every control transfer in a method, an existing GOTO, an IF*'s two branches, ordinary fall-through between blocks, is rewritten to set a synthetic state local and jump to one shared dispatcher table at the top of the method, without physically reordering the underlying blocks. Every branch ends up looking like "jump to the dispatcher, which picks the next block from a table", enough to defeat a structured decompiler's usual if/while/for recovery. Block order is not shuffled, which keeps the output easier to verify.

Scope and boundaries

Excluded, to avoid emitting incorrect bytecode: constructors; any method that already has an exception handler; any method already containing a TABLESWITCH/LOOKUPSWITCH/JSR/RET; any method where a non-parameter local isn't provably int-typed everywhere it's stored (a long/float/double/reference store anywhere excludes the whole method, since the type is not guessed); and, checked with a dataflow analysis pass before flattening is attempted, any method where a value crosses a block boundary on the operand stack instead of through a local variable. That last case is common: a ternary expression fed directly into another call's argument list joins its two branches on the stack, not in a local.

Threat model

Raises the cost of reconstructing readable control flow from decompiled output, a structured decompiler's normal loop/branch recovery has nothing to latch onto once every transfer routes through one dispatcher. It doesn't change what the method actually computes, and it doesn't claim to defeat an attacker willing to trace the dispatcher table by hand.

Verified. TABLESWITCH dispatchers are present, per javap, in at least ten classes of a protected Apache Commons Exec build, with every method that was eligible for nativization still nativized alongside them. A ternary-joins-via-the-stack method from that same codebase is excluded, not crashed, after the dataflow check was added, the fix for a verifier crash found during development.