From 1ee9ccb8e15a4544312206a9d1c6107a8cf16697 Mon Sep 17 00:00:00 2001 From: libumem Date: Fri, 4 Apr 2025 02:59:35 +0000 Subject: [PATCH 01/32] Initial commit for epoll --- qiling/os/posix/const.py | 19 +++ qiling/os/posix/syscall/epoll.py | 232 +++++++++++++++++++++++++++++++ 2 files changed, 251 insertions(+) create mode 100644 qiling/os/posix/syscall/epoll.py diff --git a/qiling/os/posix/const.py b/qiling/os/posix/const.py index 6fcea41bc..88110166a 100644 --- a/qiling/os/posix/const.py +++ b/qiling/os/posix/const.py @@ -1064,3 +1064,22 @@ class qnx_mmap_prot_flags(QlPrettyFlag): SHMDT = 22 SHMGET = 23 SHMCTL = 24 + +# epoll syscall +EPOLL_OPS = {0x001: "EPOLL_CTL_ADD", 0x002: "EPOLL_CTL_DEL", 0x003: "EPOLL_CTL_MOD"} + +EPOLLIN = 0x001 +EPOLLPRI = 0x002 +EPOLLOUT = 0x004 +EPOLLRDNORM = 0x040 +EPOLLRDBAND = 0x080 +EPOLLWRNORM = 0x100 +EPOLLWRBAND = 0x200 +EPOLLMSG = 0x400 +EPOLLERR = 0x008 +EPOLLHUP = 0x010 +EPOLLRDHUP = 0x2000 +EPOLLEXCLUSIVE = 1 << 28 +EPOLLWAKEUP = 1 << 29 +EPOLLONESHOT = 1 << 30 +EPOLLET = 1 << 31 diff --git a/qiling/os/posix/syscall/epoll.py b/qiling/os/posix/syscall/epoll.py new file mode 100644 index 000000000..cbfcdb1e3 --- /dev/null +++ b/qiling/os/posix/syscall/epoll.py @@ -0,0 +1,232 @@ +class QlEpollObj: + def __init__(self, epoll_object): + self._epoll_object = epoll_object + self._fds = {} # key: fd, value: eventmask + # keep track of which fds have what eventmasks, + # since this isn't directly supported in select.epoll + + @property + def get_epoll_instance(self): + return self._epoll_object + + @property + def get_eventmask(self, fd): + return self._fds[fd] + + @property + def get_fds(self): + if len(self._fds.keys()) == 0: + return [] + return list(self._fds.keys()) + + def set_eventmask(self, fd, newmask): + # the mask for an FD shouldn't ever be undefined + # as it is set whenever an FD is added for a QlEpollObj instance + newmask = self.get_eventmask() | newmask # or with new eventmask value + self._epoll_object.modify(fd, newmask) + + def monitor_fd(self, fd, eventmask): + self._epoll_object.register( + fd, eventmask + ) # tell the epoll object to watch the fd arg, looking for events matching the eventmask + self._fds[fd] = eventmask + + def delist_fd(self, fd): + self._fds.pop(fd) + self._epoll_object.unregister(fd) + + def close(self): + self.get_epoll_instance.close() + + def is_present(self, fd): + if fd not in self.get_fds: + return 0 + return 1 + + +def check_epoll_depth(ql_fd_list, epolls_list, depth): + if depth == 6: + return 1 + new_epolls_list = [] + flag = 0 + for ent in list(epolls_list): + watched = ent.get_fds + for w in watched: + if isinstance(ql_fd_list[w], QlEpollObj): + flag = 1 + new_epolls_list.append(ql_fd_list[w]) + if flag: + check_epoll_depth(ql_fd_list, new_epolls_list, depth + 1) + return 0 + + +def ql_epoll_ctl(ql: qiling.Qiling, epfd: int, op: int, fd: int, event: POINTER): + # Basic sanity checks first + if event != 0: + ql_event = ql.unpack32(ql.mem.read(event, 4)) # events list is uint32_t + else: + ql_event = ( + 0 # event can be null, for example, when deleting a fd from interest list + ) + ql_op = "" + epoll_obj = -1 + try: + epoll_parent_obj = ql.os.fd[epfd] + epoll_obj = epoll_parent_obj.get_epoll_instance + except KeyError as k: + ql.log.debug("Unable to grab epoll object, something wrong with ql.os.fd!") + ql.log.debug(k) + return EINVAL + try: + ql_op = EPOLL_OPS[op] + except KeyError as k: + ql.log.debug("Warning, invalid epoll op detected") + ql.log.debug(k) + return EINVAL # not clear from man page, but to be safe don't support 'undefined' ops. + """ + 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 + """ + + # Unclear if qiling supports a way to determine + # if the target file descriptor is a directory + # Check against PersistentQlFile is to ensure + # that polling stdin, stdout, stderr is supported + fd_obj = ql.os.fd[fd] + if isinstance(fd_obj, ql_file) and not isinstance( + fd_obj, PersistentQlFile + ): # EPERM The target file fd does not support epoll. This error can occur if fd refers to, for example, a regular file or a directory. + return EPERM + + if isinstance(ql.os.fd[fd], QlEpollObj) and (op & EPOLLEXCLUSIVE): + # EPOLLEXCLUSIVE was specified in event and fd refers to an epoll instance + 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 = [] + for f in ql.os.fd: + if isinstance(f, QlEpollObj): + epolls_list.append(f) + level_check = check_epoll_depth(ql.os.fd, epolls_list, 1) + if level_check: # more than five detected + return ELOOP + if epoll_obj is None or fd_obj is None: + # epfd or fd is not a valid file descriptor. + return EBADF + if epfd == fd: + return EINVAL + if epoll_obj.fileno() == fd: + return ELOOP # ELOOP ...or a nesting depth of epoll instances greater than 5. + match ql_op: + case "EPOLL_CTL_ADD": + if epoll_parent_obj.is_present( + fd + ): # can't add an fd that's already being waited on + return EEXIST # op was EPOLL_CTL_ADD, and the supplied file descriptor fd is already registered with this epoll instance. + epoll_parent_obj.monitor_fd( + fd, ql_event + ) # add to list of fds to be monitored with per-fd eventmask + # register will actual epoll instance + # and add eventmask accordingly + case "EPOLL_CTL_DEL": + if not epoll_parent_obj.is_present( + fd + ): # op was EPOLL_CTL_MOD or EPOLL_CTL_DEL, and fd is not registered with this epoll instance. + return ENOENT + epoll_parent_obj.delist_fd(fd) # remove from fds list and do so in the + # underlying epoll instance + case "EPOLL_CTL_MOD": + if not epoll_parent_obj.is_present( + fd + ): # ENOENT op was EPOLL_CTL_MOD or EPOLL_CTL_DEL, and fd is not registered with this epoll instance + return ENOENT + # EINVAL op was EPOLL_CTL_MOD and events included EPOLLEXCLUSIVE. + if op & EPOLLEXCLUSIVE and fd in epoll_obj.get_fds: + return EINVAL # EINVAL op was EPOLL_CTL_MOD and the EPOLLEXCLUSIVE flag has previously been applied to this epfd, fd pair. + epoll_parent_obj.set_eventmask(ql_event) + + return 0 + + +def ql_epoll_wait( + ql: qiling.Qiling, epfd: int, epoll_events: POINTER, maxevents: int, timeout: int +): + if maxevents <= 0: + return EINVAL + # default value is 0xffffffff, but + # this fails when passing to epoll.poll() + if timeout == 0xFFFFFFFF: + timeout = None + + try: + epoll_parent_obj = ql.os.fd[epfd] + epoll_obj = epoll_parent_obj.get_epoll_instance + if not isinstance(epoll_parent_obj, QlEpollObj): + return EINVAL + except KeyError: + ql.log.debug(f"FD {epfd} doesn't appear to be a valid epoll file descriptor") + return EBADF + try: + ql_event = ql.unpack(ql.mem.read(epoll_events, ql.arch.pointersize)) + except Exception: + ql.log.debug("Can't read from epoll_events pointer") + return EFAULT + ready_fds = list(epoll_obj.poll(timeout, maxevents)) + + # Each tuple in ready_fds consists of + # (file descriptor, eventmask) + # so we iterate through these to indicate which fds + # are ready and 'why' + ret_val = len(ready_fds) + for i in range(0, ret_val): + fd = ready_fds[i][0] + interest_mask = ready_fds[i][1] + if ( + interest_mask & EPOLLONESHOT + ): # no longer interested in this fd, so remove from list + epoll_parent_obj.delist_fd(fd) + + counter = ( + ql.arch.pointersize + 4 + ) * i # use ql.arch.pointersize to be compatible with 32-bit + data = ql.pack32(interest_mask) # uint32_t eventfds + data += ql.pack(fd) # need fd only, use pack() to handle endianness + size + ql.mem.write(epoll_events + counter, data) + return ret_val + + +""" +Use select.epoll for underlying implementation, +just as select.poll is used for emulating poll() +""" + + +def ql_epoll_create1(ql: qiling.Qiling, flags: int): + if flags != select.EPOLL_CLOEXEC and flags != 0: + return EINVAL + ret = select.epoll(sizehint=-1, flags=flags) + fd = ret.fileno() + ql_obj = QlEpollObj(ret) + ql.os.fd[fd] = ql_obj + return fd + + +""" +Almost identical to above, but can't simply wrap +because of the slightly different args and the different +syscall number +""" + + +def ql_epoll_create(ql: qiling.Qiling, size: int): + if size < 0: + return EINVAL + ret = select.epoll(sizehint=size, flags=0) + fd = ret.fileno() + ql_obj = QlEpollObj(ret) + ql.os.fd[fd] = ql_obj + return fd \ No newline at end of file From 18381f42a3d48682e0d89642657916e5f026411d Mon Sep 17 00:00:00 2001 From: libumem <163767094+libumem@users.noreply.github.com> Date: Fri, 4 Apr 2025 03:07:09 +0000 Subject: [PATCH 02/32] Simple test case --- tests/test_elf.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/test_elf.py b/tests/test_elf.py index 7977e3f11..0af4c90d5 100644 --- a/tests/test_elf.py +++ b/tests/test_elf.py @@ -770,6 +770,13 @@ def test_elf_linux_x8664_path_traversion(self): del ql - + def test_elf_linux_x8664_epoll_simple(self): + # TODO: Get the example in rootfs + rootfs = "../examples/rootfs/x8664_linux" + argv = r"../examples/rootfs/x8664_linux/epoll-0".split() + ql = qiling.Qiling(argv, rootfs, verbose=QL_VERBOSE.DEBUG) + ql.os.stdin = pipe.SimpleInStream(0) + ql.os.stdin.write(b"stop\n") + ql.run() if __name__ == "__main__": unittest.main() From 1f12da52c40fc26648325eb1470a4273732cf470 Mon Sep 17 00:00:00 2001 From: libumem <163767094+libumem@users.noreply.github.com> Date: Fri, 4 Apr 2025 03:10:22 +0000 Subject: [PATCH 03/32] Add del ql --- tests/test_elf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_elf.py b/tests/test_elf.py index 0af4c90d5..f12f3e017 100644 --- a/tests/test_elf.py +++ b/tests/test_elf.py @@ -778,5 +778,6 @@ def test_elf_linux_x8664_epoll_simple(self): ql.os.stdin = pipe.SimpleInStream(0) ql.os.stdin.write(b"stop\n") ql.run() + del ql if __name__ == "__main__": unittest.main() From e208a34520da92a5e7e409fa4bf2ec791e0f2bcf Mon Sep 17 00:00:00 2001 From: libumem <163767094+libumem@users.noreply.github.com> Date: Fri, 4 Apr 2025 03:13:05 +0000 Subject: [PATCH 04/32] Skeleton for graph --- tests/test_elf.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_elf.py b/tests/test_elf.py index f12f3e017..07b533a86 100644 --- a/tests/test_elf.py +++ b/tests/test_elf.py @@ -779,5 +779,9 @@ def test_elf_linux_x8664_epoll_simple(self): ql.os.stdin.write(b"stop\n") ql.run() del ql + + def test_elf_linux_x8664_epoll_server(self): + pass + del ql if __name__ == "__main__": unittest.main() From 2479b912bce2dd0a13c76411289e7944aa2b4440 Mon Sep 17 00:00:00 2001 From: libumem <163767094+libumem@users.noreply.github.com> Date: Mon, 7 Apr 2025 21:03:00 +0000 Subject: [PATCH 05/32] Flesh out tests, clean up --- qiling/os/posix/const.py | 1 + qiling/os/posix/syscall/epoll.py | 40 +++++++++++++++++++++-------- tests/test_elf.py | 43 ++++++++++++++++++++++++++------ tests/test_elf_multithread.py | 2 -- 4 files changed, 67 insertions(+), 19 deletions(-) diff --git a/qiling/os/posix/const.py b/qiling/os/posix/const.py index 88110166a..db1bdf69a 100644 --- a/qiling/os/posix/const.py +++ b/qiling/os/posix/const.py @@ -1083,3 +1083,4 @@ class qnx_mmap_prot_flags(QlPrettyFlag): EPOLLWAKEUP = 1 << 29 EPOLLONESHOT = 1 << 30 EPOLLET = 1 << 31 +EPOLL_CLOEXEC = 0x02000000 \ No newline at end of file diff --git a/qiling/os/posix/syscall/epoll.py b/qiling/os/posix/syscall/epoll.py index cbfcdb1e3..54ff0e5aa 100644 --- a/qiling/os/posix/syscall/epoll.py +++ b/qiling/os/posix/syscall/epoll.py @@ -1,3 +1,16 @@ +import qiling +from qiling.const import * +from qiling.os.posix.const import * +from qiling.os.const import * +from qiling.os.filestruct import ql_file +import select +from ctypes import * +from qiling.os import struct +import struct +from qiling.os.filestruct import PersistentQlFile +from qiling.extensions import pipe +import sys + class QlEpollObj: def __init__(self, epoll_object): self._epoll_object = epoll_object @@ -43,9 +56,14 @@ def is_present(self, fd): return 0 return 1 - +''' +Recursively checks each epoll instance's 'watched' +fds for an instance of epoll being watched. +If a chain of over 5 levels is detected, return +1, which will return ELOOP in ql_epoll_wait +''' def check_epoll_depth(ql_fd_list, epolls_list, depth): - if depth == 6: + if depth == 7: return 1 new_epolls_list = [] flag = 0 @@ -59,7 +77,10 @@ def check_epoll_depth(ql_fd_list, epolls_list, depth): check_epoll_depth(ql_fd_list, new_epolls_list, depth + 1) return 0 - +''' +Modify an existing epoll +man 7 epoll for more details +''' def ql_epoll_ctl(ql: qiling.Qiling, epfd: int, op: int, fd: int, event: POINTER): # Basic sanity checks first if event != 0: @@ -152,6 +173,10 @@ def ql_epoll_ctl(ql: qiling.Qiling, epfd: int, op: int, fd: int, event: POINTER) return 0 +''' +Wait on an existing epoll for events specified +earlier. man 7 epoll_wait for more info +''' def ql_epoll_wait( ql: qiling.Qiling, epfd: int, epoll_events: POINTER, maxevents: int, timeout: int ): @@ -203,8 +228,6 @@ def ql_epoll_wait( Use select.epoll for underlying implementation, just as select.poll is used for emulating poll() """ - - def ql_epoll_create1(ql: qiling.Qiling, flags: int): if flags != select.EPOLL_CLOEXEC and flags != 0: return EINVAL @@ -217,11 +240,8 @@ def ql_epoll_create1(ql: qiling.Qiling, flags: int): """ Almost identical to above, but can't simply wrap -because of the slightly different args and the different -syscall number +because of the slightly different prototype """ - - def ql_epoll_create(ql: qiling.Qiling, size: int): if size < 0: return EINVAL @@ -229,4 +249,4 @@ def ql_epoll_create(ql: qiling.Qiling, size: int): fd = ret.fileno() ql_obj = QlEpollObj(ret) ql.os.fd[fd] = ql_obj - return fd \ No newline at end of file + return fd diff --git a/tests/test_elf.py b/tests/test_elf.py index 07b533a86..b3be53da3 100644 --- a/tests/test_elf.py +++ b/tests/test_elf.py @@ -9,8 +9,9 @@ import os import io import re - +import socket import sys +import time sys.path.append("..") from typing import Any, Sequence @@ -771,17 +772,45 @@ def test_elf_linux_x8664_path_traversion(self): del ql def test_elf_linux_x8664_epoll_simple(self): - # TODO: Get the example in rootfs + # TODO: Get the example in rootfs, see https://github.com/qilingframework/rootfs/pull/35 + # epoll-0 source: https://github.com/maxasm/epoll-c/blob/main/main.c rootfs = "../examples/rootfs/x8664_linux" - argv = r"../examples/rootfs/x8664_linux/epoll-0".split() + argv = r"../examples/rootfs/x8664_linux/bin/epoll-0".split() ql = qiling.Qiling(argv, rootfs, verbose=QL_VERBOSE.DEBUG) - ql.os.stdin = pipe.SimpleInStream(0) - ql.os.stdin.write(b"stop\n") + ql.os.stdin = pipe.InteractiveInStream(0) + ql.os.stdin.write(b'echo\n') + ql.os.stdin.write(b"stop\n") # signal to exit gracefully ql.run() + self.assertIn("echo", ql.os.stdout.read().decode("utf-8")) del ql - def test_elf_linux_x8664_epoll_server(self): - pass + # TODO: https://github.com/qilingframework/rootfs/pull/35 must be merged + # Source for onestraw server: https://github.com/onestraw/epoll-example + # with a slight change to exit after the first request + def client(): + time.sleep(3) # give time for the server to listen + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + dest = ("127.0.0.1", 8000) + try: + s.connect(dest) + except Exception as e: + ql.log.debug('test failed') + ql.log.debug(e) + + test = b"hello world" + s.send(test) + s.close() + # use threads here to test how the server + # handles the request + client_thread = threading.Thread(target=client, daemon=True) + client_thread.start() + rootfs = "../examples/rootfs/" + argv = r"../examples/rootfs/x8664_linux/bin/onestraw-server s".split() # s means 'server mode' + ql = qiling.Qiling(argv, rootfs, multithread=False, verbose=QL_VERBOSE.DEBUG) + ql.os.stdout = pipe.SimpleOutStream(1) # server prints data received to stdout + ql.filter = '^data:' + ql.run() + self.assertIn('hello world', ql.os.stdout.read().decode("utf-8")) del ql if __name__ == "__main__": unittest.main() diff --git a/tests/test_elf_multithread.py b/tests/test_elf_multithread.py index 9b60e8d17..3841a011a 100644 --- a/tests/test_elf_multithread.py +++ b/tests/test_elf_multithread.py @@ -6,7 +6,6 @@ import http.client import platform import re -import socket import sys import os import threading @@ -636,6 +635,5 @@ def picohttpd(): feedback = response.read() self.assertEqual('httpd_test_successful', feedback.decode()) - if __name__ == "__main__": unittest.main() From 290f11671a3d1a1cf8f9eea120e8e2e44de71930 Mon Sep 17 00:00:00 2001 From: libumem <163767094+libumem@users.noreply.github.com> Date: Fri, 25 Apr 2025 02:03:53 +0000 Subject: [PATCH 06/32] Begin fixing this PR --- examples/rootfs | 2 +- examples/src/linux/x8664_linux_epoll.c | 54 ++++++ examples/src/linux/x8664_linux_onestraw.c | 199 ++++++++++++++++++++++ qiling/os/posix/syscall/__init__.py | 1 + qiling/os/posix/syscall/epoll.py | 67 ++++---- tests/test_elf.py | 3 +- 6 files changed, 290 insertions(+), 36 deletions(-) create mode 100644 examples/src/linux/x8664_linux_epoll.c create mode 100644 examples/src/linux/x8664_linux_onestraw.c diff --git a/examples/rootfs b/examples/rootfs index f71f45fe1..6d4d654fd 160000 --- a/examples/rootfs +++ b/examples/rootfs @@ -1 +1 @@ -Subproject commit f71f45fe1a39d58d8b8cae717f55cebeb37f63c7 +Subproject commit 6d4d654fdc2892490d98c433eca3efa5c6d062c7 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/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 index 54ff0e5aa..b67c43147 100644 --- a/qiling/os/posix/syscall/epoll.py +++ b/qiling/os/posix/syscall/epoll.py @@ -1,4 +1,4 @@ -import qiling +from qiling import * from qiling.const import * from qiling.os.posix.const import * from qiling.os.const import * @@ -60,7 +60,7 @@ def is_present(self, fd): Recursively checks each epoll instance's 'watched' fds for an instance of epoll being watched. If a chain of over 5 levels is detected, return -1, which will return ELOOP in ql_epoll_wait +1, which will return ELOOP in ql_syscall_epoll_wait ''' def check_epoll_depth(ql_fd_list, epolls_list, depth): if depth == 7: @@ -81,7 +81,7 @@ def check_epoll_depth(ql_fd_list, epolls_list, depth): Modify an existing epoll man 7 epoll for more details ''' -def ql_epoll_ctl(ql: qiling.Qiling, epfd: int, op: int, fd: int, event: POINTER): +def ql_syscall_epoll_ctl(ql: Qiling, epfd: int, op: int, fd: int, event: POINTER): # Basic sanity checks first if event != 0: ql_event = ql.unpack32(ql.mem.read(event, 4)) # events list is uint32_t @@ -142,33 +142,32 @@ def ql_epoll_ctl(ql: qiling.Qiling, epfd: int, op: int, fd: int, event: POINTER) return EINVAL if epoll_obj.fileno() == fd: return ELOOP # ELOOP ...or a nesting depth of epoll instances greater than 5. - match ql_op: - case "EPOLL_CTL_ADD": - if epoll_parent_obj.is_present( - fd - ): # can't add an fd that's already being waited on - return EEXIST # op was EPOLL_CTL_ADD, and the supplied file descriptor fd is already registered with this epoll instance. - epoll_parent_obj.monitor_fd( - fd, ql_event - ) # add to list of fds to be monitored with per-fd eventmask - # register will actual epoll instance - # and add eventmask accordingly - case "EPOLL_CTL_DEL": - if not epoll_parent_obj.is_present( - fd - ): # op was EPOLL_CTL_MOD or EPOLL_CTL_DEL, and fd is not registered with this epoll instance. - return ENOENT - epoll_parent_obj.delist_fd(fd) # remove from fds list and do so in the - # underlying epoll instance - case "EPOLL_CTL_MOD": - if not epoll_parent_obj.is_present( - fd - ): # ENOENT op was EPOLL_CTL_MOD or EPOLL_CTL_DEL, and fd is not registered with this epoll instance - return ENOENT - # EINVAL op was EPOLL_CTL_MOD and events included EPOLLEXCLUSIVE. - if op & EPOLLEXCLUSIVE and fd in epoll_obj.get_fds: - return EINVAL # EINVAL op was EPOLL_CTL_MOD and the EPOLLEXCLUSIVE flag has previously been applied to this epfd, fd pair. - epoll_parent_obj.set_eventmask(ql_event) + if ql_op == "EPOLL_CTL_ADD": + if epoll_parent_obj.is_present( + fd + ): # can't add an fd that's already being waited on + return EEXIST # op was EPOLL_CTL_ADD, and the supplied file descriptor fd is already registered with this epoll instance. + epoll_parent_obj.monitor_fd( + fd, ql_event + ) # add to list of fds to be monitored with per-fd eventmask + # register will actual epoll instance + # and add eventmask accordingly + elif ql_op == "EPOLL_CTL_DEL": + if not epoll_parent_obj.is_present( + fd + ): # op was EPOLL_CTL_MOD or EPOLL_CTL_DEL, and fd is not registered with this epoll instance. + return ENOENT + epoll_parent_obj.delist_fd(fd) # remove from fds list and do so in the + # underlying epoll instance + elif ql_op == "EPOLL_CTL_MOD": + if not epoll_parent_obj.is_present( + fd + ): # ENOENT op was EPOLL_CTL_MOD or EPOLL_CTL_DEL, and fd is not registered with this epoll instance + return ENOENT + # EINVAL op was EPOLL_CTL_MOD and events included EPOLLEXCLUSIVE. + if op & EPOLLEXCLUSIVE and fd in epoll_obj.get_fds: + return EINVAL # EINVAL op was EPOLL_CTL_MOD and the EPOLLEXCLUSIVE flag has previously been applied to this epfd, fd pair. + epoll_parent_obj.set_eventmask(ql_event) return 0 @@ -177,8 +176,8 @@ def ql_epoll_ctl(ql: qiling.Qiling, epfd: int, op: int, fd: int, event: POINTER) Wait on an existing epoll for events specified earlier. man 7 epoll_wait for more info ''' -def ql_epoll_wait( - ql: qiling.Qiling, epfd: int, epoll_events: POINTER, maxevents: int, timeout: int +def ql_syscall_epoll_wait( + ql: Qiling, epfd: int, epoll_events: POINTER, maxevents: int, timeout: int ): if maxevents <= 0: return EINVAL @@ -228,7 +227,7 @@ def ql_epoll_wait( Use select.epoll for underlying implementation, just as select.poll is used for emulating poll() """ -def ql_epoll_create1(ql: qiling.Qiling, flags: int): +def ql_syscall_epoll_create1(ql: Qiling, flags: int): if flags != select.EPOLL_CLOEXEC and flags != 0: return EINVAL ret = select.epoll(sizehint=-1, flags=flags) @@ -242,7 +241,7 @@ def ql_epoll_create1(ql: qiling.Qiling, flags: int): Almost identical to above, but can't simply wrap because of the slightly different prototype """ -def ql_epoll_create(ql: qiling.Qiling, size: int): +def ql_syscall_epoll_create(ql: Qiling, size: int): if size < 0: return EINVAL ret = select.epoll(sizehint=size, flags=0) diff --git a/tests/test_elf.py b/tests/test_elf.py index b3be53da3..ab9ffa6c6 100644 --- a/tests/test_elf.py +++ b/tests/test_elf.py @@ -770,7 +770,7 @@ def test_elf_linux_x8664_path_traversion(self): self.assertNotIn("root\n", ql.os.stdout.read().decode("utf-8")) del ql - + @unittest.skip('See PR') def test_elf_linux_x8664_epoll_simple(self): # TODO: Get the example in rootfs, see https://github.com/qilingframework/rootfs/pull/35 # epoll-0 source: https://github.com/maxasm/epoll-c/blob/main/main.c @@ -783,6 +783,7 @@ def test_elf_linux_x8664_epoll_simple(self): ql.run() self.assertIn("echo", ql.os.stdout.read().decode("utf-8")) del ql + @unittest.skip('See PR') def test_elf_linux_x8664_epoll_server(self): # TODO: https://github.com/qilingframework/rootfs/pull/35 must be merged # Source for onestraw server: https://github.com/onestraw/epoll-example From c6eee99422412d5034c85990f2b3a243f25d644a Mon Sep 17 00:00:00 2001 From: libumem <163767094+libumem@users.noreply.github.com> Date: Fri, 25 Apr 2025 02:07:36 +0000 Subject: [PATCH 07/32] Add socket import back, was not supposed to be removed --- tests/test_elf_multithread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_elf_multithread.py b/tests/test_elf_multithread.py index 3841a011a..8923efa1b 100644 --- a/tests/test_elf_multithread.py +++ b/tests/test_elf_multithread.py @@ -11,7 +11,7 @@ import threading import time import unittest - +import socket from typing import List sys.path.append("..") From 033fb4b4bc79a2a86dfca84ecd4cd1b3119c36f3 Mon Sep 17 00:00:00 2001 From: libumem <163767094+libumem@users.noreply.github.com> Date: Sat, 26 Apr 2025 01:50:44 +0000 Subject: [PATCH 08/32] Address PR comments --- qiling/os/posix/const.py | 16 +++++++---- qiling/os/posix/syscall/epoll.py | 48 ++++++++++++-------------------- tests/test_elf.py | 8 +----- 3 files changed, 29 insertions(+), 43 deletions(-) diff --git a/qiling/os/posix/const.py b/qiling/os/posix/const.py index db1bdf69a..6739c03ac 100644 --- a/qiling/os/posix/const.py +++ b/qiling/os/posix/const.py @@ -1065,8 +1065,12 @@ class qnx_mmap_prot_flags(QlPrettyFlag): SHMGET = 23 SHMCTL = 24 -# epoll syscall -EPOLL_OPS = {0x001: "EPOLL_CTL_ADD", 0x002: "EPOLL_CTL_DEL", 0x003: "EPOLL_CTL_MOD"} +# epoll syscall, see https://codebrowser.dev/glibc/glibc/sysdeps/unix/sysv/linux/sys/epoll.h.html +EPOLL_OPS = { +0x001: "EPOLL_CTL_ADD", +0x002: "EPOLL_CTL_DEL", +0x003: "EPOLL_CTL_MOD" +} EPOLLIN = 0x001 EPOLLPRI = 0x002 @@ -1079,8 +1083,8 @@ class qnx_mmap_prot_flags(QlPrettyFlag): EPOLLERR = 0x008 EPOLLHUP = 0x010 EPOLLRDHUP = 0x2000 -EPOLLEXCLUSIVE = 1 << 28 -EPOLLWAKEUP = 1 << 29 -EPOLLONESHOT = 1 << 30 -EPOLLET = 1 << 31 +EPOLLEXCLUSIVE = 268435456 #1 << 28 +EPOLLWAKEUP = 536870912 #1 << 29 +EPOLLONESHOT = 1073741824 #1 << 30 +EPOLLET = 2147483648 #1 << 31 EPOLL_CLOEXEC = 0x02000000 \ No newline at end of file diff --git a/qiling/os/posix/syscall/epoll.py b/qiling/os/posix/syscall/epoll.py index b67c43147..1a36ed04b 100644 --- a/qiling/os/posix/syscall/epoll.py +++ b/qiling/os/posix/syscall/epoll.py @@ -19,23 +19,21 @@ def __init__(self, epoll_object): # since this isn't directly supported in select.epoll @property - def get_epoll_instance(self): + def epoll_instance(self): return self._epoll_object @property - def get_eventmask(self, fd): + def eventmask(self, fd): return self._fds[fd] @property - def get_fds(self): - if len(self._fds.keys()) == 0: - return [] + def fds(self): return list(self._fds.keys()) - def set_eventmask(self, fd, newmask): + def eventmask(self, fd: int, newmask: int): # the mask for an FD shouldn't ever be undefined # as it is set whenever an FD is added for a QlEpollObj instance - newmask = self.get_eventmask() | newmask # or with new eventmask value + newmask = self.eventmask() | newmask # or with new eventmask value self._epoll_object.modify(fd, newmask) def monitor_fd(self, fd, eventmask): @@ -49,31 +47,28 @@ def delist_fd(self, fd): self._epoll_object.unregister(fd) def close(self): - self.get_epoll_instance.close() + self.epoll_instance.close() - def is_present(self, fd): - if fd not in self.get_fds: - return 0 - return 1 + def is_present(self, fd: int) -> bool: + return fd in self.get_fds -''' +""" Recursively checks each epoll instance's 'watched' fds for an instance of epoll being watched. If a chain of over 5 levels is detected, return 1, which will return ELOOP in ql_syscall_epoll_wait -''' +""" def check_epoll_depth(ql_fd_list, epolls_list, depth): if depth == 7: return 1 new_epolls_list = [] flag = 0 for ent in list(epolls_list): - watched = ent.get_fds + watched = ent.fds for w in watched: if isinstance(ql_fd_list[w], QlEpollObj): - flag = 1 new_epolls_list.append(ql_fd_list[w]) - if flag: + if new_epolls_list: check_epoll_depth(ql_fd_list, new_epolls_list, depth + 1) return 0 @@ -83,17 +78,12 @@ def check_epoll_depth(ql_fd_list, epolls_list, depth): ''' def ql_syscall_epoll_ctl(ql: Qiling, epfd: int, op: int, fd: int, event: POINTER): # Basic sanity checks first - if event != 0: - ql_event = ql.unpack32(ql.mem.read(event, 4)) # events list is uint32_t - else: - ql_event = ( - 0 # event can be null, for example, when deleting a fd from interest list - ) + ql_event = event and ql.mem.read_ptr(event, 4) ql_op = "" epoll_obj = -1 try: epoll_parent_obj = ql.os.fd[epfd] - epoll_obj = epoll_parent_obj.get_epoll_instance + epoll_obj = epoll_parent_obj.epoll_instance except KeyError as k: ql.log.debug("Unable to grab epoll object, something wrong with ql.os.fd!") ql.log.debug(k) @@ -101,8 +91,6 @@ def ql_syscall_epoll_ctl(ql: Qiling, epfd: int, op: int, fd: int, event: POINTER try: ql_op = EPOLL_OPS[op] except KeyError as k: - ql.log.debug("Warning, invalid epoll op detected") - ql.log.debug(k) return EINVAL # not clear from man page, but to be safe don't support 'undefined' ops. """ Qiling doesn't check process capabilities right now, so this case isn't explicitly handled yet @@ -165,9 +153,9 @@ def ql_syscall_epoll_ctl(ql: Qiling, epfd: int, op: int, fd: int, event: POINTER ): # ENOENT op was EPOLL_CTL_MOD or EPOLL_CTL_DEL, and fd is not registered with this epoll instance return ENOENT # EINVAL op was EPOLL_CTL_MOD and events included EPOLLEXCLUSIVE. - if op & EPOLLEXCLUSIVE and fd in epoll_obj.get_fds: + if op & EPOLLEXCLUSIVE and fd in epoll_obj.fds: return EINVAL # EINVAL op was EPOLL_CTL_MOD and the EPOLLEXCLUSIVE flag has previously been applied to this epfd, fd pair. - epoll_parent_obj.set_eventmask(ql_event) + epoll_parent_obj.eventmask(ql_event) return 0 @@ -188,14 +176,14 @@ def ql_syscall_epoll_wait( try: epoll_parent_obj = ql.os.fd[epfd] - epoll_obj = epoll_parent_obj.get_epoll_instance + epoll_obj = epoll_parent_obj.epoll_instance if not isinstance(epoll_parent_obj, QlEpollObj): return EINVAL except KeyError: ql.log.debug(f"FD {epfd} doesn't appear to be a valid epoll file descriptor") return EBADF try: - ql_event = ql.unpack(ql.mem.read(epoll_events, ql.arch.pointersize)) + ql_event = ql.mem.read_ptr(epoll_events, ql.arch.pointersize) except Exception: ql.log.debug("Can't read from epoll_events pointer") return EFAULT diff --git a/tests/test_elf.py b/tests/test_elf.py index ab9ffa6c6..dc2fc7005 100644 --- a/tests/test_elf.py +++ b/tests/test_elf.py @@ -785,19 +785,13 @@ def test_elf_linux_x8664_epoll_simple(self): del ql @unittest.skip('See PR') def test_elf_linux_x8664_epoll_server(self): - # TODO: https://github.com/qilingframework/rootfs/pull/35 must be merged # Source for onestraw server: https://github.com/onestraw/epoll-example # with a slight change to exit after the first request def client(): time.sleep(3) # give time for the server to listen s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) dest = ("127.0.0.1", 8000) - try: - s.connect(dest) - except Exception as e: - ql.log.debug('test failed') - ql.log.debug(e) - + s.connect(dest) test = b"hello world" s.send(test) s.close() From fd32154ca174a181a6ee4b0d8000c7ffd36bf3aa Mon Sep 17 00:00:00 2001 From: libumem <163767094+libumem@users.noreply.github.com> Date: Sat, 26 Apr 2025 03:38:56 +0000 Subject: [PATCH 09/32] Begin working on test issues --- examples/rootfs | 2 +- qiling/os/posix/syscall/epoll.py | 2 +- tests/test_elf.py | 15 ++++++++++----- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/examples/rootfs b/examples/rootfs index 6d4d654fd..f71f45fe1 160000 --- a/examples/rootfs +++ b/examples/rootfs @@ -1 +1 @@ -Subproject commit 6d4d654fdc2892490d98c433eca3efa5c6d062c7 +Subproject commit f71f45fe1a39d58d8b8cae717f55cebeb37f63c7 diff --git a/qiling/os/posix/syscall/epoll.py b/qiling/os/posix/syscall/epoll.py index 1a36ed04b..cdd1d3c3e 100644 --- a/qiling/os/posix/syscall/epoll.py +++ b/qiling/os/posix/syscall/epoll.py @@ -50,7 +50,7 @@ def close(self): self.epoll_instance.close() def is_present(self, fd: int) -> bool: - return fd in self.get_fds + return fd in self.fds """ Recursively checks each epoll instance's 'watched' diff --git a/tests/test_elf.py b/tests/test_elf.py index dc2fc7005..e93bdd0e2 100644 --- a/tests/test_elf.py +++ b/tests/test_elf.py @@ -770,14 +770,19 @@ def test_elf_linux_x8664_path_traversion(self): self.assertNotIn("root\n", ql.os.stdout.read().decode("utf-8")) del ql + + """ + This tests a sample binary that (e)polls on stdin + and echos back the output. Upon receiving 'stop', it + will exit. + """ @unittest.skip('See PR') def test_elf_linux_x8664_epoll_simple(self): - # TODO: Get the example in rootfs, see https://github.com/qilingframework/rootfs/pull/35 # epoll-0 source: https://github.com/maxasm/epoll-c/blob/main/main.c rootfs = "../examples/rootfs/x8664_linux" - argv = r"../examples/rootfs/x8664_linux/bin/epoll-0".split() - ql = qiling.Qiling(argv, rootfs, verbose=QL_VERBOSE.DEBUG) - ql.os.stdin = pipe.InteractiveInStream(0) + argv = r"../examples/rootfs/x8664_linux/bin/x8664_linux_epoll_0".split() + ql = Qiling(argv, rootfs, verbose=QL_VERBOSE.DEBUG) + ql.os.stdin = pipe.SimpleBufferedStream() ql.os.stdin.write(b'echo\n') ql.os.stdin.write(b"stop\n") # signal to exit gracefully ql.run() @@ -801,7 +806,7 @@ def client(): client_thread.start() rootfs = "../examples/rootfs/" argv = r"../examples/rootfs/x8664_linux/bin/onestraw-server s".split() # s means 'server mode' - ql = qiling.Qiling(argv, rootfs, multithread=False, verbose=QL_VERBOSE.DEBUG) + ql = Qiling(argv, rootfs, multithread=False, verbose=QL_VERBOSE.DEBUG) ql.os.stdout = pipe.SimpleOutStream(1) # server prints data received to stdout ql.filter = '^data:' ql.run() From f2861b7f38d563830d6662de048c34b105f667ac Mon Sep 17 00:00:00 2001 From: libumem <163767094+libumem@users.noreply.github.com> Date: Sat, 26 Apr 2025 16:36:05 +0000 Subject: [PATCH 10/32] Nominally working epoll server test --- qiling/os/posix/syscall/epoll.py | 21 +++++++++++++++++---- tests/test_elf.py | 26 ++++++++++++++++++++++---- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/qiling/os/posix/syscall/epoll.py b/qiling/os/posix/syscall/epoll.py index cdd1d3c3e..8a3d7ea26 100644 --- a/qiling/os/posix/syscall/epoll.py +++ b/qiling/os/posix/syscall/epoll.py @@ -11,6 +11,7 @@ from qiling.extensions import pipe import sys + class QlEpollObj: def __init__(self, epoll_object): self._epoll_object = epoll_object @@ -52,12 +53,15 @@ def close(self): def is_present(self, fd: int) -> bool: return fd in self.fds + """ Recursively checks each epoll instance's 'watched' fds for an instance of epoll being watched. If a chain of over 5 levels is detected, return 1, which will return ELOOP in ql_syscall_epoll_wait """ + + def check_epoll_depth(ql_fd_list, epolls_list, depth): if depth == 7: return 1 @@ -72,10 +76,13 @@ def check_epoll_depth(ql_fd_list, epolls_list, depth): check_epoll_depth(ql_fd_list, new_epolls_list, depth + 1) return 0 -''' + +""" Modify an existing epoll man 7 epoll for more details -''' +""" + + def ql_syscall_epoll_ctl(ql: Qiling, epfd: int, op: int, fd: int, event: POINTER): # Basic sanity checks first ql_event = event and ql.mem.read_ptr(event, 4) @@ -160,10 +167,12 @@ def ql_syscall_epoll_ctl(ql: Qiling, epfd: int, op: int, fd: int, event: POINTER return 0 -''' +""" Wait on an existing epoll for events specified earlier. man 7 epoll_wait for more info -''' +""" + + def ql_syscall_epoll_wait( ql: Qiling, epfd: int, epoll_events: POINTER, maxevents: int, timeout: int ): @@ -215,6 +224,8 @@ def ql_syscall_epoll_wait( Use select.epoll for underlying implementation, just as select.poll is used for emulating poll() """ + + def ql_syscall_epoll_create1(ql: Qiling, flags: int): if flags != select.EPOLL_CLOEXEC and flags != 0: return EINVAL @@ -229,6 +240,8 @@ def ql_syscall_epoll_create1(ql: Qiling, flags: int): Almost identical to above, but can't simply wrap because of the slightly different prototype """ + + def ql_syscall_epoll_create(ql: Qiling, size: int): if size < 0: return EINVAL diff --git a/tests/test_elf.py b/tests/test_elf.py index e93bdd0e2..4673262ba 100644 --- a/tests/test_elf.py +++ b/tests/test_elf.py @@ -12,6 +12,8 @@ import socket import sys import time +import threading +from ctypes import * sys.path.append("..") from typing import Any, Sequence @@ -788,10 +790,27 @@ def test_elf_linux_x8664_epoll_simple(self): ql.run() self.assertIn("echo", ql.os.stdout.read().decode("utf-8")) del ql - @unittest.skip('See PR') + """ + 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. + """ + #@unittest.skip('See PR') def test_elf_linux_x8664_epoll_server(self): # Source for onestraw server: https://github.com/onestraw/epoll-example - # with a slight change to exit after the first request + + """ + FIXME: 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: POINTER, statbuf: POINTER, flags:int): + return 0 def client(): time.sleep(3) # give time for the server to listen s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -800,13 +819,12 @@ def client(): test = b"hello world" s.send(test) s.close() - # use threads here to test how the server - # handles the request client_thread = threading.Thread(target=client, daemon=True) client_thread.start() rootfs = "../examples/rootfs/" argv = r"../examples/rootfs/x8664_linux/bin/onestraw-server s".split() # s means 'server mode' ql = Qiling(argv, rootfs, multithread=False, 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:' ql.run() From f99e07ac0bb24c77843b9271a1ab0cb28aca769c Mon Sep 17 00:00:00 2001 From: libumem <163767094+libumem@users.noreply.github.com> Date: Sat, 26 Apr 2025 16:38:22 +0000 Subject: [PATCH 11/32] Type annotations for args --- qiling/os/posix/syscall/epoll.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qiling/os/posix/syscall/epoll.py b/qiling/os/posix/syscall/epoll.py index 8a3d7ea26..48c3cbc5e 100644 --- a/qiling/os/posix/syscall/epoll.py +++ b/qiling/os/posix/syscall/epoll.py @@ -24,7 +24,7 @@ def epoll_instance(self): return self._epoll_object @property - def eventmask(self, fd): + def eventmask(self, fd: int): return self._fds[fd] @property @@ -37,13 +37,13 @@ def eventmask(self, fd: int, newmask: int): newmask = self.eventmask() | newmask # or with new eventmask value self._epoll_object.modify(fd, newmask) - def monitor_fd(self, fd, eventmask): + def monitor_fd(self, fd: int, eventmask: int) -> None: self._epoll_object.register( fd, eventmask ) # tell the epoll object to watch the fd arg, looking for events matching the eventmask self._fds[fd] = eventmask - def delist_fd(self, fd): + def delist_fd(self, fd: int) -> None: self._fds.pop(fd) self._epoll_object.unregister(fd) From 0022b8d2d4b0a7f55269f6ee6f409c1dc56c313f Mon Sep 17 00:00:00 2001 From: elicn Date: Mon, 28 Apr 2025 13:28:12 +0300 Subject: [PATCH 12/32] Refactor and fixes --- qiling/os/posix/const.py | 48 +++-- qiling/os/posix/syscall/epoll.py | 360 ++++++++++++++++--------------- 2 files changed, 206 insertions(+), 202 deletions(-) diff --git a/qiling/os/posix/const.py b/qiling/os/posix/const.py index 6739c03ac..df4c5b587 100644 --- a/qiling/os/posix/const.py +++ b/qiling/os/posix/const.py @@ -1065,26 +1065,28 @@ class qnx_mmap_prot_flags(QlPrettyFlag): SHMGET = 23 SHMCTL = 24 -# epoll syscall, see https://codebrowser.dev/glibc/glibc/sysdeps/unix/sysv/linux/sys/epoll.h.html -EPOLL_OPS = { -0x001: "EPOLL_CTL_ADD", -0x002: "EPOLL_CTL_DEL", -0x003: "EPOLL_CTL_MOD" -} - -EPOLLIN = 0x001 -EPOLLPRI = 0x002 -EPOLLOUT = 0x004 -EPOLLRDNORM = 0x040 -EPOLLRDBAND = 0x080 -EPOLLWRNORM = 0x100 -EPOLLWRBAND = 0x200 -EPOLLMSG = 0x400 -EPOLLERR = 0x008 -EPOLLHUP = 0x010 -EPOLLRDHUP = 0x2000 -EPOLLEXCLUSIVE = 268435456 #1 << 28 -EPOLLWAKEUP = 536870912 #1 << 29 -EPOLLONESHOT = 1073741824 #1 << 30 -EPOLLET = 2147483648 #1 << 31 -EPOLL_CLOEXEC = 0x02000000 \ No newline at end of file +# 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/epoll.py b/qiling/os/posix/syscall/epoll.py index 48c3cbc5e..bd2378797 100644 --- a/qiling/os/posix/syscall/epoll.py +++ b/qiling/os/posix/syscall/epoll.py @@ -1,252 +1,254 @@ +import select + +from typing import TYPE_CHECKING, Dict, List + from qiling import * from qiling.const import * from qiling.os.posix.const import * from qiling.os.const import * from qiling.os.filestruct import ql_file -import select -from ctypes import * -from qiling.os import struct -import struct from qiling.os.filestruct import PersistentQlFile -from qiling.extensions import pipe -import sys + + +if TYPE_CHECKING: + from qiling.os.posix.posix import QlFileDes class QlEpollObj: - def __init__(self, epoll_object): + def __init__(self, epoll_object: select.epoll): self._epoll_object = epoll_object - self._fds = {} # key: fd, value: eventmask + + # 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, int] = {} @property - def epoll_instance(self): - return self._epoll_object + def fds(self) -> List[int]: + return list(self._fds.keys()) @property - def eventmask(self, fd: int): - return self._fds[fd] + def epoll_instance(self) -> select.epoll: + return self._epoll_object - @property - def fds(self): - return list(self._fds.keys()) + def get_eventmask(self, fd: int) -> int: + return self._fds[fd] - def eventmask(self, fd: int, newmask: int): + def set_eventmask(self, fd: int, newmask: int): # the mask for an FD shouldn't ever be undefined # as it is set whenever an FD is added for a QlEpollObj instance - newmask = self.eventmask() | newmask # or with new eventmask value + + # elicn: don't we need to update self._fds[fd] with the new mask just like in monitor_fd? + + newmask = self.get_eventmask(fd) | newmask self._epoll_object.modify(fd, newmask) def monitor_fd(self, fd: int, eventmask: int) -> None: - self._epoll_object.register( - fd, eventmask - ) # tell the epoll object to watch the fd arg, looking for events matching the eventmask + # tell the epoll object to watch the fd arg, looking for events matching the eventmask + self._epoll_object.register(fd, eventmask) self._fds[fd] = eventmask def delist_fd(self, fd: int) -> None: self._fds.pop(fd) self._epoll_object.unregister(fd) - def close(self): + def close(self) -> None: self.epoll_instance.close() def is_present(self, fd: int) -> bool: return fd in self.fds -""" -Recursively checks each epoll instance's 'watched' -fds for an instance of epoll being watched. -If a chain of over 5 levels is detected, return -1, which will return ELOOP in ql_syscall_epoll_wait -""" +def check_epoll_depth(ql_fd_list: QlFileDes, epolls_list: List[QlEpollObj], depth: int = 0) -> None: + # Recursively checks each epoll instance's 'watched' fds for an instance of + # epoll being watched. If a chain of over 5 levels is detected, return 1, + # which will return ELOOP in ql_syscall_epoll_wait + if depth >= 5: + raise RecursionError -def check_epoll_depth(ql_fd_list, epolls_list, depth): - if depth == 7: - return 1 new_epolls_list = [] - flag = 0 - for ent in list(epolls_list): + + for ent in epolls_list: watched = ent.fds + for w in watched: - if isinstance(ql_fd_list[w], QlEpollObj): - new_epolls_list.append(ql_fd_list[w]) + obj = ql_fd_list[w] + + if isinstance(obj, QlEpollObj): + new_epolls_list.append(obj) + + # elicn: new_epolls_list is not cleared between loop iterations, rather it keeps + # aggregating items from previous iterations. is this what we want? + if new_epolls_list: check_epoll_depth(ql_fd_list, new_epolls_list, depth + 1) - return 0 -""" -Modify an existing epoll -man 7 epoll for more details -""" +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 -def ql_syscall_epoll_ctl(ql: Qiling, epfd: int, op: int, fd: int, event: POINTER): - # Basic sanity checks first - ql_event = event and ql.mem.read_ptr(event, 4) - ql_op = "" - epoll_obj = -1 - try: - epoll_parent_obj = ql.os.fd[epfd] - epoll_obj = epoll_parent_obj.epoll_instance - except KeyError as k: - ql.log.debug("Unable to grab epoll object, something wrong with ql.os.fd!") - ql.log.debug(k) - return EINVAL - try: - ql_op = EPOLL_OPS[op] - except KeyError as k: - return EINVAL # not clear from man page, but to be safe don't support 'undefined' ops. - """ - 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 - """ - - # Unclear if qiling supports a way to determine - # if the target file descriptor is a directory - # Check against PersistentQlFile is to ensure - # that polling stdin, stdout, stderr is supported - fd_obj = ql.os.fd[fd] - if isinstance(fd_obj, ql_file) and not isinstance( - fd_obj, PersistentQlFile - ): # EPERM The target file fd does not support epoll. This error can occur if fd refers to, for example, a regular file or a directory. - return EPERM - - if isinstance(ql.os.fd[fd], QlEpollObj) and (op & EPOLLEXCLUSIVE): - # EPOLLEXCLUSIVE was specified in event and fd refers to an epoll instance - 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 = [] - for f in ql.os.fd: - if isinstance(f, QlEpollObj): - epolls_list.append(f) - level_check = check_epoll_depth(ql.os.fd, epolls_list, 1) - if level_check: # more than five detected - return ELOOP - if epoll_obj is None or fd_obj is None: - # epfd or fd is not a valid file descriptor. - return EBADF if epfd == fd: - return EINVAL + 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 # ELOOP ...or a nesting depth of epoll instances greater than 5. - if ql_op == "EPOLL_CTL_ADD": - if epoll_parent_obj.is_present( - fd - ): # can't add an fd that's already being waited on - return EEXIST # op was EPOLL_CTL_ADD, and the supplied file descriptor fd is already registered with this epoll instance. - epoll_parent_obj.monitor_fd( - fd, ql_event - ) # add to list of fds to be monitored with per-fd eventmask - # register will actual epoll instance - # and add eventmask accordingly - elif ql_op == "EPOLL_CTL_DEL": - if not epoll_parent_obj.is_present( - fd - ): # op was EPOLL_CTL_MOD or EPOLL_CTL_DEL, and fd is not registered with this epoll instance. - return ENOENT - epoll_parent_obj.delist_fd(fd) # remove from fds list and do so in the - # underlying epoll instance - elif ql_op == "EPOLL_CTL_MOD": - if not epoll_parent_obj.is_present( - fd - ): # ENOENT op was EPOLL_CTL_MOD or EPOLL_CTL_DEL, and fd is not registered with this epoll instance - return ENOENT + 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 + + # TODO: not sure if qiling supports a way to determine if the target file descriptor is a + # directory Check against PersistentQlFile is to ensure that polling stdin, stdout, stderr + # is supported + + fd_obj = ql.os.fd[fd] + + if fd_obj is None: + return -EBADF + + # 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 + + # elicn: not sure how the following condition even possible after we checked that op can + # be only one of EPOLL_CTL_{ADD,DEL,MOD} (originally checked with a dict) + + # 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, epolls_list) + # more than five detected? + except RecursionError: + return -ELOOP + + ql_event = event and ql.mem.read_ptr(event, 4) + + if op == EPOLL_CTL_ADD: + # can't add an fd that's already being waited on + if epoll_parent_obj.is_present(fd): + return -EEXIST + + # add to list of fds to be monitored with per-fd eventmask register will actual epoll + # instance and add eventmask accordingly + epoll_parent_obj.monitor_fd(fd, ql_event) + + elif op == EPOLL_CTL_DEL: + if not epoll_parent_obj.is_present(fd): + return -ENOENT + + # remove from fds list and do so in the underlying epoll instance + epoll_parent_obj.delist_fd(fd) + + elif op == EPOLL_CTL_MOD: + if not epoll_parent_obj.is_present(fd): + return -ENOENT + # EINVAL op was EPOLL_CTL_MOD and events included EPOLLEXCLUSIVE. if op & EPOLLEXCLUSIVE and fd in epoll_obj.fds: - return EINVAL # EINVAL op was EPOLL_CTL_MOD and the EPOLLEXCLUSIVE flag has previously been applied to this epfd, fd pair. - epoll_parent_obj.eventmask(ql_event) + return -EINVAL - return 0 + epoll_parent_obj.set_eventmask(fd, ql_event) + return 0 -""" -Wait on an existing epoll for events specified -earlier. man 7 epoll_wait for more info -""" +def ql_syscall_epoll_wait(ql: Qiling, epfd: int, epoll_events: int, maxevents: int, timeout: int): + """Wait on an existing epoll for specific events. + """ -def ql_syscall_epoll_wait( - ql: Qiling, epfd: int, epoll_events: POINTER, maxevents: int, timeout: int -): if maxevents <= 0: - return EINVAL - # default value is 0xffffffff, but - # this fails when passing to epoll.poll() + return -EINVAL + + # default value is 0xffffffff, but this fails when passing to epoll.poll() if timeout == 0xFFFFFFFF: timeout = None - try: - epoll_parent_obj = ql.os.fd[epfd] - epoll_obj = epoll_parent_obj.epoll_instance - if not isinstance(epoll_parent_obj, QlEpollObj): - return EINVAL - except KeyError: - ql.log.debug(f"FD {epfd} doesn't appear to be a valid epoll file descriptor") - return EBADF - try: - ql_event = ql.mem.read_ptr(epoll_events, ql.arch.pointersize) - except Exception: - ql.log.debug("Can't read from epoll_events pointer") - return EFAULT - ready_fds = list(epoll_obj.poll(timeout, maxevents)) - - # Each tuple in ready_fds consists of - # (file descriptor, eventmask) - # so we iterate through these to indicate which fds - # are ready and 'why' - ret_val = len(ready_fds) - for i in range(0, ret_val): - fd = ready_fds[i][0] - interest_mask = ready_fds[i][1] - if ( - interest_mask & EPOLLONESHOT - ): # no longer interested in this fd, so remove from list + 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 + + # elicn: ql_event is not used, not sure why we need that here + # try: + # ql_event = ql.mem.read_ptr(epoll_events) + # except Exception: + # return -EFAULT + + ready_fds = epoll_obj.poll(timeout, maxevents) + + # Each tuple in ready_fds consists of (file descriptor, eventmask) so we iterate + # through these to indicate which fds are ready and 'why' + + for i, (fd, interest_mask) in enumerate(ready_fds): + # if no longer interested in this fd, remove from list + if interest_mask & EPOLLONESHOT: epoll_parent_obj.delist_fd(fd) - counter = ( - ql.arch.pointersize + 4 - ) * i # use ql.arch.pointersize to be compatible with 32-bit - data = ql.pack32(interest_mask) # uint32_t eventfds - data += ql.pack(fd) # need fd only, use pack() to handle endianness + size - ql.mem.write(epoll_events + counter, data) - return ret_val + data = ql.pack32(interest_mask) + ql.pack(fd) + offset = len(data) * i + # elicn: maybe we need to use ql_event instead of epoll_events here..? + ql.mem.write(epoll_events + offset, data) -""" -Use select.epoll for underlying implementation, -just as select.poll is used for emulating poll() -""" + return len(ready_fds) -def ql_syscall_epoll_create1(ql: Qiling, flags: int): - if flags != select.EPOLL_CLOEXEC and flags != 0: - return EINVAL - ret = select.epoll(sizehint=-1, flags=flags) +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_obj = QlEpollObj(ret) - ql.os.fd[fd] = ql_obj + ql.os.fd[fd] = QlEpollObj(ret) + return fd -""" -Almost identical to above, but can't simply wrap -because of the slightly different prototype -""" +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 - ret = select.epoll(sizehint=size, flags=0) - fd = ret.fileno() - ql_obj = QlEpollObj(ret) - ql.os.fd[fd] = ql_obj - return fd + return -EINVAL + + return __epoll_create(ql, size, 0) From 74380b26d5e757aa403d4f01b57d58390f45b7e2 Mon Sep 17 00:00:00 2001 From: libumem <163767094+libumem@users.noreply.github.com> Date: Wed, 30 Apr 2025 00:18:41 +0000 Subject: [PATCH 13/32] Address a few elicn comments, fix root for server test --- examples/src/linux/Makefile | 3 +++ qiling/os/posix/syscall/epoll.py | 17 ++++++----------- tests/test_elf.py | 12 ++++++------ 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/examples/src/linux/Makefile b/examples/src/linux/Makefile index 1c81f67b7..8a26e589f 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 @@ -125,6 +126,8 @@ libpatch_test.so: patch_test.so.h 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/qiling/os/posix/syscall/epoll.py b/qiling/os/posix/syscall/epoll.py index bd2378797..7b318bd40 100644 --- a/qiling/os/posix/syscall/epoll.py +++ b/qiling/os/posix/syscall/epoll.py @@ -12,7 +12,7 @@ if TYPE_CHECKING: from qiling.os.posix.posix import QlFileDes - +from qiling.os.posix.posix import QlFileDes class QlEpollObj: def __init__(self, epoll_object: select.epoll): @@ -38,9 +38,9 @@ def set_eventmask(self, fd: int, newmask: int): # the mask for an FD shouldn't ever be undefined # as it is set whenever an FD is added for a QlEpollObj instance - # elicn: don't we need to update self._fds[fd] with the new mask just like in monitor_fd? - + # libumem: resolved elicn feedback newmask = self.get_eventmask(fd) | newmask + self._fds[fd] = newmask self._epoll_object.modify(fd, newmask) def monitor_fd(self, fd: int, eventmask: int) -> None: @@ -117,7 +117,8 @@ def ql_syscall_epoll_ctl(ql: Qiling, epfd: int, op: int, fd: int, event: int): # If EPOLLONESHOT and EPOLLET are clear and the process has the CAP_BLOCK_SUSPEND capability # TODO: not sure if qiling supports a way to determine if the target file descriptor is a - # directory Check against PersistentQlFile is to ensure that polling stdin, stdout, stderr + # directory. + # Here, check against PersistentQlFile is to ensure that polling stdin, stdout, stderr # is supported fd_obj = ql.os.fd[fd] @@ -203,11 +204,6 @@ def ql_syscall_epoll_wait(ql: Qiling, epfd: int, epoll_events: int, maxevents: i if epoll_obj is None: return -EBADF - # elicn: ql_event is not used, not sure why we need that here - # try: - # ql_event = ql.mem.read_ptr(epoll_events) - # except Exception: - # return -EFAULT ready_fds = epoll_obj.poll(timeout, maxevents) @@ -221,8 +217,7 @@ def ql_syscall_epoll_wait(ql: Qiling, epfd: int, epoll_events: int, maxevents: i data = ql.pack32(interest_mask) + ql.pack(fd) offset = len(data) * i - - # elicn: maybe we need to use ql_event instead of epoll_events here..? + # Resolved elicn remark, ql_event was dead code ql.mem.write(epoll_events + offset, data) return len(ready_fds) diff --git a/tests/test_elf.py b/tests/test_elf.py index 4673262ba..aae738985 100644 --- a/tests/test_elf.py +++ b/tests/test_elf.py @@ -778,17 +778,17 @@ def test_elf_linux_x8664_path_traversion(self): and echos back the output. Upon receiving 'stop', it will exit. """ - @unittest.skip('See PR') + @unittest.skip("TODO: Stdin hijacking doesn't work as expected") def test_elf_linux_x8664_epoll_simple(self): # epoll-0 source: 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.SimpleBufferedStream() + #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("echo", ql.os.stdout.read().decode("utf-8")) + self.assertIn("echo\n", ql.os.stdout.read().decode("utf-8")) del ql """ This tests a simple server that uses epoll @@ -806,7 +806,7 @@ def test_elf_linux_x8664_epoll_server(self): # Source for onestraw server: https://github.com/onestraw/epoll-example """ - FIXME: Without a hook for this syscall, this error fires: + 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: POINTER, statbuf: POINTER, flags:int): @@ -821,8 +821,8 @@ def client(): s.close() client_thread = threading.Thread(target=client, daemon=True) client_thread.start() - rootfs = "../examples/rootfs/" - argv = r"../examples/rootfs/x8664_linux/bin/onestraw-server s".split() # s means 'server mode' + rootfs = "../examples/rootfs/x8664_linux_glibc2.39" + argv = r"../examples/rootfs/x8664_linux/bin/x8664_onestraw_server s".split() # s means 'server mode' ql = Qiling(argv, rootfs, multithread=False, 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 From dbf213846bb9e309afe51ca40d8c7ddfb966695d Mon Sep 17 00:00:00 2001 From: elicn Date: Wed, 30 Apr 2025 13:47:38 +0300 Subject: [PATCH 14/32] Turn check_epoll_depth into a prefix visitor --- qiling/os/posix/syscall/epoll.py | 45 ++++++++++++-------------------- 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/qiling/os/posix/syscall/epoll.py b/qiling/os/posix/syscall/epoll.py index 7b318bd40..423c75d95 100644 --- a/qiling/os/posix/syscall/epoll.py +++ b/qiling/os/posix/syscall/epoll.py @@ -59,30 +59,22 @@ def is_present(self, fd: int) -> bool: return fd in self.fds -def check_epoll_depth(ql_fd_list: QlFileDes, epolls_list: List[QlEpollObj], depth: int = 0) -> None: - # Recursively checks each epoll instance's 'watched' fds for an instance of - # epoll being watched. If a chain of over 5 levels is detected, return 1, - # which will return ELOOP in ql_syscall_epoll_wait - - if depth >= 5: - raise RecursionError - - new_epolls_list = [] - - for ent in epolls_list: - watched = ent.fds - - for w in watched: - obj = ql_fd_list[w] +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 + """ - if isinstance(obj, QlEpollObj): - new_epolls_list.append(obj) + def __visit_obj(obj: QlEpollObj, depth: int): + if depth >= 5: + raise RecursionError - # elicn: new_epolls_list is not cleared between loop iterations, rather it keeps - # aggregating items from previous iterations. is this what we want? + for fd in obj.fds: + if isinstance(ql_fd_list[fd], QlEpollObj): + __visit_obj(obj, depth + 1) - if new_epolls_list: - check_epoll_depth(ql_fd_list, new_epolls_list, 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): @@ -138,14 +130,11 @@ def ql_syscall_epoll_ctl(ql: Qiling, epfd: int, op: int, fd: int, event: int): 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, epolls_list) - # more than five detected? + # 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 reserved for tracking epoll objects. + check_epoll_depth(ql.os.fd) except RecursionError: return -ELOOP From 96674e1e7aba182ed62b6a62c7fa98a0f120adcc Mon Sep 17 00:00:00 2001 From: elicn Date: Wed, 30 Apr 2025 13:49:32 +0300 Subject: [PATCH 15/32] Use container semantics instead of is_present --- qiling/os/posix/syscall/epoll.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/qiling/os/posix/syscall/epoll.py b/qiling/os/posix/syscall/epoll.py index 423c75d95..5fa898cad 100644 --- a/qiling/os/posix/syscall/epoll.py +++ b/qiling/os/posix/syscall/epoll.py @@ -55,7 +55,10 @@ def delist_fd(self, fd: int) -> None: def close(self) -> None: self.epoll_instance.close() - def is_present(self, fd: int) -> bool: + def __contains__(self, fd: int) -> bool: + """Test whether a specific fd is already being watched by this epoll instance. + """ + return fd in self.fds @@ -142,7 +145,7 @@ def ql_syscall_epoll_ctl(ql: Qiling, epfd: int, op: int, fd: int, event: int): if op == EPOLL_CTL_ADD: # can't add an fd that's already being waited on - if epoll_parent_obj.is_present(fd): + if fd in epoll_parent_obj: return -EEXIST # add to list of fds to be monitored with per-fd eventmask register will actual epoll @@ -150,14 +153,14 @@ def ql_syscall_epoll_ctl(ql: Qiling, epfd: int, op: int, fd: int, event: int): epoll_parent_obj.monitor_fd(fd, ql_event) elif op == EPOLL_CTL_DEL: - if not epoll_parent_obj.is_present(fd): + if fd not in epoll_parent_obj: return -ENOENT # remove from fds list and do so in the underlying epoll instance epoll_parent_obj.delist_fd(fd) elif op == EPOLL_CTL_MOD: - if not epoll_parent_obj.is_present(fd): + if fd not in epoll_parent_obj: return -ENOENT # EINVAL op was EPOLL_CTL_MOD and events included EPOLLEXCLUSIVE. From f1081275e5d193b44cf1d9e8eda63a1b15f4cf7f Mon Sep 17 00:00:00 2001 From: elicn Date: Wed, 30 Apr 2025 13:50:17 +0300 Subject: [PATCH 16/32] Fix events pointer handling --- qiling/os/posix/syscall/epoll.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/qiling/os/posix/syscall/epoll.py b/qiling/os/posix/syscall/epoll.py index 5fa898cad..08d96a438 100644 --- a/qiling/os/posix/syscall/epoll.py +++ b/qiling/os/posix/syscall/epoll.py @@ -141,16 +141,24 @@ def ql_syscall_epoll_ctl(ql: Qiling, epfd: int, op: int, fd: int, event: int): except RecursionError: return -ELOOP - ql_event = event and ql.mem.read_ptr(event, 4) - 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 + + event_ptr = ql.mem.read_ptr(event) + events = ql.mem.read_ptr(event_ptr, 4) + + # EPOLLEXCLUSIVE was specified in event and fd refers to an epoll instance + if isinstance(fd_obj, QlEpollObj) and (op & EPOLLEXCLUSIVE): + return -EINVAL + # add to list of fds to be monitored with per-fd eventmask register will actual epoll # instance and add eventmask accordingly - epoll_parent_obj.monitor_fd(fd, ql_event) + epoll_parent_obj.monitor_fd(fd, events) elif op == EPOLL_CTL_DEL: if fd not in epoll_parent_obj: @@ -163,11 +171,17 @@ def ql_syscall_epoll_ctl(ql: Qiling, epfd: int, op: int, fd: int, event: int): if fd not in epoll_parent_obj: return -ENOENT - # EINVAL op was EPOLL_CTL_MOD and events included EPOLLEXCLUSIVE. - if op & EPOLLEXCLUSIVE and fd in epoll_obj.fds: + if not event: + return -EINVAL + + event_ptr = ql.mem.read_ptr(event) + events = ql.mem.read_ptr(event_ptr, 4) + + # EPOLLEXCLUSIVE cannot be set on MOD operation, only on ADD + if events & EPOLLEXCLUSIVE: return -EINVAL - epoll_parent_obj.set_eventmask(fd, ql_event) + epoll_parent_obj.set_eventmask(fd, events) return 0 From 0c96871d35210cbe97009aee324931eaeff087dc Mon Sep 17 00:00:00 2001 From: elicn Date: Wed, 30 Apr 2025 13:51:41 +0300 Subject: [PATCH 17/32] Fix returned events array --- qiling/os/posix/syscall/epoll.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qiling/os/posix/syscall/epoll.py b/qiling/os/posix/syscall/epoll.py index 08d96a438..8dadd212f 100644 --- a/qiling/os/posix/syscall/epoll.py +++ b/qiling/os/posix/syscall/epoll.py @@ -221,7 +221,9 @@ def ql_syscall_epoll_wait(ql: Qiling, epfd: int, epoll_events: int, maxevents: i if interest_mask & EPOLLONESHOT: epoll_parent_obj.delist_fd(fd) - data = ql.pack32(interest_mask) + ql.pack(fd) + # FIXME: the data packed after events should be the one passed on epoll_ctl + # for that specific fd. currently this does not align with the spec + data = ql.pack32(interest_mask) + ql.pack64(fd) offset = len(data) * i # Resolved elicn remark, ql_event was dead code ql.mem.write(epoll_events + offset, data) From bf83e9c2cf6bef787ce80a6470eb6e4a1d55d12d Mon Sep 17 00:00:00 2001 From: elicn Date: Wed, 30 Apr 2025 13:52:16 +0300 Subject: [PATCH 18/32] Cleanup and minor cosmetics --- qiling/os/posix/syscall/epoll.py | 48 +++++++++++++------------------- 1 file changed, 19 insertions(+), 29 deletions(-) diff --git a/qiling/os/posix/syscall/epoll.py b/qiling/os/posix/syscall/epoll.py index 8dadd212f..d3eb0abed 100644 --- a/qiling/os/posix/syscall/epoll.py +++ b/qiling/os/posix/syscall/epoll.py @@ -1,18 +1,17 @@ +from __future__ import annotations + import select -from typing import TYPE_CHECKING, Dict, List +from typing import TYPE_CHECKING, Dict, KeysView -from qiling import * -from qiling.const import * from qiling.os.posix.const import * -from qiling.os.const import * -from qiling.os.filestruct import ql_file -from qiling.os.filestruct import PersistentQlFile +from qiling.os.filestruct import PersistentQlFile, ql_file if TYPE_CHECKING: + from qiling import Qiling from qiling.os.posix.posix import QlFileDes -from qiling.os.posix.posix import QlFileDes + class QlEpollObj: def __init__(self, epoll_object: select.epoll): @@ -24,8 +23,8 @@ def __init__(self, epoll_object: select.epoll): self._fds: Dict[int, int] = {} @property - def fds(self) -> List[int]: - return list(self._fds.keys()) + def fds(self) -> KeysView[int]: + return self._fds.keys() @property def epoll_instance(self) -> select.epoll: @@ -38,10 +37,10 @@ def set_eventmask(self, fd: int, newmask: int): # the mask for an FD shouldn't ever be undefined # as it is set whenever an FD is added for a QlEpollObj instance - # libumem: resolved elicn feedback newmask = self.get_eventmask(fd) | newmask - self._fds[fd] = newmask + self._epoll_object.modify(fd, newmask) + self._fds[fd] = newmask def monitor_fd(self, fd: int, eventmask: int) -> None: # tell the epoll object to watch the fd arg, looking for events matching the eventmask @@ -53,7 +52,7 @@ def delist_fd(self, fd: int) -> None: self._epoll_object.unregister(fd) def close(self) -> None: - self.epoll_instance.close() + self._epoll_object.close() def __contains__(self, fd: int) -> bool: """Test whether a specific fd is already being watched by this epoll instance. @@ -111,28 +110,20 @@ def ql_syscall_epoll_ctl(ql: Qiling, epfd: int, op: int, fd: int, event: int): # EPOLLWAKEUP (since Linux 3.5) # If EPOLLONESHOT and EPOLLET are clear and the process has the CAP_BLOCK_SUSPEND capability - # 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 - 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 - # elicn: not sure how the following condition even possible after we checked that op can - # be only one of EPOLL_CTL_{ADD,DEL,MOD} (originally checked with a dict) - - # EPOLLEXCLUSIVE was specified in event and fd refers to an epoll instance - if isinstance(fd_obj, QlEpollObj) and (op & EPOLLEXCLUSIVE): - return -EINVAL - try: # 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 @@ -210,22 +201,21 @@ def ql_syscall_epoll_wait(ql: Qiling, epfd: int, epoll_events: int, maxevents: i if epoll_obj is None: return -EBADF - ready_fds = epoll_obj.poll(timeout, maxevents) # Each tuple in ready_fds consists of (file descriptor, eventmask) so we iterate # through these to indicate which fds are ready and 'why' - for i, (fd, interest_mask) in enumerate(ready_fds): + for i, (fd, events) in enumerate(ready_fds): # if no longer interested in this fd, remove from list - if interest_mask & EPOLLONESHOT: + if events & EPOLLONESHOT: epoll_parent_obj.delist_fd(fd) # FIXME: the data packed after events should be the one passed on epoll_ctl # for that specific fd. currently this does not align with the spec - data = ql.pack32(interest_mask) + ql.pack64(fd) + data = ql.pack32(events) + ql.pack64(fd) offset = len(data) * i - # Resolved elicn remark, ql_event was dead code + ql.mem.write(epoll_events + offset, data) return len(ready_fds) From 5a87323df352fc4925e1b7c61169cddbbd71781c Mon Sep 17 00:00:00 2001 From: elicn Date: Wed, 30 Apr 2025 13:52:40 +0300 Subject: [PATCH 19/32] Tidy up tests --- examples/src/linux/Makefile | 3 +- tests/test_elf.py | 86 +++++++++++++++++++------------------ 2 files changed, 47 insertions(+), 42 deletions(-) diff --git a/examples/src/linux/Makefile b/examples/src/linux/Makefile index 8a26e589f..006ea4907 100644 --- a/examples/src/linux/Makefile +++ b/examples/src/linux/Makefile @@ -33,7 +33,7 @@ TARGETS = \ x8664_hello_cpp \ x8664_hello_cpp_static \ x8664_cloexec_test \ - x8664_linux_onestraw \ + x8664_linux_onestraw \ patch_test.bin .PHONY: all clean @@ -125,6 +125,7 @@ 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 $@ $< diff --git a/tests/test_elf.py b/tests/test_elf.py index aae738985..8928e7a59 100644 --- a/tests/test_elf.py +++ b/tests/test_elf.py @@ -772,63 +772,67 @@ def test_elf_linux_x8664_path_traversion(self): self.assertNotIn("root\n", ql.os.stdout.read().decode("utf-8")) del ql - - """ - This tests a sample binary that (e)polls on stdin - and echos back the output. Upon receiving 'stop', it - will exit. - """ - @unittest.skip("TODO: Stdin hijacking doesn't work as expected") + + @unittest.skip("stdin hijacking doesn't work as expected") def test_elf_linux_x8664_epoll_simple(self): - # epoll-0 source: https://github.com/maxasm/epoll-c/blob/main/main.c + # 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("echo\n", ql.os.stdout.read().decode("utf-8")) - del ql - """ - 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. - """ - #@unittest.skip('See PR') - def test_elf_linux_x8664_epoll_server(self): - # Source for onestraw server: https://github.com/onestraw/epoll-example + ql.os.stdin.write(b'stop\n') # signal to exit gracefully + ql.run() - """ - 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: POINTER, statbuf: POINTER, flags:int): + 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(): - time.sleep(3) # give time for the server to listen + # give time for the server to listen + time.sleep(3) + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - dest = ("127.0.0.1", 8000) - s.connect(dest) - test = b"hello world" - s.send(test) + s.connect(("127.0.0.1", 8000)) + s.send(b"hello world") s.close() - client_thread = threading.Thread(target=client, daemon=True) - client_thread.start() + rootfs = "../examples/rootfs/x8664_linux_glibc2.39" argv = r"../examples/rootfs/x8664_linux/bin/x8664_onestraw_server s".split() # s means 'server mode' - ql = Qiling(argv, rootfs, multithread=False, verbose=QL_VERBOSE.DEBUG) - ql.os.set_syscall("newfstatat",hook_newfstatat, QL_INTERCEPT.CALL) + + 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('hello world', ql.os.stdout.read().decode("utf-8")) + + self.assertIn(b'hello world', ql.os.stdout.read()) del ql + + if __name__ == "__main__": unittest.main() From 12d3c58162665294730873a4468c32904a0a8f9b Mon Sep 17 00:00:00 2001 From: libumem <163767094+libumem@users.noreply.github.com> Date: Wed, 7 May 2025 00:39:23 +0000 Subject: [PATCH 20/32] Address last of feedback --- qiling/os/posix/syscall/epoll.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/qiling/os/posix/syscall/epoll.py b/qiling/os/posix/syscall/epoll.py index 7b318bd40..c11033785 100644 --- a/qiling/os/posix/syscall/epoll.py +++ b/qiling/os/posix/syscall/epoll.py @@ -12,7 +12,7 @@ if TYPE_CHECKING: from qiling.os.posix.posix import QlFileDes -from qiling.os.posix.posix import QlFileDes +#from qiling.os.posix.posix import QlFileDes class QlEpollObj: def __init__(self, epoll_object: select.epoll): @@ -59,10 +59,10 @@ def is_present(self, fd: int) -> bool: return fd in self.fds -def check_epoll_depth(ql_fd_list: QlFileDes, epolls_list: List[QlEpollObj], depth: int = 0) -> None: +def check_epoll_depth(ql_fd_list, epolls_list: List[QlEpollObj], depth: int = 0) -> None: # Recursively checks each epoll instance's 'watched' fds for an instance of - # epoll being watched. If a chain of over 5 levels is detected, return 1, - # which will return ELOOP in ql_syscall_epoll_wait + # epoll being watched. If a chain of over 5 levels is detected, raise + # an exception if depth >= 5: raise RecursionError @@ -78,11 +78,9 @@ def check_epoll_depth(ql_fd_list: QlFileDes, epolls_list: List[QlEpollObj], dept if isinstance(obj, QlEpollObj): new_epolls_list.append(obj) - # elicn: new_epolls_list is not cleared between loop iterations, rather it keeps - # aggregating items from previous iterations. is this what we want? - if new_epolls_list: check_epoll_depth(ql_fd_list, new_epolls_list, depth + 1) + new_epolls_list = [] def ql_syscall_epoll_ctl(ql: Qiling, epfd: int, op: int, fd: int, event: int): @@ -131,8 +129,6 @@ def ql_syscall_epoll_ctl(ql: Qiling, epfd: int, op: int, fd: int, event: int): if isinstance(fd_obj, ql_file) and not isinstance(fd_obj, PersistentQlFile): return -EPERM - # elicn: not sure how the following condition even possible after we checked that op can - # be only one of EPOLL_CTL_{ADD,DEL,MOD} (originally checked with a dict) # EPOLLEXCLUSIVE was specified in event and fd refers to an epoll instance if isinstance(fd_obj, QlEpollObj) and (op & EPOLLEXCLUSIVE): From e4242ca741116c66fdde120e003b608559ef0b92 Mon Sep 17 00:00:00 2001 From: libumem <163767094+libumem@users.noreply.github.com> Date: Thu, 8 May 2025 00:10:34 +0000 Subject: [PATCH 21/32] Fix mem read issue --- qiling/os/posix/syscall/epoll.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/qiling/os/posix/syscall/epoll.py b/qiling/os/posix/syscall/epoll.py index bd5a4acc4..dd8fccf17 100644 --- a/qiling/os/posix/syscall/epoll.py +++ b/qiling/os/posix/syscall/epoll.py @@ -154,7 +154,6 @@ def ql_syscall_epoll_ctl(ql: Qiling, epfd: int, op: int, fd: int, event: int): return -EINVAL event_ptr = ql.mem.read_ptr(event) - events = ql.mem.read_ptr(event_ptr, 4) # EPOLLEXCLUSIVE was specified in event and fd refers to an epoll instance if isinstance(fd_obj, QlEpollObj) and (op & EPOLLEXCLUSIVE): @@ -162,7 +161,7 @@ def ql_syscall_epoll_ctl(ql: Qiling, epfd: int, op: int, fd: int, event: int): # add to list of fds to be monitored with per-fd eventmask register will actual epoll # instance and add eventmask accordingly - epoll_parent_obj.monitor_fd(fd, events) + epoll_parent_obj.monitor_fd(fd, event_ptr) elif op == EPOLL_CTL_DEL: if fd not in epoll_parent_obj: @@ -179,13 +178,12 @@ def ql_syscall_epoll_ctl(ql: Qiling, epfd: int, op: int, fd: int, event: int): return -EINVAL event_ptr = ql.mem.read_ptr(event) - events = ql.mem.read_ptr(event_ptr, 4) # EPOLLEXCLUSIVE cannot be set on MOD operation, only on ADD if events & EPOLLEXCLUSIVE: return -EINVAL - epoll_parent_obj.set_eventmask(fd, events) + epoll_parent_obj.set_eventmask(fd, event_ptr) return 0 @@ -226,7 +224,7 @@ def ql_syscall_epoll_wait(ql: Qiling, epfd: int, epoll_events: int, maxevents: i # FIXME: the data packed after events should be the one passed on epoll_ctl # for that specific fd. currently this does not align with the spec - data = ql.pack32(events) + ql.pack64(fd) + data = ql.pack32(events) + ql.pack(fd) offset = len(data) * i ql.mem.write(epoll_events + offset, data) From f9497bc7d6998c02bef5dd4cddf18e9132b24c7a Mon Sep 17 00:00:00 2001 From: libumem <163767094+libumem@users.noreply.github.com> Date: Fri, 9 May 2025 03:26:43 +0000 Subject: [PATCH 22/32] Address more feedback --- qiling/os/posix/syscall/epoll.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/qiling/os/posix/syscall/epoll.py b/qiling/os/posix/syscall/epoll.py index dd8fccf17..659faf43c 100644 --- a/qiling/os/posix/syscall/epoll.py +++ b/qiling/os/posix/syscall/epoll.py @@ -107,6 +107,18 @@ def ql_syscall_epoll_ctl(ql: Qiling, epfd: int, op: int, fd: int, event: int): if epoll_obj.fileno() == fd: return -ELOOP + events = 0 + if event: + events = ql.mem.read_ptr(event,4) + """ + event is of type epoll_event. run man epoll_event for more info + struct epoll_event { + uint32_t events; /* Epoll events */ + epoll_data_t data; /* User data variable */ + }; + so, read 4 bytes for the events field + """ + # Qiling doesn't check process capabilities right now, so this case isn't explicitly handled yet # EPOLLWAKEUP (since Linux 3.5) @@ -153,7 +165,7 @@ def ql_syscall_epoll_ctl(ql: Qiling, epfd: int, op: int, fd: int, event: int): if not event: return -EINVAL - event_ptr = ql.mem.read_ptr(event) + # EPOLLEXCLUSIVE was specified in event and fd refers to an epoll instance if isinstance(fd_obj, QlEpollObj) and (op & EPOLLEXCLUSIVE): @@ -161,7 +173,7 @@ def ql_syscall_epoll_ctl(ql: Qiling, epfd: int, op: int, fd: int, event: int): # add to list of fds to be monitored with per-fd eventmask register will actual epoll # instance and add eventmask accordingly - epoll_parent_obj.monitor_fd(fd, event_ptr) + epoll_parent_obj.monitor_fd(fd, events) elif op == EPOLL_CTL_DEL: if fd not in epoll_parent_obj: @@ -177,13 +189,12 @@ def ql_syscall_epoll_ctl(ql: Qiling, epfd: int, op: int, fd: int, event: int): if not event: return -EINVAL - event_ptr = ql.mem.read_ptr(event) # EPOLLEXCLUSIVE cannot be set on MOD operation, only on ADD if events & EPOLLEXCLUSIVE: return -EINVAL - epoll_parent_obj.set_eventmask(fd, event_ptr) + epoll_parent_obj.set_eventmask(fd, events) return 0 @@ -222,9 +233,8 @@ def ql_syscall_epoll_wait(ql: Qiling, epfd: int, epoll_events: int, maxevents: i if events & EPOLLONESHOT: epoll_parent_obj.delist_fd(fd) - # FIXME: the data packed after events should be the one passed on epoll_ctl - # for that specific fd. currently this does not align with the spec - data = ql.pack32(events) + ql.pack(fd) + # https://elixir.bootlin.com/linux/v6.14.4/source/include/uapi/linux/eventpoll.h#L83 + data = ql.pack32(events) + ql.pack64(fd) offset = len(data) * i ql.mem.write(epoll_events + offset, data) From 47c3df1eb5cf3fccb71319137ae88bbea42a153b Mon Sep 17 00:00:00 2001 From: libumem <163767094+libumem@users.noreply.github.com> Date: Fri, 16 May 2025 02:23:58 +0000 Subject: [PATCH 23/32] Fix root for test case --- tests/test_elf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_elf.py b/tests/test_elf.py index 8928e7a59..3a5af4cd7 100644 --- a/tests/test_elf.py +++ b/tests/test_elf.py @@ -817,7 +817,7 @@ def client(): s.send(b"hello world") s.close() - rootfs = "../examples/rootfs/x8664_linux_glibc2.39" + 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) From 4d0b939da5b5e626c8f979402bda469a25a0820a Mon Sep 17 00:00:00 2001 From: libumem <163767094+libumem@users.noreply.github.com> Date: Mon, 2 Jun 2025 23:27:36 +0000 Subject: [PATCH 24/32] Revert "Fix root for test case" This reverts commit 47c3df1eb5cf3fccb71319137ae88bbea42a153b. --- tests/test_elf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_elf.py b/tests/test_elf.py index 3a5af4cd7..8928e7a59 100644 --- a/tests/test_elf.py +++ b/tests/test_elf.py @@ -817,7 +817,7 @@ def client(): s.send(b"hello world") s.close() - rootfs = "../examples/rootfs/x8664_linux" + rootfs = "../examples/rootfs/x8664_linux_glibc2.39" argv = r"../examples/rootfs/x8664_linux/bin/x8664_onestraw_server s".split() # s means 'server mode' ql = Qiling(argv, rootfs, verbose=QL_VERBOSE.DEBUG) From 34fcfb56b54aae46a1b3019923191b8190a42fc9 Mon Sep 17 00:00:00 2001 From: libumem <163767094+libumem@users.noreply.github.com> Date: Mon, 2 Jun 2025 23:35:17 +0000 Subject: [PATCH 25/32] Revert "Address more feedback" This reverts commit f9497bc7d6998c02bef5dd4cddf18e9132b24c7a. --- qiling/os/posix/syscall/epoll.py | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/qiling/os/posix/syscall/epoll.py b/qiling/os/posix/syscall/epoll.py index 659faf43c..dd8fccf17 100644 --- a/qiling/os/posix/syscall/epoll.py +++ b/qiling/os/posix/syscall/epoll.py @@ -107,18 +107,6 @@ def ql_syscall_epoll_ctl(ql: Qiling, epfd: int, op: int, fd: int, event: int): if epoll_obj.fileno() == fd: return -ELOOP - events = 0 - if event: - events = ql.mem.read_ptr(event,4) - """ - event is of type epoll_event. run man epoll_event for more info - struct epoll_event { - uint32_t events; /* Epoll events */ - epoll_data_t data; /* User data variable */ - }; - so, read 4 bytes for the events field - """ - # Qiling doesn't check process capabilities right now, so this case isn't explicitly handled yet # EPOLLWAKEUP (since Linux 3.5) @@ -165,7 +153,7 @@ def ql_syscall_epoll_ctl(ql: Qiling, epfd: int, op: int, fd: int, event: int): if not event: return -EINVAL - + event_ptr = ql.mem.read_ptr(event) # EPOLLEXCLUSIVE was specified in event and fd refers to an epoll instance if isinstance(fd_obj, QlEpollObj) and (op & EPOLLEXCLUSIVE): @@ -173,7 +161,7 @@ def ql_syscall_epoll_ctl(ql: Qiling, epfd: int, op: int, fd: int, event: int): # add to list of fds to be monitored with per-fd eventmask register will actual epoll # instance and add eventmask accordingly - epoll_parent_obj.monitor_fd(fd, events) + epoll_parent_obj.monitor_fd(fd, event_ptr) elif op == EPOLL_CTL_DEL: if fd not in epoll_parent_obj: @@ -189,12 +177,13 @@ def ql_syscall_epoll_ctl(ql: Qiling, epfd: int, op: int, fd: int, event: int): if not event: return -EINVAL + event_ptr = ql.mem.read_ptr(event) # EPOLLEXCLUSIVE cannot be set on MOD operation, only on ADD if events & EPOLLEXCLUSIVE: return -EINVAL - epoll_parent_obj.set_eventmask(fd, events) + epoll_parent_obj.set_eventmask(fd, event_ptr) return 0 @@ -233,8 +222,9 @@ def ql_syscall_epoll_wait(ql: Qiling, epfd: int, epoll_events: int, maxevents: i if events & EPOLLONESHOT: epoll_parent_obj.delist_fd(fd) - # https://elixir.bootlin.com/linux/v6.14.4/source/include/uapi/linux/eventpoll.h#L83 - data = ql.pack32(events) + ql.pack64(fd) + # FIXME: the data packed after events should be the one passed on epoll_ctl + # for that specific fd. currently this does not align with the spec + data = ql.pack32(events) + ql.pack(fd) offset = len(data) * i ql.mem.write(epoll_events + offset, data) From 82de6f9d3a45eb0ef6da1d50d6053c6bb0b8517c Mon Sep 17 00:00:00 2001 From: libumem <163767094+libumem@users.noreply.github.com> Date: Mon, 2 Jun 2025 23:35:23 +0000 Subject: [PATCH 26/32] Revert "Fix mem read issue" This reverts commit e4242ca741116c66fdde120e003b608559ef0b92. --- qiling/os/posix/syscall/epoll.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/qiling/os/posix/syscall/epoll.py b/qiling/os/posix/syscall/epoll.py index dd8fccf17..bd5a4acc4 100644 --- a/qiling/os/posix/syscall/epoll.py +++ b/qiling/os/posix/syscall/epoll.py @@ -154,6 +154,7 @@ def ql_syscall_epoll_ctl(ql: Qiling, epfd: int, op: int, fd: int, event: int): return -EINVAL event_ptr = ql.mem.read_ptr(event) + events = ql.mem.read_ptr(event_ptr, 4) # EPOLLEXCLUSIVE was specified in event and fd refers to an epoll instance if isinstance(fd_obj, QlEpollObj) and (op & EPOLLEXCLUSIVE): @@ -161,7 +162,7 @@ def ql_syscall_epoll_ctl(ql: Qiling, epfd: int, op: int, fd: int, event: int): # add to list of fds to be monitored with per-fd eventmask register will actual epoll # instance and add eventmask accordingly - epoll_parent_obj.monitor_fd(fd, event_ptr) + epoll_parent_obj.monitor_fd(fd, events) elif op == EPOLL_CTL_DEL: if fd not in epoll_parent_obj: @@ -178,12 +179,13 @@ def ql_syscall_epoll_ctl(ql: Qiling, epfd: int, op: int, fd: int, event: int): return -EINVAL event_ptr = ql.mem.read_ptr(event) + events = ql.mem.read_ptr(event_ptr, 4) # EPOLLEXCLUSIVE cannot be set on MOD operation, only on ADD if events & EPOLLEXCLUSIVE: return -EINVAL - epoll_parent_obj.set_eventmask(fd, event_ptr) + epoll_parent_obj.set_eventmask(fd, events) return 0 @@ -224,7 +226,7 @@ def ql_syscall_epoll_wait(ql: Qiling, epfd: int, epoll_events: int, maxevents: i # FIXME: the data packed after events should be the one passed on epoll_ctl # for that specific fd. currently this does not align with the spec - data = ql.pack32(events) + ql.pack(fd) + data = ql.pack32(events) + ql.pack64(fd) offset = len(data) * i ql.mem.write(epoll_events + offset, data) From 5ad49c0fdc2ded8a4ece5b6aa669eaa8adbd73c8 Mon Sep 17 00:00:00 2001 From: libumem <163767094+libumem@users.noreply.github.com> Date: Mon, 2 Jun 2025 23:37:38 +0000 Subject: [PATCH 27/32] Revert "Revert "Fix root for test case"" This reverts commit 4d0b939da5b5e626c8f979402bda469a25a0820a. --- tests/test_elf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_elf.py b/tests/test_elf.py index 8928e7a59..3a5af4cd7 100644 --- a/tests/test_elf.py +++ b/tests/test_elf.py @@ -817,7 +817,7 @@ def client(): s.send(b"hello world") s.close() - rootfs = "../examples/rootfs/x8664_linux_glibc2.39" + 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) From ca0d35a51db43c161556a9bd9d3352307cb59273 Mon Sep 17 00:00:00 2001 From: elicn Date: Tue, 3 Jun 2025 13:19:21 +0300 Subject: [PATCH 28/32] Introduce packed struct --- qiling/os/struct.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) 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 = { From 31720cfca67414f0c0409fe26232437583bc25a0 Mon Sep 17 00:00:00 2001 From: elicn Date: Tue, 3 Jun 2025 13:20:02 +0300 Subject: [PATCH 29/32] Refactor epoll to rely on ctypess tructure --- qiling/os/posix/syscall/epoll.py | 114 ++++++++++++++++++++----------- 1 file changed, 76 insertions(+), 38 deletions(-) diff --git a/qiling/os/posix/syscall/epoll.py b/qiling/os/posix/syscall/epoll.py index bd5a4acc4..50b70c6ce 100644 --- a/qiling/os/posix/syscall/epoll.py +++ b/qiling/os/posix/syscall/epoll.py @@ -1,17 +1,48 @@ from __future__ import annotations +import ctypes import select -from typing import TYPE_CHECKING, Dict, KeysView +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 @@ -19,7 +50,7 @@ def __init__(self, epoll_object: select.epoll): # 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, int] = {} + self._fds: Dict[int, QlEpollEntry] = {} @property def fds(self) -> KeysView[int]: @@ -29,31 +60,30 @@ def fds(self) -> KeysView[int]: def epoll_instance(self) -> select.epoll: return self._epoll_object - def get_eventmask(self, fd: int) -> int: + def close(self) -> None: + self._epoll_object.close() + + def __getitem__(self, fd: int) -> QlEpollEntry: return self._fds[fd] - def set_eventmask(self, fd: int, newmask: int): - # the mask for an FD shouldn't ever be undefined - # as it is set whenever an FD is added for a QlEpollObj instance + 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) - newmask = self.get_eventmask(fd) | newmask + # otherwise, register it with the epoll object + else: + self._epoll_object.register(fd, entry.events) - self._epoll_object.modify(fd, newmask) - self._fds[fd] = newmask + self._fds[fd] = entry - def monitor_fd(self, fd: int, eventmask: int) -> None: - # tell the epoll object to watch the fd arg, looking for events matching the eventmask - self._epoll_object.register(fd, eventmask) - self._fds[fd] = eventmask + def __delitem__(self, fd: int) -> None: + """Remove an fd from the epoll instance. + """ - def delist_fd(self, fd: int) -> None: self._fds.pop(fd) self._epoll_object.unregister(fd) - def close(self) -> None: - self._epoll_object.close() - - def __contains__(self, fd: int) -> bool: """Test whether a specific fd is already being watched by this epoll instance. """ @@ -153,23 +183,25 @@ def ql_syscall_epoll_ctl(ql: Qiling, epfd: int, op: int, fd: int, event: int): if not event: return -EINVAL - event_ptr = ql.mem.read_ptr(event) - events = ql.mem.read_ptr(event_ptr, 4) + # 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 (op & EPOLLEXCLUSIVE): + if isinstance(fd_obj, QlEpollObj) and (epoll_event.events & EPOLLEXCLUSIVE): return -EINVAL - # add to list of fds to be monitored with per-fd eventmask register will actual epoll - # instance and add eventmask accordingly - epoll_parent_obj.monitor_fd(fd, events) + 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 - epoll_parent_obj.delist_fd(fd) + del epoll_parent_obj[fd] elif op == EPOLL_CTL_MOD: if fd not in epoll_parent_obj: @@ -178,14 +210,18 @@ def ql_syscall_epoll_ctl(ql: Qiling, epfd: int, op: int, fd: int, event: int): if not event: return -EINVAL - event_ptr = ql.mem.read_ptr(event) - events = ql.mem.read_ptr(event_ptr, 4) + # 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 events & EPOLLEXCLUSIVE: + if epoll_event.events & EPOLLEXCLUSIVE: return -EINVAL - epoll_parent_obj.set_eventmask(fd, events) + epoll_parent_obj[fd] = QlEpollEntry( + epoll_event.events, + epoll_event.data + ) return 0 @@ -216,20 +252,22 @@ def ql_syscall_epoll_wait(ql: Qiling, epfd: int, epoll_events: int, maxevents: i 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): - # if no longer interested in this fd, remove from list - if events & EPOLLONESHOT: - epoll_parent_obj.delist_fd(fd) + entry = epoll_parent_obj[fd] + epoll_event = epoll_event_cls(events, entry.data) - # FIXME: the data packed after events should be the one passed on epoll_ctl - # for that specific fd. currently this does not align with the spec - data = ql.pack32(events) + ql.pack64(fd) - offset = len(data) * i + offset = epoll_event_cls.sizeof() * i + ql.mem.write(epoll_events + offset, bytes(epoll_event)) - ql.mem.write(epoll_events + offset, data) + # if no longer interested in this fd, remove from list + if events & EPOLLONESHOT: + del epoll_parent_obj[fd] return len(ready_fds) From 92e43a35527ff7e4ca31694959c10d66ddfa118f Mon Sep 17 00:00:00 2001 From: libumem <163767094+libumem@users.noreply.github.com> Date: Wed, 4 Jun 2025 00:40:36 +0000 Subject: [PATCH 30/32] Fix glibc issue with new test root --- tests/test_elf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_elf.py b/tests/test_elf.py index 3a5af4cd7..82c386c03 100644 --- a/tests/test_elf.py +++ b/tests/test_elf.py @@ -817,7 +817,7 @@ def client(): s.send(b"hello world") s.close() - rootfs = "../examples/rootfs/x8664_linux" + rootfs = "../examples/rootfs/x8664_linux_glibc2.39" # fix GLIBC version requirement argv = r"../examples/rootfs/x8664_linux/bin/x8664_onestraw_server s".split() # s means 'server mode' ql = Qiling(argv, rootfs, verbose=QL_VERBOSE.DEBUG) From fdfec5b6d1b1abffaa1d815038cf512bd19ea82f Mon Sep 17 00:00:00 2001 From: libumem <163767094+libumem@users.noreply.github.com> Date: Tue, 17 Jun 2025 22:39:45 +0000 Subject: [PATCH 31/32] Attempt #1 to fix test case root --- tests/test_elf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_elf.py b/tests/test_elf.py index 82c386c03..3a5af4cd7 100644 --- a/tests/test_elf.py +++ b/tests/test_elf.py @@ -817,7 +817,7 @@ def client(): s.send(b"hello world") s.close() - rootfs = "../examples/rootfs/x8664_linux_glibc2.39" # fix GLIBC version requirement + 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) From d5b51dd7247fa82f49ab01301dad9b703625640e Mon Sep 17 00:00:00 2001 From: libumem <163767094+libumem@users.noreply.github.com> Date: Sun, 22 Jun 2025 23:57:28 +0000 Subject: [PATCH 32/32] Remove dup'd comment, locally tested fix for error during ELFTest suite --- qiling/os/posix/syscall/epoll.py | 4 ---- tests/test_elf.py | 5 ++--- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/qiling/os/posix/syscall/epoll.py b/qiling/os/posix/syscall/epoll.py index 50b70c6ce..469116781 100644 --- a/qiling/os/posix/syscall/epoll.py +++ b/qiling/os/posix/syscall/epoll.py @@ -88,7 +88,6 @@ def __contains__(self, fd: int) -> bool: """Test whether a specific fd is already being watched by this epoll instance. """ - return fd in self.fds @@ -168,9 +167,6 @@ def ql_syscall_epoll_ctl(ql: Qiling, epfd: int, op: int, fd: int, event: int): epolls_list = [fobj for fobj in ql.os.fd if isinstance(fobj, QlEpollObj)] try: - # 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 reserved for tracking epoll objects. check_epoll_depth(ql.os.fd) except RecursionError: return -ELOOP diff --git a/tests/test_elf.py b/tests/test_elf.py index 3a5af4cd7..6afd49740 100644 --- a/tests/test_elf.py +++ b/tests/test_elf.py @@ -783,8 +783,7 @@ def test_elf_linux_x8664_epoll_simple(self): 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 = pipe.SimpleInStream(0) ql.os.stdin.write(b'echo\n') ql.os.stdin.write(b'stop\n') # signal to exit gracefully ql.run() @@ -830,7 +829,7 @@ def client(): ql.run() - self.assertIn(b'hello world', ql.os.stdout.read()) + self.assertIn(b'hello world', ql.os.stdout.read(200)) # 200 is arbitrary--"good enough" for this task del ql