diff --git a/linux-user/i386/cpu_loop.c b/linux-user/i386/cpu_loop.c index 98c0afcdcc..cf619d1bba 100644 --- a/linux-user/i386/cpu_loop.c +++ b/linux-user/i386/cpu_loop.c @@ -221,6 +221,7 @@ void cpu_loop(CPUX86State *env) cpu_exec_end(cs); process_queued_cpu_work(cs); #if defined(CONFIG_LATX_KZT) + kzt_reclaim_unloaded_headers(); if (latx_kzt_runtime_enabled() && trapnr == 0xCC) break; #endif diff --git a/target/i386/latx/context/box64context.c b/target/i386/latx/context/box64context.c index c052561d03..fd832a187e 100755 --- a/target/i386/latx/context/box64context.c +++ b/target/i386/latx/context/box64context.c @@ -142,6 +142,12 @@ int AddKztDebugInfo(box64context_t* ctx, struct latx_kzt_debug* debuginfo) int AddElfHeader(box64context_t* ctx, elfheader_t* head) { int idx = ctx->elfsize; + for (int i = 0; i < ctx->elfsize; i++) { + if (!ctx->elfs[i]) { + ctx->elfs[i] = head; + return i; + } + } if(idx==ctx->elfcap) { // resize... ctx->elfcap += 16; diff --git a/target/i386/latx/context/myalign.c b/target/i386/latx/context/myalign.c index 3950b17d29..e1b8b235fe 100644 --- a/target/i386/latx/context/myalign.c +++ b/target/i386/latx/context/myalign.c @@ -2131,6 +2131,7 @@ static kzt_public_loader_observer_t kzt_public_loader_observer; static int kzt_main_relocated_before_relro; static int kzt_main_fallback_reported; static int kzt_observer_failure_reported; +static bool kzt_header_cleanup_pending; static uint32 kzt_public_r_brk_inst[2]; extern void* x86free; extern void* x86realloc; @@ -2373,6 +2374,121 @@ static int kzt_loader_snapshot_visit( return 0; } +static bool kzt_public_header_is_unmapped(const elfheader_t *head) +{ + bool has_load = false; + + for (size_t i = 0; i < head->numPHEntries; i++) { + const Elf64_Phdr *phdr = &head->PHEntries[i]; + uintptr_t start, last, address; + + if (phdr->p_type != PT_LOAD || !phdr->p_memsz) { + continue; + } + has_load = true; + if (phdr->p_vaddr > UINTPTR_MAX - head->public_load_bias) { + return false; + } + start = head->public_load_bias + phdr->p_vaddr; + if (phdr->p_memsz - 1 > UINTPTR_MAX - start) { + return false; + } + last = (start + phdr->p_memsz - 1) & TARGET_PAGE_MASK; + for (address = start & TARGET_PAGE_MASK; ; + address += TARGET_PAGE_SIZE) { + if (page_get_flags(address) & PAGE_VALID) { + return false; + } + if (address == last) { + break; + } + } + } + return has_load; +} + +static bool kzt_public_header_can_retire( + const elfheader_t *head, + const kzt_public_loader_observer_t *snapshot) +{ + if (!head || !head->public_link_map || head == elf_header || head->lib || + kzt_public_loader_observer_has_map(snapshot, head->public_link_map)) { + return false; + } + for (int i = 0; i < my_context->mallocmapsize; i++) { + if (my_context->mallocmaps[i]->h == head) { + return false; + } + } + return kzt_public_header_is_unmapped(head); +} + +typedef struct KZTRetiredElfHeader { + struct rcu_head rcu; + elfheader_t *head; +} KZTRetiredElfHeader; + +static void kzt_free_retired_header(KZTRetiredElfHeader *retired) +{ + FreeElfHeader(&retired->head); + g_free(retired); +} + +/* Called outside cpu_exec, with no mmap lock held. */ +void kzt_reclaim_unloaded_headers(void) +{ + kzt_public_loader_observer_t snapshot; + kzt_public_loader_result_t result; + + if (!qatomic_read(&kzt_header_cleanup_pending) || + !qatomic_xchg(&kzt_header_cleanup_pending, false)) { + return; + } + /* Native calls can wait indefinitely. Retry after a later loader event + * rather than blocking guest execution for optional reclamation. */ + if (!start_exclusive_timeout(10)) { + return; + } + mmap_lock(); + snapshot = kzt_public_loader_observer; + result = kzt_public_loader_observer_refresh( + &snapshot, &kzt_public_loader_reader, kzt_loader_snapshot_visit, NULL); + if (result == KZT_PUBLIC_LOADER_OK) { + for (int i = 0; i < my_context->elfsize; i++) { + elfheader_t *head = my_context->elfs[i]; + + if (kzt_public_header_can_retire(head, &snapshot)) { + KZTRetiredElfHeader *retired = g_new(KZTRetiredElfHeader, 1); + + retired->head = head; + my_context->elfs[i] = NULL; + /* A nested guest callback can leave an outer native frame + * borrowing the header. Its cpu_exec RCU read section must + * finish before the ELF metadata itself is freed. */ + call_rcu(retired, kzt_free_retired_header, rcu); + } + } + while (my_context->elfsize > 0 && + !my_context->elfs[my_context->elfsize - 1]) { + my_context->elfsize--; + } + } + mmap_unlock(); + end_exclusive(); +} + +static void kzt_request_header_cleanup(CPUX86State *env) +{ + for (int i = 0; i < my_context->elfsize; i++) { + if (kzt_public_header_can_retire( + my_context->elfs[i], &kzt_public_loader_observer)) { + qatomic_set(&kzt_header_cleanup_pending, true); + cpu_exit(env_cpu(env)); + break; + } + } +} + uintptr_t kzt_resolve_guest_symbol(const char *name) { kzt_public_loader_observer_t snapshot; @@ -2714,6 +2830,8 @@ static int kzt_try_bind_loaded_object( box_free(writes); kzt_report_object_recovery_if_needed(object, name_copy); + h->public_link_map = object->link_map_addr; + h->public_load_bias = object->load_bias; AddElfHeader(my_context, h); collectX86free(h); if (!x86free && !strcmp(rbasename, libcName)) { @@ -2970,6 +3088,9 @@ static void kzt_dynamic_library_change_callback(CPUX86State *env) result = kzt_public_loader_observer_refresh( &kzt_public_loader_observer, &kzt_public_loader_reader, kzt_try_bind_observed_object, NULL); + if (result == KZT_PUBLIC_LOADER_OK) { + kzt_request_header_cleanup(env); + } mmap_unlock(); if (result != KZT_PUBLIC_LOADER_OK && result != KZT_PUBLIC_LOADER_BUSY) { diff --git a/target/i386/latx/context/pathcoll.c b/target/i386/latx/context/pathcoll.c index 27fd7e8dbc..0927610a4b 100755 --- a/target/i386/latx/context/pathcoll.c +++ b/target/i386/latx/context/pathcoll.c @@ -112,6 +112,17 @@ void PrependPath(const char* path, path_collection_t* collection, int folder) if(l) { if(folder && tmp[l-1]!='/') strcat(tmp, "/"); + for (int i = 0; i < collection->size; i++) { + if (!strcmp(tmp, collection->paths[i])) { + char *existing = collection->paths[i]; + + /* Preserve precedence without accumulating reload duplicates. */ + memmove(collection->paths + 1, collection->paths, + i * sizeof(*collection->paths)); + collection->paths[0] = existing; + return; + } + } if(collection->size==collection->cap) { collection->cap += 4; collection->paths = (char**)box_realloc(collection->paths, collection->cap*sizeof(char*)); diff --git a/target/i386/latx/include/elfloader_private.h b/target/i386/latx/include/elfloader_private.h index 933be08830..8b1f4e38f0 100755 --- a/target/i386/latx/include/elfloader_private.h +++ b/target/i386/latx/include/elfloader_private.h @@ -102,6 +102,9 @@ struct elfheader_s { int had_RelocateElf; int latx_type; int latx_hasfix; + /* Only public-loader headers participate in unload-time retirement. */ + uintptr_t public_link_map; + uintptr_t public_load_bias; }; int LoadSHNative(int fd, Elf64_Shdr *s, void** SH, const char* name, uint32_t type); int LoadSH(FILE *f, Elf64_Shdr *s, void** SH, const char* name, uint32_t type); diff --git a/target/i386/latx/include/kzt-runtime.h b/target/i386/latx/include/kzt-runtime.h index 906cb7fd55..f3372589f4 100644 --- a/target/i386/latx/include/kzt-runtime.h +++ b/target/i386/latx/include/kzt-runtime.h @@ -22,6 +22,7 @@ */ extern int option_kzt; extern uint32_t kzt_effective_groups; +void kzt_reclaim_unloaded_headers(void); static inline bool latx_kzt_runtime_enabled(void) { diff --git a/tests/unit/kzt/test-kzt-elf-lifecycle.py b/tests/unit/kzt/test-kzt-elf-lifecycle.py new file mode 100644 index 0000000000..b6b689bd72 --- /dev/null +++ b/tests/unit/kzt/test-kzt-elf-lifecycle.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Test the production public-ELF reclamation bodies at an ownership seam. + +The mock RCU queue deliberately retains callbacks until the test drains it. +Guest dlopen/close tests separately exercise the real loader and RCU runtime. +""" +import argparse +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 +#define CHECK(x) do { if (!(x)) { fprintf(stderr, "%d: %s\n", \ + __LINE__, #x); exit(1); } } while (0) +#define TARGET_PAGE_SIZE UINT64_C(4096) +#define TARGET_PAGE_MASK (~(TARGET_PAGE_SIZE - 1)) +#define PAGE_VALID 8 +#define LOG_INFO 0 +#define printf_log(...) ((void)0) +#define g_new(t, n) ((t *)calloc(n, sizeof(t))) +#define g_free free +#define box_realloc realloc +#define qatomic_xchg(p, v) __atomic_exchange_n(p, v, __ATOMIC_SEQ_CST) +#define qatomic_read(p) __atomic_load_n(p, __ATOMIC_SEQ_CST) +#define qatomic_set(p, v) __atomic_store_n(p, v, __ATOMIC_SEQ_CST) +struct rcu_head { void *next; }; +typedef struct elfheader_s { + size_t numPHEntries; + Elf64_Phdr *PHEntries; + uintptr_t public_link_map, public_load_bias; + void *lib; +} elfheader_t; +struct malloc_map { elfheader_t *h; }; +typedef struct { + elfheader_t **elfs; + int elfcap, elfsize, mallocmapsize; + struct malloc_map **mallocmaps; +} box64context_t; +typedef struct { int running; } CPUX86State; +typedef struct { uintptr_t live_maps[8]; unsigned live_map_count; } + kzt_public_loader_observer_t; +typedef int kzt_public_loader_result_t; +#define KZT_PUBLIC_LOADER_OK 0 +#define KZT_PUBLIC_LOADER_BUSY 1 +static kzt_public_loader_observer_t kzt_public_loader_observer; +static int kzt_public_loader_reader; +static int snapshot_result, allow_exclusive = 1, exclusive, mmap_locked; +static int kicked, freed, delayed_count; +static uintptr_t mapped_page; +static box64context_t context, *my_context = &context; +static elfheader_t *elf_header; +static bool kzt_header_cleanup_pending; +static void *delayed[32]; +static void (*delayed_fn[32])(void *); +static bool kzt_public_loader_observer_has_map( + const kzt_public_loader_observer_t *o, uintptr_t key) { + for (unsigned i = 0; i < o->live_map_count; i++) { + if (o->live_maps[i] == key) return true; + } + return false; +} +static int page_get_flags(uintptr_t address) { + CHECK(mmap_locked); return address == mapped_page ? PAGE_VALID : 0; +} +static bool start_exclusive_timeout(int timeout) { + CHECK(!mmap_locked && !exclusive && timeout > 0); + if (!allow_exclusive) return false; + exclusive = 1; return true; +} +static void end_exclusive(void) { + CHECK(exclusive && !mmap_locked); exclusive = 0; +} +static void mmap_lock(void) { CHECK(!mmap_locked); mmap_locked = 1; } +static void mmap_unlock(void) { CHECK(mmap_locked); mmap_locked = 0; } +static int kzt_loader_snapshot_visit(void *object, void *opaque) { return 0; } +static int kzt_public_loader_observer_refresh( + kzt_public_loader_observer_t *o, void *reader, void *visit, void *opaque) { + CHECK(exclusive && mmap_locked); return snapshot_result; +} +static CPUX86State *env_cpu(CPUX86State *env) { return env; } +static void cpu_exit(CPUX86State *env) { CHECK(mmap_locked); kicked++; } +static void FreeElfHeader(elfheader_t **head) { + CHECK(*head); free((*head)->PHEntries); free(*head); *head = NULL; freed++; +} +static void defer(void *item, void (*fn)(void *)) { + CHECK(exclusive && mmap_locked && delayed_count < 32); + delayed[delayed_count] = item; delayed_fn[delayed_count++] = fn; +} +#define call_rcu(p, fn, member) defer(p, (void (*)(void *))fn) +static void drain(void) { + for (int i = 0; i < delayed_count; i++) delayed_fn[i](delayed[i]); + delayed_count = 0; +} +''' + +TEST = r''' +static elfheader_t *new_header(uintptr_t key, uintptr_t page) { + elfheader_t *h = calloc(1, sizeof(*h)); + CHECK(h); + h->public_link_map = key; h->public_load_bias = page; + h->numPHEntries = 1; h->PHEntries = calloc(1, sizeof(Elf64_Phdr)); + CHECK(h->PHEntries); + h->PHEntries[0].p_type = PT_LOAD; + h->PHEntries[0].p_memsz = TARGET_PAGE_SIZE * 2; + AddElfHeader(my_context, h); + return h; +} +static void request(CPUX86State *env) { + mmap_lock(); kzt_request_header_cleanup(env); mmap_unlock(); +} +int main(void) { + CPUX86State cpu = {0}; + elfheader_t *main_h = new_header(0, 0x10000); + elfheader_t *live_h = new_header(1, 0x20000); + elfheader_t *dead_h = new_header(2, 0x30000); + elfheader_t *lib_h = new_header(3, 0x40000); + elfheader_t *malloc_h = new_header(4, 0x50000); + struct malloc_map malloc_owner = { .h = malloc_h }; + struct malloc_map *owners[] = { &malloc_owner }; + elf_header = main_h; lib_h->lib = (void *)1; + context.mallocmaps = owners; context.mallocmapsize = 1; + kzt_public_loader_observer.live_maps[0] = 1; + kzt_public_loader_observer.live_map_count = 1; + + /* A partly mapped object must retain its PLT metadata. */ + mapped_page = 0x31000; + request(&cpu); CHECK(!kzt_header_cleanup_pending); + mapped_page = 0; + request(&cpu); CHECK(kzt_header_cleanup_pending && kicked == 1); + allow_exclusive = 0; + kzt_reclaim_unloaded_headers(); + CHECK(!kzt_header_cleanup_pending && !freed && !delayed_count); + /* Another loader event retries existing retired candidates. */ + request(&cpu); allow_exclusive = 1; + snapshot_result = KZT_PUBLIC_LOADER_BUSY; + kzt_reclaim_unloaded_headers(); + CHECK(!freed && !delayed_count && context.elfs[2] == dead_h); + request(&cpu); snapshot_result = KZT_PUBLIC_LOADER_OK; + /* The fresh safe-point snapshot may show link_map address reuse. */ + kzt_public_loader_observer.live_maps[1] = 2; + kzt_public_loader_observer.live_map_count = 2; + kzt_reclaim_unloaded_headers(); + CHECK(!freed && !delayed_count && context.elfs[2] == dead_h); + kzt_public_loader_observer.live_map_count = 1; + request(&cpu); kzt_reclaim_unloaded_headers(); + CHECK(context.elfs[2] == NULL && delayed_count == 1 && freed == 0); + CHECK(dead_h->PHEntries[0].p_type == PT_LOAD); /* Borrow valid until RCU. */ + CHECK(context.elfs[0] == main_h && context.elfs[1] == live_h); + CHECK(context.elfs[3] == lib_h && context.elfs[4] == malloc_h); + drain(); CHECK(freed == 1); + for (unsigned i = 0; i < 2000; i++) { + elfheader_t *h = new_header(100 + i, 0x60000); + CHECK(context.elfsize == 5 && context.elfs[2] == h); + request(&cpu); kzt_reclaim_unloaded_headers(); drain(); + CHECK(context.elfs[2] == NULL && context.elfsize == 5); + } + CHECK(context.elfcap == 16 && freed == 2001); + for (int i = 0; i < context.elfsize; i++) { + if (context.elfs[i]) FreeElfHeader(&context.elfs[i]); + } + free(context.elfs); + puts("KZT ELF retirement: live/borrowed guards, safe-point retry, RCU and slot reuse passed"); + return 0; +} +''' + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('repo', type=Path) + parser.add_argument('--cc', default='cc') + args = parser.parse_args() + source = (args.repo / 'target/i386/latx/context/myalign.c').read_text() + context = (args.repo / 'target/i386/latx/context/box64context.c').read_text() + fields = source[source.index('typedef struct KZTRetiredElfHeader'):] + fields = fields[:fields.index('} KZTRetiredElfHeader;') + + len('} KZTRetiredElfHeader;')] + '\n' + body = PRELUDE + fields + for signature in ('static bool kzt_public_header_is_unmapped(', + 'static bool kzt_public_header_can_retire(', + 'static void kzt_free_retired_header(', + 'void kzt_reclaim_unloaded_headers(', + 'static void kzt_request_header_cleanup('): + body += function(source, signature) + body += function(context, 'int AddElfHeader(') + TEST + with tempfile.TemporaryDirectory(prefix='latx-elf-lifecycle-') as temp: + fixture = Path(temp) / 'fixture.c' + fixture.write_text(body) + for release in (False, True): + output = Path(temp) / ('release' if release else 'debug') + command = shlex.split(args.cc) + ['-std=gnu11', '-Wall', '-O2'] + if release: + command += ['-DNDEBUG'] + subprocess.run(command + [str(fixture), '-o', str(output)], check=True) + subprocess.run([str(output)], check=True) + + +if __name__ == '__main__': + main() diff --git a/tests/unit/kzt/test_kzt_path_collection.c b/tests/unit/kzt/test_kzt_path_collection.c new file mode 100644 index 0000000000..e288a3a5a3 --- /dev/null +++ b/tests/unit/kzt/test_kzt_path_collection.c @@ -0,0 +1,106 @@ +/* + * SPDX-FileCopyrightText: 2026 LAT Project Authors + * SPDX-License-Identifier: GPL-2.0-only + */ + +#include "qemu/osdep.h" +#include "debug.h" +#include "pathcoll.h" + +char *box_strdup(const char *s) +{ + return strdup(s); +} + +static void test_repeated_runpath(void) +{ + path_collection_t paths = { 0 }; + + ParseList("/system:/fallback", &paths, 1); + for (int i = 0; i < 2000; i++) { + /* The same normalized RUNPATH is processed on each library reload. */ + PrependList(&paths, "/application:/system", 1); + } + g_test_message("paths after 2000 repeated RUNPATH registrations: %d", + paths.size); + g_assert_cmpint(paths.size, ==, 3); + g_assert_cmpstr(paths.paths[0], ==, "/application/"); + g_assert_cmpstr(paths.paths[1], ==, "/system/"); + g_assert_cmpstr(paths.paths[2], ==, "/fallback/"); + FreeCollection(&paths); + g_assert_cmpint(paths.size, ==, 0); + g_assert_cmpint(paths.cap, ==, 0); + g_assert(paths.paths == NULL); +} + +static void test_precedence_and_pointer_ownership(void) +{ + path_collection_t paths = { 0 }; + char *system; + char *fallback; + char *application; + int capacity; + + ParseList("/system:/fallback", &paths, 1); + system = paths.paths[0]; + fallback = paths.paths[1]; + PrependList(&paths, "/application:/system/", 1); + g_assert_cmpint(paths.size, ==, 3); + application = paths.paths[0]; + capacity = paths.cap; + g_assert(paths.paths[1] == system); + g_assert(paths.paths[2] == fallback); + + /* Existing paths move to the new priority rather than being ignored. */ + PrependList(&paths, "/fallback:/application", 1); + g_assert_cmpint(paths.size, ==, 3); + g_assert_cmpint(paths.cap, ==, capacity); + g_assert(paths.paths[0] == fallback); + g_assert(paths.paths[1] == application); + g_assert(paths.paths[2] == system); + + PrependPath(paths.paths[0], &paths, 1); + PrependList(&paths, "::", 1); + PrependList(&paths, NULL, 1); + g_assert_cmpint(paths.size, ==, 3); + g_assert(paths.paths[0] == fallback); + FreeCollection(&paths); +} + +static void test_literal_names_and_existing_duplicates(void) +{ + path_collection_t paths = { 0 }; + char *first; + char *second; + + /* With folder=false, a trailing slash remains part of a literal name. */ + ParseList("name:name/:other", &paths, 0); + first = paths.paths[0]; + second = paths.paths[1]; + PrependPath("name/", &paths, 0); + g_assert_cmpint(paths.size, ==, 3); + g_assert(paths.paths[0] == second); + g_assert(paths.paths[1] == first); + FreeCollection(&paths); + + /* Do not invalidate strings already owned by the initial collection. */ + ParseList("/same:/same:/other", &paths, 1); + first = paths.paths[0]; + second = paths.paths[1]; + PrependPath("/same", &paths, 1); + g_assert_cmpint(paths.size, ==, 3); + g_assert(paths.paths[0] == first); + g_assert(paths.paths[1] == second); + FreeCollection(&paths); +} + +int main(int argc, char **argv) +{ + g_test_init(&argc, &argv, NULL); + g_test_add_func("/kzt-path/repeated-runpath", test_repeated_runpath); + g_test_add_func("/kzt-path/precedence-and-ownership", + test_precedence_and_pointer_ownership); + g_test_add_func("/kzt-path/literal-names-and-existing-duplicates", + test_literal_names_and_existing_duplicates); + return g_test_run(); +} diff --git a/tests/unit/meson.build b/tests/unit/meson.build index 1843ee81dc..7a2c07692b 100644 --- a/tests/unit/meson.build +++ b/tests/unit/meson.build @@ -140,6 +140,24 @@ if 'CONFIG_LATX' in config_host ) endif +test_kzt_path_collection = executable( + 'test-kzt-path-collection', + files( + 'kzt/test_kzt_path_collection.c', + '../../target/i386/latx/context/pathcoll.c', + ) + genh, + include_directories: include_directories('../../target/i386/latx/include'), + dependencies: [glib], +) +test('test-kzt-path-collection', test_kzt_path_collection, suite: 'lat-pr-fast') + +test( + 'test-kzt-elf-lifecycle', + python, + args: [files('kzt/test-kzt-elf-lifecycle.py'), project_source_root], + suite: 'lat-pr-fast', +) + test_kzt_public_loader_observer = executable( 'test-kzt-public-loader-observer', files(