Skip to content
Open
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
55 changes: 55 additions & 0 deletions packages/core/src/persist.test.ts
Original file line number Diff line number Diff line change
@@ -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<T extends object>(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<string, any> = {
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();
});
});
13 changes: 9 additions & 4 deletions packages/core/src/persist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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.
*/
Expand Down