Skip to content

[rb] Lock the starting port in a file rather than on the port below it - #17893

Open
ikraamg wants to merge 5 commits into
SeleniumHQ:trunkfrom
ikraamg:fix/port-lock-without-a-neighbouring-port
Open

[rb] Lock the starting port in a file rather than on the port below it#17893
ikraamg wants to merge 5 commits into
SeleniumHQ:trunkfrom
ikraamg:fix/port-lock-without-a-neighbouring-port

Conversation

@ikraamg

@ikraamg ikraamg commented Aug 9, 2026

Copy link
Copy Markdown

🔗 Related Issues

Picks up #10176, which was closed with "if anyone wants to PR a better solution, we can do that".

💥 What does this PR do?

ServiceManager takes its startup lock by binding port - 1, so starting a driver on port N needs N-1 to be free as well. When an unrelated service already listens there the lock can never be taken: startup spins for 45 seconds and then raises, even though the requested port is free. This is easy to hit once you pick driver ports yourself, for example a pool assigning a port per process that lands next to a running Redis. I found it running the Firefox WebDriver BiDi render pipeline for trmnl.com in production.

With a listener on 9998 and a driver requested on 9999:

before   FAILED after 45.1s: unable to bind to locking port 9998 within 45 seconds
after    started in 0.0s on port 9999

The lock now lives in a file under Dir.tmpdir named after the starting port, so it needs no port of its own.

  • replaces SocketLock with PortLock, which flocks tmpdir/selenium-port-.lock
  • locks on the requested port instead of the one below it, so the name matches what is being guarded
  • renames SOCKET_LOCK_TIMEOUT to PORT_LOCK_TIMEOUT and ServiceManager#socket_lock to #port_lock, both @api private
  • drops the Steepfile ignore that existed only for socket_lock.rb
  • creates the lock file 0600, since nothing but the creating process ever opens it
  • adds rb/spec/unit/selenium/webdriver/common/port_lock_spec.rb

Cross-process exclusion is unchanged, covered by a spec that holds the lock and asserts a second lock on the same port is refused while a lock on a different port goes through.

🔧 Implementation Notes

The lock only ever needed to be a name two processes could agree on, and binding a TCP port to get one is what drags an unrelated port into the requirements. A file in tmpdir gives the same mutual exclusion without consuming anything.

Keeping the socket lock and binding the requested port instead cannot work, since the lock is held across find_free_port and the port being probed has to stay free. Picking a lock port further away only moves the collision somewhere less predictable, and skipping the lock when a port was passed explicitly drops the protection for the case it exists for, several processes starting drivers at once.

The retry came out of CI. My first version opened the file read-only when it could not be opened for writing, which is fine on POSIX, but on Windows File.open raises Errno::EACCES while another process holds the lock, and an exclusive lock cannot be taken on a read-only handle there. That turned ordinary contention into a permanent spin and failed all ten Windows targets. EACCES is now treated as the lock not being available yet, and retried.

EROFS is not, since a read-only temp directory never becomes writable. Retrying it spun for the full 45 seconds and then blamed the lock, where the socket lock this replaces did not touch the filesystem at all, so it raises straight away and names the path. The spec for it takes 0.01s with the raise and 2.0s without, which is the timeout it used to wait out.

🤖 AI assistance

  • No substantial AI assistance used
  • AI assisted (complete below)
    • Tool(s): Claude Code
    • What was generated: a first draft of PortLock and its spec. The problem, the design, the measurement and the Windows fix are mine.
    • I reviewed all AI output and can explain the change

💡 Additional Considerations

  • No cross-user exclusion. If the lock file belongs to another user this treats it as contention until the timeout. Trunk has the same gap, TCPServer.new on a port another user holds fails the same way.
  • The lock file stays in tmpdir after release. It is empty and reused.
  • PortLock is @api private, as SocketLock was, so nothing public changes.

🔄 Types of changes

  • Cleanup (formatting, renaming)
  • Bug fix (backwards compatible)

@CLAassistant

CLAassistant commented Aug 9, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@selenium-ci selenium-ci added the C-rb Ruby Bindings label Aug 9, 2026
@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Use tmpdir flock-based PortLock to avoid needing a neighbouring port

🐞 Bug fix 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Replace startup TCP socket lock with a tmpdir file lock keyed by requested port.
• Prevent startup failures when port-1 is occupied by an unrelated service.
• Add unit coverage for cross-process exclusion and lock release semantics.
Diagram

graph TD
A["ServiceManager#start"] --> B["PortLock#locked"] --> C[("tmpdir/selenium-port-<port>.lock")]
B --> D["find_free_port + start_process + connect_until_stable"]
E["port_lock_spec"] --> B
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Atomic create (O_EXCL) lock files instead of flock
  • ➕ No advisory locking semantics; simple to implement
  • ➕ Works even where flock behavior is limited
  • ➖ Requires cleanup/stale-lock handling on crashes
  • ➖ More error-prone around ownership/permissions and recovery
2. Keep TCPServer-based lock but bind the requested port
  • ➕ Avoids filesystem reliance
  • ➕ Clear exclusivity semantics via OS port binding
  • ➖ Conflicts with the actual service needing the port
  • ➖ Still couples startup to networking stack and port availability quirks

Recommendation: The PR’s flock-based tmpdir lock is the best tradeoff: it removes the accidental dependency on port-1, preserves cross-process mutual exclusion, and naturally releases on process death without requiring lock cleanup. The main consideration is advisory-lock behavior, but using Dir.tmpdir (typically local) keeps this practical and reliable for the intended use.

Files changed (6) +170 / -18

Bug fix (2) +92 / -4
port_lock.rbAdd PortLock using tmpdir lock file + flock +88/-0

Add PortLock using tmpdir lock file + flock

• Introduces PortLock, which uses an exclusive non-blocking flock on a per-port lock file under Dir.tmpdir. Includes timeout-based acquisition, ensures release/close, and falls back to read-only opens when write access is denied.

rb/lib/selenium/webdriver/common/port_lock.rb

service_manager.rbUse PortLock on requested port during ServiceManager start +4/-4

Use PortLock on requested port during ServiceManager start

• Replaces the startup lock from socket_lock (binding port-1) to port_lock (file lock keyed by @port). Renames the timeout constant accordingly and updates the private helper method used by start.

rb/lib/selenium/webdriver/common/service_manager.rb

Refactor (3) +10 / -14
common.rbSwap SocketLock require for new PortLock +1/-1

Swap SocketLock require for new PortLock

• Updates the common WebDriver requires to load the new PortLock implementation instead of SocketLock. This wires the new locking strategy into the Ruby entrypoint for common components.

rb/lib/selenium/webdriver/common.rb

port_lock.rbsUpdate RBS signatures from SocketLock to PortLock +6/-10

Update RBS signatures from SocketLock to PortLock

• Renames the type definition and adjusts instance variables/method signatures to match the new PortLock API (path-based lock file, open_lock_file, release(file)). Keeps the surface area private as before.

rb/sig/lib/selenium/webdriver/common/port_lock.rbs

service_manager.rbsAlign ServiceManager RBS with port_lock and PORT_LOCK_TIMEOUT +3/-3

Align ServiceManager RBS with port_lock and PORT_LOCK_TIMEOUT

• Updates typed signatures to reflect renamed timeout constant and the new @port_lock ivar / port_lock accessor. Keeps the public API unchanged while aligning private internals.

rb/sig/lib/selenium/webdriver/common/service_manager.rbs

Tests (1) +68 / -0
port_lock_spec.rbAdd unit specs for PortLock mutual exclusion and release +68/-0

Add unit specs for PortLock mutual exclusion and release

• Adds coverage ensuring PortLock yields/returns block values, releases on normal completion and exceptions, ignores neighbouring-port occupancy, and enforces exclusivity for the same port while allowing different ports.

rb/spec/unit/selenium/webdriver/common/port_lock_spec.rb

@qodo-code-review

qodo-code-review Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (1) 📜 Skill insights (0)

Grey Divider


Action required

1. Permission errors hidden as retries ✓ Resolved 🐞 Bug ☼ Reliability
Description
PortLock#open_lock_file rescues Errno::EACCES/Errno::EROFS and returns nil, so true
permission/read-only filesystem failures are treated like temporary lock contention and will spin
until timeout before raising a generic acquire error. This can block ServiceManager#start (which
wraps startup in port_lock.locked) even when the requested port is free, while obscuring the real
root cause.
Code

rb/lib/selenium/webdriver/common/port_lock.rb[R78-81]

+      rescue Errno::EACCES, Errno::EROFS => e
+        WebDriver.logger.debug("#{self}: #{e.message}", id: :driver_service)
+        nil
+      end
Evidence
The new code converts EACCES/EROFS into nil (treated as “not available yet”), and the lock loop
retries until timeout, ultimately raising a generic acquire error. ServiceManager wraps startup in
this lock, so this behavior directly affects driver startup.

rb/lib/selenium/webdriver/common/port_lock.rb[58-69]
rb/lib/selenium/webdriver/common/port_lock.rb[71-81]
rb/lib/selenium/webdriver/common/service_manager.rb[50-60]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`PortLock#open_lock_file` currently rescues `Errno::EACCES` and `Errno::EROFS` and returns `nil`. The lock loop interprets `nil` as “lock not available yet” and retries until timeout, then raises a generic `unable to acquire ...` error. This masks genuine permission/read-only filesystem failures and can prevent driver startup in restricted environments.

### Issue Context
- `open_lock_file` returns `nil` on `EACCES`/`EROFS`, which the caller treats as transient contention.
- `ServiceManager#start` executes its critical startup section inside `port_lock.locked`, so inability to open/create the lock file fails the start path.

### Fix Focus Areas
- rb/lib/selenium/webdriver/common/port_lock.rb[58-66]
- rb/lib/selenium/webdriver/common/port_lock.rb[71-81]

### Suggested fix approach
- Keep returning `nil` only for truly transient “file temporarily unavailable” scenarios (e.g., Windows behavior when another process holds the file).
- For permission/read-only filesystem cases, fail fast with a clear error message:
 - Option A (recommended): on `Errno::EACCES`, attempt a read-only handle (`File::RDONLY`) and still `flock` it; if that also fails, raise a `WebDriverError` indicating insufficient permissions to open the lock file.
 - Option B: on `Errno::EROFS` (or failure to `CREAT`), raise immediately indicating the lock file cannot be created in `Dir.tmpdir`.
- Ensure logging/error messages include the lock path and the original exception class for diagnosability.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Stubbed File.open in spec 📘 Rule violation ▣ Testability ⭐ New
Description
The new unit test stubs File.open using RSpec, which violates the requirement to avoid mocks
unless backed by a contract-driven integration. This can reduce test fidelity by asserting behavior
against a mock rather than the real filesystem behavior.
Code

rb/spec/unit/selenium/webdriver/common/port_lock_spec.rb[R69-72]

+      it 'fails without waiting out the timeout when the lock file cannot be created' do
+        allow(File).to receive(:open).and_raise(Errno::EROFS)
+
+        expect { port_lock.locked { :never } }
Evidence
PR Compliance ID 389270 disallows introducing mocking frameworks in tests (unless contract-driven).
The added spec stubs File.open to raise Errno::EROFS, which is a mocking-based simulation of
filesystem behavior rather than exercising the real integration.

Rule 389270: Avoid mocks in tests; use real or contract-driven integrations
rb/spec/unit/selenium/webdriver/common/port_lock_spec.rb[69-73]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`rb/spec/unit/selenium/webdriver/common/port_lock_spec.rb` uses RSpec stubbing (`allow(File).to receive(:open)`) to simulate `Errno::EROFS`. The compliance rule requires avoiding mocks in tests unless using a real integration or a contract-driven stub.

## Issue Context
This test aims to verify the error path when the lock file cannot be created. Instead of mocking `File.open`, prefer a real filesystem scenario (or an explicitly contract-backed fake) that triggers the same failure mode.

## Fix Focus Areas
- rb/spec/unit/selenium/webdriver/common/port_lock_spec.rb[69-74]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Non-isolated PortLock unit test ✓ Resolved 🐞 Bug ☼ Reliability
Description
port_lock_spec hardcodes port 4444, which maps to a global, deterministic lockfile name, so
concurrent test runs on the same machine can contend for the same lock namespace and intermittently
fail. This reduces test isolation and makes failures dependent on unrelated processes using the same
lockfile path.
Code

rb/spec/unit/selenium/webdriver/common/port_lock_spec.rb[R24-27]

+    describe PortLock do
+      subject(:port_lock) { described_class.new(port, 2) }
+
+      let(:port) { 4444 }
Evidence
The spec uses a constant port which maps to a constant lockfile path; because PortLock uses
Dir.tmpdir, that lock namespace is shared across concurrent runs on the same machine.

rb/spec/unit/selenium/webdriver/common/port_lock_spec.rb[24-28]
rb/lib/selenium/webdriver/common/port_lock.rb[33-35]
rb/TESTING.md[32-45]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new unit spec uses a fixed port (`4444`), which means it always targets the same lockfile name in the system temp directory. Under parallel execution (or when another process uses the same lockfile), this can cause unexpected contention and flakes.

## Issue Context
`PortLock` derives its lockfile path from the port number, and Bazel test runs can execute concurrently unless configured otherwise.

## Fix Focus Areas
- rb/spec/unit/selenium/webdriver/common/port_lock_spec.rb[24-28]
- rb/lib/selenium/webdriver/common/port_lock.rb[33-35]

### Suggested implementation direction
- Choose a per-process/per-test unique port value for the lock namespace (e.g., `port = 40_000 + (Process.pid % 10_000)`), or derive from `SecureRandom`.
- Optionally, stub `Dir.tmpdir` (or inject the base directory) so the spec uses an isolated temporary directory and can clean up lockfiles deterministically.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Tmp lockfile path hijack 🐞 Bug ⛨ Security
Description
PortLock uses a predictable filename under Dir.tmpdir and opens it without validating that the path
is a safe regular file, allowing a local process to pre-create/lock that pathname and force
ServiceManager startup to block until timeout. This is a local availability/DoS risk and can also
produce confusing failures if the path is replaced with a directory/symlink.
Code

rb/lib/selenium/webdriver/common/port_lock.rb[R74-76]

+      def open_lock_file
+        file = File.open(@path, File::RDWR | File::CREAT) # rubocop:disable Style/FileOpen
+        file.close_on_exec = true
Evidence
The lock file name is deterministic in a shared temporary directory and is opened directly for
read/write creation; acquiring this lock gates ServiceManager startup, so any local interference
with the pathname can block startup until timeout.

rb/lib/selenium/webdriver/common/port_lock.rb[33-36]
rb/lib/selenium/webdriver/common/port_lock.rb[71-81]
rb/lib/selenium/webdriver/common/service_manager.rb[50-60]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`PortLock` builds a deterministic lockfile path in `Dir.tmpdir` and opens it without validating file type/ownership or hardening the lock namespace. In shared temp directories this permits local interference (pre-locked file, replaced path, etc.) that can block driver startup until timeout.

## Issue Context
`ServiceManager#start` wraps startup in `port_lock.locked`, so lock acquisition failures directly delay or prevent driver startup.

## Fix Focus Areas
- rb/lib/selenium/webdriver/common/port_lock.rb[33-36]
- rb/lib/selenium/webdriver/common/port_lock.rb[71-81]
- rb/lib/selenium/webdriver/common/service_manager.rb[55-59]

### Suggested implementation direction
- Create a dedicated lock directory under `Dir.tmpdir` with safe permissions (e.g., `0700`) and store lockfiles there.
- Open the lockfile with an explicit mode (e.g., `0o600`) and validate it is a regular file (e.g., `File.lstat` + `file.ftype == 'file'`) before locking.
- Consider defending against symlink/path tricks where supported (e.g., refusing symlinks).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

5. Misleading EROFS error text 🐞 Bug ◔ Observability ⭐ New
Description
PortLock#open_lock_file raises a WebDriverError saying it was unable to "create" the lock file on
Errno::EROFS, but Errno::EROFS can also occur when opening an already-existing lock file for
read/write on a read-only filesystem. This makes failures harder to diagnose, and the new spec locks
in the misleading wording.
Code

rb/lib/selenium/webdriver/common/port_lock.rb[R78-80]

+      rescue Errno::EROFS => e
+        raise Error::WebDriverError, "unable to create the lock file #{@path}: #{e.message}"
+      rescue Errno::EACCES => e
Evidence
The implementation raises a WebDriverError on Errno::EROFS with wording specific to creation, and
the newly added unit test asserts that exact phrasing. Because the open mode is RDWR | CREAT, the
operation can be either opening an existing file or creating a new one, so “create” is not always
accurate.

rb/lib/selenium/webdriver/common/port_lock.rb[71-83]
rb/spec/unit/selenium/webdriver/common/port_lock_spec.rb[27-30]
rb/spec/unit/selenium/webdriver/common/port_lock_spec.rb[69-74]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`PortLock#open_lock_file` rescues `Errno::EROFS` from `File.open(@path, File::RDWR | File::CREAT, ...)` and raises an error that specifically claims it was unable to **create** the lock file. Since the open flags also cover opening an existing file for write, `EROFS` may indicate inability to **open/access** the lock file on a read-only filesystem, not strictly creation.

The unit test currently asserts the “unable to create the lock file” wording, which entrenches the misleading message.

### Issue Context
This is a diagnostic/observability issue (not a locking correctness issue), but it affects how actionable the reported error is in real deployments.

### Fix Focus Areas
- rb/lib/selenium/webdriver/common/port_lock.rb[78-80]
- rb/spec/unit/selenium/webdriver/common/port_lock_spec.rb[69-74]

### Suggested change
- Change the message to something that covers both create/open cases, e.g.:
 - `"unable to open the lock file #{@path}: #{e.message}"`
 - or `"unable to create/open the lock file #{@path}: #{e.message}"`
- Update the spec expectation regex accordingly (match the new wording).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Stale Steep ignore entry ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
This PR deletes common/socket_lock.rb, but rb/Steepfile still ignores that now-nonexistent path,
leaving misleading/dead type-check configuration. Future maintainers may assume SocketLock still
exists or miss updating ignores for the new PortLock file if needed.
Code

rb/lib/selenium/webdriver/common/socket_lock.rb[L1-4]

-# frozen_string_literal: true
-
-# Licensed to the Software Freedom Conservancy (SFC) under one
-# or more contributor license agreements.  See the NOTICE file
Evidence
The Steep configuration still references the removed SocketLock path even though runtime now
requires port_lock instead.

rb/Steepfile[49-55]
rb/lib/selenium/webdriver/common.rb[28-34]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`rb/Steepfile` still lists `lib/selenium/webdriver/common/socket_lock.rb` in its `ignore(...)` list, but this PR deletes that file. This leaves dead configuration and can confuse future maintenance.

### Issue Context
- SocketLock has been removed/replaced by PortLock.
- `rb/Steepfile` should not reference deleted paths.

### Fix Focus Areas
- rb/Steepfile[50-55]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context used
✅ Compliance rules (platform): 19 rules

Grey Divider

Tip of the day
💡 Did you know, you can reply 'qodo' on any finding to push back, ask questions, or dig deeper

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Previous review results

Review updated until commit 130c925 ⚖️ Balanced

Results up to commit 121ceec ⚖️ Balanced


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Informational
1. Stale Steep ignore entry ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
This PR deletes common/socket_lock.rb, but rb/Steepfile still ignores that now-nonexistent path,
leaving misleading/dead type-check configuration. Future maintainers may assume SocketLock still
exists or miss updating ignores for the new PortLock file if needed.
Code

rb/lib/selenium/webdriver/common/socket_lock.rb[L1-4]

-# frozen_string_literal: true
-
-# Licensed to the Software Freedom Conservancy (SFC) under one
-# or more contributor license agreements.  See the NOTICE file
Evidence
The Steep configuration still references the removed SocketLock path even though runtime now
requires port_lock instead.

rb/Steepfile[49-55]
rb/lib/selenium/webdriver/common.rb[28-34]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`rb/Steepfile` still lists `lib/selenium/webdriver/common/socket_lock.rb` in its `ignore(...)` list, but this PR deletes that file. This leaves dead configuration and can confuse future maintenance.

### Issue Context
- SocketLock has been removed/replaced by PortLock.
- `rb/Steepfile` should not reference deleted paths.

### Fix Focus Areas
- rb/Steepfile[50-55]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Results up to commit 24fb6c1 ⚖️ Balanced


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Action required
1. Permission errors hidden as retries ✓ Resolved 🐞 Bug ☼ Reliability
Description
PortLock#open_lock_file rescues Errno::EACCES/Errno::EROFS and returns nil, so true
permission/read-only filesystem failures are treated like temporary lock contention and will spin
until timeout before raising a generic acquire error. This can block ServiceManager#start (which
wraps startup in port_lock.locked) even when the requested port is free, while obscuring the real
root cause.
Code

rb/lib/selenium/webdriver/common/port_lock.rb[R78-81]

+      rescue Errno::EACCES, Errno::EROFS => e
+        WebDriver.logger.debug("#{self}: #{e.message}", id: :driver_service)
+        nil
+      end
Evidence
The new code converts EACCES/EROFS into nil (treated as “not available yet”), and the lock loop
retries until timeout, ultimately raising a generic acquire error. ServiceManager wraps startup in
this lock, so this behavior directly affects driver startup.

rb/lib/selenium/webdriver/common/port_lock.rb[58-69]
rb/lib/selenium/webdriver/common/port_lock.rb[71-81]
rb/lib/selenium/webdriver/common/service_manager.rb[50-60]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`PortLock#open_lock_file` currently rescues `Errno::EACCES` and `Errno::EROFS` and returns `nil`. The lock loop interprets `nil` as “lock not available yet” and retries until timeout, then raises a generic `unable to acquire ...` error. This masks genuine permission/read-only filesystem failures and can prevent driver startup in restricted environments.

### Issue Context
- `open_lock_file` returns `nil` on `EACCES`/`EROFS`, which the caller treats as transient contention.
- `ServiceManager#start` executes its critical startup section inside `port_lock.locked`, so inability to open/create the lock file fails the start path.

### Fix Focus Areas
- rb/lib/selenium/webdriver/common/port_lock.rb[58-66]
- rb/lib/selenium/webdriver/common/port_lock.rb[71-81]

### Suggested fix approach
- Keep returning `nil` only for truly transient “file temporarily unavailable” scenarios (e.g., Windows behavior when another process holds the file).
- For permission/read-only filesystem cases, fail fast with a clear error message:
 - Option A (recommended): on `Errno::EACCES`, attempt a read-only handle (`File::RDONLY`) and still `flock` it; if that also fails, raise a `WebDriverError` indicating insufficient permissions to open the lock file.
 - Option B: on `Errno::EROFS` (or failure to `CREAT`), raise immediately indicating the lock file cannot be created in `Dir.tmpdir`.
- Ensure logging/error messages include the lock path and the original exception class for diagnosability.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Results up to commit 354bcfc ⚖️ Balanced


🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Remediation recommended
1. Tmp lockfile path hijack 🐞 Bug ⛨ Security
Description
PortLock uses a predictable filename under Dir.tmpdir and opens it without validating that the path
is a safe regular file, allowing a local process to pre-create/lock that pathname and force
ServiceManager startup to block until timeout. This is a local availability/DoS risk and can also
produce confusing failures if the path is replaced with a directory/symlink.
Code

rb/lib/selenium/webdriver/common/port_lock.rb[R74-76]

+      def open_lock_file
+        file = File.open(@path, File::RDWR | File::CREAT) # rubocop:disable Style/FileOpen
+        file.close_on_exec = true
Evidence
The lock file name is deterministic in a shared temporary directory and is opened directly for
read/write creation; acquiring this lock gates ServiceManager startup, so any local interference
with the pathname can block startup until timeout.

rb/lib/selenium/webdriver/common/port_lock.rb[33-36]
rb/lib/selenium/webdriver/common/port_lock.rb[71-81]
rb/lib/selenium/webdriver/common/service_manager.rb[50-60]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`PortLock` builds a deterministic lockfile path in `Dir.tmpdir` and opens it without validating file type/ownership or hardening the lock namespace. In shared temp directories this permits local interference (pre-locked file, replaced path, etc.) that can block driver startup until timeout.

## Issue Context
`ServiceManager#start` wraps startup in `port_lock.locked`, so lock acquisition failures directly delay or prevent driver startup.

## Fix Focus Areas
- rb/lib/selenium/webdriver/common/port_lock.rb[33-36]
- rb/lib/selenium/webdriver/common/port_lock.rb[71-81]
- rb/lib/selenium/webdriver/common/service_manager.rb[55-59]

### Suggested implementation direction
- Create a dedicated lock directory under `Dir.tmpdir` with safe permissions (e.g., `0700`) and store lockfiles there.
- Open the lockfile with an explicit mode (e.g., `0o600`) and validate it is a regular file (e.g., `File.lstat` + `file.ftype == 'file'`) before locking.
- Consider defending against symlink/path tricks where supported (e.g., refusing symlinks).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Non-isolated PortLock unit test ✓ Resolved 🐞 Bug ☼ Reliability
Description
port_lock_spec hardcodes port 4444, which maps to a global, deterministic lockfile name, so
concurrent test runs on the same machine can contend for the same lock namespace and intermittently
fail. This reduces test isolation and makes failures dependent on unrelated processes using the same
lockfile path.
Code

rb/spec/unit/selenium/webdriver/common/port_lock_spec.rb[R24-27]

+    describe PortLock do
+      subject(:port_lock) { described_class.new(port, 2) }
+
+      let(:port) { 4444 }
Evidence
The spec uses a constant port which maps to a constant lockfile path; because PortLock uses
Dir.tmpdir, that lock namespace is shared across concurrent runs on the same machine.

rb/spec/unit/selenium/webdriver/common/port_lock_spec.rb[24-28]
rb/lib/selenium/webdriver/common/port_lock.rb[33-35]
rb/TESTING.md[32-45]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new unit spec uses a fixed port (`4444`), which means it always targets the same lockfile name in the system temp directory. Under parallel execution (or when another process uses the same lockfile), this can cause unexpected contention and flakes.

## Issue Context
`PortLock` derives its lockfile path from the port number, and Bazel test runs can execute concurrently unless configured otherwise.

## Fix Focus Areas
- rb/spec/unit/selenium/webdriver/common/port_lock_spec.rb[24-28]
- rb/lib/selenium/webdriver/common/port_lock.rb[33-35]

### Suggested implementation direction
- Choose a per-process/per-test unique port value for the lock namespace (e.g., `port = 40_000 + (Process.pid % 10_000)`), or derive from `SecureRandom`.
- Optionally, stub `Dir.tmpdir` (or inject the base directory) so the spec uses an isolated temporary directory and can clean up lockfiles deterministically.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Qodo Logo

Comment thread rb/lib/selenium/webdriver/common/socket_lock.rb
Comment thread rb/lib/selenium/webdriver/common/port_lock.rb Outdated
@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 24fb6c1

@diemol

diemol commented Aug 10, 2026

Copy link
Copy Markdown
Member

Can you use the PR template?

ServiceManager held its startup lock by binding port - 1, so starting a driver
on port N required N-1 to be free as well. When an unrelated service already
listened there, the lock could never be taken: startup spun for 45 seconds and
then raised, even though the requested port itself was free.

The lock now lives in a file named after the starting port, so it needs no port
of its own. Mutual exclusion across processes is unchanged.
The entry was there for a TCPServer rescue that PortLock does not have, and it
named a file this branch removes. PortLock type checks without an ignore.
Windows refuses to open a file another process holds a lock on, so the read-only
fallback ran during ordinary contention, and an exclusive lock cannot be taken on
a read-only handle there. Every driver start on Windows then spun for the full
timeout. A file that cannot be opened now counts as the lock being unavailable
and is retried.
@ikraamg
ikraamg force-pushed the fix/port-lock-without-a-neighbouring-port branch from 24fb6c1 to 354bcfc Compare August 10, 2026 14:37
Comment on lines +74 to +76
def open_lock_file
file = File.open(@path, File::RDWR | File::CREAT) # rubocop:disable Style/FileOpen
file.close_on_exec = true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

1. Tmp lockfile path hijack 🐞 Bug ⛨ Security

PortLock uses a predictable filename under Dir.tmpdir and opens it without validating that the path
is a safe regular file, allowing a local process to pre-create/lock that pathname and force
ServiceManager startup to block until timeout. This is a local availability/DoS risk and can also
produce confusing failures if the path is replaced with a directory/symlink.
Agent Prompt
## Issue description
`PortLock` builds a deterministic lockfile path in `Dir.tmpdir` and opens it without validating file type/ownership or hardening the lock namespace. In shared temp directories this permits local interference (pre-locked file, replaced path, etc.) that can block driver startup until timeout.

## Issue Context
`ServiceManager#start` wraps startup in `port_lock.locked`, so lock acquisition failures directly delay or prevent driver startup.

## Fix Focus Areas
- rb/lib/selenium/webdriver/common/port_lock.rb[33-36]
- rb/lib/selenium/webdriver/common/port_lock.rb[71-81]
- rb/lib/selenium/webdriver/common/service_manager.rb[55-59]

### Suggested implementation direction
- Create a dedicated lock directory under `Dir.tmpdir` with safe permissions (e.g., `0700`) and store lockfiles there.
- Open the lockfile with an explicit mode (e.g., `0o600`) and validate it is a regular file (e.g., `File.lstat` + `file.ftype == 'file'`) before locking.
- Consider defending against symlink/path tricks where supported (e.g., refusing symlinks).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread rb/spec/unit/selenium/webdriver/common/port_lock_spec.rb Outdated
@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 354bcfc

A file that cannot be opened is treated as the lock being held elsewhere, which is
what Windows needs, but a read-only temp directory never becomes writable. Startup
spun for the full 45 seconds and then blamed the lock, hiding the real cause, where
the socket lock this replaces did not touch the filesystem at all. EROFS now raises
straight away and names the path.

The lock file is also created 0600 rather than at the default umask, since nothing
but the creating process ever needs to open it.
The spec locked port 4444, which names the same file a driver started on the
default port uses, so it contended with a real driver or with another run of the
spec on the same machine. The port only names the lock file here, so deriving it
from the pid is enough to keep runs apart.
Comment on lines +69 to +72
it 'fails without waiting out the timeout when the lock file cannot be created' do
allow(File).to receive(:open).and_raise(Errno::EROFS)

expect { port_lock.locked { :never } }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

1. Stubbed file.open in spec 📘 Rule violation ▣ Testability

The new unit test stubs File.open using RSpec, which violates the requirement to avoid mocks
unless backed by a contract-driven integration. This can reduce test fidelity by asserting behavior
against a mock rather than the real filesystem behavior.
Agent Prompt
## Issue description
`rb/spec/unit/selenium/webdriver/common/port_lock_spec.rb` uses RSpec stubbing (`allow(File).to receive(:open)`) to simulate `Errno::EROFS`. The compliance rule requires avoiding mocks in tests unless using a real integration or a contract-driven stub.

## Issue Context
This test aims to verify the error path when the lock file cannot be created. Instead of mocking `File.open`, prefer a real filesystem scenario (or an explicitly contract-backed fake) that triggers the same failure mode.

## Fix Focus Areas
- rb/spec/unit/selenium/webdriver/common/port_lock_spec.rb[69-74]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +78 to +80
rescue Errno::EROFS => e
raise Error::WebDriverError, "unable to create the lock file #{@path}: #{e.message}"
rescue Errno::EACCES => e

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Informational

2. Misleading erofs error text 🐞 Bug ◔ Observability

PortLock#open_lock_file raises a WebDriverError saying it was unable to "create" the lock file on
Errno::EROFS, but Errno::EROFS can also occur when opening an already-existing lock file for
read/write on a read-only filesystem. This makes failures harder to diagnose, and the new spec locks
in the misleading wording.
Agent Prompt
### Issue description
`PortLock#open_lock_file` rescues `Errno::EROFS` from `File.open(@path, File::RDWR | File::CREAT, ...)` and raises an error that specifically claims it was unable to **create** the lock file. Since the open flags also cover opening an existing file for write, `EROFS` may indicate inability to **open/access** the lock file on a read-only filesystem, not strictly creation.

The unit test currently asserts the “unable to create the lock file” wording, which entrenches the misleading message.

### Issue Context
This is a diagnostic/observability issue (not a locking correctness issue), but it affects how actionable the reported error is in real deployments.

### Fix Focus Areas
- rb/lib/selenium/webdriver/common/port_lock.rb[78-80]
- rb/spec/unit/selenium/webdriver/common/port_lock_spec.rb[69-74]

### Suggested change
- Change the message to something that covers both create/open cases, e.g.:
  - `"unable to open the lock file #{@path}: #{e.message}"`
  - or `"unable to create/open the lock file #{@path}: #{e.message}"`
- Update the spec expectation regex accordingly (match the new wording).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 130c925

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

Labels

C-rb Ruby Bindings

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants