This is an automated email from the ASF dual-hosted git repository.
francischuang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite-avatica.git
The following commit(s) were added to refs/heads/main by this push:
new 8f2b77551 [CALCITE-7436] Add high-coverage Jazzer fuzzing for Avatica
core modules
8f2b77551 is described below
commit 8f2b775513ae82cec7cd2bf24b5ed88b57b20dfe
Author: Vishal S <[email protected]>
AuthorDate: Sat Mar 7 21:06:27 2026 +0530
[CALCITE-7436] Add high-coverage Jazzer fuzzing for Avatica core modules
---
.github/workflows/cifuzz.yml | 70 +++++++
core/build.gradle.kts | 6 +
.../calcite/avatica/fuzz/AvaticaSiteFuzzer.java | 217 +++++++++++++++++++++
.../apache/calcite/avatica/fuzz/Base64Fuzzer.java | 48 +++++
.../avatica/fuzz/ConnectStringParserFuzzer.java | 44 +++++
.../calcite/avatica/fuzz/JsonHandlerFuzzer.java | 133 +++++++++++++
.../avatica/fuzz/ProtobufHandlerFuzzer.java | 129 ++++++++++++
.../calcite/avatica/fuzz/TypedValueFuzzer.java | 117 +++++++++++
8 files changed, 764 insertions(+)
diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml
new file mode 100644
index 000000000..0efdcb04c
--- /dev/null
+++ b/.github/workflows/cifuzz.yml
@@ -0,0 +1,70 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to you under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+name: CIFuzz
+on:
+ pull_request:
+ branches:
+ - main
+ - master
+
+permissions:
+ contents: read
+ security-events: write
+
+jobs:
+ Fuzzing:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Build Fuzzers
+ id: build
+ uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master
+ with:
+ oss-fuzz-project-name: "calcite-avatica"
+ language: jvm
+
+ - name: Run Fuzzers
+ uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master
+ with:
+ oss-fuzz-project-name: "calcite-avatica"
+ language: jvm
+ fuzz-seconds: 600
+ output-sarif: true
+ fail: false
+
+ - name: Upload SARIF results
+ uses: github/codeql-action/upload-sarif@v3
+ if: always() && steps.build.outcome == 'success'
+ with:
+ sarif_file: ./out/results.sarif.json
+ category: fuzzing
+
+ - name: Upload Crash Artifacts
+ uses: actions/upload-artifact@v4
+ if: steps.run.outcome == 'failure' && steps.build.outcome == 'success'
+ with:
+ name: fuzzing-crash-artifacts
+ path: ./out/artifacts
+ retention-days: 30
+
+ - name: Report Fuzzing Status
+ if: steps.run.outcome == 'failure'
+ run: |
+ echo "::notice::Fuzzing found potential bugs! Check the artifacts
for details."
+ echo "To reproduce locally: Download artifacts and run with Jazzer"
+ echo ""
+ echo "IMPORTANT: This is an informational check. The PR can still be
merged."
+ echo "For bug tracking, see:
https://issues.apache.org/jira/browse/CALCITE"
diff --git a/core/build.gradle.kts b/core/build.gradle.kts
index ffa8ecc6d..6f0adda7e 100644
--- a/core/build.gradle.kts
+++ b/core/build.gradle.kts
@@ -40,6 +40,7 @@ dependencies {
implementation("org.apache.httpcomponents.client5:httpclient5")
implementation("org.apache.httpcomponents.core5:httpcore5")
implementation("org.slf4j:slf4j-api")
+ testImplementation("com.code-intelligence:jazzer-api:0.22.1")
testImplementation("junit:junit")
testImplementation("org.mockito:mockito-core")
testImplementation("org.mockito:mockito-inline")
@@ -117,3 +118,8 @@ tasks.autostyleJavaCheck {
dependsOn(filterJava)
dependsOn(tasks.getByName("generateProto"))
}
+
+tasks.register<Copy>("copyFuzzDependencies") {
+ from(configurations.testRuntimeClasspath)
+ into(layout.buildDirectory.dir("fuzz-dependencies"))
+}
diff --git
a/core/src/test/java/org/apache/calcite/avatica/fuzz/AvaticaSiteFuzzer.java
b/core/src/test/java/org/apache/calcite/avatica/fuzz/AvaticaSiteFuzzer.java
new file mode 100644
index 000000000..81b001692
--- /dev/null
+++ b/core/src/test/java/org/apache/calcite/avatica/fuzz/AvaticaSiteFuzzer.java
@@ -0,0 +1,217 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.calcite.avatica.fuzz;
+
+import org.apache.calcite.avatica.AvaticaParameter;
+import org.apache.calcite.avatica.AvaticaSite;
+import org.apache.calcite.avatica.remote.TypedValue;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+
+import java.lang.reflect.Proxy;
+import java.math.BigDecimal;
+import java.sql.Date;
+import java.sql.SQLException;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.util.Calendar;
+import java.util.Locale;
+import java.util.TimeZone;
+
+/**
+ * Fuzzer for AvaticaSite.
+ */
+public class AvaticaSiteFuzzer {
+
+ private AvaticaSiteFuzzer() {
+ }
+
+ /**
+ * Fuzzes AvaticaSite methods.
+ *
+ * @param data fuzzed data
+ */
+ public static void fuzzerTestOneInput(FuzzedDataProvider data) {
+ try {
+ // Construct dependencies for an AvaticaSite
+ AvaticaParameter param = new AvaticaParameter(
+ data.consumeBoolean(),
+ data.consumeInt(),
+ data.consumeInt(), // scale
+ data.consumeInt(),
+ data.consumeString(10), // typeName
+ data.consumeString(10), // className
+ data.consumeString(10) // name
+ );
+
+ Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"),
Locale.ROOT);
+ TypedValue[] slots = new TypedValue[1];
+
+ // Target object
+ AvaticaSite site = new AvaticaSite(param, calendar, 0, slots);
+
+ // Determine what to fuzz
+ int choice = data.consumeInt(1, 16);
+
+ switch (choice) {
+ case 1:
+ site.setByte(data.consumeByte());
+ break;
+ case 2:
+ site.setChar(data.consumeChar());
+ break;
+ case 3:
+ site.setShort(data.consumeShort());
+ break;
+ case 4:
+ site.setInt(data.consumeInt());
+ break;
+ case 5:
+ site.setLong(data.consumeLong());
+ break;
+ case 6:
+ site.setBoolean(data.consumeBoolean());
+ break;
+ case 7:
+ site.setNString(data.consumeString(50));
+ break;
+ case 8:
+ site.setFloat(data.consumeFloat());
+ break;
+ case 9:
+ site.setDouble(data.consumeDouble());
+ break;
+ case 10:
+ site.setBigDecimal(new BigDecimal(data.consumeDouble()));
+ break;
+ case 11:
+ site.setString(data.consumeString(50));
+ break;
+ case 12:
+ site.setBytes(data.consumeBytes(50));
+ break;
+ case 13:
+ site.setTimestamp(new Timestamp(data.consumeLong()), calendar);
+ break;
+ case 14:
+ site.setTime(new Time(data.consumeLong()), calendar);
+ break;
+ case 15:
+ // Raw object mapping
+ Object obj = null;
+ int objType = data.consumeInt(1, 4);
+ if (objType == 1) {
+ obj = data.consumeBoolean();
+ } else if (objType == 2) {
+ obj = data.consumeString(50);
+ } else if (objType == 3) {
+ obj = data.consumeLong();
+ } else if (objType == 4) {
+ obj = data.consumeBytes(50);
+ }
+
+ site.setObject(obj, data.consumeInt(-10, 100)); // Types constants
fall in this range
+ break;
+ case 16:
+ // Test the JDBC ResultSet getter mapping using a dynamic proxy
+ org.apache.calcite.avatica.util.Cursor.Accessor accessor =
+ (org.apache.calcite.avatica.util.Cursor.Accessor)
Proxy.newProxyInstance(
+
org.apache.calcite.avatica.util.Cursor.Accessor.class.getClassLoader(),
+ new Class<?>[]
{org.apache.calcite.avatica.util.Cursor.Accessor.class},
+ (proxy, method, args) -> {
+ String name = method.getName();
+ if (name.equals("wasNull")) {
+ return data.consumeBoolean();
+ }
+ if (name.equals("getString") || name.equals("getNString")) {
+ return data.consumeString(50);
+ }
+ if (name.equals("getBoolean")) {
+ return data.consumeBoolean();
+ }
+ if (name.equals("getByte")) {
+ return data.consumeByte();
+ }
+ if (name.equals("getShort")) {
+ return data.consumeShort();
+ }
+ if (name.equals("getInt")) {
+ return data.consumeInt();
+ }
+ if (name.equals("getLong")) {
+ return data.consumeLong();
+ }
+ if (name.equals("getFloat")) {
+ return data.consumeFloat();
+ }
+ if (name.equals("getDouble")) {
+ return data.consumeDouble();
+ }
+ if (name.equals("getBigDecimal")) {
+ return new BigDecimal(data.consumeDouble());
+ }
+ if (name.equals("getBytes")) {
+ return data.consumeBytes(50);
+ }
+ if (name.equals("getDate")) {
+ return new Date(data.consumeLong());
+ }
+ if (name.equals("getTime")) {
+ return new Time(data.consumeLong());
+ }
+ if (name.equals("getTimestamp")) {
+ return new Timestamp(data.consumeLong());
+ }
+
+ if (name.equals("getUByte")) {
+ return org.joou.UByte.valueOf(data.consumeInt(0, 255));
+ }
+ if (name.equals("getUShort")) {
+ return org.joou.UShort.valueOf(data.consumeInt(0, 65535));
+ }
+ if (name.equals("getUInt")) {
+ return org.joou.UInteger.valueOf(data.consumeLong(0,
4294967295L));
+ }
+ if (name.equals("getULong")) {
+ return org.joou.ULong.valueOf(data.consumeLong(0,
Long.MAX_VALUE));
+ }
+
+ return null;
+ }
+ );
+
+ try {
+ AvaticaSite.get(accessor, data.consumeInt(-10, 100),
data.consumeBoolean(), calendar);
+ } catch (SQLException e) {
+ // Expected to throw SQLException for unsupported conversions
+ }
+ break;
+ default:
+ break;
+ }
+
+ } catch (IllegalArgumentException | UnsupportedOperationException e) {
+ // UnsupportedOperationException is explicitly thrown by
AvaticaSite.notImplemented()
+ // and unsupportedCast() when types don't align.
+ } catch (RuntimeException e) {
+ // TypedValue bindings often throw RuntimeException directly for "not
implemented"
+ if (!"not implemented".equals(e.getMessage())) {
+ throw e;
+ }
+ }
+ }
+}
diff --git
a/core/src/test/java/org/apache/calcite/avatica/fuzz/Base64Fuzzer.java
b/core/src/test/java/org/apache/calcite/avatica/fuzz/Base64Fuzzer.java
new file mode 100644
index 000000000..56092b7d8
--- /dev/null
+++ b/core/src/test/java/org/apache/calcite/avatica/fuzz/Base64Fuzzer.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.calcite.avatica.fuzz;
+
+import org.apache.calcite.avatica.util.Base64;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+
+import java.io.IOException;
+
+/**
+ * Fuzzer for Base64 utility class.
+ */
+public final class Base64Fuzzer {
+ private Base64Fuzzer() {
+ }
+
+ /**
+ * Fuzzes Base64 encode and decode methods.
+ *
+ * @param data fuzzed data provider
+ */
+ public static void fuzzerTestOneInput(FuzzedDataProvider data) {
+ try {
+ if (data.consumeBoolean()) {
+ Base64.encodeBytes(data.consumeRemainingAsBytes());
+ } else {
+ Base64.decode(data.consumeRemainingAsBytes());
+ }
+ } catch (IOException | IllegalArgumentException e) {
+ // Known exception
+ }
+ }
+}
diff --git
a/core/src/test/java/org/apache/calcite/avatica/fuzz/ConnectStringParserFuzzer.java
b/core/src/test/java/org/apache/calcite/avatica/fuzz/ConnectStringParserFuzzer.java
new file mode 100644
index 000000000..f6422a675
--- /dev/null
+++
b/core/src/test/java/org/apache/calcite/avatica/fuzz/ConnectStringParserFuzzer.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.calcite.avatica.fuzz;
+
+import org.apache.calcite.avatica.ConnectStringParser;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+
+import java.sql.SQLException;
+
+/**
+ * Fuzzer for ConnectStringParser.
+ */
+public final class ConnectStringParserFuzzer {
+ private ConnectStringParserFuzzer() {
+ }
+
+ /**
+ * Fuzzes the ConnectStringParser parse method.
+ *
+ * @param data fuzzed data provider
+ */
+ public static void fuzzerTestOneInput(FuzzedDataProvider data) {
+ try {
+ ConnectStringParser.parse(data.consumeRemainingAsString());
+ } catch (SQLException e) {
+ // Known exception
+ }
+ }
+}
diff --git
a/core/src/test/java/org/apache/calcite/avatica/fuzz/JsonHandlerFuzzer.java
b/core/src/test/java/org/apache/calcite/avatica/fuzz/JsonHandlerFuzzer.java
new file mode 100644
index 000000000..59bf7526c
--- /dev/null
+++ b/core/src/test/java/org/apache/calcite/avatica/fuzz/JsonHandlerFuzzer.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.calcite.avatica.fuzz;
+
+import org.apache.calcite.avatica.remote.JsonService;
+import org.apache.calcite.avatica.remote.Service;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JsonMappingException;
+
+import java.io.IOException;
+
+/**
+ * Fuzzer for JsonHandler (JsonService).
+ */
+public class JsonHandlerFuzzer {
+
+ private JsonHandlerFuzzer() {
+ }
+
+ static {
+ // Prevent failure on completely unknown properties that the fuzzer might
invent
+
JsonService.MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
false);
+ }
+
+ /**
+ * Fuzzes JSON serialization/deserialization for Avatica Request/Response
objects.
+ *
+ * @param data fuzzed data
+ */
+ public static void fuzzerTestOneInput(FuzzedDataProvider data) {
+ try {
+ // The goal here is to hit the deeply nested deserialization logic of
+ // Avatica's Request and Response models.
+ // Avatica uses Jackson to parse strings into classes like
+ // Service.ExecuteRequest, Service.CatalogsRequest, etc.
+
+ boolean isRequest = data.consumeBoolean();
+
+ if (isRequest) {
+ String subType = data.pickValue(new String[]{
+ "getCatalogs", "getSchemas", "getTables", "getTableTypes",
"getTypeInfo", "getColumns",
+ "execute", "prepare", "prepareAndExecute", "fetch",
"createStatement", "closeStatement",
+ "openConnection", "closeConnection", "connectionSync",
"databaseProperties", "syncResults",
+ "commit", "rollback", "prepareAndExecuteBatch", "executeBatch"
+ });
+
+ java.util.Map<String, Object> map = new java.util.HashMap<>();
+ map.put("request", subType);
+
+ // Add random key/value pairs
+ int numFields = data.consumeInt(0, 10);
+ for (int i = 0; i < numFields; i++) {
+ switch (data.consumeInt(1, 4)) {
+ case 1:
+ map.put(data.consumeString(10), data.consumeString(20));
+ break;
+ case 2:
+ map.put(data.consumeString(10), data.consumeInt());
+ break;
+ case 3:
+ map.put(data.consumeString(10), data.consumeBoolean());
+ break;
+ case 4:
+ map.put(data.consumeString(10), null);
+ break;
+ default:
+ break;
+ }
+ }
+
+ String jsonPayload = JsonService.MAPPER.writeValueAsString(map);
+ JsonService.MAPPER.readValue(jsonPayload, Service.Request.class);
+ } else {
+ String subType = data.pickValue(new String[]{
+ "openConnection", "resultSet", "prepare", "fetch",
"createStatement", "closeStatement",
+ "closeConnection", "connectionSync", "databaseProperties",
"executeResults", "error",
+ "syncResults", "rpcMetadata", "commit", "rollback", "executeBatch"
+ });
+
+ java.util.Map<String, Object> map = new java.util.HashMap<>();
+ map.put("response", subType);
+
+ // Add random key/value pairs
+ int numFields = data.consumeInt(0, 10);
+ for (int i = 0; i < numFields; i++) {
+ switch (data.consumeInt(1, 4)) {
+ case 1:
+ map.put(data.consumeString(10), data.consumeString(20));
+ break;
+ case 2:
+ map.put(data.consumeString(10), data.consumeInt());
+ break;
+ case 3:
+ map.put(data.consumeString(10), data.consumeBoolean());
+ break;
+ case 4:
+ map.put(data.consumeString(10), null);
+ break;
+ default:
+ break;
+ }
+ }
+
+ String jsonPayload = JsonService.MAPPER.writeValueAsString(map);
+ JsonService.MAPPER.readValue(jsonPayload, Service.Response.class);
+ }
+
+ } catch (JsonParseException | JsonMappingException e) {
+ // Known Jackson exceptions for invalid JSON structure or unmappable
types
+ } catch (IOException e) {
+ // General IO issues reading the string
+ } catch (IllegalArgumentException | IllegalStateException e) {
+ // Known issues when Jackson encounters valid JSON but violates
Avatica's preconditions
+ }
+ }
+}
diff --git
a/core/src/test/java/org/apache/calcite/avatica/fuzz/ProtobufHandlerFuzzer.java
b/core/src/test/java/org/apache/calcite/avatica/fuzz/ProtobufHandlerFuzzer.java
new file mode 100644
index 000000000..1475eb18a
--- /dev/null
+++
b/core/src/test/java/org/apache/calcite/avatica/fuzz/ProtobufHandlerFuzzer.java
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.calcite.avatica.fuzz;
+
+import org.apache.calcite.avatica.remote.ProtobufTranslationImpl;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import com.google.protobuf.ByteString;
+
+import java.io.IOException;
+
+/**
+ * Fuzzer for ProtobufHandler (ProtobufTranslation).
+ */
+public class ProtobufHandlerFuzzer {
+
+ private ProtobufHandlerFuzzer() {
+ }
+
+ private static final ProtobufTranslationImpl TRANSLATOR = new
ProtobufTranslationImpl();
+
+ /**
+ * Fuzzes Protobuf serialization/deserialization for Avatica
Request/Response objects.
+ *
+ * @param data fuzzed data
+ */
+ public static void fuzzerTestOneInput(FuzzedDataProvider data) {
+ try {
+ // The goal here is to hit the protobuf deserialization logic.
+ // Avatica maps Protobuf messages (WireMessage) into its POJO
Service.Request models.
+ // WireMessage requires a "name" string matching a Request subclass.
+
+ boolean isRequest = data.consumeBoolean();
+
+ if (isRequest) {
+ String subType = data.pickValue(new String[]{
+ "org.apache.calcite.avatica.proto.Requests$CatalogsRequest",
+ "org.apache.calcite.avatica.proto.Requests$SchemasRequest",
+ "org.apache.calcite.avatica.proto.Requests$TablesRequest",
+ "org.apache.calcite.avatica.proto.Requests$TableTypesRequest",
+ "org.apache.calcite.avatica.proto.Requests$TypeInfoRequest",
+ "org.apache.calcite.avatica.proto.Requests$ColumnsRequest",
+ "org.apache.calcite.avatica.proto.Requests$ExecuteRequest",
+ "org.apache.calcite.avatica.proto.Requests$PrepareRequest",
+
"org.apache.calcite.avatica.proto.Requests$PrepareAndExecuteRequest",
+ "org.apache.calcite.avatica.proto.Requests$FetchRequest",
+ "org.apache.calcite.avatica.proto.Requests$CreateStatementRequest",
+ "org.apache.calcite.avatica.proto.Requests$CloseStatementRequest",
+ "org.apache.calcite.avatica.proto.Requests$OpenConnectionRequest",
+ "org.apache.calcite.avatica.proto.Requests$CloseConnectionRequest",
+ "org.apache.calcite.avatica.proto.Requests$ConnectionSyncRequest",
+
"org.apache.calcite.avatica.proto.Requests$DatabasePropertyRequest",
+ "org.apache.calcite.avatica.proto.Requests$SyncResultsRequest",
+ "org.apache.calcite.avatica.proto.Requests$CommitRequest",
+ "org.apache.calcite.avatica.proto.Requests$RollbackRequest",
+
"org.apache.calcite.avatica.proto.Requests$PrepareAndExecuteBatchRequest",
+ "org.apache.calcite.avatica.proto.Requests$ExecuteBatchRequest"
+ });
+
+ org.apache.calcite.avatica.proto.Common.WireMessage wireMsg =
+ org.apache.calcite.avatica.proto.Common.WireMessage.newBuilder()
+ .setName(subType)
+
.setWrappedMessage(ByteString.copyFrom(data.consumeRemainingAsBytes()))
+ .build();
+
+ byte[] protobufPayload = wireMsg.toByteArray();
+ TRANSLATOR.parseRequest(protobufPayload);
+ } else {
+ String subType = data.pickValue(new String[]{
+
"org.apache.calcite.avatica.proto.Responses$OpenConnectionResponse",
+
"org.apache.calcite.avatica.proto.Responses$CloseConnectionResponse",
+
"org.apache.calcite.avatica.proto.Responses$CloseStatementResponse",
+
"org.apache.calcite.avatica.proto.Responses$ConnectionSyncResponse",
+
"org.apache.calcite.avatica.proto.Responses$CreateStatementResponse",
+
"org.apache.calcite.avatica.proto.Responses$DatabasePropertyResponse",
+ "org.apache.calcite.avatica.proto.Responses$ExecuteResponse",
+ "org.apache.calcite.avatica.proto.Responses$FetchResponse",
+ "org.apache.calcite.avatica.proto.Responses$PrepareResponse",
+ "org.apache.calcite.avatica.proto.Responses$ResultSetResponse",
+ "org.apache.calcite.avatica.proto.Responses$ErrorResponse",
+ "org.apache.calcite.avatica.proto.Responses$SyncResultsResponse",
+ "org.apache.calcite.avatica.proto.Responses$RpcMetadata",
+ "org.apache.calcite.avatica.proto.Responses$CommitResponse",
+ "org.apache.calcite.avatica.proto.Responses$RollbackResponse",
+ "org.apache.calcite.avatica.proto.Responses$ExecuteBatchResponse"
+ });
+
+ org.apache.calcite.avatica.proto.Common.WireMessage wireMsg =
+ org.apache.calcite.avatica.proto.Common.WireMessage.newBuilder()
+ .setName(subType)
+
.setWrappedMessage(ByteString.copyFrom(data.consumeRemainingAsBytes()))
+ .build();
+
+ byte[] protobufPayload = wireMsg.toByteArray();
+ TRANSLATOR.parseResponse(protobufPayload);
+ }
+
+ } catch (IOException e) {
+ // Known exception from protobuf parsing (e.g.
InvalidProtocolBufferException)
+ } catch (IllegalArgumentException | IllegalStateException |
NullPointerException e) {
+ // Known issues when Protobuf unmarshalls into Avatica types that fail
preconditions
+ } catch (RuntimeException e) {
+ // Specifically catching Avatica's custom DeserializationException or
+ // unhandled protobuf issues to ensure the fuzzer survives
+ if (e.getClass().getName().contains("DeserializationException")
+ || e.getClass().getName().contains("InvalidProtocolBufferException")
+ || e.getMessage() != null && e.getMessage().contains("Unknown type:")
+ || e.getMessage() != null && e.getMessage().contains("Unhandled
type:")) {
+ return;
+ }
+ // If it's a real bug (NullPointerException, etc), let it crash!
+ throw e;
+ }
+ }
+}
diff --git
a/core/src/test/java/org/apache/calcite/avatica/fuzz/TypedValueFuzzer.java
b/core/src/test/java/org/apache/calcite/avatica/fuzz/TypedValueFuzzer.java
new file mode 100644
index 000000000..6243382dc
--- /dev/null
+++ b/core/src/test/java/org/apache/calcite/avatica/fuzz/TypedValueFuzzer.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.calcite.avatica.fuzz;
+
+import org.apache.calcite.avatica.proto.Common;
+import org.apache.calcite.avatica.remote.TypedValue;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+
+import java.util.Calendar;
+import java.util.Locale;
+import java.util.TimeZone;
+
+/**
+ * Fuzzer for TypedValue.
+ */
+public class TypedValueFuzzer {
+
+ private TypedValueFuzzer() {
+ }
+
+ /**
+ * Fuzzes TypedValue conversion methods.
+ *
+ * @param data fuzzed data
+ */
+ public static void fuzzerTestOneInput(FuzzedDataProvider data) {
+ try {
+ // Use the Fuzzer to generate random Protobuf arrays
+ boolean isFromProto = data.consumeBoolean();
+
+ if (isFromProto) {
+ // Parse it into Common.TypedValue
+ Common.TypedValue protoValue =
Common.TypedValue.parseFrom(data.consumeRemainingAsBytes());
+
+ // Attempt to convert it into a local Avatica TypedValue and then to
JDBC representations
+ TypedValue typedValue = TypedValue.fromProto(protoValue);
+
+ // Convert to local and jdbc formats
+ typedValue.toLocal();
+ typedValue.toJdbc(Calendar.getInstance(TimeZone.getTimeZone("UTC"),
Locale.ROOT));
+
+ // Attempt Protobuf serialization back
+ typedValue.toProto();
+ } else {
+ // Fuzz the direct POJO creator
+ String typeName = data.pickValue(new String[]{
+ "STRING", "BOOLEAN", "BYTE", "SHORT", "INTEGER", "LONG", "FLOAT",
"DOUBLE", "DATE", "TIME", "TIMESTAMP"
+ });
+
+ Object fakeValue = null;
+ switch (typeName) {
+ case "STRING":
+ fakeValue = data.consumeString(50);
+ break;
+ case "BOOLEAN":
+ fakeValue = data.consumeBoolean();
+ break;
+ case "BYTE":
+ fakeValue = data.consumeByte();
+ break;
+ case "SHORT":
+ fakeValue = data.consumeShort();
+ break;
+ case "INTEGER":
+ fakeValue = data.consumeInt();
+ break;
+ case "LONG":
+ fakeValue = data.consumeLong();
+ break;
+ case "FLOAT":
+ fakeValue = data.consumeFloat();
+ break;
+ case "DOUBLE":
+ fakeValue = data.consumeDouble();
+ break;
+ case "DATE":
+ case "TIME":
+ case "TIMESTAMP":
+ fakeValue = data.consumeLong();
+ break;
+ default:
+ break;
+ }
+
+ // Fuzz create factory mapping the object value with random type
identifier
+ TypedValue created = TypedValue.create(typeName, fakeValue);
+
+ // Call accessors
+ created.toLocal();
+ created.toJdbc(Calendar.getInstance(TimeZone.getTimeZone("UTC"),
Locale.ROOT));
+ created.toProto();
+ }
+
+ } catch (java.io.IOException e) {
+ // Known exception for invalid protobuf
+ } catch (RuntimeException e) {
+ // TypedValue parser is known to throw unchecked exceptions
+ // when types don't align with values in the protobuf
+ // E.g., asking for a Boolean from a protobuf field that was stored as a
String.
+ }
+ }
+}