When a multithreaded frame (`zstdmt` feature, `CParameter::NbWorkers`) is
left unfinished, its jobs may still be running on the worker threads: the C
library only waits for them when the next frame starts, or when the context
is freed. A session reset makes the context accept changes again in the
meantime, so:
- unloading the dictionary (`load_dictionary`, `ref_cdict`, `ref_prefix`,
`disable_dictionary`, `init*`, or `reset` with parameters) freed the
dictionary a worker was still compressing against;
- changing `NbWorkers` made the next frame resize the job table before
waiting for the old jobs.
The flaw was corrected by refusing these calls while a multithreaded frame
is unfinished. The new `CCtx::wait_for_workers()` waits for the workers
first.
`InBuffer`'s fields are public, so safe code can set `pos` past the end of
`src`. The position was passed to the C library unchecked, and
`ZSTD_seekable_compressStream`, unlike the other streaming functions, does
not validate it: it read past the end of the input slice, by up to the
frame size, and compressed that memory into the output.
The flaw was corrected by checking the position before every call into the
C library.
`DCtx::try_clone` (`experimental` feature, deprecated) is a plain copy of the
C context. A context owning a dictionary (from `load_dictionary`,
`ref_prefix` or `init_using_dict`) or a dictionary table (`ref_ddict` with
`RefMultipleDDicts`) ended up with both copies freeing it. A context cloned
mid-frame resumed on uninitialized stream state. The clone also silently lost
the `WindowLogMax` limit.
The flaw was corrected by refusing to clone such contexts, and by carrying
over the parameters the copy leaves out.
When decompression fails inside a legacy (zstd v0.4 to v0.7) frame, the
bundled zstd library reports the output buffer as fully written although
nothing was written. `DCtx::decompress_stream` committed that position even
on error, and for a `Vec<u8>` destination this extended the vector's length
over uninitialized memory.
Legacy support is enabled by default. A 5- or 6-byte corrupt legacy frame is
enough: through `zstd::stream::write::Decoder`, up to 32 KiB of stale heap
memory is then passed to the wrapped writer on the next `write()` or
`flush()`, or when an `auto_flush()`/`on_flush()` decoder is dropped. Whether
this reaches an attacker depends on where the application sends the
decompressed output. `zstd::stream::read::Decoder`, `zstd::decode_all` and
the bulk API are not affected.
Before zstd-safe 4.0.0, output buffers could only be initialized slices.
The zstd crate's own writer had the same flaw from 0.4.22 to 0.7.x, which is
covered by a separate advisory.
The flaw was corrected by only committing the buffer positions of a
successful call.
`CCtx::try_clone` (`experimental` feature, deprecated) copies tables sized
from the context's applied parameters. Starting a multithreaded frame
(`zstdmt` feature) replaces those parameters with the workers' without
resizing the context's own tables, so after an abandoned single-threaded
frame, the copy wrote up to 512 KiB past a heap allocation, or read past
one. This became reachable with the bundled zstd 1.5.6.
The flaw was corrected by refusing `try_clone` (and the unsafe
`compress_block`) once `NbWorkers` was set on the context.
`ZDICT_trainFromBuffer` is never told the length of the samples buffer, so
`train_from_buffer` checked that the sample sizes add up to it. With
overflow checks off, as in release builds, that sum wrapped around: sizes
totalling 2^64 plus the buffer length passed the check, and the trainer read
past the end of the buffer. `zstd::dict::from_continuous` relies on this
check from zstd 0.4.10; older versions of zstd had their own, covered by a
separate advisory.
This requires the sample sizes to come from an untrusted source. The flaw
was corrected by summing the sizes with overflow checks.
The one-shot `DCtx` and `CCtx` methods assumed the C library resets the
context, but it leaves an open stream as it is. When a stream was left
unfinished on a context, a one-shot call such as `decompress_using_dict`
could leave the context pointing at the dictionary borrowed for that call,
and the next streaming call resumed the stream against it after it was
freed: decompressed output then contained freed heap memory. On the
compression side, the stream resumed against the one-shot call's buffers.
This requires an application to use one context for both streaming and
one-shot calls without a reset in between. The flaw was corrected by
resetting the session after one-shot calls whenever a stream may be open.
`CCtx::init_using_cdict` and `DCtx::init_using_ddict` (`experimental`
feature, deprecated) took the dictionary as `&CDict<'b>` and `&DDict<'b>`
with only `'b: 'a`, so the dictionary itself could be dropped while the
context still pointed at it. The same mistake in other constructors was
fixed earlier (<https://github.com/gyscos/zstd-rs/issues/365>).
The flaw was corrected by taking `&'a CDict<'b>` and `&'a DDict<'b>`.
`ZSTD_seekable_decompress` shortens the requested length to the end of the
data without checking that the offset is before it. For an offset past the
end, it returns a wrapped-around length without writing anything, and
`Seekable::decompress` passed that length to `Vec::set_len`.
With the `seekable` feature, whoever supplies the archive, or chooses the
offset, can leave the caller's `Vec<u8>` with a length far beyond its
capacity. Reading it then discloses neighbouring heap memory, and appending
to it corrupts memory. Offsets near `u64::MAX` can also hang the call.
The flaw was corrected by clamping requests to the end of the data, and by
checking every count reported by the C library against the buffer capacity.
`OutBuffer::around_pos` accepted a position past the initialized length of a
`Vec<u8>`. The next streaming call then marked everything before the
position as initialized, so safe code could read stale heap memory between
the vector's previous length and that position.
The flaw was corrected by zero-filling that gap.
`Seekable` did not record whether it was initialized. On a fresh `Seekable`,
or one whose `init_buff()` failed, `decompress`, `offset_to_frame_index` and
`frame_decompressed_size` made the C library read a NULL seek table. After a
failed re-initialization, `decompress` returned data from the refused buffer
using the previous seek table.
Separately, `frame_decompressed_size(num_frames())`, on `Seekable` and
`SeekTable`, read one entry past the end of the seek table.
The flaw was corrected by refusing to use a `Seekable` until an
initialization succeeded, and by checking the frame index.
With `CParameter::StableInBuffer(true)` (`experimental` feature), the C
library keeps a pointer to the input of one streaming call and reads from it
in later calls, including `flush_stream` and `end_stream`, which take no
input. An `InBuffer` only borrows its slice for one call, so safe code could
free it in between, and freed memory was compressed into the frame. The C
library behaves this way since zstd 1.5.4, whether bundled or linked from the
system.
The flaw was corrected by refusing to enable the parameter.