Skip to content

Commit 04ef84f

Browse files
committed
VariableFoldingTransformer improvements for cases where temp copy variables are used
1 parent 3d88d69 commit 04ef84f

File tree

3 files changed

+422
-52
lines changed

3 files changed

+422
-52
lines changed

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

Lines changed: 210 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.objectweb.asm.Type;
1111
import org.objectweb.asm.tree.AbstractInsnNode;
1212
import org.objectweb.asm.tree.ClassNode;
13+
import org.objectweb.asm.tree.FieldInsnNode;
1314
import org.objectweb.asm.tree.IincInsnNode;
1415
import org.objectweb.asm.tree.InsnList;
1516
import org.objectweb.asm.tree.InsnNode;
@@ -28,6 +29,8 @@
2829
import software.coley.recaf.services.transform.JvmTransformerContext;
2930
import software.coley.recaf.services.transform.TransformationException;
3031
import software.coley.recaf.util.AccessFlag;
32+
import software.coley.recaf.util.AsmInsnUtil;
33+
import software.coley.recaf.util.Types;
3134
import software.coley.recaf.util.analysis.Nullness;
3235
import software.coley.recaf.util.analysis.value.ArrayValue;
3336
import software.coley.recaf.util.analysis.value.DoubleValue;
@@ -43,8 +46,9 @@
4346
import software.coley.recaf.workspace.model.resource.WorkspaceResource;
4447

4548
import java.util.Collections;
49+
import java.util.Iterator;
50+
import java.util.NavigableSet;
4651
import java.util.Set;
47-
import java.util.SortedSet;
4852
import java.util.TreeSet;
4953
import java.util.stream.Collectors;
5054

@@ -125,7 +129,6 @@ public void transform(@Nonnull JvmTransformerContext context, @Nonnull Workspace
125129
Frame<ReValue> frame = frames[i];
126130
if (frame == null)
127131
continue; // Skip dead code
128-
ReValue top = frame.getStack(frame.getStackSize() - 1);
129132
LocalAccessState state = states.computeIfAbsent(key(vin.var, getTypeForVarInsn(vin).getSort()), k -> new LocalAccessState(vin.var));
130133
state.addRead(i, vin);
131134
}
@@ -135,13 +138,13 @@ else if (isVarStore(op) && insn instanceof VarInsnNode vin) {
135138
Frame<ReValue> frame = frames[i];
136139
if (frame == null)
137140
continue; // Skip dead code
138-
ReValue top = frame.getStack(frame.getStackSize() - 1);
139141
LocalAccessState state = states.computeIfAbsent(key(vin.var, getTypeForVarInsn(vin).getSort()), k -> new LocalAccessState(vin.var));
140142
state.addWrite(i, vin);
141143

142144
// If we're moving forward in the method from the beginning, and haven't seen
143145
// the variable being used yet or any kind of control flow, then we can safely
144146
// just replace the state with the value on the stack instead of merging it.
147+
ReValue top = frame.getStack(frame.getStackSize() - 1);
145148
if (state.getReadsUpTo(i).isEmpty() && !controlFlowObserved && !throwingObserved)
146149
state.setState(top);
147150
else
@@ -230,9 +233,11 @@ else if (!throwingObserved) {
230233
// Remove variable writes if:
231234
// - They are never read from
232235
// - They are inlinable values (arrays for instance cannot be inlined as a single value providing instruction)
233-
// - They are read from, but only used with a single known constant value (which we inline above).
236+
// - They are read from, but only used with a single known constant value (which we inline above)
234237
Type varType = getTypeForVarInsn(vin);
235-
LocalAccessState state = states.get(key(vin.var, varType.getSort()));
238+
int varSort = varType.getSort();
239+
int varKey = key(vin.var, varSort);
240+
LocalAccessState state = states.get(varKey);
236241
if (state != null && state.isInlinableValue() && (state.getReads().isEmpty() || state.isEffectiveConstant())) {
237242
AbstractInsnNode previous = insn.getPrevious();
238243
if (OpaqueConstantFoldingTransformer.isSupportedValueProducer(previous)) {
@@ -246,6 +251,36 @@ else if (!throwingObserved) {
246251
}
247252
dirty = true;
248253
}
254+
255+
// Remove variable writes if:
256+
// - This write is never read from, even if the variable is read from later after a different assignment.
257+
// Between the potential unused write (this one) and the following write preceding the first read
258+
// there must not be any control-flow altering instructions.
259+
// TODO: Implement this (probably requires better state tracking refactoring)
260+
261+
// Remove variable writes + usages if:
262+
// - The variable is a redundant copy of an existing other variable
263+
for (int keyX : states.keySet()) {
264+
// Check if the current store is redundant with another variable.
265+
int slotX = slotFromKey(keyX);
266+
if (slotX != vin.var && isRedundantStore(states, instructions, i, slotX, vin.var, varSort)) {
267+
LocalAccessState stateY = states.get(varKey);
268+
269+
// Replace usage of the redundant variable.
270+
if (!stateY.getReads().isEmpty()) {
271+
replaceRedundantVariableUsage(instructions, slotX, vin.var, varSort);
272+
stateY.getReads().clear();
273+
}
274+
275+
// Update the tracking state of the redundant variable.
276+
stateY.getWrites().removeIf(l -> l.instruction == vin);
277+
278+
// Replace store instruction with POP.
279+
if (instructions.indexOf(vin) >= 0)
280+
instructions.set(vin, new InsnNode(Types.isWide(varType) ? POP2 : POP));
281+
dirty = true;
282+
}
283+
}
249284
} else if (op == IINC && insn instanceof IincInsnNode iinc) {
250285
// Remove variable increments if:
251286
// - They value is known at all points of variable use
@@ -262,6 +297,151 @@ else if (!throwingObserved) {
262297
context.setNode(bundle, initialClassState, node);
263298
}
264299

300+
/**
301+
* Determines if a store operation to a variable is redundant, meaning it mirrors
302+
* the value of another variable, and neither variable is modified between their usage.
303+
*
304+
* @param states
305+
* Variable access states.
306+
* @param instructions
307+
* Method instructions.
308+
* @param slotX
309+
* The original variable index.
310+
* @param slotY
311+
* The target variable index to check for redundancy.
312+
* @param typeSort
313+
* The type sort of the variable, to handle all primitives + object types.
314+
*
315+
* @return {@code true} when the store to slotY is redundant.
316+
*/
317+
private static boolean isRedundantStore(@Nonnull Int2ObjectMap<LocalAccessState> states,
318+
@Nonnull InsnList instructions,
319+
int insnIndex, int slotX, int slotY, int typeSort) {
320+
LocalAccessState stateX = states.get(key(slotX, typeSort));
321+
LocalAccessState stateY = states.get(key(slotY, typeSort));
322+
323+
// If no state exists for either slot, we cannot conclude redundancy.
324+
if (stateX == null || stateY == null)
325+
return false;
326+
327+
// Redundancy only applies if there is a single write to Y.
328+
NavigableSet<LocalAccess> writesToY = stateY.getWrites();
329+
if (writesToY.size() != 1)
330+
return false;
331+
332+
// Get the single write instruction to Y.
333+
LocalAccess writeToY = writesToY.first();
334+
AbstractInsnNode writeInsnY = writeToY.instruction;
335+
if (!(writeInsnY instanceof VarInsnNode writeVin))
336+
return false;
337+
338+
// Check if the instruction type matches the expected type.
339+
if (!isMatchingStore(typeSort, writeVin.getOpcode()))
340+
return false;
341+
342+
// Check if the prior instruction is a cast.
343+
// We want to keep patterns like 'Collection -> List' being stored into new variables.
344+
AbstractInsnNode previousInsn = writeInsnY.getPrevious();
345+
if (previousInsn == null || previousInsn.getOpcode() == CHECKCAST)
346+
return false;
347+
348+
// Contents of both locals must be equal.
349+
if (!(previousInsn instanceof VarInsnNode prevVin
350+
&& isVarLoad(prevVin.getOpcode())
351+
&& prevVin.var == slotX))
352+
return false;
353+
354+
// Verify there are no intervening writes to X or Y between the observed instructions.
355+
int writeIndexY = instructions.indexOf(writeInsnY);
356+
for (int i = instructions.indexOf(previousInsn) + 1; i < writeIndexY; i++) {
357+
AbstractInsnNode insn = instructions.get(i);
358+
359+
// Check if there are any writes to X or Y in the scope.
360+
// Any intermediate modification invalidates redundancy.
361+
if (insn instanceof VarInsnNode vin) {
362+
int var = vin.var;
363+
int opcode = vin.getOpcode();
364+
if ((var == slotX || var == slotY) && isVarStore(opcode))
365+
return false;
366+
}
367+
}
368+
369+
// Check that X is defined before Y.
370+
// A variable defined later cannot make an earlier variable redundant.
371+
NavigableSet<LocalAccess> writesToX = stateX.getWrites();
372+
if (writesToX.isEmpty())
373+
return false;
374+
LocalAccess firstWriteToX = writesToX.first(); // First write to X
375+
int writeIndexX = instructions.indexOf(firstWriteToX.instruction);
376+
if (writeIndexX >= writeIndexY)
377+
return false;
378+
379+
// The store to Y is redundant. Every use case of it can be replaced with X.
380+
return true;
381+
}
382+
383+
/**
384+
* Replaces usage of the redundant variable with the original variable.
385+
*
386+
* @param instructions
387+
* Method instructions.
388+
* @param slotX
389+
* The original variable index.
390+
* @param slotY
391+
* The target variable index that is redundant.
392+
* @param typeSort
393+
* The variable's type sort.
394+
*/
395+
private static void replaceRedundantVariableUsage(@Nonnull InsnList instructions, int slotX, int slotY, int typeSort) {
396+
AbstractInsnNode replacement = AsmInsnUtil.createVarLoad(slotX, typeSort);
397+
for (int i = 0; i < instructions.size(); i++) {
398+
AbstractInsnNode insn = instructions.get(i);
399+
if (insn instanceof VarInsnNode vin && vin.var == slotY) {
400+
int op = vin.getOpcode();
401+
if (isVarLoad(op))
402+
instructions.set(insn, replacement.clone(null));
403+
}
404+
}
405+
}
406+
407+
/**
408+
* @param typeSort
409+
* The variable type sort.
410+
* @param opcode
411+
* The opcode to check.
412+
*
413+
* @return {@code true} if the opcode matches the respective store operation for the type.
414+
*/
415+
private static boolean isMatchingStore(int typeSort, int opcode) {
416+
return switch (typeSort) {
417+
case Type.INT -> opcode == ISTORE;
418+
case Type.FLOAT -> opcode == FSTORE;
419+
case Type.LONG -> opcode == LSTORE;
420+
case Type.DOUBLE -> opcode == DSTORE;
421+
case Type.OBJECT -> opcode == ASTORE;
422+
default -> false;
423+
};
424+
}
425+
426+
/**
427+
* @param typeSort
428+
* The variable type sort.
429+
* @param opcode
430+
* The opcode to check.
431+
*
432+
* @return {@code true} if the opcode matches the respective load operation for the type.
433+
*/
434+
private static boolean isMatchingLoad(int typeSort, int opcode) {
435+
return switch (typeSort) {
436+
case Type.INT -> opcode == ILOAD;
437+
case Type.FLOAT -> opcode == FLOAD;
438+
case Type.LONG -> opcode == LLOAD;
439+
case Type.DOUBLE -> opcode == DLOAD;
440+
case Type.OBJECT -> opcode == ALOAD;
441+
default -> false;
442+
};
443+
}
444+
265445
@Nonnull
266446
@Override
267447
public Set<Class<? extends ClassTransformer>> recommendedSuccessors() {
@@ -288,6 +468,16 @@ private static int key(int slot, int typeSort) {
288468
return slot | (typeSort << 16);
289469
}
290470

471+
/**
472+
* @param key
473+
* Key of typed variable.
474+
*
475+
* @return Variable index stored in the key.
476+
*/
477+
private static int slotFromKey(int key) {
478+
return key & 0xFFFF;
479+
}
480+
291481
/**
292482
* State tracking for when variables are read from and written to, along with the common merged value.
293483
* <br>
@@ -298,8 +488,8 @@ private static int key(int slot, int typeSort) {
298488
*/
299489
private static class LocalAccessState {
300490
private final int index;
301-
private SortedSet<LocalAccess> reads;
302-
private SortedSet<LocalAccess> writes;
491+
private NavigableSet<LocalAccess> reads;
492+
private NavigableSet<LocalAccess> writes;
303493
private ReValue mergedValue;
304494
private boolean isArray;
305495

@@ -342,14 +532,14 @@ public void mergeState(@Nonnull ReValue value) throws IllegalValueException {
342532
}
343533

344534
@Nonnull
345-
public SortedSet<LocalAccess> getReads() {
535+
public NavigableSet<LocalAccess> getReads() {
346536
if (reads == null)
347-
return Collections.emptySortedSet();
537+
return Collections.emptyNavigableSet();
348538
return reads;
349539
}
350540

351541
@Nonnull
352-
public SortedSet<LocalAccess> getReadsUpTo(int offset) {
542+
public NavigableSet<LocalAccess> getReadsUpTo(int offset) {
353543
// TODO: This is a linear check, which can be defeated by basic flow obfuscation
354544
// - We need to rewalk the control flow and find all the paths that lead to this instruction/offset
355545
// then get any local accesses that are along that path.
@@ -361,9 +551,17 @@ public SortedSet<LocalAccess> getReadsUpTo(int offset) {
361551
}
362552

363553
@Nonnull
364-
public SortedSet<LocalAccess> getWrites() {
554+
public NavigableSet<LocalAccess> getWritesUpTo(int offset) {
555+
// TODO: Same linear check problem as above
556+
return getWrites().stream()
557+
.filter(l -> l.offset < offset)
558+
.collect(Collectors.toCollection(TreeSet::new));
559+
}
560+
561+
@Nonnull
562+
public NavigableSet<LocalAccess> getWrites() {
365563
if (writes == null)
366-
return Collections.emptySortedSet();
564+
return Collections.emptyNavigableSet();
367565
return writes;
368566
}
369567

recaf-core/src/main/java/software/coley/recaf/util/AsmInsnUtil.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,20 @@ public static boolean isVarLoad(int op) {
165165
*/
166166
@Nonnull
167167
public static VarInsnNode createVarLoad(int index, @Nonnull Type variableType) {
168-
return switch (variableType.getSort()) {
168+
return createVarLoad(index, variableType.getSort());
169+
}
170+
171+
/**
172+
* @param index
173+
* Variable index.
174+
* @param typeSort
175+
* Variable type sort.
176+
*
177+
* @return Load instruction for variable type at the given index.
178+
*/
179+
@Nonnull
180+
public static VarInsnNode createVarLoad(int index, int typeSort) {
181+
return switch (typeSort) {
169182
case Type.BOOLEAN, Type.CHAR, Type.BYTE, Type.SHORT, Type.INT -> new VarInsnNode(ILOAD, index);
170183
case Type.FLOAT -> new VarInsnNode(FLOAD, index);
171184
case Type.DOUBLE -> new VarInsnNode(DLOAD, index);

0 commit comments

Comments
 (0)