Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions rb/Steepfile
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,6 @@ target :lib do
'lib/selenium/webdriver/common/child_process.rb',
# Ignore due to Net::HTTP not being found on line 49
'lib/selenium/webdriver/chromium/driver.rb',
# Ignore due to positional argument error with TCPServer rescue on line 69
'lib/selenium/webdriver/common/socket_lock.rb',
# Ignore due to is_a? bot error on line 70
'lib/selenium/webdriver/remote/driver.rb',
# Ignore due to line 118 causing an error with URI & Net::HTTP
Expand Down
2 changes: 1 addition & 1 deletion rb/lib/selenium/webdriver/common.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
require 'selenium/webdriver/common/selenium_manager'
require 'selenium/webdriver/common/service'
require 'selenium/webdriver/common/service_manager'
require 'selenium/webdriver/common/socket_lock'
require 'selenium/webdriver/common/port_lock'
require 'selenium/webdriver/common/socket_poller'
require 'selenium/webdriver/common/port_prober'
require 'selenium/webdriver/common/zipper'
Expand Down
95 changes: 95 additions & 0 deletions rb/lib/selenium/webdriver/common/port_lock.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# frozen_string_literal: true

# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

require 'tmpdir'

module Selenium
module WebDriver
#
# Holds a lock on a starting port so that two processes probing for a free port
# cannot both claim it. The lock lives in a file rather than on a TCP port, so it
# needs no port of its own and is released even if the process is killed.
#
# @api private
#

class PortLock
def initialize(port, timeout)
@path = File.join(Dir.tmpdir, "selenium-port-#{port}.lock")
@timeout = timeout
end

#
# Attempt to acquire the lock. Control is yielded to an execution block once it
# is held, and the lock is released when the block finishes.
#

def locked
file = lock

begin
yield
ensure
release(file)
end
end

private

def lock
max_time = current_time + @timeout

loop do
file = open_lock_file
return file if file&.flock(File::LOCK_EX | File::LOCK_NB)

file&.close
break if current_time >= max_time

sleep 0.1
end

raise Error::WebDriverError, "unable to acquire #{@path} within #{@timeout} seconds"
end

# nil means the lock is not available yet: Windows refuses to open a file another
# process has locked. The handle outlives this method when it is returned, since it
# holds the lock until #locked closes it.
def open_lock_file
file = File.open(@path, File::RDWR | File::CREAT, 0o600) # rubocop:disable Style/FileOpen
file.close_on_exec = true
Comment on lines +74 to +76

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

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

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

WebDriver.logger.debug("#{self}: #{e.message}", id: :driver_service)
nil
end

def release(file)
file.flock(File::LOCK_UN)
file.close
end

def current_time
Process.clock_gettime(Process::CLOCK_MONOTONIC)
end
end # PortLock
end # WebDriver
end # Selenium
8 changes: 4 additions & 4 deletions rb/lib/selenium/webdriver/common/service_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ module WebDriver
#
class ServiceManager
START_TIMEOUT = 20
SOCKET_LOCK_TIMEOUT = 45
PORT_LOCK_TIMEOUT = 45
STOP_TIMEOUT = 20

#
Expand All @@ -52,7 +52,7 @@ def start

Platform.exit_hook { stop } # make sure we don't leave the server running

socket_lock.locked do
port_lock.locked do
find_free_port
start_process
connect_until_stable
Expand Down Expand Up @@ -169,8 +169,8 @@ def cannot_connect_error_text
"unable to connect to #{@executable_path} #{@host}:#{@port}"
end

def socket_lock
@socket_lock ||= SocketLock.new(@port - 1, SOCKET_LOCK_TIMEOUT)
def port_lock
@port_lock ||= PortLock.new(@port, PORT_LOCK_TIMEOUT)
end
end # Service
end # WebDriver
Expand Down
82 changes: 0 additions & 82 deletions rb/lib/selenium/webdriver/common/socket_lock.rb

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,8 @@

module Selenium
module WebDriver
class SocketLock
@port: untyped

@server: untyped
class PortLock
@path: untyped

@timeout: untyped

Expand All @@ -31,15 +29,13 @@ module Selenium

private

def lock: () -> untyped?

def current_time: () -> untyped
def lock: () -> untyped

def release: () -> untyped
def open_lock_file: () -> untyped

def can_lock?: () -> untyped
def release: (untyped file) -> untyped

def did_lock?: () -> untyped
def current_time: () -> untyped
end
end
end
6 changes: 3 additions & 3 deletions rb/sig/lib/selenium/webdriver/common/service_manager.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ module Selenium

@process: untyped

@socket_lock: untyped
@port_lock: untyped

START_TIMEOUT: Integer

SOCKET_LOCK_TIMEOUT: Integer
PORT_LOCK_TIMEOUT: Integer

STOP_TIMEOUT: Integer

Expand Down Expand Up @@ -77,7 +77,7 @@ module Selenium

def cannot_connect_error_text: () -> String

def socket_lock: () -> untyped
def port_lock: () -> untyped
end
end
end
77 changes: 77 additions & 0 deletions rb/spec/unit/selenium/webdriver/common/port_lock_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# frozen_string_literal: true

# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

require File.expand_path('../spec_helper', __dir__)

module Selenium
module WebDriver
describe PortLock do
subject(:port_lock) { described_class.new(port, 2) }

# The lock file is named after the port, so a fixed one would collide with a real
# driver on 4444 and with any other run of this spec on the same machine.
let(:port) { 40_000 + (Process.pid % 10_000) }

it 'yields to the block' do
expect { |block| port_lock.locked(&block) }.to yield_control
end

it 'returns what the block returned' do
expect(port_lock.locked { :started }).to be(:started)
end

it 'releases the lock once the block is done' do
port_lock.locked { :first }

expect(described_class.new(port, 2).locked { :second }).to be(:second)
end

it 'releases the lock when the block raises' do
expect { port_lock.locked { raise 'boom' } }.to raise_error('boom')
expect(described_class.new(port, 2).locked { :second }).to be(:second)
end

it 'ignores a neighbouring port being in use' do
neighbour = TCPServer.new(Platform.localhost, 0)
busy = described_class.new(neighbour.addr[1], 2)

expect(busy.locked { :started }).to be(:started)
ensure
neighbour&.close
end

it 'keeps a second lock on the same port out' do
expect {
port_lock.locked { described_class.new(port, 0).locked { :never } }
}.to raise_error(Error::WebDriverError, /unable to acquire/)
end

it 'lets a lock on a different port through' do
expect(port_lock.locked { described_class.new(port + 1, 0).locked { :other } }).to be(:other)
end

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 } }
Comment on lines +69 to +72

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

.to raise_error(Error::WebDriverError, /unable to create the lock file/)
end
end
end
end