Skip to content

Does IORING_OP_MSG_RING provide happens-before synchronization? #1514

@hn-sl

Description

@hn-sl

Summary

TSan reports a data race when using msg_ring for cross-thread communication. I'd like to know:

  1. Does msg_ring guarantee happens-before between sender's submit and receiver's CQE?
  2. If yes, could TSan annotations be added to liburing to suppress this false positive?
  3. If no, what is the recommended synchronization pattern?

Environment

  • Kernel: 6.14.0-37-generic
  • liburing: 2.11, 2.12, 2.13, master (2.14) — all exhibit the same issue
  • Compiler: clang 21.1.0 with -fsanitize=thread

Reproducer

#include <liburing.h>
#include <thread>
#include <latch>
#include <iostream>

int main() {
    std::cout << "liburing version: "
              << IO_URING_VERSION_MAJOR << "."
              << IO_URING_VERSION_MINOR << std::endl;

    std::latch ready(1);
    int target_fd = -1;
    int shared = 0;
    int result = 0;

    std::jthread receiver([&] {
        io_uring ring{};
        io_uring_queue_init(64, &ring, IORING_SETUP_DEFER_TASKRUN | IORING_SETUP_SINGLE_ISSUER);

        target_fd = ring.ring_fd;
        ready.count_down();

        io_uring_cqe *cqe;
        io_uring_wait_cqe(&ring, &cqe);

        result = shared;  // TSan: READ (line 26)

        io_uring_cqe_seen(&ring, cqe);
        io_uring_queue_exit(&ring);
    });

    std::jthread sender([&] {
        io_uring ring{};
        io_uring_queue_init(64, &ring, IORING_SETUP_DEFER_TASKRUN | IORING_SETUP_SINGLE_ISSUER);

        ready.wait();

        shared = 42;  // TSan: WRITE (line 38)

        auto *sqe = io_uring_get_sqe(&ring);
        io_uring_prep_msg_ring(sqe, target_fd, 0, 0, 0);
        io_uring_submit(&ring);

        io_uring_queue_exit(&ring);
    });

    receiver.join();
    sender.join();

    std::cout << "result: " << result << " (expected: 42)" << std::endl;
    return 0;
}
clang++ -std=c++20 -fsanitize=thread -o main main.cpp -luring -lpthread
./main

TSan Output

liburing version: 2.14
==================
WARNING: ThreadSanitizer: data race (pid=165909)
  Read of size 4 at 0x7fffffffdcd8 by thread T1:
    #0 main::$_0::operator()() const main.cpp:26

  Previous write of size 4 at 0x7fffffffdcd8 by thread T2:
    #0 main::$_1::operator()() const main.cpp:38

SUMMARY: ThreadSanitizer: data race main.cpp:26 in main::$_0::operator()() const
==================
result: 42 (expected: 42)

Expected Behavior

Sender (T2)                          Receiver (T1)
───────────                          ─────────────
shared = 42;
    │
    ▼
io_uring_submit(msg_ring)
    │
    └───── kernel ─────►  io_uring_wait_cqe() returns
                                  │
                                  ▼
                              result = shared;  // should see 42

I expect msg_ring submit → CQE delivery to establish happens-before, making all prior writes visible to the receiver. Is this correct?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions