Error -EFAULT (Bad Address / kode -14) pada io_uring terjadi saat kernel Linux mencoba mengakses pointer virtual memory pengguna yang tidak valid, tidak terpetakan, atau berada di luar batas izin page table. Pada arsitektur backend network bersurat tinggi, pola magic ring buffer (circular buffer virtual via aliasing ganda memfd_create dan mmap) sering dipadukan dengan io_uring registered buffers (IORING_REGISTER_BUFFERS) untuk menghindari alokasi memori berulang dan memangkas overhead get_user_pages().

Masalah muncul ketika pointer baca/tulis melintasi batas siklus (wrap-around boundary). Operasi normal pengguna membaca data secara kontinu tanpa kendala, namun operasi IORING_OP_WRITE_FIXED tiba-tiba mengembalikan -EFAULT secara parsial.

Gejala: EFAULT Saat Melintasi Boundary

Pola magic ring buffer memetakan memori fisik/file descriptor yang sama ke dua rentang virtual memory yang berdampingan secara kontigu:

[ Virtual Page A (0..N-1) ][ Virtual Page B (0..N-1) ]
       \                         /
        \--> [ Physical Memory ] <--/

Pengguna dapat menulis atau membaca payload yang melintasi indeks N-1 menuju N secara transparan tanpa melakukan split copy manual. Namun saat buffer tersebut didaftarkan ke kernel via io_uring_register_buffers dengan panjang N (atau 2N), pengiriman data yang beririsan dengan titik temu rentang virtual memicu kegagalan eksekusi CQE (Completion Queue Entry) dengan nilai res = -14.

Akar Masalah: Kernel Page Pinning vs Memory Aliasing

Saat mendaftarkan buffer melalui IORING_REGISTER_BUFFERS, kernel Linux menjalankan io_sqe_buffer_register() yang memanggil pin_user_pages() untuk mengunci struct page fisik ke dalam RAM. Kernel membangun array internal berupa struct io_mapped_ubuf yang merekam basis alamat linear awal dan panjang total byte yang didaftarkan.

Ketika IORING_OP_WRITE_FIXED dipanggil:

  • Kernel memverifikasi apakah addr + len berada di dalam rentang buffer yang didaftarkan pada indeks tabel terkait.
  • Jika pengguna hanya mendaftarkan setengah bagian pertama (panjang N) untuk menghemat slot registrasi, offset yang melintasi N akan langsung ditolak sebelum submit.
  • Jika pengguna mendaftarkan keseluruhan rentang 2N, kernel mengasumsikan rentang memori tersebut adalah blok linear kontigu independen. Jika aliasing virtual memory dibuat via dua panggilan mmap terpisah, pemetaan vm_area_struct (VMA) di kernel terpecah. Validasi page pinning internal io_uring mendeteksi diskontinuitas VMA atau offset page array yang tidak cocok dengan struktur referensi pin awal, menghasilkan return value -EFAULT.

Reproduksi Bug

Berikut implementasi C minimal yang menunjukkan pendaftaran buffer dan eksekusi write yang gagal saat wrap-around:

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#include <liburing.h>

#define BUFFER_SIZE (4096 * 4) // 16KB

int main(void) {
    int fd = memfd_create("ring_buf", 0);
    ftruncate(fd, BUFFER_SIZE);

    // Alokasikan address space virtual sebesar 2 x BUFFER_SIZE
    uint8_t *addr = mmap(NULL, 2 * BUFFER_SIZE, PROT_NONE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
    
    // Petakan fisik fd ke Page A dan Page B berdampingan
    mmap(addr, BUFFER_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, fd, 0);
    mmap(addr + BUFFER_SIZE, BUFFER_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, fd, 0);

    struct io_uring ring;
    io_uring_queue_init(8, &ring, 0);

    // Daftarkan hanya base buffer berukuran BUFFER_SIZE
    struct iovec iov = { .iov_base = addr, .iov_len = BUFFER_SIZE };
    io_uring_register_buffers(&ring, &iov, 1);

    // Payload berada di ujung boundary, melintasi wrap-around (misal offset 14KB sepanjang 4KB)
    uint64_t offset = BUFFER_SIZE - 2048;
    uint32_t len = 4096;

    struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
    // Menulis ke STDOUT (fd 1)
    io_uring_prep_write_fixed(sqe, 1, addr + offset, len, 0, 0);
    io_uring_submit(&ring);

    struct io_uring_cqe *cqe;
    io_uring_wait_cqe(&ring, &cqe);
    
    // cqe->res akan bernilai -14 (-EFAULT)
    if (cqe->res < 0) {
        fprintf(stderr, "I/O Error: %s (code %d)\n", strerror(-cqe->res), cqe->res);
    }

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

Metode Tracing: strace dan bpftrace

Untuk memvalidasi letak kegagalan registrasi atau eksekusi, gunakan tracing kernel runtime.

1. Tracing Menggunakan strace

strace -e io_uring_enter,io_uring_register ./app

Perhatikan parameter io_uring_enter saat submission. Jika CQE mengembalikan -14 tanpa memicu syscall context switch failure, kegagalan berada di tahap penanganan kernel ring worker (asinkron).

2. Tracing Kernel Function Menggunakan bpftrace

Jalankan bpftrace untuk memeriksa fungsi pin buffer pada kernel io_uring:

bpftrace -e '
kprobe:io_import_fixed {
    printf("PID: %d, buf_index: %d, len: %d\n", pid, arg2, arg3);
}
kretprobe:io_import_fixed {
    if (retval != 0) {
        printf("io_import_fixed failed with error: %d\n", retval);
    }
}'

Nilai return negatif dari io_import_fixed mengonfirmasi bahwa addr + len ditolak oleh kernel logic karena melewati rentang io_mapped_ubuf.

Solusi Perbaikan

Untuk mempertahankan throughput tinggi tanpa terbentur limitasi address space aliasing kernel, terapkan salah satu dari dua solusi berikut:

Solusi 1: Splitting I/O Request Sebelum Boundary (Rekomendasi)

Jika segmen buffer melintasi indeks batas ring buffer, potong payload menjadi dua SQE terpisah yang dieksekusi secara terangkai menggunakan flag IOSQE_IO_LINK.

uint64_t tail = current_offset;
uint32_t len = total_len;

if (tail + len > BUFFER_SIZE) {
    uint32_t first_chunk = BUFFER_SIZE - tail;
    uint32_t second_chunk = len - first_chunk;

    struct io_uring_sqe *sqe1 = io_uring_get_sqe(ring);
    io_uring_prep_write_fixed(sqe1, target_fd, base_addr + tail, first_chunk, file_offset, buf_index);
    sqe1->flags |= IOSQE_IO_LINK;

    struct io_uring_sqe *sqe2 = io_uring_get_sqe(ring);
    io_uring_prep_write_fixed(sqe2, target_fd, base_addr, second_chunk, file_offset + first_chunk, buf_index);
} else {
    struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
    io_uring_prep_write_fixed(sqe, target_fd, base_addr + tail, len, file_offset, buf_index);
}

Solusi 2: Fallback ke IORING_OP_WRITEV Tanpa Fixed Buffers

Bila arsitektur tidak memungkinkan pemecahan SQE, gunakan buffer non-fixed standar (IORING_OP_WRITEV atau IORING_OP_WRITE). Kernel akan memanggil get_user_pages() saat I/O berjalan. Mode ini mengenali alias virtual memory secara terpisah di level VMA pengguna dan tidak memicu -EFAULT, dengan trade-off sedikit peningkatan overhead per request submission.