Skip to content

Fix MBA simplification cases reported at REcon 2026; optimize core math#48

Merged
kyle-elliott-tob merged 20 commits into
masterfrom
ft-recon-and-math-perf
Jun 30, 2026
Merged

Fix MBA simplification cases reported at REcon 2026; optimize core math#48
kyle-elliott-tob merged 20 commits into
masterfrom
ft-recon-and-math-perf

Conversation

@kyle-elliott-tob

@kyle-elliott-tob kyle-elliott-tob commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

This branch lands two related bodies of work on the simplifier.

MBA simplification fixes (reported by @arnaugamez at REcon 2026)

Fixes for MBA expressions CoBRA did not simplify before — left unsupported,
returned unchanged, or expanded into a larger form:

  • Recover A | B from the inclusion-exclusion form A + B - (A & B) during pattern-subtree peeling.
  • Recover parity sign-square forms by declining a no-op product-core short-circuit when the core has a full-width-confirmed spurious variable.
  • Fold carry-free high-bit masked addition ((x & 2^k) + (y & (2^k - 1))) & 2^k -> x & 2^k by seeding mask constants in the template decomposer.
  • Cancel structurally-repeated XOR operands (A ^ B) ^ B -> A at the exhaustion fallback (exact rewrite, full-width-checked).
  • Gate simplification output against input size so a change-of-basis expansion (e.g. sum(xi) - xor(xi)) keeps the smaller input instead of an exponential AND-basis form.

Simplifier cleanup and core-math optimization

Follow-on refactors and performance work, all behavior-preserving (bit-identical
outputs; dataset counts unchanged):

  • Hoist ExprStructurallyEqual to ExprUtils; generalize ContainsShr/ContainsXor into ContainsType(expr, kind); merge FlattenAdd/FlattenMul into one FlattenAssoc; match common factors in ExtractCommonFactor by structural equality.
  • Extract the output-size blow-up gate into IsCostBlowup; compute the input cost once and thread it; clear the stale exhaustion reason on the XOR-cancel success path; guard the XOR fallback with ContainsType.
  • Number theory / interpolation: stronger Newton seed in ModInverseOdd (one fewer doubling iteration); delegate ModInverseOddHalf to ModInverseOdd; canonical block butterfly in InterpolateCoefficients; reuse the interpolation grid across degrees in RecoverAndVerifyPoly (each support point evaluated once); recycle per-node buffers in EvalSigRecursive; precompute the falling-factorial and odd-part-factorial tables in WeightedPolyFit and RecoverFromGrid.
  • Boolean-first aux-var elimination and an AND-term precondition gate skip work in non-hot paths.

Testing

  • 1181 unit tests pass.
  • All dataset benchmarks pass with exact simplified/unsupported counts and reason-code categories unchanged: MSiMBA, Syntia, QSynthEA, PLDI Linear/Poly/NonPoly, LokiTiny, MbaObf, MbaFlatten, ObfuscatorX, NeuReduce, SiMBA TestData/Blast, Univariate/Multivariate/Permutation.

Credits

Bug reports by Arnau Gamez (@arnaugamez), raised during his REcon 2026 talk.

🤖 Generated with Claude Code

kyle-elliott-tob and others added 19 commits June 24, 2026 18:46
Deep Expr structural equality existed only as a file-local helper in
OrchestratorPasses.cpp. Move it to ExprUtils (declaration + definition)
so other passes can reuse it instead of re-implementing deep comparison.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
GAMBA recon cases obfuscate x | y as the inclusion-exclusion form
A + B - (A&B) with arbitrary (including arithmetic, non-disjoint)
operands, which CoBRA previously left as a messier equivalent. Add a
structural recovery to SimplifyPatternSubtrees that flattens the additive
form and collapses +c*A +c*B -c*(A&B) to +c*(A|B), reading A and B from
the AND node's children (B may span several additive terms). Gated by a
strict coefficient match (so XOR's -2*(A&B) cannot misfire), a
cost-reduction check, and a FullWidthCheck soundness backstop.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
For a single-product input, ExtractProductCore returns a core equal to the
whole input, whose direct full-width check trivially passes. Emitting it as a
solved candidate halts the worklist before the decomposition/signature passes
can recover a simpler form, so GAMBA recon cases like the parity sign-square
(x1+x2)*(1-2*(x3&1))*(1-2*(x3&1)) (and its XOR-self-cancel full form) were left
unchanged.

Decline the short-circuit when the product core is the whole input
(single_product) AND a full-width-confirmed spurious variable exists, so the
worklist continues to the extractors that recover the reduced form. Genuine
sum-of-product cores and cores with no spurious variable are unaffected, so
product-inside-bitwise decomposition (QSynth, Multivariate) is preserved.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The {0,1} signature of a high-bit masking result like x & 128 is all-zero (bit 7
is invisible on {0,1}), so the boolean candidate is 0 and fails the full-width
check. The case is solved by AuxVarEliminator + TemplateDecomposer, but its atom
pool seeded only {0, 1, 2, mask}, so it could not synthesize the literal mask
constant M for M >= 8 within the bounded layer depth. GAMBA recon cases like
((x & 128) + (y & 127)) & 128 (carry-free masked add: y&127 < 128 is disjoint
from bit 7) were left unsupported.

Seed data-derived constants into the pool after Populate: the OR-fold of the
observed probe outputs (recovers the mask) plus a few distinct output values
(cover split masks). Soundness is unchanged -- every candidate is
FullWidthCheckEval-gated, so extra constants only widen the search, and a genuine
carry (((x & 255) + (y & 255)) & 255) is not falsely collapsed to x & 255.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
(A ^ B) ^ B == A is exact at every bit width, but when the surviving operand A
has a degenerate {0,1} signature (e.g. ((x+y)&z)|3, which is constant 3 on
{0,1}), no signature or decomposition pass re-emits it, so the worklist
exhausts and CoBRA echoes the obfuscated input unchanged.

Add SimplifyXorChains (top-down: flatten each XOR chain and drop operands of
even multiplicity by exact structural equality) and apply it as an exhaustion
fallback in the orchestrator -- when the raw input collapses to a strictly
cheaper form, return that instead of reporting unsupported. The cancellation is
an exact algebraic identity (not a full-width approximation), so the result is
provably equivalent; the full-width check is defense in depth and the fallback
only fires on the otherwise-doomed exhaustion path.

QSynthEA gains 2 cases (verify-failed 7->6, search-exhausted 21->20) whose
inputs contained structurally-repeated XOR operands.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
A CoB-linear function such as sum(xi) - xor(xi) is recovered by change-of-basis
as a linear combination of AND-basis monomials whose size grows exponentially
with the variable count (an 8-variable input of ~30 weighted nodes expanded to
~1000). The core Simplify path had no input-vs-output size check, so it returned
the blow-up even though the input was already minimal.

Guard in ToSimplifyOutcome: when a successful result both more than doubles the
input AND exceeds an absolute floor (32 weighted nodes), keep the input instead
and report it unsupported with a kCostRejected reason. The ratio spares genuine
simplifications of large obfuscated inputs (cheaper output); the absolute floor
spares the small expansions of legitimate canonicalization (e.g. NOT-over-arith
lowering ~(b*b) -> -(b*b)-1, which roughly doubles a tiny expression).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The product-core no-op short-circuit ran the 128-probe full-width
EliminateAuxVars on every single-product core. Full-width spurious
variables are always a subset of the boolean ones, so an empty boolean
result is final: run the cheap 2-arg EliminateAuxVars first and only
escalate to the full-width overload when it finds a candidate. The gate
decision is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
TryRecoverInclusionExclusion flattened every additive subtree during
pattern-subtree peeling, even those with no AND term to recover. Add a
no-allocation precondition (AdditiveHasBinaryAnd) that mirrors
FlattenAdditive's descent and returns true only for the additive trees
that contain a top-level 2-child AND term -- the only terms
TryRecoverDisjunctionAt can act on. Additive nodes without an AND term
now skip the flatten and per-term scan entirely.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The data-derived constant seeding could Probe() and Push() the same
constant value repeatedly (or_fold and several equal target values). Push
already dedups the pool by probe-vals, so the repeats only wasted Probe()
calls. Track seeded values in a set and skip duplicates; the resulting
pool is identical.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…r match

- Merge the duplicated owning FlattenAdd and FlattenMul into one
  FlattenAssoc(node, kind, out); update the three call sites.
- Add ContainsXor (mirrors ContainsShr) for the orchestrator XOR guard.
- Match common factors in ExtractCommonFactor by ExprStructurallyEqual
  instead of a hash-plus-kind filter. Structural equality never matches a
  non-equal factor, removing the hash-collision unsoundness the docstring
  previously accepted; update the docstring to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…ostics

- Extract the output-size blow-up gate into IsCostBlowup(candidate,
  baseline) with named kBlowupRatio/kBlowupAbsFloor constants. Semantics
  are unchanged (output > 2x input and > 32 weighted size).
- Compute the input expression cost once in Simplify() and thread it
  through ToSimplifyOutcome, reusing it in the XOR exhaustion fallback so
  the input cost is no longer computed twice on that path.
- On the XOR-cancel success path, clear the exhaustion failure lineage
  (reason_code, cause_chain, candidate_failed_verification) so a
  kSimplified result no longer reports a stale search-exhausted reason.
- Guard the XOR fallback with ContainsXor so the clone and walk are
  skipped when the input has no XOR.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Both helpers were identical except for the node kind they matched.
Replace them with one ContainsType(expr, kind) and update all call sites
(Orchestrator shift/xor guards, SemilinearNormalizer, StructureRecovery,
the mask-audit bench, and the dynamic-mask tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Seed the Hensel inverse with ((3*x) ^ 2), which is correct to 5 bits (vs 3
for the bare x), so the doubling loop runs one fewer iteration at every
width (4 instead of 5 at 64-bit). Output is the unique inverse mod 2^bits,
so it is bit-identical to the previous seed. Add exhaustive small-width and
random 64-bit correctness tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
ModInverseOddHalf(x, w) is exactly the odd inverse at width w-1, so replace
its duplicated Hensel loop with a call to ModInverseOdd(x, w-1). It keeps
its name and tests and inherits the stronger seed.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Replace the scan-all-indices-with-branch Mobius loop with the canonical
block butterfly: each variable pairs index i with i+half over half-blocks,
touching exactly len/2 elements per variable with no per-element bit test
and contiguous access. Same subset-Mobius transform. Add a randomized
property test against a naive Mobius reference.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…Poly

Extract the forward-difference + factorial-coefficient conversion into
RecoverFromGrid. RecoverAndVerifyPoly now keeps an incrementally-grown
evaluation grid across degrees min..cap: each higher degree extends the
grid by its new shell instead of re-evaluating the whole grid from scratch,
so every support point is evaluated at most once. Reuse is exact because
the evaluator is pure; per-degree results, early-exit, and reason codes are
unchanged. RecoverMultivarPoly evaluates its grid then delegates to
RecoverFromGrid, so its behavior is identical.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The bottom-up boolean-signature evaluator allocated a fresh 2^n result
vector per tree node. Thread a free-list buffer pool through the recursion:
binary nodes release their right child's buffer, which leaf nodes then
reuse. This bounds live buffers (and allocations) to ~tree depth instead
of one per leaf. Output is bit-identical.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The matrix build recomputed FallingFactorial(coord, exp) once per
(row, col, dim) for only (grid_base) x (per_var_cap+1) distinct pairs.
Precompute the table once per solve and look it up. Bit-identical.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
OddPartFactorial(e, bw) equals the 64-bit odd-part product masked to bw,
so build a per-degree table once (incrementally) and use it under the
existing kPrecMask instead of recomputing the odd-part product per
monomial. Bit-identical.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@kyle-elliott-tob kyle-elliott-tob changed the title Recover additional GAMBA obfuscation forms; optimize core simplifier math Fix MBA simplification cases reported at REcon 2026; optimize core math Jun 30, 2026
@kyle-elliott-tob kyle-elliott-tob merged commit ccba252 into master Jun 30, 2026
9 checks passed
@kyle-elliott-tob kyle-elliott-tob deleted the ft-recon-and-math-perf branch June 30, 2026 14:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant