Skip to content

Commit b48ce98

Browse files
committed
Make blob: URLs cross‑realm and allow file‑backed Blob cloning
Detailed description: - Add a global native blob URL registry so blob: URLs created in one realm are resolvable in another (e.g. main → worker). Registry entries store the DataQueue and metadata, are mutex‑protected, and are cleaned up via Environment cleanup hooks to avoid teardown races. - Extend the DataQueue / Entry reader API to accept an optional `Environment*`, and update `FdEntry` reader creation so file handles and reader resources are created in the target environment/realm. This enables structured cloning / transferring of file‑backed Blobs across workers. - Remove the JS-side prohibition on cloning file‑backed blobs so `structuredClone()` succeeds for file‑backed Blobs. - Wire up Blob native bindings to use the new registry when storing/getting/revoking blob URLs; ensure Blob::GetDataObject constructs the Blob in the receiving environment. - Add/update regression tests covering URL resolution in workers and cloning of file‑backed Blobs. Files changed (high level): - **node_blob.cc**: add global blob URL registry, Store/Get/Revoke helpers, env cleanup hook integration, Blob::StoreDataObject/GetDataObject/RevokeObjectURL wiring. - **queue.h**, **queue.cc**: add optional `Environment*` parameter to `Entry::get_reader`, propagate env through DataQueue, implement `FdEntry::ReaderImpl::Create(entry, env)` to open file handles in the reader env. - **blob.js**: remove `kNotCloneable` flag for file‑backed Blobs so they can be cloned/ transferred. - **test-blob-url-worker.js**: new/updated regression test validating `resolveObjectURL` in a worker for a URL created on the main thread. - **test-blob-file-backed.js**: update test to assert `structuredClone()` succeeds for file‑backed Blobs and that cloned content/metadata match. Rationale and tests: - These changes fix two related issues: blob URLs that only worked per‑realm, and file‑backed Blobs that were prevented from being cloned/transferred. Both were blocking cross‑realm Blob usage (main ↔ worker). All added/updated tests pass in the local test harness used during development.
1 parent b2f6aa3 commit b48ce98

6 files changed

Lines changed: 122 additions & 48 deletions

File tree

lib/internal/blob.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -403,9 +403,7 @@ function createBlobFromFilePath(path, options) {
403403
return lazyDOMException('The blob could not be read', 'NotReadableError');
404404
}
405405
const { 0: blob, 1: length } = maybeBlob;
406-
const res = createBlob(blob, length, options?.type);
407-
res[kNotCloneable] = true;
408-
return res;
406+
return createBlob(blob, length, options?.type);
409407
}
410408

411409
function arrayBuffer(blob) {

src/dataqueue/queue.cc

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class NonIdempotentDataQueueReader;
3333

3434
class EntryImpl : public DataQueue::Entry {
3535
public:
36-
virtual std::shared_ptr<DataQueue::Reader> get_reader() = 0;
36+
std::shared_ptr<DataQueue::Reader> get_reader(Environment* env = nullptr) override = 0;
3737
};
3838

3939
class DataQueueImpl final : public DataQueue,
@@ -183,7 +183,7 @@ class DataQueueImpl final : public DataQueue,
183183
return !backpressure_listeners_.empty();
184184
}
185185

186-
std::shared_ptr<Reader> get_reader() override;
186+
std::shared_ptr<Reader> get_reader(Environment* env = nullptr) override;
187187
SET_MEMORY_INFO_NAME(DataQueue)
188188
SET_SELF_SIZE(DataQueueImpl)
189189

@@ -521,7 +521,7 @@ class NonIdempotentDataQueueReader final
521521
bool pull_pending_ = false;
522522
};
523523

524-
std::shared_ptr<DataQueue::Reader> DataQueueImpl::get_reader() {
524+
std::shared_ptr<DataQueue::Reader> DataQueueImpl::get_reader(Environment* env) {
525525
if (is_idempotent()) {
526526
return std::make_shared<IdempotentDataQueueReader>(shared_from_this());
527527
}
@@ -573,7 +573,7 @@ class EmptyEntry final : public EntryImpl {
573573
EmptyEntry& operator=(const EmptyEntry&) = delete;
574574
EmptyEntry& operator=(EmptyEntry&&) = delete;
575575

576-
std::shared_ptr<DataQueue::Reader> get_reader() override {
576+
std::shared_ptr<DataQueue::Reader> get_reader(Environment* env) override {
577577
return std::make_shared<EmptyReader>();
578578
}
579579

@@ -661,7 +661,7 @@ class InMemoryEntry final : public EntryImpl {
661661
InMemoryEntry& operator=(const InMemoryEntry&) = delete;
662662
InMemoryEntry& operator=(InMemoryEntry&&) = delete;
663663

664-
std::shared_ptr<DataQueue::Reader> get_reader() override {
664+
std::shared_ptr<DataQueue::Reader> get_reader(Environment* env) override {
665665
return std::make_shared<InMemoryReader>(*this);
666666
}
667667

@@ -732,8 +732,8 @@ class DataQueueEntry : public EntryImpl {
732732
DataQueueEntry& operator=(const DataQueueEntry&) = delete;
733733
DataQueueEntry& operator=(DataQueueEntry&&) = delete;
734734

735-
std::shared_ptr<DataQueue::Reader> get_reader() override {
736-
return std::make_shared<ReaderImpl>(data_queue_->get_reader());
735+
std::shared_ptr<DataQueue::Reader> get_reader(Environment* env) override {
736+
return std::make_shared<ReaderImpl>(data_queue_->get_reader(env));
737737
}
738738

739739
std::unique_ptr<Entry> slice(
@@ -844,8 +844,8 @@ class FdEntry final : public EntryImpl {
844844
CHECK_LE(start, end);
845845
}
846846

847-
std::shared_ptr<DataQueue::Reader> get_reader() override {
848-
return ReaderImpl::Create(this);
847+
std::shared_ptr<DataQueue::Reader> get_reader(Environment* env) override {
848+
return ReaderImpl::Create(this, env);
849849
}
850850

851851
std::unique_ptr<Entry> slice(
@@ -901,7 +901,8 @@ class FdEntry final : public EntryImpl {
901901
public StreamListener,
902902
public std::enable_shared_from_this<ReaderImpl> {
903903
public:
904-
static std::shared_ptr<ReaderImpl> Create(FdEntry* entry) {
904+
static std::shared_ptr<ReaderImpl> Create(FdEntry* entry,
905+
Environment* env) {
905906
uv_fs_t req;
906907
auto cleanup = OnScopeLeave([&] { uv_fs_req_cleanup(&req); });
907908
int file =
@@ -910,7 +911,8 @@ class FdEntry final : public EntryImpl {
910911
uv_fs_close(nullptr, &req, file, nullptr);
911912
return nullptr;
912913
}
913-
Realm* realm = entry->env()->principal_realm();
914+
Environment* reader_env = env ? env : entry->env();
915+
Realm* realm = reader_env->principal_realm();
914916
return std::make_shared<ReaderImpl>(
915917
BaseObjectPtr<fs::FileHandle>(
916918
fs::FileHandle::New(realm->GetBindingData<fs::BindingData>(),

src/dataqueue/queue.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,10 @@ class DataQueue : public MemoryRetainer {
186186
// idempotent and cannot preserve that quality, subsequent reads
187187
// must fail with an error when a variance is detected.
188188
virtual bool is_idempotent() const = 0;
189+
190+
// Create a reader for this entry. If `env` is provided, it may be used
191+
// when the entry needs to create native resources in the current realm.
192+
virtual std::shared_ptr<Reader> get_reader(Environment* env = nullptr) = 0;
189193
};
190194

191195
// Creates an idempotent DataQueue with a pre-established collection
@@ -228,7 +232,7 @@ class DataQueue : public MemoryRetainer {
228232
// any number of readers can be created, all of which are guaranteed
229233
// to provide the same data. Otherwise, only a single reader is
230234
// permitted.
231-
virtual std::shared_ptr<Reader> get_reader() = 0;
235+
virtual std::shared_ptr<Reader> get_reader(Environment* env = nullptr) = 0;
232236

233237
// Append a single new entry to the queue. Appending is only allowed
234238
// when is_idempotent() is false. std::nullopt will be returned

src/node_blob.cc

Lines changed: 69 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
#include "v8.h"
1515

1616
#include <algorithm>
17+
#include <cstdlib>
18+
#include <mutex>
19+
#include <optional>
20+
#include <unordered_map>
1721

1822
namespace node {
1923

@@ -103,6 +107,48 @@ void Concat(const FunctionCallbackInfo<Value>& args) {
103107
args.GetReturnValue().Set(ArrayBuffer::New(isolate, std::move(store)));
104108
}
105109

110+
struct BlobURLEntry {
111+
std::shared_ptr<DataQueue> data_queue;
112+
size_t length;
113+
std::string type;
114+
};
115+
116+
static std::mutex blob_url_registry_mutex;
117+
static std::unordered_map<std::string, BlobURLEntry> blob_url_registry;
118+
119+
static void RevokeBlobURLEntry(const std::string& uuid);
120+
121+
static void BlobURLCleanupHook(void* arg) {
122+
std::string* uuid = static_cast<std::string*>(arg);
123+
RevokeBlobURLEntry(*uuid);
124+
delete uuid;
125+
}
126+
127+
static void StoreBlobURLEntry(const std::string& uuid,
128+
std::shared_ptr<DataQueue> data_queue,
129+
size_t length,
130+
std::string type,
131+
Environment* env) {
132+
std::lock_guard<std::mutex> lock(blob_url_registry_mutex);
133+
blob_url_registry[uuid] = BlobURLEntry{std::move(data_queue), length,
134+
std::move(type)};
135+
136+
std::string* uuid_copy = new std::string(uuid);
137+
env->AddCleanupHook(BlobURLCleanupHook, uuid_copy);
138+
}
139+
140+
static std::optional<BlobURLEntry> GetBlobURLEntry(const std::string& uuid) {
141+
std::lock_guard<std::mutex> lock(blob_url_registry_mutex);
142+
auto it = blob_url_registry.find(uuid);
143+
if (it == blob_url_registry.end()) return std::nullopt;
144+
return it->second;
145+
}
146+
147+
static void RevokeBlobURLEntry(const std::string& uuid) {
148+
std::lock_guard<std::mutex> lock(blob_url_registry_mutex);
149+
blob_url_registry.erase(uuid);
150+
}
151+
106152
void BlobFromFilePath(const FunctionCallbackInfo<Value>& args) {
107153
Environment* env = Environment::GetCurrent(args);
108154
BufferValue path(env->isolate(), args[0]);
@@ -302,7 +348,7 @@ Blob::Reader::Reader(Environment* env,
302348
Local<Object> obj,
303349
BaseObjectPtr<Blob> strong_ptr)
304350
: AsyncWrap(env, obj, AsyncWrap::PROVIDER_BLOBREADER),
305-
inner_(strong_ptr->data_queue_->get_reader()),
351+
inner_(strong_ptr->data_queue_->get_reader(env)),
306352
strong_ptr_(std::move(strong_ptr)) {
307353
MakeWeak();
308354
}
@@ -453,7 +499,6 @@ void Blob::StoreDataObject(const FunctionCallbackInfo<Value>& args) {
453499
CHECK(args[2]->IsUint32()); // Length
454500
CHECK(args[3]->IsString()); // Type
455501

456-
BlobBindingData* binding_data = realm->GetBindingData<BlobBindingData>();
457502
Isolate* isolate = realm->isolate();
458503

459504
Utf8Value key(isolate, args[0]);
@@ -463,10 +508,8 @@ void Blob::StoreDataObject(const FunctionCallbackInfo<Value>& args) {
463508
size_t length = args[2].As<Uint32>()->Value();
464509
Utf8Value type(isolate, args[3]);
465510

466-
binding_data->store_data_object(
467-
key.ToString(),
468-
BlobBindingData::StoredDataObject(
469-
BaseObjectPtr<Blob>(blob), length, type.ToString()));
511+
StoreBlobURLEntry(key.ToString(), blob->data_queue_, length,
512+
type.ToString(), realm->env());
470513
}
471514

472515
// Note: applying the V8 Fast API to the following function does not produce
@@ -475,7 +518,6 @@ void Blob::RevokeObjectURL(const FunctionCallbackInfo<Value>& args) {
475518
CHECK_GE(args.Length(), 1);
476519
CHECK(args[0]->IsString());
477520
Realm* realm = Realm::GetCurrent(args);
478-
BlobBindingData* binding_data = realm->GetBindingData<BlobBindingData>();
479521
Isolate* isolate = realm->isolate();
480522

481523
Utf8Value input(isolate, args[0].As<String>());
@@ -492,37 +534,38 @@ void Blob::RevokeObjectURL(const FunctionCallbackInfo<Value>& args) {
492534
auto end_index = pathname.find(':', start_index + 1);
493535
if (end_index == std::string_view::npos) {
494536
auto id = std::string(pathname.substr(start_index + 1));
495-
binding_data->revoke_data_object(id);
537+
RevokeBlobURLEntry(id);
496538
}
497539
}
498540
}
499541

500542
void Blob::GetDataObject(const FunctionCallbackInfo<Value>& args) {
501543
CHECK(args[0]->IsString());
502544
Realm* realm = Realm::GetCurrent(args);
503-
BlobBindingData* binding_data = realm->GetBindingData<BlobBindingData>();
504545
Isolate* isolate = realm->isolate();
505546

506547
Utf8Value key(isolate, args[0]);
548+
std::optional<BlobURLEntry> stored = GetBlobURLEntry(key.ToString());
549+
if (!stored.has_value()) return;
550+
551+
Environment* env = realm->env();
552+
BaseObjectPtr<Blob> blob = Blob::Create(env, stored->data_queue);
553+
if (!blob) return;
554+
555+
Local<Value> type;
556+
if (!String::NewFromUtf8(isolate,
557+
stored->type.c_str(),
558+
NewStringType::kNormal,
559+
static_cast<int>(stored->type.length()))
560+
.ToLocal(&type)) {
561+
return;
562+
}
507563

508-
BlobBindingData::StoredDataObject stored =
509-
binding_data->get_data_object(key.ToString());
510-
if (stored.blob) {
511-
Local<Value> type;
512-
if (!String::NewFromUtf8(isolate,
513-
stored.type.c_str(),
514-
NewStringType::kNormal,
515-
static_cast<int>(stored.type.length()))
516-
.ToLocal(&type)) {
517-
return;
518-
}
519-
520-
Local<Value> values[] = {stored.blob->object(),
521-
Uint32::NewFromUnsigned(isolate, stored.length),
522-
type};
564+
Local<Value> values[] = {blob->object(),
565+
Uint32::NewFromUnsigned(isolate, stored->length),
566+
type};
523567

524-
args.GetReturnValue().Set(Array::New(isolate, values, arraysize(values)));
525-
}
568+
args.GetReturnValue().Set(Array::New(isolate, values, arraysize(values)));
526569
}
527570

528571
void BlobBindingData::StoredDataObject::MemoryInfo(

test/parallel/test-blob-file-backed.js

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,9 @@ writeFileSync(testfile5, '');
130130
})().then(common.mustCall());
131131

132132
(async () => {
133-
// We currently do not allow File-backed blobs to be cloned or transferred
134-
// across worker threads. This is largely because the underlying FdEntry
135-
// is bound to the Environment/Realm under which is was created.
136133
const blob = await openAsBlob(__filename);
137-
assert.throws(() => structuredClone(blob), {
138-
code: 'ERR_INVALID_STATE',
139-
message: 'Invalid state: File-backed Blobs are not cloneable'
140-
});
134+
const clone = structuredClone(blob);
135+
assert.strictEqual(clone.size, blob.size);
136+
assert.strictEqual(clone.type, blob.type);
137+
assert.strictEqual(await clone.text(), await blob.text());
141138
})().then(common.mustCall());
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const { isMainThread, parentPort, Worker } = require('worker_threads');
6+
const { Blob } = require('buffer');
7+
8+
if (isMainThread) {
9+
const blob = new Blob(['hello world']);
10+
const url = URL.createObjectURL(blob);
11+
12+
const worker = new Worker(__filename);
13+
worker.once('message', common.mustCall((value) => {
14+
assert.deepStrictEqual(value, { size: 11, type: '', text: 'hello world' });
15+
worker.terminate();
16+
}));
17+
worker.once('error', common.mustNotCall());
18+
worker.once('exit', common.mustCall((code) => {
19+
assert.strictEqual(code, 0);
20+
}));
21+
22+
worker.postMessage(url);
23+
} else {
24+
const { resolveObjectURL } = require('buffer');
25+
parentPort.once('message', async (url) => {
26+
const blob = resolveObjectURL(url);
27+
const text = await blob.text();
28+
parentPort.postMessage({ size: blob.size, type: blob.type, text });
29+
});
30+
}

0 commit comments

Comments
 (0)