-
Notifications
You must be signed in to change notification settings - Fork 4.1k
fix(install): pass security scanner package data via stdin pipe #27717
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -673,17 +673,6 @@ fn attemptSecurityScanWithRetry(manager: *PackageManager, security_scanner: []co | |
| 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; | ||
| 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); | ||
|
|
@@ -724,6 +713,19 @@ fn attemptSecurityScanWithRetry(manager: *PackageManager, security_scanner: []co | |
| return try scanner.handleResults(&collector.package_paths, start_time, packages_scanned, security_scanner, security_scanner_pkg_id, command_ctx, original_cwd, is_retry); | ||
| } | ||
|
|
||
| fn writeAllToPipe(fd: bun.FileDescriptor, data: []const u8) void { | ||
| var remaining = data; | ||
| while (remaining.len > 0) { | ||
| switch (bun.sys.write(fd, remaining)) { | ||
| .result => |written| { | ||
| if (written == 0) return; | ||
| remaining = remaining[written..]; | ||
| }, | ||
| .err => return, | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+716
to
+727
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The function returns without indication when Consider returning a boolean or error indicator so the caller can handle failures appropriately. π§ Proposed fix to return success/failure-fn writeAllToPipe(fd: bun.FileDescriptor, data: []const u8) void {
+fn writeAllToPipe(fd: bun.FileDescriptor, data: []const u8) bool {
var remaining = data;
while (remaining.len > 0) {
switch (bun.sys.write(fd, remaining)) {
.result => |written| {
- if (written == 0) return;
+ if (written == 0) return false;
remaining = remaining[written..];
},
- .err => return,
+ .err => return false,
}
}
+ return true;
}Then in - writeAllToPipe(stdin_pipe_fds[1], this.json_data);
+ if (!writeAllToPipe(stdin_pipe_fds[1], this.json_data)) {
+ stdin_pipe_fds[1].close();
+ return error.StdinWriteFailed;
+ }π€ Prompt for AI Agents |
||
|
|
||
| pub const SecurityScanSubprocess = struct { | ||
| manager: *PackageManager, | ||
| code: []const u8, | ||
|
|
@@ -752,6 +754,17 @@ pub const SecurityScanSubprocess = struct { | |
| .result => |fds| fds, | ||
| }; | ||
|
|
||
| // Create a stdin pipe to pass package JSON data to the child process. | ||
| // This avoids embedding the (potentially large) JSON in the -e argument, | ||
| // which can exceed the OS per-argument size limit (MAX_ARG_STRLEN = 128KB on Linux). | ||
| const stdin_pipe_result = bun.sys.pipe(); | ||
| const stdin_pipe_fds = switch (stdin_pipe_result) { | ||
| .err => { | ||
| return error.StdinPipeFailed; | ||
| }, | ||
| .result => |fds| fds, | ||
|
Comment on lines
+760
to
+765
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π‘ Two minor error-handling issues in the new stdin pipe code: (1) If Extended reasoning...FD leak on stdin pipe creation failureIn The idiomatic Zig fix would be to add const pipe_fds = switch (pipe_result) { ... };
errdefer { pipe_fds[0].close(); pipe_fds[1].close(); }And similarly for Concrete example of the FD leakStep-by-step: (1) Silent write errors in writeAllToPipeThe Impact assessmentBoth issues are low-impact in practice: (1) Pre-existing pattern noteThe IPC pipe already had a similar leak pattern before this PR (if |
||
| }; | ||
|
|
||
| const exec_path = try bun.selfExePath(); | ||
|
|
||
| var argv = [_]?[*:0]const u8{ | ||
|
|
@@ -771,7 +784,7 @@ pub const SecurityScanSubprocess = struct { | |
| const spawn_options = bun.spawn.SpawnOptions{ | ||
| .stdout = .inherit, | ||
| .stderr = .inherit, | ||
| .stdin = .inherit, | ||
| .stdin = .{ .pipe = stdin_pipe_fds[0] }, | ||
| .cwd = spawn_cwd, | ||
| .extra_fds = &.{.{ .pipe = pipe_fds[1] }}, | ||
| .windows = if (Environment.isWindows) .{ | ||
|
|
@@ -782,6 +795,14 @@ pub const SecurityScanSubprocess = struct { | |
| var spawned = try (try bun.spawn.spawnProcess(&spawn_options, @ptrCast(&argv), @ptrCast(std.os.environ.ptr))).unwrap(); | ||
|
|
||
| pipe_fds[1].close(); | ||
| stdin_pipe_fds[0].close(); | ||
|
|
||
| // Write JSON data to the child's stdin pipe then close it to signal EOF. | ||
| // The child process reads this data synchronously via fs.readFileSync(0). | ||
| // The write may block briefly if data exceeds the pipe buffer, but the child | ||
| // is already running and will drain it. | ||
| writeAllToPipe(stdin_pipe_fds[1], this.json_data); | ||
| stdin_pipe_fds[1].close(); | ||
|
|
||
| if (comptime bun.Environment.isPosix) { | ||
| _ = bun.sys.setNonblocking(pipe_fds[0]); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| import { afterAll, beforeAll, expect, test } from "bun:test"; | ||
| import { existsSync, readFileSync } from "fs"; | ||
| import { bunEnv, bunExe, tempDir } from "harness"; | ||
| import { join } from "path"; | ||
|
|
||
| // Regression test for https://github.com/oven-sh/bun/issues/27716 | ||
| // bun install silently fails when a security scanner is configured and | ||
| // the project has enough packages that the inline JSON would exceed the | ||
| // OS per-argument size limit (MAX_ARG_STRLEN = 128KB on Linux). | ||
|
|
||
| const PACKAGE_COUNT = 850; | ||
| const tgzPath = join(import.meta.dir, "..", "..", "cli", "install", "bar-0.0.2.tgz"); | ||
| const tgzData = readFileSync(tgzPath); | ||
|
|
||
| let server: ReturnType<typeof Bun.serve>; | ||
| let registryUrl: string; | ||
|
|
||
| beforeAll(() => { | ||
| server = Bun.serve({ | ||
| port: 0, | ||
| async fetch(req) { | ||
| const url = new URL(req.url); | ||
| const path = url.pathname; | ||
|
|
||
| if (path.endsWith(".tgz")) { | ||
| return new Response(tgzData); | ||
| } | ||
|
|
||
| // Package metadata request | ||
| const name = decodeURIComponent(path.slice(1)); | ||
| return new Response( | ||
| JSON.stringify({ | ||
| name, | ||
| versions: { | ||
| "1.0.0": { | ||
| name, | ||
| version: "1.0.0", | ||
| dist: { | ||
| tarball: `${registryUrl}${name}-1.0.0.tgz`, | ||
| }, | ||
| }, | ||
| }, | ||
| "dist-tags": { latest: "1.0.0" }, | ||
| }), | ||
| ); | ||
| }, | ||
| }); | ||
| registryUrl = `http://localhost:${server.port}/`; | ||
| }); | ||
|
|
||
| afterAll(() => { | ||
| server?.stop(true); | ||
| }); | ||
|
Comment on lines
+15
to
+53
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π§Ή Nitpick | π΅ Trivial Consider using The coding guidelines recommend using β»οΈ Proposed refactor using `using` pattern-let server: ReturnType<typeof Bun.serve>;
-let registryUrl: string;
-
-beforeAll(() => {
- server = Bun.serve({
+test("security scanner works with many packages", async () => {
+ using server = Bun.serve({
port: 0,
async fetch(req) {
// ... existing fetch logic
},
});
- registryUrl = `http://localhost:${server.port}/`;
-});
-
-afterAll(() => {
- server?.stop(true);
-});
+ const registryUrl = `http://localhost:${server.port}/`;
-test("security scanner works with many packages", async () => {
using dir = tempDir("issue-27716", {
// ... rest of test
});
+ // ... rest of test using registryUrl
+}, 60_000);π€ Prompt for AI Agents |
||
|
|
||
| test("security scanner works with many packages", async () => { | ||
| using dir = tempDir("issue-27716", { | ||
| "scanner.ts": `export const scanner = { | ||
| version: "1", | ||
| scan: async ({ packages }) => { | ||
| return []; | ||
| }, | ||
| };`, | ||
| }); | ||
|
|
||
| // Generate many dependencies | ||
| const deps: Record<string, string> = {}; | ||
| for (let i = 0; i < PACKAGE_COUNT; i++) { | ||
| deps[`pkg-with-a-longer-name-for-testing-${String(i).padStart(4, "0")}`] = "1.0.0"; | ||
| } | ||
|
|
||
| await Bun.write( | ||
| join(String(dir), "package.json"), | ||
| JSON.stringify({ name: "test-27716", version: "1.0.0", dependencies: deps }), | ||
| ); | ||
|
|
||
| await Bun.write( | ||
| join(String(dir), "bunfig.toml"), | ||
| `[install]\nregistry = "${registryUrl}"\n\n[install.security]\nscanner = "./scanner.ts"\n`, | ||
| ); | ||
|
|
||
| await using proc = Bun.spawn({ | ||
| cmd: [bunExe(), "install"], | ||
| cwd: String(dir), | ||
| env: bunEnv, | ||
| stdout: "pipe", | ||
| stderr: "pipe", | ||
| }); | ||
|
|
||
| const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); | ||
|
|
||
| expect(stderr).not.toContain("Security scanner failed"); | ||
| expect(existsSync(join(String(dir), "bun.lock"))).toBe(true); | ||
| expect(existsSync(join(String(dir), "node_modules"))).toBe(true); | ||
| expect(exitCode).toBe(0); | ||
| }, 60_000); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π‘ The test sets an explicit 60,000ms timeout at line 95 ( Extended reasoning...What the bug isThe newly introduced regression test at The rule it violatesThe Step-by-step proofLooking at the test file, line 95 reads: This is the closing of the ImpactThis is a style/convention violation rather than a functional bug. The test will still work correctly with or without the explicit timeout. However, it sets a precedent that could lead to inconsistent timeout practices across the test suite, which is exactly what the CLAUDE.md rule is designed to prevent. How to fixSimply remove the }, 60_000);to: });Bun's built-in test timeout will handle the case where the test runs too long. |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
π‘ Nit:
scanner-entry-globals.d.tsstill declares__PACKAGES_JSON__on line 1, but this PR removed all usage of that global fromscanner-entry.ts(replaced withfs.readFileSync(0)) and removed the placeholder substitution fromsecurity_scanner.zig. The stale type declaration should be removed to keep the.d.tsfile consistent with the actual code.Extended reasoning...
What the bug is
The file
src/install/PackageManager/scanner-entry-globals.d.tsline 1 declares:This global type declaration was used to provide type information for the
__PACKAGES_JSON__placeholder that was previously embedded inscanner-entry.ts. The PR replaced this mechanism with reading package data from stdin viafs.readFileSync(0, "utf-8"), making the declaration stale.The specific code path
Previously,
security_scanner.zighad code that found the__PACKAGES_JSON__placeholder in the embeddedscanner-entry.tssource and replaced it with the actual JSON data at runtime. The.d.tsfile existed so TypeScript tooling could type-checkscanner-entry.tsduring development, providing the type for the injected global.This PR removed: (1) the
__PACKAGES_JSON__usage fromscanner-entry.tsline 4 (nowJSON.parse(fs.readFileSync(0, "utf-8"))), and (2) the placeholder replacement block fromsecurity_scanner.zig. However,scanner-entry-globals.d.tswas not updated.Why existing code does not prevent it
The
.d.tsfile is only used for development-time type checking and is not included in the build (the.tssource is embedded via@embedFile). So there is no build error or runtime failure from the stale declaration β it simply goes unnoticed.Impact
This is a minor consistency issue. Future developers reading
scanner-entry-globals.d.tswould see__PACKAGES_JSON__declared as a global and might be confused about where it comes from, since it is no longer used anywhere in the codebase. It could also lead to someone mistakenly thinking the global injection mechanism still exists.How to fix
Remove line 1 from
scanner-entry-globals.d.ts:-declare const __PACKAGES_JSON__: Bun.Security.Package[]; declare const __SUPPRESS_ERROR__: boolean;Step-by-step proof
scanner-entry.tsline 4 now reads:const packages = JSON.parse(fs.readFileSync(0, "utf-8"));β no reference to__PACKAGES_JSON__.security_scanner.zigdiff shows the__PACKAGES_JSON__placeholder replacement block (lines 676-685 in the old file) was deleted entirely.scanner-entry-globals.d.tsline 1 still declaresdeclare const __PACKAGES_JSON__: Bun.Security.Package[];.__PACKAGES_JSON__other than this stale declaration.