diff --git a/packages/core/src/integrations/functiontostring.ts b/packages/core/src/integrations/functiontostring.ts index c915e55d8c3f..2f3a8ce295d3 100644 --- a/packages/core/src/integrations/functiontostring.ts +++ b/packages/core/src/integrations/functiontostring.ts @@ -15,11 +15,15 @@ const _functionToStringIntegration = (() => { setupOnce() { // eslint-disable-next-line @typescript-eslint/unbound-method const originalFunctionToString = Function.prototype.toString; + // Bound once so calls neither look up a page-replaceable `apply` nor copy arguments, which native toString ignores + const callOriginalFunctionToString = Function.prototype.call.bind(originalFunctionToString) as ( + thisArg: unknown, + ) => string; // intrinsics (like Function.prototype) might be immutable in some environments // e.g. Node with --frozen-intrinsics, XS (an embedded JavaScript engine) or SES (a JavaScript proposal) try { - Function.prototype.toString = function (this: WrappedFunction, ...args: unknown[]): string { + Function.prototype.toString = function (this: WrappedFunction): string { const originalFunction = getOriginalFunction(this); let unwrappedFunction: WrappedFunction | undefined; @@ -33,7 +37,7 @@ const _functionToStringIntegration = (() => { // `toString` never throws here, so fall back to it to avoid turning harmless introspection into noise. } - return originalFunctionToString.apply(unwrappedFunction ?? this, args); + return callOriginalFunctionToString(unwrappedFunction ?? this); }; } catch { // ignore errors here, just don't patch this diff --git a/packages/core/test/lib/integrations/functiontostring.test.ts b/packages/core/test/lib/integrations/functiontostring.test.ts index 99b54fa1c3ff..e42c83a77611 100644 --- a/packages/core/test/lib/integrations/functiontostring.test.ts +++ b/packages/core/test/lib/integrations/functiontostring.test.ts @@ -84,6 +84,42 @@ describe('FunctionToString', () => { expect(inspectedFunction.toString()).toBe(expected); }); + it('does not recurse when Function.prototype.apply performs a function toString check', () => { + function inspectedFunction(): void {} + + const fts = functionToStringIntegration(); + getClient()?.addIntegration(fts); + const expected = inspectedFunction.toString(); + + const applySpy = vi.spyOn(Function.prototype, 'apply').mockImplementation(function ( + this: Function, + thisArg: unknown, + args?: unknown[], + ) { + this.toString(); + return Reflect.apply(this, thisArg, args ?? []); + }); + let result: unknown; + try { + result = inspectedFunction.toString(); + } catch (error) { + result = error; + } + applySpy.mockRestore(); + + expect(result).toBe(expected); + }); + + it('does not overflow the stack when called with many arguments', () => { + function inspectedFunction(): void {} + + const fts = functionToStringIntegration(); + getClient()?.addIntegration(fts); + const expected = inspectedFunction.toString(); + + expect(Reflect.apply(Function.prototype.toString, inspectedFunction, new Array(100_000))).toBe(expected); + }); + it('falls back to native toString and does not throw when the carrier read throws', () => { const foo = { bar(wat: boolean): boolean {