Decline proxy inlining that would duplicate an argument - #86
Open
ryoheinamiki515 wants to merge 3 commits into
Open
Decline proxy inlining that would duplicate an argument#86ryoheinamiki515 wants to merge 3 commits into
ryoheinamiki515 wants to merge 3 commits into
Conversation
replaceProxyFunctionUsages inlines every proxy call. getReplacement pastes the argument at each usage of its parameter, so a parameter the body uses twice duplicates the argument. A nested chain of such calls grows exponentially, and a side effect in the argument runs twice. Inline such a call only when every multi-use parameter receives a literal, an identifier or this. Keep the declaration of a proxy function whose call was declined, so the remaining call still resolves.
findAliases removed an alias declaration such as var q = p before any inline decision. A call that is not inlined then names an identifier that the output no longer declares. 1.1.7 already emits that shape for an aliased cyclic proxy function. Record the alias declarations and remove them after replacement, and only for a proxy function whose calls were all inlined.
The first version of this branch allowed six node types. That declined a string-array read, which is the dominant obfuscation shape, because ProxyRemover runs before ArrayUnpacker. State the real precondition instead. An argument is safe to duplicate when it evaluates with no side effect and holds at most 4 nodes. The purity half stops the exponential chain, because a chain needs a nested call. The size half bounds the duplication whatever the pass order is. Run the proxy pass again after the array unpacking, next to the second expression simplifier that exists for the same reason. A string-array argument then reaches the guard as a literal, and it inlines.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the exponential inlining reported in #27, and the argument duplication it comes from.
The correctness bug
ProxyFunction.getReplacementpastes the argument node at every usage of its parameter. When the body uses a parameter more than once, the argument runs more than once:1.1.7 returns
var x = f() << 2 | f() >>> 30;.fnow runs twice.The growth
replaceProxyFunctionUsagesrecurses into each replacement, so a nested chain of such calls compounds the duplication. Input isp(p(...p(a,b)...,b),b)with thepabove, under the default config.Depth 64 is not synthetic. The common minified MD5 snippet chains 64 rounds as nested calls,
f = md5_ii(f = md5_ii(f = md5_hh(.... Each round passes throughmd5_cmnandbit_rol, andbit_roluses its first parameter twice.One real page script that hits it:
https://apg01.newzware.com/insights/common/insights.js, 15,555 bytes. On 1.1.7 it returns no result inside a 5-second deadline. On this branch it returns in 30 ms. #36 reports the same shape on a different script.The change
ProxyFunction.isSafeToInline(args)declines the inline when a parameter that the body uses more than once receives an argument that is not safe to duplicate.ProxyRemoverthen leaves the call in place. Inlining of every other call is unchanged.ProxyFunction.isDuplicable(node)states the precondition thatgetReplacementnever checked. An argument is safe to duplicate when both hold.new, an assignment,++,--anddeleteeach have one. A member expression can run a getter. A regular expression, an array, an object and a function each have their own identity, which a duplicate would split.maxDuplicableNodes, which is 4.Condition 1 alone stops the exponential chain, because a chain needs a nested call and a call is never duplicable. Condition 2 bounds the duplication whatever the pass order is. The confirmed trigger inlines a twice-used parameter to a 9 node expression, so the budget of 4 stops that expression from duplicating again.
A declined call still names the proxy function, so
removeProxyFunctionsmust keep that declaration.retainedProxyFunctionIdsrecords it, next to the existingcyclicProxyFunctionIds.The pass order
ProxyRemoverruns first, andArrayUnpackerruns third, so a string-array argument still reads as a member expression when the guard sees it. That declines the dominant obfuscation shape:This branch pushes a second
ProxyRemoverafter the array unpacking, next to the secondExpressionSimplifierthat already exists for the same reason. The argument then reaches the guard as the string'aa', and it inlines. Output for the script above isvar x = 0;, the same as 1.1.7.The alias binding
findAliasesremoved an alias declaration such asvar q = p;before any inline decision. A call that is not inlined then names an identifier that the output no longer declares. 1.1.7 already emits that shape for an aliased cyclic proxy function:1.1.7 drops
var c = a;and keepsvar z = c(1);.aliasDeclarationsrecords each alias, andremoveAliasDeclarationsremoves it after replacement, only for a proxy function whose calls were all inlined. The alias of a retained or cyclic proxy function stays.Verification
tscis clean. I ran the table above, the cases below, and the real script through the compiled output of this branch. I also extracted the 3,561-byte MD5 helper set from that script on its own. 1.1.7 returns no result for it inside a 10-second deadline. This branch returns 5,433 bytes in 22 ms.p(f(), 2)p(f(), 2), declaration keptp(a, 2)a << 2 | a >>> 30p(-1, 2)p(a.b, 2)p(i++, 2)++has a side effectvar x = 0;, same as 1.1.7q(f())withfunction q(d){return d+1}f() + 1p(a, 2)andp(f(), 2)in one scriptvar q = p; q(f(), 2)var q = p;is keptvar q = p; q(a, 2)var q = p;is removedvar c = a;is now kept