Compare commits

...

2 Commits

Author SHA1 Message Date
Sosuke Suzuki
e60329f6a7 fix(napi): validate cell pointer in napi_typeof to prevent segfault (BUN-1PYR)
Use bloom filter + MarkedBlock set to verify that a cell pointer belongs to
a known GC-managed block before dereferencing it. This prevents crashes when
native modules accidentally pass garbage values (e.g. C string pointers) as
napi_value to napi_typeof. Returns napi_invalid_arg instead of segfaulting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 07:18:27 +09:00
Sosuke Suzuki
6d14b90e5e test(napi): add regression test for napi_typeof crash with invalid pointer (BUN-1PYR)
Add a test that passes a raw C string pointer as napi_value to napi_typeof,
reproducing the segfault where the crash address 0x6F20726F736E6554 decoded
to ASCII "Tensor o". The test currently crashes and will pass once the fix
is applied.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 21:27:29 +09:00
3 changed files with 63 additions and 0 deletions

View File

@@ -42,6 +42,9 @@
#include <JavaScriptCore/ExceptionScope.h>
#include <JavaScriptCore/FunctionConstructor.h>
#include <JavaScriptCore/Heap.h>
#include <JavaScriptCore/Integrity.h>
#include <JavaScriptCore/MarkedBlock.h>
#include <JavaScriptCore/PreciseAllocation.h>
#include <JavaScriptCore/Identifier.h>
#include <JavaScriptCore/InitializeThreading.h>
#include <JavaScriptCore/IteratorOperations.h>
@@ -2415,6 +2418,18 @@ extern "C" napi_status napi_typeof(napi_env env, napi_value val,
if (value.isCell()) {
JSCell* cell = value.asCell();
// Validate that the cell pointer is a real GC-managed object.
// Native modules may accidentally pass garbage (e.g. a C string pointer)
// as napi_value, which would crash when we dereference the cell.
// isSanePointer rejects obviously invalid addresses (null-near, non-canonical).
// The bloom filter provides fast rejection of pointers not in any known
// MarkedBlock, using only pointer arithmetic (no dereference).
if (!JSC::Integrity::isSanePointer(cell)
|| (!JSC::PreciseAllocation::isPreciseAllocation(cell)
&& toJS(env)->vm().heap.objectSpace().blocks().filter().ruleOut(
std::bit_cast<uintptr_t>(JSC::MarkedBlock::blockFor(cell))))) [[unlikely]]
return napi_set_last_error(env, napi_invalid_arg);
switch (cell->type()) {
case JSC::JSFunctionType:
case JSC::InternalFunctionType:

View File

@@ -2119,6 +2119,35 @@ static napi_value test_napi_create_tsfn_async_context_frame(const Napi::Callback
return env.Undefined();
}
// Test for BUN-1PYR: napi_typeof should not crash when given an invalid
// napi_value that is actually a raw C string pointer. This simulates the
// scenario where a native module passes garbage data (e.g., a string pointer
// like "Tensor ...") as a napi_value to napi_typeof.
static napi_value test_napi_typeof_invalid_pointer(const Napi::CallbackInfo &info) {
Napi::Env env = info.Env();
// Simulate the exact crash scenario: a C string pointer reinterpreted as napi_value.
// The crash address 0x6F20726F736E6554 decoded to ASCII is "Tensor o",
// meaning a string pointer was being used as a JSValue.
// Use aligned_alloc to ensure 16-byte alignment (bit 3 = 0), so the pointer
// goes through the MarkedBlock validation path (not the PreciseAllocation path).
char *fake_string = static_cast<char *>(aligned_alloc(16, 64));
memcpy(fake_string, "Tensor operation test string", 29);
napi_value bad_value = reinterpret_cast<napi_value>(fake_string);
napi_valuetype type;
napi_status status = napi_typeof(env, bad_value, &type);
if (status != napi_ok) {
printf("PASS: napi_typeof returned error status %d for invalid pointer\n", status);
} else {
printf("PASS: napi_typeof did not crash for invalid pointer (returned type %d)\n", type);
}
free(fake_string);
return ok(env);
}
void register_standalone_tests(Napi::Env env, Napi::Object exports) {
REGISTER_FUNCTION(env, exports, test_issue_7685);
REGISTER_FUNCTION(env, exports, test_issue_11949);
@@ -2157,6 +2186,7 @@ void register_standalone_tests(Napi::Env env, Napi::Object exports) {
REGISTER_FUNCTION(env, exports, test_issue_25933);
REGISTER_FUNCTION(env, exports, test_napi_make_callback_async_context_frame);
REGISTER_FUNCTION(env, exports, test_napi_create_tsfn_async_context_frame);
REGISTER_FUNCTION(env, exports, test_napi_typeof_invalid_pointer);
}
} // namespace napitests

View File

@@ -822,6 +822,24 @@ describe("cleanup hooks", () => {
expect(output).toContain("PASS: napi_create_threadsafe_function accepted AsyncContextFrame");
});
it("should not crash when given an invalid pointer as napi_value", async () => {
// Regression test for BUN-1PYR: napi_typeof segfaults when a native
// module passes a raw C string pointer as napi_value. The crash address
// 0x6F20726F736E6554 decoded to "Tensor o", indicating string data was
// being dereferenced as a JSValue.
const { BUN_INSPECT_CONNECT_TO: _, ...rest } = bunEnv;
await using exec = spawn({
cmd: [bunExe(), "--expose-gc", join(__dirname, "napi-app/main.js"), "test_napi_typeof_invalid_pointer", "[]"],
env: rest,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, exitCode] = await Promise.all([new Response(exec.stdout).text(), exec.exited]);
// napi_typeof should return an error status instead of crashing
expect(stdout).toContain("PASS");
expect(exitCode).toBe(0);
});
it("should return napi_object for boxed primitives (String, Number, Boolean)", async () => {
// Regression test for https://github.com/oven-sh/bun/issues/25351
// napi_typeof was incorrectly returning napi_string for String objects (new String("hello"))