Skip to content

Commit 486369d

Browse files
author
xwings
committed
better New SYSCALL_EX IPC message, FD arg type and Forwarder closure
1 parent cb875f8 commit 486369d

6 files changed

Lines changed: 506 additions & 33 deletions

File tree

.codex

Whitespace-only changes.

qiling/os/posix/kernel_proxy/__init__.py

Lines changed: 127 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,22 @@
88
99
Usage:
1010
from qiling import Qiling
11-
from qiling.os.posix.kernel_proxy import KernelProxy
11+
from qiling.os.posix.kernel_proxy import KernelProxy, FD, PtrIn, PtrOut
1212
1313
ql = Qiling(argv=["/bin/myserver"], rootfs="rootfs/x8664_linux")
1414
proxy = KernelProxy(ql)
15-
proxy.forward_syscall("epoll_create", returns_fd=True)
16-
proxy.forward_syscall("epoll_ctl")
17-
proxy.forward_syscall("epoll_wait")
15+
16+
# integer-arg syscall returning a new FD
17+
proxy.forward_syscall("epoll_create1", returns_fd=True)
18+
19+
# epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
20+
proxy.forward_syscall("epoll_ctl",
21+
arg_types=(FD, "int", FD, PtrIn(size=12)))
22+
23+
# epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
24+
proxy.forward_syscall("epoll_wait",
25+
arg_types=(FD, PtrOut(size=lambda a: a[2] * 12), "int", "int"))
26+
1827
ql.run()
1928
"""
2029

@@ -24,17 +33,24 @@
2433
import sys
2534
import socket
2635
import subprocess
27-
from typing import Dict, Optional, TYPE_CHECKING
36+
import weakref
37+
from typing import Dict, Optional, Sequence, Tuple, TYPE_CHECKING
2838

29-
from qiling.const import QL_INTERCEPT, QL_OS
39+
from qiling.const import QL_INTERCEPT
3040
from qiling.exception import QlErrorArch, QlErrorSyscallError, QlErrorSyscallNotFound
41+
from qiling.os.posix.kernel_proxy.argtypes import (
42+
INT, FD, PtrIn, PtrOut, PtrInOut, is_pointer,
43+
)
3144
from qiling.os.posix.kernel_proxy.ipc import ProxyClient
3245
from qiling.os.posix.kernel_proxy.proxy_fd import ql_proxy_fd
3346

3447
if TYPE_CHECKING:
3548
from qiling import Qiling
3649

3750

51+
__all__ = ['KernelProxy', 'INT', 'FD', 'PtrIn', 'PtrOut', 'PtrInOut']
52+
53+
3854
class KernelProxy:
3955
"""Forward specific syscalls to a real Linux kernel via a helper process.
4056
@@ -117,40 +133,74 @@ def _resolve_syscall_nr(self, name: str) -> int:
117133
)
118134
return table[name]
119135

120-
def forward_syscall(self, name: str, returns_fd: bool = False):
136+
def forward_syscall(self, name: str, returns_fd: bool = False,
137+
arg_types: Optional[Sequence] = None):
121138
"""Register a CALL hook that forwards this syscall to the kernel proxy.
122139
123140
Args:
124-
name: syscall name (e.g. "epoll_create", "eventfd2")
141+
name: syscall name (e.g. "epoll_create1", "eventfd2").
125142
returns_fd: if True, wrap the return value in ql_proxy_fd and store
126-
in the Qiling FD table. Use this for syscalls that return
127-
file descriptors (epoll_create, eventfd, timerfd_create, etc.)
143+
it in the Qiling FD table. Use this for syscalls that
144+
return file descriptors (epoll_create1, eventfd2, etc.).
145+
arg_types: optional per-arg descriptors. Each entry is one of:
146+
INT (or "int") — pass through unchanged (default).
147+
FD (or "fd") — guest FD; translated to the proxy FD.
148+
PtrIn(size) — pointer; bytes copied from guest to proxy.
149+
PtrOut(size) — pointer; bytes copied back from proxy to guest.
150+
PtrInOut(size) — pointer; both directions.
151+
If omitted, all arguments are treated as INT.
128152
"""
129153
nr = self._resolve_syscall_nr(name)
130154
self._forwarded[name] = nr
131155

132-
forwarder = self._make_forwarder(name, nr, returns_fd)
156+
forwarder = self._make_forwarder(name, nr, returns_fd, arg_types)
133157
self.ql.os.set_syscall(name, forwarder, QL_INTERCEPT.CALL)
134158

135-
self.ql.log.info(f"forwarding syscall '{name}' (nr={nr}) to kernel proxy"
136-
f"{' [returns FD]' if returns_fd else ''}")
159+
kind = []
160+
if returns_fd:
161+
kind.append('returns FD')
162+
if arg_types:
163+
kind.append(f'arg_types={tuple(type(a).__name__ if not isinstance(a, str) else a for a in arg_types)}')
137164

138-
def _make_forwarder(self, name: str, guest_nr: int, returns_fd: bool):
139-
"""Create a CALL hook closure for one syscall."""
165+
suffix = f" [{', '.join(kind)}]" if kind else ''
166+
self.ql.log.info(f"forwarding syscall '{name}' (nr={nr}) to kernel proxy{suffix}")
167+
168+
def _make_forwarder(self, name: str, guest_nr: int, returns_fd: bool,
169+
arg_types: Optional[Sequence]):
170+
"""Create a CALL hook closure for one syscall.
171+
172+
Captures only the data the closure needs (host syscall nr, client, weakref
173+
to self) so the registered hook does not keep the KernelProxy alive.
174+
"""
175+
# resolve once at registration time so the hot path stays simple
176+
host_nr = self._get_host_syscall_nr(name)
140177
client = self._client
178+
weak_self = weakref.ref(self)
141179

142-
def _forwarder(ql, *args):
143-
# use the HOST syscall number, not the guest number.
144-
# for now, resolve from the host's syscall table at runtime.
145-
host_nr = self._get_host_syscall_nr(name)
180+
# normalize arg_types to a tuple, treating the string aliases as-is
181+
spec = tuple(arg_types) if arg_types else ()
182+
has_pointers = any(is_pointer(s) for s in spec)
146183

147-
padded = args + (0,) * (6 - len(args))
148-
retval = client.syscall(host_nr, padded[:6])
184+
def _forwarder(ql, *args):
185+
self_ref = weak_self()
186+
if self_ref is None:
187+
ql.log.error(f"kernel_proxy: {name}() called after proxy was destroyed")
188+
return -1
189+
190+
translated = self_ref._translate_args(name, args, spec)
191+
192+
if has_pointers:
193+
in_bufs, out_specs, out_arg_indices = self_ref._collect_buffers(
194+
ql, translated, spec
195+
)
196+
retval, out_data = client.syscall_ex(host_nr, translated, in_bufs, out_specs)
197+
self_ref._writeback_buffers(ql, args, out_arg_indices, out_data)
198+
else:
199+
retval = client.syscall(host_nr, translated)
149200

150201
if returns_fd and retval >= 0:
151-
# the proxy created a real FD. wrap it and store in Qiling's FD table.
152202
proxy_fd_obj = ql_proxy_fd(client, retval)
153-
guest_fd = self._alloc_fd(ql, proxy_fd_obj)
203+
guest_fd = self_ref._alloc_fd(ql, proxy_fd_obj)
154204
ql.log.debug(f"kernel_proxy: {name}() -> proxy_fd={retval}, guest_fd={guest_fd}")
155205
return guest_fd
156206

@@ -160,6 +210,60 @@ def _forwarder(ql, *args):
160210
_forwarder.__name__ = f'ql_syscall_{name}'
161211
return _forwarder
162212

213+
def _translate_args(self, name: str, args: Tuple[int, ...],
214+
spec: Tuple) -> Tuple[int, ...]:
215+
"""Translate guest FD args to proxy FD numbers; pad to 6 args.
216+
217+
Pointer args are left untouched here — _collect_buffers replaces them
218+
with the proxy-side buffer addresses just before invocation.
219+
"""
220+
out = list(args) + [0] * (6 - len(args))
221+
222+
for idx, kind in enumerate(spec):
223+
if kind == FD:
224+
guest_fd = args[idx]
225+
fd_obj = self.ql.os.fd[guest_fd] if 0 <= guest_fd < len(self.ql.os.fd) else None
226+
227+
if not isinstance(fd_obj, ql_proxy_fd):
228+
raise QlErrorSyscallError(
229+
f"kernel_proxy: {name}() arg{idx} guest_fd={guest_fd} "
230+
f"does not refer to a proxy-owned FD"
231+
)
232+
233+
out[idx] = fd_obj._proxy_fd
234+
235+
return tuple(out[:6])
236+
237+
def _collect_buffers(self, ql, args: Tuple[int, ...], spec: Tuple):
238+
"""Read PtrIn/PtrInOut buffers from guest memory; collect PtrOut sizes."""
239+
in_bufs = []
240+
out_specs = []
241+
out_arg_indices = []
242+
243+
for idx, kind in enumerate(spec):
244+
if isinstance(kind, (PtrIn, PtrInOut)):
245+
size = kind.resolve(args)
246+
if size > 0:
247+
data = bytes(ql.mem.read(args[idx], size))
248+
in_bufs.append((idx, data))
249+
250+
if isinstance(kind, (PtrOut, PtrInOut)):
251+
size = kind.resolve(args)
252+
if size > 0:
253+
out_specs.append((idx, size))
254+
out_arg_indices.append(idx)
255+
256+
return in_bufs, out_specs, out_arg_indices
257+
258+
@staticmethod
259+
def _writeback_buffers(ql, args: Tuple[int, ...],
260+
out_arg_indices: Sequence[int],
261+
out_data: Sequence[bytes]):
262+
"""Write PtrOut/PtrInOut response buffers back into guest memory."""
263+
for idx, data in zip(out_arg_indices, out_data):
264+
if data:
265+
ql.mem.write(args[idx], data)
266+
163267
def _get_host_syscall_nr(self, name: str) -> int:
164268
"""Get the syscall number on the HOST architecture."""
165269
# we are running on Linux — read from the host's syscall table
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#!/usr/bin/env python3
2+
#
3+
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
4+
#
5+
6+
"""
7+
Argument type descriptors for forwarded syscalls.
8+
9+
When forwarding a syscall whose arguments are not all plain integers, the user
10+
declares each argument's role so the forwarder knows how to marshal it:
11+
12+
INT — pass through unchanged (the default).
13+
FD — guest file descriptor; translate to the proxy-side FD before
14+
forwarding. If the FD does not refer to a ql_proxy_fd, an error
15+
is raised.
16+
PtrIn(s) — pointer to a buffer of `s` bytes. Read from guest memory and
17+
copied to the proxy.
18+
PtrOut(s) — pointer to a buffer of `s` bytes. Allocated on the proxy and
19+
copied back to guest memory after the syscall.
20+
PtrInOut — both directions.
21+
22+
`s` may be an integer or a callable taking the raw arg tuple and returning the
23+
buffer length (e.g. for syscalls where the size depends on another argument).
24+
"""
25+
26+
from __future__ import annotations
27+
28+
from dataclasses import dataclass
29+
from typing import Callable, Tuple, Union
30+
31+
32+
INT = 'int'
33+
FD = 'fd'
34+
35+
SizeSpec = Union[int, Callable[[Tuple[int, ...]], int]]
36+
37+
38+
def _resolve_size(size: SizeSpec, args: Tuple[int, ...]) -> int:
39+
if callable(size):
40+
return int(size(args))
41+
42+
return int(size)
43+
44+
45+
@dataclass(frozen=True)
46+
class PtrIn:
47+
"""Input pointer — buffer of `size` bytes is read from guest memory."""
48+
size: SizeSpec
49+
50+
def resolve(self, args: Tuple[int, ...]) -> int:
51+
return _resolve_size(self.size, args)
52+
53+
54+
@dataclass(frozen=True)
55+
class PtrOut:
56+
"""Output pointer — buffer of `size` bytes is written to guest memory."""
57+
size: SizeSpec
58+
59+
def resolve(self, args: Tuple[int, ...]) -> int:
60+
return _resolve_size(self.size, args)
61+
62+
63+
@dataclass(frozen=True)
64+
class PtrInOut:
65+
"""In/out pointer — buffer is read from guest, then written back."""
66+
size: SizeSpec
67+
68+
def resolve(self, args: Tuple[int, ...]) -> int:
69+
return _resolve_size(self.size, args)
70+
71+
72+
def is_pointer(spec) -> bool:
73+
return isinstance(spec, (PtrIn, PtrOut, PtrInOut))

0 commit comments

Comments
 (0)