Conversation
… malformed blocks
…ity with all compression levels
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR fixes a bug in the built-in RLE decoder (
ntcd::CompressionDecoder), where a peer-controlled frame length can underflow the decoder's internal state. It also adds configurable per-operation limits on inflated and deflated output, honored by the RLE decoder and by the zlib, gzip, LZ4 and Zstd plugins. Applying those limits across the plugins uncovered several existing defects in their error handling, which are fixed here, including a remotely triggerable infinite loop in the Zstd inflater.Problem
RLE frame length underflow (reported).
ntcd::CompressionDecoder::process()loaded the 32-bit frame length from the frame header intod_frameContentBytesNeeded. It then subtracted block-header and RAW payload sizes from it without checking bounds:Once the counter wrapped, the decoder never reached the frame footer and never verified the checksum. It kept expanding RLE blocks indefinitely: up to 65,535 output bytes per 4 input bytes, all appended to the caller's blob. Even well-formed frames had no output cap: a single frame could legitimately inflate to about 70 TB before its checksum was checked.
No decompression limits. None of the compression backends limited how much output one operation could produce. A small compressed read could expand into an arbitrarily large append (a decompression bomb).
Changes
ntca::CompressionConfigmaxInflateSizeandmaxDeflateSize: the maximum number of bytes a single inflate or deflate operation may produce. An operation that would exceed the limit fails withntsa::Error::e_LIMIT.INT_MAX.compressionConfigget them without any change to socket code.ntcd::CompressionDecoder/ntcd::CompressionEncoder(RLE)maxInflateSizebefore each RLE expansion and each RAW payload is added to the result.maxDeflateSizebefore adding the frame header, each block and the footer.INT_MAXbytes.ntctlcplugin (zlib, gzip, LZ4, Zstd)inflateFail/deflateFailhelpers. They discard pending output and stream state, and the inflate version also resets the decompression library. This fixes three existing defects:next_inset after an error, so the next call trippedBSLS_ASSERT(d_inflaterStream.next_in == 0). In builds with assertions enabled, one corrupt frame could crash the process.inflateNextanddeflateEndloops treated every non-zerosize_treturn as "continue", including Zstd error codes. Malformed input made the inflater spin forever; reproduced with 31 bytes of garbage. Errors are now checked first.uInt.ntci::Compressioninflate(..., const ntsa::Data&, ...)never checked the error from itsinflateRepcall; it went on toinflateEndand overwrote the error with that call's result. Every inflate error on that overload was reported as success. It now returns the error, as the matchingdeflateoverload already did. Thebdlbb::Bloboverload, which the sockets use, was not affected.Behaviour changes
e_LIMIT, but only when a limit is configured or the result blob would exceedINT_MAXbytes.ntsa::Datainflate callers now receive errors that were previously dropped.Testing
ntcd_compression.t:verifyMalformed:verifyLimits:INT_MAXdefaults.ntctlc_plugin.t: runs for every compiled-in algorithm.verifyLimits:ntsa::Databuffer array (a regression test for thentcifix).verifyMalformed: garbage input returnse_INVALID, and a later well-formed frame decodes, repeated twice.ntsa::Dataregression test fails without thentcifix.ntcd_compression.tandntctlc_plugin.tcases pass. The plugin'sverifyAllsweep was also run temporarily across all four algorithms (it is normally pinned to LZ4 byNTCTLC_PLUGIN_TEST_COMPRESSION_TYPE).Known limitations and follow-ups
e_BEST_SIZE, so it is left to a follow-up, possibly as an opt-in configuration setting.compressionConfigdefaults are applied only when a socket's owncompressionConfigis unset. A socket that setscompressionConfigwith onlytypewill not inherit an interface-levelmaxInflateSize. This is existing behaviour.