Skip to content

🔒 Fix unbounded memory growth in undo history#45

Closed
zknpr wants to merge 1 commit intomainfrom
fix-undo-history-memory-leak-7224620587777115643
Closed

🔒 Fix unbounded memory growth in undo history#45
zknpr wants to merge 1 commit intomainfrom
fix-undo-history-memory-leak-7224620587777115643

Conversation

@zknpr
Copy link
Copy Markdown
Owner

@zknpr zknpr commented Feb 7, 2026

This PR addresses a security vulnerability where the undo history could grow unbounded in memory, potentially leading to a Denial of Service via memory exhaustion.

Risk:
Previously, the undo history was only limited by the number of entries (maxEntries), not their size. An attacker or a user performing large operations (e.g., bulk updates or inserting large blobs) could fill the history with very large objects, causing the extension to crash or the system to run out of memory.

Solution:
I have implemented a memory tracking mechanism in ModificationTracker (src/core/undo-history.ts):

  1. Size Estimation: A calculateSize function estimates the memory usage of each modification entry, accounting for primitives, objects, arrays, and Uint8Arrays.
  2. Memory Limit: A maxMemory parameter (defaulting to 50MB) is added to the ModificationTracker.
  3. Eviction Policy: When recording a new modification, if the total size exceeds maxMemory (or the count exceeds maxEntries), the oldest entries are removed until the history fits within the limits.
  4. Persistence: The size tracking is preserved/recalculated during serialization and deserialization (hot exit), ensuring limits are respected after reload.

Verification:

  • Added a new test suite tests/unit/undo_memory_limit.test.ts which verifies:
    • Entries are evicted when maxMemory is exceeded.
    • At least one entry is kept to preserve basic undo functionality.
    • Size tracking works correctly across undo/redo operations.
    • Deserialization correctly restores size tracking.
  • Ran existing tests to ensure no regressions. npm test passes (after fixing dependency resolution with npm install).

PR created automatically by Jules for task 7224620587777115643 started by @zknpr

This commit introduces a memory limit to the `ModificationTracker` to prevent unbounded memory growth in the undo history.

Changes:
- Added `calculateSize` helper to estimate the memory footprint of modifications.
- Updated `ModificationTracker` to track total size of history entries.
- Added `maxMemory` configuration (default 50MB).
- Implemented eviction logic in `record()` to remove oldest entries when memory limit is exceeded.
- Updated `deserialize` to recalculate entry sizes.
- Added unit tests in `tests/unit/undo_memory_limit.test.ts`.

This fixes a potential security vulnerability where large modifications could cause the extension to consume excessive memory.

Co-authored-by: zknpr <96851588+zknpr@users.noreply.github.com>
@google-labs-jules
Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Feb 7, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
sq-lite-explorer Ready Ready Preview, Comment Feb 7, 2026 1:44pm

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello @zknpr, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the robustness and security of the application by introducing memory limits to the undo history mechanism. Previously, large undo operations could lead to excessive memory consumption and potential Denial of Service. The changes now actively monitor and control the memory footprint of undo entries, ensuring that the system remains stable and responsive even under heavy usage, while preserving the essential undo/redo functionality.

Highlights

  • Memory Limit for Undo History: Introduced a maxMemory limit (default 50MB) to prevent unbounded memory growth in the ModificationTracker, addressing a security vulnerability.
  • Dynamic Size Estimation: Added a calculateSize utility function to accurately estimate the memory footprint of individual modification entries, including primitives, objects, arrays, and Uint8Arrays.
  • Intelligent Eviction Policy: Implemented logic to evict the oldest undo entries when either the maxEntries count or the new maxMemory limit is exceeded, ensuring system stability.
  • Persistent Size Tracking: Ensured that memory size tracking is correctly preserved and recalculated during serialization and deserialization, maintaining limits across application restarts.
  • Comprehensive Unit Tests: A new test suite (undo_memory_limit.test.ts) was added to thoroughly validate the memory limit functionality, including eviction, single-entry retention, and deserialization behavior.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • package-lock.json
    • Updated project version from 1.2.5 to 1.2.7.
  • src/core/undo-history.ts
    • Added calculateSize function for estimating memory usage of arbitrary JavaScript values, handling primitives, objects, arrays, and Uint8Array.
    • Introduced maxMemory property (defaulting to 50MB) and currentSize to ModificationTracker.
    • Modified the record method to calculate the size of new entries, discard redo history sizes, and enforce both maxEntries and maxMemory limits by evicting the oldest entries. It also ensures at least one entry remains if possible.
    • Updated stepBack, stepForward, and commit methods to correctly manage associated size arrays (timelineSizes, futureStackSizes) alongside the modification entries.
    • Enhanced the deserialize static method to accept maxMemory and to recalculate the timelineSizes and currentSize upon restoring the tracker state.
  • tests/unit/undo_memory_limit.test.ts
    • Added a new test file to verify the ModificationTracker's memory limit functionality.
    • Includes tests for respecting maxMemory, ensuring at least one entry is kept, recalculating sizes on deserialization, and correct memory tracking during undo/redo operations.
Activity
  • The pull request was automatically generated by Jules for task 7224620587777115643, initiated by @zknpr.
  • The author has proactively addressed a security vulnerability related to memory exhaustion in the undo history.
  • The changes include a new memory tracking mechanism and comprehensive testing to ensure stability and prevent regressions.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request effectively addresses a critical security vulnerability by introducing a memory limit to the undo history, preventing unbounded memory growth. The implementation, including the size calculation and eviction logic, is well-conceived and the accompanying tests cover the new functionality appropriately. I've identified a couple of areas where the implementation could be made more robust against potential state desynchronization, which could undermine the memory accounting. I've also included a note on a potential performance bottleneck in the eviction logic for your consideration. Overall, this is a solid and important fix.

Comment thread src/core/undo-history.ts

if (entry) {
this.futureStack.push(entry);
this.futureStackSizes.push(size || 0);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

There's a potential issue here if timelineSizes becomes desynchronized from timeline, causing size to be undefined. Pushing 0 in this case would lead to an incorrect currentSize over time, partially negating the memory-limiting fix. To make this more robust, it's safer to recalculate the size of the entry if it's missing from the timelineSizes array. This ensures memory accounting remains accurate even if a desynchronization bug occurs.

Suggested change
this.futureStackSizes.push(size || 0);
this.futureStackSizes.push(size ?? calculateSize(entry));

Comment thread src/core/undo-history.ts

if (entry) {
this.timeline.push(entry);
this.timelineSizes.push(size || 0);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Similar to the stepBack method, if size is undefined due to a potential desynchronization between futureStack and futureStackSizes, pushing 0 will lead to incorrect memory accounting. Recalculating the size as a fallback will make the system more resilient against state corruption.

Suggested change
this.timelineSizes.push(size || 0);
this.timelineSizes.push(size ?? calculateSize(entry));

Comment thread src/core/undo-history.ts
Comment on lines +192 to 211
while (
(this.timeline.length > 0) &&
(this.timeline.length > this.maxEntries || this.currentSize > this.maxMemory)
) {
// Don't remove the just-added entry if it's the only one, to preserve ability to undo at least one step if possible.
// However, if strict memory limit is required, we might need to, but let's be practical.
if (this.timeline.length === 1) {
break;
}

const removedEntrySize = this.timelineSizes.shift();
this.timeline.shift();

if (removedEntrySize !== undefined) {
this.currentSize -= removedEntrySize;
}

// Enforce capacity limit
if (this.timeline.length > this.maxEntries) {
const overflow = this.timeline.length - this.maxEntries;
this.timeline.splice(0, overflow);
this.checkpointIndex = Math.max(0, this.checkpointIndex - overflow);
// Adjust checkpoint index since we shifted the array
this.checkpointIndex = Math.max(0, this.checkpointIndex - 1);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Using shift() inside a while loop can lead to poor performance for large arrays. Each shift() operation is O(n) because it requires re-indexing all subsequent elements. In a scenario where many small, old entries need to be evicted to make space for one large new entry, this loop's complexity could approach O(k*n), where k is the number of evicted items and n is the array length. While perhaps acceptable for the default maxEntries, this could become a performance bottleneck if the undo history grows large. For a history structure, a more performant data structure like a doubly-linked list would provide O(1) for additions and removals from both ends.

@zknpr
Copy link
Copy Markdown
Owner Author

zknpr commented Feb 7, 2026

Merged as part of v1.3.0 release in PR #65

@zknpr zknpr closed this Feb 7, 2026
@google-labs-jules
Copy link
Copy Markdown
Contributor

Merged as part of v1.3.0 release in PR #65

Thank you! Glad to have this security fix merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant