Skip to content

Commit 1576330

Browse files
committed
Add fuzz tests to endpoints VM
1 parent cb4f0e8 commit 1576330

10 files changed

Lines changed: 266 additions & 5 deletions

File tree

.github/workflows/fuzz-testing.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ jobs:
5353
run: |
5454
./gradlew fuzz -Pfuzz.maxDuration=4h --stacktrace
5555
56+
- name: Run rules engine bytecode fuzz tests
57+
env:
58+
JAZZER_FUZZ: "1"
59+
run: |
60+
./gradlew :client:client-rulesengine:test --tests "*FuzzTest*" -Djazzer.max_duration=1h --stacktrace
61+
5662
- name: Save fuzz corpus cache
5763
uses: actions/cache/save@v5
5864
if: always()

client/client-rulesengine/build.gradle.kts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,12 @@ dependencies {
1616
testImplementation(project(":aws:client:aws-client-awsjson"))
1717
testImplementation(project(":client:dynamic-client"))
1818
testImplementation(project(":aws:client:aws-client-rulesengine"))
19+
20+
// Jazzer for fuzz testing
21+
testImplementation(libs.jazzer.junit)
22+
testImplementation(libs.jazzer.api)
23+
}
24+
25+
tasks.test {
26+
maxHeapSize = "2g"
1927
}

client/client-rulesengine/src/main/java/software/amazon/smithy/java/client/rulesengine/BytecodeDisassembler.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,14 @@ String disassemble() {
207207
}
208208

209209
private void disassembleSection(StringBuilder s, int startOffset, String indent) {
210+
try {
211+
disassembleSectionUnsafe(s, startOffset, indent);
212+
} catch (IndexOutOfBoundsException | IllegalArgumentException | IllegalStateException e) {
213+
s.append(indent).append("(error disassembling section: ").append(e.getMessage()).append(")\n");
214+
}
215+
}
216+
217+
private void disassembleSectionUnsafe(StringBuilder s, int startOffset, String indent) {
210218
BytecodeWalker walker = new BytecodeWalker(bytecode.getBytecode(), startOffset);
211219

212220
if (!walker.hasNext()) {

client/client-rulesengine/src/main/java/software/amazon/smithy/java/client/rulesengine/BytecodeEvaluator.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,25 @@ private Object[] resizeTempArray(int requiredSize) {
102102
return tempArray;
103103
}
104104

105-
@SuppressWarnings("unchecked")
106105
private Object run(int start) {
107106
pc = start;
108107

109108
var instructions = bytecode.getBytecode();
110109
var functions = bytecode.getFunctions();
111110
var constantPool = bytecode.getConstantPool();
112111

112+
try {
113+
return runLoop(instructions, functions, constantPool);
114+
} catch (ArrayIndexOutOfBoundsException | ClassCastException | NullPointerException e) {
115+
throw new RulesEvaluationError(
116+
"Bytecode execution error at pc=" + pc + ": " + e.getMessage(),
117+
pc);
118+
}
119+
}
120+
121+
@SuppressWarnings("unchecked")
122+
private Object runLoop(byte[] instructions, RulesFunction[] functions, Object[] constantPool) {
123+
113124
while (pc < instructions.length) {
114125
int opcode = instructions[pc++] & 0xFF;
115126
switch (opcode) {

client/client-rulesengine/src/main/java/software/amazon/smithy/java/client/rulesengine/BytecodeWalker.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,18 +55,31 @@ public boolean advance() {
5555
if (length < 0) {
5656
return false; // Unknown opcode
5757
}
58-
pc += length;
58+
int next = pc + length;
59+
if (next > code.limit()) {
60+
return false; // Instruction extends past end
61+
}
62+
pc = next;
5963
return true;
6064
}
6165

6266
public int getInstructionLength() {
6367
if (!hasNext()) {
6468
return -1;
6569
}
66-
return getInstructionLength(code.get(pc));
70+
int length = getInstructionLength(code.get(pc));
71+
// Validate the full instruction fits within the buffer
72+
if (length > 0 && pc + length > code.limit()) {
73+
return -1;
74+
}
75+
return length;
6776
}
6877

6978
public int getOperandCount() {
79+
// If the instruction doesn't fully fit in the buffer, report 0 operands
80+
if (getInstructionLength() < 0) {
81+
return 0;
82+
}
7083
byte opcode = currentOpcode();
7184
return switch (opcode) {
7285
case Opcodes.NOT, Opcodes.ISSET, Opcodes.LIST0, Opcodes.LIST1, Opcodes.LIST2, Opcodes.MAP0, Opcodes.MAP1,

client/client-rulesengine/src/main/java/software/amazon/smithy/java/client/rulesengine/RulesEngineBuilder.java

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,16 @@ public Bytecode load(byte[] data) {
184184
throw new IllegalArgumentException("Invalid counts in bytecode header");
185185
}
186186

187+
// Validate counts don't exceed what the data can possibly hold.
188+
// Each condition/result offset is 4 bytes, each BDD node is 12 bytes.
189+
long minRequiredSize = BYTECODE_HEADER_SIZE
190+
+ (long) conditionCount * 4
191+
+ (long) resultCount * 4
192+
+ (long) bddNodeCount * 12;
193+
if (minRequiredSize > data.length) {
194+
throw new IllegalArgumentException("Bytecode header counts exceed data length");
195+
}
196+
187197
// Read offset tables
188198
int conditionTableOffset = reader.readInt();
189199
int resultTableOffset = reader.readInt();
@@ -205,18 +215,35 @@ public Bytecode load(byte[] data) {
205215
throw new IllegalArgumentException("Invalid offsets in bytecode header");
206216
}
207217

218+
// Validate that declared counts fit within their respective sections
219+
if (conditionTableOffset + (long) conditionCount * 4 > resultTableOffset) {
220+
throw new IllegalArgumentException("Condition table overflows into result table");
221+
} else if (resultTableOffset + (long) resultCount * 4 > functionTableOffset) {
222+
throw new IllegalArgumentException("Result table overflows into function table");
223+
} else if (bddTableOffset + (long) bddNodeCount * 12 > constantPoolOffset) {
224+
throw new IllegalArgumentException("BDD table overflows into constant pool");
225+
}
226+
208227
// Load condition offsets
209228
reader.offset = conditionTableOffset;
210229
int[] conditionOffsets = new int[conditionCount];
211230
for (int i = 0; i < conditionCount; i++) {
212-
conditionOffsets[i] = reader.readInt();
231+
int offset = reader.readInt();
232+
if (offset < 0 || offset >= data.length) {
233+
throw new IllegalArgumentException("Condition offset out of bounds: " + offset);
234+
}
235+
conditionOffsets[i] = offset;
213236
}
214237

215238
// Load result offsets
216239
reader.offset = resultTableOffset;
217240
int[] resultOffsets = new int[resultCount];
218241
for (int i = 0; i < resultCount; i++) {
219-
resultOffsets[i] = reader.readInt();
242+
int offset = reader.readInt();
243+
if (offset < 0 || offset >= data.length) {
244+
throw new IllegalArgumentException("Result offset out of bounds: " + offset);
245+
}
246+
resultOffsets[i] = offset;
220247
}
221248

222249
// Load function names and resolve them using this builder's functions
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.java.client.rulesengine;
7+
8+
import com.code_intelligence.jazzer.junit.FuzzTest;
9+
import java.util.List;
10+
import java.util.Map;
11+
12+
/**
13+
* Fuzz tests for {@link BytecodeEvaluator} — evaluating bytecode with random instruction streams.
14+
*
15+
* <p>Constructs minimal valid Bytecode objects with fuzz-derived instruction bytes,
16+
* then runs the evaluator to check for crash safety.
17+
*/
18+
class BytecodeEvaluatorFuzzTest {
19+
20+
private static final int MAX_FUZZ_INPUT = 512;
21+
22+
@FuzzTest
23+
void fuzzEvaluateCondition(byte[] data) {
24+
if (data.length < 1 || data.length > MAX_FUZZ_INPUT) {
25+
return;
26+
}
27+
28+
// Build a Bytecode with the fuzz data as the instruction stream and a single condition at offset 0.
29+
Bytecode bytecode = new Bytecode(
30+
data,
31+
new int[] {0}, // one condition at offset 0
32+
new int[0],
33+
new RegisterDefinition[] {
34+
new RegisterDefinition("p0", false, "default", null, false),
35+
new RegisterDefinition("t0", false, null, null, true)
36+
},
37+
new Object[] {"a", "b", 1, true, false, null, List.of(), Map.of()},
38+
new RulesFunction[0],
39+
new int[] {0, 1, -1}, // BDD: condition 0 -> TRUE/FALSE
40+
2 // root = node 0
41+
);
42+
43+
BytecodeEvaluator evaluator = new BytecodeEvaluator(bytecode,
44+
new RulesExtension[0],
45+
RegisterFiller.of(bytecode, Map.of()));
46+
evaluator.reset(null, Map.of());
47+
48+
try {
49+
evaluator.test(0);
50+
} catch (RulesEvaluationError | IllegalArgumentException ignored) {}
51+
}
52+
53+
@FuzzTest
54+
void fuzzEvaluateResult(byte[] data) {
55+
if (data.length < 1 || data.length > MAX_FUZZ_INPUT) {
56+
return;
57+
}
58+
59+
Bytecode bytecode = new Bytecode(
60+
data,
61+
new int[0],
62+
new int[] {0}, // one result at offset 0
63+
new RegisterDefinition[] {
64+
new RegisterDefinition("p0", false, "https://example.com", null, false),
65+
new RegisterDefinition("t0", false, null, null, true)
66+
},
67+
new Object[] {"https://example.com", "key", "value", 42, true, false},
68+
new RulesFunction[0],
69+
new int[] {-1, 100_000_000, -1}, // Terminal -> result 0
70+
100_000_000);
71+
72+
BytecodeEvaluator evaluator = new BytecodeEvaluator(bytecode,
73+
new RulesExtension[0],
74+
RegisterFiller.of(bytecode, Map.of()));
75+
evaluator.reset(null, Map.of());
76+
77+
try {
78+
evaluator.resolveResult(0);
79+
} catch (RulesEvaluationError | IllegalArgumentException ignored) {}
80+
}
81+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.java.client.rulesengine;
7+
8+
import com.code_intelligence.jazzer.junit.FuzzTest;
9+
10+
/**
11+
* Fuzz tests for loading bytecode from arbitrary byte arrays via {@link RulesEngineBuilder#load(byte[])}.
12+
*
13+
* <p>This exercises header parsing, offset validation, constant pool deserialization,
14+
* register definition loading, BDD node loading, and function resolution.
15+
*/
16+
class BytecodeLoadFuzzTest {
17+
18+
private static final int MAX_FUZZ_INPUT = 2048;
19+
20+
@FuzzTest
21+
void fuzzLoad(byte[] data) {
22+
if (data.length > MAX_FUZZ_INPUT) {
23+
return;
24+
}
25+
try {
26+
new RulesEngineBuilder().load(data);
27+
} catch (IllegalArgumentException | RulesEvaluationError ignored) {}
28+
}
29+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.java.client.rulesengine;
7+
8+
import com.code_intelligence.jazzer.junit.FuzzTest;
9+
10+
/**
11+
* Fuzz tests for {@link BytecodeReader} — constant deserialization and register definition parsing.
12+
*/
13+
class BytecodeReaderFuzzTest {
14+
15+
private static final int MAX_FUZZ_INPUT = 1024;
16+
17+
@FuzzTest
18+
void fuzzReadConstant(byte[] data) {
19+
if (data.length > MAX_FUZZ_INPUT) {
20+
return;
21+
}
22+
try {
23+
new BytecodeReader(data, 0).readConstant();
24+
} catch (IllegalArgumentException ignored) {}
25+
}
26+
27+
@FuzzTest
28+
void fuzzReadRegisterDefinitions(byte[] data) {
29+
if (data.length < 1 || data.length > MAX_FUZZ_INPUT) {
30+
return;
31+
}
32+
// Use first byte to determine count (1-16)
33+
int count = (data[0] & 0x0F) + 1;
34+
try {
35+
new BytecodeReader(data, 1).readRegisterDefinitions(count);
36+
} catch (IllegalArgumentException ignored) {}
37+
}
38+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.java.client.rulesengine;
7+
8+
import com.code_intelligence.jazzer.junit.FuzzTest;
9+
10+
/**
11+
* Fuzz tests for {@link BytecodeWalker} — walking arbitrary instruction streams.
12+
*/
13+
class BytecodeWalkerFuzzTest {
14+
15+
private static final int MAX_FUZZ_INPUT = 1024;
16+
17+
@FuzzTest
18+
void fuzzWalker(byte[] data) {
19+
if (data.length > MAX_FUZZ_INPUT) {
20+
return;
21+
}
22+
try {
23+
BytecodeWalker walker = new BytecodeWalker(data);
24+
int steps = 0;
25+
while (walker.hasNext() && steps++ < 500) {
26+
walker.currentOpcode();
27+
int count = walker.getOperandCount();
28+
for (int i = 0; i < count; i++) {
29+
walker.getOperand(i);
30+
}
31+
if (walker.isReturnOpcode()) {
32+
break;
33+
}
34+
if (!walker.advance()) {
35+
break;
36+
}
37+
}
38+
} catch (IllegalArgumentException | IllegalStateException ignored) {}
39+
}
40+
}

0 commit comments

Comments
 (0)