mirror of
https://github.com/oven-sh/bun
synced 2026-02-04 07:58:54 +00:00
Compare commits
29 Commits
dylan/pyth
...
ali/securi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7809d12675 | ||
|
|
bec68a5317 | ||
|
|
1cd34c1b17 | ||
|
|
bc00cf1975 | ||
|
|
36d8ea81c0 | ||
|
|
0702666bef | ||
|
|
add3745cc4 | ||
|
|
47243f203a | ||
|
|
4e81b7c443 | ||
|
|
dcc6e7ba0c | ||
|
|
432400471a | ||
|
|
0a70826c05 | ||
|
|
aa63354af1 | ||
|
|
cded70abdc | ||
|
|
d2db5f962d | ||
|
|
d4e0930995 | ||
|
|
eb5291ce8a | ||
|
|
e3c72bfa4c | ||
|
|
dfe70c5b86 | ||
|
|
587551f170 | ||
|
|
8fca2a0ca8 | ||
|
|
5cb28aac3f | ||
|
|
35e5aad611 | ||
|
|
e2a9266154 | ||
|
|
f177d2d1d2 | ||
|
|
2f91c1ff4c | ||
|
|
1c083fd75e | ||
|
|
94a82d8abf | ||
|
|
8a8bf552f4 |
@@ -149,6 +149,7 @@ pub const FilePoll = struct {
|
||||
const Subprocess = jsc.Subprocess;
|
||||
const StaticPipeWriter = Subprocess.StaticPipeWriter.Poll;
|
||||
const ShellStaticPipeWriter = bun.shell.ShellSubprocess.StaticPipeWriter.Poll;
|
||||
const SecurityScanSubprocessStaticPipeWriter = bun.install.SecurityScanSubprocess.StaticPipeWriter.Poll;
|
||||
const FileSink = jsc.WebCore.FileSink.Poll;
|
||||
const TerminalPoll = bun.api.Terminal.Poll;
|
||||
const DNSResolver = bun.api.dns.Resolver;
|
||||
@@ -171,6 +172,7 @@ pub const FilePoll = struct {
|
||||
|
||||
StaticPipeWriter,
|
||||
ShellStaticPipeWriter,
|
||||
SecurityScanSubprocessStaticPipeWriter,
|
||||
|
||||
// ShellBufferedWriter,
|
||||
|
||||
@@ -373,6 +375,10 @@ pub const FilePoll = struct {
|
||||
var handler: *StaticPipeWriter = ptr.as(StaticPipeWriter);
|
||||
handler.onPoll(size_or_offset, poll.flags.contains(.hup));
|
||||
},
|
||||
@field(Owner.Tag, @typeName(SecurityScanSubprocessStaticPipeWriter)) => {
|
||||
var handler: *SecurityScanSubprocessStaticPipeWriter = ptr.as(SecurityScanSubprocessStaticPipeWriter);
|
||||
handler.onPoll(size_or_offset, poll.flags.contains(.hup));
|
||||
},
|
||||
@field(Owner.Tag, @typeName(FileSink)) => {
|
||||
var handler: *FileSink = ptr.as(FileSink);
|
||||
handler.onPoll(size_or_offset, poll.flags.contains(.hup));
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
|
||||
const scannerModuleName = "__SCANNER_MODULE__";
|
||||
const packages = __PACKAGES_JSON__;
|
||||
const suppressError = __SUPPRESS_ERROR__;
|
||||
|
||||
type IPCMessage =
|
||||
@@ -10,26 +9,55 @@ type IPCMessage =
|
||||
| { type: "error"; code: "INVALID_VERSION"; message: string }
|
||||
| { type: "error"; code: "SCAN_FAILED"; message: string };
|
||||
|
||||
const IPC_PIPE_FD = 3;
|
||||
// Two pipes for IPC:
|
||||
// - fd 3: output - child writes response here
|
||||
// - fd 4: input - child reads JSON package list here (reads until EOF)
|
||||
const IPC_OUTPUT_FD = 3;
|
||||
const IPC_INPUT_FD = 4;
|
||||
|
||||
function writeAndExit(message: IPCMessage): never {
|
||||
function sendAndExit(message: IPCMessage): never {
|
||||
const data = JSON.stringify(message);
|
||||
|
||||
for (let remaining = data; remaining.length > 0; ) {
|
||||
const written = fs.writeSync(IPC_PIPE_FD, remaining);
|
||||
|
||||
const written = fs.writeSync(IPC_OUTPUT_FD, remaining);
|
||||
if (written === 0) {
|
||||
console.error("Failed to write to IPC pipe");
|
||||
process.exit(1);
|
||||
}
|
||||
remaining = remaining.slice(written);
|
||||
}
|
||||
|
||||
fs.closeSync(IPC_PIPE_FD);
|
||||
|
||||
fs.closeSync(IPC_OUTPUT_FD);
|
||||
process.exit(message.type === "error" ? 1 : 0);
|
||||
}
|
||||
|
||||
// Read packages JSON from fd 4 (reads until EOF when parent closes the pipe)
|
||||
let packages: Bun.Security.Package[];
|
||||
let packagesJson: string = "";
|
||||
|
||||
try {
|
||||
packagesJson = await Bun.file(IPC_INPUT_FD).text();
|
||||
} catch (error) {
|
||||
const message = `Failed to read packages from FD ${IPC_INPUT_FD}: ${error instanceof Error ? error.message : String(error)}`;
|
||||
sendAndExit({
|
||||
type: "error",
|
||||
code: "SCAN_FAILED",
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
packages = JSON.parse(packagesJson);
|
||||
if (!Array.isArray(packages)) {
|
||||
throw new Error("Expected packages to be an array");
|
||||
}
|
||||
} catch (error) {
|
||||
const message = `Failed to parse packages JSON: ${error instanceof Error ? error.message : String(error)}`;
|
||||
sendAndExit({
|
||||
type: "error",
|
||||
code: "SCAN_FAILED",
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
let scanner: Bun.Security.Scanner;
|
||||
|
||||
try {
|
||||
@@ -41,13 +69,13 @@ try {
|
||||
console.error(msg);
|
||||
}
|
||||
|
||||
writeAndExit({
|
||||
sendAndExit({
|
||||
type: "error",
|
||||
code: "MODULE_NOT_FOUND",
|
||||
module: scannerModuleName,
|
||||
});
|
||||
} else {
|
||||
writeAndExit({
|
||||
sendAndExit({
|
||||
type: "error",
|
||||
code: "SCAN_FAILED",
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
@@ -61,7 +89,7 @@ try {
|
||||
}
|
||||
|
||||
if (scanner.version !== "1") {
|
||||
writeAndExit({
|
||||
sendAndExit({
|
||||
type: "error",
|
||||
code: "INVALID_VERSION",
|
||||
message: `Security scanner must be version 1, got version ${scanner.version}`,
|
||||
@@ -78,13 +106,13 @@ try {
|
||||
throw new Error("Security scanner must return an array of advisories");
|
||||
}
|
||||
|
||||
writeAndExit({ type: "result", advisories: result });
|
||||
sendAndExit({ type: "result", advisories: result });
|
||||
} catch (error) {
|
||||
if (!suppressError) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
writeAndExit({
|
||||
sendAndExit({
|
||||
type: "error",
|
||||
code: "SCAN_FAILED",
|
||||
message: error instanceof Error ? error.message : "Unknown error occurred",
|
||||
|
||||
@@ -350,8 +350,8 @@ const PackageCollector = struct {
|
||||
const QueueItem = struct {
|
||||
pkg_id: PackageID,
|
||||
dep_id: DependencyID,
|
||||
pkg_path: std.array_list.Managed(PackageID),
|
||||
dep_path: std.array_list.Managed(DependencyID),
|
||||
pkg_path: std.ArrayList(PackageID),
|
||||
dep_path: std.ArrayList(DependencyID),
|
||||
};
|
||||
|
||||
pub fn init(manager: *PackageManager) PackageCollector {
|
||||
@@ -395,12 +395,12 @@ const PackageCollector = struct {
|
||||
|
||||
if ((try this.dedupe.getOrPut(dep_pkg_id)).found_existing) continue;
|
||||
|
||||
var pkg_path_buf = std.array_list.Managed(PackageID).init(this.manager.allocator);
|
||||
try pkg_path_buf.append(root_pkg_id);
|
||||
try pkg_path_buf.append(dep_pkg_id);
|
||||
var pkg_path_buf: std.ArrayList(PackageID) = .{};
|
||||
try pkg_path_buf.append(this.manager.allocator, root_pkg_id);
|
||||
try pkg_path_buf.append(this.manager.allocator, dep_pkg_id);
|
||||
|
||||
var dep_path_buf = std.array_list.Managed(DependencyID).init(this.manager.allocator);
|
||||
try dep_path_buf.append(dep_id);
|
||||
var dep_path_buf: std.ArrayList(DependencyID) = .{};
|
||||
try dep_path_buf.append(this.manager.allocator, dep_id);
|
||||
|
||||
try this.queue.writeItem(.{
|
||||
.pkg_id = dep_pkg_id,
|
||||
@@ -427,12 +427,12 @@ const PackageCollector = struct {
|
||||
|
||||
if ((try this.dedupe.getOrPut(dep_pkg_id)).found_existing) continue;
|
||||
|
||||
var pkg_path_buf = std.array_list.Managed(PackageID).init(this.manager.allocator);
|
||||
try pkg_path_buf.append(pkg_id);
|
||||
try pkg_path_buf.append(dep_pkg_id);
|
||||
var pkg_path_buf: std.ArrayList(PackageID) = .{};
|
||||
try pkg_path_buf.append(this.manager.allocator, pkg_id);
|
||||
try pkg_path_buf.append(this.manager.allocator, dep_pkg_id);
|
||||
|
||||
var dep_path_buf = std.array_list.Managed(DependencyID).init(this.manager.allocator);
|
||||
try dep_path_buf.append(dep_id);
|
||||
var dep_path_buf: std.ArrayList(DependencyID) = .{};
|
||||
try dep_path_buf.append(this.manager.allocator, dep_id);
|
||||
|
||||
try this.queue.writeItem(.{
|
||||
.pkg_id = dep_pkg_id,
|
||||
@@ -479,14 +479,14 @@ const PackageCollector = struct {
|
||||
if (update_dep_id == invalid_dependency_id) continue;
|
||||
if ((try this.dedupe.getOrPut(update_pkg_id)).found_existing) continue;
|
||||
|
||||
var initial_pkg_path = std.array_list.Managed(PackageID).init(this.manager.allocator);
|
||||
var initial_pkg_path: std.ArrayList(PackageID) = .{};
|
||||
if (parent_pkg_id != invalid_package_id) {
|
||||
try initial_pkg_path.append(parent_pkg_id);
|
||||
try initial_pkg_path.append(this.manager.allocator, parent_pkg_id);
|
||||
}
|
||||
try initial_pkg_path.append(update_pkg_id);
|
||||
try initial_pkg_path.append(this.manager.allocator, update_pkg_id);
|
||||
|
||||
var initial_dep_path = std.array_list.Managed(DependencyID).init(this.manager.allocator);
|
||||
try initial_dep_path.append(update_dep_id);
|
||||
var initial_dep_path: std.ArrayList(DependencyID) = .{};
|
||||
try initial_dep_path.append(this.manager.allocator, update_dep_id);
|
||||
|
||||
try this.queue.writeItem(.{
|
||||
.pkg_id = update_pkg_id,
|
||||
@@ -504,17 +504,18 @@ const PackageCollector = struct {
|
||||
const pkg_dependencies = pkgs.items(.dependencies);
|
||||
|
||||
while (this.queue.readItem()) |item| {
|
||||
defer item.pkg_path.deinit();
|
||||
defer item.dep_path.deinit();
|
||||
var mutable_item = item;
|
||||
defer mutable_item.pkg_path.deinit(this.manager.allocator);
|
||||
defer mutable_item.dep_path.deinit(this.manager.allocator);
|
||||
|
||||
const pkg_id = item.pkg_id;
|
||||
_ = item.dep_id; // Could be useful in the future for dependency-specific processing
|
||||
const pkg_id = mutable_item.pkg_id;
|
||||
_ = mutable_item.dep_id; // Could be useful in the future for dependency-specific processing
|
||||
|
||||
const pkg_path_copy = try this.manager.allocator.alloc(PackageID, item.pkg_path.items.len);
|
||||
@memcpy(pkg_path_copy, item.pkg_path.items);
|
||||
const pkg_path_copy = try this.manager.allocator.alloc(PackageID, mutable_item.pkg_path.items.len);
|
||||
@memcpy(pkg_path_copy, mutable_item.pkg_path.items);
|
||||
|
||||
const dep_path_copy = try this.manager.allocator.alloc(DependencyID, item.dep_path.items.len);
|
||||
@memcpy(dep_path_copy, item.dep_path.items);
|
||||
const dep_path_copy = try this.manager.allocator.alloc(DependencyID, mutable_item.dep_path.items.len);
|
||||
@memcpy(dep_path_copy, mutable_item.dep_path.items);
|
||||
|
||||
try this.package_paths.put(pkg_id, .{
|
||||
.pkg_path = pkg_path_copy,
|
||||
@@ -533,13 +534,13 @@ const PackageCollector = struct {
|
||||
|
||||
if ((try this.dedupe.getOrPut(next_pkg_id)).found_existing) continue;
|
||||
|
||||
var extended_pkg_path = std.array_list.Managed(PackageID).init(this.manager.allocator);
|
||||
try extended_pkg_path.appendSlice(item.pkg_path.items);
|
||||
try extended_pkg_path.append(next_pkg_id);
|
||||
var extended_pkg_path: std.ArrayList(PackageID) = .{};
|
||||
try extended_pkg_path.appendSlice(this.manager.allocator, mutable_item.pkg_path.items);
|
||||
try extended_pkg_path.append(this.manager.allocator, next_pkg_id);
|
||||
|
||||
var extended_dep_path = std.array_list.Managed(DependencyID).init(this.manager.allocator);
|
||||
try extended_dep_path.appendSlice(item.dep_path.items);
|
||||
try extended_dep_path.append(next_dep_id);
|
||||
var extended_dep_path: std.ArrayList(DependencyID) = .{};
|
||||
try extended_dep_path.appendSlice(this.manager.allocator, mutable_item.dep_path.items);
|
||||
try extended_dep_path.append(this.manager.allocator, next_dep_id);
|
||||
|
||||
try this.queue.writeItem(.{
|
||||
.pkg_id = next_pkg_id,
|
||||
@@ -557,8 +558,8 @@ const JSONBuilder = struct {
|
||||
collector: *PackageCollector,
|
||||
|
||||
pub fn buildPackageJSON(this: JSONBuilder) ![]const u8 {
|
||||
var json_buf = std.array_list.Managed(u8).init(this.manager.allocator);
|
||||
var writer = json_buf.writer();
|
||||
var json_buf: std.ArrayList(u8) = .{};
|
||||
var writer = json_buf.writer(this.manager.allocator);
|
||||
|
||||
const pkgs = this.manager.lockfile.packages.slice();
|
||||
const pkg_names = pkgs.items(.name);
|
||||
@@ -615,7 +616,7 @@ const JSONBuilder = struct {
|
||||
}
|
||||
|
||||
try writer.writeAll("\n]");
|
||||
return json_buf.toOwnedSlice();
|
||||
return json_buf.toOwnedSlice(this.manager.allocator);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -660,37 +661,26 @@ fn attemptSecurityScanWithRetry(manager: *PackageManager, security_scanner: []co
|
||||
const json_data = try json_builder.buildPackageJSON();
|
||||
defer manager.allocator.free(json_data);
|
||||
|
||||
var code = std.array_list.Managed(u8).init(manager.allocator);
|
||||
defer code.deinit();
|
||||
var code: std.ArrayList(u8) = .{};
|
||||
defer code.deinit(manager.allocator);
|
||||
|
||||
var temp_source: []const u8 = scanner_entry_source;
|
||||
|
||||
const scanner_placeholder = "__SCANNER_MODULE__";
|
||||
if (std.mem.indexOf(u8, temp_source, scanner_placeholder)) |index| {
|
||||
try code.appendSlice(temp_source[0..index]);
|
||||
try code.appendSlice(security_scanner);
|
||||
try code.appendSlice(temp_source[index + scanner_placeholder.len ..]);
|
||||
temp_source = code.items;
|
||||
}
|
||||
|
||||
const packages_placeholder = "__PACKAGES_JSON__";
|
||||
if (std.mem.indexOf(u8, temp_source, packages_placeholder)) |index| {
|
||||
var new_code = std.array_list.Managed(u8).init(manager.allocator);
|
||||
try new_code.appendSlice(temp_source[0..index]);
|
||||
try new_code.appendSlice(json_data);
|
||||
try new_code.appendSlice(temp_source[index + packages_placeholder.len ..]);
|
||||
code.deinit();
|
||||
code = new_code;
|
||||
try code.appendSlice(manager.allocator, temp_source[0..index]);
|
||||
try code.appendSlice(manager.allocator, security_scanner);
|
||||
try code.appendSlice(manager.allocator, temp_source[index + scanner_placeholder.len ..]);
|
||||
temp_source = code.items;
|
||||
}
|
||||
|
||||
const suppress_placeholder = "__SUPPRESS_ERROR__";
|
||||
if (std.mem.indexOf(u8, temp_source, suppress_placeholder)) |index| {
|
||||
var new_code = std.array_list.Managed(u8).init(manager.allocator);
|
||||
try new_code.appendSlice(temp_source[0..index]);
|
||||
try new_code.appendSlice(if (suppress_error_output) "true" else "false");
|
||||
try new_code.appendSlice(temp_source[index + suppress_placeholder.len ..]);
|
||||
code.deinit();
|
||||
var new_code: std.ArrayList(u8) = .{};
|
||||
try new_code.appendSlice(manager.allocator, temp_source[0..index]);
|
||||
try new_code.appendSlice(manager.allocator, if (suppress_error_output) "true" else "false");
|
||||
try new_code.appendSlice(manager.allocator, temp_source[index + suppress_placeholder.len ..]);
|
||||
code.deinit(manager.allocator);
|
||||
code = new_code;
|
||||
}
|
||||
|
||||
@@ -730,28 +720,22 @@ pub const SecurityScanSubprocess = struct {
|
||||
json_data: []const u8,
|
||||
process: ?*bun.spawn.Process = null,
|
||||
ipc_reader: bun.io.BufferedReader = bun.io.BufferedReader.init(@This()),
|
||||
ipc_data: std.array_list.Managed(u8),
|
||||
stderr_data: std.array_list.Managed(u8),
|
||||
ipc_data: std.ArrayList(u8),
|
||||
stderr_data: std.ArrayList(u8),
|
||||
has_process_exited: bool = false,
|
||||
has_received_ipc: bool = false,
|
||||
exit_status: ?bun.spawn.Status = null,
|
||||
remaining_fds: i8 = 0,
|
||||
stdin_writer: ?*StaticPipeWriter = null,
|
||||
|
||||
pub const new = bun.TrivialNew(@This());
|
||||
pub const StaticPipeWriter = jsc.Subprocess.NewStaticPipeWriter(@This());
|
||||
|
||||
pub fn spawn(this: *SecurityScanSubprocess) !void {
|
||||
this.ipc_data = std.array_list.Managed(u8).init(this.manager.allocator);
|
||||
this.stderr_data = std.array_list.Managed(u8).init(this.manager.allocator);
|
||||
this.ipc_data = .{};
|
||||
this.stderr_data = .{};
|
||||
this.ipc_reader.setParent(this);
|
||||
|
||||
const pipe_result = bun.sys.pipe();
|
||||
const pipe_fds = switch (pipe_result) {
|
||||
.err => {
|
||||
return error.IPCPipeFailed;
|
||||
},
|
||||
.result => |fds| fds,
|
||||
};
|
||||
|
||||
const exec_path = try bun.selfExePath();
|
||||
|
||||
var argv = [_]?[*:0]const u8{
|
||||
@@ -768,35 +752,148 @@ pub const SecurityScanSubprocess = struct {
|
||||
|
||||
const spawn_cwd = FileSystem.instance.top_level_dir;
|
||||
|
||||
if (comptime bun.Environment.isWindows) {
|
||||
try this.spawnWindows(&argv, spawn_cwd);
|
||||
} else {
|
||||
try this.spawnPosix(&argv, spawn_cwd);
|
||||
}
|
||||
}
|
||||
|
||||
fn spawnPosix(this: *SecurityScanSubprocess, argv: *[5]?[*:0]const u8, spawn_cwd: []const u8) !void {
|
||||
// Create two pipes for IPC:
|
||||
// - fd 3: child writes response → parent reads (raw pipe)
|
||||
// - fd 4: parent writes JSON → child reads (socketpair, closed after write = EOF)
|
||||
const ipc_output_fds = switch (bun.sys.pipe()) {
|
||||
.err => return error.IPCPipeFailed,
|
||||
.result => |fds| fds,
|
||||
};
|
||||
|
||||
const extra_fds = [_]bun.spawn.SpawnOptions.Stdio{
|
||||
.{ .pipe = ipc_output_fds[1] }, // fd 3: child writes here
|
||||
.ipc, // fd 4: parent writes JSON here
|
||||
};
|
||||
|
||||
const spawn_options = bun.spawn.SpawnOptions{
|
||||
.stdout = .inherit,
|
||||
.stderr = .inherit,
|
||||
.stdin = .inherit,
|
||||
.cwd = spawn_cwd,
|
||||
.extra_fds = &.{.{ .pipe = pipe_fds[1] }},
|
||||
.windows = if (Environment.isWindows) .{
|
||||
.loop = jsc.EventLoopHandle.init(&this.manager.event_loop),
|
||||
.extra_fds = &extra_fds,
|
||||
};
|
||||
|
||||
var spawned = try (try bun.spawn.spawnProcess(&spawn_options, @ptrCast(argv), @ptrCast(std.os.environ.ptr))).unwrap();
|
||||
|
||||
// Close the write end of the output pipe in the parent
|
||||
ipc_output_fds[1].close();
|
||||
|
||||
// Get the fd for reading child's output and the socketpair for writing JSON
|
||||
const ipc_read_fd = ipc_output_fds[0];
|
||||
const json_write_fd = spawned.extra_pipes.items[1];
|
||||
|
||||
this.remaining_fds = 1;
|
||||
_ = bun.sys.setNonblocking(ipc_read_fd);
|
||||
this.ipc_reader.flags.nonblocking = true;
|
||||
this.ipc_reader.flags.socket = false;
|
||||
try this.ipc_reader.start(ipc_read_fd, true).unwrap();
|
||||
|
||||
// Set up process and write JSON data to child
|
||||
var process = spawned.toProcess(&this.manager.event_loop, false);
|
||||
this.process = process;
|
||||
process.setExitHandler(this);
|
||||
|
||||
const json_data_copy = try this.manager.allocator.dupe(u8, this.json_data);
|
||||
const json_source = jsc.Subprocess.Source{
|
||||
.blob = jsc.WebCore.Blob.Any.fromOwnedSlice(this.manager.allocator, json_data_copy),
|
||||
};
|
||||
|
||||
this.stdin_writer = StaticPipeWriter.create(&this.manager.event_loop, this, json_write_fd, json_source);
|
||||
errdefer {
|
||||
if (this.stdin_writer) |writer| {
|
||||
writer.source.detach();
|
||||
writer.deref();
|
||||
this.stdin_writer = null;
|
||||
}
|
||||
}
|
||||
|
||||
switch (this.stdin_writer.?.start()) {
|
||||
.err => |err| {
|
||||
Output.errGeneric("Failed to start JSON pipe writer: {f}", .{err});
|
||||
return error.JSONPipeWriterFailed;
|
||||
},
|
||||
.result => {},
|
||||
}
|
||||
|
||||
switch (process.watchOrReap()) {
|
||||
.err => {
|
||||
return error.ProcessWatchFailed;
|
||||
},
|
||||
.result => {},
|
||||
}
|
||||
}
|
||||
|
||||
fn spawnWindows(this: *SecurityScanSubprocess, argv: *[5]?[*:0]const u8, spawn_cwd: []const u8) !void {
|
||||
const uv = bun.windows.libuv;
|
||||
|
||||
// On Windows, we use two libuv pipes for IPC:
|
||||
// - fd 3: child writes response → parent reads (buffer pipe)
|
||||
// - fd 4: parent writes JSON → child reads (ipc pipe)
|
||||
const output_pipe = try bun.default_allocator.create(uv.Pipe);
|
||||
errdefer bun.default_allocator.destroy(output_pipe);
|
||||
|
||||
const input_pipe = try bun.default_allocator.create(uv.Pipe);
|
||||
errdefer bun.default_allocator.destroy(input_pipe);
|
||||
|
||||
const extra_fds = [_]bun.spawn.SpawnOptions.Stdio{
|
||||
.{ .buffer = output_pipe }, // fd 3: child writes here
|
||||
.{ .ipc = input_pipe }, // fd 4: parent writes JSON here
|
||||
};
|
||||
|
||||
const spawn_options = bun.spawn.SpawnOptions{
|
||||
.stdout = .inherit,
|
||||
.stderr = .inherit,
|
||||
.stdin = .inherit,
|
||||
.cwd = spawn_cwd,
|
||||
.extra_fds = &extra_fds,
|
||||
.windows = .{
|
||||
.loop = .{ .mini = &this.manager.event_loop.mini },
|
||||
},
|
||||
};
|
||||
|
||||
var spawned = try (try bun.spawn.spawnProcess(&spawn_options, @ptrCast(&argv), @ptrCast(std.os.environ.ptr))).unwrap();
|
||||
var spawned = try (try bun.spawn.spawnProcess(&spawn_options, @ptrCast(argv), @ptrCast(std.os.environ.ptr))).unwrap();
|
||||
|
||||
pipe_fds[1].close();
|
||||
const result_output_pipe = spawned.extra_pipes.items[0].buffer;
|
||||
const result_input_pipe = spawned.extra_pipes.items[1].buffer;
|
||||
|
||||
if (comptime bun.Environment.isPosix) {
|
||||
_ = bun.sys.setNonblocking(pipe_fds[0]);
|
||||
}
|
||||
this.remaining_fds = 1;
|
||||
this.ipc_reader.flags.nonblocking = true;
|
||||
if (comptime bun.Environment.isPosix) {
|
||||
this.ipc_reader.flags.socket = false;
|
||||
}
|
||||
try this.ipc_reader.start(pipe_fds[0], true).unwrap();
|
||||
try this.ipc_reader.startWithPipe(result_output_pipe).unwrap();
|
||||
|
||||
var process = spawned.toProcess(&this.manager.event_loop, false);
|
||||
this.process = process;
|
||||
process.setExitHandler(this);
|
||||
|
||||
const json_data_copy = try this.manager.allocator.dupe(u8, this.json_data);
|
||||
const json_source = jsc.Subprocess.Source{
|
||||
.blob = jsc.WebCore.Blob.Any.fromOwnedSlice(this.manager.allocator, json_data_copy),
|
||||
};
|
||||
|
||||
const stdio_result = jsc.Subprocess.StdioResult{ .buffer = result_input_pipe };
|
||||
this.stdin_writer = StaticPipeWriter.create(&this.manager.event_loop, this, stdio_result, json_source);
|
||||
errdefer {
|
||||
if (this.stdin_writer) |writer| {
|
||||
writer.source.detach();
|
||||
writer.deref();
|
||||
this.stdin_writer = null;
|
||||
}
|
||||
}
|
||||
|
||||
switch (this.stdin_writer.?.start()) {
|
||||
.err => |err| {
|
||||
Output.errGeneric("Failed to start JSON pipe writer: {f}", .{err});
|
||||
return error.JSONPipeWriterFailed;
|
||||
},
|
||||
.result => {},
|
||||
}
|
||||
|
||||
switch (process.watchOrReap()) {
|
||||
.err => {
|
||||
return error.ProcessWatchFailed;
|
||||
@@ -809,6 +906,14 @@ pub const SecurityScanSubprocess = struct {
|
||||
return this.has_process_exited and this.remaining_fds == 0;
|
||||
}
|
||||
|
||||
pub fn onCloseIO(this: *SecurityScanSubprocess, _: jsc.Subprocess.StdioKind) void {
|
||||
if (this.stdin_writer) |writer| {
|
||||
writer.source.detach();
|
||||
writer.deref();
|
||||
this.stdin_writer = null;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn eventLoop(this: *const SecurityScanSubprocess) *jsc.AnyEventLoop {
|
||||
return &this.manager.event_loop;
|
||||
}
|
||||
@@ -833,7 +938,7 @@ pub const SecurityScanSubprocess = struct {
|
||||
}
|
||||
|
||||
pub fn onStderrChunk(this: *SecurityScanSubprocess, chunk: []const u8) void {
|
||||
bun.handleOom(this.stderr_data.appendSlice(chunk));
|
||||
bun.handleOom(this.stderr_data.appendSlice(this.manager.allocator, chunk));
|
||||
}
|
||||
|
||||
pub fn getReadBuffer(this: *SecurityScanSubprocess) []u8 {
|
||||
@@ -847,7 +952,7 @@ pub const SecurityScanSubprocess = struct {
|
||||
|
||||
pub fn onReadChunk(this: *SecurityScanSubprocess, chunk: []const u8, hasMore: bun.io.ReadState) bool {
|
||||
_ = hasMore;
|
||||
bun.handleOom(this.ipc_data.appendSlice(chunk));
|
||||
bun.handleOom(this.ipc_data.appendSlice(this.manager.allocator, chunk));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -865,8 +970,8 @@ pub const SecurityScanSubprocess = struct {
|
||||
_ = command_ctx; // Reserved for future use
|
||||
_ = original_cwd; // Reserved for future use
|
||||
defer {
|
||||
this.ipc_data.deinit();
|
||||
this.stderr_data.deinit();
|
||||
this.ipc_data.deinit(this.manager.allocator);
|
||||
this.stderr_data.deinit(this.manager.allocator);
|
||||
}
|
||||
|
||||
if (this.exit_status == null) {
|
||||
@@ -1076,8 +1181,8 @@ pub const SecurityScanSubprocess = struct {
|
||||
};
|
||||
|
||||
fn parseSecurityAdvisoriesFromExpr(manager: *PackageManager, advisories_expr: bun.js_parser.Expr, package_paths: *std.AutoArrayHashMap(PackageID, PackagePath)) ![]SecurityAdvisory {
|
||||
var advisories_list = std.array_list.Managed(SecurityAdvisory).init(manager.allocator);
|
||||
defer advisories_list.deinit();
|
||||
var advisories_list: std.ArrayList(SecurityAdvisory) = .{};
|
||||
defer advisories_list.deinit(manager.allocator);
|
||||
|
||||
if (advisories_expr.data != .e_array) {
|
||||
Output.errGeneric("Security scanner 'advisories' field must be an array, got: {s}", .{@tagName(advisories_expr.data)});
|
||||
@@ -1170,10 +1275,10 @@ fn parseSecurityAdvisoriesFromExpr(manager: *PackageManager, advisories_expr: bu
|
||||
.pkg_path = pkg_path,
|
||||
};
|
||||
|
||||
try advisories_list.append(advisory);
|
||||
try advisories_list.append(manager.allocator, advisory);
|
||||
}
|
||||
|
||||
return try advisories_list.toOwnedSlice();
|
||||
return try advisories_list.toOwnedSlice(manager.allocator);
|
||||
}
|
||||
|
||||
const HoistedInstall = @import("../hoisted_install.zig");
|
||||
|
||||
@@ -30,13 +30,15 @@ function test(
|
||||
bunfigScanner?: string | false;
|
||||
packages?: string[];
|
||||
scannerFile?: string;
|
||||
packageJson?: object;
|
||||
customRegistry?: (urls: string[]) => any;
|
||||
},
|
||||
) {
|
||||
it(
|
||||
name,
|
||||
async () => {
|
||||
const urls: string[] = [];
|
||||
setHandler(dummyRegistry(urls));
|
||||
setHandler(options.customRegistry ? options.customRegistry(urls) : dummyRegistry(urls));
|
||||
|
||||
const scannerPath = options.scannerFile || "./scanner.ts";
|
||||
if (typeof options.scanner === "string") {
|
||||
@@ -55,11 +57,14 @@ function test(
|
||||
await write("./bunfig.toml", `${bunfig}\n[install.security]\nscanner = "${scannerPath}"`);
|
||||
}
|
||||
|
||||
await write("package.json", {
|
||||
name: "my-app",
|
||||
version: "1.0.0",
|
||||
dependencies: {},
|
||||
});
|
||||
await write(
|
||||
"package.json",
|
||||
options.packageJson ?? {
|
||||
name: "my-app",
|
||||
version: "1.0.0",
|
||||
dependencies: {},
|
||||
},
|
||||
);
|
||||
|
||||
const expectedExitCode = options.expectedExitCode ?? (options.fails ? 1 : 0);
|
||||
const packages = options.packages ?? ["bar"];
|
||||
@@ -681,3 +686,78 @@ describe("Package Resolution", () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe("Large payload via ipc pipes", () => {
|
||||
let tgzTempDir: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const { tmpdir } = await import("node:os");
|
||||
const { mkdtemp } = await import("node:fs/promises");
|
||||
const { join } = await import("node:path");
|
||||
tgzTempDir = await mkdtemp(join(tmpdir(), "bun-test-tgz-"));
|
||||
|
||||
// Copy bar-0.0.2.tgz and create test-pkg-* copies in temp dir
|
||||
const testDir = import.meta.dir;
|
||||
const barTarball = `${testDir}/bar-0.0.2.tgz`;
|
||||
const barContent = Bun.file(barTarball);
|
||||
|
||||
// Create 10,000 packages to generate ~1.25MB of JSON
|
||||
// (each entry is ~125 bytes, so 10k * 125 = 1.25MB)
|
||||
for (let i = 0; i < 10000; i++) {
|
||||
const targetPath = `${tgzTempDir}/test-pkg-${i}-0.0.2.tgz`;
|
||||
await Bun.write(targetPath, barContent);
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
const { rm } = await import("node:fs/promises");
|
||||
try {
|
||||
await rm(tgzTempDir, { recursive: true, force: true });
|
||||
} catch (e) {}
|
||||
});
|
||||
|
||||
test("handles JSON data larger than max arg length (>1MB)", {
|
||||
testTimeout: 60_000,
|
||||
scanner: async ({ packages }) => {
|
||||
const jsonSize = JSON.stringify(packages).length;
|
||||
console.log(`Received JSON payload of ${jsonSize} bytes from ${packages.length} packages via stdin`);
|
||||
|
||||
if (jsonSize < 1024 * 1024) {
|
||||
throw new Error(`Expected JSON payload to exceed 1MB, got ${jsonSize} bytes`);
|
||||
}
|
||||
|
||||
if (packages.length === 0) {
|
||||
throw new Error("Expected to receive packages via stdin");
|
||||
}
|
||||
|
||||
return [];
|
||||
},
|
||||
|
||||
packageJson: (() => {
|
||||
const dependencies: Record<string, string> = {};
|
||||
|
||||
for (let i = 0; i < 10000; i++) {
|
||||
dependencies[`test-pkg-${i}`] = "0.0.2";
|
||||
}
|
||||
return {
|
||||
name: "my-app",
|
||||
version: "1.0.0",
|
||||
dependencies,
|
||||
};
|
||||
})(),
|
||||
packages: [],
|
||||
customRegistry: urls => dummyRegistry(urls, { "0.0.2": {} }, 0, tgzTempDir),
|
||||
expectedExitCode: 0,
|
||||
expect: ({ out }) => {
|
||||
expect(out).toContain("Received JSON payload");
|
||||
expect(out).toContain("via stdin");
|
||||
expect(out).toContain("packages");
|
||||
|
||||
const match = out.match(/Received JSON payload of (\d+) bytes/);
|
||||
if (match) {
|
||||
const bytes = parseInt(match[1], 10);
|
||||
expect(bytes).toBeGreaterThan(1024 * 1024); // >1MB
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,7 +36,12 @@ export function read(path: string) {
|
||||
return Bun.file(join(package_dir, path));
|
||||
}
|
||||
|
||||
export function dummyRegistry(urls: string[], info: any = { "0.0.2": {} }, numberOfTimesTo500PerURL = 0) {
|
||||
export function dummyRegistry(
|
||||
urls: string[],
|
||||
info: any = { "0.0.2": {} },
|
||||
numberOfTimesTo500PerURL = 0,
|
||||
tgzDir?: string,
|
||||
) {
|
||||
let retryCountsByURL = new Map<string, number>();
|
||||
const _handler: Handler = async request => {
|
||||
urls.push(request.url);
|
||||
@@ -57,7 +62,8 @@ export function dummyRegistry(urls: string[], info: any = { "0.0.2": {} }, numbe
|
||||
|
||||
expect(request.method).toBe("GET");
|
||||
if (url.endsWith(".tgz")) {
|
||||
return new Response(file(join(import.meta.dir, basename(url).toLowerCase())), { status });
|
||||
const tgzPath = join(tgzDir ?? import.meta.dir, basename(url).toLowerCase());
|
||||
return new Response(file(tgzPath), { status });
|
||||
}
|
||||
expect(request.headers.get("accept")).toBe(
|
||||
"application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*",
|
||||
|
||||
Reference in New Issue
Block a user