Skip to content

Commit 497d549

Browse files
authored
Merge branch 'master' into feat/minecraft-summerize
2 parents cec9a1c + 80e5b27 commit 497d549

21 files changed

Lines changed: 949 additions & 368 deletions

File tree

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package software.coley.recaf.services.deobfuscation.transform.generic;
2+
3+
import jakarta.annotation.Nonnull;
4+
import jakarta.enterprise.context.Dependent;
5+
import org.objectweb.asm.ClassReader;
6+
import org.objectweb.asm.ClassWriter;
7+
import org.objectweb.asm.tree.AbstractInsnNode;
8+
import org.objectweb.asm.tree.ClassNode;
9+
import org.objectweb.asm.tree.FrameNode;
10+
import org.objectweb.asm.tree.InsnList;
11+
import org.objectweb.asm.tree.MethodNode;
12+
import software.coley.recaf.info.JvmClassInfo;
13+
import software.coley.recaf.services.transform.JvmClassTransformer;
14+
import software.coley.recaf.services.transform.JvmTransformerContext;
15+
import software.coley.recaf.services.transform.TransformationException;
16+
import software.coley.recaf.workspace.model.Workspace;
17+
import software.coley.recaf.workspace.model.bundle.JvmClassBundle;
18+
import software.coley.recaf.workspace.model.resource.WorkspaceResource;
19+
20+
/**
21+
* A transformer that removes stack frames.
22+
*
23+
* @author Matt Coley
24+
*/
25+
@Dependent
26+
public class FrameRemovingTransformer implements JvmClassTransformer {
27+
@Override
28+
public void transform(@Nonnull JvmTransformerContext context, @Nonnull Workspace workspace,
29+
@Nonnull WorkspaceResource resource, @Nonnull JvmClassBundle bundle,
30+
@Nonnull JvmClassInfo initialClassState) throws TransformationException {
31+
if (context.isNode(bundle, initialClassState)) {
32+
ClassNode node = context.getNode(bundle, initialClassState);
33+
for (MethodNode method : node.methods) {
34+
InsnList instructions = method.instructions;
35+
if (instructions != null) {
36+
for (int i = instructions.size() - 1; i > 0; i--) {
37+
AbstractInsnNode insn = instructions.get(i);
38+
if (insn instanceof FrameNode)
39+
instructions.remove(insn);
40+
}
41+
}
42+
}
43+
} else {
44+
ClassReader reader = new ClassReader(context.getBytecode(bundle, initialClassState));
45+
ClassWriter writer = new ClassWriter(reader, 0);
46+
reader.accept(writer, ClassReader.SKIP_FRAMES);
47+
context.setBytecode(bundle, initialClassState, writer.toByteArray());
48+
}
49+
context.setRecomputeFrames(initialClassState.getName());
50+
}
51+
52+
@Nonnull
53+
@Override
54+
public String name() {
55+
return "Stack frame removal";
56+
}
57+
}

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

Lines changed: 152 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);
@@ -260,6 +319,7 @@ private boolean pass1PruneNeverThrowingOrDuplicate(@Nonnull JvmTransformerContex
260319
case FALOAD:
261320
case LALOAD:
262321
case SALOAD:
322+
case BALOAD:
263323
case AALOAD: {
264324
ReValue indexValue = frame.getStack(frame.getStackSize() - 1);
265325
ReValue arrayValue = frame.getStack(frame.getStackSize() - 2);
@@ -298,6 +358,7 @@ private boolean pass1PruneNeverThrowingOrDuplicate(@Nonnull JvmTransformerContex
298358
}
299359
case IASTORE:
300360
case DASTORE:
361+
case BASTORE:
301362
case FASTORE:
302363
case LASTORE:
303364
case SASTORE:
@@ -633,7 +694,10 @@ && doesHandleException(tryCatch, EX_CCE)) {
633694
return false;
634695
}
635696

636-
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+
637701
// TODO: Look for try blocks that end in 'throw T' with a 'catch T' handler
638702
// - Must always take the path
639703
// - Not always direct, can be '1 / 0' with a 'catch MathError' handler
@@ -642,6 +706,30 @@ private boolean pass2ConvertOpaqueThrowToDirectFlow() {
642706
// - Worst case, it relies on stack info from the thrown exception
643707
// (we can keep the 'new T' or replace the throwing '1 / 0' with 'new T')
644708
// - 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+
645733
return false;
646734
}
647735

@@ -737,6 +825,16 @@ boolean canThrow() {
737825
*/
738826
private record Block(@Nullable String type, int start, int end, int handler) {}
739827

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+
740838
/**
741839
* Range of instructions that can possibly throw exceptions.
742840
*
@@ -751,4 +849,50 @@ public ThrowingRange merge(@Nonnull ThrowingRange other) {
751849
return new ThrowingRange(Math.min(first, other.first), Math.max(last, other.last));
752850
}
753851
}
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+
}
754898
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package software.coley.recaf.services.deobfuscation.transform.generic;
2+
3+
import jakarta.annotation.Nonnull;
4+
import jakarta.enterprise.context.Dependent;
5+
import org.objectweb.asm.ClassReader;
6+
import org.objectweb.asm.ClassWriter;
7+
import org.objectweb.asm.tree.ClassNode;
8+
import org.objectweb.asm.tree.FieldNode;
9+
import org.objectweb.asm.tree.MethodNode;
10+
import org.objectweb.asm.tree.RecordComponentNode;
11+
import software.coley.recaf.info.JvmClassInfo;
12+
import software.coley.recaf.services.transform.JvmClassTransformer;
13+
import software.coley.recaf.services.transform.JvmTransformerContext;
14+
import software.coley.recaf.services.transform.TransformationException;
15+
import software.coley.recaf.util.visitors.UnknownAttributeRemovingVisitor;
16+
import software.coley.recaf.workspace.model.Workspace;
17+
import software.coley.recaf.workspace.model.bundle.JvmClassBundle;
18+
import software.coley.recaf.workspace.model.resource.WorkspaceResource;
19+
20+
/**
21+
* A transformer that removes unknown attributes.
22+
*
23+
* @author Matt Coley
24+
*/
25+
@Dependent
26+
public class UnknownAttributeRemovingTransformer implements JvmClassTransformer {
27+
@Override
28+
public void transform(@Nonnull JvmTransformerContext context, @Nonnull Workspace workspace,
29+
@Nonnull WorkspaceResource resource, @Nonnull JvmClassBundle bundle,
30+
@Nonnull JvmClassInfo initialClassState) throws TransformationException {
31+
if (context.isNode(bundle, initialClassState)) {
32+
ClassNode node = context.getNode(bundle, initialClassState);
33+
if (node.attrs != null)
34+
node.attrs.clear();
35+
for (FieldNode field : node.fields)
36+
if (field.attrs != null)
37+
field.attrs.clear();
38+
for (MethodNode method : node.methods)
39+
if (method.attrs != null)
40+
method.attrs.clear();
41+
if (node.recordComponents != null)
42+
for (RecordComponentNode recordComponent : node.recordComponents)
43+
if (recordComponent.attrs != null)
44+
recordComponent.attrs.clear();
45+
} else {
46+
ClassReader reader = new ClassReader(context.getBytecode(bundle, initialClassState));
47+
ClassWriter writer = new ClassWriter(reader, 0);
48+
reader.accept(new UnknownAttributeRemovingVisitor(writer), 0);
49+
context.setBytecode(bundle, initialClassState, writer.toByteArray());
50+
}
51+
}
52+
53+
@Nonnull
54+
@Override
55+
public String name() {
56+
return "Unknown attribute removal";
57+
}
58+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ else if (!throwingObserved) {
242242
// - They value is known at all points of variable use
243243
// - They value is read from, but only by other iinc operations
244244
LocalAccessState state = states.get(key(iinc.var, Type.INT));
245-
if (state != null && state.isEffectiveConstant() || state.getReads().stream().noneMatch(r -> r.instruction.getOpcode() != IINC)) {
245+
if (state != null && (state.isEffectiveConstant() || state.getReads().stream().noneMatch(r -> r.instruction.getOpcode() != IINC))) {
246246
instructions.set(iinc, new InsnNode(NOP));
247247
dirty = true;
248248
}

recaf-core/src/main/java/software/coley/recaf/services/transform/JvmTransformerContext.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,19 @@ public boolean pruneDeadCode(@Nonnull ClassNode declaringClass, @Nonnull MethodN
221221
return getJvmTransformer(DeadCodeRemovingTransformer.class).prune(declaringClass, method);
222222
}
223223

224+
/**
225+
* @param bundle
226+
* Bundle containing the class.
227+
* @param info
228+
* The class's model in the workspace.
229+
*
230+
* @return {@code true} when the context currently has the class represented as a node <i>(vs raw {@code byte[]})</i.>
231+
*/
232+
public boolean isNode(@Nonnull JvmClassBundle bundle, @Nonnull JvmClassInfo info) {
233+
JvmClassData data = getJvmClassData(bundle, info);
234+
return data.node != null;
235+
}
236+
224237
/**
225238
* Gets the current ASM node representation of the given class.
226239
* <p/>

0 commit comments

Comments
 (0)