From b448d7f38a4aab15713c133d6fc13e202834d3c9 Mon Sep 17 00:00:00 2001 From: Bryan English Date: Sun, 19 Apr 2026 00:23:55 -0400 Subject: [PATCH 1/3] doc,test: mem protection must be observed in ffi When using ffi.toBuffer, memory protection on any memory pages exposed must be observed by the caller, otherwise crashes will occur. Now documented, and tested. --- doc/api/ffi.md | 1 + test/ffi/ffi-test-common.js | 1 + test/ffi/fixture_library/ffi_test_library.c | 8 ++++++ test/ffi/test-ffi-readonly-write.js | 30 +++++++++++++++++++++ 4 files changed, 40 insertions(+) create mode 100644 test/ffi/test-ffi-readonly-write.js diff --git a/doc/api/ffi.md b/doc/api/ffi.md index 33eef15106124f..9839d115239d7e 100644 --- a/doc/api/ffi.md +++ b/doc/api/ffi.md @@ -533,6 +533,7 @@ 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. 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..2ec76f453b215f 100644 --- a/test/ffi/ffi-test-common.js +++ b/test/ffi/ffi-test-common.js @@ -77,6 +77,7 @@ const fixtureSymbols = { array_set_i32: { parameters: ['pointer', 'u64', 'i32'], result: 'void' }, array_get_f64: { parameters: ['pointer', 'u64'], result: 'f64' }, array_set_f64: { parameters: ['pointer', 'u64', 'f64'], result: 'void' }, + readonly_memory: { parameters: [], result: 'pointer' }, }; function cString(value) { diff --git a/test/ffi/fixture_library/ffi_test_library.c b/test/ffi/fixture_library/ffi_test_library.c index fed2e57eefdfcf..7ba93f4b774be6 100644 --- a/test/ffi/fixture_library/ffi_test_library.c +++ b/test/ffi/fixture_library/ffi_test_library.c @@ -2,6 +2,7 @@ #include #include #include +#include #ifdef _WIN32 #define FFI_EXPORT __declspec(dllexport) @@ -378,3 +379,10 @@ FFI_EXPORT void array_set_f64(double* arr, size_t index, double value) { arr[index] = value; } + +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; +} diff --git a/test/ffi/test-ffi-readonly-write.js b/test/ffi/test-ffi-readonly-write.js new file mode 100644 index 00000000000000..88848584123921 --- /dev/null +++ b/test/ffi/test-ffi-readonly-write.js @@ -0,0 +1,30 @@ +// 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 results in SIGBUS', () => { + const symbols = JSON.stringify(fixtureSymbols); + const { stdout, status, signal } = spawnSync(process.execPath, [ + '--experimental-ffi', + '-p', + ` + const ffi = require('node:ffi'); + const { functions } = ffi.dlopen('${libraryPath}', ${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); +}); From 48ebb7ac5a58153d74aace3e9a1042b59389e77d Mon Sep 17 00:00:00 2001 From: Bryan English Date: Sun, 19 Apr 2026 22:20:13 -0400 Subject: [PATCH 2/3] fixup! doc,test: mem protection must be observed in ffi --- doc/api/ffi.md | 3 ++- test/ffi/ffi-test-common.js | 4 ++++ test/ffi/fixture_library/ffi_test_library.c | 9 +++++---- test/ffi/test-ffi-readonly-write.js | 7 ++++--- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/doc/api/ffi.md b/doc/api/ffi.md index 9839d115239d7e..85f4cbc69154cd 100644 --- a/doc/api/ffi.md +++ b/doc/api/ffi.md @@ -533,7 +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. +* 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 2ec76f453b215f..932bce0cb05a98 100644 --- a/test/ffi/ffi-test-common.js +++ b/test/ffi/ffi-test-common.js @@ -80,6 +80,10 @@ const fixtureSymbols = { readonly_memory: { parameters: [], result: 'pointer' }, }; +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 7ba93f4b774be6..8ac19c1a6d1ae2 100644 --- a/test/ffi/fixture_library/ffi_test_library.c +++ b/test/ffi/fixture_library/ffi_test_library.c @@ -2,11 +2,10 @@ #include #include #include -#include - #ifdef _WIN32 #define FFI_EXPORT __declspec(dllexport) #else +#include #define FFI_EXPORT #endif @@ -380,9 +379,11 @@ FFI_EXPORT void array_set_f64(double* arr, size_t index, double value) { arr[index] = value; } -FFI_EXPORT void * readonly_memory() { +#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); + 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 index 88848584123921..33aa2245752ce6 100644 --- a/test/ffi/test-ffi-readonly-write.js +++ b/test/ffi/test-ffi-readonly-write.js @@ -11,14 +11,15 @@ if (isWindows) { skip('This test currently relies on POSIX APIs'); } -test('writing to readonly memory via buffer results in SIGBUS', () => { +test('writing to readonly memory via buffer fails', () => { const symbols = JSON.stringify(fixtureSymbols); - const { stdout, status, signal } = spawnSync(process.execPath, [ + const libPath = JSON.stringify(libraryPath); + const { stdout, status } = spawnSync(process.execPath, [ '--experimental-ffi', '-p', ` const ffi = require('node:ffi'); - const { functions } = ffi.dlopen('${libraryPath}', ${symbols}) + const { functions } = ffi.dlopen(${libPath}, ${symbols}) const p = functions.readonly_memory(); const b = ffi.toBuffer(p, 4096, false); b[0] = 42; From 8e3890d7afba6aff44433d0d0bac2c4177e5d53b Mon Sep 17 00:00:00 2001 From: Bryan English Date: Mon, 20 Apr 2026 10:17:45 -0400 Subject: [PATCH 3/3] fixup! fixup! doc,test: mem protection must be observed in ffi --- test/ffi/ffi-test-common.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/ffi/ffi-test-common.js b/test/ffi/ffi-test-common.js index 932bce0cb05a98..7cc64eb00cda2e 100644 --- a/test/ffi/ffi-test-common.js +++ b/test/ffi/ffi-test-common.js @@ -77,7 +77,6 @@ const fixtureSymbols = { array_set_i32: { parameters: ['pointer', 'u64', 'i32'], result: 'void' }, array_get_f64: { parameters: ['pointer', 'u64'], result: 'f64' }, array_set_f64: { parameters: ['pointer', 'u64', 'f64'], result: 'void' }, - readonly_memory: { parameters: [], result: 'pointer' }, }; if (!common.isWindows) {