- ymawky adalah server HTTP statis kecil untuk macOS yang ditulis hanya dengan assembly aarch64, dan hanya menggunakan syscall mentah Darwin tanpa wrapper libc
- Mendukung
GET, HEAD, PUT, OPTIONS, DELETE, permintaan rentang byte, daftar direktori, dan halaman error kustom, tetapi ini bukan pengganti nginx melainkan implementasi yang menanggalkan lapisan kenyamanan untuk memahami cara kerja server web
- Mulai dari parsing permintaan, decoding persen, pemeriksaan header, konversi nilai rentang, penanganan error, penutupan file, hingga pembuatan respons, semuanya harus ditulis sendiri; pekerjaan yang setara dengan pemisahan string sederhana atau
int(string) di Python menjadi puluhan hingga ratusan baris kode validasi di assembly
- Server ini memakai struktur fork-on-request yang memanggil
fork() untuk setiap koneksi baru, sehingga mudah diimplementasikan tetapi throughput koneksi simultannya rendah dan bisa rentan terhadap slowloris; karena itu diterapkan timeout header dan timeout body berbasis Content-Length
PUT lebih dulu menulis ke file sementara .ymawky_tmp_<pid> lalu menggantinya saat berhasil, dan keamanan filesystem seperti pencegahan path traversal, O_NOFOLLOW_ANY, fstat64(), serta encoding URL dan escape HTML pada daftar direktori ditangani langsung
Gambaran umum dan batasan ymawky
- ymawky adalah server HTTP statis kecil untuk macOS yang ditulis hanya dengan assembly aarch64
- Hanya menggunakan syscall mentah Darwin tanpa wrapper libc, dan tidak memakai library eksternal maupun parser yang sudah ada
- Fitur yang didukung adalah
GET, HEAD, PUT, OPTIONS, DELETE, permintaan rentang byte, daftar direktori, dan halaman error kustom
- Batasan proyeknya adalah sebagai berikut
- aarch64 assembly only
- target macOS/Darwin
- raw syscalls only, tanpa wrapper libc
- static files only
- tanpa parser yang sudah ada
- tanpa library eksternal
- Tujuannya bukan untuk menggantikan nginx, melainkan implementasi yang menanggalkan lapisan kenyamanan agar bisa memahami bagaimana server web benar-benar bekerja
Pekerjaan yang diperlukan saat membuat server web dengan assembly
- Assembly adalah lapisan antara bahasa mesin dan bahasa tingkat tinggi, dan instruksi seperti
mov, add, ldr, str, cmp berkorespondensi langsung dengan byte dalam binary eksekusi
svc #0x80 adalah bentuk yang bisa dibaca manusia dari byte D4 00 10 01 dalam binary eksekusi
- Karena tidak ada tipe string, string ada sebagai area byte berurutan di memori, dan karena juga tidak ada fitur bahasa seperti
struct di C, offset field dan ukuran total harus diketahui sendiri
- Karena tidak ada library HTTP, pembersihan otomatis, exception, atau object, pekerjaan seperti parsing permintaan, penanganan error, menutup file, dan membuat respons harus ditulis semuanya secara manual
- Meski berjalan salah, CPU tetap mengeksekusi tanpa peringatan, jadi masalahnya ada pada instruksi dan akses memori yang ditulis sendiri
Syscall mentah dan alur server
-
Syscall Darwin
- ymawky memanggil kernel secara langsung alih-alih memakai wrapper libc
- Pada Darwin aarch64, nomor syscall dimasukkan ke register
x16, sedangkan pada Linux aarch64 ke x8
- Nomor syscall
open() adalah 5, dan setelah argumen seperti nama file dan mode diletakkan langsung ke register, kernel dipanggil dengan svc #0x80
- Jika
open() gagal, carry flag akan diset dan kegagalan diproses dengan branch ke kode penanganan gagal, seperti b.cs open_failed
-
Operasi dasar server
- Alur dasar server web adalah menerima permintaan, memprosesnya, lalu mengembalikan status code dan file yang diperlukan
- Penyiapan socket terdiri dari langkah seperti
socket(AF_INET, SOCK_STREAM, 0), setsockopt(... SO_REUSEADDR ...), bind(sockfd, &addr, 16), listen(sockfd, 5), accept(sockfd, NULL, NULL)
- ymawky adalah server fork-on-request yang memanggil
fork() untuk setiap koneksi baru
- Cara ini mudah dipahami dan diimplementasikan karena memori tidak dibagi antar penanganan permintaan, tetapi overhead-nya lebih besar karena ruang memori per proses dan throughput koneksi simultannya lebih rendah dibanding model event-driven asynchronous non-blocking milik nginx
- Saat koneksi simultan bertambah, kernel akan menghabiskan lebih banyak waktu untuk berpindah proses daripada menjalankan kode di dalam proses
-
Pekerjaan yang diperlukan saat menangani permintaan
- Menentukan apakah metode permintaan adalah
GET, HEAD, OPTIONS, PUT, atau DELETE
- Mengekstrak path permintaan dan mendekode percent-encoding seperti
%20
- Melakukan pemeriksaan keamanan path dan mem-parsing field header yang dikirim klien
- Mengambil informasi file yang diminta dan membedakan apakah itu direktori atau file biasa
- Menulis body permintaan
PUT ke file sementara dan membuat header serta body respons
- Menutup file yang terbuka dan menangani error agar server tidak crash
Mengimplementasikan parsing HTTP secara manual
-
Baris permintaan dan akhir header
-
Ekstraksi path
- ymawky menentukan jenis permintaan dengan membandingkan metode yang didukung dan byte-byte awal, lalu mengekstrak path
- Header dipindai satu byte demi satu untuk menemukan
/ atau *, tetapi untuk menghindari salah mengira / dalam HTTP/1.0 sebagai path, byte sebelum / diperiksa apakah berupa spasi
- Misalnya, pada
GET HTTP/1.0\r\n\r\n, ada / di dalam HTTP/1.0, jadi jika byte sebelumnya bukan spasi maka server mengembalikan 400 Bad Request
- Karena
PATH_MAX pada kebanyakan sistem adalah 4096 byte, ymawky menyediakan filename_buffer: .skip 4097 untuk buffer nama file 4096 byte dan 1 byte terminator null
- Jika path permintaan lebih panjang daripada buffer, server harus mengembalikan
414 URI Too Long alih-alih menimpa memori sembarang
- Pekerjaan yang kira-kira setara dengan
text.split("GET /")[1].split(" ")[0] di Python menjadi sekitar 200 baris di assembly karena sekaligus mencakup pemeriksaan validitas HTTP
-
Percent decoding dan pemeriksaan field header
- Saat menemukan
% dalam path, dua byte berikutnya diperiksa apakah merupakan digit heksadesimal valid 0-9, a-f, A-F, lalu dikonversi ke nilai byte yang sesuai
GET bisa memiliki header Range:, dan PUT membutuhkan Content-Length:
- Header-header ini tidak berada pada posisi tetap seperti URL permintaan, sehingga seluruh header harus ditelusuri karakter demi karakter
- Jika setelah
\r tidak ada \n, atau jika \n muncul tanpa \r sebelumnya, header dianggap tidak valid dan 400 Bad Request dikembalikan
- Jika baris header baru dimulai dengan spasi, maka field header dianggap tidak valid karena tidak boleh dimulai dengan spasi, sehingga
400 Bad Request dikembalikan
-
Perbandingan string dan konversi angka
Penanganan PUT dan strategi file sementara
PUT adalah metode idempoten (idempotent), sehingga mengirim permintaan yang sama berkali-kali tetap menghasilkan keadaan akhir server yang sama
PUT /file.txt membuat file.txt atau menimpa file yang ada sepenuhnya, dan mengirim 1234 dua kali tidak membuat isi file menjadi 12341234, melainkan tetap 1234
- Membuka
PUT secara global bisa berbahaya, dan masalah yang perlu dipertimbangkan saat menanganinya antara lain
- Proses crash saat sedang menangani permintaan
- Klien mengatakan
Content-Length adalah 2KB tetapi hanya mengirim 100 byte
- Klien mengirim
Content-Length yang sangat besar seperti 50GB
MAX_BODY_SIZE di config.S bernilai default 1GB, dan jika Content-Length melebihi itu maka server mengembalikan 413 Content Too Large
- Jika file yang ada langsung dibuka untuk ditulis, kegagalan dapat meninggalkan file yang hanya setengah tertulis, sehingga ymawky menulis lebih dulu ke file sementara dengan nama
.ymawky_tmp_<pid>
- PID diperoleh dengan syscall
getpid() bernomor 20, lalu dikonversi menjadi string dengan itoa() buatan sendiri sambil memeriksa buffer overflow
- Jika seluruh body klien berhasil ditulis ke file sementara, file sementara itu diganti namanya ke nama final sehingga file permintaan muncul di server
- Jika klien memutus koneksi secara tak terduga, terjadi timeout, atau body yang dikirim tidak valid, file sementara dihapus dengan syscall
unlink() 10 atau unlinkat() 472
- File yang sudah ada hanya akan ditimpa setelah permintaan lengkap berhasil ditransfer
Daftar direktori dan pemrosesan escape
Keamanan jaringan dan timeout
Keamanan filesystem
-
Urutan pemeriksaan informasi file
- Pada
GET dan HEAD, ymawky membuka path yang diminta lebih dulu lalu menjalankan syscall fstat64() 339 terhadap file descriptor untuk memperoleh informasi seperti jenis dan ukuran file
- Jika syscall
stat64() 338 dijalankan pada path terlebih dulu baru kemudian file dibuka, maka bisa terjadi TOCTOU race condition di mana file berubah di antara waktu pemeriksaan dan waktu penggunaan
-
Docroot dan pencegahan path traversal
- Semua path permintaan diawali dengan docroot
- Docroot default adalah
www/, yaitu DEFAULT_DIR di config.S
- Permintaan
/etc/shadow akan menjadi www/etc/shadow, sehingga akan menghasilkan 404 kecuali www/etc/shadow benar-benar ada
- Namun
/../../../../etc/shadow menjadi www/../../../../etc/shadow dan bisa diinterpretasikan keluar dari docroot, sehingga diperlukan pertahanan tambahan
- ymawky tidak sekadar menolak semua path yang mengandung string
.., melainkan menolak jika segmen path tepat bernilai ..
- Karena
%2E%2E menjadi .. setelah decoding, pemeriksaan ini harus dilakukan setelah percent decoding
-
Penanganan symbolic link
- Flag
O_NOFOLLOW di POSIX membuat open() gagal jika komponen path terakhir adalah symbolic link
O_NOFOLLOW_ANY di Darwin membuat operasi gagal jika komponen mana pun dalam path adalah symbolic link
- Jika symbolic link tertentu bisa ditanam di dalam docroot maka kemungkinan besar sudah ada masalah lain, tetapi flag ini tetap memberi lapisan pertahanan tambahan
Perilaku khusus Apple
-
Penanganan timeout dan sigaction()
- Untuk mengimplementasikan timeout permintaan, syscall
setitimer() 83 digunakan agar SIGALRM dikirim setelah waktu tertentu
- Secara default
SIGALRM akan mematikan child, tetapi ymawky perlu lebih dulu mengirim 408 Request Timeout
- Untuk itu digunakan syscall
sigaction() 46
- Struktur
sigaction mentah di Darwin mengekspos field sa_tramp
- Biasanya libc mengatur
sa_tramp untuk menyimpan stack dan register, menyiapkan sigreturn, lalu bercabang ke handler
- Handler timeout pada ymawky mengirim
408 Request Timeout, menutup hal-hal yang diperlukan, lalu keluar dari child sehingga tidak perlu kembali
- Karena itu, slot trampoline diarahkan langsung ke kode yang menjalankan respons timeout, dengan melewati
sa_handler dan sigreturn
-
proc_info() dan pembatasan jumlah child process
- Apple memiliki syscall
proc_info() 336 yang kurang terdokumentasi tetapi bisa digunakan untuk mengambil informasi proses yang sedang berjalan dan child-nya
- Panggilan ini biasanya dipakai oleh alat seperti
ps, lsof, dan top
- ymawky menggunakan
proc_info() untuk menghitung jumlah child process yang aktif
- Karena jumlah koneksi maksimum dapat dikonfigurasi, server perlu mengetahui berapa banyak child yang masih hidup
proc_info() menulis informasi child process ke buffer, dan karena ukuran tiap elemen diketahui, jumlah child dapat dihitung dari total byte yang ditulis
- Jika jumlah child melebihi
MAX_PROCS, koneksi baru ditolak dengan 503 Service Unavailable
Kesimpulan dan informasi proyek
- Pada server web statis, bagian yang sulit bukanlah membuka socket dan melakukan listen, melainkan parsing permintaan dan menangani semua kondisi batas
- Permintaan, path, dan respons semuanya adalah byte; permintaan rentang harus tepat, dan nama file harus di-escape secara berbeda tergantung posisinya
- Assembly memaksa semua pekerjaan seperti parsing permintaan, manajemen memori, penanganan error, konversi string, timeout, dan keamanan file untuk ditulis sendiri
- ymawky dikelola oleh imtomt
1 komentar
Pendapat di Lobste.rs
Luar biasa. Dulu saya pernah mengerjakan integrasi dengan perusahaan kecil pembuat perangkat pintar, dan satu-satunya engineer di sana ternyata hanya paham bahasa assembly
Mulai dari kode kontrol hardware, sistem operasi server, sampai JSON web API yang kami pakai, semuanya ditulis sendiri langsung dalam assembly
Suatu kali kami menemukan bug di mana web API mengembalikan data dari perangkat yang salah, dan ternyata penyebabnya adalah kesalahan off-by-one di sistem penjadwalan OS sehingga “database”-nya mengembalikan baris yang keliru ke layanan web
Kalau membahas ungkapan seperti “bunuh diri”, tolong pasang peringatan konten. Atau lebih baik lagi, jangan disebut sama sekali
Setelah melihat komentar ini saya cari lagi, tapi tetap tidak menemukannya; apa saya melewatkan sesuatu?
Mendengar bahwa semuanya “ditulis sepenuhnya dalam assembly” membuat saya teringat pada laporan investigasi Therac-25