From e0f99dfe69bf0e4d9b790501dbc3fa41f1ebd547 Mon Sep 17 00:00:00 2001 From: Gilson Urbano Date: Fri, 3 Apr 2026 14:57:53 +0200 Subject: [PATCH] Prevent SIGSEGV when util.inspect reads pointer-only buffers WrapPointer(non_null_ptr, 0) forces length=1 as an N-API workaround: napi_get_typedarray_info may return NULL for the data pointer of a 0-length TypedArray, so length=1 is used to guarantee the pointer survives a round-trip through N-API. However, the single backing byte may sit at an inaccessible sentinel address (e.g. RTLD_NEXT = (void*)-1 = 0xffffffffffffffff). When util.inspect or console.log traverses an object containing such a buffer, refinspect calls the original Buffer inspect which reads that byte, causing a SIGSEGV. Fix: WrapPointer now marks synthetic length=1 buffers with a well-known Symbol (Symbol.for('nodejs.ref-napi.pointer_only')). refinspect checks for this symbol and returns the address-only form without reading bytes. The symbol is exported as ref.kPointerOnly for consumers. Fixes https://github.com/napi-ffi/node-ffi-napi/issues/4 --- README.md | 6 +-- lib/ref.js | 17 ++++++++ src/binding.cc | 19 ++++++++- test/wrap-pointer-inspect.js | 76 ++++++++++++++++++++++++++++++++++++ 4 files changed, 113 insertions(+), 5 deletions(-) create mode 100644 test/wrap-pointer-inspect.js diff --git a/README.md b/README.md index 834d380..d4b4e84 100644 --- a/README.md +++ b/README.md @@ -41,11 +41,11 @@ Examples #### referencing and derefencing ``` js -var ref = require('ref-napi') +var ref = require('@napi-ffi/ref-napi') // so we can all agree that a buffer with the int value written // to it could be represented as an "int *" -var buf = new Buffer(4) +var buf = Buffer.alloc(4) buf.writeInt32LE(12345, 0) // first, what is the memory address of the buffer? @@ -93,7 +93,7 @@ For example, you could define a "bigint" type that dereferences into a [`bigint`](https://github.com/substack/node-bigint) instance: ``` js -var ref = require('ref-napi') +var ref = require('@napi-ffi/ref-napi') var bigint = require('bigint') // define the "type" instance according to the spec diff --git a/lib/ref.js b/lib/ref.js index e883219..53d7b18 100644 --- a/lib/ref.js +++ b/lib/ref.js @@ -1428,6 +1428,16 @@ Buffer.prototype.reinterpretUntilZeros = function reinterpretUntilZeros (size, o * ``` */ +// Symbol used to mark synthetic 1-byte pointer-only buffers created by +// WrapPointer(ptr, 0) for non-NULL ptr. The N-API spec doesn't preserve the +// data pointer for 0-length TypedArrays, so WrapPointer uses length=1 as a +// workaround. However, the single byte at the sentinel address (e.g. +// RTLD_NEXT = 0xffffffffffffffff) is not readable, so refinspect must not +// attempt to read it. This symbol is set by the native WrapPointer to signal +// that the buffer holds only a pointer value, not inspectable data bytes. +const kPointerOnly = Symbol.for('nodejs.ref-napi.pointer_only'); +exports.kPointerOnly = kPointerOnly; + var inspectSym = inspect.custom || 'inspect'; /** * in node 6.91, inspect.custom does not give a correct value; so in this case, don't torch the whole process. @@ -1489,6 +1499,13 @@ function overwriteInspect (inspect) { if (this.type && this.type.size === 0){ return ``; } + // Pointer-only buffers are synthetic 1-byte buffers created by + // WrapPointer(ptr, 0) for non-NULL ptr. Their backing byte may be at an + // inaccessible sentinel address (e.g. RTLD_NEXT = 0xffffffffffffffff). + // Reading that byte via the original inspect would cause a SIGSEGV. + if (this[kPointerOnly]) { + return ``; + } var v = inspect.apply(this, arguments); return v.replace('Buffer', 'Buffer@0x' + this.hexAddress()); } diff --git a/src/binding.cc b/src/binding.cc index 28a0436..7e91215 100644 --- a/src/binding.cc +++ b/src/binding.cc @@ -169,6 +169,7 @@ class InstanceData final : public RefNapi::Instance { */ Value WrapPointer(Env env, char* ptr, size_t length) { + bool pointer_only = false; if (ptr == nullptr) { length = 0; } else if (length == 0) { @@ -177,18 +178,32 @@ Value WrapPointer(Env env, char* ptr, size_t length) { // "[out] data: ... If the length of the array is 0, this may be NULL or any // other pointer value." length = 1; + pointer_only = true; } + Value result; InstanceData* data; if (ptr != nullptr && (data = InstanceData::Get(env)) != nullptr) { ArrayBuffer ab = data->LookupOrCreateArrayBuffer(ptr, length); assert(!ab.IsEmpty()); - return data->buffer_from.Call({ + result = data->buffer_from.Call({ ab, Number::New(env, 0), Number::New(env, length) }); + } else { + result = Buffer::New(env, ptr, length, [](Env,char*){}); + } + + // Mark synthetic 1-byte buffers as pointer-only so refinspect knows not to + // read the backing byte (which may be at an inaccessible sentinel address + // like RTLD_NEXT = (void*)-1 = 0xffffffffffffffff). + if (pointer_only) { + result.As().Set( + Symbol::For(env, "nodejs.ref-napi.pointer_only"), + Boolean::New(env, true) + ); } - return Buffer::New(env, ptr, length, [](Env,char*){}); + return result; } char* GetBufferData(Value val) { diff --git a/test/wrap-pointer-inspect.js b/test/wrap-pointer-inspect.js new file mode 100644 index 0000000..f087acc --- /dev/null +++ b/test/wrap-pointer-inspect.js @@ -0,0 +1,76 @@ +'use strict'; +const assert = require('assert'); +const ref = require('../'); +const { inspect } = require('util'); + +// Regression test for https://github.com/napi-ffi/node-ffi-napi/issues/4 +// +// WrapPointer(non_null_ptr, 0) creates a synthetic 1-byte Buffer due to an +// N-API limitation: napi_get_typedarray_info may return NULL for the data of +// 0-length TypedArrays. To preserve the pointer value, length is forced to 1. +// However, the single backing byte at certain addresses (e.g. sentinel values +// like RTLD_NEXT = (void*)-1 = 0xffffffffffffffff) is not readable memory. +// util.inspect must not attempt to read it or the process crashes with SIGSEGV. +// +// The fix: WrapPointer marks such buffers with kPointerOnly = Symbol.for( +// 'nodejs.ref-napi.pointer_only'), and refinspect skips reading bytes for them. + +describe('WrapPointer pointer-only buffers (length=0, non-NULL)', function () { + // ref.reinterpret(buf, 0) calls the internal WrapPointer(ptr, 0) with a + // non-NULL ptr, which is the exact code path that triggers the kPointerOnly mark. + let pointerOnlyBuf; + const backing = Buffer.alloc(8, 0xab); + + before(function () { + pointerOnlyBuf = ref.reinterpret(backing, 0); + }); + + it('exports the kPointerOnly symbol correctly', function () { + assert.strictEqual(ref.kPointerOnly, Symbol.for('nodejs.ref-napi.pointer_only')); + }); + + it('WrapPointer(ptr, 0) sets kPointerOnly on the returned buffer', function () { + assert.strictEqual(pointerOnlyBuf[ref.kPointerOnly], true); + }); + + it('WrapPointer(ptr, 0) returns a length-1 buffer (N-API workaround)', function () { + assert.strictEqual(pointerOnlyBuf.length, 1); + }); + + it('WrapPointer(ptr, 0) preserves the original pointer address', function () { + assert.strictEqual(ref.address(pointerOnlyBuf), ref.address(backing)); + }); + + it('util.inspect on a pointer-only buffer must not crash', function () { + const result = inspect(pointerOnlyBuf); + assert.strictEqual(typeof result, 'string'); + }); + + it('util.inspect on a pointer-only buffer shows the address', function () { + const result = inspect(pointerOnlyBuf); + assert.ok(result.includes(pointerOnlyBuf.hexAddress()), + `Expected address ${pointerOnlyBuf.hexAddress()} in: ${result}`); + }); + + it('util.inspect on a pointer-only buffer does not show raw bytes', function () { + // refinspect should return the short form, not the byte-dump form + const result = inspect(pointerOnlyBuf); + // A byte-dump form would look like "" — we must not see the byte + assert.ok(!/ [0-9a-f]{2}/.test(result), + `Unexpected raw byte in inspect output: ${result}`); + }); + + it('NULL buffer does not get kPointerOnly (it is zero-length, not pointer-only)', function () { + assert.strictEqual(ref.NULL[ref.kPointerOnly], undefined); + assert.strictEqual(ref.NULL.length, 0); + }); + + it('WrapPointer(ptr, length>0) does not set kPointerOnly', function () { + const explicit = ref.reinterpret(backing, 4); + assert.strictEqual(explicit.length, 4); + assert.strictEqual(explicit[ref.kPointerOnly], undefined); + const result = inspect(explicit); + // A real 4-byte buffer shows its bytes + assert.ok(result.includes('ab'), `Expected byte content in: ${result}`); + }); +});