diff --git a/doc/api/ffi.md b/doc/api/ffi.md index 33eef15106124f..85f4cbc69154cd 100644 --- a/doc/api/ffi.md +++ b/doc/api/ffi.md @@ -533,6 +533,8 @@ native memory directly. The caller must guarantee that: * `length` stays within the allocated native region. * no native code frees or repurposes that memory while JavaScript still uses the `Buffer`. +* Memory protection is observed. For example, read-only memory pages must not + be written to. If these guarantees are not met, reading or writing the `Buffer` can corrupt memory or crash the process. diff --git a/test/ffi/ffi-test-common.js b/test/ffi/ffi-test-common.js index 4398464ea75e47..7cc64eb00cda2e 100644 --- a/test/ffi/ffi-test-common.js +++ b/test/ffi/ffi-test-common.js @@ -79,6 +79,10 @@ const fixtureSymbols = { array_set_f64: { parameters: ['pointer', 'u64', 'f64'], result: 'void' }, }; +if (!common.isWindows) { + fixtureSymbols.readonly_memory = { parameters: [], result: 'pointer' }; +} + function cString(value) { return Buffer.from(`${value}\0`); } diff --git a/test/ffi/fixture_library/ffi_test_library.c b/test/ffi/fixture_library/ffi_test_library.c index fed2e57eefdfcf..8ac19c1a6d1ae2 100644 --- a/test/ffi/fixture_library/ffi_test_library.c +++ b/test/ffi/fixture_library/ffi_test_library.c @@ -2,10 +2,10 @@ #include #include #include - #ifdef _WIN32 #define FFI_EXPORT __declspec(dllexport) #else +#include #define FFI_EXPORT #endif @@ -378,3 +378,12 @@ FFI_EXPORT void array_set_f64(double* arr, size_t index, double value) { arr[index] = value; } + +#ifndef _WIN32 +FFI_EXPORT void* readonly_memory() { + // TODO(bengl) Add a Windows version of this. + + void* p = mmap(0, 4096, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + return p; +} +#endif diff --git a/test/ffi/test-ffi-readonly-write.js b/test/ffi/test-ffi-readonly-write.js new file mode 100644 index 00000000000000..33aa2245752ce6 --- /dev/null +++ b/test/ffi/test-ffi-readonly-write.js @@ -0,0 +1,31 @@ +// Flags: --experimental-ffi +'use strict'; +const { skipIfFFIMissing, isWindows, skip } = require('../common'); +const assert = require('node:assert'); +const { spawnSync } = require('node:child_process'); +const { test } = require('node:test'); +const { fixtureSymbols, libraryPath } = require('./ffi-test-common'); + +skipIfFFIMissing(); +if (isWindows) { + skip('This test currently relies on POSIX APIs'); +} + +test('writing to readonly memory via buffer fails', () => { + const symbols = JSON.stringify(fixtureSymbols); + const libPath = JSON.stringify(libraryPath); + const { stdout, status } = spawnSync(process.execPath, [ + '--experimental-ffi', + '-p', + ` + const ffi = require('node:ffi'); + const { functions } = ffi.dlopen(${libPath}, ${symbols}) + const p = functions.readonly_memory(); + const b = ffi.toBuffer(p, 4096, false); + b[0] = 42; + console.log('success'); + `, + ]); + assert.notStrictEqual(status, 0); + assert.strictEqual(stdout.length, 0); +});