From ffe2a78978b0ed89602928f138592ed8ebef0a36 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 10 May 2026 23:25:29 +0000 Subject: [PATCH 1/2] s3: don't double-free path when presign throws after blob creation Blob.Store.initS3 takes ownership of the PathLike it receives: toThreadSafe() transfers the underlying StringImpl ref for string-backed paths, and for the other variants the store ends up holding the only handle to the same allocation. The S3Client / S3File callers passed path by value and kept an 'errdefer path.deinit()' active across the subsequent presign()/stat()/write() call, so when signRequest failed (e.g. missing credentials) the store was torn down via blob.detach() and then the caller's errdefer freed the same path a second time. The extra deref under-flowed the StringImpl refcount, which later shows up as a SIGFPE in StringImpl::costDuringGC (division by refCount()==0) or as a 'reached unreachable code' panic in debug builds. Thread *PathLike through the constructS3File* helpers and neuter the caller's copy once the store has taken ownership, so the outer errdefer becomes a no-op on every post-construction error path while still cleaning up if getCredentialsWithOptions throws before the store is built. --- src/runtime/webcore/Blob.zig | 3 +- src/runtime/webcore/S3Client.zig | 39 ++++++----- src/runtime/webcore/S3File.zig | 65 +++++++++++-------- .../s3/s3-presign-missing-credentials.test.ts | 57 ++++++++++++++++ 4 files changed, 117 insertions(+), 47 deletions(-) create mode 100644 test/js/bun/s3/s3-presign-missing-credentials.test.ts diff --git a/src/runtime/webcore/Blob.zig b/src/runtime/webcore/Blob.zig index 6cf5c047554..bbb84857fb8 100644 --- a/src/runtime/webcore/Blob.zig +++ b/src/runtime/webcore/Blob.zig @@ -2069,7 +2069,8 @@ pub fn constructBunFile( if (path == .path) { if (strings.hasPrefixComptime(path.path.slice(), "s3://")) { - return try S3File.constructInternalJS(globalObject, path.path, options); + errdefer path.deinitAndUnprotect(); + return try S3File.constructInternalJS(globalObject, &path.path, options); } } defer path.deinitAndUnprotect(); diff --git a/src/runtime/webcore/S3Client.zig b/src/runtime/webcore/S3Client.zig index 941b8afec67..77fb6c97b34 100644 --- a/src/runtime/webcore/S3Client.zig +++ b/src/runtime/webcore/S3Client.zig @@ -129,7 +129,7 @@ pub const S3Client = struct { const arguments = callframe.arguments_old(2).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 { if (args.len() == 0) { return globalThis.ERR(.MISSING_ARGS, "Expected a path ", .{}).throw(); } @@ -137,7 +137,7 @@ pub const S3Client = struct { }; errdefer path.deinit(); 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)); + 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); } @@ -145,7 +145,7 @@ pub const S3Client = struct { const arguments = callframe.arguments_old(2).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 { if (args.len() == 0) { return globalThis.ERR(.MISSING_ARGS, "Expected a path to presign", .{}).throw(); } @@ -154,7 +154,7 @@ pub const S3Client = struct { errdefer path.deinit(); const options = args.nextEat(); - var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl, ptr.storage_class, ptr.request_payer); + var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, &path, options, ptr.credentials, ptr.options, ptr.acl, ptr.storage_class, ptr.request_payer); defer blob.detach(); return S3File.getPresignUrlFrom(&blob, globalThis, options); } @@ -163,7 +163,7 @@ pub const S3Client = struct { const arguments = callframe.arguments_old(2).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 { if (args.len() == 0) { return globalThis.ERR(.MISSING_ARGS, "Expected a path to check if it exists", .{}).throw(); } @@ -171,7 +171,7 @@ pub const S3Client = struct { }; errdefer path.deinit(); const options = args.nextEat(); - var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl, ptr.storage_class, ptr.request_payer); + var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, &path, options, ptr.credentials, ptr.options, ptr.acl, ptr.storage_class, ptr.request_payer); defer blob.detach(); return S3File.S3BlobStatTask.exists(globalThis, &blob); } @@ -180,7 +180,7 @@ pub const S3Client = struct { const arguments = callframe.arguments_old(2).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 { if (args.len() == 0) { return globalThis.ERR(.MISSING_ARGS, "Expected a path to check the size of", .{}).throw(); } @@ -188,7 +188,7 @@ pub const S3Client = struct { }; errdefer path.deinit(); const options = args.nextEat(); - var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl, ptr.storage_class, ptr.request_payer); + var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, &path, options, ptr.credentials, ptr.options, ptr.acl, ptr.storage_class, ptr.request_payer); defer blob.detach(); return S3File.S3BlobStatTask.size(globalThis, &blob); } @@ -197,7 +197,7 @@ pub const S3Client = struct { const arguments = callframe.arguments_old(2).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 { if (args.len() == 0) { return globalThis.ERR(.MISSING_ARGS, "Expected a path to check the stat of", .{}).throw(); } @@ -205,7 +205,7 @@ pub const S3Client = struct { }; errdefer path.deinit(); const options = args.nextEat(); - var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl, ptr.storage_class, ptr.request_payer); + var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, &path, options, ptr.credentials, ptr.options, ptr.acl, ptr.storage_class, ptr.request_payer); defer blob.detach(); return S3File.S3BlobStatTask.stat(globalThis, &blob); } @@ -214,7 +214,7 @@ 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(); }; errdefer path.deinit(); @@ -223,7 +223,7 @@ 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); + var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, &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, .{ @@ -238,7 +238,8 @@ pub const S3Client = struct { const object_keys = args[0]; const options = args[1]; - var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, .{ .string = bun.PathString.empty }, options, ptr.credentials, ptr.options, null, null, ptr.request_payer); + var empty_path: jsc.Node.PathLike = .{ .string = bun.PathString.empty }; + var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, &empty_path, options, ptr.credentials, ptr.options, null, null, ptr.request_payer); defer blob.detach(); return blob.store.?.data.s3.listObjects(blob.store.?, globalThis, object_keys, options); @@ -248,12 +249,12 @@ pub const S3Client = struct { const arguments = callframe.arguments_old(2).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 unlink", .{}).throw(); }; errdefer path.deinit(); const options = args.nextEat(); - var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, path, options, ptr.credentials, ptr.options, ptr.acl, ptr.storage_class, ptr.request_payer); + var blob = try S3File.constructS3FileWithS3CredentialsAndOptions(globalThis, &path, options, ptr.credentials, ptr.options, ptr.acl, ptr.storage_class, ptr.request_payer); defer blob.detach(); return blob.store.?.data.s3.unlink(blob.store.?, globalThis, options); } @@ -296,11 +297,12 @@ pub const S3Client = struct { var args = jsc.CallFrame.ArgumentsSlice.init(globalThis.bunVM(), arguments); defer args.deinit(); - const path = (try jsc.Node.PathLike.fromJS(globalThis, &args)) orelse { + var path = (try jsc.Node.PathLike.fromJS(globalThis, &args)) orelse { return globalThis.throwInvalidArguments("Expected file path string", .{}); }; + errdefer path.deinit(); - return try S3File.constructInternalJS(globalThis, path, args.nextEat()); + return try S3File.constructInternalJS(globalThis, &path, args.nextEat()); } pub fn staticStat(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue { return S3File.stat(globalThis, callframe); @@ -314,7 +316,8 @@ pub const S3Client = struct { // get credentials from env const existing_credentials = globalThis.bunVM().transpiler.env.getS3Credentials(); - var blob = try S3File.constructS3FileWithS3Credentials(globalThis, .{ .string = bun.PathString.empty }, options, existing_credentials); + var empty_path: jsc.Node.PathLike = .{ .string = bun.PathString.empty }; + var blob = try S3File.constructS3FileWithS3Credentials(globalThis, &empty_path, options, existing_credentials); defer blob.detach(); return blob.store.?.data.s3.listObjects(blob.store.?, globalThis, object_keys, options); diff --git a/src/runtime/webcore/S3File.zig b/src/runtime/webcore/S3File.zig index 41e900fb987..42735588e8a 100644 --- a/src/runtime/webcore/S3File.zig +++ b/src/runtime/webcore/S3File.zig @@ -78,12 +78,12 @@ pub fn presign(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.J } switch (path_or_blob) { - .path => |path| { - if (path == .fd) { + .path => |*path| { + if (path.* == .fd) { return globalThis.throwInvalidArguments("Expected a S3 or path to presign", .{}); } const options = args.nextEat(); - var blob = try constructS3FileInternalStore(globalThis, path.path, options); + var blob = try constructS3FileInternalStore(globalThis, &path.path, options); defer blob.deinit(); return try getPresignUrlFrom(&blob, globalThis, options); }, @@ -108,12 +108,12 @@ pub fn unlink(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JS } switch (path_or_blob) { - .path => |path| { - if (path == .fd) { + .path => |*path| { + if (path.* == .fd) { return globalThis.throwInvalidArguments("Expected a S3 or path to delete", .{}); } const options = args.nextEat(); - var blob = try constructS3FileInternalStore(globalThis, path.path, options); + var blob = try constructS3FileInternalStore(globalThis, &path.path, options); defer blob.deinit(); return try blob.store.?.data.s3.unlink(blob.store.?, globalThis, options); }, @@ -145,12 +145,12 @@ pub fn write(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSE }; switch (path_or_blob) { - .path => |path| { + .path => |*path| { const options = args.nextEat(); - if (path == .fd) { + if (path.* == .fd) { return globalThis.throwInvalidArguments("Expected a S3 or path to upload", .{}); } - var blob = try constructS3FileInternalStore(globalThis, path.path, options); + var blob = try constructS3FileInternalStore(globalThis, &path.path, options); defer blob.deinit(); var blob_internal: PathOrBlob = .{ .blob = blob }; @@ -184,12 +184,12 @@ pub fn size(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSEr } switch (path_or_blob) { - .path => |path| { + .path => |*path| { const options = args.nextEat(); - if (path == .fd) { + if (path.* == .fd) { return globalThis.throwInvalidArguments("Expected a S3 or path to get size", .{}); } - var blob = try constructS3FileInternalStore(globalThis, path.path, options); + var blob = try constructS3FileInternalStore(globalThis, &path.path, options); defer blob.deinit(); return S3BlobStatTask.size(globalThis, &blob); @@ -217,12 +217,12 @@ pub fn exists(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JS } switch (path_or_blob) { - .path => |path| { + .path => |*path| { const options = args.nextEat(); - if (path == .fd) { + if (path.* == .fd) { return globalThis.throwInvalidArguments("Expected a S3 or path to check if it exists", .{}); } - var blob = try constructS3FileInternalStore(globalThis, path.path, options); + var blob = try constructS3FileInternalStore(globalThis, &path.path, options); defer blob.deinit(); return S3BlobStatTask.exists(globalThis, &blob); @@ -235,7 +235,7 @@ pub fn exists(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JS fn constructS3FileInternalStore( globalObject: *jsc.JSGlobalObject, - path: jsc.Node.PathLike, + path: *jsc.Node.PathLike, options: ?jsc.JSValue, ) bun.JSError!Blob { // get credentials from env @@ -245,7 +245,7 @@ fn constructS3FileInternalStore( /// if the credentials have changed, we need to clone it, if not we can just ref/deref it pub fn constructS3FileWithS3CredentialsAndOptions( globalObject: *jsc.JSGlobalObject, - path: jsc.Node.PathLike, + path: *jsc.Node.PathLike, options: ?jsc.JSValue, default_credentials: *S3.S3Credentials, default_options: bun.S3.MultiPartUploadOptions, @@ -258,11 +258,16 @@ pub fn constructS3FileWithS3CredentialsAndOptions( const store = brk: { if (aws_options.changed_credentials) { - break :brk bun.handleOom(Blob.Store.initS3(path, null, aws_options.credentials, bun.default_allocator)); + break :brk bun.handleOom(Blob.Store.initS3(path.*, null, aws_options.credentials, bun.default_allocator)); } else { - break :brk bun.handleOom(Blob.Store.initS3WithReferencedCredentials(path, null, default_credentials, bun.default_allocator)); + break :brk bun.handleOom(Blob.Store.initS3WithReferencedCredentials(path.*, null, default_credentials, bun.default_allocator)); } }; + // initS3 took ownership of the PathLike (toThreadSafe transfers the + // underlying ref for string-backed paths, and the store now holds the + // only handle for the others); neuter the caller's copy so any outer + // `errdefer path.deinit()` doesn't double-free on a later error. + path.* = .{ .string = bun.PathString.empty }; errdefer store.deinit(); store.data.s3.options = aws_options.options; store.data.s3.acl = aws_options.acl; @@ -300,13 +305,16 @@ pub fn constructS3FileWithS3CredentialsAndOptions( pub fn constructS3FileWithS3Credentials( globalObject: *jsc.JSGlobalObject, - path: jsc.Node.PathLike, + path: *jsc.Node.PathLike, options: ?jsc.JSValue, existing_credentials: S3.S3Credentials, ) bun.JSError!Blob { 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)); + const store = bun.handleOom(Blob.Store.initS3(path.*, null, aws_options.credentials, bun.default_allocator)); + // initS3 took ownership of the PathLike; neuter the caller's copy so any + // outer `errdefer path.deinit()` doesn't double-free on a later error. + path.* = .{ .string = bun.PathString.empty }; errdefer store.deinit(); store.data.s3.options = aws_options.options; store.data.s3.acl = aws_options.acl; @@ -343,7 +351,7 @@ pub fn constructS3FileWithS3Credentials( } fn constructS3FileInternal( globalObject: *jsc.JSGlobalObject, - path: jsc.Node.PathLike, + path: *jsc.Node.PathLike, options: ?jsc.JSValue, ) bun.JSError!*Blob { return Blob.new(try constructS3FileInternalStore(globalObject, path, options)); @@ -568,12 +576,12 @@ pub fn stat(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSEr } switch (path_or_blob) { - .path => |path| { + .path => |*path| { const options = args.nextEat(); - if (path == .fd) { + if (path.* == .fd) { return globalThis.throwInvalidArguments("Expected a S3 or path to get size", .{}); } - var blob = try constructS3FileInternalStore(globalThis, path.path, options); + var blob = try constructS3FileInternalStore(globalThis, &path.path, options); defer blob.deinit(); return S3BlobStatTask.stat(globalThis, &blob); @@ -586,7 +594,7 @@ pub fn stat(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSEr pub fn constructInternalJS( globalObject: *jsc.JSGlobalObject, - path: jsc.Node.PathLike, + path: *jsc.Node.PathLike, options: ?jsc.JSValue, ) bun.JSError!JSValue { const blob = try constructS3FileInternal(globalObject, path, options); @@ -609,10 +617,11 @@ pub fn constructInternal( var args = jsc.CallFrame.ArgumentsSlice.init(vm, arguments); defer args.deinit(); - const path = (try jsc.Node.PathLike.fromJS(globalObject, &args)) orelse { + var path = (try jsc.Node.PathLike.fromJS(globalObject, &args)) orelse { return globalObject.throwInvalidArguments("Expected file path string", .{}); }; - return constructS3FileInternal(globalObject, path, args.nextEat()); + errdefer path.deinit(); + return constructS3FileInternal(globalObject, &path, args.nextEat()); } pub fn construct(globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) callconv(jsc.conv) ?*Blob { diff --git a/test/js/bun/s3/s3-presign-missing-credentials.test.ts b/test/js/bun/s3/s3-presign-missing-credentials.test.ts new file mode 100644 index 00000000000..d80e155aa7d --- /dev/null +++ b/test/js/bun/s3/s3-presign-missing-credentials.test.ts @@ -0,0 +1,57 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe } from "harness"; + +// When S3 credentials are missing, presign() throws synchronously from +// signRequest after a temporary Blob.Store has already taken ownership of +// the path. The caller's errdefer used to deinit that path a second time, +// which under-flowed the StringImpl refcount and SIGFPE'd in +// StringImpl::costDuringGC on the next collection. + +const cleanEnv: Record = { + ...bunEnv, + AWS_ACCESS_KEY_ID: undefined, + AWS_SECRET_ACCESS_KEY: undefined, + AWS_SESSION_TOKEN: undefined, + AWS_REGION: undefined, + AWS_ENDPOINT: undefined, + AWS_BUCKET: undefined, + S3_ACCESS_KEY_ID: undefined, + S3_SECRET_ACCESS_KEY: undefined, + S3_SESSION_TOKEN: undefined, + S3_REGION: undefined, + S3_ENDPOINT: undefined, + S3_BUCKET: undefined, +}; + +test("S3 presign with missing credentials throws instead of crashing", async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + ` + const path = "some-key-name"; + try { Bun.s3.presign(path); } catch (e) { console.log(e.code); } + try { new Bun.S3Client().presign(path); } catch (e) { console.log(e.code); } + try { Bun.S3Client.presign(path); } catch (e) { console.log(e.code); } + try { Bun.file("s3://bucket/" + path).presign(); } catch (e) { console.log(e.code); } + Bun.gc(true); + console.log("ok"); + `, + ], + env: cleanEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stderr).toBe(""); + expect(stdout.trim().split("\n")).toEqual([ + "ERR_S3_MISSING_CREDENTIALS", + "ERR_S3_MISSING_CREDENTIALS", + "ERR_S3_MISSING_CREDENTIALS", + "ERR_S3_MISSING_CREDENTIALS", + "ok", + ]); + expect(exitCode).toBe(0); +}); From 6319bc16d20ca04508516f9c61bf5048593bd2b5 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 10 May 2026 23:52:51 +0000 Subject: [PATCH 2/2] Blob: use path.deinit() in s3:// errdefer to avoid double-unprotect args.deinit() already unprotects .buffer paths via the protectEat bitset; deinitAndUnprotect() here would unprotect a second time on the pre-initS3 error path. deinit() still frees the string-backed variants and matches the other S3 call sites. --- src/runtime/webcore/Blob.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/webcore/Blob.zig b/src/runtime/webcore/Blob.zig index bbb84857fb8..e40bac0dfc4 100644 --- a/src/runtime/webcore/Blob.zig +++ b/src/runtime/webcore/Blob.zig @@ -2069,7 +2069,7 @@ pub fn constructBunFile( if (path == .path) { if (strings.hasPrefixComptime(path.path.slice(), "s3://")) { - errdefer path.deinitAndUnprotect(); + errdefer path.deinit(); return try S3File.constructInternalJS(globalObject, &path.path, options); } }