diff --git a/packages/core/src/persist.test.ts b/packages/core/src/persist.test.ts new file mode 100644 index 0000000..534302d --- /dev/null +++ b/packages/core/src/persist.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from "vitest"; +import { __test } from "./persist"; + +const { createDeepProxy } = __test; + +// No-op trigger for testing (we don't need persistence in these tests) +const noopTrigger = () => {}; + +/** + * Helper to create a proxy using the real createDeepProxy function. + * Uses minimal mock instance and property key for testing purposes. + */ +function createTestProxy(value: T): T { + return createDeepProxy(value, {}, "testProp", noopTrigger); +} + +describe("persist proxy - error handler heap overflow bug", () => { + it("should return undefined on error instead of creating infinite proxy chain", () => { + // Create an object with a throwing getter + const throwingObj = { + get badProp(): never { + throw new Error("This getter throws"); + }, + normalProp: "hello", + }; + + const proxied = createTestProxy(throwingObj); + + // Accessing the throwing getter should return undefined, not create {} and recurse + // Before the fix, this would cause a heap overflow from infinite proxy recursion + const result = proxied.badProp; + + expect(result).toBe(undefined); + // The normal prop should still work + expect(proxied.normalProp).toBe("hello"); + }); + + it("should not corrupt the original object on error", () => { + const original: Record = { + get explosive(): never { + throw new Error("boom"); + }, + }; + + const proxied = createTestProxy(original); + + // Access the throwing getter + void proxied.explosive; + + // The original should not be mutated with {} + // Check that 'explosive' is still a getter, not {} + const descriptor = Object.getOwnPropertyDescriptor(original, "explosive"); + expect(descriptor?.get).toBeDefined(); + }); +}); diff --git a/packages/core/src/persist.ts b/packages/core/src/persist.ts index dc78742..bf84a15 100644 --- a/packages/core/src/persist.ts +++ b/packages/core/src/persist.ts @@ -123,10 +123,10 @@ function createDeepProxy(value: any, instance: any, propertyKey: string, trigger return prop; } catch (e) { console.error(`Error accessing property ${String(key)}:`, e); - // Return an empty object proxy for error recovery - const newObj = {}; - Reflect.set(target, key, newObj); - return createDeepProxy(newObj, instance, propertyKey, triggerPersist); + // Return undefined on error - don't auto-vivify as it causes: + // 1. Silent data corruption (replaces original value with {}) + // 2. Infinite proxy recursion leading to heap overflow + return undefined; } }, set(target, key, newValue) { @@ -515,6 +515,11 @@ function safeParse(json: string): any { }); } +// Test exports - expose internal functions for unit testing +export const __test = { + createDeepProxy, +}; + /** * Helper function to persist a property value to storage. */