Skip to content

Commit 935791f

Browse files
committed
Add pass to redundant try-catch transformer that prunes entries that are impossible to be utilized at runtime
1 parent 115bdae commit 935791f

File tree

2 files changed

+220
-8
lines changed

2 files changed

+220
-8
lines changed

recaf-core/src/main/java/software/coley/recaf/services/deobfuscation/transform/generic/RedundantTryCatchRemovingTransformer.java

Lines changed: 150 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@
99
import org.objectweb.asm.tree.ClassNode;
1010
import org.objectweb.asm.tree.InsnList;
1111
import org.objectweb.asm.tree.InsnNode;
12+
import org.objectweb.asm.tree.JumpInsnNode;
1213
import org.objectweb.asm.tree.LabelNode;
1314
import org.objectweb.asm.tree.MethodNode;
1415
import org.objectweb.asm.tree.MultiANewArrayInsnNode;
1516
import org.objectweb.asm.tree.TryCatchBlockNode;
1617
import org.objectweb.asm.tree.TypeInsnNode;
1718
import org.objectweb.asm.tree.analysis.Frame;
1819
import software.coley.recaf.info.JvmClassInfo;
19-
import software.coley.recaf.services.assembler.ExpressionCompileException;
2020
import software.coley.recaf.services.inheritance.InheritanceGraph;
2121
import software.coley.recaf.services.inheritance.InheritanceGraphService;
2222
import software.coley.recaf.services.inheritance.InheritanceVertex;
@@ -43,10 +43,13 @@
4343
import java.util.HashMap;
4444
import java.util.HashSet;
4545
import java.util.IdentityHashMap;
46+
import java.util.Iterator;
4647
import java.util.List;
4748
import java.util.Map;
49+
import java.util.Objects;
4850
import java.util.OptionalInt;
4951
import java.util.Set;
52+
import java.util.stream.Collectors;
5053

5154
import static org.objectweb.asm.Opcodes.*;
5255

@@ -93,14 +96,69 @@ public void transform(@Nonnull JvmTransformerContext context, @Nonnull Workspace
9396
if (method.instructions == null || method.tryCatchBlocks == null || method.tryCatchBlocks.isEmpty())
9497
continue;
9598

96-
dirty |= pass0PruneNeverThrown(context, workspace, node, method);
97-
dirty |= pass1PruneNeverThrowingOrDuplicate(context, node, method);
98-
dirty |= pass2ConvertOpaqueThrowToDirectFlow();
99+
dirty |= pass0PruneIgnoredHandlers(context, node, method);
100+
dirty |= pass1PruneNeverThrown(context, workspace, node, method);
101+
dirty |= pass2PruneNeverThrowingOrDuplicate(context, node, method);
99102
}
100103
if (dirty)
101104
context.setNode(bundle, initialClassState, node);
102105
}
103106

107+
/**
108+
* Remove try-catch blocks that cannot possibly be utilized at runtime.
109+
*
110+
* @param context
111+
* Transformer context.
112+
* @param node
113+
* Defining class.
114+
* @param method
115+
* Method to transform.
116+
*
117+
* @return {@code true} when one or more try-catch blocks have been removed.
118+
*
119+
* @throws TransformationException
120+
* Thrown when dead code after transformation could not be pruned.
121+
*/
122+
private boolean pass0PruneIgnoredHandlers(@Nonnull JvmTransformerContext context, @Nonnull ClassNode node, @Nonnull MethodNode method) throws TransformationException {
123+
// Given the following {start, end, handler, ex-type} blocks:
124+
// { R, S, Q, * },
125+
// { R, S, C, * },
126+
// { R, S, S, Ljava/lang/ArrayIndexOutOfBoundsException; }
127+
// Only the first is going to be used.
128+
// - It appears first, so it will be checked first by the JVM
129+
// - Its range covers all possible instructions of the other two try blocks
130+
// - Its handled type is more generic ("*" is catch-all)
131+
// See: https://github.com/openjdk/jdk21u/blob/master/src/hotspot/share/oops/method.cpp#L227
132+
//
133+
// Process:
134+
// 1. Collect try-catch handlers keyed by their range
135+
// 2. Prune handlers of narrower types in the collection
136+
// 3. Retain only remaining handlers in the collection
137+
List<TryCatchBlockNode> blocks = new ArrayList<>(method.tryCatchBlocks);
138+
Map<ThrowingRange, Handlers> handlersMap = new HashMap<>();
139+
for (TryCatchBlockNode block : blocks) {
140+
int start = AsmInsnUtil.indexOf(block.start);
141+
int end = AsmInsnUtil.indexOf(block.end);
142+
if (start < end) {
143+
ThrowingRange range = new ThrowingRange(start, end);
144+
handlersMap.computeIfAbsent(range, r -> new Handlers()).addBlock(block);
145+
}
146+
}
147+
for (Handlers handlers : handlersMap.values())
148+
handlers.prune(inheritanceGraph);
149+
Set<TryCatchBlockNode> allHandlers = handlersMap.values()
150+
.stream()
151+
.flatMap(handlers -> handlers.blocks.stream())
152+
.collect(Collectors.toSet());
153+
if (method.tryCatchBlocks.retainAll(allHandlers)) {
154+
// Removing handlers can mean blocks starting with an expected 'Throwable' on the stack are now invalid.
155+
// These should be dead code though, so if we prune code that isn't visitable these should go away.
156+
context.pruneDeadCode(node, method);
157+
return true;
158+
}
159+
return false;
160+
}
161+
104162
/**
105163
* Remove try-catch blocks that have handle exception types that are defined in the workspace
106164
* but never actually constructed and thrown.
@@ -117,9 +175,10 @@ public void transform(@Nonnull JvmTransformerContext context, @Nonnull Workspace
117175
* @return {@code true} when one or more try-catch blocks have been removed.
118176
*
119177
* @throws TransformationException
120-
* Thrown when the {@link ExpressionCompileException} cannot be found in the transformer context.
178+
* Thrown when the {@link ExceptionCollectionTransformer} cannot be found in the transformer context,
179+
* or when dead code couldn't be pruned.
121180
*/
122-
private boolean pass0PruneNeverThrown(@Nonnull JvmTransformerContext context, @Nonnull Workspace workspace, @Nonnull ClassNode node, @Nonnull MethodNode method) throws TransformationException {
181+
private boolean pass1PruneNeverThrown(@Nonnull JvmTransformerContext context, @Nonnull Workspace workspace, @Nonnull ClassNode node, @Nonnull MethodNode method) throws TransformationException {
123182
ExceptionCollectionTransformer exceptions = context.getJvmTransformer(ExceptionCollectionTransformer.class);
124183

125184
// Collect which blocks are candidates for removal.
@@ -228,7 +287,7 @@ private boolean pass0PruneNeverThrown(@Nonnull JvmTransformerContext context, @N
228287
* @throws TransformationException
229288
* Thrown when code cannot be analyzed <i>(Needed for certain checks)</i>.
230289
*/
231-
private boolean pass1PruneNeverThrowingOrDuplicate(@Nonnull JvmTransformerContext context, @Nonnull ClassNode node, @Nonnull MethodNode method) throws TransformationException {
290+
private boolean pass2PruneNeverThrowingOrDuplicate(@Nonnull JvmTransformerContext context, @Nonnull ClassNode node, @Nonnull MethodNode method) throws TransformationException {
232291
InsnList instructions = method.instructions;
233292
List<TryCatchBlockNode> tryCatchBlocks = method.tryCatchBlocks;
234293
Frame<ReValue>[] frames = context.analyze(inheritanceGraph, node, method);
@@ -635,7 +694,10 @@ && doesHandleException(tryCatch, EX_CCE)) {
635694
return false;
636695
}
637696

638-
private boolean pass2ConvertOpaqueThrowToDirectFlow() {
697+
private boolean pass3ConvertOpaqueThrowToDirectFlow(@Nonnull JvmTransformerContext context, @Nonnull ClassNode node, @Nonnull MethodNode method) {
698+
// TODO: Rather than make this a separate pass, I think we can work the intended behavior outlined here into the prior pass
699+
if (true) return false;
700+
639701
// TODO: Look for try blocks that end in 'throw T' with a 'catch T' handler
640702
// - Must always take the path
641703
// - Not always direct, can be '1 / 0' with a 'catch MathError' handler
@@ -644,6 +706,30 @@ private boolean pass2ConvertOpaqueThrowToDirectFlow() {
644706
// - Worst case, it relies on stack info from the thrown exception
645707
// (we can keep the 'new T' or replace the throwing '1 / 0' with 'new T')
646708
// - If found, replace the throwing code with a 'goto handler'
709+
InsnList instructions = method.instructions;
710+
for (TryCatchBlockNode tryCatch : new ArrayList<>(method.tryCatchBlocks)) {
711+
int start = instructions.indexOf(tryCatch.start);
712+
int end = instructions.indexOf(tryCatch.end);
713+
714+
// TODO: Validate that the block WILL flow into a 'throw' case
715+
// - Same code as prior pass?
716+
boolean willThrow = true;
717+
Set<AbstractInsnNode> throwingInstructions = Collections.newSetFromMap(new IdentityHashMap<>());
718+
for (int i = start; i < end; i++) {
719+
// TODO: Mark willThow false if control flow leads to path where no-100% throwing behavior is observed
720+
}
721+
722+
// If the try range of the block WILL throw, we can replace the offending instructions with jumps to the handler block
723+
if (willThrow && !throwingInstructions.isEmpty()) {
724+
for (AbstractInsnNode thrower : throwingInstructions) {
725+
// TODO: If exception is not already on stack top, replace with junk exception (null is not a good replacement)
726+
instructions.insertBefore(thrower, new InsnNode(ACONST_NULL));
727+
instructions.insertBefore(thrower, new JumpInsnNode(GOTO, tryCatch.handler));
728+
}
729+
method.tryCatchBlocks.remove(tryCatch);
730+
}
731+
}
732+
647733
return false;
648734
}
649735

@@ -739,6 +825,16 @@ boolean canThrow() {
739825
*/
740826
private record Block(@Nullable String type, int start, int end, int handler) {}
741827

828+
/**
829+
* Range of some code.
830+
*
831+
* @param start
832+
* Start label.
833+
* @param end
834+
* End label.
835+
*/
836+
private record Range(@Nonnull LabelNode start, @Nonnull LabelNode end) {}
837+
742838
/**
743839
* Range of instructions that can possibly throw exceptions.
744840
*
@@ -753,4 +849,50 @@ public ThrowingRange merge(@Nonnull ThrowingRange other) {
753849
return new ThrowingRange(Math.min(first, other.first), Math.max(last, other.last));
754850
}
755851
}
852+
853+
/**
854+
* Collection of try catch blocks.
855+
*
856+
* @param blocks
857+
* Wrapped list of blocks.
858+
* @param seenTypes
859+
* Observed types handled by the blocks.
860+
*/
861+
private record Handlers(@Nonnull List<TryCatchBlockNode> blocks, @Nonnull Set<String> seenTypes) {
862+
private Handlers() {
863+
this(new ArrayList<>(), new HashSet<>());
864+
}
865+
866+
/**
867+
* @param block
868+
* Block to add.
869+
*/
870+
public void addBlock(@Nonnull TryCatchBlockNode block) {
871+
blocks.add(block);
872+
}
873+
874+
/**
875+
* Remove entries from {@link #blocks} that are redundant.
876+
*
877+
* @param graph
878+
* Inheritance graph for classes in the workspace.
879+
*/
880+
public void prune(@Nonnull InheritanceGraph graph) {
881+
Iterator<TryCatchBlockNode> it = blocks.iterator();
882+
while (it.hasNext()) {
883+
TryCatchBlockNode block = it.next();
884+
String handledType = Objects.requireNonNullElse(block.type, "java/lang/Object");
885+
inner:
886+
{
887+
for (String seenType : seenTypes) {
888+
if (graph.isAssignableFrom(seenType, handledType)) {
889+
it.remove();
890+
break inner;
891+
}
892+
}
893+
seenTypes.add(handledType);
894+
}
895+
}
896+
}
897+
}
756898
}

recaf-core/src/test/java/software/coley/recaf/services/deobfuscation/TryCatchDeobfuscationTest.java

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package software.coley.recaf.services.deobfuscation;
22

3+
import org.junit.jupiter.api.Disabled;
34
import org.junit.jupiter.api.Test;
45
import software.coley.recaf.services.deobfuscation.transform.generic.DuplicateCatchMergingTransformer;
56
import software.coley.recaf.services.deobfuscation.transform.generic.RedundantTryCatchRemovingTransformer;
@@ -87,6 +88,75 @@ void redundantTryCatch() {
8788
});
8889
}
8990

91+
@Test
92+
void removeCatchBlocksNotUsableAtRuntime() {
93+
// The JVM will use the first of "duplicate" blocks like this.
94+
// More details about this behavior can be found in the redundant catch removing transformer.
95+
String asm = """
96+
.method public static example (I[B)V {
97+
parameters: { index, array },
98+
exceptions: {
99+
{ A, B, C, * },
100+
{ A, B, D, * },
101+
{ A, B, C, Ljava/lang/ArrayIndexOutOfBoundsException; }
102+
},
103+
code: {
104+
A:
105+
aload array
106+
iload index
107+
baload
108+
pop
109+
B:
110+
goto END
111+
C:
112+
pop
113+
goto END
114+
D:
115+
invokevirtual java/lang/Throwable.printStackTrace ()V
116+
goto END
117+
END:
118+
return
119+
}
120+
}
121+
""";
122+
validateAfterAssembly(asm, List.of(RedundantTryCatchRemovingTransformer.class), dis -> {
123+
assertEquals(1, StringUtil.count("{ A, B, C, * }", dis), "Expected to keep first try-catch");
124+
assertEquals(0, StringUtil.count("{ A, B, D, * }", dis), "Expected to drop second try-catch");
125+
assertEquals(0, StringUtil.count("ArrayIndexOutOfBoundsException", dis), "Expected to drop third try-catch");
126+
assertEquals(0, StringUtil.count("printStackTrace", dis), "Expected to prune dead code of removed D handler");
127+
});
128+
}
129+
130+
@Test
131+
@Disabled
132+
void convertAlwaysThrowIntoDirectControlFlow() {
133+
// TODO: Implement redundant transformer pass for this
134+
String asm = """
135+
.method public static example ()V {
136+
exceptions: {
137+
{ A, B, C, Ljava/lang/Exception; }
138+
},
139+
code: {
140+
A:
141+
aconst_null
142+
athrow
143+
B:
144+
invokestatic Foo.skipped ()V
145+
goto END
146+
C:
147+
invokestatic Foo.prepop ()V
148+
pop
149+
invokestatic Foo.postpop ()V
150+
END:
151+
return
152+
}
153+
}
154+
""";
155+
validateAfterAssembly(asm, List.of(RedundantTryCatchRemovingTransformer.class), dis -> {
156+
157+
});
158+
}
159+
90160
@Test
91161
void redundantCatchOfTypeNeverThrownInWorkspace() {
92162
// If we observe 'BogusException' defined in the workspace and know it is never actually constructed

0 commit comments

Comments
 (0)