Skip to content

browser: stopping an attached session leaves its owned Chromium tab running #1107

Description

@McHersheys

Observed defect

With the unmodified browser/v0.2.12 Linux x86_64 release, browser::sessions::stop removes an attached session from browser::sessions::list but does not close the new tab that the session created (adopted: false). The page remains in the external Chromium CDP target list and its JavaScript continues running. Repeated attach/stop cycles accumulate live polling tabs.

Test environment: Linux, Chromium 145.0.7632.6, private CDP connection to a separately owned browser. No browser-worker source changes. In an integration rehearsal, eight logically stopped sessions left four active application tabs in each of two independent browsers. Exact owned-target closure through CDP removed the extra traffic; shutting down the two dedicated browser processes removed all remaining resources. No adopted user tab was closed.

Minimal reproduction

  1. Start a dedicated Chromium instance exposing CDP only to the test worker.
  2. Record its existing target IDs (e.g. the initial about:blank tab).
  3. Call browser::sessions::attach with that browser's cdp_url, an allowed test page URL and read_only: false. Verify the response says adopted: false and record the newly created target.
  4. Call browser::sessions::stop with the returned session ID.
  5. Verify browser::sessions::list is empty, then independently inspect the same browser's CDP target inventory.
  6. Observe that the owned page target is still present. A test page with a bounded interval counter/request makes continued execution visible. Repeat a few times to distinguish a transient delay from a leaked page.

Expected: a fresh tab owned by an attached session is closed; pre-existing/adopted tabs and the external browser remain untouched. Logical session deletion must not be the only cleanup assertion.

Likely source cause (not a tested patch)

The same ordering is present in release source deb6f98dbd3c7cbc226234844761430a47b911cf and current main 00417f34dcee7465d5d4602f4ab497db106f3e55:

  • browser/src/session.rs:668–675: shutdown aborts every task in self.tasks.
  • The CDP handler task is included in that collection during attachment.
  • browser/src/session.rs:692–697: only afterward, SessionKind::Attached { owns_page: true } awaits self.page.clone().close(). Failure is logged at debug level and not surfaced as an unsuccessful cleanup.

This appears to stop the protocol driver before asking it to close its owned target. Launched-browser shutdown has a similar protocol-ordering concern but was not qualified by this attach-mode reproduction.

Suggested fix/acceptance boundary

Keep the protocol handler alive until bounded page/browser teardown completes, then stop and await owned background tasks. Preserve the distinction between fresh owned tabs, adopted tabs and launched browsers; never close someone else's tab as a fallback. Add real-CDP regression tests asserting both logical session removal and physical target/process cleanup, including an unresponsive-browser timeout case. Report cleanup failure rather than treating a removed session-map entry as proof that the page stopped.

Activity

  1. McHersheys commented on Sep 6, 2026

    @McHersheys
    Author

    Bounded follow-up: supported launched sessions can provide per-task physical teardown

    This does not close the attached-owned-tab bug reported above. I tested the alternative supported lifetime (browser::sessions::start with an ephemeral profile, then browser::sessions::stop) against the unchanged official browser 0.2.12. No worker patch, UI audit, live site, credentials, or manual tab cleanup was used.

    Fixture and evidence

    • Browser source deb6f98dbd3c7cbc226234844761430a47b911cf; binary SHA-256 90dc8a3dd2f212d4eb9971d67b5426064ce4bc7881c0bb5d3f131b47b5282219.
    • Chromium: Google Chrome for Testing 145.0.7632.6, executable SHA-256 481fea1516a1f2b76454664272f12cd9dd1f20117b21e1f1498e08bc7f872c00.
    • Playwright image digest sha256:65cefd09a5e943921ecd3a6e5414c603db2eb161e9eb48f2e2ccc63486dc7dc0, bounded networkless runsc, 2 CPU / 3 GiB / 512 PIDs, read-only root, disposable tmpfs, non-root.
    • Real Engine and its configuration owner; browser's documented --config seed. allow_attach=false, empty user_data_dir, two launched read-only sessions; each local sentinel page makes a counted HTTP request every 60 ms to an in-container loopback server. A sealed exec wrapper adds Chromium's container flags; it does not implement cleanup.
    • Standard Docker --init with PID1 /sbin/docker-init, SHA-256 cc5aa4571d1ec88f939a98b2242dfd8d7e28a51f0840886c11e65441e25cd460.

    13/13 identity/lifetime assertions passed in a 9.77 s fixture. Specifically:

    1. Two sessions owned separate Chromium processes and ephemeral profiles.
    2. sessions::stop(b1) returned {ok:true, was_running:true} in 269 ms.
    3. Before any fixture shutdown, b1's polling stayed at 69 requests while b2 continued from 21 to 28.
    4. b1's ephemeral profile was physically absent and every captured PID + process-start identity was absent. This was not merely removal from sessions::list.
    5. sessions::list contained only b2; b2's process remained alive.
    6. Repeating supported stop on b1 returned {ok:true, was_running:false}.
    7. Send the browser worker its supported SIGTERM: it exited code 0, b2 polling stopped, b2 profile disappeared, and all captured Chromium identities were absent. The other Engine/fixture processes were still alive during these assertions.
    8. A final process-table check found no Chromium descendants, before container disposal.

    The container and sealed source were then removed by exact identity as fixture housekeeping. Their removal did not substitute for the native lifetime assertions.

    Important envelope control

    Without a PID1 reaper, a preceding run stopped polling and deleted the profile but left terminated Chromium descendants as zombies reparented to PID1. They were not still executing browser activity, but physical PID-absence failed. Adding the documented --init reaper fixed that inspection/runtime envelope; the browser binary was unchanged. This distinguishes process reaping from the original still-active attached-tab leak. See Docker's PID1 guidance.

    Two earlier setup attempts failed before launching any page: the configuration seed was not selected, then the launcher was placed on a noexec tmpfs. Neither is lifecycle evidence. The successful fixture uses the real configuration owner and a sealed read-only executable launcher.

    Scope and upstream regression proposal

    This qualifies launched, per-task sessions with an actual reaper, supported stop, and worker SIGTERM in the tested envelope. It does not qualify adoption/attachment, idle sweeping, worker SIGKILL, host failure, Cardflow integration, or an immutable deployment. Our deployment/autonomy gates remain held for the separate Harness issue.

    For the original attached-tab branch, please retain regression cases that assert physical CDP target absence AND cessation of page activity after stop; a logically empty session table is insufficient. An adopted caller-owned tab must remain untouched. Source still aborts event/driver tasks before attempting page close; a candidate fix should keep the CDP driver alive through the owned-target close, bound that operation, and report failure rather than silently claiming full teardown. That is a source-level repair proposal, not a tested patch or a downstream fork.

    For our integration, the next candidate can use the supported launched-per-task lifetime instead of shared attached browsers, retaining exact task ownership and process reaping. No permanent packaging or live cutover has been performed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions