From e3d0dd96bc8459f87d2a3725c691c027ea7ce780 Mon Sep 17 00:00:00 2001 From: robobun Date: Sun, 22 Mar 2026 04:11:02 +0000 Subject: [PATCH 01/17] Fix use-after-free in S3 Store.initS3 PathLike refcounting initS3 and initS3WithReferencedCredentials take a PathLike by value, creating a struct copy that shares the underlying WTFStringImpl without incrementing the refcount. The subsequent toThreadSafe() call may create a new thread-safe copy of the WTFStringImpl and deref the original, leaving the caller's PathLike with a dangling pointer. When the caller's errdefer later calls path.deinit(), it derefs the already-freed impl. Fix by refing the underlying string before toThreadSafe() so the caller's copy remains valid. --- src/bun.js/webcore/blob/Store.zig | 12 ++++++++++-- test/js/bun/s3-presign-error.test.ts | 20 ++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 test/js/bun/s3-presign-error.test.ts diff --git a/src/bun.js/webcore/blob/Store.zig b/src/bun.js/webcore/blob/Store.zig index ca01a387410b..53c937d0ef84 100644 --- a/src/bun.js/webcore/blob/Store.zig +++ b/src/bun.js/webcore/blob/Store.zig @@ -67,7 +67,11 @@ pub fn external(ptr: ?*anyopaque, _: ?*anyopaque, _: usize) callconv(.c) void { } pub fn initS3WithReferencedCredentials(pathlike: node.PathLike, mime_type: ?MimeType, credentials: *bun.S3.S3Credentials, allocator: std.mem.Allocator) !*Store { var path = pathlike; - // this actually protects/refs the pathlike + // Ref before toThreadSafe because the value copy of pathlike shares the + // underlying WTFStringImpl with the caller. toThreadSafe may deref the + // original impl after replacing it with a thread-safe copy, which would + // leave the caller's copy dangling. + if (path == .slice_with_underlying_string) path.slice_with_underlying_string.underlying.ref(); path.toThreadSafe(); const store = Blob.Store.new(.{ @@ -96,7 +100,11 @@ pub fn initS3WithReferencedCredentials(pathlike: node.PathLike, mime_type: ?Mime pub fn initS3(pathlike: node.PathLike, mime_type: ?MimeType, credentials: bun.S3.S3Credentials, allocator: std.mem.Allocator) !*Store { var path = pathlike; - // this actually protects/refs the pathlike + // Ref before toThreadSafe because the value copy of pathlike shares the + // underlying WTFStringImpl with the caller. toThreadSafe may deref the + // original impl after replacing it with a thread-safe copy, which would + // leave the caller's copy dangling. + if (path == .slice_with_underlying_string) path.slice_with_underlying_string.underlying.ref(); path.toThreadSafe(); const store = Blob.Store.new(.{ diff --git a/test/js/bun/s3-presign-error.test.ts b/test/js/bun/s3-presign-error.test.ts new file mode 100644 index 000000000000..fc7dd9b8de78 --- /dev/null +++ b/test/js/bun/s3-presign-error.test.ts @@ -0,0 +1,20 @@ +import { test, expect } from "bun:test"; +import { bunExe, bunEnv } from "harness"; + +test("s3 presign with missing credentials throws instead of crashing", async () => { + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", `try { Bun.s3.presign("mykey"); } catch(e) { console.log(e.code); }`], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([ + proc.stdout.text(), + proc.stderr.text(), + proc.exited, + ]); + + expect(stdout.trim()).toBe("ERR_S3_MISSING_CREDENTIALS"); + expect(exitCode).toBe(0); +}); From ca64e3d2dcaf6f7cf7bb72f66bc3e400c795ec9f Mon Sep 17 00:00:00 2001 From: robobun Date: Sun, 22 Mar 2026 04:30:05 +0000 Subject: [PATCH 02/17] Scrub AWS/S3 env vars in test for hermetic missing-credentials path --- test/js/bun/s3-presign-error.test.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/test/js/bun/s3-presign-error.test.ts b/test/js/bun/s3-presign-error.test.ts index fc7dd9b8de78..8d3f03f3243f 100644 --- a/test/js/bun/s3-presign-error.test.ts +++ b/test/js/bun/s3-presign-error.test.ts @@ -2,9 +2,18 @@ import { test, expect } from "bun:test"; import { bunExe, bunEnv } from "harness"; test("s3 presign with missing credentials throws instead of crashing", async () => { + // Scrub AWS credential/config env vars so the test always hits the + // missing-credentials path regardless of ambient host configuration. + const env: Record = {}; + for (const [key, value] of Object.entries(bunEnv)) { + if (!key.startsWith("AWS_") && !key.startsWith("S3_")) { + env[key] = value as string; + } + } + await using proc = Bun.spawn({ cmd: [bunExe(), "-e", `try { Bun.s3.presign("mykey"); } catch(e) { console.log(e.code); }`], - env: bunEnv, + env, stdout: "pipe", stderr: "pipe", }); From 023b5265201a0227031b36a696fd18fa12b822ea Mon Sep 17 00:00:00 2001 From: robobun Date: Sun, 22 Mar 2026 05:23:58 +0000 Subject: [PATCH 03/17] Fix S3 PathLike use-after-free: transfer ownership in construct functions Instead of adding ref() calls in initS3/initS3WithReferencedCredentials, fix ownership at the caller level. The construct functions (constructS3FileWithS3CredentialsAndOptions, constructS3FileWithS3Credentials) now take ownership of the PathLike and handle cleanup via errdefer if getCredentialsWithOptions throws before the path is consumed by toThreadSafe. Callers no longer errdefer path.deinit() since the construct functions own it. For static S3File functions using PathOrBlob, neutralize the errdefer by clearing the path after the construct call consumes it. --- src/bun.js/webcore/S3Client.zig | 15 +++++++-------- src/bun.js/webcore/S3File.zig | 27 +++++++++++++++++++++++++++ src/bun.js/webcore/blob/Store.zig | 14 ++++---------- test/js/bun/s3-presign-error.test.ts | 6 +----- 4 files changed, 39 insertions(+), 23 deletions(-) diff --git a/src/bun.js/webcore/S3Client.zig b/src/bun.js/webcore/S3Client.zig index 941b8afec671..be99860c000f 100644 --- a/src/bun.js/webcore/S3Client.zig +++ b/src/bun.js/webcore/S3Client.zig @@ -135,7 +135,7 @@ pub const S3Client = struct { } return globalThis.throwInvalidArguments("Expected a path", .{}); }; - errdefer path.deinit(); + // constructS3FileWithS3CredentialsAndOptions takes ownership of path. const options = args.nextEat(); var blob = Blob.new(try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl, ptr.storage_class, ptr.request_payer)); return blob.toJS(globalThis); @@ -151,8 +151,7 @@ pub const S3Client = struct { } return globalThis.throwInvalidArguments("Expected a path to presign", .{}); }; - errdefer path.deinit(); - + // constructS3FileWithS3CredentialsAndOptions takes ownership of path. const options = args.nextEat(); var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl, ptr.storage_class, ptr.request_payer); defer blob.detach(); @@ -169,7 +168,7 @@ pub const S3Client = struct { } return globalThis.throwInvalidArguments("Expected a path to check if it exists", .{}); }; - errdefer path.deinit(); + // constructS3FileWithS3CredentialsAndOptions takes ownership of path. const options = args.nextEat(); var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl, ptr.storage_class, ptr.request_payer); defer blob.detach(); @@ -186,7 +185,7 @@ pub const S3Client = struct { } return globalThis.throwInvalidArguments("Expected a path to check the size of", .{}); }; - errdefer path.deinit(); + // constructS3FileWithS3CredentialsAndOptions takes ownership of path. const options = args.nextEat(); var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl, ptr.storage_class, ptr.request_payer); defer blob.detach(); @@ -203,7 +202,7 @@ pub const S3Client = struct { } return globalThis.throwInvalidArguments("Expected a path to check the stat of", .{}); }; - errdefer path.deinit(); + // constructS3FileWithS3CredentialsAndOptions takes ownership of path. const options = args.nextEat(); var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl, ptr.storage_class, ptr.request_payer); defer blob.detach(); @@ -217,7 +216,7 @@ pub const S3Client = struct { const path: jsc.Node.PathLike = try jsc.Node.PathLike.fromJS(globalThis, &args) orelse { return globalThis.ERR(.MISSING_ARGS, "Expected a path to write to", .{}).throw(); }; - errdefer path.deinit(); + // constructS3FileWithS3CredentialsAndOptions takes ownership of path. const data = args.nextEat() orelse { return globalThis.ERR(.MISSING_ARGS, "Expected a Blob-y thing to write", .{}).throw(); }; @@ -251,7 +250,7 @@ pub const S3Client = struct { const path: jsc.Node.PathLike = try jsc.Node.PathLike.fromJS(globalThis, &args) orelse { return globalThis.ERR(.MISSING_ARGS, "Expected a path to unlink", .{}).throw(); }; - errdefer path.deinit(); + // constructS3FileWithS3CredentialsAndOptions takes ownership of path. const options = args.nextEat(); var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl, ptr.storage_class, ptr.request_payer); defer blob.detach(); diff --git a/src/bun.js/webcore/S3File.zig b/src/bun.js/webcore/S3File.zig index 29d0524f7dba..4459d6c143f5 100644 --- a/src/bun.js/webcore/S3File.zig +++ b/src/bun.js/webcore/S3File.zig @@ -83,6 +83,8 @@ pub fn presign(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.J return globalThis.throwInvalidArguments("Expected a S3 or path to presign", .{}); } const options = args.nextEat(); + // constructS3FileInternalStore takes ownership of path. + path_or_blob = .{ .blob = .initEmpty(globalThis) }; var blob = try constructS3FileInternalStore(globalThis, path.path, options); defer blob.deinit(); return try getPresignUrlFrom(&blob, globalThis, options); @@ -113,6 +115,8 @@ pub fn unlink(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JS return globalThis.throwInvalidArguments("Expected a S3 or path to delete", .{}); } const options = args.nextEat(); + // constructS3FileInternalStore takes ownership of path. + path_or_blob = .{ .blob = .initEmpty(globalThis) }; var blob = try constructS3FileInternalStore(globalThis, path.path, options); defer blob.deinit(); return try blob.store.?.data.s3.unlink(blob.store.?, globalThis, options); @@ -150,6 +154,8 @@ pub fn write(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSE if (path == .fd) { return globalThis.throwInvalidArguments("Expected a S3 or path to upload", .{}); } + // constructS3FileInternalStore takes ownership of path. + path_or_blob = .{ .blob = .initEmpty(globalThis) }; var blob = try constructS3FileInternalStore(globalThis, path.path, options); defer blob.deinit(); @@ -189,6 +195,8 @@ pub fn size(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSEr if (path == .fd) { return globalThis.throwInvalidArguments("Expected a S3 or path to get size", .{}); } + // constructS3FileInternalStore takes ownership of path. + path_or_blob = .{ .blob = .initEmpty(globalThis) }; var blob = try constructS3FileInternalStore(globalThis, path.path, options); defer blob.deinit(); @@ -222,6 +230,8 @@ pub fn exists(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JS if (path == .fd) { return globalThis.throwInvalidArguments("Expected a S3 or path to check if it exists", .{}); } + // constructS3FileInternalStore takes ownership of path. + path_or_blob = .{ .blob = .initEmpty(globalThis) }; var blob = try constructS3FileInternalStore(globalThis, path.path, options); defer blob.deinit(); @@ -253,6 +263,12 @@ pub fn constructS3FileWithS3CredentialsAndOptions( default_storage_class: ?bun.S3.StorageClass, default_request_payer: bool, ) bun.JSError!Blob { + // This function takes ownership of `path`. If we fail before passing + // it to initS3/initS3WithReferencedCredentials (which consume it via + // toThreadSafe), we must clean it up ourselves. + var path_to_clean = path; + errdefer path_to_clean.deinit(); + var aws_options = try S3.S3Credentials.getCredentialsWithOptions(default_credentials.*, default_options, options, default_acl, default_storage_class, default_request_payer, globalObject); defer aws_options.deinit(); @@ -263,6 +279,9 @@ pub fn constructS3FileWithS3CredentialsAndOptions( break :brk bun.handleOom(Blob.Store.initS3WithReferencedCredentials(path, null, default_credentials, bun.default_allocator)); } }; + // Path has been consumed by initS3/initS3WithReferencedCredentials + // via toThreadSafe — neutralize errdefer to prevent double-free. + path_to_clean = .{ .string = bun.PathString.empty }; errdefer store.deinit(); store.data.s3.options = aws_options.options; store.data.s3.acl = aws_options.acl; @@ -304,9 +323,15 @@ pub fn constructS3FileWithS3Credentials( options: ?jsc.JSValue, existing_credentials: S3.S3Credentials, ) bun.JSError!Blob { + // This function takes ownership of `path`. + var path_to_clean = path; + errdefer path_to_clean.deinit(); + var aws_options = try S3.S3Credentials.getCredentialsWithOptions(existing_credentials, .{}, options, null, null, false, globalObject); defer aws_options.deinit(); const store = bun.handleOom(Blob.Store.initS3(path, null, aws_options.credentials, bun.default_allocator)); + // Path consumed by initS3 via toThreadSafe. + path_to_clean = .{ .string = bun.PathString.empty }; errdefer store.deinit(); store.data.s3.options = aws_options.options; store.data.s3.acl = aws_options.acl; @@ -573,6 +598,8 @@ pub fn stat(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSEr if (path == .fd) { return globalThis.throwInvalidArguments("Expected a S3 or path to get size", .{}); } + // constructS3FileInternalStore takes ownership of path. + path_or_blob = .{ .blob = .initEmpty(globalThis) }; var blob = try constructS3FileInternalStore(globalThis, path.path, options); defer blob.deinit(); diff --git a/src/bun.js/webcore/blob/Store.zig b/src/bun.js/webcore/blob/Store.zig index 53c937d0ef84..d2ac6874e990 100644 --- a/src/bun.js/webcore/blob/Store.zig +++ b/src/bun.js/webcore/blob/Store.zig @@ -67,11 +67,8 @@ pub fn external(ptr: ?*anyopaque, _: ?*anyopaque, _: usize) callconv(.c) void { } pub fn initS3WithReferencedCredentials(pathlike: node.PathLike, mime_type: ?MimeType, credentials: *bun.S3.S3Credentials, allocator: std.mem.Allocator) !*Store { var path = pathlike; - // Ref before toThreadSafe because the value copy of pathlike shares the - // underlying WTFStringImpl with the caller. toThreadSafe may deref the - // original impl after replacing it with a thread-safe copy, which would - // leave the caller's copy dangling. - if (path == .slice_with_underlying_string) path.slice_with_underlying_string.underlying.ref(); + // toThreadSafe takes ownership of the underlying string — callers + // must not deinit their copy after this call. path.toThreadSafe(); const store = Blob.Store.new(.{ @@ -100,11 +97,8 @@ pub fn initS3WithReferencedCredentials(pathlike: node.PathLike, mime_type: ?Mime pub fn initS3(pathlike: node.PathLike, mime_type: ?MimeType, credentials: bun.S3.S3Credentials, allocator: std.mem.Allocator) !*Store { var path = pathlike; - // Ref before toThreadSafe because the value copy of pathlike shares the - // underlying WTFStringImpl with the caller. toThreadSafe may deref the - // original impl after replacing it with a thread-safe copy, which would - // leave the caller's copy dangling. - if (path == .slice_with_underlying_string) path.slice_with_underlying_string.underlying.ref(); + // toThreadSafe takes ownership of the underlying string — callers + // must not deinit their copy after this call. path.toThreadSafe(); const store = Blob.Store.new(.{ diff --git a/test/js/bun/s3-presign-error.test.ts b/test/js/bun/s3-presign-error.test.ts index 8d3f03f3243f..2a288ae06303 100644 --- a/test/js/bun/s3-presign-error.test.ts +++ b/test/js/bun/s3-presign-error.test.ts @@ -18,11 +18,7 @@ test("s3 presign with missing credentials throws instead of crashing", async () stderr: "pipe", }); - const [stdout, stderr, exitCode] = await Promise.all([ - proc.stdout.text(), - proc.stderr.text(), - proc.exited, - ]); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); expect(stdout.trim()).toBe("ERR_S3_MISSING_CREDENTIALS"); expect(exitCode).toBe(0); From b6a0a269b0ae536987f93d3708ecc392cd3d6144 Mon Sep 17 00:00:00 2001 From: robobun Date: Sun, 22 Mar 2026 05:26:46 +0000 Subject: [PATCH 04/17] Also scrub BUN_S3_ env vars in test --- test/js/bun/s3-presign-error.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/js/bun/s3-presign-error.test.ts b/test/js/bun/s3-presign-error.test.ts index 2a288ae06303..8d60aa9bc3f3 100644 --- a/test/js/bun/s3-presign-error.test.ts +++ b/test/js/bun/s3-presign-error.test.ts @@ -6,7 +6,7 @@ test("s3 presign with missing credentials throws instead of crashing", async () // missing-credentials path regardless of ambient host configuration. const env: Record = {}; for (const [key, value] of Object.entries(bunEnv)) { - if (!key.startsWith("AWS_") && !key.startsWith("S3_")) { + if (!key.startsWith("AWS_") && !key.startsWith("S3_") && !key.startsWith("BUN_S3_")) { env[key] = value as string; } } From b7fb02fed8cdb28e03e2cb92f6d2a5aa43ac1ec6 Mon Sep 17 00:00:00 2001 From: robobun Date: Sun, 22 Mar 2026 05:55:16 +0000 Subject: [PATCH 05/17] Restore errdefer in S3Client.write for early return before ownership transfer --- src/bun.js/webcore/S3Client.zig | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/bun.js/webcore/S3Client.zig b/src/bun.js/webcore/S3Client.zig index be99860c000f..61ef6d2413c6 100644 --- a/src/bun.js/webcore/S3Client.zig +++ b/src/bun.js/webcore/S3Client.zig @@ -213,16 +213,20 @@ pub const S3Client = struct { const arguments = callframe.arguments_old(3).slice(); var args = jsc.CallFrame.ArgumentsSlice.init(globalThis.bunVM(), arguments); defer args.deinit(); - const path: jsc.Node.PathLike = try jsc.Node.PathLike.fromJS(globalThis, &args) orelse { + var path: jsc.Node.PathLike = try jsc.Node.PathLike.fromJS(globalThis, &args) orelse { return globalThis.ERR(.MISSING_ARGS, "Expected a path to write to", .{}).throw(); }; - // constructS3FileWithS3CredentialsAndOptions takes ownership of path. + // Guard against early return before constructS3FileWithS3CredentialsAndOptions + // takes ownership of path. + errdefer path.deinit(); const data = args.nextEat() orelse { return globalThis.ERR(.MISSING_ARGS, "Expected a Blob-y thing to write", .{}).throw(); }; const options = args.nextEat(); var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl, ptr.storage_class, ptr.request_payer); + // Path ownership transferred — neutralize errdefer. + path = .{ .string = bun.PathString.empty }; defer blob.detach(); var blob_internal: PathOrBlob = .{ .blob = blob }; return Blob.writeFileInternal(globalThis, &blob_internal, data, .{ From d2bacc7afa4e42b12d04f691125c952519370725 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 22 Mar 2026 06:23:14 +0000 Subject: [PATCH 06/17] [autofix.ci] apply automated fixes --- test/js/bun/s3-presign-error.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/js/bun/s3-presign-error.test.ts b/test/js/bun/s3-presign-error.test.ts index 8d60aa9bc3f3..0bac9c51833d 100644 --- a/test/js/bun/s3-presign-error.test.ts +++ b/test/js/bun/s3-presign-error.test.ts @@ -1,5 +1,5 @@ -import { test, expect } from "bun:test"; -import { bunExe, bunEnv } from "harness"; +import { expect, test } from "bun:test"; +import { bunEnv, bunExe } from "harness"; test("s3 presign with missing credentials throws instead of crashing", async () => { // Scrub AWS credential/config env vars so the test always hits the From 9ef58f0038481673e003d3ecd8219893b2cb7b68 Mon Sep 17 00:00:00 2001 From: robobun Date: Sun, 22 Mar 2026 06:55:10 +0000 Subject: [PATCH 07/17] Neutralize path errdefer before construct call to prevent double-free --- src/bun.js/webcore/S3Client.zig | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/bun.js/webcore/S3Client.zig b/src/bun.js/webcore/S3Client.zig index 61ef6d2413c6..79ab49971e53 100644 --- a/src/bun.js/webcore/S3Client.zig +++ b/src/bun.js/webcore/S3Client.zig @@ -224,9 +224,11 @@ pub const S3Client = struct { }; const options = args.nextEat(); - var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl, ptr.storage_class, ptr.request_payer); - // Path ownership transferred — neutralize errdefer. + // constructS3FileWithS3CredentialsAndOptions takes ownership of path + // (has its own errdefer) — neutralize ours before the call. + const owned_path = path; path = .{ .string = bun.PathString.empty }; + var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, owned_path, options, ptr.credentials, ptr.options, ptr.acl, ptr.storage_class, ptr.request_payer); defer blob.detach(); var blob_internal: PathOrBlob = .{ .blob = blob }; return Blob.writeFileInternal(globalThis, &blob_internal, data, .{ From 4ab6e753464ccfad15af76cca950e489bbc0def6 Mon Sep 17 00:00:00 2001 From: robobun Date: Sun, 22 Mar 2026 08:08:27 +0000 Subject: [PATCH 08/17] Retry CI From ee3ff22a188923aab92235268b490aa291648801 Mon Sep 17 00:00:00 2001 From: robobun Date: Sun, 22 Mar 2026 08:48:22 +0000 Subject: [PATCH 09/17] Move s3-presign-error test to test/js/bun/s3/ for consistency --- test/js/bun/{ => s3}/s3-presign-error.test.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/js/bun/{ => s3}/s3-presign-error.test.ts (100%) diff --git a/test/js/bun/s3-presign-error.test.ts b/test/js/bun/s3/s3-presign-error.test.ts similarity index 100% rename from test/js/bun/s3-presign-error.test.ts rename to test/js/bun/s3/s3-presign-error.test.ts From 3008e5b0b72fa86d9864de1e70aabdc62b42678c Mon Sep 17 00:00:00 2001 From: robobun Date: Sun, 22 Mar 2026 11:16:38 +0000 Subject: [PATCH 10/17] Add initS3 path coverage to presign error test --- test/js/bun/s3/s3-presign-error.test.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/test/js/bun/s3/s3-presign-error.test.ts b/test/js/bun/s3/s3-presign-error.test.ts index 0bac9c51833d..20262242e15e 100644 --- a/test/js/bun/s3/s3-presign-error.test.ts +++ b/test/js/bun/s3/s3-presign-error.test.ts @@ -11,8 +11,15 @@ test("s3 presign with missing credentials throws instead of crashing", async () } } + // Test both initS3WithReferencedCredentials (no credential overrides) + // and initS3 (with per-request credentials that still lack endpoint/bucket). + const code = [ + `try { Bun.s3.presign("mykey"); } catch(e) { console.log(e.code); }`, + `try { Bun.s3.presign("mykey", { accessKeyId: "x", secretAccessKey: "y" }); } catch(e) { console.log(e.code); }`, + ].join("\n"); + await using proc = Bun.spawn({ - cmd: [bunExe(), "-e", `try { Bun.s3.presign("mykey"); } catch(e) { console.log(e.code); }`], + cmd: [bunExe(), "-e", code], env, stdout: "pipe", stderr: "pipe", @@ -20,6 +27,6 @@ test("s3 presign with missing credentials throws instead of crashing", async () const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect(stdout.trim()).toBe("ERR_S3_MISSING_CREDENTIALS"); + expect(stdout.trim()).toBe("ERR_S3_MISSING_CREDENTIALS\nERR_S3_INVALID_PATH"); expect(exitCode).toBe(0); }); From be8105eea719302fa8cd28d73d05822ff8c0fec7 Mon Sep 17 00:00:00 2001 From: robobun Date: Sun, 22 Mar 2026 16:16:13 +0000 Subject: [PATCH 11/17] Retry CI (darwin expired + ASAN runner flake) From 9b1ffdacb9388c0e71ca9a959bc922cd561dc52c Mon Sep 17 00:00:00 2001 From: robobun Date: Sun, 22 Mar 2026 17:07:50 +0000 Subject: [PATCH 12/17] Use stderr inherit instead of pipe since ASAN builds emit diagnostics --- test/js/bun/s3/s3-presign-error.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/js/bun/s3/s3-presign-error.test.ts b/test/js/bun/s3/s3-presign-error.test.ts index 20262242e15e..eb3eac072a5d 100644 --- a/test/js/bun/s3/s3-presign-error.test.ts +++ b/test/js/bun/s3/s3-presign-error.test.ts @@ -22,10 +22,10 @@ test("s3 presign with missing credentials throws instead of crashing", async () cmd: [bunExe(), "-e", code], env, stdout: "pipe", - stderr: "pipe", + stderr: "inherit", }); - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); expect(stdout.trim()).toBe("ERR_S3_MISSING_CREDENTIALS\nERR_S3_INVALID_PATH"); expect(exitCode).toBe(0); From bef6631c71c5d70de23b4459d2f30c820d907c4e Mon Sep 17 00:00:00 2001 From: robobun Date: Sun, 22 Mar 2026 19:56:12 +0000 Subject: [PATCH 13/17] Retry CI (darwin runner timeouts) From 737b8ef9631cb7f44b5f3fe0bcba3970908ccaa3 Mon Sep 17 00:00:00 2001 From: robobun Date: Sun, 22 Mar 2026 21:11:57 +0000 Subject: [PATCH 14/17] Add static presign path coverage (constructS3FileWithS3Credentials) --- test/js/bun/s3/s3-presign-error.test.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/test/js/bun/s3/s3-presign-error.test.ts b/test/js/bun/s3/s3-presign-error.test.ts index eb3eac072a5d..3625901f9086 100644 --- a/test/js/bun/s3/s3-presign-error.test.ts +++ b/test/js/bun/s3/s3-presign-error.test.ts @@ -11,11 +11,15 @@ test("s3 presign with missing credentials throws instead of crashing", async () } } - // Test both initS3WithReferencedCredentials (no credential overrides) - // and initS3 (with per-request credentials that still lack endpoint/bucket). + // Test instance method (constructS3FileWithS3CredentialsAndOptions): + // - initS3WithReferencedCredentials (no credential overrides) + // - initS3 (with per-request credentials that still lack endpoint/bucket) + // Test static method (constructS3FileWithS3Credentials): + // - Bun.S3Client.presign (static path) const code = [ `try { Bun.s3.presign("mykey"); } catch(e) { console.log(e.code); }`, `try { Bun.s3.presign("mykey", { accessKeyId: "x", secretAccessKey: "y" }); } catch(e) { console.log(e.code); }`, + `try { Bun.S3Client.presign("mykey"); } catch(e) { console.log(e.code); }`, ].join("\n"); await using proc = Bun.spawn({ @@ -27,6 +31,6 @@ test("s3 presign with missing credentials throws instead of crashing", async () const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); - expect(stdout.trim()).toBe("ERR_S3_MISSING_CREDENTIALS\nERR_S3_INVALID_PATH"); + expect(stdout.trim()).toBe("ERR_S3_MISSING_CREDENTIALS\nERR_S3_INVALID_PATH\nERR_S3_MISSING_CREDENTIALS"); expect(exitCode).toBe(0); }); From 97ad865ebb10ef9083ee9af8f42cd61120084190 Mon Sep 17 00:00:00 2001 From: robobun Date: Mon, 23 Mar 2026 00:02:15 +0000 Subject: [PATCH 15/17] Retry CI (darwin runner timeouts) From ca543de9cf0d97fae8b3d87e7eff63a63c6244b9 Mon Sep 17 00:00:00 2001 From: robobun Date: Mon, 23 Mar 2026 01:05:32 +0000 Subject: [PATCH 16/17] Retry CI (windows-aarch64 runner flake) From c5505f108d3a23430b60b93219f409c04ff2b7f6 Mon Sep 17 00:00:00 2001 From: robobun Date: Mon, 23 Mar 2026 03:05:43 +0000 Subject: [PATCH 17/17] Retry CI (infra flakes: darwin expired, aarch64 agent crash, asan runner)