1 पॉइंट द्वारा GN⁺ 5 시간 전 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • ymawky macOS के लिए aarch64 assembly में लिखा गया एक छोटा static HTTP server है, जो libc wrapper के बिना केवल Darwin raw system calls का उपयोग करता है
  • यह GET, HEAD, PUT, OPTIONS, DELETE, byte-range requests, directory listing, custom error pages को support करता है, लेकिन nginx का विकल्प बनने के लिए नहीं, बल्कि web server के काम करने के तरीके को समझने के लिए convenience layers हटाकर बनाया गया implementation है
  • request parsing, percent decoding, header inspection, range value conversion, error handling, file closing, response generation तक सब कुछ सीधे लिखना पड़ता है, और Python में simple string splitting या int(string) जैसा काम भी assembly में दर्जनों से सैकड़ों लाइनों के validation code में बदल जाता है
  • server हर नए connection पर fork() कॉल करने वाली fork-on-request संरचना अपनाता है, इसलिए implementation आसान है, लेकिन concurrent connections की throughput कम हो सकती है और slowloris के प्रति कमजोर हो सकता है; इसी वजह से header timeout और Content-Length आधारित body timeout लागू किए गए हैं
  • PUT पहले .ymawky_tmp_<pid> temporary file में लिखता है और सफल होने पर replace करता है, साथ ही path traversal रोकथाम, O_NOFOLLOW_ANY, fstat64(), directory listing में URL encoding और HTML escaping जैसी filesystem safety को भी सीधे handle करता है

ymawky का अवलोकन और सीमाएँ

  • ymawky macOS के लिए aarch64 assembly में लिखा गया एक छोटा static HTTP server है
  • यह libc wrapper के बिना केवल Darwin raw system calls का उपयोग करता है, और कोई external library या existing parser इस्तेमाल नहीं करता
  • supported features हैं GET, HEAD, PUT, OPTIONS, DELETE, byte-range requests, directory listing, और custom error pages
  • project की सीमाएँ इस प्रकार हैं
    • aarch64 assembly only
    • macOS/Darwin target
    • raw syscalls only, libc wrappers नहीं
    • static files only
    • preexisting parsers नहीं
    • external libraries नहीं
  • इसका उद्देश्य nginx को replace करना नहीं है, बल्कि web server वास्तव में कैसे काम करता है यह समझने के लिए convenience layers हटाकर implementation करना है

assembly में web server बनाते समय क्या-क्या करना पड़ता है

  • assembly machine code और high-level languages के बीच की layer है, और mov, add, ldr, str, cmp जैसे instructions executable binary के bytes से सीधे मेल खाते हैं
  • svc #0x80 executable binary के D4 00 10 01 bytes का human-readable रूप है
  • string type न होने के कारण strings memory में लगातार bytes के क्षेत्र के रूप में मौजूद होती हैं, और C के struct जैसी language feature भी नहीं होती, इसलिए field offsets और total size खुद जानने पड़ते हैं
  • HTTP library, automatic cleanup, exceptions, objects कुछ भी नहीं होने के कारण request parsing, error handling, file closing, response generation जैसे सारे काम सीधे लिखने पड़ते हैं
  • गलत काम करने पर भी CPU बिना warning दिए वैसे ही execute करता रहता है, इसलिए समस्या लिखे गए instructions और memory access में ही होती है

raw system calls और server flow

  • Darwin system calls

    • ymawky libc wrapper के बजाय kernel को सीधे call करता है
    • Darwin aarch64 में system call number x16 register में रखा जाता है, जबकि Linux aarch64 में x8 में
    • open() system call number 5 है, और filename व mode जैसे arguments को registers में सीधे रखकर svc #0x80 के माध्यम से kernel को call किया जाता है
    • open() fail होने पर carry flag set होता है, और b.cs open_failed की तरह carry flag जाँचकर failure-handling code पर branch किया जाता है
  • basic server behavior

    • web server का बुनियादी flow request लेना, उसे process करना, और status code व आवश्यक file वापस करना है
    • socket setup में socket(AF_INET, SOCK_STREAM, 0), setsockopt(... SO_REUSEADDR ...), bind(sockfd, &addr, 16), listen(sockfd, 5), accept(sockfd, NULL, NULL) जैसे चरण शामिल हैं
    • ymawky हर नए connection पर fork() call करने वाला fork-on-request server है
    • यह तरीका request handling के बीच memory share नहीं करता, इसलिए इसे समझना और implement करना आसान है, लेकिन process-per-memory-space के कारण overhead बढ़ता है और nginx के event-driven asynchronous non-blocking model की तुलना में concurrent connection throughput कम रहती है
    • concurrent connections बढ़ने पर kernel, process के भीतर execution से ज्यादा समय process switching में खर्च कर सकता है
  • request handling में जरूरी काम

    • request method GET, HEAD, OPTIONS, PUT, DELETE में से कौन-सी है यह पहचानना
    • request path निकालना और %20 जैसी percent encoding decode करना
    • path safety checks करना, और client द्वारा भेजे गए header fields parse करना
    • requested file की जानकारी लाकर यह तय करना कि वह directory है या regular file
    • PUT request body को temporary file में लिखना और response headers व body बनाना
    • खुले हुए files बंद करना और server crash न हो इसके लिए errors handle करना

HTTP parsing खुद implement करना

  • request line और header termination

    • HTTP request एक string है जिसे server को interpret करना होता है, उदाहरण इस प्रकार है
      GET /index.html HTTP/1.0\r\n
      Range: bytes=1-5\r\n\r\n
      
    • पहली line में GET request, target file index.html, और HTTP version HTTP/1.0 होती है
    • \r\n line ending है, और \r\n\r\n headers का अंत है
    • अगर \r\n\r\n प्राप्त नहीं होता, तो 400 Bad Request के साथ रोकना चाहिए
  • path extraction

    • ymawky supported methods और शुरुआती bytes की तुलना करके request type पहचानता है, फिर path निकालता है
    • यह headers को byte-by-byte scan करके / या * ढूँढता है, लेकिन HTTP/1.0 के अंदर के / को path न समझ ले, इसके लिए / से ठीक पहले का byte space है या नहीं यह जाँचता है
    • उदाहरण के लिए GET HTTP/1.0\r\n\r\n में HTTP/1.0 के अंदर / है, इसलिए अगर उसके पहले का byte space न हो तो 400 Bad Request लौटाया जाता है
    • अधिकांश systems में PATH_MAX 4096 bytes होता है, इसलिए ymawky में 4096-byte filename buffer और null terminator के 1 byte के लिए filename_buffer: .skip 4097 रखा गया है
    • अगर request path buffer से लंबा हो, तो arbitrary memory overwrite करने के बजाय 414 URI Too Long लौटाना चाहिए
    • Python के text.split("GET /")[1].split(" ")[0] जैसे काम को assembly में HTTP validity checks सहित लगभग 200 lines लगती हैं
  • percent decoding और header field validation

    • path में % मिलने पर अगले दो bytes 0-9, a-f, A-F के valid hex digits हैं या नहीं, यह जाँचा जाता है, और फिर उन्हें संबंधित byte value में बदला जाता है
    • GET में Range: header हो सकता है और PUT में Content-Length: आवश्यक होता है
    • ये headers request URL की तरह fixed position पर नहीं होते, इसलिए पूरे header को character-by-character traverse करना पड़ता है
    • अगर \r के बाद \n न हो, या पहले \r के बिना \n आ जाए, तो header को invalid मानकर 400 Bad Request लौटाया जाता है
    • अगर नई header line space से शुरू होती है, तो header field space से शुरू नहीं हो सकती, इसलिए 400 Bad Request लौटाया जाता है
  • string comparison और number conversion

    • Range: या Content-Length: खोजने के लिए दो string pointers x0, x1 और maximum length x2 लेकर character-by-character तुलना करने वाला streqn function लिखा गया है
    • Range: header में नीचे की तरह start या end में से कोई एक छूट सकता है, लेकिन दोनों में से कम-से-कम एक होना जरूरी है
      Range: bytes=10-
      Range: bytes=-10
      Range: bytes=5-10
      
    • range values strings होती हैं, इसलिए ASCII digits को integer में बदलने वाला atoi-style function चाहिए
    • 64-bit register overflow से बचने के लिए अगर संख्या 19 digits या उससे ज्यादा की हो तो उसे error माना जाता है
    • Python के int(string) जैसा काम भी assembly में digit validation, multiplication, addition, और carry flag आधारित success/failure signaling को सीधे implement करने की मांग करता है

PUT handling और temporary file strategy

  • PUT एक idempotent method है, यानी वही request कई बार भेजने पर भी अंतिम server state समान रहती है
  • PUT /file.txt file.txt को बनाता है या existing file को पूरी तरह overwrite करता है, और 1234 को दो बार भेजने पर file content 12341234 नहीं बल्कि 1234 रहता है
  • globally open PUT खतरनाक हो सकता है, और handling के दौरान ये समस्याएँ ध्यान में रखनी होती हैं
    • request processing के दौरान process crash हो जाना
    • client का Content-Length 2KB बताकर केवल 100 bytes भेजना
    • client का Content-Length 50GB जैसी बहुत बड़ी value भेजना
  • config.S में MAX_BODY_SIZE default रूप से 1GB है, और Content-Length इससे ज्यादा होने पर 413 Content Too Large लौटाया जाता है
  • existing file को सीधे खोलकर लिखने पर failure की स्थिति में आधी-लिखी file रह सकती है, इसलिए ymawky पहले .ymawky_tmp_<pid> format की temporary file में लिखता है
  • getpid() system call number 20 से pid लिया जाता है, और custom itoa() से उसे string में बदला जाता है, साथ ही buffer overflow की जाँच भी होती है
  • client body को temporary file में पूरा लिखने और सफलता मिलने पर temporary file को in-place नाम से replace किया जाता है, जिससे requested file server पर बनती है
  • अगर client अनपेक्षित रूप से connection तोड़ दे, timeout हो जाए, या invalid body भेजे, तो temporary file को unlink() system call 10 या unlinkat() system call 472 से delete कर दिया जाता है
  • existing file को केवल तभी overwrite किया जाता है जब पूरी request सफलतापूर्वक transfer हो जाए

directory listing और escaping

  • GET /somedir/ request मिलने पर config.S में ALLOW_DIR_LISTING enabled है या नहीं यह जाँचा जाता है
  • अगर directory listing disabled हो, तो 403 Forbidden लौटाया जाता है
  • enabled होने पर getdirentries64() system call 344 से requested directory की file info buffer भरी जाती है
  • buffer में हर file का नाम और filename length शामिल होती है, और ymawky इन्हीं से clickable HTML बनाता है
  • हर file के लिए client को भेजा जाने वाला basic form इस प्रकार है
    <a href="filename">filename</a>
    
  • href="..." के अंदर filename को URL path segment के रूप में percent-encode करना पड़ता है, और screen पर दिखने वाले text को HTML-escape करना पड़ता है
  • अगर filename &.-~><foo हो, तो href %26.-~%3E%3Cfoo और display text &amp;.-~&gt;&lt;foo बनता है, और अंतिम output इस प्रकार होता है
    <a href="%26.-~%3E%3Cfoo">&amp;.-~&gt;&lt;foo</a>
    
  • <script>something evil</script> जैसे body area में XSS संभव बनाने वाले नाम, या "><script>something dastardly</script> जैसे href="..." area में XSS संभव बनाने वाले नाम भी execute न हों, इसके लिए encoding की जाती है

network security और timeouts

  • slowloris एक denial-of-service attack है जो बहुत सारे connections खोलकर requests को पूरा नहीं करता और server resources को बाँधे रखता है
  • ymawky fork-on-request architecture होने के कारण slowloris के प्रति कमजोर हो सकता है
  • अगर पूरा header config.S के HEADER_REQ_TIMEOUT_SECS के भीतर receive नहीं होता, तो 408 Request Timeout भेजकर connection बंद कर दिया जाता है
  • request body receive करते समय अगर client बहुत देर तक data न भेजे, तो config.S के RECV_TIMEOUT के अनुसार वही प्रक्रिया अपनाई जाती है
  • केवल per-read timeout पर्याप्त नहीं होता
    • कोई malicious client Content-Length: 1073741823 भेजे और हर 9 seconds में 1 byte भेजे, तो content length maximum से 1 byte कम होने के कारण स्वीकार हो जाएगी, और 10-second timeout में 300 साल से अधिक इंतजार करना पड़ सकता है
  • इसे कम करने के लिए ymawky Content-Length और minimum bytes-per-second के आधार पर timeout की गणना करता है
    timeout = grace_period + content_length / min_bps
    
  • grace_period हर body के लिए दिया गया minimum समय है, और min_bps वह सबसे धीमी transfer speed है जिसे server अनुमति देता है
  • default min_bps 16KB/s है, जो उदार है लेकिन अनंत नहीं
  • यह तरीका denial-of-service attack को पूरी तरह नहीं रोकता, लेकिन कुछ हमलों में resources के बँधे रहने का समय सीमित करता है

filesystem safety

  • file information check का क्रम

    • GET और HEAD में requested path खोलने के बाद file descriptor पर fstat64() system call 339 चलाकर file type और size जैसी जानकारी ली जाती है
    • अगर पहले path पर stat64() system call 338 चलाया जाए और उसके बाद file खोली जाए, तो check और use के बीच file बदल जाने वाली TOCTOU race condition पैदा हो सकती है
  • docroot और path traversal रोकथाम

    • हर request path के आगे docroot जोड़ा जाता है
    • default docroot config.S के DEFAULT_DIR यानी www/ है
    • /etc/shadow request www/etc/shadow बन जाती है, इसलिए जब तक www/etc/shadow वास्तव में मौजूद न हो, 404 मिलेगा
    • लेकिन /../../../../etc/shadow www/../../../../etc/shadow बनकर docroot के बाहर resolve हो सकती है, इसलिए अतिरिक्त बचाव जरूरी है
    • ymawky केवल .. string वाले हर path को reject नहीं करता, बल्कि केवल उन path segments को reject करता है जो ठीक-ठीक .. हों
    • %2E%2E decode होने के बाद .. बनता है, इसलिए यह जाँच percent decoding के बाद करनी चाहिए
  • symbolic links handling

    • POSIX का O_NOFOLLOW flag यह सुनिश्चित करता है कि अगर अंतिम path component symbolic link हो, तो open() fail हो जाए
    • Darwin का O_NOFOLLOW_ANY यह सुनिश्चित करता है कि path का कोई भी component symbolic link हो, तो call fail हो जाए
    • अगर कोई docroot के भीतर खास symbolic link डाल सकता है, तो शायद पहले से कोई और बड़ी समस्या मौजूद है, लेकिन यह flag अतिरिक्त सुरक्षा देता है

Apple-specific behavior

  • timeout handling और sigaction()

    • request timeout implement करने के लिए setitimer() system call 83 से कुछ समय बाद SIGALRM भेजना पड़ता है
    • default रूप से SIGALRM child को मार देता है, लेकिन ymawky पहले 408 Request Timeout भेजना चाहता है
    • इसके लिए sigaction() system call 46 का उपयोग होता है
    • Darwin का raw sigaction struct sa_tramp field को expose करता है
    • सामान्यतः libc sa_tramp set करके stack और registers save करता है, sigreturn तैयार करता है, और फिर handler पर branch करता है
    • ymawky का timeout handler 408 Request Timeout भेजता है, जरूरी चीजें बंद करता है, और child को exit कर देता है, इसलिए उसे return करने की जरूरत नहीं होती
    • इसलिए trampoline slot को सीधे timeout response चलाने वाले code की ओर point कराया जाता है, और sa_handlersigreturn को bypass किया जाता है
  • proc_info() और child process count limit

    • Apple में running processes और उनके child की जानकारी लेने के लिए कम documented proc_info() system call 336 उपलब्ध है
    • यह call आम तौर पर ps, lsof, top जैसे tools में उपयोग होती है
    • ymawky active child processes की गिनती के लिए proc_info() का उपयोग करता है
    • maximum connections configurable हैं, इसलिए live child count जानना जरूरी है
    • proc_info() child process information को buffer में लिखता है, और हर element का size ज्ञात होने के कारण written bytes से child count निकाला जा सकता है
    • अगर child count MAX_PROCS से ऊपर हो जाए, तो नए connections को 503 Service Unavailable के साथ reject किया जाता है

निष्कर्ष और project जानकारी

  • static web server में कठिन हिस्सा sockets खोलना और listen करना नहीं, बल्कि request parsing और सभी edge cases को संभालना था
  • requests, paths, responses सब bytes हैं; range requests को सटीक होना पड़ता है और filenames को उनकी position के हिसाब से अलग-अलग escape करना पड़ता है
  • assembly request parsing, memory management, error handling, string conversion, timeouts, और file safety जैसे हर काम को सीधे लिखने पर मजबूर करती है
  • ymawky को imtomt maintain करते हैं

1 टिप्पणियां

 
GN⁺ 5 시간 전
Lobste.rs की राय
  • कमाल है। पहले मैंने एक छोटी कंपनी के साथ इंटीग्रेशन का काम किया था जो smart devices बनाती थी, और उस कंपनी का इकलौता engineer assembly language के अलावा कुछ जानता ही नहीं था
    hardware control code से लेकर server operating system, और यहाँ तक कि हमारे द्वारा इस्तेमाल किया जाने वाला JSON web API भी सब कुछ उसने सीधे assembly में लिखा था
    एक बार हमें ऐसा bug मिला जहाँ web API किसी बिल्कुल अलग device का data लौटाता था, और बाद में पता चला कि operating system scheduling system में off-by-one error थी, जिसकी वजह से “database” web service को गलत row वापस दे रहा था

    • क्या उसका नाम कहीं Mel तो नहीं था?
  • “आत्महत्या” जैसे expressions को संभालते समय कृपया content warning लगाएँ। इससे भी बेहतर है कि उसका ज़िक्र ही न किया जाए

    • क्या? मैंने लेख के कुछ हिस्से बस सरसरी तौर पर पढ़े थे, लेकिन पहली बार पढ़ते समय मुझे आत्महत्या से जुड़ा कोई ज़िक्र नहीं दिखा
      यह comment देखने के बाद मैंने फिर से ढूँढा, लेकिन तब भी नहीं मिला — क्या मुझसे कुछ छूट गया?
    • humor sense का बिल्कुल न होना, उल्टा, आपकी अपनी सेहत और पूरे समाज — दोनों के लिए कहीं ज़्यादा खतरनाक है
  • “सब कुछ assembly में लिखा गया” वाली बात देखकर मुझे Therac-25 investigation report याद आ गई