From bf21751c502c1562f4c6145138ac4abe2995f928 Mon Sep 17 00:00:00 2001 From: Hanlu Li Date: Tue, 8 Sep 2026 09:32:28 +0800 Subject: [PATCH] linux-user, fix: Reclaim thread and seccomp resources Guest thread exit and failed clone paths leave translator allocations, netlink staging buffers and seccomp filter references without an owner. TSYNC can also observe a child before its task and filter are initialized. Release thread-local translator resources on normal exit and KILL_THREAD, and roll back child-owned resources when thread creation fails. Restore netlink iovecs on receive failure and release pending buffers at teardown. Reference-count immutable filter chains across clone, installation, TSYNC, exit and fork. Publish initialized tasks under the CPU-list lock and use release/acquire ordering for filter roots seen by syscall readers. Add thread-cleanup and seccomp-lifetime regressions for partial teardown, failed clone, filter ownership and publication order. The isolated branch passed 26 lat-pr-fast tests, plus sanitizer ownership checks and focused thread-churn, failed-clone and concurrent TSYNC workloads. Guest integration tests requiring the unavailable toolchain were skipped. Signed-off-by: Hanlu Li --- accel/tcg/translate-all.c | 7 + include/exec/translate-all.h | 4 + linux-user/guest-seccomp.c | 60 ++- linux-user/guest-seccomp.h | 5 + linux-user/main.c | 7 + linux-user/qemu.h | 1 + linux-user/syscall.c | 90 +++- target/i386/latx/include/latx-config.h | 1 + target/i386/latx/include/tu.h | 1 + target/i386/latx/latx-config.c | 18 + target/i386/latx/optimization/tu.c | 10 + tests/unit/meson.build | 14 + tests/unit/test-latx-seccomp-lifetime.py | 202 +++++++++ tests/unit/test-latx-thread-cleanup.py | 496 +++++++++++++++++++++++ 14 files changed, 889 insertions(+), 27 deletions(-) create mode 100644 tests/unit/test-latx-seccomp-lifetime.py create mode 100644 tests/unit/test-latx-thread-cleanup.py diff --git a/accel/tcg/translate-all.c b/accel/tcg/translate-all.c index f4d515e257a..8b6070a2627 100644 --- a/accel/tcg/translate-all.c +++ b/accel/tcg/translate-all.c @@ -193,6 +193,13 @@ static void smc_retrans_destory(void) } } +void latx_smc_thread_cleanup(void) +{ + /* The tree owns its nodes, but the referenced TBs belong to the shared + * translation cache and must remain alive for other guest threads. */ + smc_retrans_destory(); +} + #endif /** diff --git a/include/exec/translate-all.h b/include/exec/translate-all.h index 1b65c291a55..dd559216f6a 100644 --- a/include/exec/translate-all.h +++ b/include/exec/translate-all.h @@ -33,6 +33,10 @@ void tb_invalidate_phys_page_fast(struct page_collection *pages, void tb_invalidate_phys_page(tb_page_addr_t addr); void tb_check_watchpoint(CPUState *cpu, uintptr_t retaddr); +#ifdef CONFIG_LATX_SMC_OPT +void latx_smc_thread_cleanup(void); +#endif + int get_insn_len_readable(target_ulong address); int latx_mprotect_one_page_rw(abi_ulong addr); int latx_mprotect_one_page_rw_resolv(abi_ulong addr); diff --git a/linux-user/guest-seccomp.c b/linux-user/guest-seccomp.c index a47fb87f044..a83adda83bd 100644 --- a/linux-user/guest-seccomp.c +++ b/linux-user/guest-seccomp.c @@ -22,10 +22,30 @@ typedef struct GuestSeccompData { typedef struct GuestSeccompFilter { struct GuestSeccompFilter *previous; + gint references; unsigned int len; struct sock_filter insns[]; } GuestSeccompFilter; +GuestSeccompFilter *guest_seccomp_filter_ref(GuestSeccompFilter *filter) +{ + if (filter) { + g_atomic_int_inc(&filter->references); + } + return filter; +} + +void guest_seccomp_filter_unref(GuestSeccompFilter *filter) +{ + /* Each task root and each immutable previous edge owns a reference. */ + while (filter && g_atomic_int_dec_and_test(&filter->references)) { + GuestSeccompFilter *previous = filter->previous; + + g_free(filter); + filter = previous; + } +} + static bool seccomp_jump_valid(unsigned int pc, uint32_t offset, unsigned int len) { @@ -284,6 +304,7 @@ static abi_long seccomp_load_filter(abi_ulong target_filter, filter = g_malloc(sizeof(*filter) + len * sizeof(filter->insns[0])); filter->previous = NULL; + filter->references = 1; filter->len = len; for (i = 0; i < len; i++) { filter->insns[i].code = tswap16(target_insns[i].code); @@ -329,8 +350,6 @@ static abi_long seccomp_install_filter(CPUArchState *env, abi_ulong flags, if (ret) { return ret; } - filter->previous = task->seccomp_filter; - if (flags & SECCOMP_FILTER_FLAG_TSYNC) { CPUState *other_cpu; @@ -339,27 +358,41 @@ static abi_long seccomp_install_filter(CPUArchState *env, abi_ulong flags, CPU_FOREACH(other_cpu) { TaskState *other_task = other_cpu->opaque; + /* A CPU under construction inherits the root when published. */ + if (!other_task || other_task->seccomp_exiting) { + continue; + } if (other_task->seccomp_filter != task->seccomp_filter) { - ret = other_task->ts_tid; + ret = other_task->ts_tid ? other_task->ts_tid : -TARGET_EAGAIN; break; } } if (ret == 0) { + filter->previous = guest_seccomp_filter_ref(task->seccomp_filter); CPU_FOREACH(other_cpu) { TaskState *other_task = other_cpu->opaque; - - other_task->seccomp_filter = filter; + GuestSeccompFilter *previous; + + if (!other_task || other_task->seccomp_exiting) { + continue; + } + previous = other_task->seccomp_filter; + qatomic_store_release(&other_task->seccomp_filter, + guest_seccomp_filter_ref(filter)); + guest_seccomp_filter_unref(previous); } } + guest_seccomp_filter_unref(filter); cpu_list_unlock(); end_exclusive(); - if (ret != 0) { - g_free(filter); - } return ret; } - task->seccomp_filter = filter; + /* Transfer the task's old root reference to the new node's edge. */ + cpu_list_lock(); + filter->previous = task->seccomp_filter; + qatomic_store_release(&task->seccomp_filter, filter); + cpu_list_unlock(); return 0; } @@ -369,8 +402,8 @@ abi_long guest_seccomp_prctl(CPUArchState *env, abi_long option, TaskState *task = env_cpu(env)->opaque; if (option == PR_GET_SECCOMP) { - return task->seccomp_filter ? SECCOMP_MODE_FILTER : - SECCOMP_MODE_DISABLED; + return qatomic_read(&task->seccomp_filter) ? SECCOMP_MODE_FILTER : + SECCOMP_MODE_DISABLED; } if (mode != SECCOMP_MODE_FILTER) { return -TARGET_EINVAL; @@ -427,7 +460,10 @@ GuestSeccompAction guest_seccomp_filter_syscall(CPUArchState *env, int num, abi_long *result) { TaskState *task = env_cpu(env)->opaque; - GuestSeccompFilter *filter = task->seccomp_filter; + /* TSYNC stops cpu_exec, not other threads already handling syscalls. + * Publish/read the immutable chain with release/acquire ordering. Old + * chains remain alive through the new chain's owning previous edge. */ + GuestSeccompFilter *filter = qatomic_load_acquire(&task->seccomp_filter); GuestSeccompData data; uint32_t decision = SECCOMP_RET_ALLOW; unsigned int i; diff --git a/linux-user/guest-seccomp.h b/linux-user/guest-seccomp.h index d484601069b..3c7851929ba 100644 --- a/linux-user/guest-seccomp.h +++ b/linux-user/guest-seccomp.h @@ -1,6 +1,11 @@ #ifndef LINUX_USER_GUEST_SECCOMP_H #define LINUX_USER_GUEST_SECCOMP_H +struct GuestSeccompFilter; +struct GuestSeccompFilter *guest_seccomp_filter_ref( + struct GuestSeccompFilter *filter); +void guest_seccomp_filter_unref(struct GuestSeccompFilter *filter); + typedef enum GuestSeccompAction { GUEST_SECCOMP_CONTINUE, GUEST_SECCOMP_RETURN, diff --git a/linux-user/main.c b/linux-user/main.c index 5e2541a1942..0a80d16638e 100644 --- a/linux-user/main.c +++ b/linux-user/main.c @@ -31,6 +31,7 @@ #include "qapi/error.h" #include "qemu.h" +#include "guest-seccomp.h" #include "qemu/path.h" #include "qemu/queue.h" #include "qemu/config-file.h" @@ -235,6 +236,12 @@ void fork_end(int child) Discard information about the parent threads. */ CPU_FOREACH_SAFE(cpu, next_cpu) { if (cpu != thread_cpu) { + TaskState *task = cpu->opaque; + + if (task) { + guest_seccomp_filter_unref(task->seccomp_filter); + task->seccomp_filter = NULL; + } QTAILQ_REMOVE_RCU(&cpus, cpu, node); } } diff --git a/linux-user/qemu.h b/linux-user/qemu.h index 3b566ce597a..fee2569abb4 100644 --- a/linux-user/qemu.h +++ b/linux-user/qemu.h @@ -185,6 +185,7 @@ typedef struct TaskState { bool ipc_namespace_isolated; /* Immutable seccomp filter chain inherited by guest threads. */ struct GuestSeccompFilter *seccomp_filter; + bool seccomp_exiting; /* A seccomp errno result must not be treated as an internal restart. */ bool seccomp_errno_return; #ifdef TARGET_X86_64 diff --git a/linux-user/syscall.c b/linux-user/syscall.c index 52d225705c5..9d8ddbf6f90 100644 --- a/linux-user/syscall.c +++ b/linux-user/syscall.c @@ -147,6 +147,7 @@ #include "ioctl/mpt3sas_ctl.h" #include "qemu.h" +#include "exec/translate-all.h" #include "guest-seccomp.h" #include "signal-common.h" #include "qemu/guest-random.h" @@ -4567,6 +4568,14 @@ static __thread struct nlmsghdr *pre_nlmh; static __thread void* buf; static __thread abi_long all_len; +static void reset_16k_buf(void) +{ + free(buf); + buf = NULL; + pre_nlmh = NULL; + all_len = 0; +} + static void set_16k_buf(struct msghdr *msg) { buf = malloc(BUFF_16K); @@ -4597,9 +4606,7 @@ static abi_long get_from_16k_buf(struct msghdr *msg) if (NLMSG_OK(nlmh, all_len)) { pre_nlmh = nlmh; } else { - pre_nlmh = NULL; - free(buf); - buf = NULL; + reset_16k_buf(); } return curr_nlmh_len; } @@ -4703,12 +4710,12 @@ static abi_long do_sendrecvmsg_locked(int fd, struct target_msghdr *msgp, size_t iov_len = msg.msg_iov->iov_len; set_16k_buf(&msg); ret = get_errno(safe_recvmsg(fd, &msg, flags)); + msg.msg_iov->iov_base = iov_base; + msg.msg_iov->iov_len = iov_len; if (is_error(ret)) { - assert(0); + reset_16k_buf(); goto out; } - msg.msg_iov->iov_base = iov_base; - msg.msg_iov->iov_len = iov_len; all_len = ret; } ret = get_from_16k_buf(&msg); @@ -9651,6 +9658,36 @@ static void cleanup_guest_thread_resources(CPUArchState *env) target_munmap(env->gdt.base, sizeof(uint64_t) * TARGET_GDT_ENTRIES, 0); } +static void cleanup_guest_seccomp(TaskState *ts) +{ + /* Keep fork's CPU-list snapshot consistent with the filter references. */ + cpu_list_lock(); + ts->seccomp_exiting = true; + guest_seccomp_filter_unref(ts->seccomp_filter); + ts->seccomp_filter = NULL; + cpu_list_unlock(); +} + +/* The child has not started; the caller still owns its CPU and TaskState. */ +static void cleanup_failed_guest_thread(CPUArchState *env) +{ + CPUState *cpu = env_cpu(env); + TaskState *ts = cpu->opaque; +#ifdef CONFIG_LATX_FAST_JMPCACHE + void *fast_jmp_cache = env->tb_jmp_cache_ptr; +#endif + + cleanup_guest_seccomp(ts); + cleanup_guest_thread_resources(env); + object_property_set_bool(OBJECT(cpu), "realized", false, NULL); + object_unparent(OBJECT(cpu)); + object_unref(OBJECT(cpu)); +#ifdef CONFIG_LATX_FAST_JMPCACHE + latx_fast_jmp_cache_free_rcu(fast_jmp_cache); +#endif + g_free(ts); +} + /* clone_lock is held and at least one other guest thread exists. */ static void QEMU_NORETURN exit_guest_thread_locked(CPUArchState *env) { @@ -9661,6 +9698,7 @@ static void QEMU_NORETURN exit_guest_thread_locked(CPUArchState *env) void *fast_jmp_cache = x86env->tb_jmp_cache_ptr; #endif + cleanup_guest_seccomp(ts); object_property_set_bool(OBJECT(cpu), "realized", false, NULL); object_unparent(OBJECT(cpu)); object_unref(OBJECT(cpu)); @@ -9677,6 +9715,13 @@ static void QEMU_NORETURN exit_guest_thread_locked(CPUArchState *env) } thread_cpu = NULL; g_free(ts); + reset_16k_buf(); +#ifdef CONFIG_LATX + latx_lsenv_destroy(); +#endif +#ifdef CONFIG_LATX_SMC_OPT + latx_smc_thread_cleanup(); +#endif rcu_unregister_thread(); pthread_exit(NULL); } @@ -9770,6 +9815,7 @@ static int do_fork(CPUArchState *env, unsigned int flags, abi_ulong newsp, TaskState *parent_ts = (TaskState *)cpu->opaque; new_thread_info info; pthread_attr_t attr; + int thread_errno = 0; rcu_start_deferred_thread(); @@ -9800,11 +9846,9 @@ static int do_fork(CPUArchState *env, unsigned int flags, abi_ulong newsp, cpu_clone_regs_child(new_env, newsp, flags); cpu_clone_regs_parent(env, flags); new_cpu = env_cpu(new_env); - new_cpu->opaque = ts; ts->bprm = parent_ts->bprm; ts->info = parent_ts->info; ts->signal_mask = parent_ts->signal_mask; - ts->seccomp_filter = parent_ts->seccomp_filter; ts->ipc_namespace_isolated = parent_ts->ipc_namespace_isolated; if (flags & CLONE_CHILD_CLEARTID) { @@ -9815,16 +9859,28 @@ static int do_fork(CPUArchState *env, unsigned int flags, abi_ulong newsp, cpu_set_tls (new_env, newtls); } + /* Publish the initialized task together with its inherited root. + * TSYNC uses this lock and skips CPUs whose task is not yet visible. */ + cpu_list_lock(); + ts->seccomp_filter = guest_seccomp_filter_ref(parent_ts->seccomp_filter); + new_cpu->opaque = ts; + cpu_list_unlock(); + +#ifdef CONFIG_LATX_FAST_JMPCACHE + /* cpu_copy copied the parent's pointer; it remains parent-owned. */ + new_env->tb_jmp_cache_ptr = NULL; + if (!latx_fast_jmp_cache_init(new_env)) { + cleanup_failed_guest_thread(new_env); + pthread_mutex_unlock(&clone_lock); + errno = ENOMEM; + return -1; + } +#endif memset(&info, 0, sizeof(info)); pthread_mutex_init(&info.mutex, NULL); pthread_mutex_lock(&info.mutex); pthread_cond_init(&info.cond, NULL); info.env = new_env; -#ifdef CONFIG_LATX_FAST_JMPCACHE - if(!latx_fast_jmp_cache_init(new_env)) { - fprintf(stderr, "[LATX-ERR] latx_fast_jmp_cache_init error!\n"); - } -#endif if (flags & CLONE_CHILD_SETTID) { info.child_tidptr = child_tidptr; } @@ -9842,7 +9898,6 @@ static int do_fork(CPUArchState *env, unsigned int flags, abi_ulong newsp, cpu->random_seed = qemu_guest_random_seed_thread_part1(); ret = pthread_create(&info.thread, &attr, clone_func, &info); - /* TODO: Free new CPU state if thread creation failed. */ sigprocmask(SIG_SETMASK, &info.sigmask, NULL); pthread_attr_destroy(&attr); @@ -9851,12 +9906,17 @@ static int do_fork(CPUArchState *env, unsigned int flags, abi_ulong newsp, pthread_cond_wait(&info.cond, &info.mutex); ret = info.tid; } else { + thread_errno = ret; + cleanup_failed_guest_thread(new_env); ret = -1; } pthread_mutex_unlock(&info.mutex); pthread_cond_destroy(&info.cond); pthread_mutex_destroy(&info.mutex); pthread_mutex_unlock(&clone_lock); + if (thread_errno) { + errno = thread_errno; + } } else { /* if no CLONE_VM, we consider it is a fork */ if (flags & CLONE_INVALID_FORK_FLAGS) { @@ -21204,7 +21264,7 @@ abi_long do_syscall_with_seccomp(void *cpu_env, int num, int seccomp_num, } #ifdef CONFIG_LATX_TUNNEL_LIB - suppress_tunnel = ts->seccomp_filter && loader_tunnel; + suppress_tunnel = qatomic_read(&ts->seccomp_filter) && loader_tunnel; #endif if (suppress_tunnel) { ret = 0; diff --git a/target/i386/latx/include/latx-config.h b/target/i386/latx/include/latx-config.h index 2badbe33500..d4fa91829b8 100644 --- a/target/i386/latx/include/latx-config.h +++ b/target/i386/latx/include/latx-config.h @@ -36,6 +36,7 @@ void latx_guest_stack_init(CPUArchState *env); void latx_init_fpu_regs(CPUArchState *env); void latx_lsenv_init(CPUArchState *env); +void latx_lsenv_destroy(void); void latx_dt_init(void); void ht_pc_thunk_insert(uint32_t thunk_addr, int reg_index); int ht_pc_thunk_lookup(uint32_t thunk_addr); diff --git a/target/i386/latx/include/tu.h b/target/i386/latx/include/tu.h index 9b62d8cdc57..a6ba8bfb854 100644 --- a/target/i386/latx/include/tu.h +++ b/target/i386/latx/include/tu.h @@ -93,6 +93,7 @@ void tu_enough_space(CPUState *cpu); void tu_trees_reset(void); TranslationBlock *tu_tree_lookup(target_ulong pc); void tu_control_init(void); +void tu_control_destroy(void); TranslationBlock* tb_create(CPUState *cpu, target_ulong pc, target_ulong cs_base, uint32_t flags, int cflags, int max_insns, uint16_t bool_flags, TU_TB_START_TYPE mode); diff --git a/target/i386/latx/latx-config.c b/target/i386/latx/latx-config.c index 3d067e169b6..e415c864679 100644 --- a/target/i386/latx/latx-config.c +++ b/target/i386/latx/latx-config.c @@ -18,6 +18,7 @@ #include "translate.h" #include "latx-config.h" #include "syscall-tunnel.h" +#include "imm-cache.h" #if defined(CONFIG_LATX_KZT) #include "wrappertbbridge.h" #endif @@ -646,6 +647,23 @@ void latx_init_fpu_regs(CPUArchState *env) } } +void latx_lsenv_destroy(void) +{ + TRANSLATION_DATA *t = &tr_data_real; + + free(t->ir2_inst_array); + if (t->imm_cache) { + free(t->imm_cache->bucket); + free(t->imm_cache); + } + memset(t, 0, sizeof(*t)); +#ifdef CONFIG_LATX_TU + tu_control_destroy(); +#endif + memset(&lsenv_real, 0, sizeof(lsenv_real)); + lsenv = NULL; +} + void latx_lsenv_init(CPUArchState *env) { lsenv = &lsenv_real; diff --git a/target/i386/latx/optimization/tu.c b/target/i386/latx/optimization/tu.c index 35c749c9cc1..74a8a2f0b66 100644 --- a/target/i386/latx/optimization/tu.c +++ b/target/i386/latx/optimization/tu.c @@ -109,6 +109,16 @@ void tu_control_init(void) return; } +void tu_control_destroy(void) +{ + if (tu_data && tu_data->tree) { + /* TranslationBlocks belong to the code cache, not this index. */ + g_tree_destroy(tu_data->tree); + } + memset(&tu_data_rel, 0, sizeof(tu_data_rel)); + tu_data = NULL; +} + inline void tu_push_back(TranslationBlock *tb) { if (!tb) { diff --git a/tests/unit/meson.build b/tests/unit/meson.build index 1843ee81dc4..a9446eec0c8 100644 --- a/tests/unit/meson.build +++ b/tests/unit/meson.build @@ -188,3 +188,17 @@ test( test_kzt_address_policy, suite: 'lat-pr-fast', ) + +test( + 'test-latx-thread-cleanup', + python, + args: [files('test-latx-thread-cleanup.py'), '--repo', project_source_root], + suite: 'lat-pr-fast', +) + +test( + 'test-latx-seccomp-lifetime', + python, + args: [files('test-latx-seccomp-lifetime.py'), project_source_root], + suite: 'lat-pr-fast', +) diff --git a/tests/unit/test-latx-seccomp-lifetime.py b/tests/unit/test-latx-seccomp-lifetime.py new file mode 100644 index 00000000000..514acba5b26 --- /dev/null +++ b/tests/unit/test-latx-seccomp-lifetime.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Compile production filter ownership paths with isolated CPU/guest-memory seams. + +This tests allocation lifetime, not BPF interpretation or guest syscall delivery. +The unmodified implementation deliberately fails the final allocation assertions. +""" +import argparse +import os +from pathlib import Path +import shlex +import subprocess +import tempfile + + +def function(source, signature): + start = source.index(signature) + return source[start:source.index("\n}", start) + 2] + "\n" + + +PRELUDE = r''' +#include +#include +#include +#include +#include +#include +#include +#include +#include +#define CHECK(x) do { if (!(x)) { fprintf(stderr, "%d: %s\n", __LINE__, #x); abort(); } } while (0) +typedef unsigned long abi_ulong; +typedef long abi_long; +typedef struct GuestSeccompFilter GuestSeccompFilter; +typedef struct { GuestSeccompFilter *seccomp_filter; int ts_tid; bool seccomp_exiting; } TaskState; +typedef struct CPUState { TaskState *opaque; struct CPUState *next; } CPUState; +typedef CPUState CPUArchState; +static CPUState *cpus, *thread_cpu; +static unsigned allocations; +static unsigned root_publications; +#define qatomic_store_release(p, v) do { \ + root_publications++; __atomic_store_n(p, v, __ATOMIC_RELEASE); \ +} while (0) +static bool valid = true; +static void *tracked_malloc(size_t size) { + void *p = malloc(size); CHECK(p); allocations++; return p; +} +static void tracked_free(void *p) { if (p) { CHECK(allocations); allocations--; free(p); } } +#undef g_malloc +#undef g_free +#define g_malloc tracked_malloc +#define g_free tracked_free +#define TARGET_EINVAL 22 +#define TARGET_EFAULT 14 +#define TARGET_EACCES 13 +#define TARGET_EAGAIN 11 +#define VERIFY_READ 0 +#define target_sock_filter sock_filter +#define lock_user(mode, addr, size, copy) ((void *)(addr)) +#define unlock_user(ptr, addr, copy) ((void)0) +#define tswap16(x) (x) +#define tswap32(x) (x) +#define env_cpu(env) (env) +#define CPU_FOREACH(cpu) for ((cpu) = cpus; (cpu); (cpu) = (cpu)->next) +#define CPU_FOREACH_SAFE(cpu, next_cpu) for ((cpu) = cpus; (cpu) && (((next_cpu) = (cpu)->next), 1); (cpu) = (next_cpu)) +static void remove_cpu(CPUState **head, CPUState *cpu) { + while (*head != cpu) { CHECK(*head); head = &(*head)->next; } + *head = cpu->next; +} +#define QTAILQ_REMOVE_RCU(head, cpu, node) remove_cpu(head, cpu) +#define prctl(...) 1 +#define start_exclusive() ((void)0) +#define end_exclusive() ((void)0) +#define cpu_list_lock() ((void)0) +#define cpu_list_unlock() ((void)0) +#define mmap_fork_end(child) ((void)0) +#define sigact_fork_end(child) ((void)0) +#define path_fork_end(child) ((void)0) +#define fd_trans_fork_end() ((void)0) +#define qemu_init_cpu_list() ((void)0) +#define gdbserver_fork(cpu) ((void)0) +static bool seccomp_filter_valid(const struct sock_filter *insns, unsigned len) { return valid; } +static abi_long seccomp_load_program(abi_ulong program, unsigned *len, abi_ulong *filter) { + *len = 1; *filter = program; return 0; +} +''' + +MAIN = r''' +static struct sock_filter allow = BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW); +static void install(CPUState *cpu, unsigned flags) { + unsigned before = root_publications; + CHECK(seccomp_install_filter(cpu, flags, (abi_ulong)&allow) == 0); + /* Syscall readers need publication ordering even outside cpu_exec. */ + CHECK(root_publications > before); +} +static void release(TaskState *task) { + guest_seccomp_filter_unref(task->seccomp_filter); task->seccomp_filter = NULL; +} +int main(void) { + TaskState tasks[3] = {{ .ts_tid = 1 }, { .ts_tid = 2 }, { .ts_tid = 3 }}; + CPUState cpu[3] = {{ .opaque = &tasks[0] }, { .opaque = &tasks[1] }, { .opaque = &tasks[2] }}; + cpus = &cpu[0]; thread_cpu = cpus; + CHECK(guest_seccomp_filter_ref(NULL) == NULL); + guest_seccomp_filter_unref(NULL); + valid = false; + CHECK(seccomp_install_filter(cpus, 0, (abi_ulong)&allow) == -TARGET_EINVAL); + CHECK(allocations == 0); valid = true; + /* cpu_create publishes a CPU before its TaskState is initialized. */ + cpu[0].next = &cpu[1]; cpu[1].opaque = NULL; + install(&cpu[0], SECCOMP_FILTER_FLAG_TSYNC); + CHECK(allocations == 1); + /* An unpublished guest TID cannot report TSYNC failure as success (0). */ + cpu[1].opaque = &tasks[1]; tasks[1].ts_tid = 0; + CHECK(seccomp_install_filter(&cpu[0], SECCOMP_FILTER_FLAG_TSYNC, (abi_ulong)&allow) == -TARGET_EAGAIN); + CHECK(allocations == 1); + release(&tasks[0]); CHECK(allocations == 0); + tasks[1].seccomp_exiting = true; + install(&cpu[0], SECCOMP_FILTER_FLAG_TSYNC); + CHECK(allocations == 1 && tasks[1].seccomp_filter == NULL); + release(&tasks[0]); CHECK(allocations == 0); + tasks[1].seccomp_exiting = false; + tasks[1].ts_tid = 2; cpu[0].next = NULL; + for (unsigned cycle = 0; cycle < 64; cycle++) { + /* Clone sharing, private extension, then child exit. */ + install(&cpu[0], 0); + tasks[1].seccomp_filter = guest_seccomp_filter_ref(tasks[0].seccomp_filter); + cpu[0].next = &cpu[1]; + install(&cpu[1], 0); + CHECK(allocations == 2); + /* A rejected TSYNC must drop only its new node. */ + CHECK(seccomp_install_filter(&cpu[0], SECCOMP_FILTER_FLAG_TSYNC, (abi_ulong)&allow) == 2); + CHECK(allocations == 2); + release(&tasks[1]); + CHECK(allocations == 1); + tasks[1].seccomp_filter = guest_seccomp_filter_ref(tasks[0].seccomp_filter); + install(&cpu[0], SECCOMP_FILTER_FLAG_TSYNC); + CHECK(allocations == 2 && tasks[0].seccomp_filter == tasks[1].seccomp_filter); + release(&tasks[0]); + CHECK(allocations == 2); /* Child still owns the full chain. */ + release(&tasks[1]); + CHECK(allocations == 0); + cpu[0].next = NULL; + } + /* A fork child loses all roots belonging to vanished parent threads. */ + install(&cpu[0], 0); + tasks[1].seccomp_filter = guest_seccomp_filter_ref(tasks[0].seccomp_filter); + tasks[2].seccomp_filter = guest_seccomp_filter_ref(tasks[0].seccomp_filter); + cpu[0].next = &cpu[1]; cpu[1].next = &cpu[2]; + install(&cpu[2], 0); + fork_end(1); + CHECK(cpus == &cpu[0] && cpus->next == NULL); + CHECK(allocations == 1); + release(&tasks[0]); CHECK(allocations == 0); + /* Destruction is iterative even with a deep filter chain. */ + for (unsigned i = 0; i < 10000; i++) { install(&cpu[0], 0); } + CHECK(allocations == 10000); + release(&tasks[0]); CHECK(allocations == 0); + puts("seccomp lifetime: clone, private/TSYNC, failure, fork, deep chain PASS"); + return 0; +} +''' + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("source", type=Path) + parser.add_argument("--revision", help="Read production sources from a git revision") + args = parser.parse_args() + + def read_source(path): + if args.revision: + return subprocess.check_output( + ["git", "-C", str(args.source), "show", f"{args.revision}:{path}"], + text=True) + return (args.source / path).read_text() + + source = read_source("linux-user/guest-seccomp.c") + main_source = read_source("linux-user/main.c") + begin = source.index("typedef struct GuestSeccompFilter {") + end = source.index("} GuestSeccompFilter;", begin) + len("} GuestSeccompFilter;") + text = PRELUDE + source[begin:end] + "\n" + if "guest_seccomp_filter_ref(" in source: + text += function(source, "GuestSeccompFilter *guest_seccomp_filter_ref(") + text += function(source, "void guest_seccomp_filter_unref(") + else: + text += "GuestSeccompFilter *guest_seccomp_filter_ref(GuestSeccompFilter *p) { return p; }\n" + text += "void guest_seccomp_filter_unref(GuestSeccompFilter *p) {}\n" + text += function(source, "static abi_long seccomp_load_filter(") + text += function(source, "static abi_long seccomp_install_filter(") + text += function(main_source, "void fork_end(") + MAIN + flags = shlex.split(subprocess.check_output(["pkg-config", "--cflags", "--libs", "glib-2.0"], text=True)) + with tempfile.TemporaryDirectory(prefix="lat-seccomp-lifetime-") as temporary: + src = Path(temporary) / "test.c" + src.write_text(text) + for mode in ([], ["-DNDEBUG"]): + binary = Path(temporary) / "test" + subprocess.run(shlex.split(os.environ.get("CC", "cc")) + ["-std=gnu11", "-O2"] + mode + [str(src), "-o", str(binary)] + flags, check=True) + subprocess.run([str(binary)], check=True) + + +if __name__ == "__main__": + main() diff --git a/tests/unit/test-latx-thread-cleanup.py b/tests/unit/test-latx-thread-cleanup.py new file mode 100644 index 00000000000..9156fb7be5d --- /dev/null +++ b/tests/unit/test-latx-thread-cleanup.py @@ -0,0 +1,496 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Exercise production thread teardown and netlink receive rollback bodies. + +This isolated ownership seam checks allocations and teardown ordering; target +thread-churn and seccomp tests additionally validate the full emulator path. +""" + +import argparse +import os +import pathlib +import shlex +import subprocess +import tempfile + + +def function(source, signature, optional=False): + start = source.find(signature) + if start < 0 and optional: + return "" + if start < 0: + raise ValueError(f"Missing production function: {signature}") + return source[start:source.index("\n}", start) + 2] + "\n" + + +PRELUDE = r''' +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#define CHECK(x) do { if (!(x)) { \ + fprintf(stderr, "%s:%d: %s\n", __FILE__, __LINE__, #x); exit(1); \ +} } while (0) +#define CONFIG_LATX 1 +#define CONFIG_LATX_FAST_JMPCACHE 1 +#define CONFIG_LATX_SMC_OPT 1 +#define QEMU_NORETURN __attribute__((noreturn)) +#define OBJECT(x) (x) +#define FUTEX_WAKE 1 +#define BUFF_4K 4096 +#define BUFF_16K 16384 +#define TARGET_GDT_ENTRIES 9 +typedef long abi_long; +typedef struct { void *bucket; } IMM_CACHE; +typedef struct { void *ir2_inst_array; IMM_CACHE *imm_cache; } TRANSLATION_DATA; +typedef struct { unsigned long pc; unsigned marker; } TranslationBlock; +typedef struct { GTree *tree; unsigned tb_num, ir1_num_in_tu; } TUControl; +typedef struct { void *cpu_state; TRANSLATION_DATA *tr_data; } ENV; +typedef struct { + unsigned child_tidptr; + void *seccomp_filter; + bool seccomp_exiting; +} TaskState; +typedef struct { TaskState *opaque; } CPUState; +typedef struct { + CPUState *cpu; + void *tb_jmp_cache_ptr; + struct { uintptr_t base; } gdt; +} CPUArchState; +typedef CPUArchState CPUX86State; +static __thread TRANSLATION_DATA tr_data_real; +static __thread TUControl tu_data_rel, *tu_data; +static __thread ENV lsenv_real, *lsenv; +static __thread GTree *smc_retrans_tree; +static __thread struct nlmsghdr *pre_nlmh; +static __thread void *buf; +static __thread abi_long all_len; +static CPUState *thread_cpu; +static int clone_lock, allocations, trees, retired_caches, rcu_unregistered; +static int filter_unref_count, release_cpu, cpu_unrealized, gdt_unmapped; +static bool cpu_list_locked; +static jmp_buf exit_buffer; +static void *tracked_malloc(size_t n) { + void *p = malloc(n); CHECK(p); allocations++; return p; +} +static void tracked_free(void *p) { if (p) { allocations--; free(p); } } +static GTree *tracked_tree_new(GCompareFunc cmp) { + trees++; return g_tree_new(cmp); +} +static void tracked_tree_destroy(GTree *tree) { + CHECK(tree); trees--; g_tree_destroy(tree); +} +static CPUState *env_cpu(CPUArchState *env) { return env->cpu; } +static void object_property_set_bool(void *o, const char *p, bool b, void *e) { + TaskState *task = ((CPUState *)o)->opaque; + CHECK(task->seccomp_exiting && !task->seccomp_filter && !cpu_list_locked); + cpu_unrealized++; +} +static void object_unparent(void *o) {} +static void object_unref(void *o) { if (release_cpu) tracked_free(o); } +static void fake_mutex_unlock(void *m) {} +static void put_user_u32(unsigned n, unsigned p) {} +static void *g2h(CPUState *cpu, unsigned p) { return NULL; } +static void do_sys_futex(void *p, int op, int val, void *t, void *p2, int val2) {} +static void latx_fast_jmp_cache_free_rcu(void *p) { + if (!p) { return; } + CHECK(p == (void *)0x1234); retired_caches++; +} +static void target_munmap(uintptr_t address, size_t length, int flags) { + CHECK(length == sizeof(uint64_t) * TARGET_GDT_ENTRIES); + tracked_free((void *)address); gdt_unmapped++; +} +static void rcu_unregister_thread(void) { rcu_unregistered++; } +static void cpu_list_lock(void) { CHECK(!cpu_list_locked); cpu_list_locked = true; } +static void cpu_list_unlock(void) { CHECK(cpu_list_locked); cpu_list_locked = false; } +static void guest_seccomp_filter_unref(void *p) { + CHECK(cpu_list_locked && p == (void *)0x5678); filter_unref_count++; +} +static void QEMU_NORETURN fake_pthread_exit(void *p) { + CHECK(rcu_unregistered); longjmp(exit_buffer, 1); +} +#define malloc tracked_malloc +#define free tracked_free +#define g_free tracked_free +#define g_tree_new tracked_tree_new +#define g_tree_destroy tracked_tree_destroy +#define pthread_mutex_unlock fake_mutex_unlock +#define pthread_exit fake_pthread_exit +''' + +MAIN = r''' +static void check_empty(void) { + CHECK(allocations == 0); + CHECK(!buf && !pre_nlmh && all_len == 0); + CHECK(!lsenv && !tr_data_real.ir2_inst_array && !tr_data_real.imm_cache); +#ifdef CONFIG_LATX_TU + CHECK(!tu_data && trees == 0); +#endif + CHECK(!smc_retrans_tree && trees == 0); +} +static void thread_exit_case(int mode) { + TranslationBlock shared_tb = { .pc = 42, .marker = 0xfeed }; + CPUState cpu = { .opaque = malloc(sizeof(TaskState)) }; + CPUArchState env = { .cpu = &cpu, .tb_jmp_cache_ptr = (void *)0x1234 }; + cpu.opaque->child_tidptr = 0; + cpu.opaque->seccomp_filter = (void *)0x5678; + thread_cpu = &cpu; + lsenv = &lsenv_real; + lsenv->tr_data = &tr_data_real; +#ifdef CONFIG_LATX_TU + tu_control_init(); + g_tree_insert(tu_data->tree, &shared_tb, &shared_tb); +#endif + smc_retrans_tree_init(); + CHECK(smc_retrans_insert(&shared_tb)); + if (mode != 0) { + tr_data_real.ir2_inst_array = malloc(400 * 44); + tr_data_real.imm_cache = malloc(sizeof(IMM_CACHE)); + tr_data_real.imm_cache->bucket = mode == 1 ? NULL : malloc(300 * 72); + } + if (mode == 3) { + struct iovec iov = {0}; + struct msghdr msg = { .msg_iov = &iov, .msg_iovlen = 1 }; + set_16k_buf(&msg); + all_len = 8192; /* The guest exits before draining pending messages. */ + } + if (!setjmp(exit_buffer)) { + exit_guest_thread_locked(&env); + } + CHECK(thread_cpu == NULL); + CHECK(shared_tb.marker == 0xfeed); /* TU trees do not own shared TBs. */ + check_empty(); + latx_smc_thread_cleanup(); + latx_smc_thread_cleanup(); + check_empty(); +#ifdef HAVE_LATX_DESTROY + latx_lsenv_destroy(); + latx_lsenv_destroy(); + check_empty(); +#endif +} +int main(void) { +#ifdef HAVE_LATX_DESTROY + latx_lsenv_destroy(); /* No initialization. */ + check_empty(); +#endif + for (int i = 0; i < 32; i++) { + const int modes[] = { 2, 0, 1, 3 }; + for (int j = 0; j < 4; j++) { + thread_exit_case(modes[j]); + } + } + CHECK(retired_caches == 128 && rcu_unregistered == 128); + CHECK(filter_unref_count == 128); + puts("thread teardown: lazy, partial, full, pending netlink, repeated cleanup passed"); + return 0; +} +''' + +NETLINK_STUBS = r''' +static bool buf_need_fix(struct msghdr *msg, int fd) { return true; } +static int get_errno(int ret) { return ret; } +static bool is_error(int ret) { return ret < 0; } +static int recv_result; +static int safe_recvmsg(int fd, struct msghdr *msg, int flags) { + CHECK(msg->msg_iov->iov_len == BUFF_16K); + if (recv_result >= 0) { + memset(msg->msg_iov->iov_base, 0, BUFF_16K); + struct nlmsghdr *h = msg->msg_iov->iov_base; + h->nlmsg_len = recv_result; + } + return recv_result; +} +static abi_long receive_fragment(struct msghdr msg) { + abi_long ret; + int fd = 3, flags = 0; +''' + +NETLINK_MAIN = r''' +out: + return ret; +} +int main(void) { + char guest_buffer[BUFF_4K]; + struct iovec iov = { .iov_base = guest_buffer, .iov_len = sizeof(guest_buffer) }; + struct msghdr msg = { .msg_iov = &iov, .msg_iovlen = 1 }; + for (int i = 0; i < 32; i++) { + recv_result = -11; /* EAGAIN must leave the original guest iov intact. */ + CHECK(receive_fragment(msg) == -11); + CHECK(iov.iov_base == guest_buffer && iov.iov_len == BUFF_4K); + CHECK(allocations == 0 && !buf && !pre_nlmh && all_len == 0); + recv_result = NLMSG_LENGTH(0); + CHECK(receive_fragment(msg) == recv_result); + CHECK(iov.iov_base == guest_buffer && iov.iov_len == BUFF_4K); + CHECK(allocations == 0 && !buf && !pre_nlmh && all_len == 0); + } + puts("netlink receive: EAGAIN rollback, successful drain, retry passed"); + return 0; +} +''' + + +CLONE_STUBS = r''' +typedef struct { int mutex, cond; unsigned tid; } new_thread_info; +#define sigprocmask(...) ((void)0) +#define pthread_attr_destroy(...) ((void)0) +#define pthread_cond_wait(...) ((void)0) +#define pthread_cond_destroy(...) ((void)0) +#define pthread_mutex_destroy(...) ((void)0) +static int finish_clone(int ret, CPUArchState *new_env) { + int thread_errno = 0; + new_thread_info info = { .tid = 42 }; +''' + +CLONE_MAIN = r''' + return ret; +} +int main(void) { + TRANSLATION_DATA *parent_data = &tr_data_real; + ENV *parent_lsenv = &lsenv_real; + char *parent_gdt = malloc(72); + parent_gdt[0] = 0x42; + lsenv = parent_lsenv; + lsenv->tr_data = parent_data; + release_cpu = 1; + for (int i = 0; i < 64; i++) { + CPUArchState child_env = {0}; + child_env.cpu = malloc(sizeof(CPUState)); + child_env.cpu->opaque = malloc(sizeof(TaskState)); + child_env.cpu->opaque->seccomp_filter = (void *)0x5678; + child_env.gdt.base = (uintptr_t)malloc(72); + child_env.tb_jmp_cache_ptr = (void *)0x1234; + errno = EBUSY; /* pthread_create returns an error number, not errno. */ + CHECK(finish_clone(EAGAIN, &child_env) == -1); + CHECK(allocations == 1 && errno == EAGAIN); + CHECK(lsenv == parent_lsenv && lsenv->tr_data == parent_data); + CHECK(parent_gdt[0] == 0x42 && rcu_unregistered == 0); + } + CHECK(cpu_unrealized == 64 && gdt_unmapped == 64); + CHECK(retired_caches == 64 && filter_unref_count == 64); + CHECK(finish_clone(0, NULL) == 42); /* A successful child stays owned by it. */ + CHECK(allocations == 1 && cpu_unrealized == 64); + free(parent_gdt); + puts("clone rollback: child CPU/GDT/cache/filter release, parent ownership, errno passed"); + return 0; +} +''' + + +CACHE_MAIN = r''' + return 0; +} +int main(void) { + CPUArchState child_env = {0}; + char *parent_gdt = malloc(72); + parent_gdt[0] = 0x42; + child_env.cpu = malloc(sizeof(CPUState)); + child_env.cpu->opaque = malloc(sizeof(TaskState)); + child_env.cpu->opaque->seccomp_filter = (void *)0x5678; + child_env.gdt.base = (uintptr_t)malloc(72); + child_env.tb_jmp_cache_ptr = (void *)0x1234; /* Borrowed from cpu_copy. */ + release_cpu = 1; + errno = EBUSY; + CHECK(fail_cache_init(&child_env) == -1 && errno == ENOMEM); + CHECK(allocations == 1 && parent_gdt[0] == 0x42); + CHECK(retired_caches == 0); /* The inherited parent cache is not ours. */ + CHECK(cpu_unrealized == 1 && gdt_unmapped == 1 && filter_unref_count == 1); + free(parent_gdt); + puts("clone cache allocation failure: child rollback, borrowed parent cache preserved"); + return 0; +} +''' + + +PUBLISH_PRELUDE = r''' +#include +#include +#include +#include +#include +#define CHECK(x) do { if (!(x)) { \ + fprintf(stderr, "%s:%d: %s\n", __FILE__, __LINE__, #x); exit(1); \ +} } while (0) +#define CLONE_CHILD_CLEARTID 1 +#define CLONE_SETTLS 2 +typedef struct Filter { struct Filter *previous; int references; } Filter; +typedef struct { + int bprm, info, signal_mask, ipc_namespace_isolated, child_tidptr; + Filter *seccomp_filter; +} TaskState; +typedef struct { TaskState *opaque; } CPUState; +typedef struct { CPUState *cpu; } CPUArchState; +static Filter first, second; +static TaskState parent, child; +static CPUState child_cpu; +static bool list_locked, pending; +static int schedule; +static void run_tsync(void) { + if (!pending) { return; } + pending = false; + second.references = 1; + second.previous = parent.seccomp_filter; + second.previous->references++; + TaskState *tasks[] = { &parent, child_cpu.opaque }; + for (int i = 0; i < 2; i++) { + TaskState *task = tasks[i]; + if (!task) { continue; } + if (task->seccomp_filter) { task->seccomp_filter->references--; } + task->seccomp_filter = &second; + second.references++; + } + second.references--; +} +static void cpu_list_lock(void) { + CHECK(!list_locked); + if (schedule == 0) { run_tsync(); } /* TSYNC wins the publication lock. */ + list_locked = true; +} +static void cpu_list_unlock(void) { + CHECK(list_locked && child_cpu.opaque == &child); + CHECK(child.seccomp_filter == parent.seccomp_filter); + list_locked = false; + if (schedule == 1) { run_tsync(); } /* TSYNC follows full publication. */ +} +static Filter *guest_seccomp_filter_ref(Filter *filter) { + if (pending && schedule == 0 && !list_locked) { + run_tsync(); /* Reproduce a TSYNC racing an unlocked clone root read. */ + } + if (filter) { filter->references++; } + return filter; +} +static CPUState *env_cpu(CPUArchState *env) { return env->cpu; } +static void cpu_set_tls(CPUArchState *env, unsigned long tls) {} +static void publish_child(CPUArchState *new_env, TaskState *ts, + TaskState *parent_ts) { + CPUState *new_cpu; + int flags = 0, child_tidptr = 0; + unsigned long newtls = 0; +''' + +PUBLISH_MAIN = r''' +} +int main(void) { + for (schedule = 0; schedule < 2; schedule++) { + memset(&first, 0, sizeof(first)); + memset(&second, 0, sizeof(second)); + memset(&child, 0, sizeof(child)); + parent = (TaskState) { .bprm = 42, .info = 43, .signal_mask = 44, + .ipc_namespace_isolated = 45, .seccomp_filter = &first }; + first.references = 1; + child_cpu.opaque = NULL; + CPUArchState env = { .cpu = &child_cpu }; + pending = true; + publish_child(&env, &child, &parent); + CHECK(!pending && !list_locked && child_cpu.opaque == &child); + CHECK(child.seccomp_filter == &second && parent.seccomp_filter == &second); + CHECK(first.references == 1 && second.references == 2); + CHECK(child.bprm == parent.bprm && child.info == parent.info); + CHECK(child.signal_mask == parent.signal_mask); + CHECK(child.ipc_namespace_isolated == parent.ipc_namespace_isolated); + } + puts("clone publication: TSYNC before/after task publication preserves exact root ownership"); + return 0; +} +''' + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--repo", type=pathlib.Path, + default=pathlib.Path(__file__).resolve().parents[2]) + parser.add_argument("--revision", help="Read production sources from a git revision") + parser.add_argument("--case", choices=("all", "thread", "netlink", "clone", "publication"), + default="all") + args = parser.parse_args() + root = args.repo + + def source(path): + if args.revision: + return subprocess.check_output( + ["git", "-C", str(root), "show", f"{args.revision}:{path}"], text=True) + return (root / path).read_text() + + syscall = source("linux-user/syscall.c") + config = source("target/i386/latx/latx-config.c") + tu = source("target/i386/latx/optimization/tu.c") + translate = source("accel/tcg/translate-all.c") + reset = function(syscall, "static void reset_16k_buf(", optional=True) + set_buffer = function(syscall, "static void set_16k_buf(") + destroy = function(config, "void latx_lsenv_destroy(", optional=True) + tu_destroy = function(tu, "void tu_control_destroy(", optional=True) + # Both syscall exit and seccomp KILL_THREAD must keep the common teardown. + assert syscall.count("exit_guest_thread_locked(env);") == 2 + cases = {} + if args.case in ("all", "thread"): + smc_functions = "".join(function(translate, signature) for signature in ( + "static gint smc_retrans_cmp(", "static inline void smc_retrans_tree_init(", + "static inline void *smc_retrans_lookup(", "static bool smc_retrans_insert(", + "static void smc_retrans_destory(")) + smc_functions += function(translate, "void latx_smc_thread_cleanup(", optional=True) + if "void latx_smc_thread_cleanup(" not in translate: + smc_functions += "static void latx_smc_thread_cleanup(void) {}\n" + common = (PRELUDE + reset + set_buffer + smc_functions + + function(syscall, "static void cleanup_guest_seccomp(", optional=True)) + tu_functions = "".join(function(tu, signature) for signature in ( + "static gint gpc_cmp(", "static inline void tu_trees_init(", + "void tu_control_init(")) + tu_destroy + exit_body = function(syscall, "static void QEMU_NORETURN exit_guest_thread_locked(") + define_destroy = "#define HAVE_LATX_DESTROY 1\n" if destroy else "" + cases["thread-no-tu"] = common + define_destroy + destroy + exit_body + MAIN + cases["thread-tu"] = ("#define CONFIG_LATX_TU 1\n" + common + + define_destroy + tu_functions + destroy + exit_body + MAIN) + if args.case in ("all", "netlink"): + start = syscall.index(" bool need_fix = buf_need_fix(&msg, fd);") + end = syscall.index("\n\n if (!is_error(ret))", start) + cases["netlink"] = (PRELUDE + reset + set_buffer + + function(syscall, "static abi_long get_from_16k_buf(") + + NETLINK_STUBS + syscall[start:end] + NETLINK_MAIN) + if args.case in ("all", "clone"): + start = syscall.index(" ret = pthread_create(&info.thread,") + start = syscall.index(" sigprocmask(SIG_SETMASK,", start) + end = syscall.index("\n } else {\n /* if no CLONE_VM", start) + cases["clone"] = (PRELUDE + + function(syscall, "static void cleanup_guest_thread_resources(") + + function(syscall, "static void cleanup_guest_seccomp(", optional=True) + + function(syscall, "static void cleanup_failed_guest_thread(", optional=True) + + CLONE_STUBS + syscall[start:end] + CLONE_MAIN) + if "new_env->tb_jmp_cache_ptr = NULL;" in syscall: + start = syscall.index(" new_env->tb_jmp_cache_ptr = NULL;") + end = syscall.index("\n#endif", start) + cases["clone-cache"] = (PRELUDE + + function(syscall, "static void cleanup_guest_thread_resources(") + + function(syscall, "static void cleanup_guest_seccomp(", optional=True) + + function(syscall, "static void cleanup_failed_guest_thread(") + + "static bool latx_fast_jmp_cache_init(CPUArchState *env) {\n" + " CHECK(env->tb_jmp_cache_ptr == NULL); return false;\n}\n" + "static int fail_cache_init(CPUArchState *new_env) {\n" + + syscall[start:end] + CACHE_MAIN) + if args.case in ("all", "publication"): + start = syscall.index(" new_cpu = env_cpu(new_env);") + end = syscall.index("\n#ifdef CONFIG_LATX_FAST_JMPCACHE", start) + cases["publication"] = PUBLISH_PRELUDE + syscall[start:end] + PUBLISH_MAIN + cc = shlex.split(os.environ.get("CC", "cc")) + glib_flags = shlex.split(subprocess.check_output( + ["pkg-config", "--cflags", "--libs", "glib-2.0"], text=True)) + with tempfile.TemporaryDirectory(prefix="latx-thread-cleanup-") as directory: + for name, source in cases.items(): + for mode in ([], ["-DNDEBUG"]): + binary = pathlib.Path(directory) / name + subprocess.run(cc + ["-std=gnu11", "-O2"] + mode + + ["-x", "c", "-", "-o", str(binary)] + glib_flags, + input=source, text=True, check=True) + subprocess.run([str(binary)], cwd=directory, check=True) + + +if __name__ == "__main__": + main()