Skip to content

Commit 4900f89

Browse files
jpbempeldevflow.devflow-routing-intake
andauthored
Fix maxStack for computeFrames (#12054)
Fix maxStack for computeFrames increase method maxStack before running computeFrames analysis to avoid stack error related to previous instrumentation fromm probe on the same method. Change the classGeneratedFailed test using large tablelookup to generate incorrect class instead of incorrect frame which is now caught by computeFrames analysis. Fix off-by-1 the newVar id. fix oversizing max stack set a probable max stack of 16. detect if an analysis failed because of insufficient stack, and double the stack up to 2* the number of instructions. move computeFrames to ASMHelper and add test fix newVar Co-authored-by: devflow.devflow-routing-intake <devflow.devflow-routing-intake@kubernetes.us1.ddbuild.io>
1 parent a6cffaf commit 4900f89

8 files changed

Lines changed: 122 additions & 46 deletions

File tree

dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/ASMHelper.java

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
import java.util.Arrays;
2020
import java.util.Collections;
2121
import java.util.Comparator;
22+
import java.util.IdentityHashMap;
2223
import java.util.List;
24+
import java.util.Map;
2325
import java.util.StringJoiner;
2426
import java.util.stream.Collectors;
2527
import org.objectweb.asm.ClassWriter;
@@ -38,12 +40,21 @@
3840
import org.objectweb.asm.tree.MethodInsnNode;
3941
import org.objectweb.asm.tree.MethodNode;
4042
import org.objectweb.asm.tree.TypeInsnNode;
43+
import org.objectweb.asm.tree.analysis.Analyzer;
44+
import org.objectweb.asm.tree.analysis.AnalyzerException;
45+
import org.objectweb.asm.tree.analysis.BasicInterpreter;
46+
import org.objectweb.asm.tree.analysis.BasicValue;
47+
import org.objectweb.asm.tree.analysis.Frame;
4148
import org.objectweb.asm.util.Printer;
4249
import org.objectweb.asm.util.Textifier;
4350
import org.objectweb.asm.util.TraceClassVisitor;
51+
import org.slf4j.Logger;
52+
import org.slf4j.LoggerFactory;
4453

4554
/** Helper class for bytecode generation */
4655
public class ASMHelper {
56+
private static final Logger LOGGER = LoggerFactory.getLogger(ASMHelper.class);
57+
4758
public static final Type INT_TYPE = new Type(org.objectweb.asm.Type.INT_TYPE);
4859
public static final Type OBJECT_TYPE = new Type(Types.OBJECT_TYPE);
4960
public static final Type STRING_TYPE = new Type(Types.STRING_TYPE);
@@ -428,6 +439,65 @@ public static List<Integer> getLineNumbers(MethodNode methodNode) {
428439
return lines;
429440
}
430441

442+
/**
443+
* Returns a map for all instructions what is the current frame with value on the stacks Helps to
444+
* know at exit points of a method what remains on the stack
445+
*/
446+
protected static Map<AbstractInsnNode, Frame<BasicValue>> computeFrames(
447+
String owner, MethodNode methodNode) {
448+
// Initial guess used for methodNode.maxStack when retrying computeFrames() below; small
449+
// enough to be cheap, large enough to cover the vast majority of probe-generated methods on
450+
// the first attempt.
451+
final int INITIAL_MAX_STACK_GUESS = 16;
452+
// Probes already applied to this method may have inserted bytecode that requires more
453+
// stack than originally declared, without updating methodNode.maxStack (only maxLocals is
454+
// tracked, see newVar()). The ASM analyzer sizes its Frame objects from the declared
455+
// maxStack (each Frame costs maxLocals+maxStack slots), so a stale/too-small value makes
456+
// the analysis fail with "Insufficient maximum stack size" even though the class itself is
457+
// valid. Rather than guessing an upper bound from the instruction count (which blows up
458+
// Frame memory quadratically for large methods), retry with a growing maxStack only when
459+
// the Analyzer itself reports it as insufficient - this converges to close to the actual
460+
// stack depth the method needs. The real maxStack is recomputed again from scratch by the
461+
// ClassWriter's COMPUTE_FRAMES pass when the class is finally written.
462+
Map<AbstractInsnNode, Frame<BasicValue>> frames = new IdentityHashMap<>();
463+
int attemptMaxStack = Math.max(methodNode.maxStack, INITIAL_MAX_STACK_GUESS);
464+
int hardLimitMaxStack = methodNode.instructions.size() * 2;
465+
while (true) {
466+
methodNode.maxStack = attemptMaxStack;
467+
try {
468+
Frame<BasicValue>[] frameArray =
469+
new Analyzer<>(new BasicInterpreter()).analyze(owner, methodNode);
470+
AbstractInsnNode current = methodNode.instructions.getFirst();
471+
int idx = 0;
472+
while (current != null) {
473+
frames.put(current, frameArray[idx++]);
474+
current = current.getNext();
475+
}
476+
break;
477+
} catch (AnalyzerException ex) {
478+
if (isInsufficientMaxStack(ex) && attemptMaxStack < hardLimitMaxStack) {
479+
attemptMaxStack *= 2;
480+
continue;
481+
}
482+
LOGGER.debug(
483+
"Failed to analyze method[{}::{}{}] instructions",
484+
owner,
485+
methodNode.name,
486+
methodNode.desc,
487+
ex);
488+
// when running tests, fails if an exception is thrown during analysis
489+
// Gradle is running tests with assertions enbabled
490+
assert false;
491+
break;
492+
}
493+
}
494+
return frames;
495+
}
496+
497+
private static boolean isInsufficientMaxStack(AnalyzerException ex) {
498+
return ex.getMessage() != null && ex.getMessage().contains("Insufficient maximum stack size");
499+
}
500+
431501
/** Wraps ASM's {@link org.objectweb.asm.Type} with associated generic types */
432502
public static class Type {
433503
private final org.objectweb.asm.Type mainType;

dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/CapturedContextInstrumenter.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.datadog.debugger.instrumentation;
22

3+
import static com.datadog.debugger.instrumentation.ASMHelper.computeFrames;
34
import static com.datadog.debugger.instrumentation.ASMHelper.getStatic;
45
import static com.datadog.debugger.instrumentation.ASMHelper.invokeConstructor;
56
import static com.datadog.debugger.instrumentation.ASMHelper.invokeStatic;

dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/ExceptionInstrumenter.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ public InstrumentationResult.Status instrument() {
2525
// a try/catch that create a subscobe and even level method local vars are not accessible
2626
// in the catch clause for capture
2727
hoistedLocalVars = initAndHoistLocalVars(methodNode);
28-
Map<AbstractInsnNode, Frame<BasicValue>> frames = computeFrames(classNode.name, methodNode);
28+
Map<AbstractInsnNode, Frame<BasicValue>> frames =
29+
ASMHelper.computeFrames(classNode.name, methodNode);
2930
processInstructions(frames); // fill returnHandlerLabel
3031
addFinallyHandler(methodEnterLabel, returnHandlerLabel);
3132
installFinallyBlocks();

dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/Instrumenter.java

Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
import java.util.ArrayList;
1414
import java.util.Arrays;
1515
import java.util.HashMap;
16-
import java.util.IdentityHashMap;
1716
import java.util.List;
1817
import java.util.Map;
1918
import org.objectweb.asm.Opcodes;
@@ -29,17 +28,11 @@
2928
import org.objectweb.asm.tree.MethodNode;
3029
import org.objectweb.asm.tree.TryCatchBlockNode;
3130
import org.objectweb.asm.tree.TypeInsnNode;
32-
import org.objectweb.asm.tree.analysis.Analyzer;
33-
import org.objectweb.asm.tree.analysis.AnalyzerException;
34-
import org.objectweb.asm.tree.analysis.BasicInterpreter;
3531
import org.objectweb.asm.tree.analysis.BasicValue;
3632
import org.objectweb.asm.tree.analysis.Frame;
37-
import org.slf4j.Logger;
38-
import org.slf4j.LoggerFactory;
3933

4034
/** Common class for generating instrumentation */
4135
public abstract class Instrumenter {
42-
private static final Logger LOGGER = LoggerFactory.getLogger(Instrumenter.class);
4336
protected static final String CONSTRUCTOR_NAME = "<init>";
4437
protected static final String PROBEID_TAG_NAME = "debugger.probeid";
4538

@@ -193,29 +186,6 @@ protected InsnList stackCleanupInsnList(
193186
return result;
194187
}
195188

196-
protected static Map<AbstractInsnNode, Frame<BasicValue>> computeFrames(
197-
String owner, MethodNode methodNode) {
198-
Map<AbstractInsnNode, Frame<BasicValue>> frames = new IdentityHashMap<>();
199-
try {
200-
Frame<BasicValue>[] frameArray =
201-
new Analyzer<>(new BasicInterpreter()).analyze(owner, methodNode);
202-
AbstractInsnNode current = methodNode.instructions.getFirst();
203-
int idx = 0;
204-
while (current != null) {
205-
frames.put(current, frameArray[idx++]);
206-
current = current.getNext();
207-
}
208-
} catch (AnalyzerException ex) {
209-
LOGGER.debug(
210-
"Failed to analyze method[{}::{}{}] instructions",
211-
owner,
212-
methodNode.name,
213-
methodNode.desc,
214-
ex);
215-
}
216-
return frames;
217-
}
218-
219189
protected AbstractInsnNode processInstruction(
220190
AbstractInsnNode node, Map<AbstractInsnNode, Frame<BasicValue>> frames) {
221191
switch (node.getOpcode()) {
@@ -285,13 +255,11 @@ protected void pushTags(InsnList insnList, ProbeDefinition.Tag[] tags) {
285255
}
286256

287257
protected int newVar(Type type) {
288-
int varId = methodNode.maxLocals + 1;
289-
methodNode.maxLocals += type.getSize();
290-
return varId;
258+
return newVar(type.getSize());
291259
}
292260

293261
protected int newVar(int size) {
294-
int varId = methodNode.maxLocals + 1;
262+
int varId = methodNode.maxLocals;
295263
methodNode.maxLocals += size;
296264
return varId;
297265
}

dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/MetricInstrumenter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ public InstrumentationResult.Status instrument() {
112112
case EXIT:
113113
{
114114
Map<AbstractInsnNode, Frame<BasicValue>> frames =
115-
computeFrames(classNode.name, methodNode);
115+
ASMHelper.computeFrames(classNode.name, methodNode);
116116
processInstructions(frames);
117117
addFinallyHandler(returnHandlerLabel);
118118
installFinallyBlocks();

dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/SpanInstrumenter.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ public InstrumentationResult.Status instrument() {
4242
return addRangeSpan(classFileLines);
4343
}
4444
spanVar = newVar(DEBUGGER_SPAN_TYPE);
45-
Map<AbstractInsnNode, Frame<BasicValue>> frames = computeFrames(classNode.name, methodNode);
45+
Map<AbstractInsnNode, Frame<BasicValue>> frames =
46+
ASMHelper.computeFrames(classNode.name, methodNode);
4647
processInstructions(frames);
4748
LabelNode initSpanLabel = new LabelNode();
4849
InsnList insnList = createSpan(initSpanLabel);

dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/DebuggerTransformerTest.java

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,10 @@
5555
import org.junit.jupiter.api.condition.DisabledIf;
5656
import org.mockito.ArgumentCaptor;
5757
import org.objectweb.asm.Opcodes;
58-
import org.objectweb.asm.tree.VarInsnNode;
58+
import org.objectweb.asm.tree.InsnList;
59+
import org.objectweb.asm.tree.InsnNode;
60+
import org.objectweb.asm.tree.LabelNode;
61+
import org.objectweb.asm.tree.TableSwitchInsnNode;
5962

6063
public class DebuggerTransformerTest {
6164
private static final String LANGUAGE = "java";
@@ -350,23 +353,23 @@ public void classGenerationFailed() {
350353
.startsWith(
351354
"Instrumentation failed for "
352355
+ CLASS_NAME
353-
+ ": java.lang.ArrayIndexOutOfBoundsException:"));
356+
+ ": org.objectweb.asm.MethodTooLargeException:"));
354357
assertTrue(
355358
strCaptor
356359
.getAllValues()
357360
.get(1)
358361
.startsWith(
359362
"Instrumentation failed for "
360363
+ CLASS_NAME
361-
+ ": java.lang.ArrayIndexOutOfBoundsException:"));
364+
+ ": org.objectweb.asm.MethodTooLargeException:"));
362365
assertTrue(
363366
strCaptor
364367
.getAllValues()
365368
.get(2)
366369
.startsWith(
367370
"Instrumentation failed for "
368371
+ CLASS_NAME
369-
+ ": java.lang.ArrayIndexOutOfBoundsException:"));
372+
+ ": org.objectweb.asm.MethodTooLargeException:"));
370373
}
371374

372375
@Test
@@ -443,11 +446,19 @@ public MockProbe(ProbeId probeId, Where where) {
443446
@Override
444447
public InstrumentationResult.Status instrument(
445448
MethodInfo methodInfo, List<DiagnosticMessage> diagnostics, List<Integer> probeIndices) {
446-
methodInfo
447-
.getMethodNode()
448-
.instructions
449-
.insert(
450-
new VarInsnNode(Opcodes.ASTORE, methodInfo.getMethodNode().localVariables.size()));
449+
// Build a single TABLESWITCH with enough case entries (4 bytes each) to push the
450+
// method's bytecode past the 65535-byte limit, while keeping the ASM tree
451+
// instruction *count* tiny so that dataflow analysis (computeFrames) stays cheap
452+
// and succeeds; the failure should only manifest when ASM serializes the method.
453+
LabelNode target = new LabelNode();
454+
int high = 20_000;
455+
LabelNode[] labels = new LabelNode[high + 1];
456+
Arrays.fill(labels, target);
457+
InsnList list = new InsnList();
458+
list.add(new InsnNode(Opcodes.ICONST_0));
459+
list.add(new TableSwitchInsnNode(0, high, target, labels));
460+
list.add(target);
461+
methodInfo.getMethodNode().instructions.insert(list);
451462
return InstrumentationResult.Status.INSTALLED;
452463
}
453464

dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/instrumentation/ASMHelperTest.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,18 @@
1111

1212
import java.util.Collections;
1313
import java.util.List;
14+
import java.util.Map;
1415
import org.junit.jupiter.api.Test;
16+
import org.objectweb.asm.Opcodes;
1517
import org.objectweb.asm.Type;
18+
import org.objectweb.asm.tree.AbstractInsnNode;
19+
import org.objectweb.asm.tree.InsnNode;
1620
import org.objectweb.asm.tree.LabelNode;
1721
import org.objectweb.asm.tree.LineNumberNode;
1822
import org.objectweb.asm.tree.LocalVariableNode;
1923
import org.objectweb.asm.tree.MethodNode;
24+
import org.objectweb.asm.tree.analysis.BasicValue;
25+
import org.objectweb.asm.tree.analysis.Frame;
2026

2127
public class ASMHelperTest {
2228

@@ -187,4 +193,22 @@ void getLineNumbers() {
187193
assertEquals(2, lineNumbers.get(1).intValue());
188194
assertEquals(82, lineNumbers.get(2).intValue());
189195
}
196+
197+
@Test
198+
void computeFramesGrowsMaxStackWhenInsufficient() {
199+
MethodNode methodNode = new MethodNode(Opcodes.ACC_STATIC, "deepLongStack", "()V", null, null);
200+
methodNode.maxStack = 1; // deliberately too small for the actual stack depth needed below
201+
int longCount = 20; // stack depth of 20, well above the initial guess of 16
202+
for (int i = 0; i < longCount; i++) {
203+
methodNode.instructions.add(new InsnNode(Opcodes.LCONST_0));
204+
}
205+
for (int i = 0; i < longCount; i++) {
206+
methodNode.instructions.add(new InsnNode(Opcodes.POP2));
207+
}
208+
methodNode.instructions.add(new InsnNode(Opcodes.RETURN));
209+
Map<AbstractInsnNode, Frame<BasicValue>> frames =
210+
ASMHelper.computeFrames("com/datadog/debugger/FakeOwner", methodNode);
211+
assertEquals(methodNode.instructions.size(), frames.size());
212+
assertTrue(methodNode.maxStack >= longCount);
213+
}
190214
}

0 commit comments

Comments
 (0)