Mempelajari mekanisme SECCOMP_RET_USER_NOTIF: syscall tidak diputuskan oleh kernel secara statis, melainkan ditengahi proses supervisor lewat listener fd dan ioctl SECCOMP_IOCTL_NOTIF. Lengkap dengan contoh kode supervisor dan kapan sebaiknya memakai mekanisme ini.

Di episode 5, kalian mempelajari argfilter dan architecture handling — cara membuat keputusan lebih presisi di dalam BPF: memeriksa nomor syscall sekaligus argumennya, dan menangani perbedaan arsitektur. Namun semua keputusan itu punya satu sifat yang sama: dikunci saat filter di-load. Setelah filter masuk ke kernel, perilakunya statis — ia tidak bisa bertanya kepada siapa pun, tidak bisa menyesuaikan diri dengan kondisi runtime.
Episode 6 ini memperkenalkan pengecualian besar: Seccomp User Notification (NOTIFY). Dengan mekanisme ini, sebuah syscall tidak lagi diputuskan oleh aturan yang beku, melainkan bisa ditangguhkan dan ditanyakan ke proses supervisor yang hidup. Inilah yang membuka pintu untuk userspace sandboxing dengan intervensi supervisor-dalam-loop.
Bayangkan perbedaannya begini. Klub malam dengan daftar pengunjung statis: penjaga pintu memutuskan sendiri, tidak bisa bertanya ke mana pun. Itulah seccomp biasa. Seccomp NOTIFY adalah klub yang penjaganya punya radio: tamu yang tidak ada di daftar tidak langsung ditolak atau diterima — penjaga menghubungi ruang kontrol, dan keputusan datang dari sana. Ruang kontrol itu adalah supervisor.
Return action SECCOMP_RET_USER_NOTIF mengubah perjalanan syscall secara fundamental. Saat filter menghasilkan action ini, kernel melakukan tiga hal: menangguhkan thread yang memanggil syscall, membuat event notifikasi, dan membangunkan supervisor lewat listener fd.
Supervisor lalu membaca event tersebut, memeriksa konteks syscall, dan mengirim balasan. Ada dua bentuk balasan di struct seccomp_notif_resp:
error != 0 — syscall tidak dijalankan sama sekali, target langsung menerima errno sesuai nilai error.error == 0 — syscall dijalankan seperti biasa, dan nilai val menjadi return value-nya.Perhatikan poin penting: jika supervisor memutuskan mengizinkan, itu bukan "jalan terus tanpa syarat" — kernel tetap mengeksekusi syscall-nya. Supervisor hanyalah hakim, bukan eksekutor. Dan dalam rantai prioritas action, USER_NOTIF berada di atas ALLOW (di bawah TRAP), jadi syscall yang cocok dengan aturan notify akan selalu ditanyakan dulu.
Note
Karena thread target ditangguhkan selama menunggu balasan, kecepatan supervisor adalah biaya langsung dari mekanisme ini. Setiap milidetik yang dihabiskan supervisor menunda syscall target. Ini bukan mekanisme untuk syscall hot-path — tapi untuk syscall yang memang butuh pengawasan.
Ketika supervisor menerima notifikasi lewat SECCOMP_IOCTL_NOTIF_RECV, kernel mengisi struct seccomp_notif. Dua bagian yang paling sering dibaca:
struct seccomp_notif {
__u64 id; /* id unik notifikasi ini */
__u32 pid; /* pid target (dalam root userns) */
__u32 flags; /* belum dipakai, baca sebagai 0 */
struct seccomp_data data; /* nr + args + arch dari syscall */
};Bagian data persis snapshot struct seccomp_data yang sudah kalian kenal dari episode 2 dan 5: arch, syscall_nr, args[6], dan instruction_pointer. Supervisor bisa membaca argumen syscall tanpa perlu menebak — kernel menyalinnya sebelum thread ditangguhkan.
Untuk balasan, strukturnya lebih sederhana:
struct seccomp_notif_resp {
__u64 id; /* harus sama dengan id permintaan */
__s64 val; /* return value jika error == 0 */
__s32 error;/* 0 = eksekusi syscall, selain itu errno */
__u32 flags;/* untuk SECCOMP_USER_NOTIF_FLAG_* */
};Aturan mainnya: id wajib dikopi dari permintaan, dan hanya satu dari error/val yang benar-benar berarti — keduanya tidak bisa dipakai bersamaan.
Secara ringkas, alurnya seperti ini:
SECCOMP_FILTER_FLAG_NEW_LISTENER lewat syscall seccomp(2) (kernel 5.0 ke atas). Kernel membuat listener fd dan mengembalikannya.SECCOMP_IOCTL_NOTIF_RECV pada listener fd.id dengan SECCOMP_IOCTL_NOTIF_ID_VALID untuk memastikan notifikasi masih berlaku — ini melindungi dari race TOCTOU.SECCOMP_IOCTL_NOTIF_SEND.Ada satu ioctl bonus: SECCOMP_IOCTL_NOTIF_ADDFD (kernel 5.9) memungkinkan supervisor menyuntikkan fd ke target — berguna untuk pola di mana target butuh file descriptor yang hanya dimiliki supervisor, seperti membukakan file rahasia untuk target tertentu saja.
Mari kita lihat bentuk nyatanya. Pertama, sisi target — filter yang mengarahkan read ke user notification sekaligus membuat listener fd:
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <stddef.h>
#include <sys/syscall.h>
#include <unistd.h>
static int install_filter(void) {
struct sock_filter insn[] = {
BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
offsetof(struct seccomp_data, nr)),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_read, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_USER_NOTIF),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
};
struct sock_fprog prog = {
.len = sizeof(insn) / sizeof(insn[0]),
.filter = insn,
};
return syscall(SYS_seccomp, SECCOMP_SET_MODE_FILTER,
SECCOMP_FILTER_FLAG_NEW_LISTENER, &prog);
}Filter di atas membaca nomor syscall; jika itu read, hasilnya SECCOMP_RET_USER_NOTIF — sisanya ALLOW. Nilai kembalian fungsi ini adalah listener fd, yang kemudian diteruskan ke supervisor.
Sekarang sisi supervisor — loop utama yang menunggu, menilai, dan memutuskan:
#include <errno.h>
#include <linux/seccomp.h>
#include <stdio.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
static void supervise(int listener) {
for (;;) {
struct seccomp_notif req = { 0 };
struct seccomp_notif_resp resp = { 0 };
if (ioctl(listener, SECCOMP_IOCTL_NOTIF_RECV, &req) < 0) {
perror("RECV");
return;
}
resp.id = req.id;
if (req.data.nr == __NR_read && req.data.args[0] == 0) {
resp.error = -EACCES; /* baca dari stdin diblokir */
} else {
resp.error = 0; /* izinkan, syscall dieksekusi */
resp.val = 0;
}
ioctl(listener, SECCOMP_IOCTL_NOTIF_SEND, &resp);
}
}Bacalah baris-baris ini pelan-pelan: RECV memblokir sampai ada notifikasi, keputusan diambil dari isi req.data, dan SEND membebaskan thread target. Sederhana secara mekanik, tetapi di sinilah seluruh kebijakan runtime hidup — supervisor bisa membaca konfigurasi, menelusuri policy, bahkan meminta persetujuan manusia.
Important
Supervisor adalah garis hidup target. Jika supervisor mati atau menutup listener fd, syscall yang sedang menunggu akan dibatalkan (umumnya dengan ENOSYS), dan syscall baru yang seharusnya ditanyakan juga gagal. Pastikan supervisor dijaga — misalnya dijalankan sebagai daemon di bawah systemd dengan Restart=always. Di sisi target, jangan pernah menganggap notifikasi selalu datang; selalu siapkan jalur fallback.
Mekanisme ini punya tiga perilaku yang wajib kalian pahami agar tidak menulis supervisor yang rapuh:
SECCOMP_IOCTL_NOTIF_ID_VALID. Antara saat notifikasi diterima dan saat balasan dikirim, target bisa saja di-kill atau berganti identitas. Sebelum mengambil tindakan eksternal (misalnya membukakan fd lewat ADDFD), validasi id dulu. Jangan mengirim SEND untuk id yang sudah kedaluwarsa.SECCOMP_USER_NOTIF_FLAG_CONTINUE (kernel 5.5). Jika supervisor memutuskan target boleh jalan tanpa modifikasi apa pun, ia bisa mengirim balasan dengan flag CONTINUE alih-alih error == 0. Perbedaannya halus tapi penting: dengan CONTINUE, kernel menjalankan syscall persis seperti tanpa filter — tanpa nilai val yang ditimpa. Gunakan ini ketika balasan kalian tidak mengubah apa pun.USER_NOTIF yang aktif untuk syscall itu. USER_NOTIF juga membutuhkan no_new_privs atau privilege — pastikan target memenuhi syarat sebelum memanggil seccomp(2).Warning
Jangan menjadikan USER_NOTIF sebagai lapisan deny yang lambat. Pola yang benar: syscall yang jelas-jelas berbahaya tetap di-deny langsung dengan ERRNO atau KILL, dan hanya syscall yang memang butuh keputusan kontekstual yang dialihkan ke supervisor. Filter yang mengirim SEMUA syscall ke supervisor akan membuat aplikasi berjalan sepelan siput.
Mekanisme ini mahal, jadi gunakan dengan tujuan yang jelas:
Jangan pakai NOTIFY untuk syscall yang dipanggil ribuan kali per detik — biayanya context switch dua arah per syscall. Untuk deny murni, ERRNO tetap juara. Detail lengkap tersedia di man 2 seccomp bagian SECCOMP_USER_NOTIF.
Di episode 6 ini kalian telah memahami bahwa seccomp tidak harus statis: SECCOMP_RET_USER_NOTIF memindahkan keputusan ke proses supervisor lewat listener fd dan tiga ioctl inti — RECV, ID_VALID, dan SEND. Kalian juga melihat bentuk nyata target dan supervisor dalam C, memahami peran error versus val dalam balasan, serta tahu kapan mekanisme ini layak dipakai.
Kunci yang harus dibawa pulang:
error != 0 membatalkan syscall; error == 0 menjalankannya dengan val.Semakin kompleks filter yang kalian bangun, semakin besar kebutuhan untuk melihat apa yang sebenarnya terjadi, bukan menebak. Di episode 7, kita masuk ke Debugging & Testing Filters: membongkar BPF dengan seccomp-tools, mengamati syscall dengan strace, memakai SECCOMP_RET_LOG untuk audit, dan menyusun test suite deny behavior dengan strategi fallback bila filter ternyata salah.