[GH-ISSUE #4921] [Bug] Hopper (Linux): session teardown leaves a stale /tmp/.X<n>-lock; accumulated locks can make startup fail with address_collision #3054

Closed
opened 2026-10-31 00:35:35 +00:00 by bsakaguchi · 3 comments
Owner

Originally created by @Abel-I-Akhigbe on GitHub (Oct 17, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/4921

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Native analysis or decompilation

Observed behavior

On Linux with the Hopper demo provider (private Xvfb, user-mount-namespace strategy), Hopper sessions run over MCP leave /tmp/.X<n>-lock files behind after they end. Each file is owned by the invoking user and records a PID that is no longer running. No Xvfb process remains. Teardown looked clean wherever we recorded it:

  • close_binary returned without error ({"result":null}).
  • The sessions ended by stdin EOF exited with code 0 and left no surviving processes.
  • All three native verification sessions in the batches (2 in A, 1 in B), whose server logs were kept, logged owned-shutdown with status verified-cleanup.

Counts were taken per batch. Before each batch, all lock files had been removed. After each batch, the number of new lock files equalled the number of Hopper sessions in it:

  • Batch A, 4 sessions → 4 new lock files: 2 × the native verification flow over MCP (requests under Reproduction), 1 × a session ended with close_binary, 1 × a session ended by closing the client's stdin without close_binary.
  • Batch B, 3 sessions → 3 new lock files: 1 × the native verification flow, 1 × close_binary, 1 × stdin EOF.

Two of the seven locks were also traced to their sessions. In each batch, the close_binary session's process tree was recorded while it was open, and it included an Xvfb PID. One of the batch's leftover locks names that PID: 1198147 → /tmp/.X535-lock in batch A, and 1353852 → /tmp/.X626-lock in batch B. The other five locks were counted per batch only.

Before these batches, 8 lock files with dead PIDs were found. Their modification times were 15:29–18:40 the same day, and they were not mapped to specific sessions.

Control: running the helper scripts/hopper-demo-x11.py directly in --probe mode, with the same user-mount-namespace strategy, left 0 new lock files.

Reproduction

  1. Linux x86_64 with the Hopper 6.4.2 demo, Xvfb and xauth installed. REA selected the user-mount-namespace strategy; here /tmp/.X11-unix is read-only (WSLg).

  2. Build the fixture with gcc -g -O0 -fno-pie -no-pie probe.c -o probe:

    __attribute__((noinline)) int rea_provider_probe(int value) {
        if (value > 10) return value * 3 + 7;
        return value - 4;
    }
    int main(int argc, char **argv) { (void)argv; return rea_provider_probe(argc); }
    
  3. Run ls /tmp/.X*-lock and note the result.

  4. Start rea mcp and send these tools/call requests in order:

    • open_binary {"path": "<abs path>/probe", "provider_id": "hopper"}
    • procedure_pseudo_code {"procedure": "rea_provider_probe"}
    • close_binary {}

    To tie the lock to this session, run pgrep -a Xvfb before close_binary and note the PID.

  5. Run ls /tmp/.X*-lock again and compare. Frequency in our runs: 7 sessions produced 7 new locks across 2 batches; 2 locks were individually matched to their sessions by Xvfb PID, while the other 5 were counted only at batch level (see Observed behavior). For a new file, cat it to get its PID: /proc/<pid> does not exist, and pgrep Xvfb finds nothing.

Both batches also included a session ended by closing the client's stdin instead of calling close_binary, and the counts still matched.

Expected behavior

When a Hopper session ends, its Xvfb lock file is removed. Reference: the helper's own shutdown, stop_process, sends Xvfb SIGTERM and waits. The --probe control ends through that path, per the source, and left no lock (observed).

Proposed fallback, not a documented contract: a lock whose PID no longer exists should not stop a later session from using that display number.

Impact and scope

Reliability, not confidentiality: the lock files contain only a PID.

  • Evidence integrity: no effect seen. The verification-flow sessions returned pseudocode for rea_provider_probe, and their server exited 0.
  • Process isolation: no surviving process was found. Only the lock file remains.
  • Recovery: affected. REA does not reclaim the locks, and they persist until /tmp is cleaned (distribution policy or a reboot).

Potential impact, derived from the source and not observed (details under Analysis): each leftover lock blocks one of the 900 display numbers Hopper picks from. Startup gives up after 3 blocked picks, so it fails more often as locks accumulate. Under the stated assumptions, that is about 3.7% at 300 locks and 30% at 600. Long-lived hosts that run many Hopper sessions would degrade gradually.

REA version

6.5.0 (installed); source analysis at main f4322b15; the cited code is unchanged at main d0253c99 (2026-10-17T23:06Z)

Runtime and platform

Node.js 26.12.0 (also seen on 26.5.1); Ubuntu 24.04 on WSL2, kernel 6.18, x86_64; Hopper 6.4.2 demo; Xvfb 2:21.1.12-1ubuntu1.5 and -1ubuntu1.8

Target and artifact context

Small x86_64 ELF built from the two-function fixture under Reproduction; only this fixture was tested

Interface

MCP

Earliest failure stage

Cleanup, cancellation, or lifecycle

Relevant output or evidence

Command or MCP request actually run: rea mcp, with the three tools/call requests under Reproduction. Batch B had 3 sessions: one native verification flow, one ended with close_binary, and one ended by closing stdin. No lock files existed beforehand.

Relevant result, error, or artifact:

  1. The close_binary session's process tree, recorded by an audit process inspector while the session was open. It supports the PID match for /tmp/.X626-lock (excerpt):
    "1353852:/usr/bin/Xvfb"
    
  2. Output of an audit cleanup check run after the batch, with no Xvfb running. It reads each lock's PID, tests /proc/<pid>, and removes the lock only if the PID is dead. It supports "3 sessions → 3 stale locks":
    removed /tmp/.X514-lock owner=<user> pid=1353270 dead
    removed /tmp/.X626-lock owner=<user> pid=1353852 dead
    removed /tmp/.X761-lock owner=<user> pid=1354473 dead
    
  3. A server stderr line captured by the verification script at the end of the batch's native verification session, unmodified. It supports "teardown reported verified-cleanup":
    {"level":30,"time":1792240620419,"application":"rea","mode":"mcp","layer":"bridge","type":"owned-shutdown","status":"verified-cleanup","launcher_pid":1353267,"process_group_id":1353267,"cleanup_signaled":true,"reason":null,"msg":"Owned Hopper launcher shutdown completed"}
    
  4. Control: the helper run directly with --probe, in the same user-mount-namespace strategy, left 0 new lock files.

For source-only findings, link the file/revision and relevant code: the commit-pinned links under Analysis or suspected cause. They point to f4322b15. The cited code is unchanged at d0253c99: BridgeLauncher.ts, ProcessOwnership.ts and hopper-demo-x11.py are byte-identical, and in ProviderProcess.ts only the line numbers of the cited code moved (+26). See the re-check below.

Missing evidence or remaining uncertainty:

  • No signal trace was captured.
  • 5 of the 7 locks are matched only at batch level.
  • The startup-failure rates are derived from the source, not observed.
  • Nothing was run on a main build; the source was only compared.

Typed outcome or recovery expected

verified-cleanup should mean that nothing the session created is left. The session's display lock should be gone before teardown reports verified-cleanup. If it is not, teardown should report the leftover lock as a cleanup residual. A later Hopper open_binary should not fail with address_collision because of locks whose PID no longer exists.

Analysis or suspected cause

Source-derived; no signal trace was captured. Links and line numbers are pinned to main f4322b15; the 6.5.0 package has the same code.

  • src/hopper/BridgeLauncher.ts:306-314: the Hopper launch sets cleanup = () => cleanupOwnedProcessGroup(started.ownership).
  • src/process/ProviderProcess.ts:581-605: when launch.cleanup is set, stopping runs that callback. The SIGTERM → grace → SIGKILL path at lines 610-617 is not used.
  • src/process/ProcessOwnership.ts:276: cleanupOwnedProcessGroup sends SIGKILL to each owned process group, with no SIGTERM first.
  • scripts/hopper-demo-x11.py installs no signal handler. It starts Xvfb with a plain subprocess.Popen, with no new session or process group. Xvfb is stopped only by stop_process (lines 462-470), which sends SIGTERM and then waits. stop_process is called from the helper's own exit paths: run_launch's finally (lines 807-812) and run_probe (lines 647-655). A process-group SIGKILL never runs those paths.

Suspected cause: teardown SIGKILLs the helper's process group, Xvfb included. Xvfb never runs its orderly shutdown, so its lock file stays. In the --probe control, the helper stops Xvfb with SIGTERM, and no lock is left.

Display selection and the failure estimate, also source-derived:

  • scripts/hopper-demo-x11.py:576-644 holds start_xvfb, which is used when no display number is given. It tries at most 3 random display numbers in 100–999 (100 + secrets.randbelow(900), line 583).
  • It treats an existing /tmp/.X<n>-lock or /tmp/.X11-unix/X<n> as a collision (lines 585-595).
  • An Xvfb start that fails with address_collision also uses up a try (lines 637-639). After 3 tries it raises address_collision (line 640).

Assume L distinct blocked display numbers in 100–999, 3 independent uniform picks with replacement, and no other failures or concurrent sessions. Startup then fails with probability (L/900)³:

Stale locks (L) Startup failure probability
100 0.14%
300 3.7%
600 29.6%
800 70.2%

Re-check at main 7e4eed9d (2026-10-17T22:33Z), 53 commits later:

  • BridgeLauncher.ts, ProcessOwnership.ts and hopper-demo-x11.py are byte-identical to f4322b15.
  • In ProviderProcess.ts, the cleanup branch and the SIGTERM path are textually identical but moved to lines 607-631 and 636-643. The other changes in that file concern output-close accounting.
  • None of the four files changed in the next 2 commits, up to main d0253c99 (2026-10-17T23:06Z).

Possible fixes (untested suggestions):

  1. In the Hopper cleanup, ask the helper to stop before the process-group SIGKILL. One way: SIGTERM to the group with a short grace period, plus a SIGTERM handler in the helper that calls stop_process on Xvfb.
  2. After the group is killed, remove the /tmp/.X<n>-lock for the display the session owned, but only if the lock still names that session's Xvfb PID.
  3. In start_xvfb, treat a lock owned by the current user whose recorded PID no longer exists as reclaimable, not as a collision.

Additional context

Related but distinct: #3985 (a timed-out probe survives close), #4227 (lifecycle contract audit), #3436 (xauth argv).

No duplicate was found in the recorded searches (gh search issues, all states, on 2026-10-17 and again at 2026-10-17T22:49Z). Queries: "X lock", Xvfb lock, stale lock, address_collision, display collision, X11 lock, hopper Xvfb, hopper cleanup SIGKILL, .X11-unix, Xvfb OR X11 OR lock OR display in:title,body.

Workaround: remove stale locks that you own and whose PID is not running. Checking and deleting are separate steps, so an X server starting on the same display in between could lose its new lock. Run this only while no Hopper session or other X server is starting.

for f in /tmp/.X*-lock; do
  [ -f "$f" ] && [ ! -L "$f" ] && [ -O "$f" ] || continue   # your own regular files only
  read -r p < "$f" || continue                              # first line, surrounding blanks trimmed
  case "$p" in ''|*[!0-9]*) continue ;; esac                 # skip empty/malformed PIDs
  [ -d "/proc/$p" ] || rm -f -- "$f"                         # remove only if no such process
done

This snippet was tested on fake lock files in a private scratch directory, not on a live /tmp.

Under dash, 9 lock files:

  • a dead PID in Xvfb's %10d format, and unpadded: removed;
  • a live PID: kept;
  • an empty file: kept;
  • malformed contents (abc, -5, 123 456): kept;
  • a symlink: kept, and its target untouched;
  • a directory: kept.

Under bash, 3 lock files:

  • a dead PID: removed;
  • a live PID: kept;
  • abc: kept.

Also under bash, a directory with no lock files: nothing removed, exit status 0.

A lock owned by another user was not tested.

Proposed regression test: run one Hopper session through MCP and end it with close_binary, then again by closing stdin. In each case, assert that no /tmp/.X<n>-lock remains for the display the session used.

Publication re-check (2026-10-17T23:49:59+00:00): the four source files above and the bug-report form are unchanged between d0253c99 and upstream main d205c528. Fresh all-state issue searches for Xvfb/lock, stale/lock/Hopper, address_collision, X11/lock, and session-teardown/Hopper found no duplicate. This was a source and issue-search check; no newer build was executed.

Originally created by @Abel-I-Akhigbe on GitHub (Oct 17, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/4921 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Native analysis or decompilation ### Observed behavior On Linux with the Hopper demo provider (private Xvfb, `user-mount-namespace` strategy), Hopper sessions run over MCP leave `/tmp/.X<n>-lock` files behind after they end. Each file is owned by the invoking user and records a PID that is no longer running. No Xvfb process remains. Teardown looked clean wherever we recorded it: - `close_binary` returned without error (`{"result":null}`). - The sessions ended by stdin EOF exited with code 0 and left no surviving processes. - All three native verification sessions in the batches (2 in A, 1 in B), whose server logs were kept, logged `owned-shutdown` with status `verified-cleanup`. Counts were taken per batch. Before each batch, all lock files had been removed. After each batch, the number of new lock files equalled the number of Hopper sessions in it: - Batch A, 4 sessions → 4 new lock files: 2 × the native verification flow over MCP (requests under Reproduction), 1 × a session ended with `close_binary`, 1 × a session ended by closing the client's stdin without `close_binary`. - Batch B, 3 sessions → 3 new lock files: 1 × the native verification flow, 1 × `close_binary`, 1 × stdin EOF. Two of the seven locks were also traced to their sessions. In each batch, the `close_binary` session's process tree was recorded while it was open, and it included an Xvfb PID. One of the batch's leftover locks names that PID: 1198147 → `/tmp/.X535-lock` in batch A, and 1353852 → `/tmp/.X626-lock` in batch B. The other five locks were counted per batch only. Before these batches, 8 lock files with dead PIDs were found. Their modification times were 15:29–18:40 the same day, and they were not mapped to specific sessions. Control: running the helper `scripts/hopper-demo-x11.py` directly in `--probe` mode, with the same `user-mount-namespace` strategy, left 0 new lock files. ### Reproduction 1. Linux x86_64 with the Hopper 6.4.2 demo, Xvfb and xauth installed. REA selected the `user-mount-namespace` strategy; here `/tmp/.X11-unix` is read-only (WSLg). 2. Build the fixture with `gcc -g -O0 -fno-pie -no-pie probe.c -o probe`: ```c __attribute__((noinline)) int rea_provider_probe(int value) { if (value > 10) return value * 3 + 7; return value - 4; } int main(int argc, char **argv) { (void)argv; return rea_provider_probe(argc); } ``` 3. Run `ls /tmp/.X*-lock` and note the result. 4. Start `rea mcp` and send these `tools/call` requests in order: - `open_binary` `{"path": "<abs path>/probe", "provider_id": "hopper"}` - `procedure_pseudo_code` `{"procedure": "rea_provider_probe"}` - `close_binary` `{}` To tie the lock to this session, run `pgrep -a Xvfb` before `close_binary` and note the PID. 5. Run `ls /tmp/.X*-lock` again and compare. Frequency in our runs: 7 sessions produced 7 new locks across 2 batches; 2 locks were individually matched to their sessions by Xvfb PID, while the other 5 were counted only at batch level (see Observed behavior). For a new file, `cat` it to get its PID: `/proc/<pid>` does not exist, and `pgrep Xvfb` finds nothing. Both batches also included a session ended by closing the client's stdin instead of calling `close_binary`, and the counts still matched. ### Expected behavior When a Hopper session ends, its Xvfb lock file is removed. Reference: the helper's own shutdown, `stop_process`, sends Xvfb SIGTERM and waits. The `--probe` control ends through that path, per the source, and left no lock (observed). Proposed fallback, not a documented contract: a lock whose PID no longer exists should not stop a later session from using that display number. ### Impact and scope Reliability, not confidentiality: the lock files contain only a PID. - Evidence integrity: no effect seen. The verification-flow sessions returned pseudocode for `rea_provider_probe`, and their server exited 0. - Process isolation: no surviving process was found. Only the lock file remains. - Recovery: affected. REA does not reclaim the locks, and they persist until `/tmp` is cleaned (distribution policy or a reboot). Potential impact, derived from the source and not observed (details under Analysis): each leftover lock blocks one of the 900 display numbers Hopper picks from. Startup gives up after 3 blocked picks, so it fails more often as locks accumulate. Under the stated assumptions, that is about 3.7% at 300 locks and 30% at 600. Long-lived hosts that run many Hopper sessions would degrade gradually. ### REA version 6.5.0 (installed); source analysis at main f4322b15; the cited code is unchanged at main d0253c99 (2026-10-17T23:06Z) ### Runtime and platform Node.js 26.12.0 (also seen on 26.5.1); Ubuntu 24.04 on WSL2, kernel 6.18, x86_64; Hopper 6.4.2 demo; Xvfb 2:21.1.12-1ubuntu1.5 and -1ubuntu1.8 ### Target and artifact context Small x86_64 ELF built from the two-function fixture under Reproduction; only this fixture was tested ### Interface MCP ### Earliest failure stage Cleanup, cancellation, or lifecycle ### Relevant output or evidence **Command or MCP request actually run:** `rea mcp`, with the three `tools/call` requests under Reproduction. Batch B had 3 sessions: one native verification flow, one ended with `close_binary`, and one ended by closing stdin. No lock files existed beforehand. **Relevant result, error, or artifact:** 1. The `close_binary` session's process tree, recorded by an audit process inspector while the session was open. It supports the PID match for `/tmp/.X626-lock` (excerpt): ```text "1353852:/usr/bin/Xvfb" ``` 2. Output of an audit cleanup check run after the batch, with no Xvfb running. It reads each lock's PID, tests `/proc/<pid>`, and removes the lock only if the PID is dead. It supports "3 sessions → 3 stale locks": ```text removed /tmp/.X514-lock owner=<user> pid=1353270 dead removed /tmp/.X626-lock owner=<user> pid=1353852 dead removed /tmp/.X761-lock owner=<user> pid=1354473 dead ``` 3. A server stderr line captured by the verification script at the end of the batch's native verification session, unmodified. It supports "teardown reported `verified-cleanup`": ```text {"level":30,"time":1792240620419,"application":"rea","mode":"mcp","layer":"bridge","type":"owned-shutdown","status":"verified-cleanup","launcher_pid":1353267,"process_group_id":1353267,"cleanup_signaled":true,"reason":null,"msg":"Owned Hopper launcher shutdown completed"} ``` 4. Control: the helper run directly with `--probe`, in the same `user-mount-namespace` strategy, left 0 new lock files. **For source-only findings, link the file/revision and relevant code:** the commit-pinned links under Analysis or suspected cause. They point to [`f4322b15`](https://github.com/morluto/rea/tree/f4322b15ed1423d736bd2572cf7a8eadf2b466e1). The cited code is unchanged at [`d0253c99`](https://github.com/morluto/rea/tree/d0253c99d65afbbd12732a2fdaf711640574e17f): `BridgeLauncher.ts`, `ProcessOwnership.ts` and `hopper-demo-x11.py` are byte-identical, and in `ProviderProcess.ts` only the line numbers of the cited code moved (+26). See the re-check below. **Missing evidence or remaining uncertainty:** - No signal trace was captured. - 5 of the 7 locks are matched only at batch level. - The startup-failure rates are derived from the source, not observed. - Nothing was run on a `main` build; the source was only compared. ### Typed outcome or recovery expected `verified-cleanup` should mean that nothing the session created is left. The session's display lock should be gone before teardown reports `verified-cleanup`. If it is not, teardown should report the leftover lock as a cleanup residual. A later Hopper `open_binary` should not fail with `address_collision` because of locks whose PID no longer exists. ### Analysis or suspected cause Source-derived; no signal trace was captured. Links and line numbers are pinned to main `f4322b15`; the 6.5.0 package has the same code. - [`src/hopper/BridgeLauncher.ts:306-314`](https://github.com/morluto/rea/blob/f4322b15ed1423d736bd2572cf7a8eadf2b466e1/src/hopper/BridgeLauncher.ts#L306-L314): the Hopper launch sets `cleanup = () => cleanupOwnedProcessGroup(started.ownership)`. - [`src/process/ProviderProcess.ts:581-605`](https://github.com/morluto/rea/blob/f4322b15ed1423d736bd2572cf7a8eadf2b466e1/src/process/ProviderProcess.ts#L581-L605): when `launch.cleanup` is set, stopping runs that callback. The SIGTERM → grace → SIGKILL path at [lines 610-617](https://github.com/morluto/rea/blob/f4322b15ed1423d736bd2572cf7a8eadf2b466e1/src/process/ProviderProcess.ts#L610-L617) is not used. - [`src/process/ProcessOwnership.ts:276`](https://github.com/morluto/rea/blob/f4322b15ed1423d736bd2572cf7a8eadf2b466e1/src/process/ProcessOwnership.ts#L276): `cleanupOwnedProcessGroup` sends `SIGKILL` to each owned process group, with no SIGTERM first. - `scripts/hopper-demo-x11.py` installs no signal handler. It starts Xvfb with a plain `subprocess.Popen`, with no new session or process group. Xvfb is stopped only by `stop_process` ([lines 462-470](https://github.com/morluto/rea/blob/f4322b15ed1423d736bd2572cf7a8eadf2b466e1/scripts/hopper-demo-x11.py#L462-L470)), which sends SIGTERM and then waits. `stop_process` is called from the helper's own exit paths: `run_launch`'s `finally` ([lines 807-812](https://github.com/morluto/rea/blob/f4322b15ed1423d736bd2572cf7a8eadf2b466e1/scripts/hopper-demo-x11.py#L807-L812)) and `run_probe` ([lines 647-655](https://github.com/morluto/rea/blob/f4322b15ed1423d736bd2572cf7a8eadf2b466e1/scripts/hopper-demo-x11.py#L647-L655)). A process-group SIGKILL never runs those paths. Suspected cause: teardown SIGKILLs the helper's process group, Xvfb included. Xvfb never runs its orderly shutdown, so its lock file stays. In the `--probe` control, the helper stops Xvfb with SIGTERM, and no lock is left. Display selection and the failure estimate, also source-derived: - [`scripts/hopper-demo-x11.py:576-644`](https://github.com/morluto/rea/blob/f4322b15ed1423d736bd2572cf7a8eadf2b466e1/scripts/hopper-demo-x11.py#L576-L644) holds `start_xvfb`, which is used when no display number is given. It tries at most 3 random display numbers in 100–999 (`100 + secrets.randbelow(900)`, [line 583](https://github.com/morluto/rea/blob/f4322b15ed1423d736bd2572cf7a8eadf2b466e1/scripts/hopper-demo-x11.py#L583)). - It treats an existing `/tmp/.X<n>-lock` or `/tmp/.X11-unix/X<n>` as a collision ([lines 585-595](https://github.com/morluto/rea/blob/f4322b15ed1423d736bd2572cf7a8eadf2b466e1/scripts/hopper-demo-x11.py#L585-L595)). - An Xvfb start that fails with `address_collision` also uses up a try ([lines 637-639](https://github.com/morluto/rea/blob/f4322b15ed1423d736bd2572cf7a8eadf2b466e1/scripts/hopper-demo-x11.py#L637-L639)). After 3 tries it raises `address_collision` ([line 640](https://github.com/morluto/rea/blob/f4322b15ed1423d736bd2572cf7a8eadf2b466e1/scripts/hopper-demo-x11.py#L640)). Assume L distinct blocked display numbers in 100–999, 3 independent uniform picks with replacement, and no other failures or concurrent sessions. Startup then fails with probability (L/900)³: | Stale locks (L) | Startup failure probability | |---|---| | 100 | 0.14% | | 300 | 3.7% | | 600 | 29.6% | | 800 | 70.2% | Re-check at main `7e4eed9d` (2026-10-17T22:33Z), 53 commits later: - `BridgeLauncher.ts`, `ProcessOwnership.ts` and `hopper-demo-x11.py` are byte-identical to `f4322b15`. - In `ProviderProcess.ts`, the cleanup branch and the SIGTERM path are textually identical but moved to lines 607-631 and 636-643. The other changes in that file concern output-close accounting. - None of the four files changed in the next 2 commits, up to main `d0253c99` (2026-10-17T23:06Z). Possible fixes (untested suggestions): 1. In the Hopper cleanup, ask the helper to stop before the process-group SIGKILL. One way: SIGTERM to the group with a short grace period, plus a SIGTERM handler in the helper that calls `stop_process` on Xvfb. 2. After the group is killed, remove the `/tmp/.X<n>-lock` for the display the session owned, but only if the lock still names that session's Xvfb PID. 3. In `start_xvfb`, treat a lock owned by the current user whose recorded PID no longer exists as reclaimable, not as a collision. ### Additional context Related but distinct: #3985 (a timed-out probe survives close), #4227 (lifecycle contract audit), #3436 (xauth argv). No duplicate was found in the recorded searches (`gh search issues`, all states, on 2026-10-17 and again at 2026-10-17T22:49Z). Queries: `"X lock"`, `Xvfb lock`, `stale lock`, `address_collision`, `display collision`, `X11 lock`, `hopper Xvfb`, `hopper cleanup SIGKILL`, `.X11-unix`, `Xvfb OR X11 OR lock OR display in:title,body`. Workaround: remove stale locks that you own and whose PID is not running. Checking and deleting are separate steps, so an X server starting on the same display in between could lose its new lock. Run this only while no Hopper session or other X server is starting. ```sh for f in /tmp/.X*-lock; do [ -f "$f" ] && [ ! -L "$f" ] && [ -O "$f" ] || continue # your own regular files only read -r p < "$f" || continue # first line, surrounding blanks trimmed case "$p" in ''|*[!0-9]*) continue ;; esac # skip empty/malformed PIDs [ -d "/proc/$p" ] || rm -f -- "$f" # remove only if no such process done ``` This snippet was tested on fake lock files in a private scratch directory, not on a live `/tmp`. Under dash, 9 lock files: - a dead PID in Xvfb's `%10d` format, and unpadded: removed; - a live PID: kept; - an empty file: kept; - malformed contents (`abc`, `-5`, `123 456`): kept; - a symlink: kept, and its target untouched; - a directory: kept. Under bash, 3 lock files: - a dead PID: removed; - a live PID: kept; - `abc`: kept. Also under bash, a directory with no lock files: nothing removed, exit status 0. A lock owned by another user was not tested. Proposed regression test: run one Hopper session through MCP and end it with `close_binary`, then again by closing stdin. In each case, assert that no `/tmp/.X<n>-lock` remains for the display the session used. Publication re-check (2026-10-17T23:49:59+00:00): the four source files above and the bug-report form are unchanged between `d0253c99` and upstream main [`d205c528`](https://github.com/morluto/rea/tree/d205c528e31897f1f2919b80a4245356b502c703). Fresh all-state issue searches for Xvfb/lock, stale/lock/Hopper, `address_collision`, X11/lock, and session-teardown/Hopper found no duplicate. This was a source and issue-search check; no newer build was executed.
Author
Owner

@XinwangTao commented on GitHub (Oct 18, 2026):

Thanks for the detailed report — the per-session lock matching (PID → /tmp/.X-lock) makes the root cause fairly clear: the X server lock file is left behind even though the Xvfb process itself is gone and teardown reports verified-cleanup. Since the lock records a dead PID, the cleanup verification is probably only checking the process tree, not the lock artifact itself.

Suggestions:

  1. In the Hopper teardown path (BridgeLauncher/ProcessOwnership), after confirming the Xvfb process exited, explicitly remove /tmp/.X-lock for that display, or verify it no longer exists before reporting verified-cleanup.
  2. Track the display number used by each session so teardown removes exactly that lock instead of scanning all /tmp/.X*-lock files.
  3. As defense-in-depth, startup could detect stale locks (dead PID, owned by user) and clear them before allocating a display, given the helper already documents the safe workaround.

The proposed regression test (no /tmp/.X-lock remains after close_binary and after stdin EOF) looks reasonable — would also recommend asserting the lock count is unchanged after a full batch, not just a single session.

<!-- gh-comment-id:6244195314 --> @XinwangTao commented on GitHub (Oct 18, 2026): Thanks for the detailed report — the per-session lock matching (PID → /tmp/.X<n>-lock) makes the root cause fairly clear: the X server lock file is left behind even though the Xvfb process itself is gone and teardown reports verified-cleanup. Since the lock records a dead PID, the cleanup verification is probably only checking the process tree, not the lock artifact itself. Suggestions: 1. In the Hopper teardown path (BridgeLauncher/ProcessOwnership), after confirming the Xvfb process exited, explicitly remove /tmp/.X<n>-lock for that display, or verify it no longer exists before reporting verified-cleanup. 2. Track the display number used by each session so teardown removes exactly that lock instead of scanning all /tmp/.X*-lock files. 3. As defense-in-depth, startup could detect stale locks (dead PID, owned by user) and clear them before allocating a display, given the helper already documents the safe workaround. The proposed regression test (no /tmp/.X<n>-lock remains after close_binary and after stdin EOF) looks reasonable — would also recommend asserting the lock count is unchanged after a full batch, not just a single session.
Author
Owner

@matbrgz commented on GitHub (Oct 19, 2026):

Cross-referencing with sibling Hopper session teardown reports in Cluster AB: #4226 ([Bug] Hopper pre-handshake startup failure causes repeated cleanup errors while session reports idle) and recent fix #5189. Both isolate virtual X11 server lifecycle management on Linux where orphaned lock files (/tmp/.X<n>-lock) persist across sessions and cause address_collision on subsequent startups.

Consolidated under Cluster AB (Hopper Linux Virtual X11 Display & Stale Lock Cleanup Failures) in tracker #4330.

<!-- gh-comment-id:6271435683 --> @matbrgz commented on GitHub (Oct 19, 2026): Cross-referencing with sibling Hopper session teardown reports in **Cluster AB**: #4226 (`[Bug] Hopper pre-handshake startup failure causes repeated cleanup errors while session reports idle`) and recent fix #5189. Both isolate virtual X11 server lifecycle management on Linux where orphaned lock files (`/tmp/.X<n>-lock`) persist across sessions and cause `address_collision` on subsequent startups. Consolidated under **Cluster AB (Hopper Linux Virtual X11 Display & Stale Lock Cleanup Failures)** in tracker #4330.
Author
Owner

@XinwangTao commented on GitHub (Oct 20, 2026):

Agree with the consolidation — #4226 and #5189 share the same Linux X11 lifecycle root, and grouping them under Cluster AB in #4330 keeps the stale-lock pattern visible. The #4966 fix (orderly demo-helper shutdown so the X lock is removed) covers the teardown half of the report; the defense-in-depth half from my earlier comment — startup reclaiming locks whose recorded PID is dead — would still be worth a follow-up so a single crashed session can't wedge later startups.

<!-- gh-comment-id:6281016970 --> @XinwangTao commented on GitHub (Oct 20, 2026): Agree with the consolidation — #4226 and #5189 share the same Linux X11 lifecycle root, and grouping them under Cluster AB in #4330 keeps the stale-lock pattern visible. The #4966 fix (orderly demo-helper shutdown so the X lock is removed) covers the teardown half of the report; the defense-in-depth half from my earlier comment — startup reclaiming locks whose recorded PID is dead — would still be worth a follow-up so a single crashed session can't wedge later startups.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#3054
No description provided.