Refuse a frame this decoder cannot account for
Three ways a header was taken on trust. Frame_Content_Size is up to a 64-bit number straight from the input and it sized the output buffer unchecked, so a sixteen-byte frame could ask for four exbibytes. It is reachable by accident: one bit flipped in a real tile's frame descriptor moves the content-size flag from 2 to 3, eight bytes of compressed data are read as the size, and the eighth lands on the sign bit. That is a RangeError or an OutOfMemoryError, both Errors rather than Exceptions, so a caller writing `on ZstdException` catches neither and the isolate dies. There is a ceiling now, and a caller that passes expectedSize gets an exact check instead, which is what the .hgtz reader wants since a block is height * width * 2. Reading one frame is the whole API, and nothing compared the cursor with the input length, so a second frame, a trailing skippable frame and four bytes of junk were accepted and silently ignored. Two frames concatenated returned the first one's bytes and raised nothing. The checksum's four bytes were stepped over rather than read, so a frame could claim one and supply none. And an RLE block was exempt from the block size ceiling, which let 97 bytes return 44 MB that a caller cannot tell from a good tile.
This commit is contained in:
parent
489c898b14
commit
09b06ca9a0
1 changed files with 56 additions and 3 deletions
|
|
@ -12,8 +12,35 @@ import 'tables.dart';
|
||||||
/// One instance is not safe to share between isolates, and a frame must be
|
/// One instance is not safe to share between isolates, and a frame must be
|
||||||
/// decoded to completion before the next one starts.
|
/// decoded to completion before the next one starts.
|
||||||
class ZstdDecoder {
|
class ZstdDecoder {
|
||||||
|
ZstdDecoder({this.maxOutputSize = defaultMaxOutputSize});
|
||||||
|
|
||||||
static const int magic = 0xFD2FB528;
|
static const int magic = 0xFD2FB528;
|
||||||
|
|
||||||
|
/// The largest output this decoder will allocate for one frame.
|
||||||
|
///
|
||||||
|
/// `Frame_Content_Size` is up to a 64-bit number taken straight from the
|
||||||
|
/// input and it used to size the output buffer unchecked, so a sixteen-byte
|
||||||
|
/// frame could ask for four exbibytes. It is reachable by accident as well
|
||||||
|
/// as by design: one bit flipped in a real tile's frame descriptor moves
|
||||||
|
/// the content-size flag from 2 to 3, eight bytes of compressed data are
|
||||||
|
/// read as the size, and the eighth lands on the sign bit. That is a
|
||||||
|
/// `RangeError` for a negative length, or an `OutOfMemoryError` for a large
|
||||||
|
/// positive one, and both are `Error` rather than `Exception`, so a caller
|
||||||
|
/// writing `on ZstdException` does not catch either and the isolate dies.
|
||||||
|
///
|
||||||
|
/// RFC 8878 section 3.1.1.1.2 lets a decoder refuse a frame that asks for
|
||||||
|
/// more memory than it is willing to give, which is the job of the window
|
||||||
|
/// descriptor this package skips. Allocating the output whole is why it can
|
||||||
|
/// skip it, and this is the bound that replaces it.
|
||||||
|
///
|
||||||
|
/// A caller that knows the exact size should pass [decode]'s `expectedSize`
|
||||||
|
/// instead and get an exact check rather than a ceiling. The `.hgtz` reader
|
||||||
|
/// knows it: a block is `height * width * 2`.
|
||||||
|
static const int defaultMaxOutputSize = 64 * 1024 * 1024;
|
||||||
|
|
||||||
|
/// The ceiling this instance enforces. See [defaultMaxOutputSize].
|
||||||
|
final int maxOutputSize;
|
||||||
|
|
||||||
/// The largest block a frame may contain, and so the most literals one
|
/// The largest block a frame may contain, and so the most literals one
|
||||||
/// block can hold.
|
/// block can hold.
|
||||||
static const int maxBlockSize = 128 * 1024;
|
static const int maxBlockSize = 128 * 1024;
|
||||||
|
|
@ -98,6 +125,17 @@ class ZstdDecoder {
|
||||||
'frame declares no content size and none was given',
|
'frame declares no content size and none was given',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// Before the allocation, and before any of it is trusted.
|
||||||
|
if (size < 0 || size > maxOutputSize) {
|
||||||
|
throw ZstdException('frame declares $size bytes, over the maximum');
|
||||||
|
}
|
||||||
|
// When the caller knows the size, the frame agreeing with it is the
|
||||||
|
// check. Left to the end, a frame declaring less decoded its own
|
||||||
|
// content happily and a frame declaring more allocated whatever it
|
||||||
|
// asked for first.
|
||||||
|
if (contentSize != null && expectedSize != null && size != expectedSize) {
|
||||||
|
throw ZstdException('frame declares $size bytes, not $expectedSize');
|
||||||
|
}
|
||||||
final out = Uint8List(size);
|
final out = Uint8List(size);
|
||||||
|
|
||||||
_huffman.log = 0;
|
_huffman.log = 0;
|
||||||
|
|
@ -118,7 +156,10 @@ class ZstdDecoder {
|
||||||
final last = header & 1;
|
final last = header & 1;
|
||||||
final type = (header >> 1) & 3;
|
final type = (header >> 1) & 3;
|
||||||
final blockSize = header >> 3;
|
final blockSize = header >> 3;
|
||||||
if (blockSize > maxBlockSize && type != 1) {
|
// Every type, RLE included. Its Block_Size is its regenerated size and
|
||||||
|
// the format bounds it like any other, and exempting it let a 97-byte
|
||||||
|
// input return 44 MB that a caller cannot tell from a good tile.
|
||||||
|
if (blockSize > maxBlockSize) {
|
||||||
throw ZstdException('block of $blockSize bytes is over the maximum');
|
throw ZstdException('block of $blockSize bytes is over the maximum');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -153,13 +194,25 @@ class ZstdDecoder {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (checksum == 1) {
|
if (checksum == 1) {
|
||||||
// Parsed so the frame is consumed whole, but not verified. Every block
|
// Not verified yet. Required to be present, though: stepping the
|
||||||
// in a .hgtz is its own frame and pyzstd writes none.
|
// cursor over four bytes that were never there let a frame claim a
|
||||||
|
// checksum and supply none.
|
||||||
|
if (at + 4 > src.length) {
|
||||||
|
throw const ZstdException('truncated content checksum');
|
||||||
|
}
|
||||||
at += 4;
|
at += 4;
|
||||||
}
|
}
|
||||||
if (written != out.length) {
|
if (written != out.length) {
|
||||||
throw ZstdException('frame decoded to $written bytes, not ${out.length}');
|
throw ZstdException('frame decoded to $written bytes, not ${out.length}');
|
||||||
}
|
}
|
||||||
|
// Reading one frame is the whole API, so anything after it is input this
|
||||||
|
// decoder does not understand. Nothing used to compare the cursor with
|
||||||
|
// the length, so a second frame, a trailing skippable frame, and four
|
||||||
|
// bytes of junk were all accepted and silently ignored: two frames
|
||||||
|
// concatenated returned the first one's bytes and raised nothing.
|
||||||
|
if (at != src.length) {
|
||||||
|
throw ZstdException('${src.length - at} bytes after the frame');
|
||||||
|
}
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue