diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f83888ff..51047df4 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -97,7 +97,7 @@ jobs:
uses: actions/checkout@v6
- name: Configure
- run: cmake --preset dev
+ run: cmake --preset dev -DPV_ENABLE_IO_URING=OFF
- name: Build
run: cmake --build --preset dev --parallel
@@ -113,6 +113,38 @@ jobs:
path: build/dev/Testing
if-no-files-found: ignore
+ unit-tests-io-uring:
+ name: Unit tests (io_uring, Ubuntu 26.04, Clang)
+ runs-on: ubuntu-24.04
+ timeout-minutes: 60
+ container:
+ image: ci.trafficserver.apache.org/proxy-verifier/ubuntu:26.04
+ options: --security-opt seccomp=unconfined
+ env:
+ CC: clang
+ CXX: clang++
+ PV_TEST_REQUIRE_IO_URING: "1"
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v6
+
+ - name: Configure
+ run: cmake --preset dev -DPV_ENABLE_IO_URING=ON
+
+ - name: Build
+ run: cmake --build --preset dev --parallel
+
+ - name: Run unit tests
+ run: ctest --preset dev
+
+ - name: Upload unit-test logs
+ if: failure()
+ uses: actions/upload-artifact@v6
+ with:
+ name: unit-test-io-uring-logs
+ path: build/dev/Testing
+ if-no-files-found: ignore
+
urtests:
name: Uranium tests (Ubuntu 26.04, GCC)
runs-on: ubuntu-24.04
@@ -127,7 +159,10 @@ jobs:
uses: actions/checkout@v6
- name: Configure
- run: cmake --preset dev -DURTEST_SANDBOX=/tmp/proxy-verifier-urtest
+ run: >-
+ cmake --preset dev
+ -DPV_ENABLE_IO_URING=OFF
+ -DURTEST_SANDBOX=/tmp/proxy-verifier-urtest
- name: Build
run: cmake --build --preset dev --parallel
@@ -152,3 +187,47 @@ jobs:
name: urtest-sandbox
path: artifacts/urtest-sandbox.tar.gz
if-no-files-found: ignore
+
+ urtests-io-uring:
+ name: Uranium tests (io_uring, Ubuntu 26.04, GCC)
+ runs-on: ubuntu-24.04
+ timeout-minutes: 60
+ container:
+ image: ci.trafficserver.apache.org/proxy-verifier/ubuntu:26.04
+ options: --security-opt seccomp=unconfined
+ env:
+ CC: gcc
+ CXX: g++
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v6
+
+ - name: Configure
+ run: >-
+ cmake --preset dev
+ -DPV_ENABLE_IO_URING=ON
+ -DURTEST_SANDBOX=/tmp/proxy-verifier-urtest-io-uring
+
+ - name: Build
+ run: cmake --build --preset dev --parallel
+
+ - name: Run Uranium tests
+ run: ./build/dev/urtest.sh -v -n "$(nproc)" --verifier-io-uring required
+
+ - name: Collect Uranium sandbox
+ if: failure()
+ run: |
+ mkdir -p artifacts
+ if [[ -d /tmp/proxy-verifier-urtest-io-uring ]]; then
+ tar --create --gzip \
+ --file="${GITHUB_WORKSPACE}/artifacts/urtest-io-uring-sandbox.tar.gz" \
+ --directory=/tmp proxy-verifier-urtest-io-uring
+ fi
+
+ - name: Upload Uranium sandbox
+ if: failure()
+ uses: actions/upload-artifact@v6
+ with:
+ name: urtest-io-uring-sandbox
+ path: artifacts/urtest-io-uring-sandbox.tar.gz
+ if-no-files-found: ignore
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 3c6d08f4..13d09feb 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -37,6 +37,8 @@ option(PV_ENABLE_ASAN
"Build the verifier binaries with AddressSanitizer instrumentation." OFF)
option(PV_RELEASE_LAYOUT
"Install release artifacts into a platform-specific bindir." OFF)
+option(PV_ENABLE_IO_URING
+ "Use io_uring for socket readiness when liburing is available." ON)
option(
BUILD_PORTABLE
"Enable portable build settings such as conservative Linux amd64 ISA flags."
@@ -153,6 +155,9 @@ pv_fetch_dependencies()
include(HttpDependencies)
pv_find_http_dependencies(PV_HTTP_DEPENDENCY_TARGETS)
+include(IoUring)
+pv_find_io_uring(PV_IO_URING_TARGET)
+
add_subdirectory(src)
string(SHA256 _pv_source_hash "${CMAKE_SOURCE_DIR}")
string(SUBSTRING "${_pv_source_hash}" 0 8 _pv_source_hash)
diff --git a/README.md b/README.md
index 40eaa53d..ab774cb9 100644
--- a/README.md
+++ b/README.md
@@ -68,6 +68,7 @@ Table of Contents
* [--tls-secrets-log-file <secrets_log_file_name>](#--tls-secrets-log-file-secrets_log_file_name)
* [--send-buffer-size <size>](#--send-buffer-size-size)
* [--poll-timeout <timeout_ms>](#--poll-timeout-timeout_ms)
+ * [--io-uring <mode>](#--io-uring-mode)
* [Tools](#tools)
* [Replay Gen replay-gen.py](tools/replay-gen.py)
* [-n,--number <NUMBER>](#-n--number-number)
@@ -1832,12 +1833,18 @@ presets. The build requires:
* OpenSSL 3.5 or newer development files
* nghttp2 1.60 or newer development files
* nghttp3 0.8 or newer development files
+* liburing 2.0 or newer development files on Linux (optional)
CMake fetches pinned `libswoc` and `yaml-cpp` sources during configure. HTTP/3
uses OpenSSL 3.5 or newer for the native QUIC transport and nghttp3 for HTTP/3
framing and QPACK. HTTP/2 uses nghttp2. Install these libraries through the
system package manager before configuring Proxy Verifier. ngtcp2 is not
-required because OpenSSL provides the QUIC transport.
+required because OpenSSL provides the QUIC transport. On Linux, CMake enables
+io_uring socket readiness support when liburing is installed. Builds without
+liburing, non-Linux builds, and kernels that cannot initialize io_uring retain
+the existing `poll(2)` behavior. Dynamic Linux builds that enable io_uring
+support require the liburing shared library at runtime; portable Linux release
+binaries link it statically.
For Mac builds, the following brew command can be helpful:
@@ -1868,9 +1875,9 @@ This places the build-tree binaries under `build/dev/bin`.
The Dockerfiles under `docker/alpine_3.24`, `docker/fedora_44`, and
`docker/ubuntu_26.04` are build and development environments, not deployment
-images. Each installs OpenSSL, nghttp2, and nghttp3 from its distribution along
-with the Proxy Verifier build, formatting, license-audit, and Uranium/pytest
-toolchains.
+images. Each installs OpenSSL, nghttp2, nghttp3, and liburing from its
+distribution along with the Proxy Verifier build, formatting, license-audit,
+and Uranium/pytest toolchains.
GitHub Actions uses only the Ubuntu 26.04 image. Alpine 3.24 is used to build
the portable Linux release binaries, while Fedora 44 provides an additional
current-distribution development environment. Users who prefer Alpine or
@@ -2355,6 +2362,31 @@ non-blocking sockets with a timeout. By default, this timeout is set to 5
seconds (5,000 milliseconds). This optional argument provides a way to specify a
different timeout in milliseconds for these operations.
+#### --io-uring \
+
+On Linux builds with liburing, Proxy Verifier uses one process-wide io_uring
+to multiplex socket readiness waits. This avoids creating an io_uring for each
+connection thread. The HTTP, TLS, and replay state machines remain unchanged,
+and Proxy Verifier transparently uses `poll(2)` when io_uring is unavailable.
+
+The mode can be one of:
+
+* `auto` uses io_uring when both the build and running kernel support it, then
+ falls back to `poll(2)` if initialization is unavailable. This is the
+ default.
+* `off` always uses `poll(2)`. This is useful as the control side of a
+ production performance comparison using the same binary.
+* `required` exits with an error unless io_uring initializes successfully and
+ does not fall back if the backend later fails. Use this for an io_uring
+ production canary so kernel security policy or a container seccomp profile
+ cannot silently turn the test into a `poll(2)` run.
+
+Regardless of the selected mode, zero or negative timeouts use a direct
+`poll(2)` call because they do not benefit from asynchronous dispatch.
+
+Set the CMake option `PV_ENABLE_IO_URING=OFF` to omit io_uring support even if
+liburing is installed.
+
## Tools
This section describes how to use some of the scripts under the [tools](tools) directory.
diff --git a/cmake/IoUring.cmake b/cmake/IoUring.cmake
new file mode 100644
index 00000000..ec2f3a14
--- /dev/null
+++ b/cmake/IoUring.cmake
@@ -0,0 +1,48 @@
+# @file
+#
+# Copyright 2026, Verizon Media SPDX-License-Identifier: Apache-2.0
+#
+
+include_guard(GLOBAL)
+
+function(pv_find_io_uring out_var)
+ set(_pv_io_uring_target "")
+ if(PV_ENABLE_IO_URING AND CMAKE_SYSTEM_NAME STREQUAL "Linux")
+ if(PV_STATIC_BUILD)
+ set(CMAKE_FIND_LIBRARY_SUFFIXES "${CMAKE_STATIC_LIBRARY_SUFFIX}")
+ endif()
+ find_package(PkgConfig REQUIRED)
+ pkg_check_modules(PV_LIBURING QUIET IMPORTED_TARGET GLOBAL "liburing>=2.0")
+ if(PV_LIBURING_FOUND)
+ if(PV_STATIC_BUILD)
+ if(NOT PV_LIBURING_LINK_LIBRARIES)
+ message(FATAL_ERROR "Static builds require a static liburing archive")
+ endif()
+ foreach(_pv_io_uring_library IN LISTS PV_LIBURING_LINK_LIBRARIES)
+ if(NOT _pv_io_uring_library MATCHES
+ "\\${CMAKE_STATIC_LIBRARY_SUFFIX}$")
+ message(
+ FATAL_ERROR
+ "Static builds require a static liburing archive, but pkg-config resolved "
+ "${_pv_io_uring_library}")
+ endif()
+ endforeach()
+ endif()
+ set(_pv_io_uring_target PkgConfig::PV_LIBURING)
+ message(STATUS "Building with io_uring socket readiness support")
+ else()
+ message(
+ STATUS "liburing was not found; using the poll socket readiness backend"
+ )
+ endif()
+ elseif(PV_ENABLE_IO_URING)
+ message(
+ STATUS "io_uring is not available on ${CMAKE_SYSTEM_NAME}; using poll")
+ else()
+ message(STATUS "io_uring support is disabled; using poll")
+ endif()
+
+ set(${out_var}
+ "${_pv_io_uring_target}"
+ PARENT_SCOPE)
+endfunction()
diff --git a/docker/alpine_3.24/Dockerfile b/docker/alpine_3.24/Dockerfile
index 411e220e..8edf25ac 100644
--- a/docker/alpine_3.24/Dockerfile
+++ b/docker/alpine_3.24/Dockerfile
@@ -24,6 +24,7 @@ RUN apk add --no-cache \
gcompat \
git \
libffi-dev \
+ liburing-dev \
linux-headers \
nghttp2-dev \
nghttp2-static \
diff --git a/docker/fedora_44/Dockerfile b/docker/fedora_44/Dockerfile
index 313705ba..5e491659 100644
--- a/docker/fedora_44/Dockerfile
+++ b/docker/fedora_44/Dockerfile
@@ -30,6 +30,7 @@ dnf install -y \
libffi-devel \
libnghttp2-devel \
libnghttp3-devel \
+ liburing-devel \
openssl-devel \
pkgconf-pkg-config \
procps-ng \
diff --git a/docker/ubuntu_26.04/Dockerfile b/docker/ubuntu_26.04/Dockerfile
index 4736e0e2..6873bd31 100644
--- a/docker/ubuntu_26.04/Dockerfile
+++ b/docker/ubuntu_26.04/Dockerfile
@@ -38,6 +38,7 @@ apt-get install -y --no-install-recommends \
libnghttp2-dev \
libnghttp3-dev \
libssl-dev \
+ liburing-dev \
libzstd-dev \
lbzip2 \
pkg-config \
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 624fcc36..851a4a70 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -15,6 +15,7 @@ set(PV_CORE_SOURCES
"${CMAKE_CURRENT_SOURCE_DIR}/core/Localizer.cc"
"${CMAKE_CURRENT_SOURCE_DIR}/core/ProxyVerifier.cc"
"${CMAKE_CURRENT_SOURCE_DIR}/core/proxy_protocol_util.cc"
+ "${CMAKE_CURRENT_SOURCE_DIR}/core/socket_io.cc"
"${CMAKE_CURRENT_SOURCE_DIR}/core/verification.cc"
"${CMAKE_CURRENT_SOURCE_DIR}/core/YamlParser.cc")
@@ -29,7 +30,11 @@ target_compile_definitions(verifier-core
target_include_directories(verifier-core PUBLIC "${PROJECT_SOURCE_DIR}/src")
target_link_libraries(
verifier-core PUBLIC Threads::Threads libswoc-static yaml-cpp::yaml-cpp
- ${PV_HTTP_DEPENDENCY_TARGETS})
+ ${PV_HTTP_DEPENDENCY_TARGETS} ${PV_IO_URING_TARGET})
+
+if(PV_IO_URING_TARGET)
+ target_compile_definitions(verifier-core PRIVATE PV_HAVE_LIBURING=1)
+endif()
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
target_link_libraries(verifier-core PUBLIC stdc++fs)
diff --git a/src/client/verifier-client.cc b/src/client/verifier-client.cc
index 51ec6aaa..c7f9537a 100644
--- a/src/client/verifier-client.cc
+++ b/src/client/verifier-client.cc
@@ -12,6 +12,7 @@
#include "core/https.h"
#include "core/ProxyVerifier.h"
#include "core/YamlParser.h"
+#include "core/socket_io.h"
#include
#include
@@ -747,6 +748,16 @@ Engine::parse_args()
Errata errata;
auto args{arguments.get("run")};
+ auto const io_uring_arg = arguments.get("io-uring");
+ auto &&[socket_io_backend, socket_io_errata] =
+ configure_socket_io(io_uring_arg ? io_uring_arg[0] : "auto");
+ static_cast(socket_io_backend);
+ errata.note(std::move(socket_io_errata));
+ if (!errata.is_ok()) {
+ process_exit_code = 1;
+ return false;
+ }
+
if (args.size() < 1) {
errata.note(S_ERROR, R"("run" command requires a directory path as an argument.)");
process_exit_code = 1;
@@ -1396,6 +1407,14 @@ main(int /* argc */, char const *argv[])
"",
1,
"")
+ .add_option(
+ "--io-uring",
+ "",
+ "Select the socket readiness backend: auto, off, or required. "
+ "Default is auto.",
+ "",
+ 1,
+ "auto")
.add_option(
"--keys",
"-k",
diff --git a/src/core/http.cc b/src/core/http.cc
index 906c66a1..7d261ad6 100644
--- a/src/core/http.cc
+++ b/src/core/http.cc
@@ -6,6 +6,7 @@
*/
#include "core/http.h"
+#include "core/socket_io.h"
#include "core/verification.h"
#include "core/ProxyVerifier.h"
@@ -1394,9 +1395,7 @@ Session::poll_for_data_on_socket(chrono::milliseconds timeout, short events)
if (is_closed()) {
return {-1, Errata(S_DIAG, "Poll called on a closed connection.")};
}
- auto const timeout_ms = std::max(timeout.count(), 0);
- struct pollfd pfd = {.fd = _fd, .events = events, .revents = 0};
- return ::poll(&pfd, 1, timeout_ms);
+ return poll_for_socket_io(_fd, timeout, events);
}
swoc::Rv
diff --git a/src/core/http.h b/src/core/http.h
index f3c66679..b7cd8e99 100644
--- a/src/core/http.h
+++ b/src/core/http.h
@@ -883,8 +883,9 @@ struct Ssn
};
/** A session reader.
- * This is essentially a wrapper around a socket to support use of @c poll on
- * the socket. The goal is to enable a read operation that waits for data but
+ * This is essentially a wrapper around a socket with timed readiness waits.
+ * Linux builds use io_uring when it is available and otherwise fall back to
+ * @c poll. The goal is to enable a read operation that waits for data but
* returns as soon as any data is available.
*/
class Session
diff --git a/src/core/socket_io.cc b/src/core/socket_io.cc
new file mode 100644
index 00000000..960487fe
--- /dev/null
+++ b/src/core/socket_io.cc
@@ -0,0 +1,752 @@
+/** @file
+ * Socket readiness backend selection and polling.
+ *
+ * Copyright 2026, Verizon Media
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include "core/socket_io.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include