Skip to content

Commit 50417a4

Browse files
committed
Add compat flag to prevent creating Blob with resizable ArrayBuffer
1 parent 56862af commit 50417a4

8 files changed

Lines changed: 70 additions & 11 deletions

File tree

src/workerd/api/blob.c++

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
#include <workerd/api/streams/readable-source.h>
88
#include <workerd/api/streams/readable.h>
9+
#include <workerd/io/features.h>
910
#include <workerd/io/observer.h>
1011
#include <workerd/util/mimetype.h>
1112
#include <workerd/util/stream-utils.h>
@@ -20,14 +21,21 @@ kj::Maybe<jsg::JsBufferSource> concat(jsg::Lock& js, jsg::Optional<Blob::Bits> m
2021
return kj::none;
2122
}
2223

24+
auto rejectResizable = FeatureFlags::get(js).getNoResizableArrayBufferInBlob();
2325
auto maxBlobSize = Worker::Isolate::from(js).getLimitEnforcer().getBlobSizeLimit();
2426
static constexpr int kMaxInt KJ_UNUSED = kj::maxValue;
2527
KJ_DASSERT(maxBlobSize <= kMaxInt, "Blob size limit exceeds int range");
2628
size_t size = 0;
29+
kj::SmallArray<size_t, 8> cachedPartSizes(bits.size());
30+
int index = 0;
2731
for (auto& part: bits) {
2832
size_t partSize = 0;
2933
KJ_SWITCH_ONEOF(part) {
30-
KJ_CASE_ONEOF(bytes, kj::Array<const byte>) {
34+
KJ_CASE_ONEOF(bytes, jsg::JsBufferSource) {
35+
if (rejectResizable) {
36+
JSG_REQUIRE(
37+
!bytes.isResizable(), TypeError, "Cannot create a Blob with a resizable ArrayBuffer");
38+
}
3139
partSize = bytes.size();
3240
}
3341
KJ_CASE_ONEOF(text, kj::String) {
@@ -37,6 +45,7 @@ kj::Maybe<jsg::JsBufferSource> concat(jsg::Lock& js, jsg::Optional<Blob::Bits> m
3745
partSize = blob->getData().size();
3846
}
3947
}
48+
cachedPartSizes[index++] = partSize;
4049

4150
// We can skip the remaining checks if the part is empty.
4251
if (partSize == 0) continue;
@@ -66,25 +75,34 @@ kj::Maybe<jsg::JsBufferSource> concat(jsg::Lock& js, jsg::Optional<Blob::Bits> m
6675

6776
auto view = u8.asArrayPtr();
6877

78+
index = 0;
6979
for (auto& part: bits) {
7080
KJ_SWITCH_ONEOF(part) {
71-
KJ_CASE_ONEOF(bytes, kj::Array<const byte>) {
72-
if (bytes.size() == 0) continue;
73-
KJ_ASSERT(view.size() >= bytes.size());
74-
view.first(bytes.size()).copyFrom(bytes);
75-
view = view.slice(bytes.size());
81+
KJ_CASE_ONEOF(bytes, jsg::JsBufferSource) {
82+
size_t cachedSize = cachedPartSizes[index++];
83+
// If the ArrayBuffer was resized larger, we'll ignore the additional bytes.
84+
// If the ArrayBuffer was resized smaller, we'll copy up the current size
85+
// and the remaining bytes for this chunk will be left as zeros.
86+
size_t toCopy = kj::min(bytes.size(), cachedSize);
87+
if (toCopy > 0) {
88+
KJ_ASSERT(view.size() >= toCopy);
89+
view.first(toCopy).copyFrom(bytes.asArrayPtr().first(toCopy));
90+
}
91+
view = view.slice(cachedSize);
7692
}
7793
KJ_CASE_ONEOF(text, kj::String) {
7894
auto byteLength = text.asBytes().size();
7995
if (byteLength == 0) continue;
8096
KJ_ASSERT(view.size() >= byteLength);
97+
KJ_ASSERT(byteLength == cachedPartSizes[index++]);
8198
view.first(byteLength).copyFrom(text.asBytes());
8299
view = view.slice(byteLength);
83100
}
84101
KJ_CASE_ONEOF(blob, jsg::Ref<Blob>) {
85102
auto data = blob->getData();
86103
if (data.size() == 0) continue;
87104
KJ_ASSERT(view.size() >= data.size());
105+
KJ_ASSERT(data.size() == cachedPartSizes[index++]);
88106
view.first(data.size()).copyFrom(data);
89107
view = view.slice(data.size());
90108
}
@@ -129,7 +147,12 @@ Blob::Blob(kj::Array<byte> data, kj::String type)
129147
Blob::Blob(jsg::Lock& js, jsg::JsBufferSource data, kj::String type)
130148
: ownData(data.addRef(js)),
131149
data(data.asArrayPtr()),
132-
type(kj::mv(type)) {}
150+
type(kj::mv(type)) {
151+
if (FeatureFlags::get(js).getNoResizableArrayBufferInBlob()) {
152+
JSG_REQUIRE(
153+
!data.isResizable(), TypeError, "Cannot create a Blob with a resizable ArrayBuffer");
154+
}
155+
}
133156

134157
Blob::Blob(jsg::Ref<Blob> parent, kj::ArrayPtr<const byte> data, kj::String type)
135158
: ownData(kj::mv(parent)),

src/workerd/api/blob.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ class Blob: public jsg::Object {
3232
JSG_STRUCT(type, endings);
3333
};
3434

35-
using Bits = kj::Array<kj::OneOf<kj::Array<const byte>, kj::String, jsg::Ref<Blob>>>;
35+
using BitsValue = kj::OneOf<jsg::JsBufferSource, kj::String, jsg::Ref<Blob>>;
36+
using Bits = kj::Array<BitsValue>;
3637

3738
static jsg::Ref<Blob> constructor(
3839
jsg::Lock& js, jsg::Optional<Bits> bits, jsg::Optional<Options> options);

src/workerd/api/tests/blob-test.wd-test

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ const unitTests :Workerd.Config = (
88
modules = [
99
(name = "worker", esModule = embed "blob-test.js")
1010
],
11-
compatibilityFlags = ["nodejs_compat", "set_tostring_tag", "workers_api_getters_setters_on_prototype"],
11+
compatibilityFlags = [
12+
"nodejs_compat",
13+
"set_tostring_tag",
14+
"workers_api_getters_setters_on_prototype",
15+
"resizable_array_buffer_in_blob"],
1216
bindings = [
1317
(name = "request-blob", service = "blob-test")
1418
]

src/workerd/api/tests/blob2-test.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Licensed under the Apache 2.0 license found in the LICENSE file or at:
33
// https://opensource.org/licenses/Apache-2.0
44

5-
import { ok, strictEqual, deepStrictEqual } from 'node:assert';
5+
import { ok, strictEqual, deepStrictEqual, throws } from 'node:assert';
66

77
export const test = {
88
async test() {
@@ -36,3 +36,13 @@ export const bytes = {
3636
ok(u8_2 instanceof Uint8Array);
3737
},
3838
};
39+
40+
export const noResizable = {
41+
test() {
42+
const ab = new ArrayBuffer(8, { maxByteLength: 16 });
43+
throws(() => new Blob([ab]), {
44+
name: 'TypeError',
45+
message: 'Cannot create a Blob with a resizable ArrayBuffer',
46+
});
47+
},
48+
};

src/workerd/api/tests/blob2-test.wd-test

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ const unitTests :Workerd.Config = (
77
modules = [
88
(name = "worker", esModule = embed "blob2-test.js")
99
],
10-
compatibilityFlags = ["nodejs_compat", "blob_standard_mime_type"],
10+
compatibilityFlags = [
11+
"nodejs_compat",
12+
"blob_standard_mime_type",
13+
"no_resizable_array_buffer_in_blob",
14+
],
1115
)
1216
),
1317
],

src/workerd/io/compatibility-date.capnp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1502,4 +1502,10 @@ struct CompatibilityFlags @0x8f8c1b68151b6cef {
15021502
# When paired with `enable_version_api`, also exposes `ctx.version.metadata`. This is a separate
15031503
# flag as we haven't decided on the exact behaviour of `ctx.version.metadata`, but the rest of
15041504
# `ctx.version` is much more well defined. The behaviour of this flag will change in the future.
1505+
1506+
noResizableArrayBufferInBlob @173 :Bool
1507+
$compatEnableFlag("no_resizable_array_buffer_in_blob")
1508+
$compatDisableFlag("resizable_array_buffer_in_blob");
1509+
# When enabled, creating a Blob with a resizable ArrayBuffer will throw a TypeError, matching
1510+
# expected spec behavior.
15051511
}

src/workerd/jsg/jsvalue.c++

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,16 @@ kj::Array<kj::byte> JsBufferSource::copy() {
818818
return kj::heapArray(ptr);
819819
}
820820

821+
bool JsBufferSource::isResizable() const {
822+
v8::Local<v8::Value> inner = *this;
823+
if (inner->IsArrayBuffer()) {
824+
return inner.As<v8::ArrayBuffer>()->IsResizableByUserJavaScript();
825+
} else if (inner->IsArrayBufferView()) {
826+
return inner.As<v8::ArrayBufferView>()->Buffer()->IsResizableByUserJavaScript();
827+
}
828+
return false;
829+
}
830+
821831
// ======================================================================================
822832
// JsUint8Array
823833

src/workerd/jsg/jsvalue.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,7 @@ class JsBufferSource final: public JsBase<v8::Value, JsBufferSource> {
340340
bool isSharedArrayBuffer() const;
341341
bool isArrayBuffer() const;
342342
bool isArrayBufferView() const;
343+
bool isResizable() const;
343344

344345
// Return a copy of this buffer's data as a kj::Array.
345346
kj::Array<kj::byte> copy();

0 commit comments

Comments
 (0)