Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions packages/core/src/integrations/functiontostring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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
Expand Down
36 changes: 36 additions & 0 deletions packages/core/test/lib/integrations/functiontostring.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading