|
@@ -0,0 +1,326 @@
|
|
|
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
|
|
+From: Adam Rice <[email protected]>
|
|
|
+Date: Fri, 4 Dec 2020 10:19:12 +0000
|
|
|
+Subject: Correctly handle detach during (de)compression
|
|
|
+
|
|
|
+Sometimes CompressionStream and DecompressionStream enqueue multiple
|
|
|
+output chunks for a single input chunk. When this happens, JavaScript
|
|
|
+code can detach the input ArrayBuffer while the stream is processing it.
|
|
|
+This will cause an error when zlib tries to read the buffer again
|
|
|
+afterwards.
|
|
|
+
|
|
|
+To prevent this, buffer output chunks until the entire input chunk has
|
|
|
+been processed, and then enqueue them all at once.
|
|
|
+
|
|
|
+Bug: 1151298
|
|
|
+Change-Id: I03fca26fc641d54b09067e3994b76ee8efca6839
|
|
|
+Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2567539
|
|
|
+Commit-Queue: Adam Rice <[email protected]>
|
|
|
+Reviewed-by: Yutaka Hirano <[email protected]>
|
|
|
+Cr-Commit-Position: refs/heads/master@{#833659}
|
|
|
+
|
|
|
+diff --git a/third_party/blink/renderer/modules/compression/deflate_transformer.cc b/third_party/blink/renderer/modules/compression/deflate_transformer.cc
|
|
|
+index e663e563d58e90cdce5185636d1b1aa75491195c..e806b84afbb6cd82c53832e0c0b455ae0767a34a 100644
|
|
|
+--- a/third_party/blink/renderer/modules/compression/deflate_transformer.cc
|
|
|
++++ b/third_party/blink/renderer/modules/compression/deflate_transformer.cc
|
|
|
+@@ -111,6 +111,10 @@ void DeflateTransformer::Deflate(const uint8_t* start,
|
|
|
+ // Zlib treats this pointer as const, so this cast is safe.
|
|
|
+ stream_.next_in = const_cast<uint8_t*>(start);
|
|
|
+
|
|
|
++ // enqueue() may execute JavaScript which may invalidate the input buffer. So
|
|
|
++ // accumulate all the output before calling enqueue().
|
|
|
++ HeapVector<Member<DOMUint8Array>, 1u> buffers;
|
|
|
++
|
|
|
+ do {
|
|
|
+ stream_.avail_out = out_buffer_.size();
|
|
|
+ stream_.next_out = out_buffer_.data();
|
|
|
+@@ -120,16 +124,21 @@ void DeflateTransformer::Deflate(const uint8_t* start,
|
|
|
+
|
|
|
+ wtf_size_t bytes = out_buffer_.size() - stream_.avail_out;
|
|
|
+ if (bytes) {
|
|
|
+- controller->enqueue(
|
|
|
+- script_state_,
|
|
|
+- ScriptValue::From(script_state_,
|
|
|
+- DOMUint8Array::Create(out_buffer_.data(), bytes)),
|
|
|
+- exception_state);
|
|
|
+- if (exception_state.HadException()) {
|
|
|
+- return;
|
|
|
+- }
|
|
|
++ buffers.push_back(DOMUint8Array::Create(out_buffer_.data(), bytes));
|
|
|
+ }
|
|
|
+ } while (stream_.avail_out == 0);
|
|
|
++
|
|
|
++ DCHECK_EQ(stream_.avail_in, 0u);
|
|
|
++
|
|
|
++ // JavaScript may be executed inside this loop, however it is safe because
|
|
|
++ // |buffers| is a local variable that JavaScript cannot modify.
|
|
|
++ for (DOMUint8Array* buffer : buffers) {
|
|
|
++ controller->enqueue(script_state_, ScriptValue::From(script_state_, buffer),
|
|
|
++ exception_state);
|
|
|
++ if (exception_state.HadException()) {
|
|
|
++ return;
|
|
|
++ }
|
|
|
++ }
|
|
|
+ }
|
|
|
+
|
|
|
+ void DeflateTransformer::Trace(Visitor* visitor) const {
|
|
|
+diff --git a/third_party/blink/renderer/modules/compression/inflate_transformer.cc b/third_party/blink/renderer/modules/compression/inflate_transformer.cc
|
|
|
+index f348a5086e4254bc972dd321109d9fbf676f1d5e..29667b5752250466cd71542ab8868d6e14961e81 100644
|
|
|
+--- a/third_party/blink/renderer/modules/compression/inflate_transformer.cc
|
|
|
++++ b/third_party/blink/renderer/modules/compression/inflate_transformer.cc
|
|
|
+@@ -20,6 +20,7 @@
|
|
|
+ #include "third_party/blink/renderer/platform/bindings/exception_state.h"
|
|
|
+ #include "third_party/blink/renderer/platform/bindings/to_v8.h"
|
|
|
+ #include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
|
|
|
++#include "third_party/blink/renderer/platform/wtf/vector.h"
|
|
|
+ #include "v8/include/v8.h"
|
|
|
+
|
|
|
+ namespace blink {
|
|
|
+@@ -93,11 +94,15 @@ ScriptPromise InflateTransformer::Flush(
|
|
|
+ TransformStreamDefaultController* controller,
|
|
|
+ ExceptionState& exception_state) {
|
|
|
+ DCHECK(!was_flush_called_);
|
|
|
++ was_flush_called_ = true;
|
|
|
+ Inflate(nullptr, 0u, IsFinished(true), controller, exception_state);
|
|
|
+ inflateEnd(&stream_);
|
|
|
+- was_flush_called_ = true;
|
|
|
+ out_buffer_.clear();
|
|
|
+
|
|
|
++ if (exception_state.HadException()) {
|
|
|
++ return ScriptPromise();
|
|
|
++ }
|
|
|
++
|
|
|
+ if (!reached_end_) {
|
|
|
+ exception_state.ThrowTypeError("Compressed input was truncated.");
|
|
|
+ }
|
|
|
+@@ -121,12 +126,22 @@ void InflateTransformer::Inflate(const uint8_t* start,
|
|
|
+ // Zlib treats this pointer as const, so this cast is safe.
|
|
|
+ stream_.next_in = const_cast<uint8_t*>(start);
|
|
|
+
|
|
|
++ // enqueue() may execute JavaScript which may invalidate the input buffer. So
|
|
|
++ // accumulate all the output before calling enqueue().
|
|
|
++ HeapVector<Member<DOMUint8Array>, 1u> buffers;
|
|
|
++
|
|
|
+ do {
|
|
|
+ stream_.avail_out = out_buffer_.size();
|
|
|
+ stream_.next_out = out_buffer_.data();
|
|
|
+ const int err = inflate(&stream_, finished ? Z_FINISH : Z_NO_FLUSH);
|
|
|
+ if (err != Z_OK && err != Z_STREAM_END && err != Z_BUF_ERROR) {
|
|
|
+ DCHECK_NE(err, Z_STREAM_ERROR);
|
|
|
++
|
|
|
++ EnqueueBuffers(controller, std::move(buffers), exception_state);
|
|
|
++ if (exception_state.HadException()) {
|
|
|
++ return;
|
|
|
++ }
|
|
|
++
|
|
|
+ if (err == Z_DATA_ERROR) {
|
|
|
+ exception_state.ThrowTypeError(
|
|
|
+ String("The compressed data was not valid: ") + stream_.msg + ".");
|
|
|
+@@ -138,25 +153,44 @@ void InflateTransformer::Inflate(const uint8_t* start,
|
|
|
+
|
|
|
+ wtf_size_t bytes = out_buffer_.size() - stream_.avail_out;
|
|
|
+ if (bytes) {
|
|
|
+- controller->enqueue(
|
|
|
+- script_state_,
|
|
|
+- ScriptValue::From(script_state_,
|
|
|
+- DOMUint8Array::Create(out_buffer_.data(), bytes)),
|
|
|
+- exception_state);
|
|
|
+- if (exception_state.HadException()) {
|
|
|
+- return;
|
|
|
+- }
|
|
|
++ buffers.push_back(DOMUint8Array::Create(out_buffer_.data(), bytes));
|
|
|
+ }
|
|
|
+
|
|
|
+ if (err == Z_STREAM_END) {
|
|
|
+ reached_end_ = true;
|
|
|
+- if (stream_.next_in < start + length) {
|
|
|
++ const bool junk_found = stream_.avail_in > 0;
|
|
|
++
|
|
|
++ EnqueueBuffers(controller, std::move(buffers), exception_state);
|
|
|
++ if (exception_state.HadException()) {
|
|
|
++ return;
|
|
|
++ }
|
|
|
++
|
|
|
++ if (junk_found) {
|
|
|
+ exception_state.ThrowTypeError(
|
|
|
+ "Junk found after end of compressed data.");
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ } while (stream_.avail_out == 0);
|
|
|
++
|
|
|
++ DCHECK_EQ(stream_.avail_in, 0u);
|
|
|
++
|
|
|
++ EnqueueBuffers(controller, std::move(buffers), exception_state);
|
|
|
++}
|
|
|
++
|
|
|
++void InflateTransformer::EnqueueBuffers(
|
|
|
++ TransformStreamDefaultController* controller,
|
|
|
++ HeapVector<Member<DOMUint8Array>, 1u> buffers,
|
|
|
++ ExceptionState& exception_state) {
|
|
|
++ // JavaScript may be executed inside this loop, however it is safe because
|
|
|
++ // |buffers| is a local variable that JavaScript cannot modify.
|
|
|
++ for (DOMUint8Array* buffer : buffers) {
|
|
|
++ controller->enqueue(script_state_, ScriptValue::From(script_state_, buffer),
|
|
|
++ exception_state);
|
|
|
++ if (exception_state.HadException()) {
|
|
|
++ return;
|
|
|
++ }
|
|
|
++ }
|
|
|
+ }
|
|
|
+
|
|
|
+ void InflateTransformer::Trace(Visitor* visitor) const {
|
|
|
+diff --git a/third_party/blink/renderer/modules/compression/inflate_transformer.h b/third_party/blink/renderer/modules/compression/inflate_transformer.h
|
|
|
+index 290e91808dfcc16ed7ff6ec9e6e42eb412dfc969..c5df437f1684e6ca1201d9b3d32dff19903a0b5e 100644
|
|
|
+--- a/third_party/blink/renderer/modules/compression/inflate_transformer.h
|
|
|
++++ b/third_party/blink/renderer/modules/compression/inflate_transformer.h
|
|
|
+@@ -41,6 +41,10 @@ class InflateTransformer final : public TransformStreamTransformer {
|
|
|
+ TransformStreamDefaultController*,
|
|
|
+ ExceptionState&);
|
|
|
+
|
|
|
++ void EnqueueBuffers(TransformStreamDefaultController*,
|
|
|
++ HeapVector<Member<DOMUint8Array>, 1u> buffers,
|
|
|
++ ExceptionState&);
|
|
|
++
|
|
|
+ Member<ScriptState> script_state_;
|
|
|
+
|
|
|
+ z_stream stream_;
|
|
|
+diff --git a/third_party/blink/web_tests/external/wpt/compression/compression-with-detach.tentative.any.js b/third_party/blink/web_tests/external/wpt/compression/compression-with-detach.tentative.any.js
|
|
|
+new file mode 100644
|
|
|
+index 0000000000000000000000000000000000000000..786bba21c800ca9f067a6d033f0345a52bfbb218
|
|
|
+--- /dev/null
|
|
|
++++ b/third_party/blink/web_tests/external/wpt/compression/compression-with-detach.tentative.any.js
|
|
|
+@@ -0,0 +1,55 @@
|
|
|
++// META: global=window,worker
|
|
|
++// META: script=resources/concatenate-stream.js
|
|
|
++
|
|
|
++'use strict';
|
|
|
++
|
|
|
++const kInputLength = 500000;
|
|
|
++
|
|
|
++function createLargeRandomInput() {
|
|
|
++ const buffer = new ArrayBuffer(kInputLength);
|
|
|
++ // The getRandomValues API will only let us get 65536 bytes at a time, so call
|
|
|
++ // it multiple times.
|
|
|
++ const kChunkSize = 65536;
|
|
|
++ for (let offset = 0; offset < kInputLength; offset += kChunkSize) {
|
|
|
++ const length =
|
|
|
++ offset + kChunkSize > kInputLength ? kInputLength - offset : kChunkSize;
|
|
|
++ const view = new Uint8Array(buffer, offset, length);
|
|
|
++ crypto.getRandomValues(view);
|
|
|
++ }
|
|
|
++ return new Uint8Array(buffer);
|
|
|
++}
|
|
|
++
|
|
|
++function decompress(view) {
|
|
|
++ const ds = new DecompressionStream('deflate');
|
|
|
++ const writer = ds.writable.getWriter();
|
|
|
++ writer.write(view);
|
|
|
++ writer.close();
|
|
|
++ return concatenateStream(ds.readable);
|
|
|
++}
|
|
|
++
|
|
|
++promise_test(async () => {
|
|
|
++ const input = createLargeRandomInput();
|
|
|
++ const inputCopy = input.slice(0, input.byteLength);
|
|
|
++ const cs = new CompressionStream('deflate');
|
|
|
++ const writer = cs.writable.getWriter();
|
|
|
++ writer.write(input);
|
|
|
++ writer.close();
|
|
|
++ // Object.prototype.then will be looked up synchronously when the promise
|
|
|
++ // returned by read() is resolved.
|
|
|
++ Object.defineProperty(Object.prototype, 'then', {
|
|
|
++ get() {
|
|
|
++ // Cause input to become detached and unreferenced.
|
|
|
++ try {
|
|
|
++ postMessage(undefined, 'nowhere', [input.buffer]);
|
|
|
++ } catch (e) {
|
|
|
++ // It's already detached.
|
|
|
++ }
|
|
|
++ }
|
|
|
++ });
|
|
|
++ const output = await concatenateStream(cs.readable);
|
|
|
++ // Perform the comparison as strings since this is reasonably fast even when
|
|
|
++ // JITted JavaScript is running under an emulator.
|
|
|
++ assert_equals(
|
|
|
++ inputCopy.toString(), (await decompress(output)).toString(),
|
|
|
++ 'decompressing the output should return the input');
|
|
|
++}, 'data should be correctly compressed even if input is detached partway');
|
|
|
+diff --git a/third_party/blink/web_tests/external/wpt/compression/decompression-with-detach.tentative.any.js b/third_party/blink/web_tests/external/wpt/compression/decompression-with-detach.tentative.any.js
|
|
|
+new file mode 100644
|
|
|
+index 0000000000000000000000000000000000000000..a2f8bda09148f0d323022b1f93be78d83c4aa654
|
|
|
+--- /dev/null
|
|
|
++++ b/third_party/blink/web_tests/external/wpt/compression/decompression-with-detach.tentative.any.js
|
|
|
+@@ -0,0 +1,41 @@
|
|
|
++// META: global=window,worker
|
|
|
++// META: script=resources/concatenate-stream.js
|
|
|
++
|
|
|
++'use strict';
|
|
|
++
|
|
|
++const kInputLength = 1000000;
|
|
|
++
|
|
|
++async function createLargeCompressedInput() {
|
|
|
++ const cs = new CompressionStream('deflate');
|
|
|
++ // The input has to be large enough that it won't fit in a single chunk when
|
|
|
++ // decompressed.
|
|
|
++ const writer = cs.writable.getWriter();
|
|
|
++ writer.write(new Uint8Array(kInputLength));
|
|
|
++ writer.close();
|
|
|
++ return concatenateStream(cs.readable);
|
|
|
++}
|
|
|
++
|
|
|
++promise_test(async () => {
|
|
|
++ const input = await createLargeCompressedInput();
|
|
|
++ const ds = new DecompressionStream('deflate');
|
|
|
++ const writer = ds.writable.getWriter();
|
|
|
++ writer.write(input);
|
|
|
++ writer.close();
|
|
|
++ // Object.prototype.then will be looked up synchronously when the promise
|
|
|
++ // returned by read() is resolved.
|
|
|
++ Object.defineProperty(Object.prototype, 'then', {
|
|
|
++ get() {
|
|
|
++ // Cause input to become detached and unreferenced.
|
|
|
++ try {
|
|
|
++ postMessage(undefined, 'nowhere', [input.buffer]);
|
|
|
++ } catch (e) {
|
|
|
++ // It's already detached.
|
|
|
++ }
|
|
|
++ }
|
|
|
++ });
|
|
|
++ const output = await concatenateStream(ds.readable);
|
|
|
++ // If output successfully decompressed and gave the right length, we can be
|
|
|
++ // reasonably confident that no data corruption happened.
|
|
|
++ assert_equals(
|
|
|
++ output.byteLength, kInputLength, 'output should be the right length');
|
|
|
++}, 'data should be correctly decompressed even if input is detached partway');
|
|
|
+diff --git a/third_party/blink/web_tests/external/wpt/compression/resources/concatenate-stream.js b/third_party/blink/web_tests/external/wpt/compression/resources/concatenate-stream.js
|
|
|
+new file mode 100644
|
|
|
+index 0000000000000000000000000000000000000000..a35bb1416e754893e331c0089d97720ae3b5af8e
|
|
|
+--- /dev/null
|
|
|
++++ b/third_party/blink/web_tests/external/wpt/compression/resources/concatenate-stream.js
|
|
|
+@@ -0,0 +1,25 @@
|
|
|
++'use strict';
|
|
|
++
|
|
|
++// Read all the chunks from a stream that returns BufferSource objects and
|
|
|
++// concatenate them into a single Uint8Array.
|
|
|
++async function concatenateStream(readableStream) {
|
|
|
++ const reader = readableStream.getReader();
|
|
|
++ let totalSize = 0;
|
|
|
++ const buffers = [];
|
|
|
++ while (true) {
|
|
|
++ const { value, done } = await reader.read();
|
|
|
++ if (done) {
|
|
|
++ break;
|
|
|
++ }
|
|
|
++ buffers.push(value);
|
|
|
++ totalSize += value.byteLength;
|
|
|
++ }
|
|
|
++ reader.releaseLock();
|
|
|
++ const concatenated = new Uint8Array(totalSize);
|
|
|
++ let offset = 0;
|
|
|
++ for (const buffer of buffers) {
|
|
|
++ concatenated.set(buffer, offset);
|
|
|
++ offset += buffer.byteLength;
|
|
|
++ }
|
|
|
++ return concatenated;
|
|
|
++}
|