Hi!

I’ve contacted Yann Collet (developer of ZSTD) and told him about our 
discussion. Here is his comment:

> Hi Daniil
>       • Is this an expected behavior of ZSTD to consume more memory during 
> the decompression of data that was compressed with a high compression ratio?
>  
> I assume that the target application is employing the streaming mode.
> In which case, yes, the memory usage is directly dependent on the Window 
> size, and the Window size tend to increase with compression level.
>  
>       • how we can restrict the maximal memory usage during decompression?
>  
> There are several ways.
>  
>       • From a decompression perspective
>  
> the first method is to _not_ use the streaming mode,
> and employ the direct buffer-to-buffer compression instead,
> like ZSTD_decompress() for example.
> In which case, the decompressor will not need additional memory, it will only 
> employ the provided buffers.
>  
> This however entirely depends on the application and can therefore be 
> unpractical.
> It’s fine when decompressing small blocks, it’s not when decompressing 
> gigantic streams of data.
>  
> The second method is more straightforward : set a limit to the window size 
> that the decoder accepts to decode.
> This is the ZSTD_d_windowLogMax parameter, documented here : 
> https://github.com/facebook/zstd/blob/v1.4.7/lib/zstd.h#L536
>  
> This can be set to any arbitrary power of 2 limit.
> A frame requiring more than this value will be rejected by the decoder, 
> precisely to avoid sustaining large memory requirements.
>  
> Lastly, note that, in presence of a large window size requirement, the 
> decoder will allocate a correspondingly large buffer,
> but will not necessarily use it.
> For example, if a frame generated with streaming mode at level 22 declares a 
> 128 MB window size, but effectively only contains ~200 KB of data,
> the buffer will only use 200 KB.
> The rest of the buffer is “allocated” from an address space perspective but 
> is not “used” and therefore does not really occupy physical RAM space.
> This is a capability of all modern OS and contributes to minimizing the 
> impact of outsized window sizes.
>  
>  
>       • From a compression perspective
>  
> Knowing the set limitation, the compressor should be compliant, and avoid 
> going above the threshold.
> One way to do it is to limit the compression level to those which remain 
> below the set limit.
> For example, if the limit is 8 MB, all levels <= 19 will be compatible, as 
> they require 8 MB max (and generally less).
>  
> Another method is to manually set a window size, so that it doesn’t exceed 
> the limit.
> This is the ZSTD_c_windowLog parameter, which is documented here : 
> https://github.com/facebook/zstd/blob/v1.4.7/lib/zstd.h#L289
>  
> Another complementary way is to provide the source size when it’s known.
> By default, the streaming mode doesn’t know the input size, since it’s 
> supposed to receive it in multiple blocks.
> It will only discover it at the end, by which point it’s too late to use this 
> information in the frame header.
> This can be solved, by providing the source size upfront, before starting 
> compression.
> This is the function ZSTD_CCtx_setPledgedSrcSize(), documented here : 
> https://github.com/facebook/zstd/blob/v1.4.7/lib/zstd.h#L483
> Of course, then the total amount of data in the frame must be exact, 
> otherwise it’s detected as an error.
>  
> Taking again the previous example of compressing 200 KB with level 22, on 
> knowing the source size,
> the compressor will resize the window to fit the input, and therefore employ 
> 200 KB, instead of 128 MB.
> This information will be present in the header, and the decompressor will 
> also be able to use 200 KB instead of 128 MB.
> Also, presuming the decompressor has a hard limit set to 8 MB (for example), 
> the header using a 200 KB window size will pass and be properly decoded, 
> while the header using 128 MB will be rejected.
> This method is cumulative with the one setting a manual window size (the 
> compressor will select the smallest of both).
>  
>  
> So yes, memory consumption is a serious topic, and there are tools in the 
> `zstd` library to deal with it.
>  
>  
> Hope it helps
>  
> Best Regards
>  
> Yann Collet

After reading Yann’s advice I repeated yesterday single-directional 
decompression benchmarks with ZSTD_d_windowLogMax set to 23, i.e 8MB max window 
size.

Total committed memory (Committed_AS) size for ZSTD compression levels 1-19 was 
pretty much the same:

Committed_AS baseline (size without any benchmark running) - 42.4 GiB

Scenario                        Committed_AS    Committed_AS - Baseline
no compression  44,36 GiB               1,05 GiB
ZSTD:1                  45,03 GiB               1,06 GiB
ZSTD:5                  46,06 GiB               1,09 GiB
ZSTD:9                  46,00 GiB               1,08 GiB
ZSTD:13                 47,46 GiB               1,12 GiB
ZSTD:17                 50,23 GiB               1,18 GiB
ZSTD:19                 50,21 GiB               1,18 GiB

As for ZSTD levels higher than 19, decompressor returned the appropriate error 
(excerpt from PostgreSQL server log):
LOG:  failed to decompress data: Frame requires too much memory for decoding

Full benchmark report: 
https://docs.google.com/document/d/1LI8hPzMkzkdQLf7pTN-LXPjIJdjN33bEAqVJj0PLnHA
Pull request with max window size limit: 
https://github.com/postgrespro/libpq_compression/pull/5

This should fix the possible attack vectors related to high ZSTD compression 
levels.

—
Daniil Zakhlystov

Reply via email to