diff --git a/examples/src/linux/Makefile b/examples/src/linux/Makefile index 1c81f67b7..006ea4907 100644 --- a/examples/src/linux/Makefile +++ b/examples/src/linux/Makefile @@ -33,6 +33,7 @@ TARGETS = \ x8664_hello_cpp \ x8664_hello_cpp_static \ x8664_cloexec_test \ + x8664_linux_onestraw \ patch_test.bin .PHONY: all clean @@ -124,7 +125,10 @@ libpatch_test.so: patch_test.so.h patch_test.so.c $(CC) $(CPPFLAGS) -Wall -s -O0 -shared -fpic -o $@ patch_test.so.c patch_test.bin: patch_test.bin.c libpatch_test.so + $(CC) $(CPPFLAGS) $(CFLAGS) -o $@ $^ +x8664_onestraw_server: x8664_linux_onestraw.c + $(CC) $(CPPFLAGS) $(CFLAGS) -m64 -o $@ $< $(OBJS):%.o:%.c $(CC) $(CFLAGS) -c $< -o $@ diff --git a/examples/src/linux/x8664_linux_epoll.c b/examples/src/linux/x8664_linux_epoll.c new file mode 100644 index 000000000..afd0c8a49 --- /dev/null +++ b/examples/src/linux/x8664_linux_epoll.c @@ -0,0 +1,54 @@ +#define MAX_EVENTS 5 +#define READ_SIZE 10 +#include // for fprintf() +#include // for close(), read() +#include // for epoll_create1(), epoll_ctl(), struct epoll_event +#include // for strncmp + +int main() +{ + //setvbuf(stdin, NULL, _IONBF, 0); + int running = 1, event_count, i; + size_t bytes_read; + char read_buffer[READ_SIZE + 1]; + struct epoll_event event, events[MAX_EVENTS]; + int epoll_fd = epoll_create1(0); + + if (epoll_fd == -1) { + fprintf(stderr, "Failed to create epoll file descriptor\n"); + return 1; + } + + event.events = EPOLLIN; + event.data.fd = 0; + + if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, 0, &event)) + { + fprintf(stderr, "Failed to add file descriptor to epoll\n"); + close(epoll_fd); + return 1; + } + + while (running) { + printf("\nPolling for input...\n"); + event_count = epoll_wait(epoll_fd, events, MAX_EVENTS, 30000); + printf("%d ready events\n", event_count); + for (i = 0; i < event_count; i++) { + printf("Reading file descriptor '%d' -- ", events[i].data.fd); + bytes_read = read(events[i].data.fd, read_buffer, READ_SIZE); + printf("%zd bytes read.\n", bytes_read); + read_buffer[bytes_read] = '\0'; + printf("Read '%s'\n", read_buffer); + + if(!strncmp(read_buffer, "stop\n", 5)) + running = 0; + } + } + + if (close(epoll_fd)) { + fprintf(stderr, "Failed to close epoll file descriptor\n"); + return 1; + } + + return 0; +} diff --git a/examples/src/linux/x8664_linux_onestraw.c b/examples/src/linux/x8664_linux_onestraw.c new file mode 100644 index 000000000..43a93df8e --- /dev/null +++ b/examples/src/linux/x8664_linux_onestraw.c @@ -0,0 +1,199 @@ +/* + * Attention: + * To keep things simple, do not handle socket/bind/listen/.../epoll_create/epoll_wait API error + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DEFAULT_PORT 8000 +#define MAX_CONN 16 +#define MAX_EVENTS 32 +#define BUF_SIZE 16 +#define MAX_LINE 256 + +void server_run(); +void client_run(); + +int main(int argc, char *argv[]) +{ + int opt; + char role = 's'; + while ((opt = getopt(argc, argv, "cs")) != -1) { + switch (opt) { + case 'c': + role = 'c'; + break; + case 's': + break; + default: + printf("usage: %s [-cs]\n", argv[0]); + exit(1); + } + } + if (role == 's') { + server_run(); + } else { + client_run(); + } + return 0; +} + +/* + * register events of fd to epfd + */ +static void epoll_ctl_add(int epfd, int fd, uint32_t events) +{ + struct epoll_event ev; + ev.events = events; + ev.data.fd = fd; + if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev) == -1) { + perror("epoll_ctl()\n"); + exit(1); + } +} + +static void set_sockaddr(struct sockaddr_in *addr) +{ + bzero((char *)addr, sizeof(struct sockaddr_in)); + addr->sin_family = AF_INET; + addr->sin_addr.s_addr = INADDR_ANY; + addr->sin_port = htons(DEFAULT_PORT); +} + +static int setnonblocking(int sockfd) +{ + if (fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFL, 0) | O_NONBLOCK) == + -1) { + return -1; + } + return 0; +} + +/* + * epoll echo server + */ +void server_run() +{ + int i; + int n; + int epfd; + int nfds; + int listen_sock; + int conn_sock; + int socklen; + char buf[BUF_SIZE]; + struct sockaddr_in srv_addr; + struct sockaddr_in cli_addr; + struct epoll_event events[MAX_EVENTS]; + + listen_sock = socket(AF_INET, SOCK_STREAM, 0); + + set_sockaddr(&srv_addr); + bind(listen_sock, (struct sockaddr *)&srv_addr, sizeof(srv_addr)); + + setnonblocking(listen_sock); + listen(listen_sock, MAX_CONN); + + epfd = epoll_create(1); + epoll_ctl_add(epfd, listen_sock, EPOLLIN | EPOLLOUT | EPOLLET); + + socklen = sizeof(cli_addr); + int a; + for (a = 0; a < 2; a++) { + nfds = epoll_wait(epfd, events, MAX_EVENTS, -1); + for (i = 0; i < nfds; i++) { + if (events[i].data.fd == listen_sock) { + /* handle new connection */ + conn_sock = + accept(listen_sock, + (struct sockaddr *)&cli_addr, + &socklen); + + inet_ntop(AF_INET, (char *)&(cli_addr.sin_addr), + buf, sizeof(cli_addr)); + printf("[+] connected with %s:%d\n", buf, + ntohs(cli_addr.sin_port)); + + setnonblocking(conn_sock); + epoll_ctl_add(epfd, conn_sock, + EPOLLIN | EPOLLET | EPOLLRDHUP | + EPOLLHUP); + } else if (events[i].events & EPOLLIN) { + /* handle EPOLLIN event */ + for (;;) { + bzero(buf, sizeof(buf)); + n = read(events[i].data.fd, buf, + sizeof(buf)); + if (n <= 0 /* || errno == EAGAIN */ ) { + break; + } else { + printf("[+] data: %s\n", buf); + write(events[i].data.fd, buf, + strlen(buf)); + } + } + } else { + printf("[+] unexpected\n"); + } + /* check if the connection is closing */ + if (events[i].events & (EPOLLRDHUP | EPOLLHUP)) { + printf("[+] connection closed\n"); + epoll_ctl(epfd, EPOLL_CTL_DEL, + events[i].data.fd, NULL); + close(events[i].data.fd); + continue; + } + } + } +} + +/* + * test clinet + */ +void client_run() +{ + int n; + int c; + int sockfd; + char buf[MAX_LINE]; + struct sockaddr_in srv_addr; + + sockfd = socket(AF_INET, SOCK_STREAM, 0); + + set_sockaddr(&srv_addr); + + if (connect(sockfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr)) < 0) { + perror("connect()"); + exit(1); + } + + for (;;) { + printf("input: "); + fgets(buf, sizeof(buf), stdin); + c = strlen(buf) - 1; + buf[c] = '\0'; + write(sockfd, buf, c + 1); + + bzero(buf, sizeof(buf)); + while (errno != EAGAIN + && (n = read(sockfd, buf, sizeof(buf))) > 0) { + printf("echo: %s\n", buf); + bzero(buf, sizeof(buf)); + + c -= n; + if (c <= 0) { + break; + } + } + } + close(sockfd); +} diff --git a/qiling/os/posix/const.py b/qiling/os/posix/const.py index 6fcea41bc..df4c5b587 100644 --- a/qiling/os/posix/const.py +++ b/qiling/os/posix/const.py @@ -1064,3 +1064,29 @@ class qnx_mmap_prot_flags(QlPrettyFlag): SHMDT = 22 SHMGET = 23 SHMCTL = 24 + +# see: https://elixir.bootlin.com/linux/v5.19.17/source/include/uapi/linux/eventpoll.h +EPOLL_CTL_ADD = 1 +EPOLL_CTL_DEL = 2 +EPOLL_CTL_MOD = 3 + +EPOLLIN = 0x00000001 +EPOLLPRI = 0x00000002 +EPOLLOUT = 0x00000004 +EPOLLERR = 0x00000008 +EPOLLHUP = 0x00000010 +EPOLLNVAL = 0x00000020 +EPOLLRDNORM = 0x00000040 +EPOLLRDBAND = 0x00000080 +EPOLLWRNORM = 0x00000100 +EPOLLWRBAND = 0x00000200 +EPOLLMSG = 0x00000400 +EPOLLRDHUP = 0x00002000 + +# TODO: should be aligned to emulated system's close-on-exec value +EPOLL_CLOEXEC = 0o2000000 + +EPOLLEXCLUSIVE = 0b1 << 28 +EPOLLWAKEUP = 0b1 << 29 +EPOLLONESHOT = 0b1 << 30 +EPOLLET = 0b1 << 31 diff --git a/qiling/os/posix/syscall/__init__.py b/qiling/os/posix/syscall/__init__.py index 38b10e64e..fec37e526 100644 --- a/qiling/os/posix/syscall/__init__.py +++ b/qiling/os/posix/syscall/__init__.py @@ -30,3 +30,4 @@ from .unistd import * from .utsname import * from .wait import * +from .epoll import * \ No newline at end of file diff --git a/qiling/os/posix/syscall/epoll.py b/qiling/os/posix/syscall/epoll.py new file mode 100644 index 000000000..469116781 --- /dev/null +++ b/qiling/os/posix/syscall/epoll.py @@ -0,0 +1,294 @@ +from __future__ import annotations + +import ctypes +import select + +from typing import TYPE_CHECKING, Dict, KeysView, NamedTuple + +from qiling.os import struct +from qiling.os.posix.const import * +from qiling.os.filestruct import PersistentQlFile, ql_file + + +if TYPE_CHECKING: + from qiling import Qiling + from qiling.arch.arch import QlArch + from qiling.os.posix.posix import QlFileDes + + +class QlEpollEntry(NamedTuple): + """A named tuple to represent an epoll entry. + + This is used to store the events mask and the data for each entry in + the epoll instance. + """ + + events: int + data: int + + +@struct.cache +def __make_epoll_event(arch: QlArch): + """Create a structure to represent an epoll event. + """ + + Struct = struct.get_packed_struct(arch.endian) + + class epoll_event(Struct): + _fields_ = ( + ('events', ctypes.c_uint32), + ('data', ctypes.c_uint64) + ) + + return epoll_event + + +class QlEpollObj: + def __init__(self, epoll_object: select.epoll): + self._epoll_object = epoll_object + + # maps fd to eventmask + # keep track of which fds have what eventmasks, + # since this isn't directly supported in select.epoll + self._fds: Dict[int, QlEpollEntry] = {} + + @property + def fds(self) -> KeysView[int]: + return self._fds.keys() + + @property + def epoll_instance(self) -> select.epoll: + return self._epoll_object + + def close(self) -> None: + self._epoll_object.close() + + def __getitem__(self, fd: int) -> QlEpollEntry: + return self._fds[fd] + + def __setitem__(self, fd: int, entry: QlEpollEntry) -> None: + # if fd is already being watched, modify its eventmask. + if fd in self: + self._epoll_object.modify(fd, entry.events) + + # otherwise, register it with the epoll object + else: + self._epoll_object.register(fd, entry.events) + + self._fds[fd] = entry + + def __delitem__(self, fd: int) -> None: + """Remove an fd from the epoll instance. + """ + + self._fds.pop(fd) + self._epoll_object.unregister(fd) + + def __contains__(self, fd: int) -> bool: + """Test whether a specific fd is already being watched by this epoll instance. + """ + + return fd in self.fds + + +def check_epoll_depth(ql_fd_list: QlFileDes) -> None: + """Recursively check each epoll instance's 'watched' fds for an instance of + epoll being watched. If a chain of over 5 levels is detected, raise an exception + """ + + def __visit_obj(obj: QlEpollObj, depth: int): + if depth >= 5: + raise RecursionError + + for fd in obj.fds: + if isinstance(ql_fd_list[fd], QlEpollObj): + __visit_obj(obj, depth + 1) + + for obj in ql_fd_list: + if isinstance(obj, QlEpollObj): + __visit_obj(obj, 1) + + + +def ql_syscall_epoll_ctl(ql: Qiling, epfd: int, op: int, fd: int, event: int): + """Modify an existing epoll. + """ + + # not clear from man page, but to be safe don't support 'undefined' ops. + if op not in (EPOLL_CTL_ADD, EPOLL_CTL_DEL, EPOLL_CTL_MOD): + return -EINVAL + + if epfd == fd: + return -EINVAL + + if epfd not in range(NR_OPEN): + return -EBADF + + epoll_parent_obj = ql.os.fd[epfd] + + if not isinstance(epoll_parent_obj, QlEpollObj): + return -EINVAL + + epoll_obj = epoll_parent_obj.epoll_instance + + if epoll_obj is None: + return -EBADF + + if epoll_obj.fileno() == fd: + return -ELOOP + + # Qiling doesn't check process capabilities right now, so this case isn't explicitly handled yet + # EPOLLWAKEUP (since Linux 3.5) + # If EPOLLONESHOT and EPOLLET are clear and the process has the CAP_BLOCK_SUSPEND capability + + fd_obj = ql.os.fd[fd] + + if fd_obj is None: + return -EBADF + + # TODO: not sure if qiling supports a way to determine if the target file descriptor is a + # directory. Here, check against PersistentQlFile is to ensure that polling stdin, stdout, + # stderr is supported + + # The target file fd does not support epoll. This error can occur if fd refers to, for + # example, a regular file or a directory. + if isinstance(fd_obj, ql_file) and not isinstance(fd_obj, PersistentQlFile): + return -EPERM + + + + # EPOLLEXCLUSIVE was specified in event and fd refers to an epoll instance + if isinstance(fd_obj, QlEpollObj) and (op & EPOLLEXCLUSIVE): + return -EINVAL + + # Necessary to iterate over all possible qiling fds to determine if we have a chain of more + # than five epolls monitoring each other This may be removed in the future if the QlOsLinux + # class had a separate field for reserved for tracking epoll objects. + epolls_list = [fobj for fobj in ql.os.fd if isinstance(fobj, QlEpollObj)] + + try: + check_epoll_depth(ql.os.fd) + except RecursionError: + return -ELOOP + + if op == EPOLL_CTL_ADD: + # can't add an fd that's already being waited on + if fd in epoll_parent_obj: + return -EEXIST + + if not event: + return -EINVAL + + # dereference the event pointer to get structure fields + epoll_event_cls = __make_epoll_event(ql.arch) + epoll_event = epoll_event_cls.load_from(ql.mem, event) + + # EPOLLEXCLUSIVE was specified in event and fd refers to an epoll instance + if isinstance(fd_obj, QlEpollObj) and (epoll_event.events & EPOLLEXCLUSIVE): + return -EINVAL + + epoll_parent_obj[fd] = QlEpollEntry( + epoll_event.events, + epoll_event.data + ) + + elif op == EPOLL_CTL_DEL: + if fd not in epoll_parent_obj: + return -ENOENT + + # remove from fds list and do so in the underlying epoll instance + del epoll_parent_obj[fd] + + elif op == EPOLL_CTL_MOD: + if fd not in epoll_parent_obj: + return -ENOENT + + if not event: + return -EINVAL + + # dereference the event pointer to get structure fields + epoll_event_cls = __make_epoll_event(ql.arch) + epoll_event = epoll_event_cls.load_from(ql.mem, event) + + # EPOLLEXCLUSIVE cannot be set on MOD operation, only on ADD + if epoll_event.events & EPOLLEXCLUSIVE: + return -EINVAL + + epoll_parent_obj[fd] = QlEpollEntry( + epoll_event.events, + epoll_event.data + ) + + return 0 + + +def ql_syscall_epoll_wait(ql: Qiling, epfd: int, epoll_events: int, maxevents: int, timeout: int): + """Wait on an existing epoll for specific events. + """ + + if maxevents <= 0: + return -EINVAL + + # default value is 0xffffffff, but this fails when passing to epoll.poll() + if timeout == 0xFFFFFFFF: + timeout = None + + if epfd not in range(NR_OPEN): + return -EBADF + + epoll_parent_obj = ql.os.fd[epfd] + + if not isinstance(epoll_parent_obj, QlEpollObj): + return -EINVAL + + epoll_obj = epoll_parent_obj.epoll_instance + + if epoll_obj is None: + return -EBADF + + ready_fds = epoll_obj.poll(timeout, maxevents) + + epoll_event_cls = __make_epoll_event(ql.arch) + + # Each tuple in ready_fds consists of (file descriptor, eventmask) so we iterate + # through these to indicate which fds are ready and 'why' + # + # FIXME: emulated system fds are not the same as hosted system fds + for i, (fd, events) in enumerate(ready_fds): + entry = epoll_parent_obj[fd] + epoll_event = epoll_event_cls(events, entry.data) + + offset = epoll_event_cls.sizeof() * i + ql.mem.write(epoll_events + offset, bytes(epoll_event)) + + # if no longer interested in this fd, remove from list + if events & EPOLLONESHOT: + del epoll_parent_obj[fd] + + return len(ready_fds) + + +def __epoll_create(ql: Qiling, sizehint: int, flags: int) -> int: + # Use select.epoll for underlying implementation, just as select.poll is + # used for emulating poll() + + ret = select.epoll(sizehint, flags) + + fd = ret.fileno() + ql.os.fd[fd] = QlEpollObj(ret) + + return fd + + +def ql_syscall_epoll_create1(ql: Qiling, flags: int): + if flags != select.EPOLL_CLOEXEC and flags != 0: + return -EINVAL + + return __epoll_create(ql, -1, flags) + + +def ql_syscall_epoll_create(ql: Qiling, size: int): + if size < 0: + return -EINVAL + + return __epoll_create(ql, size, 0) diff --git a/qiling/os/struct.py b/qiling/os/struct.py index 9e156777d..3b94d7bef 100644 --- a/qiling/os/struct.py +++ b/qiling/os/struct.py @@ -222,6 +222,26 @@ class BaseStructEB(BaseStruct, ctypes.BigEndianStructure): pass +@cache +def get_packed_struct(endian: QL_ENDIAN = QL_ENDIAN.EL) -> Type[BaseStruct]: + """Provide a packed version of BaseStruct based on the emulated + architecture endianess. + + Args: + archbits: required alignment in bits + """ + + Struct = { + QL_ENDIAN.EL: BaseStructEL, + QL_ENDIAN.EB: BaseStructEB + }[endian] + + class PackedStruct(Struct): + _pack_ = 1 + + return PackedStruct + + @cache def get_aligned_struct(archbits: int, endian: QL_ENDIAN = QL_ENDIAN.EL) -> Type[BaseStruct]: """Provide an aligned version of BaseStruct based on the emulated @@ -229,6 +249,7 @@ def get_aligned_struct(archbits: int, endian: QL_ENDIAN = QL_ENDIAN.EL) -> Type[ Args: archbits: required alignment in bits + endian: required endianness """ Struct = { diff --git a/tests/test_elf.py b/tests/test_elf.py index 7977e3f11..6afd49740 100644 --- a/tests/test_elf.py +++ b/tests/test_elf.py @@ -9,8 +9,11 @@ import os import io import re - +import socket import sys +import time +import threading +from ctypes import * sys.path.append("..") from typing import Any, Sequence @@ -770,6 +773,65 @@ def test_elf_linux_x8664_path_traversion(self): del ql + @unittest.skip("stdin hijacking doesn't work as expected") + def test_elf_linux_x8664_epoll_simple(self): + # This tests a sample binary that (e)polls on stdin and echos back the output. Upon + # receiving 'stop', it will exit. + # + # epoll-0 tkaen from: https://github.com/maxasm/epoll-c/blob/main/main.c + + rootfs = "../examples/rootfs/x8664_linux" + argv = r"../examples/rootfs/x8664_linux/bin/x8664_linux_epoll_0".split() + ql = Qiling(argv, rootfs, verbose=QL_VERBOSE.DEBUG) + ql.os.stdin = pipe.SimpleInStream(0) + ql.os.stdin.write(b'echo\n') + ql.os.stdin.write(b'stop\n') # signal to exit gracefully + ql.run() + + self.assertIn(b'echo\n', ql.os.stdout.read()) + del ql + + def test_elf_linux_x8664_epoll_server(self): + # This tests a simple server that uses epoll to wait for data, then prints it out. It has + # been modified to exit after data has been received; instead of a typical server operation + # that reads requests indefinitely. + # + # It listens on port 8000, and a separate thread is spawned in order to test how the server + # handles a 'hello world' input. The server prints out whatever it receives, so the assert + # statement checks the input is present as expected. + # + # onestraw server taken from: https://github.com/onestraw/epoll-example + + # Note: Without a hook for this syscall, this error fires: + # TypeError: stat: path should be string, bytes, os.PathLike or integer, not NoneType + def hook_newfstatat(ql: Qiling, dirfd: int, pathname: int, statbuf: int, flags: int): + return 0 + + def client(): + # give time for the server to listen + time.sleep(3) + + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.connect(("127.0.0.1", 8000)) + s.send(b"hello world") + s.close() + + rootfs = "../examples/rootfs/x8664_linux" + argv = r"../examples/rootfs/x8664_linux/bin/x8664_onestraw_server s".split() # s means 'server mode' + + ql = Qiling(argv, rootfs, verbose=QL_VERBOSE.DEBUG) + ql.os.set_syscall("newfstatat", hook_newfstatat, QL_INTERCEPT.CALL) + ql.os.stdout = pipe.SimpleOutStream(1) # server prints data received to stdout + ql.filter = '^data:' + + client_thread = threading.Thread(target=client, daemon=True) + client_thread.start() + + ql.run() + + self.assertIn(b'hello world', ql.os.stdout.read(200)) # 200 is arbitrary--"good enough" for this task + del ql + if __name__ == "__main__": unittest.main() diff --git a/tests/test_elf_multithread.py b/tests/test_elf_multithread.py index 9b60e8d17..8923efa1b 100644 --- a/tests/test_elf_multithread.py +++ b/tests/test_elf_multithread.py @@ -6,13 +6,12 @@ import http.client import platform import re -import socket import sys import os import threading import time import unittest - +import socket from typing import List sys.path.append("..") @@ -636,6 +635,5 @@ def picohttpd(): feedback = response.read() self.assertEqual('httpd_test_successful', feedback.decode()) - if __name__ == "__main__": unittest.main()