From f5c0afab5826d04132f37d986a1eed1069cf48a2 Mon Sep 17 00:00:00 2001 From: retrocpugeek Date: Thu, 25 Jun 2026 20:22:02 +1000 Subject: [PATCH] Implement the clone3 syscall Modern glibc (recent toolchains) issues clone3 from pthread_create rather than clone, so thread creation fails on any guest built against it: Qiling does not return -ENOSYS for unimplemented syscalls (it logs a warning and leaves the return register untouched), so glibc's clone3->clone fallback never fires. Add ql_syscall_clone3, which unpacks struct clone_args and delegates to the existing clone() handler. Translations: child_stack = stack + stack_size (clone3 passes the stack base plus a size; legacy clone wants the highest address), exit_signal folded into the flags' CSIGNAL byte, and an x8664 pre-swap that cancels ql_syscall_clone's arch-specific newtls<->child_tidptr swap. Add a self-contained regression test (test_clone3_translates_to_clone) that drives ql_syscall_clone3 directly and asserts the translation for both the generic path and the x8664 swap. Runs on stock unicorn; no clone3 binary needed. Co-Authored-By: Claude Opus 4.8 (1M context) --- qiling/os/posix/syscall/sched.py | 32 ++++++++++++++++++++ tests/test_elf_multithread.py | 52 +++++++++++++++++++++++++++++++- 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/qiling/os/posix/syscall/sched.py b/qiling/os/posix/syscall/sched.py index a7c297c15..b2073d17c 100644 --- a/qiling/os/posix/syscall/sched.py +++ b/qiling/os/posix/syscall/sched.py @@ -130,6 +130,38 @@ def ql_syscall_clone(ql: Qiling, flags: int, child_stack: int, parent_tidptr: in return regreturn +def ql_syscall_clone3(ql: Qiling, cl_args: int, size: int): + # clone3(struct clone_args *uargs, size_t size). Translate the struct into + # the legacy clone() argument set and delegate. Modern glibc (used by recent + # toolchains) issues clone3 from pthread_create; without this it falls through + # to the "not implemented" path and thread creation fails. + # + # struct clone_args (all fields __aligned_u64): + # 0:flags 8:pidfd 16:child_tid 24:parent_tid 32:exit_signal + # 40:stack 48:stack_size 56:tls 64:set_tid 72:set_tid_size 80:cgroup + flags = ql.mem.read_ptr(cl_args + 0, 8) + child_tid = ql.mem.read_ptr(cl_args + 16, 8) + parent_tid = ql.mem.read_ptr(cl_args + 24, 8) + exit_signal = ql.mem.read_ptr(cl_args + 32, 8) + stack = ql.mem.read_ptr(cl_args + 40, 8) + stack_size = ql.mem.read_ptr(cl_args + 48, 8) + tls = ql.mem.read_ptr(cl_args + 56, 8) + + # legacy clone() takes the highest stack address; clone3 gives the base + size + child_stack = (stack + stack_size) if stack else 0 + + # clone3 keeps exit_signal in its own field; legacy clone packs it into the + # low CSIGNAL byte of flags + flags |= exit_signal & 0xff + + # ql_syscall_clone swaps newtls<->child_tidptr for x8664 to undo that arch's + # raw-syscall register order. clone3 hands us already-logical args, so on + # x8664 we pre-swap to cancel that out. + if ql.arch.type == QL_ARCH.X8664: + return ql_syscall_clone(ql, flags, child_stack, parent_tid, child_tid, tls) + + return ql_syscall_clone(ql, flags, child_stack, parent_tid, tls, child_tid) + def ql_syscall_sched_yield(ql: Qiling): def _sched_yield(cur_thread): gevent.sleep(0) diff --git a/tests/test_elf_multithread.py b/tests/test_elf_multithread.py index 8923efa1b..8fedeff49 100644 --- a/tests/test_elf_multithread.py +++ b/tests/test_elf_multithread.py @@ -17,9 +17,10 @@ sys.path.append("..") from qiling import Qiling from qiling.arch.models import X86_CPU_MODEL -from qiling.const import QL_VERBOSE, QL_INTERCEPT +from qiling.const import QL_VERBOSE, QL_INTERCEPT, QL_ARCH, QL_OS from qiling.os.filestruct import ql_file from qiling.os.stats import QlOsNullStats +import qiling.os.posix.syscall.sched as ql_sched BASE_ROOTFS = r'../examples/rootfs' @@ -201,6 +202,55 @@ def check_write(ql: Qiling, fd: int, write_buf, count: int): self.assertTrue(logged[-2].startswith('thread 1 ret val is')) self.assertTrue(logged[-1].startswith('thread 2 ret val is')) + def test_clone3_translates_to_clone(self): + # clone3(struct clone_args *, size) must unpack the struct and delegate + # to the legacy clone() handler with translated args. Modern glibc issues + # clone3 from pthread_create, so without this thread creation fails. + # This drives ql_syscall_clone3 directly (stock unicorn, no clone3 binary + # needed) and asserts the translation. + CL = 0x100000 + FLAGS, CHILD_TID, PARENT_TID = 0x00010f00, 0x222000, 0x223000 + EXIT_SIGNAL, STACK, STACK_SIZE, TLS = 0x11, 0x7ff00000, 0x8000, 0x224000 + + def run_case(archtype: QL_ARCH, rootfs: str): + ql = Qiling(code=b"\x00\x00\x00\x00", archtype=archtype, ostype=QL_OS.LINUX, + rootfs=rootfs, verbose=QL_VERBOSE.OFF) + ql.mem.map(CL, 0x1000) + for off, val in ((0, FLAGS), (16, CHILD_TID), (24, PARENT_TID), + (32, EXIT_SIGNAL), (40, STACK), (48, STACK_SIZE), (56, TLS)): + ql.mem.write_ptr(CL + off, val, 8) + + captured = {} + + def fake_clone(ql, flags, child_stack, parent_tidptr, newtls, child_tidptr): + captured.update(flags=flags, child_stack=child_stack, parent_tidptr=parent_tidptr, + newtls=newtls, child_tidptr=child_tidptr) + return 0xabc + + orig = ql_sched.ql_syscall_clone + ql_sched.ql_syscall_clone = fake_clone + try: + ret = ql_sched.ql_syscall_clone3(ql, CL, 88) + finally: + ql_sched.ql_syscall_clone = orig + + return captured, ret + + # generic path (no x8664 register swap) + cap, ret = run_case(QL_ARCH.ARM, fr'{ARM_LINUX_ROOTFS}') + self.assertEqual(ret, 0xabc) + self.assertEqual(cap['child_stack'], STACK + STACK_SIZE) # base + size -> stack top + self.assertEqual(cap['flags'], FLAGS | EXIT_SIGNAL) # exit_signal folded in + self.assertEqual(cap['parent_tidptr'], PARENT_TID) + self.assertEqual(cap['newtls'], TLS) + self.assertEqual(cap['child_tidptr'], CHILD_TID) + + # x8664: clone3 pre-swaps newtls<->child_tid so ql_syscall_clone's own + # x8664 swap cancels out to the same logical mapping + cap, _ = run_case(QL_ARCH.X8664, fr'{X64_LINUX_ROOTFS}') + self.assertEqual(cap['newtls'], CHILD_TID) + self.assertEqual(cap['child_tidptr'], TLS) + def test_tcp_elf_linux_x86(self): logged: List[str] = []