From 5b90a552d90c18e84524ea268269705344aacc3e Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Wed, 29 Jul 2026 11:20:29 -0700 Subject: [PATCH 1/6] X-Smart-Branch-Parent: main From a28ab9a263f0c709d7fc0340a0b26b087c42afcd Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Thu, 30 Jul 2026 09:34:44 -0700 Subject: [PATCH 2/6] Added a test for io_uring Assisted-by: Claude Code (claude-opus-4-6) --- tests/io_uring_write.c | 87 +++++++++++++++++++ tests/io_uring_write_raw.c | 167 +++++++++++++++++++++++++++++++++++++ tests/test_io_uring.py | 91 ++++++++++++++++++++ 3 files changed, 345 insertions(+) create mode 100644 tests/io_uring_write.c create mode 100644 tests/io_uring_write_raw.c create mode 100644 tests/test_io_uring.py diff --git a/tests/io_uring_write.c b/tests/io_uring_write.c new file mode 100644 index 00000000..b6e5df15 --- /dev/null +++ b/tests/io_uring_write.c @@ -0,0 +1,87 @@ +/* + * Helper that uses io_uring to write content to a file. + * All I/O (open, write, close) goes through io_uring, + * bypassing the normal syscall path. + * + * Usage: io_uring_write + * + * Exit codes: + * 0 - success + * 1 - usage error + * 2 - io_uring not available + * 3 - I/O error + */ +#include +#include +#include +#include + +int main(int argc, char *argv[]) +{ + struct io_uring ring; + struct io_uring_sqe *sqe; + struct io_uring_cqe *cqe; + int ret, fd; + + if (argc != 3) { + fprintf(stderr, "Usage: %s \n", argv[0]); + return 1; + } + + ret = io_uring_queue_init(8, &ring, 0); + if (ret < 0) { + fprintf(stderr, "io_uring_queue_init: %s\n", strerror(-ret)); + return 2; + } + + /* Open file via io_uring */ + sqe = io_uring_get_sqe(&ring); + io_uring_prep_openat(sqe, AT_FDCWD, argv[1], O_WRONLY | O_TRUNC, 0); + io_uring_submit(&ring); + ret = io_uring_wait_cqe(&ring, &cqe); + if (ret < 0) { + fprintf(stderr, "wait openat: %s\n", strerror(-ret)); + goto err; + } + if (cqe->res < 0) { + fprintf(stderr, "openat: %s\n", strerror(-cqe->res)); + io_uring_cqe_seen(&ring, cqe); + goto err; + } + fd = cqe->res; + io_uring_cqe_seen(&ring, cqe); + + /* Write content via io_uring */ + sqe = io_uring_get_sqe(&ring); + io_uring_prep_write(sqe, fd, argv[2], strlen(argv[2]), 0); + io_uring_submit(&ring); + ret = io_uring_wait_cqe(&ring, &cqe); + if (ret < 0) { + fprintf(stderr, "wait write: %s\n", strerror(-ret)); + goto err; + } + if (cqe->res < 0) { + fprintf(stderr, "write: %s\n", strerror(-cqe->res)); + io_uring_cqe_seen(&ring, cqe); + goto err; + } + io_uring_cqe_seen(&ring, cqe); + + /* Close file via io_uring */ + sqe = io_uring_get_sqe(&ring); + io_uring_prep_close(sqe, fd); + io_uring_submit(&ring); + ret = io_uring_wait_cqe(&ring, &cqe); + if (ret < 0) { + fprintf(stderr, "wait close: %s\n", strerror(-ret)); + goto err; + } + io_uring_cqe_seen(&ring, cqe); + + io_uring_queue_exit(&ring); + return 0; + +err: + io_uring_queue_exit(&ring); + return 3; +} diff --git a/tests/io_uring_write_raw.c b/tests/io_uring_write_raw.c new file mode 100644 index 00000000..51938684 --- /dev/null +++ b/tests/io_uring_write_raw.c @@ -0,0 +1,167 @@ +/* + * Helper that uses raw io_uring syscalls to write content to a file. + * No liburing dependency — only uses io_uring_setup/io_uring_enter + * syscalls directly, so it can be statically linked. + * + * Usage: io_uring_write_raw + * + * Exit codes: + * 0 - success + * 1 - usage error + * 2 - io_uring not available + * 3 - I/O error + */ +#include +#include +#include +#include +#include +#include +#include +#include + +struct ring { + int fd; + struct io_uring_sqe *sqes; + unsigned *sq_tail; + unsigned *sq_mask; + unsigned *sq_array; + struct io_uring_cqe *cqes; + unsigned *cq_head; + unsigned *cq_tail; + unsigned *cq_mask; +}; + +static int ring_init(struct ring *r, unsigned entries) +{ + struct io_uring_params p; + + memset(&p, 0, sizeof(p)); + + int fd = syscall(__NR_io_uring_setup, entries, &p); + if (fd < 0) + return -1; + + size_t sq_sz = p.sq_off.array + p.sq_entries * sizeof(unsigned); + size_t cq_sz = p.cq_off.cqes + + p.cq_entries * sizeof(struct io_uring_cqe); + size_t sqe_sz = p.sq_entries * sizeof(struct io_uring_sqe); + + void *sq = mmap(NULL, sq_sz, PROT_READ | PROT_WRITE, + MAP_SHARED | MAP_POPULATE, fd, IORING_OFF_SQ_RING); + void *cq = mmap(NULL, cq_sz, PROT_READ | PROT_WRITE, + MAP_SHARED | MAP_POPULATE, fd, IORING_OFF_CQ_RING); + void *sqes = mmap(NULL, sqe_sz, PROT_READ | PROT_WRITE, + MAP_SHARED | MAP_POPULATE, fd, IORING_OFF_SQES); + + if (sq == MAP_FAILED || cq == MAP_FAILED || sqes == MAP_FAILED) { + close(fd); + return -1; + } + + r->fd = fd; + r->sqes = sqes; + r->sq_tail = sq + p.sq_off.tail; + r->sq_mask = sq + p.sq_off.ring_mask; + r->sq_array = sq + p.sq_off.array; + r->cqes = cq + p.cq_off.cqes; + r->cq_head = cq + p.cq_off.head; + r->cq_tail = cq + p.cq_off.tail; + r->cq_mask = cq + p.cq_off.ring_mask; + + return 0; +} + +static struct io_uring_sqe *get_sqe(struct ring *r) +{ + unsigned tail = __atomic_load_n(r->sq_tail, __ATOMIC_RELAXED); + unsigned idx = tail & *r->sq_mask; + struct io_uring_sqe *sqe = &r->sqes[idx]; + + memset(sqe, 0, sizeof(*sqe)); + return sqe; +} + +static int submit_and_wait(struct ring *r, struct io_uring_cqe **cqe) +{ + unsigned tail = __atomic_load_n(r->sq_tail, __ATOMIC_RELAXED); + unsigned idx = tail & *r->sq_mask; + + r->sq_array[idx] = idx; + __atomic_store_n(r->sq_tail, tail + 1, __ATOMIC_RELEASE); + + int ret = syscall(__NR_io_uring_enter, r->fd, 1, 1, + IORING_ENTER_GETEVENTS, NULL, 0); + if (ret < 0) + return -1; + + unsigned head = __atomic_load_n(r->cq_head, __ATOMIC_RELAXED); + + *cqe = &r->cqes[head & *r->cq_mask]; + return 0; +} + +static void cqe_advance(struct ring *r) +{ + unsigned head = __atomic_load_n(r->cq_head, __ATOMIC_RELAXED); + + __atomic_store_n(r->cq_head, head + 1, __ATOMIC_RELEASE); +} + +int main(int argc, char *argv[]) +{ + struct ring r; + struct io_uring_sqe *sqe; + struct io_uring_cqe *cqe; + + if (argc != 3) { + fprintf(stderr, "Usage: %s \n", argv[0]); + return 1; + } + + if (ring_init(&r, 4) < 0) { + fprintf(stderr, "io_uring_setup: %s\n", strerror(errno)); + return 2; + } + + /* Open file via io_uring */ + sqe = get_sqe(&r); + sqe->opcode = IORING_OP_OPENAT; + sqe->fd = AT_FDCWD; + sqe->addr = (unsigned long)argv[1]; + sqe->open_flags = O_WRONLY | O_TRUNC; + if (submit_and_wait(&r, &cqe) < 0 || cqe->res < 0) { + fprintf(stderr, "openat: %s\n", + strerror(-(cqe ? cqe->res : errno))); + return 3; + } + int fd = cqe->res; + cqe_advance(&r); + + /* Write content via io_uring */ + sqe = get_sqe(&r); + sqe->opcode = IORING_OP_WRITE; + sqe->fd = fd; + sqe->addr = (unsigned long)argv[2]; + sqe->len = strlen(argv[2]); + if (submit_and_wait(&r, &cqe) < 0 || cqe->res < 0) { + fprintf(stderr, "write: %s\n", + strerror(-(cqe ? cqe->res : errno))); + return 3; + } + cqe_advance(&r); + + /* Close file via io_uring */ + sqe = get_sqe(&r); + sqe->opcode = IORING_OP_CLOSE; + sqe->fd = fd; + if (submit_and_wait(&r, &cqe) < 0 || cqe->res < 0) { + fprintf(stderr, "close: %s\n", + strerror(-(cqe ? cqe->res : errno))); + return 3; + } + cqe_advance(&r); + + close(r.fd); + return 0; +} diff --git a/tests/test_io_uring.py b/tests/test_io_uring.py new file mode 100644 index 00000000..6b4090b2 --- /dev/null +++ b/tests/test_io_uring.py @@ -0,0 +1,91 @@ +from __future__ import annotations + +import os +import subprocess + +import pytest + +from event import Event, EventType, Process +from server import EventServer + +IO_URING_SRC = os.path.join(os.path.dirname(__file__), 'io_uring_write_raw.c') +IO_URING_BIN = os.path.join(os.path.dirname(__file__), 'io_uring_write_raw') + + +@pytest.fixture(scope='session') +def io_uring_helper(): + """Compile the raw io_uring helper statically. Skips if glibc-static is unavailable.""" + result = subprocess.run( + ['cc', '-static', '-o', IO_URING_BIN, IO_URING_SRC], + capture_output=True, + ) + if result.returncode != 0: + pytest.skip( + 'io_uring helper compilation failed (glibc-static missing?): ' + + result.stderr.decode() + ) + yield IO_URING_BIN + if os.path.exists(IO_URING_BIN): + os.unlink(IO_URING_BIN) + + +def test_io_uring_write( + monitored_dir: str, + server: EventServer, + io_uring_helper: str, +): + """ + Verifies that io_uring write operations modify files but are not + currently tracked by fact. + + Creates a file with 'hi', modifies it to 'bye' via io_uring + (open, write, and close all go through io_uring, bypassing the + normal syscall path), then verifies the content changed and that + only the expected creation events are captured. + """ + fut = os.path.join(monitored_dir, 'io_uring_test.txt') + process = Process.from_proc() + + # Create file with initial content via normal I/O. + with open(fut, 'w') as f: + f.write('hi') + + creation = Event( + process=process, + event_type=EventType.CREATION, + file=fut, + host_path=fut, + ) + server.wait_events([creation]) + + # Modify the file using io_uring (bypasses normal syscall path). + result = subprocess.run( + [io_uring_helper, fut, 'bye'], + capture_output=True, + ) + if result.returncode == 2: + pytest.skip( + f'io_uring not supported: {result.stderr.decode()}' + ) + assert result.returncode == 0, ( + f'io_uring write failed: {result.stderr.decode()}' + ) + + # Create a sentinel file via normal I/O to verify event ordering. + # With strict=True (the default), any unexpected event appearing + # before the sentinel would cause the test to fail. + sentinel = os.path.join(monitored_dir, 'sentinel.txt') + with open(sentinel, 'w') as f: + f.write('sentinel') + + sentinel_event = Event( + process=process, + event_type=EventType.CREATION, + file=sentinel, + host_path=sentinel, + ) + server.wait_events([sentinel_event]) + + # Verify the file was actually modified by io_uring. + with open(fut) as f: + assert f.read() == 'bye' From 807044f6612528e5cc29f4b4d78f7ee24d61031b Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Mon, 3 Aug 2026 15:06:21 -0700 Subject: [PATCH 3/6] Dockerized test --- tests/containers/io-uring/Containerfile | 11 +++ .../io-uring}/io_uring_write.c | 0 .../io-uring}/io_uring_write_raw.c | 0 tests/test_io_uring.py | 71 +++++++++++-------- 4 files changed, 53 insertions(+), 29 deletions(-) create mode 100644 tests/containers/io-uring/Containerfile rename tests/{ => containers/io-uring}/io_uring_write.c (100%) rename tests/{ => containers/io-uring}/io_uring_write_raw.c (100%) diff --git a/tests/containers/io-uring/Containerfile b/tests/containers/io-uring/Containerfile new file mode 100644 index 00000000..9ad6cda0 --- /dev/null +++ b/tests/containers/io-uring/Containerfile @@ -0,0 +1,11 @@ +FROM quay.io/centos/centos:stream9 AS builder + +RUN dnf install -y --enablerepo=crb gcc glibc-static + +WORKDIR /build +COPY io_uring_write_raw.c . +RUN cc -static -o io_uring_write_raw io_uring_write_raw.c + +FROM quay.io/centos/centos:stream9-minimal + +COPY --from=builder /build/io_uring_write_raw /usr/local/bin/ diff --git a/tests/io_uring_write.c b/tests/containers/io-uring/io_uring_write.c similarity index 100% rename from tests/io_uring_write.c rename to tests/containers/io-uring/io_uring_write.c diff --git a/tests/io_uring_write_raw.c b/tests/containers/io-uring/io_uring_write_raw.c similarity index 100% rename from tests/io_uring_write_raw.c rename to tests/containers/io-uring/io_uring_write_raw.c diff --git a/tests/test_io_uring.py b/tests/test_io_uring.py index 6b4090b2..e10f3900 100644 --- a/tests/test_io_uring.py +++ b/tests/test_io_uring.py @@ -1,38 +1,56 @@ from __future__ import annotations import os -import subprocess +import docker +import docker.models.containers +import docker.models.images import pytest from event import Event, EventType, Process from server import EventServer -IO_URING_SRC = os.path.join(os.path.dirname(__file__), 'io_uring_write_raw.c') -IO_URING_BIN = os.path.join(os.path.dirname(__file__), 'io_uring_write_raw') - @pytest.fixture(scope='session') -def io_uring_helper(): - """Compile the raw io_uring helper statically. Skips if glibc-static is unavailable.""" - result = subprocess.run( - ['cc', '-static', '-o', IO_URING_BIN, IO_URING_SRC], - capture_output=True, +def io_uring_image(docker_client: docker.DockerClient): + image, _ = docker_client.images.build( + path='containers/io-uring', + tag='io-uring:latest', + dockerfile='Containerfile', ) - if result.returncode != 0: - pytest.skip( - 'io_uring helper compilation failed (glibc-static missing?): ' - + result.stderr.decode() - ) - yield IO_URING_BIN - if os.path.exists(IO_URING_BIN): - os.unlink(IO_URING_BIN) + return image + + +@pytest.fixture +def get_io_uring_container( + io_uring_image: docker.models.images.Image, + docker_client: docker.DockerClient, + monitored_dir: str, +): + container = docker_client.containers.run( + io_uring_image.tags[0], + detach=True, + tty=True, + name='io-uring', + security_opt=['seccomp=unconfined'], + volumes={ + monitored_dir: { + 'bind': '/data', + 'mode': 'z', + }, + }, + ) + + yield container + + container.stop(timeout=1) + container.remove() def test_io_uring_write( monitored_dir: str, server: EventServer, - io_uring_helper: str, + get_io_uring_container: docker.models.containers.Container, ): """ Verifies that io_uring write operations modify files but are not @@ -58,18 +76,13 @@ def test_io_uring_write( ) server.wait_events([creation]) - # Modify the file using io_uring (bypasses normal syscall path). - result = subprocess.run( - [io_uring_helper, fut, 'bye'], - capture_output=True, - ) - if result.returncode == 2: - pytest.skip( - f'io_uring not supported: {result.stderr.decode()}' - ) - assert result.returncode == 0, ( - f'io_uring write failed: {result.stderr.decode()}' + # Modify the file using io_uring inside the container. + exit_code, output = get_io_uring_container.exec_run( + ['io_uring_write_raw', '/data/io_uring_test.txt', 'bye'], ) + if exit_code == 2: + pytest.skip(f'io_uring not supported: {output.decode()}') + assert exit_code == 0, f'io_uring write failed: {output.decode()}' # Create a sentinel file via normal I/O to verify event ordering. # With strict=True (the default), any unexpected event appearing From bc7c395927218c5c3db3093405cdec2bb37e9a29 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Mon, 3 Aug 2026 15:44:56 -0700 Subject: [PATCH 4/6] Created submit_wait function --- tests/containers/io-uring/io_uring_write.c | 54 ++++++++++------------ 1 file changed, 24 insertions(+), 30 deletions(-) diff --git a/tests/containers/io-uring/io_uring_write.c b/tests/containers/io-uring/io_uring_write.c index b6e5df15..37103fd0 100644 --- a/tests/containers/io-uring/io_uring_write.c +++ b/tests/containers/io-uring/io_uring_write.c @@ -16,11 +16,28 @@ #include #include +static int submit_wait(struct io_uring *ring, const char *op) +{ + struct io_uring_cqe *cqe; + int ret; + + io_uring_submit(ring); + ret = io_uring_wait_cqe(ring, &cqe); + if (ret < 0) { + fprintf(stderr, "wait %s: %s\n", op, strerror(-ret)); + return ret; + } + ret = cqe->res; + if (ret < 0) + fprintf(stderr, "%s: %s\n", op, strerror(-ret)); + io_uring_cqe_seen(ring, cqe); + return ret; +} + int main(int argc, char *argv[]) { struct io_uring ring; struct io_uring_sqe *sqe; - struct io_uring_cqe *cqe; int ret, fd; if (argc != 3) { @@ -37,46 +54,23 @@ int main(int argc, char *argv[]) /* Open file via io_uring */ sqe = io_uring_get_sqe(&ring); io_uring_prep_openat(sqe, AT_FDCWD, argv[1], O_WRONLY | O_TRUNC, 0); - io_uring_submit(&ring); - ret = io_uring_wait_cqe(&ring, &cqe); - if (ret < 0) { - fprintf(stderr, "wait openat: %s\n", strerror(-ret)); + fd = submit_wait(&ring, "openat"); + if (fd < 0) goto err; - } - if (cqe->res < 0) { - fprintf(stderr, "openat: %s\n", strerror(-cqe->res)); - io_uring_cqe_seen(&ring, cqe); - goto err; - } - fd = cqe->res; - io_uring_cqe_seen(&ring, cqe); /* Write content via io_uring */ sqe = io_uring_get_sqe(&ring); io_uring_prep_write(sqe, fd, argv[2], strlen(argv[2]), 0); - io_uring_submit(&ring); - ret = io_uring_wait_cqe(&ring, &cqe); - if (ret < 0) { - fprintf(stderr, "wait write: %s\n", strerror(-ret)); + ret = submit_wait(&ring, "write"); + if (ret < 0) goto err; - } - if (cqe->res < 0) { - fprintf(stderr, "write: %s\n", strerror(-cqe->res)); - io_uring_cqe_seen(&ring, cqe); - goto err; - } - io_uring_cqe_seen(&ring, cqe); /* Close file via io_uring */ sqe = io_uring_get_sqe(&ring); io_uring_prep_close(sqe, fd); - io_uring_submit(&ring); - ret = io_uring_wait_cqe(&ring, &cqe); - if (ret < 0) { - fprintf(stderr, "wait close: %s\n", strerror(-ret)); + ret = submit_wait(&ring, "close"); + if (ret < 0) goto err; - } - io_uring_cqe_seen(&ring, cqe); io_uring_queue_exit(&ring); return 0; From 28594344d4f75eb2428ff89633cb93cacde157e4 Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Mon, 3 Aug 2026 15:56:40 -0700 Subject: [PATCH 5/6] Defined int return = 3 --- tests/containers/io-uring/io_uring_write.c | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/containers/io-uring/io_uring_write.c b/tests/containers/io-uring/io_uring_write.c index 37103fd0..7e6cb1e3 100644 --- a/tests/containers/io-uring/io_uring_write.c +++ b/tests/containers/io-uring/io_uring_write.c @@ -38,7 +38,7 @@ int main(int argc, char *argv[]) { struct io_uring ring; struct io_uring_sqe *sqe; - int ret, fd; + int result = 3, ret, fd; if (argc != 3) { fprintf(stderr, "Usage: %s \n", argv[0]); @@ -72,10 +72,8 @@ int main(int argc, char *argv[]) if (ret < 0) goto err; - io_uring_queue_exit(&ring); - return 0; - + result = 0; err: io_uring_queue_exit(&ring); - return 3; + return result; } From 013c0e1750e0cbed4d6a50b7850649ecb012107e Mon Sep 17 00:00:00 2001 From: JoukoVirtanen Date: Mon, 3 Aug 2026 16:53:40 -0700 Subject: [PATCH 6/6] Removed io_uring_write.c which used liburing --- tests/containers/io-uring/io_uring_write.c | 79 ---------------------- 1 file changed, 79 deletions(-) delete mode 100644 tests/containers/io-uring/io_uring_write.c diff --git a/tests/containers/io-uring/io_uring_write.c b/tests/containers/io-uring/io_uring_write.c deleted file mode 100644 index 7e6cb1e3..00000000 --- a/tests/containers/io-uring/io_uring_write.c +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Helper that uses io_uring to write content to a file. - * All I/O (open, write, close) goes through io_uring, - * bypassing the normal syscall path. - * - * Usage: io_uring_write - * - * Exit codes: - * 0 - success - * 1 - usage error - * 2 - io_uring not available - * 3 - I/O error - */ -#include -#include -#include -#include - -static int submit_wait(struct io_uring *ring, const char *op) -{ - struct io_uring_cqe *cqe; - int ret; - - io_uring_submit(ring); - ret = io_uring_wait_cqe(ring, &cqe); - if (ret < 0) { - fprintf(stderr, "wait %s: %s\n", op, strerror(-ret)); - return ret; - } - ret = cqe->res; - if (ret < 0) - fprintf(stderr, "%s: %s\n", op, strerror(-ret)); - io_uring_cqe_seen(ring, cqe); - return ret; -} - -int main(int argc, char *argv[]) -{ - struct io_uring ring; - struct io_uring_sqe *sqe; - int result = 3, ret, fd; - - if (argc != 3) { - fprintf(stderr, "Usage: %s \n", argv[0]); - return 1; - } - - ret = io_uring_queue_init(8, &ring, 0); - if (ret < 0) { - fprintf(stderr, "io_uring_queue_init: %s\n", strerror(-ret)); - return 2; - } - - /* Open file via io_uring */ - sqe = io_uring_get_sqe(&ring); - io_uring_prep_openat(sqe, AT_FDCWD, argv[1], O_WRONLY | O_TRUNC, 0); - fd = submit_wait(&ring, "openat"); - if (fd < 0) - goto err; - - /* Write content via io_uring */ - sqe = io_uring_get_sqe(&ring); - io_uring_prep_write(sqe, fd, argv[2], strlen(argv[2]), 0); - ret = submit_wait(&ring, "write"); - if (ret < 0) - goto err; - - /* Close file via io_uring */ - sqe = io_uring_get_sqe(&ring); - io_uring_prep_close(sqe, fd); - ret = submit_wait(&ring, "close"); - if (ret < 0) - goto err; - - result = 0; -err: - io_uring_queue_exit(&ring); - return result; -}