Skip to content

Commit 36376b7

Browse files
authored
fix(java): avoid deflater meta decompression hang on invalid input (#3472)
## What does this PR do? - Adds a no-progress guard to `DeflaterMetaCompressor.decompress` so invalid/truncated deflate streams fail fast instead of spinning forever. - Introduces `InvalidDataException` (subclass of `ForyException`) and raises it for malformed/truncated meta-compression input. - Adds `DeflaterMetaCompressorTest` coverage for roundtrip + truncated/corrupt payload failures (with timeout guard). - Registers `InvalidDataException` in `fory-core` `native-image.properties`. ## Why is this needed? Issue #3471 reports an infinite loop (`inflate() == 0` while `finished() == false`) that can peg CPU at 100% for corrupt or truncated streams. ## Verification - `cd java && mvn -T16 -pl fory-core test -Dtest=org.apache.fory.meta.DeflaterMetaCompressorTest` Closes #3471
1 parent 7f44cfa commit 36376b7

File tree

4 files changed

+121
-2
lines changed

4 files changed

+121
-2
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.fory.exception;
21+
22+
/** Exception thrown when fory receives malformed or truncated input data. */
23+
public class InvalidDataException extends ForyException {
24+
25+
public InvalidDataException(String message) {
26+
super(message);
27+
}
28+
29+
public InvalidDataException(Throwable cause) {
30+
super(cause);
31+
}
32+
33+
public InvalidDataException(String message, Throwable cause) {
34+
super(message, cause);
35+
}
36+
}

java/fory-core/src/main/java/org/apache/fory/meta/DeflaterMetaCompressor.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.zip.DataFormatException;
2424
import java.util.zip.Deflater;
2525
import java.util.zip.Inflater;
26+
import org.apache.fory.exception.InvalidDataException;
2627

2728
/** A meta compressor based on {@link Deflater} compression algorithm. */
2829
public class DeflaterMetaCompressor implements MetaCompressor {
@@ -49,10 +50,22 @@ public byte[] decompress(byte[] input, int offset, int size) {
4950
try {
5051
while (!inflater.finished()) {
5152
int decompressedSize = inflater.inflate(buffer);
52-
outputStream.write(buffer, 0, decompressedSize);
53+
if (decompressedSize > 0) {
54+
outputStream.write(buffer, 0, decompressedSize);
55+
continue;
56+
}
57+
if (inflater.needsDictionary()) {
58+
throw new InvalidDataException("Invalid compressed metadata, dictionary is required.");
59+
}
60+
if (inflater.needsInput()) {
61+
throw new InvalidDataException("Invalid compressed metadata, stream is truncated.");
62+
}
63+
throw new InvalidDataException("Invalid compressed metadata, inflater made no progress.");
5364
}
5465
} catch (DataFormatException e) {
55-
throw new RuntimeException(e);
66+
throw new InvalidDataException("Invalid compressed metadata format.", e);
67+
} finally {
68+
inflater.end();
5669
}
5770
return outputStream.toByteArray();
5871
}

java/fory-core/src/main/resources/META-INF/native-image/org.apache.fory/fory-core/native-image.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ Args=--initialize-at-build-time=org.apache.fory.memory.MemoryBuffer,\
227227
org.apache.fory.config.Language,\
228228
org.apache.fory.config.LongEncoding,\
229229
org.apache.fory.config.UnknownEnumValueStrategy,\
230+
org.apache.fory.exception.InvalidDataException,\
230231
org.apache.fory.logging.ForyLogger,\
231232
org.apache.fory.logging.LoggerFactory,\
232233
org.apache.fory.logging.LogLevel,\
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.fory.meta;
21+
22+
import static org.testng.Assert.assertEquals;
23+
import static org.testng.Assert.assertTrue;
24+
25+
import java.nio.charset.StandardCharsets;
26+
import java.util.Arrays;
27+
import org.apache.fory.exception.InvalidDataException;
28+
import org.testng.Assert;
29+
import org.testng.annotations.Test;
30+
31+
public class DeflaterMetaCompressorTest {
32+
private final DeflaterMetaCompressor compressor = new DeflaterMetaCompressor();
33+
34+
@Test
35+
public void testCompressDecompressRoundTrip() {
36+
byte[] input = sampleInput();
37+
byte[] compressed = compressor.compress(input, 0, input.length);
38+
byte[] decompressed = compressor.decompress(compressed, 0, compressed.length);
39+
assertEquals(decompressed, input);
40+
}
41+
42+
@Test(timeOut = 5_000)
43+
public void testDecompressTruncatedInputThrowsQuickly() {
44+
byte[] compressed = compressor.compress(sampleInput(), 0, sampleInput().length);
45+
byte[] truncated = Arrays.copyOf(compressed, compressed.length - 1);
46+
InvalidDataException e =
47+
Assert.expectThrows(
48+
InvalidDataException.class,
49+
() -> compressor.decompress(truncated, 0, truncated.length));
50+
assertTrue(e.getMessage().contains("truncated"));
51+
}
52+
53+
@Test(timeOut = 5_000)
54+
public void testDecompressCorruptedInputThrows() {
55+
byte[] compressed = compressor.compress(sampleInput(), 0, sampleInput().length);
56+
byte[] corrupted = Arrays.copyOf(compressed, compressed.length);
57+
corrupted[corrupted.length / 2] ^= 0x40;
58+
InvalidDataException e =
59+
Assert.expectThrows(
60+
InvalidDataException.class,
61+
() -> compressor.decompress(corrupted, 0, corrupted.length));
62+
assertTrue(e.getMessage().contains("Invalid compressed metadata"));
63+
}
64+
65+
private static byte[] sampleInput() {
66+
return "0123456789abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz"
67+
.getBytes(StandardCharsets.UTF_8);
68+
}
69+
}

0 commit comments

Comments
 (0)