Skip to content

throw instead of hanging on incomplete compressed input - #77

Open
knQzx wants to merge 1 commit into
dynatrace-oss:mainfrom
knQzx:fix/decompress-truncated-input
Open

throw instead of hanging on incomplete compressed input#77
knQzx wants to merge 1 commit into
dynatrace-oss:mainfrom
knQzx:fix/decompress-truncated-input

Conversation

@knQzx

@knQzx knQzx commented Jul 31, 2026

Copy link
Copy Markdown

SerializationUtil.decompress drives the inflater with only finished() as the stop condition:

Inflater inflater = new Inflater();
inflater.setInput(data);
byte[] buffer = new byte[1024];
while (!inflater.finished()) {
  outputStream.write(buffer, 0, inflater.inflate(buffer));
}

inflate returns 0 when it needs more input, and finished() stays false in that case, so an empty or truncated payload makes this spin at full CPU forever. it never throws the DataFormatException the method already declares, so a caller cannot recover either

it is reachable from three public entry points that take a caller-supplied array: readCompressedAsStatic, readCompressedAsDynamic and readCompressedAsPreprocessed. a short read from a socket or a partially written file is enough

worth noting: random bytes do throw, because the zlib header check catches them. the hang needs either an empty array or a stream that starts correctly and is cut short, for example Arrays.copyOf(writeCompressed(h), 3)

changes

bail out with DataFormatException when the inflater made no progress and is waiting for input or a dictionary

testing

two tests, one with an empty array and one with a truncated stream, both wrapped in assertTimeoutPreemptively since a plain assertThrows would wedge the runner. both fail with a 5 second timeout before the change and pass after

I could not use gradle locally, its toolchain wants a jdk newer than I have, so I compiled the sources with javac and ran the test class through the junit console launcher: 9 tests pass with the change, 7 pass and 2 time out without it. formatting checked against google-java-format 1.34.1, the version spotless pins

The inflate loop only stopped once the stream was finished. Inflater.inflate
returns 0 when it needs more input, and finished() stays false in that case, so
an empty or truncated payload spun forever at full CPU instead of throwing the
DataFormatException the method already declares.

Reachable from readCompressedAsStatic, readCompressedAsDynamic and
readCompressedAsPreprocessed, which take a caller-supplied byte array.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant