Repository navigation
Replace jbrotli Brotli codec with brotli4j for CVE-2026-111497 - #3880
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.
Release the native encoder even after write or flush failures, retaining the original error and any cleanup errors without try-with-resources self-suppression. Reject negative decode limits and non-empty output under a zero limit. Add six regression tests and strengthen existing exception assertions.
|
Moving this forward, thanks @Devdha |
Rationale for this change
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, which wrapsorg.meteogroup.jbrotli:jbrotli:0.5.0. Neither project has had a commit since 2017, so reading an untrusted Brotli-compressed Parquet file goes through a native decoder that predates this fix and will not receive new ones.This PR moves to the maintained
com.aayushatharva.brotli4j:brotli4j:1.24.0, which bundles Brotli 1.2.1, and keeps the Parquet file format and existing Hadoop configuration compatible.References:
What changes are included in this PR?
org.apache.parquet.hadoop.codec.BrotliCodecand pointCompressionCodecName.BROTLIat it. The default compression quality stays at1.org.apache.hadoop.io.compress.BrotliCodecclass name as an alias forBROTLI. Without the alias, a Hadoop job that still has the old class on its classpath logs a warning and writes uncompressed data. With it, compression goes through the new implementation.non-aarch64profile, and addbrotli4jas an optional dependency.TestDirectCodecFactoryno longer skips Brotli on aarch64.DirectCodecFactory, use the stream-based decompression path for codecs that do not expose a HadoopDecompressor.Decoding correctness and output bounds
CodecFactorypasses the uncompressed size from the page header as that limit. A negative limit is rejected, a limit of zero requires empty output, and data that expands beyond the declared size fails with anIOException.createInputStream(InputStream)path also has a finite limit.Output stream and error cleanup
flush()flushes the encoder and the target,close()closes the target, andfinish()on its own leaves the target open.close()releases the native encoder even afterwrite()orflush()has failed. The first error is kept as the cause, and later cleanup errors are attached as suppressed exceptions.try-with-resourcesdoes not try to suppress an exception onto itself.finish()still reports the failure.resetState()is explicitly unsupported.Are these changes tested?
Yes. Local environment: macOS aarch64, JDK 17, Thrift 0.25.0.
TestCompressionCodecName(5),TestBrotliCodec(17),TestDirectCodecFactory(15),CodecConfigTest(1).try-with-resources, and zero and negative limits. The existing large-output, compression-bomb, and stream-contract tests also pass.spotless:checkpasses for the whole repository, andparquet-hadooppackages along with the modules it depends on.Broken pipe,close()releases the native handle.The local test and package runs use
-Denforcer.skip=truebecause this branch's base commit still has the JUnit 4 import inTestFooterIntegrity, which was fixed upstream in #3878.CI passes on
ubuntu-latest. Linux aarch64 was not run locally and is not covered by this repository's workflows.Are there any user-facing changes?
.brextension are unchanged.parquet.compression=BROTLIor the new class name.brotli4jdependency. Cross-platform distributions need to include thenative-*artifact for each platform they run on.