From bf8d2c795475f9c205b5017892217033a19d465f Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 2 Jun 2025 18:05:25 +0100 Subject: [PATCH] Fix decompression of empty messages with a ratio limit Motivation: The decompressor has a decompression limit to protect against zip bombs. This can either be absolute or ratio based. It's also possible in gRPC for a zero length message to be marked as compressed. gRPC attempts to decompress the zero length message and fails (because zlib wants a non-zero sized buffer and gRPC won't give it one as the limit is the buffer size is limited by the `ratio * msg_size` which in this case is zero). Modifications: - If the input to decompress has no length, skip decompression altogether Result: - Can decompress zero length payloads with the ratio limit - Resolves #2245 --- Sources/GRPC/Compression/Zlib.swift | 6 ++++++ Tests/GRPCTests/ZlibTests.swift | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/Sources/GRPC/Compression/Zlib.swift b/Sources/GRPC/Compression/Zlib.swift index 4186f1f9e..0c21d685e 100644 --- a/Sources/GRPC/Compression/Zlib.swift +++ b/Sources/GRPC/Compression/Zlib.swift @@ -265,6 +265,12 @@ enum Zlib { /// - Returns: The number of bytes written into `output`. @discardableResult func inflate(_ input: inout ByteBuffer, into output: inout ByteBuffer) throws -> Int { + if input.readableBytes == 0 { + // Zero length compressed messages are always empty messages. Skip the inflate step + // below and just return the number of bytes we wrote. + return 0 + } + return try input.readWithUnsafeMutableReadableBytes { inputPointer -> (Int, Int) in // Setup the input buffer. self.stream.availableInputBytes = inputPointer.count diff --git a/Tests/GRPCTests/ZlibTests.swift b/Tests/GRPCTests/ZlibTests.swift index 5ee37be52..436f7ebdd 100644 --- a/Tests/GRPCTests/ZlibTests.swift +++ b/Tests/GRPCTests/ZlibTests.swift @@ -207,4 +207,16 @@ class ZlibTests: GRPCTestCase { let ratio: DecompressionLimit = .ratio(2) XCTAssertEqual(ratio.maximumDecompressedSize(compressedSize: 10), 20) } + + func testDecompressEmptyPayload() throws { + for limit in [DecompressionLimit.ratio(1), .absolute(1)] { + for format in [Zlib.CompressionFormat.deflate, .gzip] { + var compressed = self.allocator.buffer(capacity: 0) + let inflate = Zlib.Inflate(format: format, limit: limit) + var output = self.allocator.buffer(capacity: 0) + XCTAssertEqual(try inflate.inflate(&compressed, into: &output), 0) + XCTAssertEqual(output.readableBytes, 0) + } + } + } }