Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions doc/api/ffi.md
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,25 @@ This is unsafe and dangerous. The returned pointer can become invalid if the
underlying memory is detached, resized, transferred, or otherwise invalidated.
Using stale pointers can cause memory corruption or process crashes.

## `ffi.getCurrentEventLoop()`

<!-- YAML
added: REPLACEME
-->

* Returns: {bigint}

Returns the address of the current thread's `uv_loop_t` as a `bigint`.

The returned address is for the current Node.js environment. In the main thread,
this is the main thread event loop. In a worker thread, this is that worker's
event loop.

This is unsafe and dangerous. The returned pointer is only valid for the lifetime
of the current environment. Using it after the environment exits, or from native
code that assumes a different thread or lifetime, can crash the process or
corrupt memory.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heh, I love this comment. "Here you go! Have fun cutting yourself!" 😆

Is there someway we could make this safer by returning an opaque value that becomes invalid when the environment exits or if it is used from the wrong thread?


## Safety notes

The `node:ffi` module does not track pointer validity, memory ownership, or
Expand Down
2 changes: 2 additions & 0 deletions lib/ffi.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const {
getUint64,
getFloat32,
getFloat64,
getCurrentEventLoop,
exportBytes,
getRawPointer,
kFastArguments,
Expand Down Expand Up @@ -320,6 +321,7 @@ module.exports = {
getUint64,
getFloat32,
getFloat64,
getCurrentEventLoop,
getRawPointer,
setInt8,
setUint8,
Expand Down
12 changes: 12 additions & 0 deletions src/node_ffi.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1215,6 +1215,17 @@ Local<FunctionTemplate> DynamicLibrary::GetConstructorTemplate(
return tmpl;
}

void GetCurrentEventLoop(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate();

THROW_IF_INSUFFICIENT_PERMISSIONS(env, permission::PermissionScope::kFFI, "");

args.GetReturnValue().Set(BigInt::NewFromUnsigned(
isolate,
static_cast<uint64_t>(reinterpret_cast<uintptr_t>(env->event_loop()))));
}

// Module initialization.
static void Initialize(Local<Object> target,
Local<Value> unused,
Expand All @@ -1230,6 +1241,7 @@ static void Initialize(Local<Object> target,
SetMethod(context, target, "toArrayBuffer", ToArrayBuffer);
SetMethod(context, target, "exportBytes", ExportBytes);
SetMethod(context, target, "getRawPointer", GetRawPointer);
SetMethod(context, target, "getCurrentEventLoop", GetCurrentEventLoop);

SetMethod(context, target, "getInt8", GetInt8);
SetMethod(context, target, "getUint8", GetUint8);
Expand Down
1 change: 1 addition & 0 deletions src/node_ffi.h
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ void ToBuffer(const v8::FunctionCallbackInfo<v8::Value>& args);
void ToArrayBuffer(const v8::FunctionCallbackInfo<v8::Value>& args);
void ExportBytes(const v8::FunctionCallbackInfo<v8::Value>& args);
void GetRawPointer(const v8::FunctionCallbackInfo<v8::Value>& args);
void GetCurrentEventLoop(const v8::FunctionCallbackInfo<v8::Value>& args);

} // namespace node::ffi

Expand Down
33 changes: 33 additions & 0 deletions test/ffi/test-ffi-module.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const { spawnSyncAndAssert } = require('../common/child_process');
const assert = require('node:assert');
const { spawnSync } = require('node:child_process');
const { test } = require('node:test');
const { Worker } = require('node:worker_threads');

common.skipIfFFIMissing();

Expand Down Expand Up @@ -87,6 +88,7 @@ test('ffi exports expected API surface', () => {
'exportArrayBufferView',
'exportBuffer',
'exportString',
'getCurrentEventLoop',
'getFloat32',
'getFloat64',
'getInt16',
Expand Down Expand Up @@ -135,6 +137,7 @@ test('ffi exports expected API surface', () => {
assert.strictEqual(typeof ffi.getUint64, 'function');
assert.strictEqual(typeof ffi.getFloat32, 'function');
assert.strictEqual(typeof ffi.getFloat64, 'function');
assert.strictEqual(typeof ffi.getCurrentEventLoop, 'function');
assert.strictEqual(typeof ffi.setInt8, 'function');
assert.strictEqual(typeof ffi.setUint8, 'function');
assert.strictEqual(typeof ffi.setInt16, 'function');
Expand Down Expand Up @@ -180,3 +183,33 @@ test('ffi.types exports canonical type constants', () => {
assert.deepStrictEqual(ffi.types, expected);
assert.strictEqual(Object.isFrozen(ffi.types), true);
});

test('ffi.getCurrentEventLoop returns the current thread event loop address', async () => {
const ffi = require('node:ffi');
const mainLoop = ffi.getCurrentEventLoop();

assert.strictEqual(typeof mainLoop, 'bigint');
assert.ok(mainLoop > 0n);
assert.strictEqual(ffi.getCurrentEventLoop(), mainLoop);

const workerLoop = await new Promise((resolve, reject) => {
const worker = new Worker(`
const { parentPort } = require('node:worker_threads');
const ffi = require('node:ffi');
const loop = ffi.getCurrentEventLoop();
parentPort.postMessage([loop, ffi.getCurrentEventLoop()]);
`, { eval: true });

worker.on('message', resolve);
worker.on('error', reject);
worker.on('exit', (code) => {
if (code !== 0) reject(new Error(`Worker stopped with exit code ${code}`));
});
});

assert.strictEqual(typeof workerLoop[0], 'bigint');
assert.ok(workerLoop[0] > 0n);
assert.strictEqual(workerLoop[0], workerLoop[1]);
assert.notStrictEqual(workerLoop[0], mainLoop);
});
4 changes: 4 additions & 0 deletions test/ffi/test-ffi-permissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ test('permission model blocks ffi memory and helper APIs', () => {
ffi.getRawPointer(Buffer.alloc(0));
}, denied);

assert.throws(() => {
ffi.getCurrentEventLoop();
}, denied);

assert.throws(() => {
ffi.dlclose({ close() {} });
}, denied);
Expand Down
Loading