worker,fs: make FileHandle transferable · nodejs/node@dd51ba3 · GitHub
Skip to content

Commit dd51ba3

Browse files
committed
worker,fs: make FileHandle transferable
Allow passing `FileHandle` instances in the transfer list of a `.postMessage()` call. PR-URL: #33772 Backport-PR-URL: #33965 Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
1 parent 0d35eaa commit dd51ba3

8 files changed

Lines changed: 235 additions & 4 deletions

doc/api/worker_threads.md

Lines changed: 10 additions & 2 deletions

lib/internal/fs/promises.js

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,17 @@ const { promisify } = require('internal/util');
5454
const kHandle = Symbol('kHandle');
5555
const kFd = Symbol('kFd');
5656
const { kUsePromises } = binding;
57+
const {
58+
JSTransferable, kDeserialize, kTransfer, kTransferList
59+
} = require('internal/worker/js_transferable');
5760

5861
const getDirectoryEntriesPromise = promisify(getDirents);
5962

60-
class FileHandle {
63+
class FileHandle extends JSTransferable {
6164
constructor(filehandle) {
65+
super();
6266
this[kHandle] = filehandle;
63-
this[kFd] = filehandle.fd;
67+
this[kFd] = filehandle ? filehandle.fd : -1;
6468
}
6569

6670
getAsyncId() {
@@ -131,6 +135,26 @@ class FileHandle {
131135
this[kFd] = -1;
132136
return this[kHandle].close();
133137
}
138+
139+
[kTransfer]() {
140+
const handle = this[kHandle];
141+
this[kFd] = -1;
142+
this[kHandle] = null;
143+
144+
return {
145+
data: { handle },
146+
deserializeInfo: 'internal/fs/promises:FileHandle'
147+
};
148+
}
149+
150+
[kTransferList]() {
151+
return [ this[kHandle] ];
152+
}
153+
154+
[kDeserialize]({ handle }) {
155+
this[kHandle] = handle;
156+
this[kFd] = handle.fd;
157+
}
134158
}
135159

136160
function validateFileHandle(handle) {

lib/internal/worker/js_transferable.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
'use strict';
2+
const { Error } = primordials;
23
const {
34
messaging_deserialize_symbol,
45
messaging_transfer_symbol,
@@ -17,6 +18,13 @@ function setup() {
1718
setDeserializerCreateObjectFunction((deserializeInfo) => {
1819
const [ module, ctor ] = deserializeInfo.split(':');
1920
const Ctor = require(module)[ctor];
21+
if (typeof Ctor !== 'function' ||
22+
!(Ctor.prototype instanceof JSTransferable)) {
23+
// Not one of the official errors because one should not be able to get
24+
// here without messing with Node.js internals.
25+
// eslint-disable-next-line no-restricted-syntax
26+
throw new Error(`Unknown deserialize spec ${deserializeInfo}`);
27+
}
2028
return new Ctor();
2129
});
2230
}

src/node_file.cc

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,40 @@ void FileHandle::MemoryInfo(MemoryTracker* tracker) const {
185185
tracker->TrackField("current_read", current_read_);
186186
}
187187

188+
FileHandle::TransferMode FileHandle::GetTransferMode() const {
189+
return reading_ || closing_ || closed_ ?
190+
TransferMode::kUntransferable : TransferMode::kTransferable;
191+
}
192+
193+
std::unique_ptr<worker::TransferData> FileHandle::TransferForMessaging() {
194+
CHECK_NE(GetTransferMode(), TransferMode::kUntransferable);
195+
auto ret = std::make_unique<TransferData>(fd_);
196+
closed_ = true;
197+
return std::move(ret);
198+
}
199+
200+
FileHandle::TransferData::TransferData(int fd) : fd_(fd) {}
201+
202+
FileHandle::TransferData::~TransferData() {
203+
if (fd_ > 0) {
204+
uv_fs_t close_req;
205+
CHECK_EQ(0, uv_fs_close(nullptr, &close_req, fd_, nullptr));
206+
uv_fs_req_cleanup(&close_req);
207+
}
208+
}
209+
210+
BaseObjectPtr<BaseObject> FileHandle::TransferData::Deserialize(
211+
Environment* env_,
212+
v8::Local<v8::Context> context,
213+
std::unique_ptr<worker::TransferData> self) {
214+
Environment* env = Environment::GetCurrent(context);
215+
if (env == nullptr) return {};
216+
217+
int fd = fd_;
218+
fd_ = -1;
219+
return BaseObjectPtr<BaseObject> { FileHandle::New(env, fd) };
220+
}
221+
188222
// Close the file descriptor if it hasn't already been closed. A process
189223
// warning will be emitted using a SetImmediate to avoid calling back to
190224
// JS during GC. If closing the fd fails at this point, a fatal exception

src/node_file.h

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
#include "node.h"
77
#include "aliased_buffer.h"
8+
#include "node_messaging.h"
89
#include "stream_base.h"
910
#include <iostream>
1011

@@ -250,7 +251,28 @@ class FileHandle final : public AsyncWrap, public StreamBase {
250251
FileHandle(const FileHandle&&) = delete;
251252
FileHandle& operator=(const FileHandle&&) = delete;
252253

254+
TransferMode GetTransferMode() const override;
255+
std::unique_ptr<worker::TransferData> TransferForMessaging() override;
256+
253257
private:
258+
class TransferData : public worker::TransferData {
259+
public:
260+
explicit TransferData(int fd);
261+
~TransferData();
262+
263+
BaseObjectPtr<BaseObject> Deserialize(
264+
Environment* env,
265+
v8::Local<v8::Context> context,
266+
std::unique_ptr<worker::TransferData> self) override;
267+
268+
SET_NO_MEMORY_INFO()
269+
SET_MEMORY_INFO_NAME(FileHandleTransferData)
270+
SET_SELF_SIZE(TransferData)
271+
272+
private:
273+
int fd_;
274+
};
275+
254276
FileHandle(Environment* env, v8::Local<v8::Object> obj, int fd);
255277

256278
// Synchronous close that emits a warning
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const fs = require('fs').promises;
5+
const { MessageChannel } = require('worker_threads');
6+
const { once } = require('events');
7+
8+
// Test that overriding the internal kTransfer method of a JSTransferable does
9+
// not enable loading arbitrary code from internal Node.js core modules.
10+
11+
(async function() {
12+
const fh = await fs.open(__filename);
13+
assert.strictEqual(fh.constructor.name, 'FileHandle');
14+
15+
const kTransfer = Object.getOwnPropertySymbols(Object.getPrototypeOf(fh))
16+
.filter((symbol) => symbol.description === 'messaging_transfer_symbol')[0];
17+
assert.strictEqual(typeof kTransfer, 'symbol');
18+
fh[kTransfer] = () => {
19+
return {
20+
data: '✨',
21+
deserializeInfo: 'net:Socket'
22+
};
23+
};
24+
25+
const { port1, port2 } = new MessageChannel();
26+
port1.postMessage(fh, [ fh ]);
27+
port2.on('message', common.mustNotCall());
28+
29+
const [ exception ] = await once(process, 'uncaughtException');
30+
31+
assert.strictEqual(exception.message, 'Unknown deserialize spec net:Socket');
32+
port2.close();
33+
})().then(common.mustCall());
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const fs = require('fs').promises;
5+
const { MessageChannel } = require('worker_threads');
6+
const { once } = require('events');
7+
8+
// Test that overriding the internal kTransfer method of a JSTransferable does
9+
// not enable loading arbitrary code from the disk.
10+
11+
module.exports = {
12+
NotARealClass: common.mustNotCall()
13+
};
14+
15+
(async function() {
16+
const fh = await fs.open(__filename);
17+
assert.strictEqual(fh.constructor.name, 'FileHandle');
18+
19+
const kTransfer = Object.getOwnPropertySymbols(Object.getPrototypeOf(fh))
20+
.filter((symbol) => symbol.description === 'messaging_transfer_symbol')[0];
21+
assert.strictEqual(typeof kTransfer, 'symbol');
22+
fh[kTransfer] = () => {
23+
return {
24+
data: '✨',
25+
deserializeInfo: `${__filename}:NotARealClass`
26+
};
27+
};
28+
29+
const { port1, port2 } = new MessageChannel();
30+
port1.postMessage(fh, [ fh ]);
31+
port2.on('message', common.mustNotCall());
32+
33+
const [ exception ] = await once(process, 'uncaughtException');
34+
35+
assert.match(exception.message, /Missing internal module/);
36+
port2.close();
37+
})().then(common.mustCall());
Lines changed: 65 additions & 0 deletions

0 commit comments

Comments
 (0)