This is an automated email from the ASF dual-hosted git repository.
vinothchandar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hudi.git
The following commit(s) were added to refs/heads/master by this push:
new afe9e6bcedab fix(common): use GenericData model in parquet-avro
readers to avoid java-class resolution (#20187)
afe9e6bcedab is described below
commit afe9e6bcedab79ce27e156e22d4da2985a4447f5
Author: databird07 <[email protected]>
AuthorDate: Fri Oct 2 08:25:21 2026 -0700
fix(common): use GenericData model in parquet-avro readers to avoid
java-class resolution (#20187)
Dependabot flags org.apache.parquet:parquet-avro < 1.15.1 ("Apache Parquet
Avro Module Vulnerable to Arbitrary Code Execution"). The flagged artifact
comes from ${hive.parquet.version} (1.10.1), and it is shaded into
hudi-hadoop-mr-bundle.
We can't just bump the version. The bundle runs on Hive's own parquet
(1.10.x on Hive 3.1.3), and parquet-avro 1.15.x needs LogicalTypeAnnotation,
ParquetConfiguration and HadoopParquetConfiguration, which that runtime doesn't
have. The Dependabot bump #17654 failed validate-bundles for this reason and
was closed.
The vulnerable path is parquet-avro reading the java-class / java-key-class
schema properties from the file footer and instantiating that class
(AvroRecordConverter.getStringableClass → ClassUtils.forName). It only does
this when the Avro data model is SpecificData/ReflectData. When no model is
set, parquet-avro falls back to SpecificDataSupplier, and that is the default
every Hudi reader was getting.
---
.../org/apache/hudi/common/util/ParquetUtils.java | 8 +-
.../avro/HoodieAvroParquetReaderBuilder.java | 2 +-
.../apache/parquet/avro/HoodieAvroReadSupport.java | 1 +
.../avro/TestAvroStringableClassResolution.java | 108 +++++++++++++++++++++
.../hudi/hadoop/avro/HoodieAvroParquetReader.java | 2 +-
.../apache/hudi/cli/HDFSParquetImporterUtils.java | 2 +
6 files changed, 118 insertions(+), 5 deletions(-)
diff --git
a/hudi-hadoop-common/src/main/java/org/apache/hudi/common/util/ParquetUtils.java
b/hudi-hadoop-common/src/main/java/org/apache/hudi/common/util/ParquetUtils.java
index 31b0e0868fe6..87961f8223cf 100644
---
a/hudi-hadoop-common/src/main/java/org/apache/hudi/common/util/ParquetUtils.java
+++
b/hudi-hadoop-common/src/main/java/org/apache/hudi/common/util/ParquetUtils.java
@@ -47,6 +47,7 @@ import org.apache.hudi.storage.StorageConfiguration;
import org.apache.hudi.storage.StoragePath;
import lombok.extern.slf4j.Slf4j;
+import org.apache.avro.generic.GenericData;
import org.apache.avro.generic.GenericRecord;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
@@ -171,7 +172,7 @@ public class ParquetUtils extends FileFormatUtils {
AvroReadSupport.setRequestedProjection(conf,
HoodieSchemaUtils.getRecordKeySchema().toAvroSchema());
try {
ParquetReaderIterator<GenericRecord> rowIterator = new
ParquetReaderIterator<>(
- AvroParquetReader.<GenericRecord>builder(new
Path(filePath.toUri())).withConf(conf).build());
+ AvroParquetReader.<GenericRecord>builder(new
Path(filePath.toUri())).withDataModel(GenericData.get()).withConf(conf).build());
return new ClosableIterator<String>() {
private long rowPosition = 0;
@@ -241,7 +242,7 @@ public class ParquetUtils extends FileFormatUtils {
AvroReadSupport.setRequestedProjection(conf, readSchema.toAvroSchema());
Set<Pair<String, Long>> rowKeys = new HashSet<>();
long rowPosition = 0;
- try (ParquetReader reader =
AvroParquetReader.builder(filePath).withConf(conf).build()) {
+ try (ParquetReader reader =
AvroParquetReader.builder(filePath).withDataModel(GenericData.get()).withConf(conf).build())
{
Object obj = reader.read();
while (obj != null) {
if (obj instanceof GenericRecord) {
@@ -291,7 +292,7 @@ public class ParquetUtils extends FileFormatUtils {
AvroReadSupport.setAvroReadSchema(conf, readSchema.toAvroSchema());
AvroReadSupport.setRequestedProjection(conf, readSchema.toAvroSchema());
ParquetReader<GenericRecord> reader =
- AvroParquetReader.<GenericRecord>builder(new
Path(filePath.toUri())).withConf(conf).build();
+ AvroParquetReader.<GenericRecord>builder(new
Path(filePath.toUri())).withDataModel(GenericData.get()).withConf(conf).build();
return HoodieKeyIterator.getInstance(new
ParquetReaderIterator<>(reader), keyGeneratorOpt, partitionPath);
} catch (IOException e) {
throw new HoodieIOException("Failed to read from Parquet file " +
filePath, e);
@@ -431,6 +432,7 @@ public class ParquetUtils extends FileFormatUtils {
public List<GenericRecord> readAvroRecords(HoodieStorage storage,
StoragePath filePath) {
List<GenericRecord> records = new ArrayList<>();
try (ParquetReader reader = AvroParquetReader.builder(new
Path(filePath.toUri()))
+ .withDataModel(GenericData.get())
.withConf(storage.getConf().unwrapAs(Configuration.class)).build()) {
Object obj = reader.read();
while (obj != null) {
diff --git
a/hudi-hadoop-common/src/main/java/org/apache/parquet/avro/HoodieAvroParquetReaderBuilder.java
b/hudi-hadoop-common/src/main/java/org/apache/parquet/avro/HoodieAvroParquetReaderBuilder.java
index 9fe32b456ef3..77f9a25a7cd6 100644
---
a/hudi-hadoop-common/src/main/java/org/apache/parquet/avro/HoodieAvroParquetReaderBuilder.java
+++
b/hudi-hadoop-common/src/main/java/org/apache/parquet/avro/HoodieAvroParquetReaderBuilder.java
@@ -36,7 +36,7 @@ import org.apache.parquet.schema.MessageType;
*/
public class HoodieAvroParquetReaderBuilder<T> extends
ParquetReader.Builder<T> {
- private GenericData model = null;
+ private GenericData model = GenericData.get();
private boolean enableCompatibility = true;
private boolean isReflect = true;
private Option<MessageType> tableSchema = Option.empty();
diff --git
a/hudi-hadoop-common/src/main/java/org/apache/parquet/avro/HoodieAvroReadSupport.java
b/hudi-hadoop-common/src/main/java/org/apache/parquet/avro/HoodieAvroReadSupport.java
index 9292baf65dbf..a895db255639 100644
---
a/hudi-hadoop-common/src/main/java/org/apache/parquet/avro/HoodieAvroReadSupport.java
+++
b/hudi-hadoop-common/src/main/java/org/apache/parquet/avro/HoodieAvroReadSupport.java
@@ -50,6 +50,7 @@ public class HoodieAvroReadSupport<T> extends
AvroReadSupport<T> {
}
public HoodieAvroReadSupport() {
+ super(GenericData.get());
tableSchema = Option.empty();
}
diff --git
a/hudi-hadoop-common/src/test/java/org/apache/parquet/avro/TestAvroStringableClassResolution.java
b/hudi-hadoop-common/src/test/java/org/apache/parquet/avro/TestAvroStringableClassResolution.java
new file mode 100644
index 000000000000..5501b4f91f11
--- /dev/null
+++
b/hudi-hadoop-common/src/test/java/org/apache/parquet/avro/TestAvroStringableClassResolution.java
@@ -0,0 +1,108 @@
+/*
+ * 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.parquet.avro;
+
+import org.apache.hudi.common.util.ParquetUtils;
+import org.apache.hudi.storage.StoragePath;
+import org.apache.hudi.storage.hadoop.HoodieHadoopStorage;
+
+import org.apache.avro.Schema;
+import org.apache.avro.SchemaBuilder;
+import org.apache.avro.generic.GenericData;
+import org.apache.avro.generic.GenericRecord;
+import org.apache.avro.generic.IndexedRecord;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.Path;
+import org.apache.parquet.hadoop.ParquetReader;
+import org.apache.parquet.hadoop.ParquetWriter;
+import org.apache.parquet.hadoop.util.HadoopInputFile;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+/**
+ * Verifies that Hudi's parquet-avro readers never instantiate classes named
by the
+ * {@code java-class} property of an Avro schema stored in a parquet file
footer.
+ */
+class TestAvroStringableClassResolution {
+
+ @TempDir
+ java.nio.file.Path tempDir;
+
+ private Path filePath;
+
+ public static class StringableProbe {
+ static final AtomicInteger INSTANCES = new AtomicInteger();
+
+ public StringableProbe(String value) {
+ INSTANCES.incrementAndGet();
+ }
+ }
+
+ @BeforeEach
+ void writeFile() throws IOException {
+ Schema stringSchema = Schema.create(Schema.Type.STRING);
+ stringSchema.addProp("java-class", StringableProbe.class.getName());
+ Schema schema =
SchemaBuilder.record("rec").fields().name("f").type(stringSchema).noDefault().endRecord();
+
+ filePath = new Path(tempDir.resolve("stringable.parquet").toUri());
+ try (ParquetWriter<GenericRecord> writer =
AvroParquetWriter.<GenericRecord>builder(filePath)
+ .withSchema(schema)
+ .withDataModel(GenericData.get())
+ .withConf(new Configuration())
+ .build()) {
+ GenericRecord record = new GenericData.Record(schema);
+ record.put("f", "value");
+ writer.write(record);
+ }
+ StringableProbe.INSTANCES.set(0);
+ }
+
+ @Test
+ void hoodieAvroParquetReaderBuilderIgnoresJavaClass() throws IOException {
+ Configuration conf = new Configuration();
+ try (ParquetReader<IndexedRecord> reader =
+ new
HoodieAvroParquetReaderBuilder<IndexedRecord>(HadoopInputFile.fromPath(filePath,
conf))
+ .withConf(conf)
+ .build()) {
+ IndexedRecord record = reader.read();
+ assertEquals("value", record.get(0).toString());
+ assertFalse(record.get(0) instanceof StringableProbe);
+ }
+ assertEquals(0, StringableProbe.INSTANCES.get());
+ }
+
+ @Test
+ void parquetUtilsReadAvroRecordsIgnoresJavaClass() {
+ Configuration conf = new Configuration();
+ conf.setBoolean(AvroReadSupport.AVRO_COMPATIBILITY, false);
+ StoragePath storagePath = new StoragePath(filePath.toUri());
+ List<GenericRecord> records = new ParquetUtils().readAvroRecords(new
HoodieHadoopStorage(filePath, conf), storagePath);
+ assertEquals(1, records.size());
+ assertEquals("value", records.get(0).get("f").toString());
+ assertEquals(0, StringableProbe.INSTANCES.get());
+ }
+}
diff --git
a/hudi-hadoop-mr/src/main/java/org/apache/hudi/hadoop/avro/HoodieAvroParquetReader.java
b/hudi-hadoop-mr/src/main/java/org/apache/hudi/hadoop/avro/HoodieAvroParquetReader.java
index e30e1e5c7335..a4bde548268b 100644
---
a/hudi-hadoop-mr/src/main/java/org/apache/hudi/hadoop/avro/HoodieAvroParquetReader.java
+++
b/hudi-hadoop-mr/src/main/java/org/apache/hudi/hadoop/avro/HoodieAvroParquetReader.java
@@ -82,7 +82,7 @@ public class HoodieAvroParquetReader extends
RecordReader<Void, ArrayWritable> {
AvroReadSupport.setRequestedProjection(conf,
filterSchema.toAvroSchema());
}
}
- parquetRecordReader = new ParquetRecordReader<>(new AvroReadSupport<>(),
getFilter(conf));
+ parquetRecordReader = new ParquetRecordReader<>(new
AvroReadSupport<>(GenericData.get()), getFilter(conf));
}
@Override
diff --git
a/hudi-spark-datasource/hudi-spark/src/main/java/org/apache/hudi/cli/HDFSParquetImporterUtils.java
b/hudi-spark-datasource/hudi-spark/src/main/java/org/apache/hudi/cli/HDFSParquetImporterUtils.java
index 7fc06fc4e380..2b4b5ac55069 100644
---
a/hudi-spark-datasource/hudi-spark/src/main/java/org/apache/hudi/cli/HDFSParquetImporterUtils.java
+++
b/hudi-spark-datasource/hudi-spark/src/main/java/org/apache/hudi/cli/HDFSParquetImporterUtils.java
@@ -52,6 +52,7 @@ import org.apache.hadoop.fs.Path;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.parquet.avro.AvroReadSupport;
+import org.apache.parquet.avro.GenericDataSupplier;
import org.apache.parquet.hadoop.ParquetInputFormat;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.JavaSparkContext;
@@ -184,6 +185,7 @@ public class HDFSParquetImporterUtils implements
Serializable {
job.getConfiguration().set(FileInputFormat.LIST_STATUS_NUM_THREADS,
"1024");
AvroReadSupport.setAvroReadSchema(jsc.hadoopConfiguration(),
HoodieSchema.parse(schemaStr).getAvroSchema());
ParquetInputFormat.setReadSupportClass(job, (AvroReadSupport.class));
+ AvroReadSupport.setAvroDataSupplier(job.getConfiguration(),
GenericDataSupplier.class);
HoodieEngineContext context = new HoodieSparkEngineContext(jsc);
context.setJobStatus(this.getClass().getSimpleName(), "Build records for
import: " + this.tableName);