Skip to content

fix(core): Read __sentry_original__ as own property to avoid overflow on cyclic Proxy prototype chains - #25964

Closed
chiliec wants to merge 1 commit into
getsentry:developfrom
chiliec:fix/functiontostring-cyclic-proxy-prototype
Closed

chiliec wants to merge 1 commit into
getsentry:developfrom
chiliec:fix/functiontostring-cyclic-proxy-prototype

Conversation

@chiliec

@chiliec chiliec commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

Fixes #25963

What

functionToStringIntegration patches Function.prototype.toString to return the original source for wrapped functions. The wrapper calls getOriginalFunction(this), which reads this.__sentry_original__. For a function that does not have that own property, the read walks the prototype chain. If the chain loops back through a Proxy, the walk never terminates and overflows the stack inside the wrapper frame, while native toString returns normally for the same function. Anti-tamper/fingerprinting probes (e.g. Castle bot detection) use exactly this kind of cyclic-proxy probe to detect patched natives.

This remained after #25921 / #25922, which fixed the apply re-entry and huge-argument-list overflows — not this property lookup.

Fix

markFunctionWrapped always sets __sentry_original__ as an own non-enumerable property, so reading it as an own property is equivalent for every legitimately wrapped function and avoids the unbounded prototype-chain walk:

return Object.prototype.hasOwnProperty.call(func, '__sentry_original__') ? func.__sentry_original__ : undefined;

Tests

Added a regression test to the existing FunctionToString suite that gives a function a prototype chain looping through a Proxy and asserts the patched toString (invoked via the bound Function.prototype.call form, as the reported probe does) returns the native result instead of overflowing.

Validation (local):

cd packages/core
pnpm vitest run test/lib/integrations/functiontostring.test.ts test/lib/utils/object.test.ts
  • RED (before the fix): does not overflow the stack when the prototype chain loops through a Proxy fails with RangeError: Maximum call stack size exceeded at getOriginalFunction (src/utils/object.ts).
  • GREEN (after the fix): Test Files 2 passed (2), Tests 46 passed (46) — the new test plus all existing functiontostring and object tests pass.

@chiliec
chiliec requested review from a team as code owners October 30, 2026 17:36
@chiliec
chiliec requested review from isaacs, logaretm, msonnb and mydea and removed request for a team October 30, 2026 17:36
…Function

The FunctionToString integration's patched toString reads this.__sentry_original__
via getOriginalFunction, which walks the prototype chain for a function that lacks
the own property. When the chain loops back through a Proxy the walk never
terminates and overflows the stack inside the wrapper, while native toString
returns normally. markFunctionWrapped always sets __sentry_original__ as an own
non-enumerable property, so reading it as an own property is equivalent for every
wrapped function and avoids the unbounded prototype walk.

Signed-off-by: Vladimir Babin <vovababin@gmail.com>
@chiliec
chiliec requested review from nicohrubec and removed request for a team October 30, 2026 17:36
@chiliec
chiliec force-pushed the fix/functiontostring-cyclic-proxy-prototype branch from bee0f9d to 78f76f3 Compare October 30, 2026 17:36
@github-actions github-actions Bot added the external PR from an external contributor label Oct 30, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 78f76f3. Configure here.

getClient()?.addIntegration(fts);

expect(callToString(inspectedFunction)).toBe(expected);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regression test skips patched toString

Low Severity

The new test binds Function.prototype.toString before addIntegration, so callToString keeps the native function when this case runs first or alone. That path never enters getOriginalFunction and can pass without the overflow fix. Flagged from the review rules: a fix PR's regression test needs to fail without the change.

Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit 78f76f3. Configure here.

@logaretm logaretm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for digging into this! Per our contributing guide, we would have appreciated a discussion first before taking it on, since we shipped two PRs in this area.

I'm closing this because Object.prototype.hasOwnProperty.call looks up .call every time it runs, so a page that hooks Function.prototype.call sends the wrapper into infinite recursion again, which is the same kind of re-entry #25921 fixed.

Here is an example:

// e.g. an anti-tamper or call-tracing script
Function.prototype.call = function (thisArg, ...args) {
  const isNative = this.toString().includes('[native code]');
  return Reflect.apply(this, thisArg, args);
};

setTimeout.toString(); // RangeError: Maximum call stack size exceeded

@logaretm logaretm closed this Oct 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

external PR from an external contributor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FunctionToString overflows on cyclic Proxy prototype chains

2 participants