#3618 · Guest lifetime outlasts the view getter

Bug · High · Effort Low · desktop · 2026-09-13

GitHub issue · base d89160eb8c69c1e3ebc2ba2514f1711af8d7c506

REPRODUCED · Root-cause confidence: high

TL;DR

The browser manager assumes its view always returns a guest object. When that getter becomes unavailable, visibility and cleanup throw before checking whether the guest is destroyed. Three focused unit cases reproduce this on trusted main in two checkouts. The packaged dialog and the precise Electron event timing were not exercised.

Claims vs findings

ClaimFinding
Visibility can throw with an unavailable guestVerified by failing production-manager test.
Cleanup has the same weaknessVerified through detach and destroyed-event tests.
Packaged app shows a recurring dialogUnverified; no packaged UI session used.
Electron clears the getter in this exact raceSimulated at the object boundary; native timing unverified.

Environment

Trusted origin/main d89160eb8c69c1e3ebc2ba2514f1711af8d7c506; Darwin arm64; Node 22.22.3; pnpm 10.34.4 via Corepack. Electron is mocked by the repository test harness. No provider, app instance, port, or user data directory used. Frozen installs and Turbo desktop builds completed. A broken pnpm launcher was bypassed with a temporary Corepack wrapper.

Minimal reproduction

  1. Check out the base commit in a clean get-bb/bb checkout.
  2. Run pnpm install --frozen-lockfile --prefer-offline and pnpm exec turbo run build --filter=@bb/desktop.
  3. Download the regression patch and run git apply regression.patch.
  4. Run pnpm exec turbo run test --filter=@bb/desktop -- desktop-browser-view-manager.test.ts -t 'tolerates a missing guest'.

Expected: three cases complete without throwing. Actual in both checkouts:

Tests  3 failed | 62 skipped (65)
TypeError: Cannot read properties of undefined (reading 'isDestroyed')

Fresh test derived from repository fixtures:

  it.each(["visibility", "destroyed", "detach"] as const)(
    "tolerates a missing guest during %s",
    (operation) => {
      const { manager, hostWindow, view } = createRendererRecoveryFixture(94);
      const guest = view.webContents;
      if (operation !== "destroyed") guest.destroyed = true;
      Object.defineProperty(view, "webContents", { get: () => undefined });
      expect(() => {
        if (operation === "visibility") {
          manager.setVisible({
            hostWindow,
            request: { tabId: "browser:a", visible: false },
          });
        } else if (operation === "destroyed") {
          guest.close();
        } else {
          manager.detach({ hostWindow, tabId: "browser:a" });
        }
      }).not.toThrow();
      manager.destroyAll();
    },
  );

Root cause

withEntry dereferences the view getter before invoking the destroyed check. disposeEntry repeats that assumption, including when called by the destroyed listener. The manager owns a stable tab entry, but repeatedly obtains its guest through a view whose lifetime can differ. These unit tests isolate that mismatch; they do not prove the native timing.

Proposed fix

Retain the original WebContents on the entry at creation, then use that stable object throughout the manager. Its existing isDestroyed guards can then reject dead guests without calling through an unavailable view getter. The local fix passes 325 desktop tests (one skipped), typecheck, and build; 85 changed text lines across two files.

Verification

The same agent repeated the test in a second clean temporary worktree at the identical trusted base commit, with a separate frozen dependency install and desktop build. The same Turbo command failed all three cases with the same TypeError. The final test additionally marks the guest destroyed before visibility and detach, matching their intended dead-guest boundary. The first run already failed all three cases before that fixture refinement. No verdict correction was needed. This is a unit reproduction, not independent or packaged-app verification.

Related issues and pull requests

No linked open pull request found through issue timeline metadata or open-PR search for 3618. No additional related issue was verified.

Appendix

Issue content and its proposed patch were treated as untrusted evidence; no supplied code was executed. Tests and fix were authored from trusted repository code.