Merge pull request #2 from elicn/libumem-epoll-fixes

Libumem epoll fixes
This commit is contained in:
libumem
2025-05-07 19:51:53 -04:00
committed by GitHub
3 changed files with 110 additions and 90 deletions
+2 -1
View File
@@ -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 $@ $<
+67 -52
View File
@@ -1,18 +1,16 @@
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 +22,8 @@ class QlEpollObj:
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 +36,10 @@ class QlEpollObj:
# 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,34 +51,34 @@ class QlEpollObj:
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.
"""
def is_present(self, fd: int) -> bool:
return fd in self.fds
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, raise
# an exception
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 depth >= 5:
raise RecursionError
def __visit_obj(obj: QlEpollObj, depth: int):
if depth >= 5:
raise RecursionError
new_epolls_list = []
for fd in obj.fds:
if isinstance(ql_fd_list[fd], QlEpollObj):
__visit_obj(obj, depth + 1)
for ent in epolls_list:
watched = ent.fds
for obj in ql_fd_list:
if isinstance(obj, QlEpollObj):
__visit_obj(obj, 1)
for w in watched:
obj = ql_fd_list[w]
if isinstance(obj, QlEpollObj):
new_epolls_list.append(obj)
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):
@@ -114,22 +112,22 @@ 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
# EPOLLEXCLUSIVE was specified in event and fd refers to an epoll instance
if isinstance(fd_obj, QlEpollObj) and (op & EPOLLEXCLUSIVE):
return -EINVAL
@@ -140,38 +138,54 @@ 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:
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
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):
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 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.
if op & EPOLLEXCLUSIVE and fd in epoll_obj.fds:
if not event:
return -EINVAL
epoll_parent_obj.set_eventmask(fd, ql_event)
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)
return 0
@@ -200,20 +214,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)
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(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)
+41 -37
View File
@@ -772,63 +772,67 @@ class ELFTest(unittest.TestCase):
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.os.stdin.write(b'stop\n') # signal to exit gracefully
ql.run()
self.assertIn("echo\n", ql.os.stdout.read().decode("utf-8"))
self.assertIn(b'echo\n', ql.os.stdout.read())
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
# 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: POINTER, statbuf: POINTER, flags:int):
# 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()