From 78f76f3d14286b82d85429f04222e3290c564af6 Mon Sep 17 00:00:00 2001 From: Vladimir Babin Date: Sat, 31 Oct 2026 01:35:44 +0800 Subject: [PATCH] fix(core): Read __sentry_original__ as an own property in getOriginalFunction 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 --- CHANGELOG.md | 4 ++++ packages/core/src/utils/object.ts | 2 +- .../test/lib/integrations/functiontostring.test.ts | 12 ++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fff7847dd506..3e38190139d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ When Next.js serves a page from its cache, for example an ISR page or a `cacheComponents` shell, the pageload now has the `page.server_rendering.type` attribute. It is `static` if the whole page came from the cache, and `partially_static` if parts of it rendered for this request. Then the pageload stays connected to this request, as before. If the request that rendered the cached page was sampled, the pageload also links to it. So in Sentry, a cached page no longer looks like missing server instrumentation. +### Other Changes + +- fix(core): Read `__sentry_original__` as an own property in `getOriginalFunction` ([#25964](https://github.com/getsentry/sentry-javascript/pull/25964)) + Work in this release was contributed by @lazerg and @chiliec. Thank you for your contributions! ## 11.10.0 diff --git a/packages/core/src/utils/object.ts b/packages/core/src/utils/object.ts index 46f40c2cd3c7..415f4bedc1fb 100644 --- a/packages/core/src/utils/object.ts +++ b/packages/core/src/utils/object.ts @@ -120,7 +120,7 @@ export function wrapMethod( */ // eslint-disable-next-line @typescript-eslint/ban-types export function getOriginalFunction(func: WrappedFunction): T | undefined { - return func.__sentry_original__; + return Object.prototype.hasOwnProperty.call(func, '__sentry_original__') ? func.__sentry_original__ : undefined; } /** diff --git a/packages/core/test/lib/integrations/functiontostring.test.ts b/packages/core/test/lib/integrations/functiontostring.test.ts index e42c83a77611..c47ce362f692 100644 --- a/packages/core/test/lib/integrations/functiontostring.test.ts +++ b/packages/core/test/lib/integrations/functiontostring.test.ts @@ -120,6 +120,18 @@ describe('FunctionToString', () => { expect(Reflect.apply(Function.prototype.toString, inspectedFunction, new Array(100_000))).toBe(expected); }); + it('does not overflow the stack when the prototype chain loops through a Proxy', () => { + function inspectedFunction(): void {} + Object.setPrototypeOf(inspectedFunction, Object.create(new Proxy(inspectedFunction, {}))); + const callToString = Function.prototype.call.bind(Function.prototype.toString) as (thisArg: unknown) => string; + const expected = callToString(inspectedFunction); + + const fts = functionToStringIntegration(); + getClient()?.addIntegration(fts); + + expect(callToString(inspectedFunction)).toBe(expected); + }); + it('falls back to native toString and does not throw when the carrier read throws', () => { const foo = { bar(wat: boolean): boolean {