Pada infrastruktur backend CI/CD yang menggunakan worker emulasi RISC-V (berbasis QEMU sistem atau custom VMM layer), salah satu masalah paling sulit diisolasi adalah crash non-deterministik. Gejala khas muncul ketika guest kernel menerima beban syscall tinggi dari kompilasi paralel: kernel panik secara acak dengan pesan Instruction page fault atau Kernel access to user space without uaccess flag pada privilege mode yang tidak sesuai.

Artikel ini membahas investigasi teknis dari akar masalah debug trap handling RISC-V, di mana kegagalan sinkronisasi status Control and Status Register (CSR) saat trap bersarang (nested traps) merusak transisi privilege level dan konteks eksekusi guest.

Gejala dan Karakteristik Kerusakan

Saat menjalankan suite kompilasi intensif, runner microVM mengeksekusi puluhan thread yang memicu clone, mmap, dan I/O syscall secara agresif. Dalam kondisi ini, guest kernel Linux (RISC-V 64-bit) sewaktu-waktu melempar panik:

Unable to handle kernel paging request at virtual address 0x0000002badc0ffee
Oops [#1] PREEMPT SMP
CPU: 0 PID: 412 Comm: rustc Not tainted 6.1.0 #1
epc : 0xffffffff800091a4 ra : 0xffffffff80009120 sp : 0xfffffff0018a3d40
sstatus: 0x0000000200000100 sbadaddr: 0x0000002badc0ffee scause: 0x000000000000000c

Analisis log crash menunjukkan inkonsistensi pada bit sstatus.SPP (Supervisor Previous Privilege) dan sstatus.SPIE (Supervisor Previous Interrupt Enable). Register sstatus mengindikasikan bahwa CPU kembali ke User mode (U-mode) padahal program counter (sepc) masih mengarah ke halaman memori kernel internal, atau sebaliknya: eksekusi di U-mode membawa hak akses kernel (S-mode).

Root Cause: Korupsi CSR sstatus dan mie pada Nested Trap

Pada arsitektur RISC-V, mekanisme trap entry ke Supervisor-mode otomatis memodifikasi field pada register sstatus:

  • Bit SPP diisi dengan privilege mode sebelum trap terjadi (0 untuk U-mode, 1 untuk S-mode).
  • Bit SPIE diisi dengan nilai SIE aktif saat ini.
  • Bit SIE dinonaktifkan (di-set 0) untuk mencegah trap lanjutan secara default.

Masalah terjadi jika emulasi backend menangani asynchronous trap (seperti timer interrupt atau software interrupt IPI) tepat saat CPU sedang berada di dalam trap handler synchronous (misalnya penanganan ecall lingkungan syscall), sebelum kode perakitan kernel sempat menyimpan sstatus dan sepc ke trap frame di memori stack (pt_regs).

Jika backend hipervisor menyuntikkan virtual interrupt ke guest sebelum stack frame sepenuhnya terisolasi dan state register diamankan, modifikasi CSR oleh perangkat keras/emulatur menimpa nilai sstatus.SPP yang lama. Akibatnya, saat instruksi sret dieksekusi di akhir penanganan syscall, privilege CPU tidak kembali ke U-mode melainkan tetap di S-mode dengan address space yang korup.

Isolasi dan Tracing dengan QEMU

Langkah pertama isolasi adalah memetakan titik kegagalan menggunakan fitur tracing QEMU. Jalankan instance runner dengan flag instrumentasi CPU register dan trap execution:

qemu-system-riscv64 \
  -M virt -m 4G -smp 4 \
  -kernel Image \
  -append "console=ttyS0 root=/dev/vda rw init=/init" \
  -d int,cpu,exec \
  -D ./qemu_trap_anomaly.log

Menganalisis file qemu_trap_anomaly.log saat crash terjadi mengungkap rangkaian instruksi kritis berikut:

Trap: scause=0000000000000008 (Environment call from U-mode) epc=0x0000000010042a10
  sstatus=0x0000000200000020 -> SPP=0, SPIE=1, SIE=0
[Virtual CPU Interruption Injected by Host]
Trap: scause=8000000000000005 (Supervisor timer interrupt) epc=0xffffffff80003004
  sstatus overwrite: SPP=1, SPIE=0, SIE=0
... (Handler interrupt selesai, sret dipanggil)
Returning from trap: sstatus.SPP=1 -> CPU tetap berada di S-mode pada user context!

Reproduksi Sederhana (Minimal Trigger)

Snippet assembly berikut mereproduksi racing condition pada trap frame saat penanganan interrupt terjadi sebelum penyimpanan state selesai:

# Assembly reproduksi: simulasi window kerentanan trap frame
.globl trigger_nested_trap_bug
trigger_nested_trap_bug:
    // Masuk melalui syscall (ecall otomatis set SPP=0)
    li a7, 64        # sys_write
    ecall            # Hardware set sstatus.SPP = 0, sepc = address berikut

    // Kerentanan terjadi di trap handler entry:
    // Jika host menginjeksi interrupt sebelum CSR dibaca:
    csrr t0, sstatus # Mengambil nilai yang sudah tertimpa interrupt lain
    csrr t1, sepc
    sd t0, 0(sp)     # Trap frame korup tersimpan ke memori
    ret

Solusi: Perbaikan Sinkronisasi Trap Frame di Host Layer

Jika problem berada pada backend emulasi/virtualisasi (custom runner layer atau QEMU accelerator state injection), host tidak boleh menginjeksi trap interrupt jika state guest CPU saat ini memiliki interupsi Supervisor dimatikan (sstatus.SIE == 0) atau jika guest masih berada dalam critical section transisi CSR.

Patch pada logic injeksi interrupt backend emulasi harus memvalidasi bit penahan interrupt sebelum memaksa pengalihan context execution:

--- a/target/riscv/cpu_helper.c
+++ b/target/riscv/cpu_helper.c
@@ -89,8 +89,14 @@ bool riscv_cpu_has_interrupt_pending(CPURISCVState *env)
     uint64_t mie = env->mie;
     uint64_t mstatus = env->mstatus;
 
-    /* Cek apakah Supervisor Interrupt aktif */
-    bool s_enabled = (env->priv < PRV_S) || (env->priv == PRV_S && (mstatus & MSTATUS_SIE));
+    /* Fix: Jangan izinkan pengiriman interrupt Supervisor jika trap entry
+       sedang berlangsung atau status SIE berada dalam kondisi terkunci */
+    if (env->priv == PRV_S && !(env->sstatus & SSTATUS_SIE)) {
+        return false;
+    }
+
+    uint64_t pending = env->mip & env->mie;
+    return pending != 0;
 }

Verifikasi dan Penanganan di Sisi Guest Kernel

Di level guest kernel, pastikan trap vector (stvec) mengarah ke handler yang memprioritaskan penyimpanan scratch register (sscratch) secara atomik tanpa mengaktifkan kembali interupsi lokal sebelum seluruh isi register volatile tersimpan sempurna pada stack frame:

ENTRY(handle_exception)
    csrrw sp, CSR_SSCRATCH, sp
    bnez sp, .Lsave_context
    csrr sp, CSR_SSCRATCH
.Lsave_context:
    addi sp, sp, -CONTEXT_SIZE
    sd ra, 1*8(sp)
    sd gp, 3*8(sp)
    // Simpan CSR kritis secepat mungkin
    csrr t0, CSR_SSTATUS
    csrr t1, CSR_SEPC
    sd t0, 32*8(sp)
    sd t1, 33*8(sp)
    // Baru izinkan interrupt nesting jika didukung
    ...
END(handle_exception)

Kesimpulan

Root cause kernel panic pada microVM emulasi RISC-V di bawah beban syscall berat sering kali bukan berasal dari bug alokasi memori Linux, melainkan inkonsistensi isolasi state antara interrupt asynchronous dan exception synchronous pada layer emulasi host. Menjaga kepatuhan aturan arsitektur terkait masking CSR sstatus.SIE saat trap execution menjamin integritas transisi privilege CPU guest dan menstabilkan build runner backend.