Skip to content

Fix service worker init race, selection capture deadlock, and post-logout singleton leak#32

Draft
Copilot wants to merge 2 commits intomainfrom
copilot/fix-service-worker-state-management
Draft

Fix service worker init race, selection capture deadlock, and post-logout singleton leak#32
Copilot wants to merge 2 commits intomainfrom
copilot/fix-service-worker-state-management

Conversation

Copy link

Copilot AI commented Mar 13, 2026

Three interconnected state management bugs in the service worker: message handlers could fire before storage was ready, a second selection capture would permanently hang the first, and NumbersApiManager's lazy singleton persisted stale upload state across user sessions after logout.

Changes

src/background/service-worker.ts

  • Init readiness gate: Stored the top-level Promise.all([assetStorage.init(), metadataStorage.init()]) as initPromise. Every onMessage case handler now await initPromise before touching storage, closing the race window on service worker wake-up:

    const initPromise = Promise.all([assetStorage.init(), metadataStorage.init()]).then(...)
    // In each handler:
    await initPromise;
  • Selection deadlock prevention: In handleSelectionCapture, any pre-existing pending selection promise is now explicitly rejected before overwriting the callbacks, so a second capture attempt doesn't strand the first caller indefinitely:

    if (pendingSelectionReject) {
      pendingSelectionReject(new Error('Selection cancelled: a new selection was started'));
      pendingSelectionResolve = null;
      pendingSelectionReject = null;
    }
  • Error visibility: Added .catch() on the SELECTION_COMPLETE async IIFE to surface errors rather than swallowing them silently.

src/services/NumbersApiManager.ts

  • Singleton reset on logout: Added export function resetInstance() and called it from clearAuth(). After logout the next getNumbersApi() call constructs a clean instance with a fresh UploadService and no leftover callbacks from the prior session.

  • Init-time clearAuth guard: getNumbersApi() now handles the edge case where initialize() invalidates a stale stored token (calling clearAuth() internally, which resets instance). If instance is null after initialize(), a fresh unauthenticated manager is created and stored rather than returning a dereferenced null.

Original prompt

This section details on the original issue you should resolve

<issue_title>[Security][High] Service worker state management: init race, singleton leak, and selection deadlock</issue_title>
<issue_description>## Summary

Three interconnected state management vulnerabilities in the service worker create reliability and security risks.

Findings

1. No Readiness Gate on Service Worker Initialization (HIGH)

Files: src/background/service-worker.ts lines 18-37, 94

Storage services (IndexedDB and chrome.storage) are initialized with Promise.all(...) at the top level, but chrome.runtime.onMessage listener (line 94) can fire immediately upon service worker wake-up—before assetStorage and metadataStorage are ready. This causes intermittent Database not initialized errors when users interact with the extension right after browser startup or service worker idle recovery.

Fix: Create an initPromise that all message handlers await before proceeding:

const initPromise = Promise.all([assetStorage.init(), metadataStorage.init()]);
// In each handler:
await initPromise;

2. Singleton Selection State Race Condition (HIGH)

Files: src/background/service-worker.ts lines 162-164

Module-level mutable variables (pendingSelectionResolve, pendingSelectionReject, pendingSelectionFromPopup) track a single pending selection. If a second selection capture is initiated before the first completes (e.g., keyboard shortcut while popup-initiated selection is active), the new callbacks silently overwrite the first, leaving the first promise permanently pending. This causes the popup to hang indefinitely in the "Snapping..." state.

Fix: Before assigning new callbacks, reject any existing pending promise with a cancellation error. Alternatively, use an AbortController or request-ID correlation pattern.

3. NumbersApiManager Singleton Not Reset After Logout (MEDIUM)

Files: src/services/NumbersApiManager.ts lines 151-159

getNumbersApi() uses a lazy singleton that is never nullified. After logout, clearAuth() clears the token, but the same instance persists in the service worker's module cache. If a different user logs in, the UploadService still has its old upload queue and callbacks from the previous user's session, risking data leakage.

Fix: Add a resetInstance() export that sets instance = null and call it from clearAuth().

Impact

  • Users experience intermittent capture failures on browser startup
  • Selection capture can permanently hang the popup UI
  • Potential cross-user data leakage in shared device scenarios

Labels

security, priority:high</issue_description>

Comments on the Issue (you are @copilot in this section)


📍 Connect Copilot coding agent with Jira, Azure Boards or Linear to delegate work to Copilot in one click without leaving your project management tool.

… after logout

Co-authored-by: numbers-official <181934381+numbers-official@users.noreply.github.com>
Copilot AI changed the title [WIP] [Security] Fix service worker initialization race condition Fix service worker init race, selection capture deadlock, and post-logout singleton leak Mar 13, 2026
Copilot AI requested a review from numbers-official March 13, 2026 22:38
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.

[Security][High] Service worker state management: init race, singleton leak, and selection deadlock

2 participants