Safety rails & budgeting¶
Obfuscation multiplies IR. Left unchecked, a heavy annotation on a large function can blow up compile time and binary size. xollvm gates every transform on size thresholds and an IR-growth budget, so obfuscation degrades gracefully instead of exploding.
Function-size gates¶
Skip functions that are too big or too deeply nested before any pass touches them:
| Flag | Default | Meaning |
|---|---|---|
-obf-max-function-insts=<N> |
0 (off) | Skip functions with more than N instructions. |
-obf-max-function-blocks=<N> |
0 (off) | Skip functions with more than N basic blocks. |
-obf-max-loop-depth=<N> |
0 (off) | Skip functions whose loop nesting exceeds N. |
0 disables a gate. These are global — they apply to every annotated function.
The IR-growth budget¶
Each function has a budget: a ceiling on how much its instruction count may grow across all passes. When a pass would exceed it, sites are throttled or skipped rather than emitted.
| Flag | Default | Meaning |
|---|---|---|
-obf-ir-budget-multiplier=<N> |
50 | Budget = insts_before × N (clamped by the max below). 0 = unlimited. |
-obf-ir-budget-max=<N> |
0 (off) | Absolute per-function instruction ceiling. 0 = no hard cap. |
So by default a function may grow up to 50× its original size, with no absolute cap. Tighten both for size-sensitive targets:
opt -passes=obfuscation app.ll -S -o app.obf.ll \
-obf-ir-budget-multiplier=20 -obf-ir-budget-max=8000
Two enforcement tiers:
- The multiplier limit is advisory — budget-aware passes self-throttle toward it, and the driver skips remaining passes once it is already exceeded before a pass runs.
- The hard cap (
-obf-ir-budget-max) is a hard guarantee. If a single pass expands a function past the cap, the driver rolls that function back to its pre-pass body and records the pass as skipped (reasonbudget_rollback). This stops a non-throttling or pathological pass (e.g.flatteningon a nasty CFG) from ever shipping a function larger than the cap. With no hard cap set (the default) no snapshot is taken and output is unchanged.
Budgets: global or per-function
Budgets can be set globally via the command-line flags above, or per function via annotation
tokens — budget= / budgetMultiplier= (the multiplier) and budgetMax= (the hard cap).
A per-function value overrides the global default for that function (last annotation wins).
Per-pass caps like maxSites / maxBlocks are per-annotation too and stack on top of the budget.
Reading budget pressure¶
When the report shows many "skipped due to budget" entries, the function hit its ceiling. Two ways to respond:
- Want more obfuscation: raise
-obf-ir-budget-multiplier/-obf-ir-budget-max. - Want smaller output: lower per-pass
prob,maxDepth,loop,maxSites, or annotate fewer functions.
# see what was skipped and why
opt -passes=obfuscation app.ll -S -o app.obf.ll -obf-report-dir=obf_report -obf-verbose
The report JSON records budget utilisation per function; -obf-verbose prints skip reasons live.
See Reports & CFG visualization.
Correctness rails¶
Beyond size, two flags guard correctness and debuggability:
| Flag | Default | Meaning |
|---|---|---|
-obf-verify |
off | Run IR verification before/after each stage — catches an invalid transform close to its source. |
-obf-debug-synthetic |
on | Give inserted instructions synthetic line-0 debug locations so source steppers don't jump erratically. |
Keep -obf-verify on while developing an annotation set; it turns a mysterious downstream crash
into a precise "this pass produced invalid IR" error.