Skip to content

Instantly share code, notes, and snippets.

@karolba
Last active July 25, 2023 19:24
Show Gist options
  • Save karolba/6c827b1ebe5d54c604d54ae14ed17c6b to your computer and use it in GitHub Desktop.
Save karolba/6c827b1ebe5d54c604d54ae14ed17c6b to your computer and use it in GitHub Desktop.
A forking web-server with directory contents listing in x86_64 assembly
extern printf
extern perror
extern exit
extern puts
extern socket
extern bind
extern listen
extern accept
extern close
extern fdopen
extern fclose
extern fprintf
extern fscanf
extern malloc
extern free
extern setsockopt
extern strcmp
extern open
extern read
extern fwrite
%define NULL 0
%define AF_INET 2
%define SOCK_STREAM 1
%define SOL_SOCKET 1
%define SO_REUSEADDR 2
%define O_RDONLY 0
%define O_LARGEFILE 100000q
%define O_DIRECTORY 200000q
%define RESOLVE_IN_ROOT 0x10 ; for openat2: Make all jumps to "/" and ".." be scoped inside the dirfd (similar to chroot(2)).
%define minus_EISDIR -21
%define DT_DIR 4
%define SYS_openat2 437 ; https://github.com/torvalds/linux/blob/master/arch/x86/entry/syscalls/syscall_64.tbl#L361
%define SYS_read 0
%define SYS_getdents64 217
%define BUFFER_SIZE 8196
; what to tell listen(3) - how big of a backlog do we want
%define LISTEN_BACKLOG_SIZE 4
section .data
string_current_directory_for_open: db '.', 0
string_fdopen_read_write_mode: db 'r+', 0
string_socket_creation_fail: db 'socket() failed', 0
string_socket_creation_successfull: db 'socket() succeeded, sockfd = %d', 10, 0
string_setsockopt_successfull: db 'setsockopt() - setting SO_REUSEADDR succeeded', 0
string_setsockopt_fail: db 'setsockopt() - setting SO_REUSEADDR failed', 0
string_bind_fail: db 'bind() failed', 0
string_bind_successfull: db 'bind() succeeded', 0
string_listen_fail: db 'listen() failed', 0
string_listen_successfull: db 'listen() succeeded - waiting for any clients', 0
string_accept_fail: db 'accept() failed', 0
string_accept_successfull: db 'accept() succeeded - new client connected, connfd = %d', 10, 0
string_fdopen_fail: db 'fdopen() failed', 0
string_fscanf_first_http_line: db '%31s %1023s HTTP/1.%c%*[', 13, ']%*[', 10, ']', 0
string_fscanf_first_http_line_fail: db 'Could not parse the HTTP request, closing the connection', 0
string_GET_method: db 'GET', 0
string_400_bad_request_resp: db 'HTTP/1.%c 400 Bad Request', 13, 10
db 'Connection: close', 13, 10
db 'Content-Type: text/plain', 13, 10
db 13, 10
db '400: Bad Request', 13, 10, 0
string_404_not_found_resp: db 'HTTP/1.%c 404 Not Found', 13, 10
db 'Connection: close', 13, 10
db 'Content-Type: text/plain', 13, 10
db 13, 10
db '404: Not Found', 13, 10, 0
string_405_method_not_allowed_resp: db 'HTTP/1.%c 405 Method Not Allowed', 13, 10
db 'Connection: close', 13, 10
db 'Content-Type: text/plain', 13, 10
db 13, 10
db '405: Method Not Allowed', 13, 10
db 'Only the GET method is allowed', 13, 10, 0
string_http_correct_response_prelude: db 'HTTP/1.%c 200 OK', 13, 10
db 'Connection: close', 13, 10
db 'X-Requested-Path: [%s]', 13, 10
db 13, 10, 0
string_http_preamble_directory: db 'HTTP/1.%c 200 OK', 13, 10
db 'Connection: close', 13, 10
db 'Content-Type: text/html', 13, 10
db 13, 10
db '<!doctype html>', 13, 10
db '<html><head>', 13, 10
db ' <meta charset="UTF-8">', 13, 10
db ' <title>Directory listing: %s</title>', 13, 10
db ' <base href="%s/">', 13, 10
db '</head>', 13, 10
db '<body><h3>Directory listing for %s</h3><pre>', 13, 10
db ' type | filename', 13, 10
db '---------+-------------------------------------------------------', 13, 10, 0
string_http_file_in_listing: db '%.8s | <a href="%s">%s</a>', 13, 10, 0
string_http_directory_in_listing: db '%.8s | <a href="%s">%s/</a>', 13, 10, 0
strings_file_types: db ' ???' ; #define DT_UNKNOWN 0
db ' fifo' ; #define DT_FIFO 1
db 'char dev' ; #define DT_CHR 2
db ' ???'
db ' dir' ; #define DT_DIR 4
db ' ???'
db 'blck dev' ; #define DT_BLK 6
db ' ???'
db ' file' ; #define DT_REG 8
db ' ???'
db ' symlink' ; #define DT_LNK 10
db ' ???'
db ' socket' ; #define DT_SOCK 12
db ' ???'
db 'whiteout' ; #define DT_WHT 14
db ' ???'
db ' ???'
section .text
; macros for creating and exiting out of a stack frame
%macro create_stack_frame 1
push rbp
mov rbp, rsp
sub rsp, %1
%endmacro
%macro exit_stack_frame_return 0
mov rsp, rbp
pop rbp
ret
%endmacro
; opens a file restricting access to the current directory only
; using the RESOLVE_IN_ROOT option to the openat2 syscall, which makes
; the syscall behave kind of like it was ran from a chroot
;
; avoids `curl 127.0.0.1:8080/../../../../etc/passwd` from working
;
; returns either the file descriptor of the file or -1 if the file doesn't
; exist / there was any other error
;
;int safe_open_in_current_dir(char *path)
%define target_file_des DWORD [rbp-50q]
%define opened_current_dir DWORD [rbp-44q]
%define open_how QWORD [rbp-40q]
%define open_how.flags QWORD [rbp-40q]
%define open_how.mode QWORD [rbp-30q]
%define open_how.resolve QWORD [rbp-20q]
%define OPEN_HOW_SIZE 24
%define path QWORD [rbp-10q]
safe_open_in_current_dir:
create_stack_frame 60q
; save the argument
mov path, rdi
; O_DIRECTORY makes this fail if "." somehow wasn't a directory
; opened_current_dir = open(".", O_RDONLY | O_DIRECTORY)
mov rdi, string_current_directory_for_open
mov rsi, O_RDONLY | O_DIRECTORY
call open
mov opened_current_dir, eax
; check open() return value
cmp eax, -1
je .open_current_dir_failed
; setup data for openat2
mov open_how.flags, O_RDONLY | O_LARGEFILE
mov open_how.mode, 0
mov open_how.resolve, RESOLVE_IN_ROOT ; Make all jumps to "/" and ".." be scoped inside the dirfd (similar to chroot(2)).
; invoke openat2 via a syscall directly, as the manpage suggests there isn't a glibc wrapper
; SYS_openat2(int dirfd, const char *pathname, struct open_how *how, size_t size);
mov rax, SYS_openat2
mov edi, opened_current_dir
mov rsi, path
lea rdx, open_how
lea r10, OPEN_HOW_SIZE
syscall
mov target_file_des, eax
; check exit status code from the syscall
cmp rax, 0
jl .openat2_failed
.close_current_dir_and_end:
mov edi, opened_current_dir
call close
.end:
mov eax, target_file_des
exit_stack_frame_return
.open_current_dir_failed:
; return (int64)-1
mov rax, -1
jmp .end
.openat2_failed:
; something is wrong: we don't really care what as we are just going to return 404 Not Found
mov target_file_des, -1 ; return -1 instead of -errno
jmp .close_current_dir_and_end
;
; from `man 2 getdents`:
;
; struct linux_dirent64 {
; ino64_t d_ino; /* 64-bit inode number */ <-- offset 0
; off64_t d_off; /* 64-bit offset to next structure */ <-- offset 8
; unsigned short d_reclen; /* Size of this dirent */ <-- offset 16
; unsigned char d_type; /* File type */ <-- offset 18
; char d_name[]; /* Filename (null-terminated) */ <-- offset 19
; };
%define LINUX_DIRENT64_OFFSET_D_INO 0
%define LINUX_DIRENT64_OFFSET_D_OFF 8
%define LINUX_DIRENT64_OFFSET_D_RECLEN 16
%define LINUX_DIRENT64_OFFSET_D_TYPE 18
%define LINUX_DIRENT64_OFFSET_D_NAME 19
; sends out one line in the directory listing with the given directory
;void send_one_entry_in_listing(FILE *conn, struct linux_dirent64 *linux_dirent64_entry)
%define format_str QWORD [rbp-50q]
%define linux_dirent64_entry QWORD [rbp-40q]
%define listing_dir_type_str QWORD [rbp-30q]
%define listing_dir_name_str QWORD [rbp-20q]
%define conn QWORD [rbp-10q]
send_one_entry_in_listing:
create_stack_frame 60q
; save the arguments
mov conn, rdi
mov linux_dirent64_entry, rsi
; point to the d_name from this linux_dirent64
mov rax, linux_dirent64_entry
lea rax, [rax+LINUX_DIRENT64_OFFSET_D_NAME]
mov listing_dir_name_str, rax
; load d_type from this linux_dirent64 to rcx
mov rax, linux_dirent64_entry
xor ecx, ecx
mov cl, [rax+LINUX_DIRENT64_OFFSET_D_TYPE]
;
; for safety `and` rcx so the value is max 15
and rcx, 0xf
;
; load strings_file_types[d_type * 8] into listing_dir_type_str (d_type is in rcx)
lea rdx, [strings_file_types + rcx * 8]
mov listing_dir_type_str, rdx
;
; select the right format string based on d_type (rcx)
mov format_str, string_http_file_in_listing
cmp rcx, DT_DIR
jne .after_format_string_set_to_directory
mov format_str, string_http_directory_in_listing
.after_format_string_set_to_directory:
; printf the dir - use either string_http_file_in_listing or string_http_directory_in_listing
; as a special case
mov rdi, conn
mov rsi, format_str
mov rdx, listing_dir_type_str
mov rcx, listing_dir_name_str
mov r8, listing_dir_name_str
call fprintf
exit_stack_frame_return
;%define html_base_string QWORD [rbp-60q]
;void serve_directory_GET(FILE *conn, char *path, char http_minor_version, int requested_dirfd)
%define http_minor_version_char BYTE [rbp-65q]
%define requested_dirfd DWORD [rbp-64q]
%define buffer_end QWORD [rbp-60q]
%define current_linux_dirent64 QWORD [rbp-50q]
%define getdirent64_result_size QWORD [rbp-40q]
%define dirent64_buf QWORD [rbp-30q]
%define path_str QWORD [rbp-20q]
%define conn QWORD [rbp-10q]
serve_directory_GET:
create_stack_frame 100q ; q = octal
; save all function arguments
mov conn, rdi
mov path_str, rsi
mov http_minor_version_char, dl
mov requested_dirfd, ecx
; alloate a buffer for directory entries from getdirent64
mov rdi, BUFFER_SIZE
call malloc
mov dirent64_buf, rax
; send the HTTP preamble/headers for a directory
mov rdi, conn
mov rsi, string_http_preamble_directory
xor edx, edx
mov dl, http_minor_version_char
mov rcx, path_str
mov r8, path_str
mov r9, path_str
call fprintf
.getdents64_loop:
; read the dir
mov rax, SYS_getdents64
mov edi, requested_dirfd
mov rsi, dirent64_buf
mov rdx, BUFFER_SIZE
syscall
mov getdirent64_result_size, rax
;
; either an error or end of file
cmp rax, 0
jle .end
;
; compute the end of the buffer by adding getdirent64_result_size (rax) to the start
; of the buffer
add rax, dirent64_buf
mov buffer_end, rax
; look at the first linux_dirent64
mov rax, dirent64_buf
mov current_linux_dirent64, rax
; loop over all received linux_dirent64's
.inside_one_buffer_loop:
; send the dir
mov rdi, conn
mov rsi, current_linux_dirent64
call send_one_entry_in_listing
; get current_linux_dirent64.d_reclen into rcx
mov rax, current_linux_dirent64
xor ecx, ecx
mov cx, [rax+LINUX_DIRENT64_OFFSET_D_RECLEN]
;
; advance the current_linux_dirent64 pointer (now in rax) by d_reclen (rcx)
add rax, rcx
mov current_linux_dirent64, rax
;
; check if we aren't out of bounds
cmp rax, buffer_end
; if we aren't, loop back to handle the next dir
jl .inside_one_buffer_loop
; there might still data about the directory to be read, loop back
jmp .getdents64_loop
.end:
; free the buffer
mov rdi, dirent64_buf
call free
exit_stack_frame_return
;void serve_file_GET(FILE *conn, char *path, char http_minor_version)
%define headers_are_already_sent BYTE [rbp-46q]
%define http_minor_version_char BYTE [rbp-45q]
%define requested_file DWORD [rbp-44q]
%define read_result QWORD [rbp-40q]
%define buffer QWORD [rbp-30q]
%define path_str QWORD [rbp-20q]
%define conn QWORD [rbp-10q]
serve_file_GET:
create_stack_frame 60q
; save all function arguments
mov conn, rdi
mov path_str, rsi
mov http_minor_version_char, dl
; allocate memory for the buffer used for copying data
mov rdi, BUFFER_SIZE
call malloc
mov buffer, rax
; requested_file = safe_open_in_current_dir(path_str);
mov rdi, path_str
call safe_open_in_current_dir
mov requested_file, eax
; handle safe_open_in_current_dir errors
cmp eax, -1
je .safe_open_in_current_dir_error
; mark the headers not yet sent
mov headers_are_already_sent, 0
.read_write_loop:
; read something from the file
; assume the file is a regular file and not a directory, in case it actually is a directory
; read() will return EISDIR - handle it then
mov rax, SYS_read
mov edi, requested_file
mov rsi, buffer
mov rdx, BUFFER_SIZE
syscall ; use a syscall and not a glibc wrapper because it's more important to get the error
; code into rax (for EISDIR) here than into errno so perror works
mov read_result, rax
; handle read errors (both real errors and EISDIR)
cmp rax, 0
jl .read_error
; send the HTTP preamble/headers if they weren't already sent
cmp headers_are_already_sent, 1
je .after_sent_headers
; fprintf(conn, ...)
mov rdi, conn
mov rsi, string_http_correct_response_prelude
xor edx, edx
mov dl, http_minor_version_char
mov rcx, path_str
call fprintf
; remember the preamble/headers were sent
mov headers_are_already_sent, 1
.after_sent_headers:
; check for EOF (0 returned from read)
cmp read_result, 0
je .cleanup_end
; write binary data
mov rdi, buffer
mov rsi, read_result
mov rdx, 1
mov rcx, conn
call fwrite
; do the whole thing again
jmp .read_write_loop
; cleanup
.cleanup_end:
; close the requested file
mov edi, requested_file
call close
.end:
; free the buffer
mov rdi, buffer
call free
exit_stack_frame_return
.read_error:
; check if read errored because the file is actually a directory (minus_EISDIR)
; or for some other reason
cmp read_result, minus_EISDIR
je .this_is_actually_a_directory
; on a real read error close the file and return a 404 Not Found
mov edi, requested_file
call close
;;; fallthrough to .safe_open_in_current_dir_error
.safe_open_in_current_dir_error:
; send 404 Not Found
mov rdi, conn
mov rsi, string_404_not_found_resp
xor edx, edx
mov dl, http_minor_version_char
call fprintf
jmp .end
.this_is_actually_a_directory:
; read returned -EISDIR - switch to serve_directory_GET()
mov rdi, conn
mov rsi, path_str
xor edx, edx
mov dl, http_minor_version_char
mov ecx, requested_file
call serve_directory_GET
; then cleanup and return
jmp .cleanup_end
;void handle_client(int connfd)
%define minor_http_version_char BYTE [rbp-41q]
%define path_str QWORD [rbp-40q]
%define method_str QWORD [rbp-30q]
%define conn_FILE QWORD [rbp-20q]
%define connfd DWORD [rbp-10q]
handle_client:
create_stack_frame 60q
; save the function argument
mov connfd, edi
;; message_buf = malloc(1024);
;mov rdi, 1024
;call malloc
;mov message_buf, rax
; method_str = malloc(32);
mov rdi, 32
call malloc
mov method_str, rax
; path_str = malloc(1024);
mov rdi, 1024
call malloc
mov path_str, rax
; turn the fd into a FILE* - makes it possible to use fscanf/fprintf on the socket
; if((conn_FILE = fdopen(connfd, "r+")) == NULL) goto .fdopen_failed;
xor rdi, rdi
mov edi, connfd
mov rsi, string_fdopen_read_write_mode
call fdopen
mov conn_FILE, rax
; error checking:
cmp rax, NULL
je .fdopen_failed
; read the first line which should be in a format of something like "GET / HTTP/1.1\r\n"
; fscanf(conn_FILE, '%31s %1023s HTTP/1.%c%*[\r]%*[\n]', method_str, path_str, &minor_http_version_char);
mov rdi, conn_FILE
mov rsi, string_fscanf_first_http_line
mov rdx, method_str
mov rcx, path_str
lea r8, minor_http_version_char
call fscanf
; error check fscanf
cmp rax, 3 ; three assigned items -> success
jne .fscanf_failure
; ensure the HTTP method is a GET
; if(strcmp(method_str, "GET") != 0) goto .reply_method_not_allowed;
mov rdi, method_str
mov rsi, string_GET_method
call strcmp
cmp rax, 0
jne .reply_method_not_allowed
; call serve_file_GET(conn_FILE, method_str, minor_http_version_char) for further processing
mov rdi, conn_FILE
mov rsi, path_str
xor edx, edx
mov dl, minor_http_version_char
call serve_file_GET
.close_and_end:
; call fclose(conn_FILE)
; this also closes the underlying stream from listen()
mov rdi, conn_FILE
call fclose
.end:
; free allocated buffers
mov rdi, path_str
call free
mov rdi, method_str
call free
exit_stack_frame_return
;;; handle_client() error handlers: ;;;
; failed fdopen() - the file descriptor is not turned into a FILE* stream, so
; it should be closed using close() instread of fclose()
.fdopen_failed:
; print a diagnostic message
mov rdi, string_fdopen_fail
call perror
; close(connfd)
; don't quit on errors, because: from `man 3 close`:
; Note, however, that a failure return should be used only for
; diagnostic purposes (i.e., a warning to the application that
; there may still be I/O pending or there may have been failed I/O)
; or remedial purposes (e.g., writing the file once more or
; creating a backup).
xor rdi, rdi
mov edi, connfd
call close
jmp .end
; failed parsing the first HTTP line using fscanf
; emit a debug message to stdout and close the connection
.fscanf_failure:
mov rdi, string_fscanf_first_http_line_fail
call puts
jmp .close_and_end
; handle the HTTP method being something other than a GET by returning a 405 Method Not Allowed
.reply_method_not_allowed:
mov rdi, conn_FILE
mov rsi, string_405_method_not_allowed_resp
xor rdx, rdx
mov dl, minor_http_version_char
call fprintf
jmp .close_and_end
; TODO: (maybe?) error out on versions other than HTTP/1.0 and HTTP/1.1
; exits with 1
fail:
and rsp, 0xfffffffffffffff0 ; make sure the stack is aligned
mov rdi, 1
call exit
socket_creation_failed:
mov rdi, string_socket_creation_fail
call perror
jmp fail
setsockopt_failed:
mov rdi, string_setsockopt_fail
call perror
jmp fail
bind_failed:
mov rdi, string_bind_fail
call perror
jmp fail
listen_failed:
mov rdi, string_listen_fail
call perror
jmp fail
accept_failed:
mov rdi, string_accept_fail
call perror
jmp fail
; sizes of the sockaddr_in struct
;
; from /usr/include/netinet/in.h: (musl libc)
;
; struct sockaddr_in {
; sa_family_t sin_family;
; in_port_t sin_port;
; struct in_addr sin_addr;
; uint8_t sin_zero[8];
; };
;
;
; sin_family:
; typedef unsigned short sa_family_t;
; -> 2 bytes
; -> WORD
;
; sin_port:
; typedef uint16_t in_port_t;
; -> 2 bytes
; -> WORD
;
; sin_addr:
; struct in_addr sin_addr;
; -> struct in_addr { in_addr_t s_addr; };
; -> typedef uint32_t in_addr_t;
; -> 4 bytes
; -> DWORD
;
; sin_zero:
; uint8_t[8]
; -> 8 bytes
; -> QWORD
global main
; int main(int argc, char *argv[])
; int main(int edi, char *rsi[])
; stack variables:
%define so_reuseaddr_enable DWORD [rbp-54q] ; use octal here (NNq) so it's easy to decrement by 8
%define serveraddr [rbp-50q]
%define serveraddr.sin_family WORD [rbp-50q]
%define serveraddr.sin_port WORD [rbp-46q]
%define serveraddr.sin_addr DWORD [rbp-44q]
%define serveraddr.sin_zero QWORD [rbp-40q]
%define SERVERADDR_SIZE 16
%define length QWORD [rbp-30q]
%define connfd QWORD [rbp-20q]
%define sockfd QWORD [rbp-10q]
main:
create_stack_frame 60q
; sockfd = socket(AF_INET, SOCK_STREAM, 0);
; sockfd = socket(2, 1, 0);
mov rdi, AF_INET
mov rsi, SOCK_STREAM
xor edx, edx ; mov rdx, 0
call socket
mov sockfd, rax
; check socket() return value: -1 means an error
cmp eax, -1
je socket_creation_failed
; print a message that socket creation was successfull
mov rdi, string_socket_creation_successfull
mov rsi, sockfd
call printf
; enable SO_REUSEADDR on the socket to not have to wait for a connection in a TIME_WAIT state
; setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &so_reuseaddr_enable, sizeof(int))
mov so_reuseaddr_enable, 1
mov rdi, sockfd
mov rsi, SOL_SOCKET
mov rdx, SO_REUSEADDR
lea rcx, so_reuseaddr_enable
mov r8, 4 ; sizeof(int) /* == sizeof(so_reuseaddr_enable) */
call setsockopt
; error handling: setsockopt should return 0
cmp eax, 0
jne setsockopt_failed
; print a message that setsockopt was successfull
mov rdi, string_setsockopt_successfull
call puts
; initialise serveraddr
mov serveraddr.sin_family, AF_INET
mov serveraddr.sin_port, 0x901f ; htons(8080) = htons(0x1f90) = 0x901f
mov serveraddr.sin_addr, 0 ; 0.0.0.0 = 0x00_00_00_00 (the same in network and host order)
mov serveraddr.sin_zero, 0
; bind(sockfd, &serveraddr, sizeof serveraddr)
mov rdi, sockfd
lea rsi, serveraddr
mov rdx, SERVERADDR_SIZE
call bind
; handle bind() failure (when return value is not zero)
cmp eax, 0
jne bind_failed
; print a message that bind() succeeded
mov rdi, string_bind_successfull
call puts
; listen(sockfd, LISTEN_BACKLOG_SIZE)
mov rdi, sockfd
lea rsi, LISTEN_BACKLOG_SIZE
call listen
; handle listen() failure (when return value is not zero)
cmp eax, 0
jne listen_failed
; print a message that listen() succeeded
mov rdi, string_listen_successfull
call puts
.accept_handle_close_loop:
; connfd = accept(sockfd, NULL, NULL)
mov rdi, sockfd
xor esi, esi ; mov rsi, 0
xor edx, edx ; mov rdx, 0
call accept
mov connfd, rax
; handle accept() failure
cmp eax, -1
je accept_failed
; print a message stating accept() succeeded
mov rdi, string_accept_successfull
mov rsi, connfd
call printf
; handle client here, in handle_client(connfd)
mov rdi, connfd
call handle_client
; jump back to accept a new client
jmp .accept_handle_close_loop
; TODO: set SO_REUSEADDR
; vim: ft=nasm ts=4 sw=4 expandtab
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment