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'