Membuat server web dalam assembly aarch64 untuk memberi hidup saya sedikit (kekurangan) makna
(imtomt.github.io)- 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 berbasisContent-Length PUTlebih dulu menulis ke file sementara.ymawky_tmp_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,cmpberkorespondensi langsung dengan byte dalam binary eksekusi svc #0x80adalah bentuk yang bisa dibaca manusia dari byteD4 00 10 01dalam binary eksekusi- Karena tidak ada tipe string, string ada sebagai area byte berurutan di memori, dan karena juga tidak ada fitur bahasa seperti
structdi 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 kex8 - Nomor syscall
open()adalah5, dan setelah argumen seperti nama file dan mode diletakkan langsung ke register, kernel dipanggil dengansvc #0x80 - Jika
open()gagal, carry flag akan diset dan kegagalan diproses dengan branch ke kode penanganan gagal, sepertib.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, atauDELETE - 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
PUTke file sementara dan membuat header serta body respons - Menutup file yang terbuka dan menangani error agar server tidak crash
- Menentukan apakah metode permintaan adalah
Mengimplementasikan parsing HTTP secara manual
-
Baris permintaan dan akhir header
- Permintaan HTTP adalah string yang harus diinterpretasikan server, contohnya seperti berikut
GET /index.html HTTP/1.0\r\n Range: bytes=1-5\r\n\r\n - Baris pertama berisi permintaan
GET, file targetindex.html, dan versi HTTPHTTP/1.0 \r\nadalah akhir baris, dan\r\n\r\nadalah akhir header- Jika
\r\n\r\ntidak diterima, pemrosesan harus dihentikan dengan400 Bad Request
- Permintaan HTTP adalah string yang harus diinterpretasikan server, contohnya seperti berikut
-
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/dalamHTTP/1.0sebagai path, byte sebelum/diperiksa apakah berupa spasi - Misalnya, pada
GET HTTP/1.0\r\n\r\n, ada/di dalamHTTP/1.0, jadi jika byte sebelumnya bukan spasi maka server mengembalikan400 Bad Request - Karena
PATH_MAXpada kebanyakan sistem adalah 4096 byte, ymawky menyediakanfilename_buffer: .skip 4097untuk buffer nama file 4096 byte dan 1 byte terminator null - Jika path permintaan lebih panjang daripada buffer, server harus mengembalikan
414 URI Too Longalih-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 valid0-9,a-f,A-F, lalu dikonversi ke nilai byte yang sesuai GETbisa memiliki headerRange:, danPUTmembutuhkanContent-Length:- Header-header ini tidak berada pada posisi tetap seperti URL permintaan, sehingga seluruh header harus ditelusuri karakter demi karakter
- Jika setelah
\rtidak ada\n, atau jika\nmuncul tanpa\rsebelumnya, header dianggap tidak valid dan400 Bad Requestdikembalikan - Jika baris header baru dimulai dengan spasi, maka field header dianggap tidak valid karena tidak boleh dimulai dengan spasi, sehingga
400 Bad Requestdikembalikan
- Saat menemukan
-
Perbandingan string dan konversi angka
- Untuk mencari
Range:atauContent-Length:, dibuat fungsistreqnyang menerima dua pointer stringx0,x1dan panjang maksimumx2, lalu membandingkannya per karakter - Header
Range:dapat menghilangkan salah satu dari nilai awal atau akhir seperti berikut, tetapi setidaknya salah satunya harus adaRange: bytes=10- Range: bytes=-10 Range: bytes=5-10 - Karena nilai rentang berupa string, dibutuhkan fungsi bergaya
atoiuntuk mengubah digit ASCII menjadi integer - Untuk menghindari overflow register 64-bit, angka dengan panjang 19 digit atau lebih diperlakukan sebagai error
- Pekerjaan yang setara dengan
int(string)di Python pun di assembly mengharuskan implementasi manual atas pemeriksaan digit, perkalian, penjumlahan, dan sinyal sukses/gagal berbasis carry flag
- Untuk mencari
Penanganan PUT dan strategi file sementara
PUTadalah metode idempoten (idempotent), sehingga mengirim permintaan yang sama berkali-kali tetap menghasilkan keadaan akhir server yang samaPUT /file.txtmembuatfile.txtatau menimpa file yang ada sepenuhnya, dan mengirim1234dua kali tidak membuat isi file menjadi12341234, melainkan tetap1234- Membuka
PUTsecara global bisa berbahaya, dan masalah yang perlu dipertimbangkan saat menanganinya antara lain- Proses crash saat sedang menangani permintaan
- Klien mengatakan
Content-Lengthadalah 2KB tetapi hanya mengirim 100 byte - Klien mengirim
Content-Lengthyang sangat besar seperti 50GB
MAX_BODY_SIZEdiconfig.Sbernilai default 1GB, dan jikaContent-Lengthmelebihi itu maka server mengembalikan413 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 diperoleh dengan syscall
getpid()bernomor20, lalu dikonversi menjadi string denganitoa()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()10atauunlinkat()472 - File yang sudah ada hanya akan ditimpa setelah permintaan lengkap berhasil ditransfer
Daftar direktori dan pemrosesan escape
- Saat menerima permintaan
GET /somedir/, server memeriksa apakahALLOW_DIR_LISTINGdiconfig.Saktif - Jika daftar direktori dinonaktifkan, server mengembalikan
403 Forbidden - Jika aktif, buffer informasi file untuk direktori yang diminta diisi dengan syscall
getdirentries64()344 - Buffer tersebut berisi nama setiap file dan panjang nama file, dan ymawky memanfaatkannya untuk menghasilkan HTML yang dapat diklik
- Bentuk dasar yang dikirim ke klien untuk tiap file adalah sebagai berikut
filename - Nama file di dalam
href="..."harus di-percent-encode sebagai segmen path URL, sedangkan teks yang terlihat di body harus di-escape HTML - Jika nama file adalah `&.-~>](%26.-~%3E%3Cfoo)
- Dengan begitu, nama seperti
something evilyang bisa memicu XSS di area body, maupun nama seperti\">something dastardlyyang bisa memicu XSS di areahref="...", tidak akan dieksekusi karena sudah dienkode
Keamanan jaringan dan timeout
- slowloris adalah serangan denial-of-service yang membuka banyak koneksi dan tidak pernah menyelesaikan permintaan, sehingga resource server tertahan
- Karena memakai struktur fork-on-request, ymawky dapat rentan terhadap slowloris
- Jika seluruh header tidak diterima dalam
HEADER_REQ_TIMEOUT_SECSdiconfig.S, server mengirim408 Request Timeoutlalu menutup koneksi - Jika saat menerima body permintaan klien terlalu lama tidak mengirim data, pemrosesannya dilakukan dengan cara yang sama berdasarkan
RECV_TIMEOUTdiconfig.S - Timeout sederhana per operasi baca saja tidak cukup
- Klien jahat bisa mengirim
Content-Length: 1073741823lalu mengirim 1 byte setiap 9 detik; karena panjang konten itu 1 byte di bawah batas maksimum maka tetap diizinkan, dan dengan timeout 10 detik per interval server bisa menunggu lebih dari 300 tahun
- Klien jahat bisa mengirim
- Untuk mengurangi hal ini, ymawky menghitung timeout berdasarkan
Content-Lengthdan jumlah byte minimum per detiktimeout = grace_period + content_length / min_bps grace_periodadalah waktu minimum yang diberikan untuk semua body, danmin_bpsadalah laju transfer paling lambat yang masih diizinkan server- Nilai default
min_bpsadalah 16KB/s, cukup longgar tetapi tidak tak terbatas - Pendekatan ini tidak sepenuhnya mencegah serangan denial-of-service, tetapi membatasi lamanya serangan tertentu dapat menahan resource
Keamanan filesystem
-
Urutan pemeriksaan informasi file
- Pada
GETdanHEAD, ymawky membuka path yang diminta lebih dulu lalu menjalankan syscallfstat64()339terhadap file descriptor untuk memperoleh informasi seperti jenis dan ukuran file - Jika syscall
stat64()338dijalankan 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
- Pada
-
Docroot dan pencegahan path traversal
- Semua path permintaan diawali dengan docroot
- Docroot default adalah
www/, yaituDEFAULT_DIRdiconfig.S - Permintaan
/etc/shadowakan menjadiwww/etc/shadow, sehingga akan menghasilkan 404 kecualiwww/etc/shadowbenar-benar ada - Namun
/../../../../etc/shadowmenjadiwww/../../../../etc/shadowdan 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%2Emenjadi..setelah decoding, pemeriksaan ini harus dilakukan setelah percent decoding
-
Penanganan symbolic link
- Flag
O_NOFOLLOWdi POSIX membuatopen()gagal jika komponen path terakhir adalah symbolic link O_NOFOLLOW_ANYdi 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
- Flag
Perilaku khusus Apple
-
Penanganan timeout dan
sigaction()- Untuk mengimplementasikan timeout permintaan, syscall
setitimer()83digunakan agarSIGALRMdikirim setelah waktu tertentu - Secara default
SIGALRMakan mematikan child, tetapi ymawky perlu lebih dulu mengirim408 Request Timeout - Untuk itu digunakan syscall
sigaction()46 - Struktur
sigactionmentah di Darwin mengekspos fieldsa_tramp - Biasanya libc mengatur
sa_trampuntuk menyimpan stack dan register, menyiapkansigreturn, 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_handlerdansigreturn
- Untuk mengimplementasikan timeout permintaan, syscall
-
proc_info()dan pembatasan jumlah child process- Apple memiliki syscall
proc_info()336yang kurang terdokumentasi tetapi bisa digunakan untuk mengambil informasi proses yang sedang berjalan dan child-nya - Panggilan ini biasanya dipakai oleh alat seperti
ps,lsof, dantop - 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 dengan503 Service Unavailable
- Apple memiliki syscall
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