mirror of
https://github.com/oven-sh/bun
synced 2026-03-03 05:51:03 +01:00
Compare commits
1 Commits
claude/fix
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c7e97231b |
@@ -42,9 +42,6 @@
|
||||
#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>
|
||||
@@ -2418,18 +2415,6 @@ 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:
|
||||
|
||||
@@ -301,10 +301,9 @@ pub fn scheduleBarrelDeferredImports(this: *BundleV2, result: *ParseTask.Result.
|
||||
// Handle import records without named bindings (not in named_imports).
|
||||
// - `import "x"` (bare statement): tree-shakeable with sideEffects: false — skip.
|
||||
// - `require("x")`: synchronous, needs full module — always mark as .all.
|
||||
// - `import("x")`: mark as .all ONLY if the barrel has no prior requests,
|
||||
// meaning this is the sole reference. If the barrel already has a .partial
|
||||
// entry from a static import, the dynamic import is likely a secondary
|
||||
// (possibly circular) reference and should not escalate requirements.
|
||||
// - `import("x")`: returns the full module namespace at runtime — consumer
|
||||
// can destructure or access any export. Must mark as .all. We cannot
|
||||
// safely assume which exports will be used.
|
||||
for (file_import_records.slice(), 0..) |ir, idx| {
|
||||
const target = if (ir.source_index.isValid())
|
||||
ir.source_index.get()
|
||||
@@ -319,10 +318,9 @@ pub fn scheduleBarrelDeferredImports(this: *BundleV2, result: *ParseTask.Result.
|
||||
const gop = try this.requested_exports.getOrPut(this.allocator(), target);
|
||||
gop.value_ptr.* = .all;
|
||||
} else if (ir.kind == .dynamic) {
|
||||
// Only escalate to .all if no prior requests exist for this target.
|
||||
if (!this.requested_exports.contains(target)) {
|
||||
try this.requested_exports.put(this.allocator(), target, .all);
|
||||
}
|
||||
// import() returns the full module namespace — must preserve all exports.
|
||||
const gop = try this.requested_exports.getOrPut(this.allocator(), target);
|
||||
gop.value_ptr.* = .all;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,8 +352,8 @@ pub fn scheduleBarrelDeferredImports(this: *BundleV2, result: *ParseTask.Result.
|
||||
}
|
||||
}
|
||||
|
||||
// Add bare require/dynamic-import targets to BFS as star imports (matching
|
||||
// the seeding logic above — require always, dynamic only when sole reference).
|
||||
// Add bare require/dynamic-import targets to BFS as star imports — both
|
||||
// always need the full namespace.
|
||||
for (file_import_records.slice(), 0..) |ir, idx| {
|
||||
const target = if (ir.source_index.isValid())
|
||||
ir.source_index.get()
|
||||
@@ -366,8 +364,7 @@ pub fn scheduleBarrelDeferredImports(this: *BundleV2, result: *ParseTask.Result.
|
||||
if (ir.flags.is_internal) continue;
|
||||
if (named_ir_indices.contains(@intCast(idx))) continue;
|
||||
if (ir.flags.was_originally_bare_import) continue;
|
||||
const is_all = if (this.requested_exports.get(target)) |re| re == .all else false;
|
||||
const should_add = ir.kind == .require or (ir.kind == .dynamic and is_all);
|
||||
const should_add = ir.kind == .require or ir.kind == .dynamic;
|
||||
if (should_add) {
|
||||
try queue.append(queue_alloc, .{ .barrel_source_index = target, .alias = "", .is_star = true });
|
||||
}
|
||||
|
||||
@@ -167,18 +167,8 @@ fn onClose(this: *WindowsNamedPipe) void {
|
||||
log("onClose", .{});
|
||||
if (!this.flags.is_closed) {
|
||||
this.flags.is_closed = true; // only call onClose once
|
||||
// Stop reading and clear the timer to prevent further callbacks,
|
||||
// but don't call deinit() here. The context (owner) will call
|
||||
// named_pipe.deinit() when it runs its own deferred deinit.
|
||||
// Calling deinit() synchronously here causes use-after-free when
|
||||
// this callback is triggered from within the SSL wrapper's
|
||||
// handleTraffic() call chain (the wrapper.deinit() frees the SSL
|
||||
// state while we're still on the wrapper's call stack).
|
||||
this.setTimeout(0);
|
||||
if (this.writer.getStream()) |stream| {
|
||||
_ = stream.readStop();
|
||||
}
|
||||
this.handlers.onClose(this.handlers.ctx);
|
||||
this.deinit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -595,7 +585,6 @@ pub fn deinit(this: *WindowsNamedPipe) void {
|
||||
wrapper.deinit();
|
||||
this.wrapper = null;
|
||||
}
|
||||
this.incoming.clearAndFree(bun.default_allocator);
|
||||
var ssl_error = this.ssl_error;
|
||||
ssl_error.deinit();
|
||||
this.ssl_error = .{};
|
||||
|
||||
@@ -968,7 +968,7 @@ pub const SystemErrno = enum(u16) {
|
||||
if (@TypeOf(code) == u16 or (@TypeOf(code) == c_int and code > 0)) {
|
||||
// Win32Error and WSA Error codes
|
||||
if (code <= @intFromEnum(Win32Error.IO_REISSUE_AS_CACHED) or (code >= @intFromEnum(Win32Error.WSAEINTR) and code <= @intFromEnum(Win32Error.WSA_QOS_RESERVED_PETYPE))) {
|
||||
return init(std.meta.intToEnum(Win32Error, code) catch return null);
|
||||
return init(@as(Win32Error, @enumFromInt(code)));
|
||||
} else {
|
||||
// uv error codes
|
||||
inline for (@typeInfo(SystemErrno).@"enum".fields) |field| {
|
||||
@@ -988,7 +988,7 @@ pub const SystemErrno = enum(u16) {
|
||||
}
|
||||
|
||||
if (comptime @TypeOf(code) == Win32Error or @TypeOf(code) == std.os.windows.Win32Error) {
|
||||
return switch (std.meta.intToEnum(Win32Error, @intFromEnum(code)) catch return null) {
|
||||
return switch (@as(Win32Error, @enumFromInt(@intFromEnum(code)))) {
|
||||
Win32Error.NOACCESS => SystemErrno.EACCES,
|
||||
Win32Error.WSAEACCES => SystemErrno.EACCES,
|
||||
Win32Error.ELEVATION_REQUIRED => SystemErrno.EACCES,
|
||||
|
||||
@@ -760,7 +760,7 @@ pub const WindowsBufferedReader = struct {
|
||||
return Type.onReaderError(@as(*Type, @ptrCast(@alignCast(this))), err);
|
||||
}
|
||||
fn loop(this: *anyopaque) *Async.Loop {
|
||||
return Type.loop(@as(*Type, @ptrCast(@alignCast(this))));
|
||||
return Type.loop(@as(*Type, @alignCast(@ptrCast(this))));
|
||||
}
|
||||
};
|
||||
return .{
|
||||
@@ -955,14 +955,14 @@ pub const WindowsBufferedReader = struct {
|
||||
}
|
||||
|
||||
fn onStreamAlloc(handle: *uv.Handle, suggested_size: usize, buf: *uv.uv_buf_t) callconv(.c) void {
|
||||
const this: *WindowsBufferedReader = @ptrCast(@alignCast(handle.data orelse return));
|
||||
var this = bun.cast(*WindowsBufferedReader, handle.data);
|
||||
const result = this.getReadBufferWithStableMemoryAddress(suggested_size);
|
||||
buf.* = uv.uv_buf_t.init(result);
|
||||
}
|
||||
|
||||
fn onStreamRead(handle: *uv.uv_handle_t, nread: uv.ReturnCodeI64, buf: *const uv.uv_buf_t) callconv(.c) void {
|
||||
const stream = bun.cast(*uv.uv_stream_t, handle);
|
||||
const this: *WindowsBufferedReader = @ptrCast(@alignCast(stream.data orelse return));
|
||||
var this = bun.cast(*WindowsBufferedReader, stream.data);
|
||||
|
||||
const nread_int = nread.int();
|
||||
|
||||
@@ -1199,15 +1199,13 @@ pub const WindowsBufferedReader = struct {
|
||||
}
|
||||
|
||||
fn onPipeClose(handle: *uv.Pipe) callconv(.c) void {
|
||||
// Use the handle directly for destroy, not handle.data which may be null
|
||||
// during cleanup races.
|
||||
bun.destroy(handle);
|
||||
const this = bun.cast(*uv.Pipe, handle.data);
|
||||
bun.destroy(this);
|
||||
}
|
||||
|
||||
fn onTTYClose(handle: *uv.uv_tty_t) callconv(.c) void {
|
||||
// Use the handle directly for destroy, not handle.data which may be null
|
||||
// during cleanup races.
|
||||
bun.default_allocator.destroy(handle);
|
||||
const this = bun.cast(*uv.uv_tty_t, handle.data);
|
||||
bun.default_allocator.destroy(this);
|
||||
}
|
||||
|
||||
pub fn onRead(this: *WindowsBufferedReader, amount: bun.sys.Maybe(usize), slice: []u8, hasMore: ReadState) void {
|
||||
|
||||
@@ -804,28 +804,22 @@ fn BaseWindowsPipeWriter(
|
||||
}
|
||||
|
||||
fn onPipeClose(handle: *uv.Pipe) callconv(.c) void {
|
||||
// Use the handle directly for destroy, not handle.data which may be
|
||||
// stale during cleanup races.
|
||||
bun.default_allocator.destroy(handle);
|
||||
const this = bun.cast(*uv.Pipe, handle.data);
|
||||
bun.default_allocator.destroy(this);
|
||||
}
|
||||
|
||||
fn onTTYClose(handle: *uv.uv_tty_t) callconv(.c) void {
|
||||
// Use the handle directly for destroy, not handle.data which may be
|
||||
// stale during cleanup races.
|
||||
bun.default_allocator.destroy(handle);
|
||||
const this = bun.cast(*uv.uv_tty_t, handle.data);
|
||||
bun.default_allocator.destroy(this);
|
||||
}
|
||||
|
||||
pub fn close(this: *WindowsPipeWriter) void {
|
||||
this.is_done = true;
|
||||
const source = this.source orelse return;
|
||||
// Check if there's a pending async write before closing.
|
||||
// If so, we must defer onCloseSource() to the write-complete
|
||||
// callback, because the parent's onClose handler may free
|
||||
// resources that the pending callback still needs to access.
|
||||
const has_pending_async_write = this.hasPendingAsyncWrite();
|
||||
// For StreamingWriter: also check the file state for in-flight
|
||||
// writes so we can balance processSend's ref().
|
||||
const has_inflight_file_write = if (@hasField(WindowsPipeWriter, "current_payload")) switch (source) {
|
||||
// Check for in-flight file write before detaching. detach()
|
||||
// nulls fs.data so onFsWriteComplete can't recover the writer
|
||||
// to call deref(). We must balance processSend's ref() here.
|
||||
const has_inflight_write = if (@hasField(WindowsPipeWriter, "current_payload")) switch (source) {
|
||||
.sync_file, .file => |file| file.state == .operating or file.state == .canceling,
|
||||
else => false,
|
||||
} else false;
|
||||
@@ -850,16 +844,9 @@ fn BaseWindowsPipeWriter(
|
||||
},
|
||||
}
|
||||
this.source = null;
|
||||
if (!has_pending_async_write) {
|
||||
// Safe to notify parent immediately — no pending callback.
|
||||
this.onCloseSource();
|
||||
}
|
||||
// When has_pending_async_write is true, onCloseSource() will
|
||||
// be called from onWriteComplete/onFsWriteComplete after the
|
||||
// pending write callback completes safely.
|
||||
|
||||
this.onCloseSource();
|
||||
// Deref last — this may free the parent and `this`.
|
||||
if (has_inflight_file_write) {
|
||||
if (has_inflight_write) {
|
||||
this.parent.deref();
|
||||
}
|
||||
}
|
||||
@@ -1012,22 +999,7 @@ pub fn WindowsBufferedWriter(Parent: type, function_table: anytype) type {
|
||||
return .success;
|
||||
}
|
||||
|
||||
/// Returns true if there is an outstanding async write request
|
||||
/// (uv_write or uv_fs_write) that hasn't completed yet.
|
||||
pub fn hasPendingAsyncWrite(this: *const WindowsWriter) bool {
|
||||
return this.pending_payload_size > 0;
|
||||
}
|
||||
|
||||
fn onWriteComplete(this: *WindowsWriter, status: uv.ReturnCode) void {
|
||||
// If the source was closed while a write was in-flight,
|
||||
// close() deferred onCloseSource(). Complete it now that
|
||||
// the write callback has safely finished.
|
||||
if (this.source == null) {
|
||||
this.pending_payload_size = 0;
|
||||
this.onCloseSource();
|
||||
return;
|
||||
}
|
||||
|
||||
const written = this.pending_payload_size;
|
||||
this.pending_payload_size = 0;
|
||||
if (status.toError(.write)) |err| {
|
||||
@@ -1066,14 +1038,6 @@ pub fn WindowsBufferedWriter(Parent: type, function_table: anytype) type {
|
||||
|
||||
const this = bun.cast(*WindowsWriter, parent_ptr);
|
||||
|
||||
// If source was closed while write was in-flight, close()
|
||||
// deferred onCloseSource(). Complete it now.
|
||||
if (this.source == null) {
|
||||
this.pending_payload_size = 0;
|
||||
this.onCloseSource();
|
||||
return;
|
||||
}
|
||||
|
||||
if (was_canceled) {
|
||||
// Canceled write - clear pending state
|
||||
this.pending_payload_size = 0;
|
||||
@@ -1338,12 +1302,6 @@ pub fn WindowsStreamingWriter(comptime Parent: type, function_table: anytype) ty
|
||||
return (this.outgoing.isNotEmpty() or this.current_payload.isNotEmpty());
|
||||
}
|
||||
|
||||
/// Returns true if there is an outstanding async write request
|
||||
/// (uv_write or uv_fs_write) that hasn't completed yet.
|
||||
pub fn hasPendingAsyncWrite(this: *const WindowsWriter) bool {
|
||||
return this.current_payload.isNotEmpty();
|
||||
}
|
||||
|
||||
fn isDone(this: *WindowsWriter) bool {
|
||||
// done is flags andd no more data queued? so we are done!
|
||||
return this.is_done and !this.hasPendingData();
|
||||
@@ -1354,16 +1312,6 @@ pub fn WindowsStreamingWriter(comptime Parent: type, function_table: anytype) ty
|
||||
// processSend before submitting the async write request.
|
||||
defer this.parent.deref();
|
||||
|
||||
// If the source was closed while a write was in-flight,
|
||||
// close() deferred onCloseSource(). Complete it now that
|
||||
// the write callback has safely finished.
|
||||
if (this.source == null) {
|
||||
this.current_payload.reset();
|
||||
this.outgoing.reset();
|
||||
this.onCloseSource();
|
||||
return;
|
||||
}
|
||||
|
||||
if (status.toError(.write)) |err| {
|
||||
this.last_write_result = .{ .err = err };
|
||||
log("onWrite() = {s}", .{err.name()});
|
||||
@@ -1421,16 +1369,6 @@ pub fn WindowsStreamingWriter(comptime Parent: type, function_table: anytype) ty
|
||||
|
||||
const this = bun.cast(*WindowsWriter, parent_ptr);
|
||||
|
||||
// If source was closed while write was in-flight, close()
|
||||
// deferred onCloseSource(). Complete it now and deref.
|
||||
if (this.source == null) {
|
||||
this.current_payload.reset();
|
||||
this.outgoing.reset();
|
||||
this.parent.deref();
|
||||
this.onCloseSource();
|
||||
return;
|
||||
}
|
||||
|
||||
if (was_canceled) {
|
||||
// Canceled write - reset buffers and deref to balance processSend ref
|
||||
this.current_payload.reset();
|
||||
|
||||
@@ -149,7 +149,7 @@ pub extern "kernel32" fn SetCurrentDirectoryW(
|
||||
lpPathName: win32.LPCWSTR,
|
||||
) callconv(.winapi) win32.BOOL;
|
||||
pub const SetCurrentDirectory = SetCurrentDirectoryW;
|
||||
pub extern "ntdll" fn RtlNtStatusToDosError(win32.NTSTATUS) callconv(.winapi) u32;
|
||||
pub extern "ntdll" fn RtlNtStatusToDosError(win32.NTSTATUS) callconv(.winapi) Win32Error;
|
||||
pub extern "advapi32" fn SaferiIsExecutableFileType(szFullPathname: win32.LPCWSTR, bFromShellExecute: win32.BOOLEAN) callconv(.winapi) win32.BOOL;
|
||||
// This was originally copied from Zig's standard library
|
||||
/// Codes are from https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/18d8fbe8-a967-4f1c-ae50-99ca8e491d2d
|
||||
@@ -2952,8 +2952,7 @@ pub const Win32Error = enum(u16) {
|
||||
pub const WSA_QOS_RESERVED_PETYPE: Win32Error = @enumFromInt(11031);
|
||||
|
||||
pub fn get() Win32Error {
|
||||
const raw: u16 = @intFromEnum(bun.windows.kernel32.GetLastError());
|
||||
return std.meta.intToEnum(Win32Error, raw) catch .MR_MID_NOT_FOUND;
|
||||
return @enumFromInt(@intFromEnum(bun.windows.kernel32.GetLastError()));
|
||||
}
|
||||
|
||||
pub fn int(this: Win32Error) u16 {
|
||||
@@ -2972,12 +2971,7 @@ pub const Win32Error = enum(u16) {
|
||||
}
|
||||
|
||||
pub fn fromNTStatus(status: win32.NTSTATUS) Win32Error {
|
||||
// RtlNtStatusToDosError returns a u32 Win32 error code that may not be
|
||||
// in our Win32Error enum subset. Safely convert to avoid panic on
|
||||
// invalid enum values.
|
||||
const raw = RtlNtStatusToDosError(status);
|
||||
if (raw > std.math.maxInt(u16)) return .MR_MID_NOT_FOUND;
|
||||
return std.meta.intToEnum(Win32Error, @as(u16, @intCast(raw))) catch .MR_MID_NOT_FOUND;
|
||||
return RtlNtStatusToDosError(status);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -541,7 +541,9 @@ describe("bundler", () => {
|
||||
});
|
||||
|
||||
// --- Ported from Rolldown: dynamic-import-entry ---
|
||||
// A submodule dynamically imports the barrel back
|
||||
// A submodule dynamically imports the barrel back. import() returns the full
|
||||
// module namespace — all barrel exports must be preserved, even if the
|
||||
// import() result is discarded (we can't statically prove it isn't used).
|
||||
|
||||
itBundled("barrel/DynamicImportInSubmodule", {
|
||||
files: {
|
||||
@@ -562,17 +564,103 @@ describe("bundler", () => {
|
||||
export const a = 'dyn-a';
|
||||
import('./index.js');
|
||||
`,
|
||||
// b.js has a syntax error — only a is imported, so b should be skipped
|
||||
"/node_modules/dynlib/b.js": /* js */ `
|
||||
export const b = <<<SYNTAX_ERROR>>>;
|
||||
export const b = 'dyn-b';
|
||||
`,
|
||||
},
|
||||
outdir: "/out",
|
||||
onAfterBundle(api) {
|
||||
api.expectFile("/out/entry.js").toContain("dyn-a");
|
||||
// b must be included — import() needs the full namespace
|
||||
api.expectFile("/out/entry.js").toContain("dyn-b");
|
||||
},
|
||||
});
|
||||
|
||||
// Dynamic import returns the full namespace at runtime — consumer can access any export.
|
||||
// When a file also has a static named import of the same barrel, the barrel
|
||||
// optimization must not drop exports the dynamic import might use.
|
||||
// Previously, the dynamic import was ignored if a static import already seeded
|
||||
// requested_exports, producing invalid JS (export clause referencing undeclared symbol).
|
||||
itBundled("barrel/DynamicImportWithStaticImportSameTarget", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
import { a } from "barrel";
|
||||
console.log(a);
|
||||
const run = async () => {
|
||||
const { b } = await import("barrel");
|
||||
console.log(b);
|
||||
};
|
||||
run();
|
||||
`,
|
||||
"/node_modules/barrel/package.json": JSON.stringify({
|
||||
name: "barrel",
|
||||
main: "./index.js",
|
||||
sideEffects: false,
|
||||
}),
|
||||
"/node_modules/barrel/index.js": /* js */ `
|
||||
export { a } from "./a.js";
|
||||
export { b } from "./b.js";
|
||||
`,
|
||||
"/node_modules/barrel/a.js": /* js */ `
|
||||
export const a = "A";
|
||||
`,
|
||||
"/node_modules/barrel/b.js": /* js */ `
|
||||
export const b = "B";
|
||||
`,
|
||||
},
|
||||
splitting: true,
|
||||
format: "esm",
|
||||
target: "bun",
|
||||
outdir: "/out",
|
||||
run: {
|
||||
stdout: "A\nB",
|
||||
},
|
||||
});
|
||||
|
||||
// Same as above but static and dynamic importers are in separate files.
|
||||
// This was parse-order dependent — if the static importer's
|
||||
// scheduleBarrelDeferredImports ran first, it seeded .partial and the dynamic
|
||||
// importer's escalation was skipped. Now import() always escalates to .all.
|
||||
itBundled("barrel/DynamicImportWithStaticImportSeparateFiles", {
|
||||
files: {
|
||||
"/static-user.js": /* js */ `
|
||||
import { a } from "barrel2";
|
||||
console.log(a);
|
||||
`,
|
||||
"/dynamic-user.js": /* js */ `
|
||||
const run = async () => {
|
||||
const { b } = await import("barrel2");
|
||||
console.log(b);
|
||||
};
|
||||
run();
|
||||
`,
|
||||
"/node_modules/barrel2/package.json": JSON.stringify({
|
||||
name: "barrel2",
|
||||
main: "./index.js",
|
||||
sideEffects: false,
|
||||
}),
|
||||
"/node_modules/barrel2/index.js": /* js */ `
|
||||
export { a } from "./a.js";
|
||||
export { b } from "./b.js";
|
||||
`,
|
||||
"/node_modules/barrel2/a.js": /* js */ `
|
||||
export const a = "A";
|
||||
`,
|
||||
"/node_modules/barrel2/b.js": /* js */ `
|
||||
export const b = "B";
|
||||
`,
|
||||
},
|
||||
entryPoints: ["/static-user.js", "/dynamic-user.js"],
|
||||
splitting: true,
|
||||
format: "esm",
|
||||
target: "bun",
|
||||
outdir: "/out",
|
||||
run: [
|
||||
{ file: "/out/static-user.js", stdout: "A" },
|
||||
{ file: "/out/dynamic-user.js", stdout: "B" },
|
||||
],
|
||||
});
|
||||
|
||||
// --- Ported from Rolldown: multiple-entries ---
|
||||
// Multiple entry points that each import different things from barrels
|
||||
|
||||
|
||||
@@ -2119,35 +2119,6 @@ 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);
|
||||
@@ -2186,7 +2157,6 @@ 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
|
||||
|
||||
@@ -829,15 +829,6 @@ describe("cleanup hooks", () => {
|
||||
expect(output).toContain("PASS: napi_create_threadsafe_function accepted AsyncContextFrame");
|
||||
});
|
||||
|
||||
it("should not crash when given an invalid pointer (BUN-1PYR)", async () => {
|
||||
// Test that napi_typeof validates cell pointers before dereferencing.
|
||||
// Native modules may accidentally pass garbage (e.g., a C string pointer)
|
||||
// as napi_value. This should return an error, not crash.
|
||||
// Bun-only: Node.js doesn't have this validation and would crash/UB.
|
||||
const output = await runOn(bunExe(), "test_napi_typeof_invalid_pointer", []);
|
||||
expect(output).toContain("PASS");
|
||||
});
|
||||
|
||||
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"))
|
||||
|
||||
Reference in New Issue
Block a user