Repository navigation
Conversation
|
Updated the PR to cover the missing cases:
Verification: |
| </repositories> | ||
| <dependencies> | ||
| <dependency> | ||
| <groupId>com.github.rdblue</groupId> |
| <version>${zstd-jni.version}</version> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>com.aayushatharva.brotli4j</groupId> |
There was a problem hiding this comment.
Checked the package and it looks legit. Even it is optional, the licenses are also in line with the ASF requirements.
| GZIP("org.apache.hadoop.io.compress.GzipCodec", CompressionCodec.GZIP, ".gz"), | ||
| LZO("com.hadoop.compression.lzo.LzoCodec", CompressionCodec.LZO, ".lzo"), | ||
| BROTLI("org.apache.hadoop.io.compress.BrotliCodec", CompressionCodec.BROTLI, ".br"), | ||
| BROTLI("org.apache.parquet.hadoop.codec.BrotliCodec", CompressionCodec.BROTLI, ".br"), |
There was a problem hiding this comment.
Renaming the class here changes how CodecConfig.getHadoopCompressionCodec() resolves mapred.output.compression.codec. That goes through fromCompressionCodec(Class), which matches on the exact class name. A job that still has mapred.output.compression.codec=org.apache.hadoop.io.compress.BrotliCodec set (with the old brotli-codec jar on its classpath) no longer matches. It hits the CompressionCodecNotSupportedException catch in CodecConfig and silently writes UNCOMPRESSED, with only a warning in the logs.
Could we keep the old class name as an alias? Here's a patch with a small test; TestBrotliCodec and TestDirectCodecFactory still pass with it:
Patch
diff --git a/parquet-common/src/main/java/org/apache/parquet/hadoop/metadata/CompressionCodecName.java b/parquet-common/src/main/java/org/apache/parquet/hadoop/metadata/CompressionCodecName.java
index 5354e6c69..176328a38 100644
--- a/parquet-common/src/main/java/org/apache/parquet/hadoop/metadata/CompressionCodecName.java
+++ b/parquet-common/src/main/java/org/apache/parquet/hadoop/metadata/CompressionCodecName.java
@@ -39,17 +39,31 @@ public enum CompressionCodecName {
return valueOf(name.toUpperCase(Locale.ENGLISH));
}
+ // Brotli codec class from the removed jbrotli-based brotli-codec artifact. Still accepted so that
+ // jobs configured through mapred.output.compression.codec keep writing Brotli.
+ private static final String LEGACY_BROTLI_CODEC_CLASS = "org.apache.hadoop.io.compress.BrotliCodec";
+
public static CompressionCodecName fromCompressionCodec(Class<?> clazz) {
if (clazz == null) {
return UNCOMPRESSED;
}
- String name = clazz.getName();
+ CompressionCodecName codec = fromCompressionCodecClassName(clazz.getName());
+ if (codec == null) {
+ throw new CompressionCodecNotSupportedException(clazz);
+ }
+ return codec;
+ }
+
+ static CompressionCodecName fromCompressionCodecClassName(String name) {
for (CompressionCodecName codec : CompressionCodecName.values()) {
if (name.equals(codec.getHadoopCompressionCodecClassName())) {
return codec;
}
}
- throw new CompressionCodecNotSupportedException(clazz);
+ if (LEGACY_BROTLI_CODEC_CLASS.equals(name)) {
+ return BROTLI;
+ }
+ return null;
}
public static CompressionCodecName fromParquet(CompressionCodec codec) {
diff --git a/parquet-common/src/test/java/org/apache/parquet/hadoop/metadata/TestCompressionCodecName.java b/parquet-common/src/test/java/org/apache/parquet/hadoop/metadata/TestCompressionCodecName.java
new file mode 100644
index 000000000..5bb2145a5
--- /dev/null
+++ b/parquet-common/src/test/java/org/apache/parquet/hadoop/metadata/TestCompressionCodecName.java
@@ -0,0 +1,64 @@
+/*
+ * 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.hadoop.metadata;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import org.apache.parquet.hadoop.codec.CompressionCodecNotSupportedException;
+import org.junit.jupiter.api.Test;
+
+public class TestCompressionCodecName {
+
+ @Test
+ public void testLegacyBrotliCodecClassMapsToBrotli() {
+ assertThat(CompressionCodecName.fromCompressionCodecClassName("org.apache.hadoop.io.compress.BrotliCodec"))
+ .isEqualTo(CompressionCodecName.BROTLI);
+ }
+
+ @Test
+ public void testBrotliCodecClassMapsToBrotli() {
+ assertThat(CompressionCodecName.fromCompressionCodecClassName("org.apache.parquet.hadoop.codec.BrotliCodec"))
+ .isEqualTo(CompressionCodecName.BROTLI);
+ }
+
+ @Test
+ public void testCodecClassNamesRoundTrip() {
+ for (CompressionCodecName codec : CompressionCodecName.values()) {
+ if (codec.getHadoopCompressionCodecClassName() != null) {
+ assertThat(CompressionCodecName.fromCompressionCodecClassName(
+ codec.getHadoopCompressionCodecClassName()))
+ .isEqualTo(codec);
+ }
+ }
+ }
+
+ @Test
+ public void testUnknownCodecClass() {
+ assertThat(CompressionCodecName.fromCompressionCodecClassName(String.class.getName()))
+ .isNull();
+ assertThatThrownBy(() -> CompressionCodecName.fromCompressionCodec(String.class))
+ .isInstanceOf(CompressionCodecNotSupportedException.class);
+ }
+
+ @Test
+ public void testNullCodecClassIsUncompressed() {
+ assertThat(CompressionCodecName.fromCompressionCodec(null)).isEqualTo(CompressionCodecName.UNCOMPRESSED);
+ }
+}There was a problem hiding this comment.
Applied your suggested alias in 93054b4. The legacy class name now maps to BROTLI, while BROTLI continues to resolve to the new brotli4j-backed codec.
Added the five regression tests for legacy/current class names, other codecs, unknown classes, and null. I confirmed that the legacy-name test fails without the alias and passes with it.
Validation:
parquet-common: all 337 tests passed.- On JDK 17:
TestBrotliCodec(11), the fullTestDirectCodecFactory(15), andCodecConfigTest(1) all passed. - Repository-wide
spotless:checkpassed.
The Hadoop test run used -Denforcer.skip=true for the pre-existing JUnit 4 imports in TestFooterIntegrity. The new GitHub Actions runs currently show action_required.
Accept the legacy Hadoop Brotli codec name while keeping the brotli4j-backed codec as the implementation. Add regression coverage for legacy and current names, other codecs, and unknown or null classes. Based on the patch suggested by Fokko in apache#3880.
Background
CVE-2026-111497 is an out-of-bounds write in the Google Brotli decoder, fixed in Brotli 1.2.1. Parquet's
BROTLIcodec currently comes from the abandonedcom.github.rdblue:brotli-codec:0.1.1artifact (last release 2021), which wrapsorg.meteogroup.jbrotli:jbrotli:0.5.0. That native library bundles Brotli 0.5.0, so reading an untrusted Brotli-compressed Parquet file can reach a very old decoder.The replacement uses the maintained
com.aayushatharva.brotli4j:brotli4j:1.24.0, which bundles Brotli 1.2.1:Change
org.apache.parquet.hadoop.codec.BrotliCodecbacked by brotli4j.CompressionCodecName.BROTLIat the new codec.brotli-codecdependency with optionalbrotli4j; remove the JitPack repository and thenon-aarch64profile. brotli4j ships ARM64 natives, so the PARQUET-1975 ARM64 skip can go away.DirectCodecFactoryto fall back to the stream-based decompressor for codecs that do not expose a HadoopDecompressor(Brotli is one).compression.brotli.quality=1).Decoding correctness and bounds
brotli4j 1.24.0's
BrotliInputStreamfails withunexpected end of inputon valid streams once the decompressed output crosses the 4 MiB encoder window (hyperxpro/Brotli4j#267). Parquet feeds one bounded page at a time, soBrotliCompressionInputStreamuses brotli4j's bounded one-shotDecoder.decompress(byte[], int, int, int).CodecFactorypasses the uncompressed page size from the page header as the output bound. A crafted page that expands beyond its declared size is rejected with anIOExceptioninstead of allocating unbounded memory. The genericcreateInputStream(InputStream)path uses a large but finite cap.The output stream follows the Hadoop contract:
close()closes the caller's target,flush()flushes both the encoder and the target, and a failedfinish()is reported on retry.resetState()is explicitly unsupported rather than replaying stale decoded bytes.Compatibility: the on-disk format and
.brextension are unchanged. Code that referenced the oldorg.apache.hadoop.io.compress.BrotliCodecclass directly should useCompressionCodecName.BROTLI/CodecFactoryor the new class. Users still need to add the optionalbrotli4jdependency (as they previously neededbrotli-codec); brotli4j selects native artifacts for the build platform, so cross-platform distributions need the matchingnative-*artifacts.Testing
TestBrotliCodec: 11 passed (round-trip, 5 MiB compressible stream crossing the encoder window,CodecFactorypath at quality 5, bounded-decompression bomb rejection, invalid quality, zero/negative sizes, close/flush/finish/position behavior)TestDirectCodecFactory.compressionCodecs: 1 passedspotless:check: passedpackage: passedThe local test run used
-Denforcer.skip=trueonly because current master has an unrelated JUnit 4 import inTestFooterIntegrity(being migrated in #3878). The ARM64 run was on macOS aarch64; Linux x86_64/aarch64 coverage comes from CI.I could not find an existing PARQUET JIRA for this; happy to file one if required before review.