Memahami seccomp_rule_add dengan SCMP_CMP untuk menyaring syscall berdasarkan argumennya, plus menangani multi-arsitektur x86_64, i386, dan arm64 beserta bahaya syscall multiplexing x32.

Di episode 4 kalian sudah menulis filter pertama yang menyaring berdasarkan nama syscall. Tapi menyaring hanya berdasarkan nama sering kali tidak cukup. Bayangkan syscall socket: ia dipakai untuk membuat socket TCP (AF_INET) yang menghubungkan aplikasi ke jaringan luar — sekaligus socket UNIX (AF_UNIX) yang hanya komunikasi antar proses di mesin yang sama. Kalian mungkin ingin mengizinkan satu tapi menolak yang lain — dan perbedaannya hanya terlihat dari argumen syscall tersebut.
Episode ini membahas dua kemampuan lanjutan libseccomp: arg filter dengan SCMP_CMP, dan architecture handling untuk sistem multi-arsitektur. Keduanya adalah kunci untuk menulis filter yang benar-benar presisi dan aman.
Arg filter adalah aturan yang menyaring syscall berdasarkan nilai argumennya, bukan hanya namanya. Ingat kembali seccomp_data dari episode 2: kernel menyediakan enam argumen syscall (args[0] sampai args[5]) kepada program BPF. libseccomp mengekspos ini lewat parameter SCMP_CMP pada seccomp_rule_add.
seccomp_rule_add(ctx, action, syscall, argc, SCMP_CMP(...), ...);Argumen argc menyatakan berapa banyak kondisi argumen yang harus dipenuhi (bisa 0, 1, atau lebih). Jika argc lebih dari nol, setiap kondisi SCMP_CMP(...) harus bernilai benar — ibarat rangkaian AND.
Satu batasan penting yang wajib kalian pahami sejak awal: kernel seccomp hanya membandingkan nilai argumen, bukan isi memori yang ditunjuk argumen. Kalian bisa membandingkan nilai pointer-nya, tapi tidak bisa membaca string yang dituju pointer tersebut. Untuk menyaring berdasarkan isi path file (misalnya "hanya boleh membuka /etc/passwd"), kalian butuh mekanisme lain seperti Landlock — bukan seccomp murni.
SCMP_CMP mendukung enam operator perbandingan. Bentuk umumnya SCMP_CMP(index, op, ...) dengan index adalah posisi argumen (0 sampai 5):
| Operator | Makna | Contoh Penggunaan |
|---|---|---|
SCMP_CMP_EQ | arg == datum_a | socket hanya jika domain-nya AF_UNIX |
SCMP_CMP_NE | arg != datum_a | clone hanya jika bukan flag CLONE_NEWUSER |
SCMP_CMP_LT | arg < datum_a | nilai fd di bawah ambang tertentu |
SCMP_CMP_LE | arg <= datum_a | batas atas sebuah nilai |
SCMP_CMP_GT | arg > datum_a | nilai melebihi ambang |
SCMP_CMP_GE | arg >= datum_a | batas bawah sebuah nilai |
SCMP_CMP_MASKED_EQ | (arg & datum_a) == datum_b | memeriksa flag tertentu tanpa peduli bit lain |
Mari kita bangun filter yang lebih presisi: default-deny, tapi izinkan socket hanya jika domain-nya AF_UNIX (nilai 1). Socket AF_INET dan lainnya akan ditolak dengan EPERM.
#include <seccomp.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/socket.h>
int main(void) {
scmp_filter_ctx ctx;
int fd;
/* 1. Default: tolak semua dengan EPERM */
ctx = seccomp_init(SCMP_ACT_ERRNO(EPERM));
if (ctx == NULL) {
perror("seccomp_init");
return 1;
}
/* 2. Izinkan socket hanya untuk domain AF_UNIX */
if (seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(socket), 1,
SCMP_CMP(0, SCMP_CMP_EQ, AF_UNIX)) != 0) {
perror("seccomp_rule_add socket");
return 1;
}
/* 3. Izinkan menulis ke stdout agar program bisa mencetak */
if (seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 1,
SCMP_CMP(1, SCMP_CMP_EQ, STDOUT_FILENO)) != 0) {
perror("seccomp_rule_add write");
return 1;
}
/* 4. Izinkan keluar dengan bersih */
if (seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0) != 0) {
perror("seccomp_rule_add exit_group");
return 1;
}
/* 5. Pasang filter */
if (seccomp_load(ctx) != 0) {
perror("seccomp_load");
return 1;
}
seccomp_release(ctx);
/* 6. Pengujian */
fd = socket(AF_INET, SOCK_STREAM, 0);
printf("socket AF_INET -> %s\n", fd < 0 ? strerror(errno) : "berhasil");
fd = socket(AF_UNIX, SOCK_STREAM, 0);
printf("socket AF_UNIX -> %s\n", fd < 0 ? strerror(errno) : "berhasil");
return 0;
}Mari bedah bagian arg filternya. Pada aturan socket:
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(socket), 1,
SCMP_CMP(0, SCMP_CMP_EQ, AF_UNIX));SCMP_SYS(socket) — syscall yang disaring.1 — jumlah kondisi argumen.SCMP_CMP(0, SCMP_CMP_EQ, AF_UNIX) — argumen ke-0 (domain) harus sama dengan AF_UNIX.Artinya: socket diizinkan hanya jika argumen pertamanya sama dengan AF_UNIX. Socket dengan domain lain jatuh ke default action EPERM.
Pada aturan write, kita melakukan hal yang sama untuk argumen ke-1 (fd):
SCMP_CMP(1, SCMP_CMP_EQ, STDOUT_FILENO)write diizinkan hanya jika file descriptornya adalah stdout — tidak ada syscall lain yang bisa menulis ke file mana pun selain stdout. Ini contoh pola "least privilege" yang sangat ketat.
Kompilasi dan jalankan:
gcc -o filter-socket filter_socket.c -lseccomp
./filter-socket
socket AF_INET -> Operation not permitted
socket AF_UNIX -> berhasilTip
Perhatikan output di atas: panggilan socket(AF_INET, ...) gagal dengan EPERM, sementara socket(AF_UNIX, ...) berhasil. Filter seccomp kalian sekarang membedakan dua panggilan ke syscall yang sama berdasarkan argumennya — inilah kekuatan arg filter.
Sekarang mari lihat SCMP_CMP_MASKED_EQ dalam aksi. Bayangkan kita ingin hanya mengizinkan openat dengan mode read-only. Pada Linux, bit 0-1 dari argumen flags adalah mode akses: O_RDONLY (0), O_WRONLY (1), O_RDWR (2). Kita bisa memblokir setiap openat yang meminta akses tulis:
#include <fcntl.h>
/* deny-list: tolak openat yang meminta akses tulis */
/* (arg & (O_WRONLY|O_RDWR)) == 0 berarti read-only */
seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EACCES), SCMP_SYS(openat), 1,
SCMP_CMP(2, SCMP_CMP_MASKED_EQ,
O_WRONLY | O_RDWR, O_WRONLY | O_RDWR));Bagian SCMP_CMP(2, SCMP_CMP_MASKED_EQ, O_WRONLY | O_RDWR, O_WRONLY | O_RDWR) berarti: jika (flags & (O_WRONLY | O_RDWR)) == (O_WRONLY | O_RDWR) — yaitu jika bit mode akses menandakan tulis — maka syscall ditolak. Karena kita memakai deny-list (default SCMP_ACT_ALLOW), openat dengan O_RDONLY tetap diizinkan.
Warning
Sekali lagi perlu ditegaskan: seccomp tidak bisa memeriksa isi path file yang dibuka, karena ia hanya membaca nilai argumen (pointer), bukan memori yang ditunjuk. Jika kebutuhan kalian adalah membatasi path tertentu secara isi, gunakan mekanisme berbasis path seperti Landlock atau AppArmor — seccomp fokus pada syscall dan argumennya.
Filter seccomp bersifat spesifik terhadap arsitektur: setiap arsitektur punya nomor syscall yang berbeda, dan program BPF harus tahu arsitektur mana yang sedang berjalan. Inilah kenapa seccomp_data menyertakan field arch.
Ketika kalian membuat context dengan seccomp_init, libseccomp secara default mengikuti native ABI — arsitektur utama dari proses yang sedang berjalan (misalnya x86_64). Kalian bisa menambahkan arsitektur lain dengan seccomp_arch_add:
#include <seccomp.h>
/* Context mengikuti arsitektur native (misal x86_64) */
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ERRNO(EPERM));
/* Tambahkan dukungan untuk 32-bit i386 dan arm64 */
seccomp_arch_add(ctx, SCMP_ARCH_X86);
seccomp_arch_add(ctx, SCMP_ARCH_AARCH64);Setiap aturan yang kalian tambahkan otomatis diterapkan ke semua arsitektur yang terdaftar — libseccomp menangani pemetaan nomor syscall antar arsitektur secara internal. Inilah alasan kalian tidak perlu menulis prolog pengecekan arsitektur dengan tangan (kita melihatnya saat disassemble di episode 4).
Sekarang bagian yang sering diabaikan tapi berbahaya: ABI x32. Linux x86_64 sebenarnya punya tiga ABI yang bisa dipakai satu proses:
read menjadi nomor syscall 0x40000000 + 0).Bahayanya jelas: jika filter kalian mengizinkan read pada ABI x86_64, sebuah program jahat bisa memanggil syscall yang sama melalui ABI x32 — dan jika filter tidak memeriksa arsitektur, aturan itu mungkin di-bypass. Syscall multiplexing seperti ini adalah celah klasik dalam filter seccomp yang ditulis tangan. libseccomp menangani ini secara otomatis: ketika kalian mendaftarkan SCMP_ARCH_X86_64, libseccomp juga menambahkan handling untuk ABI x32 di prolog BPF — memastikan syscall x32 tidak lolos dari aturan yang ditujukan untuk x86_64. Inilah salah satu alasan terkuat untuk selalu memakai libseccomp, bukan menulis BPF manual.
Important
Jika kalian menemukan kode yang "menulis filter seccomp dengan BPF tangan" tanpa penanganan arsitektur, anggap filter itu tidak aman. Periksa selalu apakah arsitektur dicek (biasanya lewat instruksi A = arch pada program BPF) dan bagaimana ABI x32 diperlakukan. libseccomp melakukan semuanya ini secara otomatis.
Di episode 5 ini kalian sudah menguasai dua kemampuan lanjutan:
SCMP_CMP: EQ, NE, LT, LE, GT, GE, dan MASKED_EQ — menyaring syscall berdasarkan nilai argumen.socket hanya untuk AF_UNIX, dan membatasi write hanya ke stdout.SCMP_ARCH_X86 / SCMP_ARCH_AARCH64, dan bahaya syscall multiplexing x32 yang ditangani otomatis oleh libseccomp.Dengan kemampuan ini, kalian sudah bisa menulis filter seccomp yang presisi dan aman untuk aplikasi sendiri. Di episode 6 berikutnya kita akan membahas fitur paling modern: seccomp user notification (SECCOMP_RET_NOTIFY) — membiarkan supervisor di user-space mengambil keputusan atas syscall tertentu secara dinamis, dan bagaimana ini mengubah cara kita merancang sandbox. Sampai jumpa!