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)