#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
| Claim | Finding |
|---|---|
| Visibility can throw with an unavailable guest | Verified by failing production-manager test. |
| Cleanup has the same weakness | Verified through detach and destroyed-event tests. |
| Packaged app shows a recurring dialog | Unverified; no packaged UI session used. |
| Electron clears the getter in this exact race | Simulated 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
- Check out the base commit in a clean get-bb/bb checkout.
- Run
pnpm install --frozen-lockfile --prefer-offlineandpnpm exec turbo run build --filter=@bb/desktop. - Download the regression patch and run
git apply regression.patch. - 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.