Skip to content

Fix race condition in content exclusion that causes intermittent bypass#308814

Draft
ulugbekna wants to merge 2 commits intomainfrom
fix/content-exclusion-race-condition
Draft

Fix race condition in content exclusion that causes intermittent bypass#308814
ulugbekna wants to merge 2 commits intomainfrom
fix/content-exclusion-race-condition

Conversation

@ulugbekna
Copy link
Copy Markdown
Contributor

@ulugbekna ulugbekna commented Apr 9, 2026

Problem

Concurrent isIgnored() calls in RemoteContentExclusion can permanently cache stale "not ignored" results when a content exclusion rule fetch is in progress, causing files that should be excluded to intermittently bypass content exclusion rules.

Reported in: github/Copilot-Controls#650

Root Cause

The race condition occurs between concurrent isIgnored() calls during the initial content exclusion rule fetch:

  1. Call A enters isIgnored(), yields at getRepositoryFetchUrls() (async)
  2. Call B enters isIgnored(), yields at getRepositoryFetchUrls() (async)
  3. Call A resumes, calls shouldFetchContentExclusionRules() which seeds _contentExclusionCache with empty patterns and triggers a network fetch — yields again
  4. Call B resumes: shouldFetchContentExclusionRules() returns false (entries already seeded), stale-time check passes (just set), no fetch is awaited. Matches against empty patterns → caches false in _ignoreGlobResultCache
  5. Fetch completes with real rules, but Call B's stale false entry persists until the 30-minute refresh

This explains the customer's symptoms: "it manages to exclude files sometimes but after a reload it excludes another set of files" — which files hit the race window depends on timing.

Fix

Three changes to remoteContentExclusion.ts:

  1. Cache shouldFetchContentExclusionRules result — the method has side effects (seeds _contentExclusionCache with empty entries), so calling it twice could produce different results and misleading log output. Store the result in a local variable.

  2. Re-check _contentExclusionFetchPromise after the getRepositoryFetchUrls yield point — if a concurrent caller started a fetch while we were suspended, wait for it to complete before matching patterns. This prevents matching against empty seed entries.

  3. Clear both _ignoreGlobResultCache and _ignoreRegexResultCache at both start and end of _contentExclusionRequest() — the start-of-method clear was already there but only for the glob cache. Now both caches are cleared symmetrically at both ends, invalidating any stale entries written by concurrent callers during the fetch window.

Commits

  • Commit 1 — Adds the failing tests that reproduce the exact race condition using per-call barriers to force deterministic interleaving. The primary test fails on main.
  • Commit 2 — Adds the fix. All tests pass.

Testing

Three new tests in remoteContentExclusion.spec.ts:

  • "concurrent call must not cache false while rules are being fetched" — The primary regression test. Uses per-call barriers on getRepositoryFetchUrls and the CAPI fetch to force the exact interleaving: Call A seeds empty patterns and starts the fetch, Call B resumes and must wait instead of matching against empty rules. Fails without the fix, passes with it.
  • "post-fetch cache clear invalidates stale entries from concurrent callers" — Verifies that the post-fetch cache clear ensures a third sequential call also correctly excludes files.
  • "should exclude non-git files when rules arrive after call starts" — Verifies correct exclusion for non-git files when the CAPI response is delayed.

Copilot AI review requested due to automatic review settings April 9, 2026 16:14
@ulugbekna ulugbekna marked this pull request as draft April 9, 2026 16:16
@ulugbekna ulugbekna force-pushed the fix/content-exclusion-race-condition branch from e935283 to cb24d7a Compare April 9, 2026 16:17
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes a race in RemoteContentExclusion.isIgnored() where concurrent calls could evaluate against seeded empty exclusion rules and then cache stale “not ignored” results for an extended period, intermittently bypassing content exclusion.

Changes:

  • Await an in-flight content exclusion fetch that may have been started by a concurrent caller after an await point in isIgnored().
  • Clear ignore-result caches after rules are fetched to invalidate any stale entries written during the fetch window.
  • Add unit tests that reproduce the concurrency window using Barrier to control async timing.
Show a summary per file
File Description
extensions/copilot/src/platform/ignore/node/remoteContentExclusion.ts Adds an extra await for concurrent fetches and clears ignore caches again after successful rule fetch.
extensions/copilot/src/platform/ignore/node/test/remoteContentExclusion.spec.ts Adds barrier-controlled tests covering the reported concurrency timing windows.

Copilot's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 4

@ulugbekna ulugbekna force-pushed the fix/content-exclusion-race-condition branch from cb24d7a to cbf4613 Compare April 9, 2026 16:21
Adds tests that reproduce the intermittent content exclusion bypass
reported in github/Copilot-Controls#650.

The primary test uses per-call barriers to force a deterministic
interleaving where Call A starts the CAPI fetch (seeding empty patterns)
and Call B resumes before the fetch completes, matching against empty
rules and incorrectly caching 'not ignored'.

This test FAILS on the current code — the next commit provides the fix.
Concurrent isIgnored() calls could cache stale 'not ignored' results
when a content exclusion rule fetch was in progress. This happened
because:

1. After yielding at getRepositoryFetchUrls, a concurrent caller could
   have already started a fetch and seeded the cache with empty patterns.
   The current caller would skip the fetch (patterns already seeded,
   lastRuleFetch just set) and match against empty rules.

2. The glob result cache was only cleared at the start of the fetch, so
   any 'false' entries written during the fetch window persisted until
   the next 30-minute refresh.

Fix:
- Cache the result of shouldFetchContentExclusionRules to avoid calling
  it twice (it has side effects that seed the cache).
- Re-check _contentExclusionFetchPromise after the getRepositoryFetchUrls
  yield point and wait for any in-progress fetch before pattern matching.
- Clear both glob and regex result caches at both the start and end of
  the fetch to invalidate stale entries written by concurrent callers.

Fixes github/Copilot-Controls#650
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.

2 participants