Skip to content

fix: enforce plugin entry limit during discovery - #146

Open
GautamSharma99 wants to merge 1 commit into
openai:mainfrom
GautamSharma99:fix/bounded-plugin-directory-walk
Open

fix: enforce plugin entry limit during discovery#146
GautamSharma99 wants to merge 1 commit into
openai:mainfrom
GautamSharma99:fix/bounded-plugin-directory-walk

Conversation

@GautamSharma99

Copy link
Copy Markdown
Contributor

Summary

Fixes #134.

Enforce the configured-plugin 4,096-entry ceiling while directory entries are discovered instead of after an entire directory has already been loaded into memory.

Previously, both plugin fingerprinting and marketplace copying called readdir() for each directory. A directory containing hundreds of thousands of children was therefore materialized as one complete name array before any of those children reached the existing entry counter. Cancellation also could not be observed until that full read completed.

Changes

Incremental directory iteration

Configured plugin traversal now uses one shared asynchronous opendir() iterator for both:

  • plugin projection fingerprint discovery;
  • plugin marketplace copy discovery.

The iterator reads one Dirent at a time with Dir.read() and yields only its name. No traversal path creates a complete array of directory children.

Discovery-time entry enforcement

The traversal root remains counted as the first entry, preserving the existing 4,096-entry semantics.

Each child is now counted immediately after it is yielded and before it is added to the traversal queue. The 4,097th discovered path therefore raises the existing PluginBootstrapError immediately, without reading the remaining names in that directory.

This applies identically to fingerprint construction and safe-copy traversal.

Cancellation during discovery

The abort signal is checked:

  • before opening a directory;
  • before each individual directory read;
  • after each read completes;
  • before the discovered entry is yielded to the traversal.

Cancellation can now interrupt a large, flat directory rather than waiting for every name to be allocated first.

Directory-handle cleanup

The shared iterator closes its directory handle in a finally block. Handles are therefore released after:

  • normal end-of-directory;
  • cancellation;
  • entry-limit rejection;
  • any other traversal error.

Existing integrity boundaries preserved

The change retains the existing protections around configured plugin directories:

  • source paths are still checked with lstat() before processing;
  • symbolic links and non-regular files remain rejected;
  • copied directories are still re-stat'd after enumeration;
  • directory device/inode, size, modification time, mode, and type identity must remain unchanged;
  • queued descendants still revalidate every recorded ancestor before copying;
  • per-file and aggregate byte limits are unchanged;
  • partially created marketplace trees are removed on failure.

Fingerprint paths remain sorted before hashing, so switching to incremental directory iteration does not change fingerprint determinism.

Security and resource impact

A malicious or accidentally enormous flat plugin directory can no longer bypass the practical value of the configured 4,096-entry limit by forcing one unbounded readdir() allocation.

Directory plugins now have the same entry-by-entry resource-boundary behavior already used for ZIP plugin extraction, and cancellation remains responsive throughout discovery.

Tests

Added regression coverage for:

  • a configured plugin containing 4,096 additional siblings in one directory;
  • rejection through the existing copy-entry limit;
  • removal of the partially created marketplace after overflow;
  • cancellation triggered during the second individual directory read;
  • preservation of the original AbortError reason;
  • confirmation that discovery stops immediately at cancellation;
  • removal of partial marketplace output after cancellation.

Existing configured-plugin, marketplace projection, symlink rejection, queued-directory identity race, ZIP limit, and runtime tests continue to pass.

Verification

  • pnpm run types
  • pnpm run format
  • pnpm run build
  • complete runtime test file
  • PATH="/opt/homebrew/bin:$PATH" pnpm run test

Full test result:

  • 472 passed
  • 6 expected platform/integration skips
  • 0 failed

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.

Configured plugin directory entry limit is applied after unbounded readdir

1 participant