From 708eeaa4ac334fbcba11e3db573f2d3ffb764225 Mon Sep 17 00:00:00 2001 From: Hanlu Li Date: Mon, 20 Jul 2026 18:31:03 +0800 Subject: [PATCH] LATX, kzt: rebase Step1-4 integration on latest upstream Signed-off-by: Hanlu Li --- linux-user/main.c | 43 + target/i386/cpu.h | 9 + target/i386/latx/context/box64context.c | 69 + .../latx/context/dlopen_recycle_transaction.c | 27 + target/i386/latx/context/elf_plt_relocation.c | 9 + target/i386/latx/context/elfloader.c | 527 +++- target/i386/latx/context/elfmap.c | 110 + target/i386/latx/context/kzt_bridge_exact.c | 16 + target/i386/latx/context/kzt_guest_dynamic.c | 265 ++ .../context/kzt_guest_dynamic_diagnostics.c | 499 ++++ .../latx/context/kzt_guest_library_adapter.c | 75 + .../latx/context/kzt_guest_library_binding.c | 1598 +++++++++++ .../latx/context/kzt_guest_link_map_reader.c | 387 +++ target/i386/latx/context/kzt_guest_registry.c | 1551 +++++++++++ .../latx/context/kzt_guest_registry_context.c | 147 ++ .../latx/context/kzt_jump_slot_production.c | 1189 +++++++++ .../i386/latx/context/kzt_jump_slot_route.c | 250 ++ target/i386/latx/context/kzt_lazy_binding.c | 201 ++ .../i386/latx/context/kzt_lazy_diagnostics.c | 247 ++ .../latx/context/kzt_observation_adapter.c | 371 +++ target/i386/latx/context/kzt_owner_resolver.c | 304 +++ target/i386/latx/context/kzt_patch_planner.c | 397 +++ .../i386/latx/context/kzt_patch_spike_guard.c | 388 +++ .../latx/context/kzt_patch_spike_writer.c | 384 +++ .../latx/context/kzt_plt_resolver_adapter.c | 85 + .../i386/latx/context/kzt_rela_diagnostics.c | 439 ++++ .../context/kzt_rela_immediate_candidate.c | 174 ++ .../latx/context/kzt_rela_request_enricher.c | 371 +++ .../latx/context/kzt_rela_runtime_bridge.c | 476 ++++ .../context/kzt_runtime_candidate_shadow.c | 388 +++ .../context/kzt_runtime_got_plt_candidate.c | 995 +++++++ .../context/kzt_wrapper_bridge_provider.c | 183 ++ target/i386/latx/context/kzt_wrapper_probe.c | 240 ++ target/i386/latx/context/librarian.c | 58 +- target/i386/latx/context/library.c | 20 +- target/i386/latx/context/meson.build | 29 + target/i386/latx/context/myalign.c | 287 +- target/i386/latx/context/wrappedlibc.c | 179 +- target/i386/latx/context/wrappedlibdl.c | 180 +- target/i386/latx/include/box64context.h | 20 + target/i386/latx/include/debug.h | 14 + .../latx/include/dlopen_recycle_transaction.h | 16 + target/i386/latx/include/elf_plt_relocation.h | 10 + target/i386/latx/include/elfmap.h | 21 + target/i386/latx/include/kzt_bridge_exact.h | 11 + target/i386/latx/include/kzt_guest_dynamic.h | 47 + .../include/kzt_guest_dynamic_diagnostics.h | 130 + .../latx/include/kzt_guest_dynamic_view.h | 68 + .../latx/include/kzt_guest_library_adapter.h | 29 + .../latx/include/kzt_guest_library_binding.h | 214 ++ .../latx/include/kzt_guest_link_map_reader.h | 72 + target/i386/latx/include/kzt_guest_registry.h | 269 ++ .../latx/include/kzt_guest_registry_context.h | 42 + .../latx/include/kzt_jump_slot_production.h | 71 + .../i386/latx/include/kzt_jump_slot_route.h | 85 + target/i386/latx/include/kzt_lazy_binding.h | 125 + .../i386/latx/include/kzt_lazy_diagnostics.h | 84 + .../latx/include/kzt_loader_callback_scope.h | 13 + .../latx/include/kzt_observation_adapter.h | 82 + target/i386/latx/include/kzt_owner_resolver.h | 56 + target/i386/latx/include/kzt_patch_planner.h | 204 ++ .../i386/latx/include/kzt_patch_spike_guard.h | 114 + .../latx/include/kzt_patch_spike_writer.h | 116 + .../latx/include/kzt_plt_resolver_adapter.h | 67 + .../i386/latx/include/kzt_rela_diagnostics.h | 140 + .../include/kzt_rela_immediate_candidate.h | 89 + .../latx/include/kzt_rela_request_enricher.h | 44 + .../latx/include/kzt_rela_runtime_bridge.h | 36 + .../latx/include/kzt_rela_stub_detector.h | 18 + .../include/kzt_runtime_candidate_shadow.h | 108 + .../include/kzt_runtime_got_plt_candidate.h | 75 + .../include/kzt_wrapper_bridge_provider.h | 72 + target/i386/latx/include/kzt_wrapper_probe.h | 87 + target/i386/latx/include/latx-options.h | 16 +- target/i386/latx/include/librarian.h | 5 + target/i386/latx/latx-options.c | 10 + tests/unit/kzt/guest_e2e/build_guest_probe.sh | 81 + tests/unit/kzt/guest_e2e/kzt_guest_main.c | 79 + tests/unit/kzt/guest_e2e/kzt_guest_probe.c | 85 + tests/unit/kzt/guest_e2e/kzt_guest_probe.h | 19 + tests/unit/kzt/kzt_test_options.c | 3 + tests/unit/kzt/kzt_test_options.h | 8 + .../kzt/test_dlopen_recycle_transaction.c | 354 +++ tests/unit/kzt/test_elf_map_range.c | 250 ++ .../unit/kzt/test_guest_dynamic_diagnostics.c | 496 ++++ tests/unit/kzt/test_guest_dynamic_parser.c | 450 ++++ tests/unit/kzt/test_guest_dynamic_snapshot.c | 500 ++++ tests/unit/kzt/test_guest_library_binding.c | 1692 ++++++++++++ .../kzt/test_guest_library_binding_teardown.c | 51 + tests/unit/kzt/test_guest_link_map_reader.c | 591 +++++ tests/unit/kzt/test_guest_registry.c | 1261 +++++++++ .../kzt/test_guest_registry_concurrency.c | 529 ++++ tests/unit/kzt/test_guest_registry_context.c | 378 +++ tests/unit/kzt/test_jump_slot_route.c | 558 ++++ tests/unit/kzt/test_observation_adapter.c | 2326 +++++++++++++++++ tests/unit/kzt/test_owner_resolver.c | 338 +++ tests/unit/kzt/test_patch_planner.c | 509 ++++ tests/unit/kzt/test_patch_spike_guard.c | 520 ++++ tests/unit/kzt/test_patch_spike_writer.c | 907 +++++++ tests/unit/kzt/test_real_guest_e2e.py | 214 ++ tests/unit/kzt/test_real_guest_performance.py | 148 ++ .../unit/kzt/test_registry_diagnostics_gate.c | 300 +++ .../unit/kzt/test_rela_immediate_candidate.c | 618 +++++ tests/unit/kzt/test_rela_request_enricher.c | 765 ++++++ .../unit/kzt/test_rela_runtime_helper_chain.c | 479 ++++ ...test_runtime_candidate_enrichment_shadow.c | 1093 ++++++++ .../unit/kzt/test_runtime_got_plt_candidate.c | 933 +++++++ tests/unit/kzt/test_wi236_enrichment_matrix.c | 325 +++ .../test_wi237_jump_slot_route_contract.py | 89 + .../kzt/test_wi238_structured_diagnostics.c | 695 +++++ tests/unit/kzt/test_wi254_loader_contract.py | 114 + tests/unit/kzt/test_wi256_lazy_binding.c | 489 ++++ tests/unit/kzt/test_wi256_lazy_diagnostics.c | 341 +++ ..._wi256_lazy_diagnostics_source_contract.py | 137 + .../kzt/test_wi256_lazy_production_bridge.c | 1434 ++++++++++ .../kzt/test_wi256_plt_resolver_adapter.c | 346 +++ ...test_wi256_plt_resolver_source_contract.py | 139 + ...82_registry_observation_source_contract.py | 130 + tests/unit/kzt/test_wrapper_bridge_provider.c | 381 +++ tests/unit/kzt/test_wrapper_probe.c | 405 +++ .../kzt/wi231_step4_writer_test_report.md | 92 + tests/unit/meson.build | 516 ++++ 122 files changed, 37938 insertions(+), 77 deletions(-) create mode 100644 target/i386/latx/context/dlopen_recycle_transaction.c create mode 100644 target/i386/latx/context/elf_plt_relocation.c create mode 100644 target/i386/latx/context/elfmap.c create mode 100644 target/i386/latx/context/kzt_bridge_exact.c create mode 100644 target/i386/latx/context/kzt_guest_dynamic.c create mode 100644 target/i386/latx/context/kzt_guest_dynamic_diagnostics.c create mode 100644 target/i386/latx/context/kzt_guest_library_adapter.c create mode 100644 target/i386/latx/context/kzt_guest_library_binding.c create mode 100644 target/i386/latx/context/kzt_guest_link_map_reader.c create mode 100644 target/i386/latx/context/kzt_guest_registry.c create mode 100644 target/i386/latx/context/kzt_guest_registry_context.c create mode 100644 target/i386/latx/context/kzt_jump_slot_production.c create mode 100644 target/i386/latx/context/kzt_jump_slot_route.c create mode 100644 target/i386/latx/context/kzt_lazy_binding.c create mode 100644 target/i386/latx/context/kzt_lazy_diagnostics.c create mode 100644 target/i386/latx/context/kzt_observation_adapter.c create mode 100644 target/i386/latx/context/kzt_owner_resolver.c create mode 100644 target/i386/latx/context/kzt_patch_planner.c create mode 100644 target/i386/latx/context/kzt_patch_spike_guard.c create mode 100644 target/i386/latx/context/kzt_patch_spike_writer.c create mode 100644 target/i386/latx/context/kzt_plt_resolver_adapter.c create mode 100644 target/i386/latx/context/kzt_rela_diagnostics.c create mode 100644 target/i386/latx/context/kzt_rela_immediate_candidate.c create mode 100644 target/i386/latx/context/kzt_rela_request_enricher.c create mode 100644 target/i386/latx/context/kzt_rela_runtime_bridge.c create mode 100644 target/i386/latx/context/kzt_runtime_candidate_shadow.c create mode 100644 target/i386/latx/context/kzt_runtime_got_plt_candidate.c create mode 100644 target/i386/latx/context/kzt_wrapper_bridge_provider.c create mode 100644 target/i386/latx/context/kzt_wrapper_probe.c create mode 100644 target/i386/latx/include/dlopen_recycle_transaction.h create mode 100644 target/i386/latx/include/elf_plt_relocation.h create mode 100644 target/i386/latx/include/elfmap.h create mode 100644 target/i386/latx/include/kzt_bridge_exact.h create mode 100644 target/i386/latx/include/kzt_guest_dynamic.h create mode 100644 target/i386/latx/include/kzt_guest_dynamic_diagnostics.h create mode 100644 target/i386/latx/include/kzt_guest_dynamic_view.h create mode 100644 target/i386/latx/include/kzt_guest_library_adapter.h create mode 100644 target/i386/latx/include/kzt_guest_library_binding.h create mode 100644 target/i386/latx/include/kzt_guest_link_map_reader.h create mode 100644 target/i386/latx/include/kzt_guest_registry.h create mode 100644 target/i386/latx/include/kzt_guest_registry_context.h create mode 100644 target/i386/latx/include/kzt_jump_slot_production.h create mode 100644 target/i386/latx/include/kzt_jump_slot_route.h create mode 100644 target/i386/latx/include/kzt_lazy_binding.h create mode 100644 target/i386/latx/include/kzt_lazy_diagnostics.h create mode 100644 target/i386/latx/include/kzt_loader_callback_scope.h create mode 100644 target/i386/latx/include/kzt_observation_adapter.h create mode 100644 target/i386/latx/include/kzt_owner_resolver.h create mode 100644 target/i386/latx/include/kzt_patch_planner.h create mode 100644 target/i386/latx/include/kzt_patch_spike_guard.h create mode 100644 target/i386/latx/include/kzt_patch_spike_writer.h create mode 100644 target/i386/latx/include/kzt_plt_resolver_adapter.h create mode 100644 target/i386/latx/include/kzt_rela_diagnostics.h create mode 100644 target/i386/latx/include/kzt_rela_immediate_candidate.h create mode 100644 target/i386/latx/include/kzt_rela_request_enricher.h create mode 100644 target/i386/latx/include/kzt_rela_runtime_bridge.h create mode 100644 target/i386/latx/include/kzt_rela_stub_detector.h create mode 100644 target/i386/latx/include/kzt_runtime_candidate_shadow.h create mode 100644 target/i386/latx/include/kzt_runtime_got_plt_candidate.h create mode 100644 target/i386/latx/include/kzt_wrapper_bridge_provider.h create mode 100644 target/i386/latx/include/kzt_wrapper_probe.h create mode 100755 tests/unit/kzt/guest_e2e/build_guest_probe.sh create mode 100644 tests/unit/kzt/guest_e2e/kzt_guest_main.c create mode 100644 tests/unit/kzt/guest_e2e/kzt_guest_probe.c create mode 100644 tests/unit/kzt/guest_e2e/kzt_guest_probe.h create mode 100644 tests/unit/kzt/kzt_test_options.c create mode 100644 tests/unit/kzt/kzt_test_options.h create mode 100644 tests/unit/kzt/test_dlopen_recycle_transaction.c create mode 100644 tests/unit/kzt/test_elf_map_range.c create mode 100644 tests/unit/kzt/test_guest_dynamic_diagnostics.c create mode 100644 tests/unit/kzt/test_guest_dynamic_parser.c create mode 100644 tests/unit/kzt/test_guest_dynamic_snapshot.c create mode 100644 tests/unit/kzt/test_guest_library_binding.c create mode 100644 tests/unit/kzt/test_guest_library_binding_teardown.c create mode 100644 tests/unit/kzt/test_guest_link_map_reader.c create mode 100644 tests/unit/kzt/test_guest_registry.c create mode 100644 tests/unit/kzt/test_guest_registry_concurrency.c create mode 100644 tests/unit/kzt/test_guest_registry_context.c create mode 100644 tests/unit/kzt/test_jump_slot_route.c create mode 100644 tests/unit/kzt/test_observation_adapter.c create mode 100644 tests/unit/kzt/test_owner_resolver.c create mode 100644 tests/unit/kzt/test_patch_planner.c create mode 100644 tests/unit/kzt/test_patch_spike_guard.c create mode 100644 tests/unit/kzt/test_patch_spike_writer.c create mode 100755 tests/unit/kzt/test_real_guest_e2e.py create mode 100644 tests/unit/kzt/test_real_guest_performance.py create mode 100644 tests/unit/kzt/test_registry_diagnostics_gate.c create mode 100644 tests/unit/kzt/test_rela_immediate_candidate.c create mode 100644 tests/unit/kzt/test_rela_request_enricher.c create mode 100644 tests/unit/kzt/test_rela_runtime_helper_chain.c create mode 100644 tests/unit/kzt/test_runtime_candidate_enrichment_shadow.c create mode 100644 tests/unit/kzt/test_runtime_got_plt_candidate.c create mode 100644 tests/unit/kzt/test_wi236_enrichment_matrix.c create mode 100644 tests/unit/kzt/test_wi237_jump_slot_route_contract.py create mode 100644 tests/unit/kzt/test_wi238_structured_diagnostics.c create mode 100644 tests/unit/kzt/test_wi254_loader_contract.py create mode 100644 tests/unit/kzt/test_wi256_lazy_binding.c create mode 100644 tests/unit/kzt/test_wi256_lazy_diagnostics.c create mode 100644 tests/unit/kzt/test_wi256_lazy_diagnostics_source_contract.py create mode 100644 tests/unit/kzt/test_wi256_lazy_production_bridge.c create mode 100644 tests/unit/kzt/test_wi256_plt_resolver_adapter.c create mode 100644 tests/unit/kzt/test_wi256_plt_resolver_source_contract.py create mode 100644 tests/unit/kzt/test_wi382_registry_observation_source_contract.py create mode 100644 tests/unit/kzt/test_wrapper_bridge_provider.c create mode 100644 tests/unit/kzt/test_wrapper_probe.c create mode 100644 tests/unit/kzt/wi231_step4_writer_test_report.md diff --git a/linux-user/main.c b/linux-user/main.c index e397b76516d..ed38cd4619f 100644 --- a/linux-user/main.c +++ b/linux-user/main.c @@ -80,6 +80,7 @@ int box64_pagesize; uintptr_t box64_load_addr = 0; int dlsym_error = 0; int kzt_call_log = 0; +int kzt_registry_diagnostics = 0; int cycle_log = 0; int allow_missing_libs = 1; int box64_nogtk = 0; @@ -635,6 +636,33 @@ static void handle_arg_latx_kzt(const char *arg) { option_kzt = strtol(arg, NULL, 0); } + +static void handle_arg_latx_kzt_lazy_diagnostics(const char *arg) +{ + option_kzt_lazy_diagnostics = strtol(arg, NULL, 0) > 0; +} + +static void handle_arg_latx_kzt_registry_diagnostics(const char *arg) +{ + kzt_registry_diagnostics = strtol(arg, NULL, 0); +} + +static void handle_arg_latx_kzt_patch_spike(const char *arg) +{ + option_kzt_patch_spike = strtol(arg, NULL, 0) > 0; +} + +static void handle_arg_latx_kzt_patch_spike_write(const char *arg) +{ + option_kzt_patch_spike_write = strtol(arg, NULL, 0) > 0; +} + +static void handle_arg_latx_kzt_patch_spike_budget(const char *arg) +{ + long value = strtol(arg, NULL, 0); + + option_kzt_patch_spike_budget = value > 0 ? (unsigned long)value : 0; +} #endif static void handle_arg_latx_fputag(const char *arg) @@ -818,6 +846,21 @@ static const struct qemu_argument arg_table[] = { #if defined(CONFIG_LATX_KZT) {"latx-kzt", "LATX_KZT", true, handle_arg_latx_kzt, "", "enable kuzhitong"}, + {"latx-kzt-lazy-diagnostics", "LATX_KZT_LAZY_DIAGNOSTICS", + true, handle_arg_latx_kzt_lazy_diagnostics, + "", "enable KZT lazy completion diagnostics"}, + {"latx-kzt-registry-diagnostics", "LATX_KZT_REGISTRY_DIAGNOSTICS", + true, handle_arg_latx_kzt_registry_diagnostics, + "", "enable KZT guest registry diagnostics"}, + {"latx-kzt-patch-spike", "LATX_KZT_PATCH_SPIKE", + true, handle_arg_latx_kzt_patch_spike, + "", "enable KZT controlled patch spike planning"}, + {"latx-kzt-patch-spike-write", "LATX_KZT_PATCH_SPIKE_WRITE", + true, handle_arg_latx_kzt_patch_spike_write, + "", "enable KZT controlled patch spike writes"}, + {"latx-kzt-patch-spike-budget", "LATX_KZT_PATCH_SPIKE_BUDGET", + true, handle_arg_latx_kzt_patch_spike_budget, + "", "set KZT controlled patch spike write budget"}, #endif {"latx-fputag", "LATX_FPUTAG", true, handle_arg_latx_fputag, "", "enable fputag"}, diff --git a/target/i386/cpu.h b/target/i386/cpu.h index 7a3f8aeb894..63e53473d3f 100644 --- a/target/i386/cpu.h +++ b/target/i386/cpu.h @@ -24,6 +24,10 @@ #include "cpu-qom.h" #include "exec/cpu-defs.h" #include "qapi/qapi-types-common.h" +#ifdef CONFIG_LATX_KZT +#include "kzt_lazy_binding.h" +#include "kzt_loader_callback_scope.h" +#endif /* The x86 has a strong memory model with some store-after-load re-ordering */ #define TCG_GUEST_DEFAULT_MO (TCG_MO_ALL & ~TCG_MO_ST_LD) @@ -1404,6 +1408,11 @@ typedef struct HVFX86LazyFlags { typedef struct CPUX86State { #ifdef CONFIG_LATX +#ifdef CONFIG_LATX_KZT + kzt_lazy_binding_pending_t kzt_lazy_binding_pending; + uintptr_t kzt_lazy_original_return; + kzt_guest_library_loader_scope_t kzt_guest_library_loader_scope; +#endif void* checksum_fail_tb; void *ibtc_table_p; uint64_t vregs[5]; diff --git a/target/i386/latx/context/box64context.c b/target/i386/latx/context/box64context.c index c052561d035..d122a115035 100755 --- a/target/i386/latx/context/box64context.c +++ b/target/i386/latx/context/box64context.c @@ -19,10 +19,18 @@ #include "librarian.h" #include "library.h" #include "wrapper.h" +#ifdef CONFIG_LATX_KZT +#include "kzt_guest_registry.h" +#include "kzt_guest_registry_context.h" +#include "kzt_guest_library_binding.h" +#endif #include box64context_t *NewBox64Context(int argc) { +#ifdef CONFIG_LATX_KZT + kzt_patch_spike_config_t patch_spike_config; +#endif // init and put default values box64context_t *context = (box64context_t*)box_calloc(1, sizeof(box64context_t)); @@ -38,10 +46,56 @@ box64context_t *NewBox64Context(int argc) context->argc = argc; context->argv = (char**)box_calloc(context->argc+1, sizeof(char*)); pthread_mutex_init(&context->mutex_lock, NULL); +#ifdef CONFIG_LATX_KZT + /* Optional acceleration metadata is created on first KZT use so a + * context that never observes a guest link-map keeps the old footprint. */ + pthread_mutex_init(&context->kzt_bridge_mutex, NULL); + (void)kzt_guest_library_access_init(&context->kzt_guest_library_access); + kzt_patch_spike_config_from_options(&patch_spike_config); + kzt_patch_spike_guard_init(&context->kzt_patch_spike_guard, + &patch_spike_config); +#endif return context; } +#ifdef CONFIG_LATX_KZT +kzt_guest_registry_t *KztGuestRegistryForContext(box64context_t *context) +{ + if (!context) { + return NULL; + } + return kzt_guest_registry_context_get( + &context->kzt_guest_registry_context, &context->mutex_lock); +} + +kzt_guest_library_bindings_t *KztGuestLibraryBindingsForContext(box64context_t *context) +{ + return context ? context->kzt_guest_library_access.bindings : NULL; +} + +int KztGuestLibraryLookupForContext( + box64context_t *context, + const kzt_guest_library_binding_key_t *key, + kzt_guest_library_handle_t *handle) +{ + return context + ? kzt_guest_library_access_lookup( + &context->kzt_guest_library_access, key, handle) + : -1; +} +#endif + +kzt_patch_spike_guard_t *KztPatchSpikeGuardForContext(box64context_t *context) +{ +#ifdef CONFIG_LATX_KZT + return context ? &context->kzt_patch_spike_guard : NULL; +#else + (void)context; + return NULL; +#endif +} + EXPORTDYN void FreeBox64Context(box64context_t** context) { @@ -53,10 +107,25 @@ void FreeBox64Context(box64context_t** context) box64context_t* ctx = *context; // local copy to do the cleanning +#ifdef CONFIG_LATX_KZT + /* FreeBox64Context is entered only after guest execution and loader + * callbacks have stopped. Close the context-owned lookup gate first and + * drain acquired handles, while keeping registry/binding storage alive + * for the librarian destruction pass below. */ + kzt_guest_library_access_begin_teardown( + &ctx->kzt_guest_library_access); +#endif + if(ctx->local_maplib) FreeLibrarian(&ctx->local_maplib); if(ctx->maplib) FreeLibrarian(&ctx->maplib); +#ifdef CONFIG_LATX_KZT + kzt_guest_library_access_destroy(&ctx->kzt_guest_library_access); + kzt_guest_registry_context_destroy(&ctx->kzt_guest_registry_context, + &ctx->mutex_lock); + pthread_mutex_destroy(&ctx->kzt_bridge_mutex); +#endif FreeDictionnary(&ctx->versym); for(int i=0; ielfsize; ++i) { diff --git a/target/i386/latx/context/dlopen_recycle_transaction.c b/target/i386/latx/context/dlopen_recycle_transaction.c new file mode 100644 index 00000000000..88586ee3342 --- /dev/null +++ b/target/i386/latx/context/dlopen_recycle_transaction.c @@ -0,0 +1,27 @@ +#include "dlopen_recycle_transaction.h" + +void *dlopen_recycle_transaction( + void *success_handle, size_t *count, int increment_count, void *opaque, + dlopen_recycle_prepare_fn prepare, dlopen_recycle_step_fn guest_open, + dlopen_recycle_step_fn reload, + dlopen_recycle_rollback_fn rollback) +{ + int guest_opened = 0; + + if (!success_handle || !count || !prepare || !guest_open || !reload || + !rollback) + return NULL; + prepare(opaque); + if (guest_open(opaque) != 0) { + rollback(opaque, 0); + return NULL; + } + guest_opened = 1; + if (reload(opaque) != 0) { + rollback(opaque, guest_opened); + return NULL; + } + if (increment_count) + ++*count; + return success_handle; +} diff --git a/target/i386/latx/context/elf_plt_relocation.c b/target/i386/latx/context/elf_plt_relocation.c new file mode 100644 index 00000000000..1ea8a754f6c --- /dev/null +++ b/target/i386/latx/context/elf_plt_relocation.c @@ -0,0 +1,9 @@ +#include "elf_plt_relocation.h" + +int elf_plt_relocation_apply(elf_plt_relocation_apply_fn apply, void *opaque, + int *need_resolver) +{ + if (!apply) + return -1; + return apply(opaque, need_resolver) ? -1 : 0; +} diff --git a/target/i386/latx/context/elfloader.c b/target/i386/latx/context/elfloader.c index 691cf583503..0f2b0fc9e5d 100755 --- a/target/i386/latx/context/elfloader.c +++ b/target/i386/latx/context/elfloader.c @@ -31,6 +31,19 @@ #include "dictionnary.h" #include "symbols.h" #include "lsenv.h" +#include "kzt_rela_stub_detector.h" +#include "kzt_guest_registry.h" +#include "kzt_observation_adapter.h" +#include "kzt_jump_slot_production.h" +#include "kzt_lazy_diagnostics.h" +#include "kzt_plt_resolver_adapter.h" +#include "elf_plt_relocation.h" +#include "elfmap.h" + +#ifdef CONFIG_LATX_KZT +#include "qemu.h" +extern int wine_option_kzt; +#endif void* my__IO_2_1_stderr_ = NULL; void* my__IO_2_1_stdin_ = NULL; @@ -649,6 +662,7 @@ int RelocateElfRELA(lib_t *maplib, lib_t *local_maplib, int bindnow, elfheader_t uint64_t *p = (uint64_t*)(rela[i].r_offset + head->delta); uintptr_t offs = 0; uintptr_t end = 0; + library_t *resolved_provider = NULL; int version = head->VerSym?((Elf64_Half*)((uintptr_t)head->VerSym+head->delta))[ELF64_R_SYM(rela[i].r_info)]:-1; if(version!=-1) version &=0x7fff; const char* vername = GetSymbolVersion(head, version); @@ -664,9 +678,13 @@ int RelocateElfRELA(lib_t *maplib, lib_t *local_maplib, int bindnow, elfheader_t }*/ // so weak symbol are the one left if(!offs && !end) { - GetGlobalSymbolStartEnd(maplib, symname, &offs, &end, head, version, vername); + GetGlobalSymbolStartEndWithProvider( + maplib, symname, &offs, &end, head, version, vername, + &resolved_provider); if(!offs && !end && local_maplib) { - GetGlobalSymbolStartEnd(local_maplib, symname, &offs, &end, head, version, vername); + GetGlobalSymbolStartEndWithProvider( + local_maplib, symname, &offs, &end, head, version, + vername, &resolved_provider); } } } @@ -685,19 +703,64 @@ int RelocateElfRELA(lib_t *maplib, lib_t *local_maplib, int bindnow, elfheader_t *p = offs/* + rela[i].r_addend*/; // not addend it seems } break; - case R_X86_64_JUMP_SLOT: + case R_X86_64_JUMP_SLOT: { // apply immediatly for gobject closure marshal or for LOCAL binding. Also, apply immediatly if it doesn't jump in the got - tmp = (uintptr_t)(*p); + uintptr_t slot_observation = (uintptr_t)(*p); + uintptr_t expected_guest_target = slot_observation; + uintptr_t legacy_target = offs + rela[i].r_addend; + int slot_is_unresolved_stub = + kzt_rela_slot_current_is_unresolved_stub( + slot_observation, + KZT_RELA_STUB_COORDINATE_RUNTIME_REBASED, + head->delta, head->plt, head->plt_end, + head->gotplt, head->gotplt_end); + tmp = slot_observation; if (bind==STB_LOCAL || !tmp || !((tmp>=head->plt && tmpplt_end) || (tmp>=head->gotplt && tmpgotplt_end)) || !need_resolv || bindnow - ) { + ) { if (offs){ if(p) { +#ifdef CONFIG_LATX_KZT + if (option_kzt || wine_option_kzt) { + kzt_jump_slot_route_result_t route_result; + + if (kzt_production_jump_slot_route( + my_context, resolved_provider, + legacy_target, head, + need_resolv != NULL, i, + &rela[i], p, + slot_observation, + slot_is_unresolved_stub, + ELF64_R_SYM(rela[i].r_info), + symname, vername, + 1, expected_guest_target, + legacy_target, + &route_result) == 0 && + route_result.status == + KZT_JUMP_SLOT_ROUTE_NATIVE_APPLIED) { + break; + } + if (route_result.status == + KZT_JUMP_SLOT_ROUTE_GUEST_PRESERVED || + route_result.status == + KZT_JUMP_SLOT_ROUTE_CAS_MISMATCH) { + break; + } + printf_kzt_registry_diagnostics( + "KZT eager route fallback slot=%p " + "observed=%p legacy=%p selected=%p " + "status=%d symbol=%s\n", + p, (void *)slot_observation, (void*)legacy_target, + (void *)route_result.selected_target, + route_result.status, + symname ? symname : "(none)"); + } +#endif printf_log(LOG_INFO, "RelocateElfRELA : Apply %s R_X86_64_JUMP_SLOT @%p with sym=%s (%p -> %p)\n", (bind==STB_LOCAL)?"Local":"Global", p, symname, *(void**)p, (void*)(offs+rela[i].r_addend)); - *p =(uint64_t) (offs + rela[i].r_addend); + *p =(uint64_t) legacy_target; } else { printf_log(LOG_INFO, "Warning, Symbol %s found, but Jump Slot Offset is NULL \n", symname); } @@ -709,6 +772,7 @@ int RelocateElfRELA(lib_t *maplib, lib_t *local_maplib, int bindnow, elfheader_t *need_resolv = 1; } break; + } /* case R_X86_64_NONE: break; @@ -730,9 +794,189 @@ int RelocateElfRELA(lib_t *maplib, lib_t *local_maplib, int bindnow, elfheader_t return 0; } +typedef struct elf_plt_rela_context { + lib_t *maplib; + lib_t *local_maplib; + int bindnow; + elfheader_t *head; + int count; + Elf64_Rela *rela; +} elf_plt_rela_context_t; + +static int RelocateElfPltRELA(void *opaque, int *need_resolver) +{ + elf_plt_rela_context_t *rela = opaque; + return RelocateElfRELA(rela->maplib, rela->local_maplib, rela->bindnow, + rela->head, rela->count, rela->rela, + need_resolver); +} + +#ifdef CONFIG_LATX_KZT +static void KztLazyBindingCompleteResolver(void); + +static int kzt_elfloader_read_guest_memory(uintptr_t guest_addr, void *dst, + size_t size, void *opaque) +{ + void *host_ptr; + + (void)opaque; + if ((!dst && size) || (!guest_addr && size)) { + return -1; + } + if (!size) { + return 0; + } + host_ptr = lock_user(VERIFY_READ, (abi_ulong)guest_addr, size, true); + if (!host_ptr) { + return -1; + } + memcpy(dst, host_ptr, size); + unlock_user(host_ptr, (abi_ulong)guest_addr, 0); + return 0; +} + +static int kzt_elfloader_head_identity( + const elfheader_t *head, + kzt_guest_link_map_identity_t *identity) +{ + size_t i; + + if (!head || !identity || !head->PHEntries) { + return -1; + } + memset(identity, 0, sizeof(*identity)); + for (i = 0; i < head->numPHEntries; ++i) { + const Elf64_Phdr *entry = &head->PHEntries[i]; + + if (entry->p_type != PT_DYNAMIC) { + continue; + } + if (head->delta < 0 || + entry->p_vaddr > UINTPTR_MAX - (uintptr_t)head->delta) { + return -1; + } + identity->load_bias = (uintptr_t)head->delta; + identity->dynamic_addr = entry->p_vaddr + identity->load_bias; + return identity->dynamic_addr ? 0 : -1; + } + return -1; +} + +static void kzt_observe_plt_source(elfheader_t *head, + uintptr_t guest_link_map, + const kzt_guest_link_map_identity_t *object_identity) +{ + const kzt_guest_link_map_reader_ops_t reader_ops = { + .read_memory = kzt_elfloader_read_guest_memory, + }; + kzt_observation_adapter_request_t request; + kzt_observation_adapter_result_t result; + uintptr_t map_start = 0; + uintptr_t map_end = 0; + uintptr_t confirmed_main_head = 0; + uintptr_t namespace_head = 0; + uintptr_t predecessor = 0; + kzt_guest_registry_t *registry; + kzt_guest_link_map_identity_t main_identity = { 0 }; + int main_namespace; + int range_available; + + if (!head || !guest_link_map || !(option_kzt || wine_option_kzt)) { + return; + } + registry = KztGuestRegistryForContext(my_context); + (void)kzt_guest_registry_context_get_main_namespace_head( + &my_context->kzt_guest_registry_context, &confirmed_main_head); + if (confirmed_main_head && object_identity && + kzt_guest_registry_context_has_main_namespace_evidence( + &my_context->kzt_guest_registry_context, registry, + guest_link_map, object_identity->load_bias, + object_identity->dynamic_addr)) { + main_namespace = 1; + } else { + if (confirmed_main_head && + kzt_guest_link_map_read_predecessor( + guest_link_map, &reader_ops, &predecessor) == 0 && + predecessor == confirmed_main_head) { + main_namespace = 1; + } else if (confirmed_main_head || + kzt_elfloader_head_identity(elf_header, &main_identity) == 0) { + main_namespace = kzt_guest_link_map_classify_namespace( + guest_link_map, &main_identity, confirmed_main_head, &reader_ops, + &namespace_head); + } else { + main_namespace = -1; + } + if (main_namespace == 1 && !confirmed_main_head && + kzt_guest_registry_context_confirm_main_namespace_head( + &my_context->kzt_guest_registry_context, + &my_context->mutex_lock, namespace_head) != 0) { + main_namespace = -1; + } + } + range_available = GetElfLoadRange( + head->PHEntries, head->numPHEntries, head->delta, TARGET_PAGE_SIZE, + &map_start, &map_end) == 0; + memset(&request, 0, sizeof(request)); + request.enabled = 1; + request.link_map_addr = guest_link_map; + request.registry = registry; + request.library_bindings = + KztGuestLibraryBindingsForContext(my_context); + request.reader_ops = &reader_ops; + request.namespace_id_present = main_namespace == 1; + request.namespace_id = 0; + request.map_range_present = range_available; + request.map_start = map_start; + request.map_end = map_end; + result = KZT_OBSERVATION_ADAPTER_DISABLED; + (void)kzt_observe_guest_object_from_callback(&request, &result); + printf_kzt_registry_diagnostics( + "KZT PLT source observation result=%d link_map=0x%lx " + "main_namespace=%d map_start=0x%lx map_end=0x%lx\n", + result, (unsigned long)guest_link_map, main_namespace, + (unsigned long)map_start, (unsigned long)map_end); +} +#endif + int RelocateElfPlt(lib_t *maplib, lib_t *local_maplib, int bindnow, elfheader_t* head) { int need_resolver = 0; + uintptr_t resolver_got = head->pltgot ? head->pltgot : head->got; + uintptr_t resolver_got_runtime = resolver_got ? + resolver_got + head->delta : 0; + uintptr_t guest_link_map = 0; + uintptr_t guest_resolver = 0; +#ifdef CONFIG_LATX_KZT + uintptr_t kzt_evidence_got = head->pltgot; + uintptr_t kzt_evidence_got_runtime = kzt_evidence_got ? + kzt_evidence_got + head->delta : 0; + kzt_guest_link_map_identity_t expected_identity = { 0 }; + kzt_guest_link_map_identity_t observed_identity = { 0 }; + int resolver_snapshot_available = 0; + + if (kzt_evidence_got_runtime && (option_kzt || wine_option_kzt) && + kzt_elfloader_head_identity(head, &expected_identity) == 0 && + kzt_elfloader_read_guest_memory( + kzt_evidence_got_runtime + 8, &guest_link_map, + sizeof(guest_link_map), NULL) == 0 && + kzt_elfloader_read_guest_memory( + kzt_evidence_got_runtime + 16, &guest_resolver, + sizeof(guest_resolver), NULL) == 0 && guest_resolver && + kzt_guest_link_map_read_identity( + guest_link_map, + &(kzt_guest_link_map_reader_ops_t) { + .read_memory = kzt_elfloader_read_guest_memory, + }, + &observed_identity) == 0 && + kzt_guest_link_map_identity_matches( + &observed_identity, expected_identity.load_bias, + expected_identity.dynamic_addr)) { + resolver_snapshot_available = 1; + kzt_observe_plt_source(head, guest_link_map, &observed_identity); + head->self_link_map = guest_link_map; + } +#endif head->had_RelocateElfPlt = 1; if(head->pltrel) { int cnt = head->pltsz / head->pltent; @@ -743,32 +987,83 @@ int RelocateElfPlt(lib_t *maplib, lib_t *local_maplib, int bindnow, elfheader_t* // return -1; return 0; } else if(head->pltrel==DT_RELA) { + elf_plt_rela_context_t rela = { + .maplib = maplib, + .local_maplib = local_maplib, + .bindnow = bindnow, + .head = head, + .count = cnt, + .rela = (Elf64_Rela *)(head->jmprel + head->delta), + }; DumpRelATable(head, cnt, (Elf64_Rela *)(head->jmprel + head->delta), "PLT"); printf_log(LOG_INFO, "Applying %d PLT Relocation(s) with Addend for %s\n", cnt, head->name); - if(RelocateElfRELA(maplib, local_maplib, bindnow, head, cnt, (Elf64_Rela *)(head->jmprel + head->delta), &need_resolver)) - //return -1; + if(elf_plt_relocation_apply(RelocateElfPltRELA, &rela, + &need_resolver)) { printf_log(LOG_INFO, "RelocateElfRELA run ERROR!"); + return -1; + } } if(need_resolver) { if(pltResolver==~0LL) { pltResolver = AddBridge(my_context->system, vFE, PltResolver, 0, "PltResolver"); } - if(head->pltgot) { - if(dl_runtime_resolver ==~0LL){ - dl_runtime_resolver = *(uintptr_t*)(head->pltgot+head->delta+16); +#ifdef CONFIG_LATX_KZT + if ((option_kzt || wine_option_kzt) && + !my_context->kzt_lazy_completion_bridge) { + my_context->kzt_lazy_completion_bridge = AddBridge( + my_context->system, vFE, + KztLazyBindingCompleteResolver, 0, + "KztLazyBindingCompleteResolver"); + } +#endif + if(resolver_got_runtime) { +#ifdef CONFIG_LATX_KZT + if (!resolver_snapshot_available) { +#endif + guest_link_map = + *(uintptr_t *)(resolver_got_runtime + 8); + guest_resolver = + *(uintptr_t *)(resolver_got_runtime + 16); +#ifdef CONFIG_LATX_KZT + } +#endif +#ifdef CONFIG_LATX_KZT + if (option_kzt || wine_option_kzt) { + kzt_guest_object_snapshot_t *snapshot = NULL; + kzt_guest_lazy_resolver_t resolver = { + .link_map_slot = resolver_got_runtime + 8, + .resolver_slot = resolver_got_runtime + 16, + .guest_link_map = guest_link_map, + .guest_resolver = guest_resolver, + }; + if (kzt_guest_registry_find_by_link_map( + KztGuestRegistryForContext(my_context), + guest_link_map, &snapshot) == 0 && snapshot && + snapshot->namespace_id.status == KZT_GUEST_FIELD_OK && + snapshot->namespace_id.value == 0) { + (void)kzt_guest_registry_publish_lazy_resolver( + KztGuestRegistryForContext(my_context), + guest_link_map, snapshot->generation, 0, + &resolver); + } + kzt_guest_object_snapshot_free(snapshot); } - *(uintptr_t*)(head->pltgot+head->delta+16) = pltResolver; - head->self_link_map = *(uintptr_t*)(head->pltgot+head->delta+8); - *(uintptr_t*)(head->pltgot+head->delta+8) = (uintptr_t)head; - printf_log(LOG_INFO, "PLT Resolver injected in plt.got at %p\n", (void*)(head->pltgot+head->delta+16)); - } else if(head->got) { +#endif if(dl_runtime_resolver ==~0LL){ dl_runtime_resolver = *(uintptr_t*)(head->got+head->delta+16); } - *(uintptr_t*)(head->got+head->delta+16) = pltResolver; - head->self_link_map = *(uintptr_t*)(head->got+head->delta+8); - *(uintptr_t*)(head->got+head->delta+8) = (uintptr_t)head; - printf_log(LOG_INFO, "PLT Resolver injected in got at %p\n", (void*)(head->got+head->delta+16)); + *(uintptr_t*)(resolver_got_runtime+16) = pltResolver; +#ifdef CONFIG_LATX_KZT + if (!(option_kzt || wine_option_kzt) || + resolver_snapshot_available) { + head->self_link_map = guest_link_map; + } +#else + head->self_link_map = guest_link_map; +#endif + *(uintptr_t*)(resolver_got_runtime+8) = (uintptr_t)head; + printf_log(LOG_INFO, "PLT Resolver injected in got at %p\n", + (void*)(resolver_got_runtime+16)); } } } @@ -1291,6 +1586,114 @@ static void Push64(CPUX86State *cpu, uint64_t v) uintptr_t pltResolver = ~0LL; uintptr_t dl_runtime_resolver = ~0LL; uintptr_t link_map_obj=0; +#ifdef CONFIG_LATX_KZT +static void KztLazyBindingCompleteResolver(void) +{ + CPUX86State *cpu = (CPUX86State *)lsenv->cpu_state; + kzt_lazy_binding_pending_t *pending = + &cpu->kzt_lazy_binding_pending; + uintptr_t original_return = cpu->kzt_lazy_original_return; + uintptr_t slot_addr = pending->slot_addr; + char symbol[KZT_LAZY_BINDING_SYMBOL_MAX]; + kzt_lazy_binding_result_t result; + + (void)slot_addr; + + symbol[0] = '\0'; + if (pending->symbol) { + strncpy(symbol, pending->symbol, sizeof(symbol) - 1); + symbol[sizeof(symbol) - 1] = '\0'; + } + if (pending->armed) { + if (option_kzt_lazy_diagnostics) { + kzt_lazy_binding_pending_t pending_snapshot = *pending; + kzt_lazy_diagnostic_emit_result_t diagnostic_result; + + pending_snapshot.symbol = pending->symbol ? + pending_snapshot.symbol_storage : NULL; + pending_snapshot.version = pending->version ? + pending_snapshot.version_storage : NULL; + (void)kzt_production_lazy_complete( + (void *)pending->context_id, pending, &result); + (void)kzt_lazy_diagnostics_emit_production( + &pending_snapshot, &result, &diagnostic_result); + } else { + (void)kzt_production_lazy_complete( + (void *)pending->context_id, pending, &result); + } + printf_log(LOG_DEBUG, + "KZT: lazy complete status=%d reason=%d slot=%p " + "before=%p after=%p sym=%s\n", + result.status, result.reason, + (void *)slot_addr, + (void *)result.slot_before, (void *)result.slot_after, + symbol[0] ? symbol : "(none)"); + } + kzt_lazy_binding_cancel(pending); + cpu->kzt_lazy_original_return = 0; + Push64(cpu, original_return); +} + +typedef struct kzt_plt_resolver_production_state { + elfheader_t *head; + uintptr_t slot_addr; + uintptr_t unresolved_stub; + const char *symbol; + const char *version; + long addend; +} kzt_plt_resolver_production_state_t; + +static int kzt_plt_resolver_lookup_source( + uintptr_t object_head, kzt_plt_resolver_source_t *source, void *opaque) +{ + kzt_plt_resolver_production_state_t *state = opaque; + kzt_guest_registry_t *registry = KztGuestRegistryForContext(my_context); + kzt_guest_object_snapshot_t *snapshot = NULL; + kzt_guest_lazy_resolver_t resolver; + int result = -1; + + if (!state || !state->head || object_head != (uintptr_t)state->head || + !source || !state->head->self_link_map || + kzt_guest_registry_find_by_link_map( + registry, state->head->self_link_map, &snapshot) != 0 || + !snapshot || snapshot->namespace_id.status != KZT_GUEST_FIELD_OK || + snapshot->namespace_id.value != 0 || + kzt_guest_registry_find_lazy_resolver( + registry, state->head->self_link_map, snapshot->generation, 0, + &resolver) != 0 || + resolver.guest_link_map != state->head->self_link_map) { + goto out; + } + *source = (kzt_plt_resolver_source_t) { + .enabled = my_context->kzt_lazy_completion_bridge != 0, + .context_id = (uintptr_t)my_context, + .object_head = object_head, + .source_link_map = state->head->self_link_map, + .source_generation = snapshot->generation, + .namespace_id = snapshot->namespace_id.value, + .namespace_kind = KZT_GUEST_LIBRARY_NAMESPACE_MAIN, + .slot_addr = state->slot_addr, + .unresolved_stub = state->unresolved_stub, + .symbol = state->symbol, + .version = state->version, + .addend = state->addend, + .guest_resolver = resolver.guest_resolver, + }; + result = 0; +out: + kzt_guest_object_snapshot_free(snapshot); + return result; +} + +static int kzt_plt_resolver_begin_lazy_binding( + const kzt_lazy_binding_begin_request_t *request, + kzt_lazy_binding_pending_t *pending, + kzt_lazy_binding_result_t *result, void *opaque) +{ + (void)opaque; + return kzt_lazy_binding_begin(request, pending, result); +} +#endif void PltResolver(void) { CPUX86State *cpu = (CPUX86State*)lsenv->cpu_state; @@ -1311,15 +1714,61 @@ void PltResolver(void) uint64_t *p = (uint64_t*)(rel->r_offset + h->delta); uintptr_t offs = 0; uintptr_t end = 0; + library_t *resolved_provider = NULL; + + (void)bind; + +#ifdef CONFIG_LATX_KZT + if (option_kzt_lazy_diagnostics) { + printf_kzt_registry_diagnostics( + "kzt_lazy_resolver_entry symbol=%s slot=%p source=%p\n", + symname ? symname : "(none)", (void *)p, + (void *)h->self_link_map); + } + if (option_kzt || wine_option_kzt) { + kzt_plt_resolver_production_state_t state = { + .head = h, + .slot_addr = (uintptr_t)p, + .unresolved_stub = p ? + __atomic_load_n((uintptr_t *)p, __ATOMIC_ACQUIRE) : 0, + .symbol = symname, + .version = vername, + .addend = rel->r_addend, + }; + kzt_plt_resolver_runtime_ops_t ops = { + .lookup_source = kzt_plt_resolver_lookup_source, + .begin_lazy_binding = kzt_plt_resolver_begin_lazy_binding, + .pending = &cpu->kzt_lazy_binding_pending, + .completion_bridge = my_context->kzt_lazy_completion_bridge, + .original_return = &cpu->kzt_lazy_original_return, + .opaque = &state, + }; + kzt_plt_resolver_enter_result_t enter_result; + + if (kzt_plt_resolver_enter(cpu, &ops, &enter_result) == 0 && + enter_result.status != KZT_PLT_RESOLVER_LEGACY_FRAME_RESTORED) { + return; + } + } +#endif + + (void)Pop64(cpu); + (void)Pop64(cpu); library_t* lib = h->lib; lib_t* local_maplib = GetMaplib(lib); - GetGlobalSymbolStartEnd(my_context->maplib, symname, &offs, &end, h, version, vername); + GetGlobalSymbolStartEndWithProvider( + my_context->maplib, symname, &offs, &end, h, version, vername, + &resolved_provider); if(!offs && !end && local_maplib) { - GetGlobalSymbolStartEnd(local_maplib, symname, &offs, &end, h, version, vername); + GetGlobalSymbolStartEndWithProvider( + local_maplib, symname, &offs, &end, h, version, vername, + &resolved_provider); } if(!offs && !end && !version) - GetGlobalSymbolStartEnd(my_context->maplib, symname, &offs, &end, h, -1, NULL); + GetGlobalSymbolStartEndWithProvider( + my_context->maplib, symname, &offs, &end, h, -1, NULL, + &resolved_provider); if (!offs) { // printf_log(LOG_INFO, "Error: PltResolver: Symbol %s(ver %d: %s%s%s) not found, cannot apply R_X86_64_JUMP_SLOT %p (%p) in %s\n", symname, version, symname, vername?"@":"", vername?vername:"", p, *(void**)p, h->name); @@ -1331,6 +1780,38 @@ void PltResolver(void) } else { offs = (uintptr_t)getAlternate((void*)offs); if(p) { + uintptr_t slot_observation = (uintptr_t)(*p); + uintptr_t legacy_target = offs; +#ifdef CONFIG_LATX_KZT + if (option_kzt || wine_option_kzt) { + kzt_jump_slot_route_result_t route_result; + + if (kzt_production_jump_slot_route( + my_context, resolved_provider, legacy_target, h, + 1, slot, rel, p, + slot_observation, 1, + ELF64_R_SYM(rel->r_info), symname, + vername, 0, 0, legacy_target, &route_result) == 0 && + route_result.status == + KZT_JUMP_SLOT_ROUTE_NATIVE_APPLIED) { + printf_kzt_registry_diagnostics( + "KZT lazy route applied slot=%p observed=%p " + "legacy=%p selected=%p symbol=%s\n", + p, (void *)slot_observation, (void*)legacy_target, + (void *)route_result.selected_target, + symname ? symname : "(none)"); + Push64(cpu, route_result.selected_target); + return; + } + if (route_result.status == + KZT_JUMP_SLOT_ROUTE_GUEST_PRESERVED || + route_result.status == + KZT_JUMP_SLOT_ROUTE_CAS_MISMATCH) { + Push64(cpu, route_result.final_value); + return; + } + } +#endif printf_log(LOG_INFO, " Apply %s R_X86_64_JUMP_SLOT %p with sym=%s(ver %d: %s%s%s) (%p -> %p / %s)\n", (bind==STB_LOCAL)?"Local":"Global", p, symname, version, symname, vername?"@":"", vername?vername:"",*(void**)p, (void*)offs, ElfName(FindElfAddress(my_context, offs))); *p = offs; } else { diff --git a/target/i386/latx/context/elfmap.c b/target/i386/latx/context/elfmap.c new file mode 100644 index 00000000000..88ad385ae4c --- /dev/null +++ b/target/i386/latx/context/elfmap.c @@ -0,0 +1,110 @@ +#include + +#include "elfmap.h" + +static int AddToAddress(uintptr_t base, uint64_t offset, uintptr_t *result) +{ + if (offset > UINTPTR_MAX || base > UINTPTR_MAX - (uintptr_t)offset) { + return -1; + } + + *result = base + (uintptr_t)offset; + return 0; +} + +int GetElfLoadRange(const Elf64_Phdr *program_headers, + size_t program_header_count, + uintptr_t load_bias, + uintptr_t page_size, + uintptr_t *map_start, + uintptr_t *map_end) +{ + uintptr_t first = UINTPTR_MAX; + uintptr_t last = 0; + uintptr_t page_mask; + int found_load_segment = 0; + + if (!program_headers || !program_header_count || !map_start || !map_end || + map_start == map_end || !page_size || + (page_size & (page_size - 1)) != 0) { + return -1; + } + + page_mask = page_size - 1; + for (size_t i = 0; i < program_header_count; ++i) { + const Elf64_Phdr *header = &program_headers[i]; + uint64_t segment_end; + uintptr_t aligned_start; + uintptr_t aligned_end; + uintptr_t runtime_start; + uintptr_t runtime_end; + + if (header->p_type != PT_LOAD || header->p_memsz == 0) { + continue; + } + + if (header->p_vaddr > UINT64_MAX - header->p_memsz) { + return -1; + } + segment_end = header->p_vaddr + header->p_memsz; + + if (header->p_vaddr > UINTPTR_MAX || segment_end > UINTPTR_MAX || + segment_end > UINTPTR_MAX - page_mask) { + return -1; + } + + aligned_start = (uintptr_t)header->p_vaddr & ~page_mask; + aligned_end = ((uintptr_t)segment_end + page_mask) & ~page_mask; + if (AddToAddress(load_bias, aligned_start, &runtime_start) != 0 || + AddToAddress(load_bias, aligned_end, &runtime_end) != 0) { + return -1; + } + + if (runtime_start < first) { + first = runtime_start; + } + if (runtime_end > last) { + last = runtime_end; + } + found_load_segment = 1; + } + + if (!found_load_segment || first >= last) { + return -1; + } + + *map_start = first; + *map_end = last; + return 0; +} + +int GetElfDynamicAddress(const Elf64_Phdr *program_headers, + size_t program_header_count, + uintptr_t load_bias, + uintptr_t *dynamic_addr) +{ + uintptr_t found = 0; + + if (!program_headers || !program_header_count || !dynamic_addr) { + return -1; + } + + for (size_t i = 0; i < program_header_count; ++i) { + const Elf64_Phdr *header = &program_headers[i]; + + if (header->p_type != PT_DYNAMIC) { + continue; + } + if (found || header->p_vaddr > UINTPTR_MAX || + AddToAddress(load_bias, header->p_vaddr, &found) != 0 || + !found) { + return -1; + } + } + + if (!found) { + return -1; + } + *dynamic_addr = found; + return 0; +} diff --git a/target/i386/latx/context/kzt_bridge_exact.c b/target/i386/latx/context/kzt_bridge_exact.c new file mode 100644 index 00000000000..769d10ae8b8 --- /dev/null +++ b/target/i386/latx/context/kzt_bridge_exact.c @@ -0,0 +1,16 @@ +#include "kzt_bridge_exact.h" + +#include "bridge_private.h" + +int kzt_bridge_is_exact(uintptr_t target, kzt_bridge_wrapper_t wrapper, + void *native_symbol) +{ + onebridge_t *entry = (onebridge_t *)target; + + if (!entry || entry->CC != 0xCC || entry->S != 'S' || + entry->C != 'C' || (entry->C3 != 0xC3 && entry->C3 != 0xC2) || + entry->w != wrapper || entry->f != (uintptr_t)native_symbol) { + return 0; + } + return 1; +} diff --git a/target/i386/latx/context/kzt_guest_dynamic.c b/target/i386/latx/context/kzt_guest_dynamic.c new file mode 100644 index 00000000000..70422ddc1ad --- /dev/null +++ b/target/i386/latx/context/kzt_guest_dynamic.c @@ -0,0 +1,265 @@ +#include "kzt_guest_dynamic.h" + +#include + +static void kzt_guest_dynamic_field_set( + kzt_guest_dynamic_field_t *field, + uint64_t value, + kzt_guest_dynamic_address_semantics_t semantics) +{ + field->present = 1; + field->value = value; + field->address_semantics = semantics; +} + +static int kzt_guest_dynamic_read_entry( + uintptr_t dynamic_addr, + size_t index, + const kzt_guest_link_map_reader_ops_t *reader_ops, + Elf64_Dyn *entry, + uintptr_t *entry_addr) +{ + uintptr_t offset; + + if (index > UINTPTR_MAX / sizeof(*entry)) { + return -1; + } + + offset = index * sizeof(*entry); + if (dynamic_addr > UINTPTR_MAX - offset) { + return -1; + } + + *entry_addr = dynamic_addr + offset; + return reader_ops->read_memory(*entry_addr, entry, sizeof(*entry), + reader_ops->opaque) == 0 ? 0 : -1; +} + +static int kzt_guest_dynamic_add_needed(kzt_guest_dynamic_view_t *view, + uint64_t offset) +{ + size_t new_count = view->needed_count + 1; + + if (new_count < view->needed_count || + new_count > KZT_GUEST_DYNAMIC_NEEDED_LIMIT) { + return -1; + } + + view->needed_offsets[view->needed_count] = offset; + view->needed_count = new_count; + view->needed_address_semantics = KZT_GUEST_DYNAMIC_STRING_TABLE_OFFSET; + return 0; +} + +static void kzt_guest_dynamic_record_unknown_tag( + kzt_guest_dynamic_view_t *view, + int64_t tag, + size_t index) +{ + if (view->unknown_tag_count == 0) { + view->first_unknown_tag = tag; + view->first_unknown_tag_index = index; + } + ++view->unknown_tag_count; +} + +static int kzt_guest_dynamic_record_entry(kzt_guest_dynamic_view_t *view, + const Elf64_Dyn *entry, + size_t index) +{ + uint64_t value = entry->d_un.d_val; + uint64_t ptr = entry->d_un.d_ptr; + + switch (entry->d_tag) { + case DT_NEEDED: + return kzt_guest_dynamic_add_needed(view, value); + case DT_SYMTAB: + kzt_guest_dynamic_field_set(&view->symtab, ptr, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS); + break; + case DT_STRTAB: + kzt_guest_dynamic_field_set(&view->strtab, ptr, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS); + break; + case DT_SYMENT: + kzt_guest_dynamic_field_set(&view->syment, value, + KZT_GUEST_DYNAMIC_SCALAR); + break; + case DT_STRSZ: + kzt_guest_dynamic_field_set(&view->strsz, value, + KZT_GUEST_DYNAMIC_SCALAR); + break; + case DT_HASH: + kzt_guest_dynamic_field_set(&view->hash, ptr, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS); + break; + case DT_GNU_HASH: + kzt_guest_dynamic_field_set(&view->gnu_hash, ptr, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS); + break; + case DT_VERSYM: + kzt_guest_dynamic_field_set(&view->versym, ptr, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS); + break; + case DT_VERNEED: + kzt_guest_dynamic_field_set(&view->verneed, ptr, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS); + break; + case DT_VERNEEDNUM: + kzt_guest_dynamic_field_set(&view->verneednum, value, + KZT_GUEST_DYNAMIC_SCALAR); + break; + case DT_VERDEF: + kzt_guest_dynamic_field_set(&view->verdef, ptr, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS); + break; + case DT_VERDEFNUM: + kzt_guest_dynamic_field_set(&view->verdefnum, value, + KZT_GUEST_DYNAMIC_SCALAR); + break; + case DT_RELA: + kzt_guest_dynamic_field_set(&view->rela, ptr, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS); + break; + case DT_RELASZ: + kzt_guest_dynamic_field_set(&view->relasz, value, + KZT_GUEST_DYNAMIC_SCALAR); + break; + case DT_RELAENT: + kzt_guest_dynamic_field_set(&view->relaent, value, + KZT_GUEST_DYNAMIC_SCALAR); + break; + case DT_REL: + kzt_guest_dynamic_field_set(&view->rel, ptr, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS); + break; + case DT_RELSZ: + kzt_guest_dynamic_field_set(&view->relsz, value, + KZT_GUEST_DYNAMIC_SCALAR); + break; + case DT_RELENT: + kzt_guest_dynamic_field_set(&view->relent, value, + KZT_GUEST_DYNAMIC_SCALAR); + break; + case DT_JMPREL: + kzt_guest_dynamic_field_set(&view->jmprel, ptr, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS); + break; + case DT_PLTRELSZ: + kzt_guest_dynamic_field_set(&view->pltrelsz, value, + KZT_GUEST_DYNAMIC_SCALAR); + break; + case DT_PLTREL: + kzt_guest_dynamic_field_set(&view->pltrel, value, + KZT_GUEST_DYNAMIC_SCALAR); + break; + case DT_PLTGOT: + kzt_guest_dynamic_field_set(&view->pltgot, ptr, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS); + break; + default: + kzt_guest_dynamic_record_unknown_tag(view, entry->d_tag, index); + break; + } + + return 0; +} + +static void kzt_guest_dynamic_publish_view( + kzt_guest_dynamic_parse_result_t *result, + const kzt_guest_dynamic_view_t *view) +{ + result->status = view->status; + result->entry_count = view->entry_count; + result->scan_limit = view->scan_limit; + result->unknown_tag_count = view->unknown_tag_count; + result->first_unknown_tag = view->first_unknown_tag; + result->first_unknown_tag_index = view->first_unknown_tag_index; + result->view = *view; +} + +int kzt_guest_dynamic_parse( + uintptr_t dynamic_addr, + uintptr_t load_bias, + const kzt_guest_link_map_reader_ops_t *reader_ops, + kzt_guest_dynamic_parse_result_t *result) +{ + kzt_guest_dynamic_view_t view; + size_t i; + + if (!result) { + return -1; + } + + memset(result, 0, sizeof(*result)); + result->status = KZT_GUEST_DYNAMIC_ERROR; + result->error = KZT_GUEST_DYNAMIC_ERROR_INVALID_ARGUMENT; + + if (!dynamic_addr || !reader_ops || !reader_ops->read_memory) { + return -1; + } + + memset(&view, 0, sizeof(view)); + view.dynamic_addr = dynamic_addr; + view.load_bias = load_bias; + view.scan_limit = KZT_GUEST_DYNAMIC_SCAN_LIMIT; + + for (i = 0; i < KZT_GUEST_DYNAMIC_SCAN_LIMIT; ++i) { + Elf64_Dyn entry; + uintptr_t entry_addr = 0; + + if (kzt_guest_dynamic_read_entry(dynamic_addr, i, reader_ops, + &entry, &entry_addr) != 0) { + view.status = KZT_GUEST_DYNAMIC_READ_ERROR; + view.entry_count = i; + result->read_error_addr = entry_addr; + result->error = KZT_GUEST_DYNAMIC_ERROR_READ_FAILURE; + kzt_guest_dynamic_publish_view(result, &view); + return 0; + } + + if (entry.d_tag == DT_NULL) { + view.status = KZT_GUEST_DYNAMIC_COMPLETE; + view.entry_count = i; + view.has_null = 1; + result->error = KZT_GUEST_DYNAMIC_ERROR_NONE; + kzt_guest_dynamic_publish_view(result, &view); + return 0; + } + + if (kzt_guest_dynamic_record_entry(&view, &entry, i) != 0) { + view.status = KZT_GUEST_DYNAMIC_ERROR; + view.entry_count = i; + result->status = KZT_GUEST_DYNAMIC_ERROR; + result->error = KZT_GUEST_DYNAMIC_ERROR_TOO_MANY_NEEDED; + kzt_guest_dynamic_publish_view(result, &view); + return 0; + } + } + + view.status = KZT_GUEST_DYNAMIC_TRUNCATED_NO_NULL; + view.entry_count = KZT_GUEST_DYNAMIC_SCAN_LIMIT; + result->error = KZT_GUEST_DYNAMIC_ERROR_SCAN_LIMIT_EXCEEDED; + kzt_guest_dynamic_publish_view(result, &view); + return 0; +} + +void kzt_guest_dynamic_view_destroy(kzt_guest_dynamic_view_t *view) +{ + if (!view) { + return; + } + + memset(view, 0, sizeof(*view)); +} + +void kzt_guest_dynamic_parse_result_clear( + kzt_guest_dynamic_parse_result_t *result) +{ + if (!result) { + return; + } + + kzt_guest_dynamic_view_destroy(&result->view); + memset(result, 0, sizeof(*result)); +} diff --git a/target/i386/latx/context/kzt_guest_dynamic_diagnostics.c b/target/i386/latx/context/kzt_guest_dynamic_diagnostics.c new file mode 100644 index 00000000000..b2db896b980 --- /dev/null +++ b/target/i386/latx/context/kzt_guest_dynamic_diagnostics.c @@ -0,0 +1,499 @@ +#include "kzt_guest_dynamic_diagnostics.h" + +#include +#include +#include + +typedef struct kzt_guest_dynamic_field_spec { + const char *name; + size_t offset; +} kzt_guest_dynamic_field_spec_t; + +static int kzt_guest_dynamic_status_is_blocking( + kzt_guest_dynamic_status_t status) +{ + return status == KZT_GUEST_DYNAMIC_TRUNCATED_NO_NULL || + status == KZT_GUEST_DYNAMIC_READ_ERROR || + status == KZT_GUEST_DYNAMIC_ERROR; +} + +static kzt_guest_dynamic_diagnostic_match_t kzt_guest_dynamic_compare_size( + size_t old_value, + size_t new_value) +{ + return old_value == new_value ? KZT_GUEST_DYNAMIC_DIAGNOSTIC_MATCHED : + KZT_GUEST_DYNAMIC_DIAGNOSTIC_MISMATCH; +} + +static kzt_guest_dynamic_diagnostic_match_t kzt_guest_dynamic_compare_status( + kzt_guest_dynamic_status_t old_status, + kzt_guest_dynamic_status_t new_status) +{ + return old_status == new_status ? KZT_GUEST_DYNAMIC_DIAGNOSTIC_MATCHED : + KZT_GUEST_DYNAMIC_DIAGNOSTIC_MISMATCH; +} + +static void kzt_guest_dynamic_count_field_match( + kzt_guest_dynamic_diagnostic_report_t *report, + kzt_guest_dynamic_diagnostic_match_t match) +{ + switch (match) { + case KZT_GUEST_DYNAMIC_DIAGNOSTIC_MATCHED: + ++report->matched_count; + break; + case KZT_GUEST_DYNAMIC_DIAGNOSTIC_MISSING_OLD: + ++report->missing_old_count; + break; + case KZT_GUEST_DYNAMIC_DIAGNOSTIC_MISSING_NEW: + ++report->missing_new_count; + break; + case KZT_GUEST_DYNAMIC_DIAGNOSTIC_MISMATCH: + ++report->mismatch_count; + break; + } +} + +static void kzt_guest_dynamic_add_field_report( + kzt_guest_dynamic_diagnostic_report_t *report, + const kzt_guest_dynamic_diagnostic_field_t *field) +{ + if (report->field_count >= KZT_GUEST_DYNAMIC_DIAGNOSTIC_FIELD_LIMIT) { + return; + } + + report->fields[report->field_count++] = *field; + kzt_guest_dynamic_count_field_match(report, field->match); +} + +static kzt_guest_dynamic_diagnostic_match_t kzt_guest_dynamic_compare_field( + const kzt_guest_dynamic_field_t *old_field, + const kzt_guest_dynamic_field_t *new_field) +{ + if (!old_field->present && !new_field->present) { + return KZT_GUEST_DYNAMIC_DIAGNOSTIC_MATCHED; + } + if (!old_field->present) { + return KZT_GUEST_DYNAMIC_DIAGNOSTIC_MISSING_OLD; + } + if (!new_field->present) { + return KZT_GUEST_DYNAMIC_DIAGNOSTIC_MISSING_NEW; + } + if (old_field->value != new_field->value || + old_field->address_semantics != new_field->address_semantics) { + return KZT_GUEST_DYNAMIC_DIAGNOSTIC_MISMATCH; + } + + return KZT_GUEST_DYNAMIC_DIAGNOSTIC_MATCHED; +} + +static const kzt_guest_dynamic_field_t *kzt_guest_dynamic_field_at( + const kzt_guest_dynamic_view_t *view, + const kzt_guest_dynamic_field_spec_t *spec) +{ + return (const kzt_guest_dynamic_field_t *)((const char *)view + + spec->offset); +} + +static void kzt_guest_dynamic_compare_named_field( + kzt_guest_dynamic_diagnostic_report_t *report, + const kzt_guest_dynamic_view_t *old_view, + const kzt_guest_dynamic_view_t *new_view, + const kzt_guest_dynamic_field_spec_t *spec) +{ + const kzt_guest_dynamic_field_t *old_field = + kzt_guest_dynamic_field_at(old_view, spec); + const kzt_guest_dynamic_field_t *new_field = + kzt_guest_dynamic_field_at(new_view, spec); + kzt_guest_dynamic_diagnostic_field_t field = { + .name = spec->name, + .match = kzt_guest_dynamic_compare_field(old_field, new_field), + .old_present = old_field->present, + .old_value = old_field->value, + .old_address_semantics = old_field->address_semantics, + .new_present = new_field->present, + .new_value = new_field->value, + .new_address_semantics = new_field->address_semantics, + }; + + kzt_guest_dynamic_add_field_report(report, &field); +} + +static int kzt_guest_dynamic_needed_offsets_equal( + const kzt_guest_dynamic_view_t *old_view, + const kzt_guest_dynamic_view_t *new_view) +{ + size_t i; + + if (old_view->needed_count != new_view->needed_count || + old_view->needed_address_semantics != + new_view->needed_address_semantics) { + return 0; + } + + for (i = 0; i < old_view->needed_count; ++i) { + if (old_view->needed_offsets[i] != new_view->needed_offsets[i]) { + return 0; + } + } + + return 1; +} + +static void kzt_guest_dynamic_compare_needed_offsets( + kzt_guest_dynamic_diagnostic_report_t *report, + const kzt_guest_dynamic_view_t *old_view, + const kzt_guest_dynamic_view_t *new_view) +{ + int old_present = old_view->needed_count > 0; + int new_present = new_view->needed_count > 0; + kzt_guest_dynamic_diagnostic_field_t field = { + .name = "needed_offsets", + .old_present = old_present, + .old_value = old_present ? old_view->needed_offsets[0] : 0, + .old_address_semantics = old_view->needed_address_semantics, + .old_count = old_view->needed_count, + .new_present = new_present, + .new_value = new_present ? new_view->needed_offsets[0] : 0, + .new_address_semantics = new_view->needed_address_semantics, + .new_count = new_view->needed_count, + }; + + if (!old_present && !new_present) { + field.match = KZT_GUEST_DYNAMIC_DIAGNOSTIC_MATCHED; + } else if (!old_present) { + field.match = KZT_GUEST_DYNAMIC_DIAGNOSTIC_MISSING_OLD; + } else if (!new_present) { + field.match = KZT_GUEST_DYNAMIC_DIAGNOSTIC_MISSING_NEW; + } else if (!kzt_guest_dynamic_needed_offsets_equal(old_view, new_view)) { + field.match = KZT_GUEST_DYNAMIC_DIAGNOSTIC_MISMATCH; + } else { + field.match = KZT_GUEST_DYNAMIC_DIAGNOSTIC_MATCHED; + } + + kzt_guest_dynamic_add_field_report(report, &field); +} + +static kzt_guest_dynamic_diagnostic_match_t kzt_guest_dynamic_compare_unknown( + const kzt_guest_dynamic_parse_result_t *old_result, + const kzt_guest_dynamic_parse_result_t *new_result) +{ + if (old_result->unknown_tag_count != new_result->unknown_tag_count) { + return KZT_GUEST_DYNAMIC_DIAGNOSTIC_MISMATCH; + } + + if (old_result->unknown_tag_count == 0) { + return KZT_GUEST_DYNAMIC_DIAGNOSTIC_MATCHED; + } + + return old_result->first_unknown_tag == new_result->first_unknown_tag && + old_result->first_unknown_tag_index == + new_result->first_unknown_tag_index ? + KZT_GUEST_DYNAMIC_DIAGNOSTIC_MATCHED : + KZT_GUEST_DYNAMIC_DIAGNOSTIC_MISMATCH; +} + +static size_t kzt_guest_dynamic_count_summary_differences( + const kzt_guest_dynamic_diagnostic_report_t *report) +{ + size_t count = report->missing_old_count + report->missing_new_count + + report->mismatch_count; + + if (report->status_match != KZT_GUEST_DYNAMIC_DIAGNOSTIC_MATCHED) { + ++count; + } + if (report->entry_count_match != KZT_GUEST_DYNAMIC_DIAGNOSTIC_MATCHED) { + ++count; + } + if (report->unknown_tags_match != KZT_GUEST_DYNAMIC_DIAGNOSTIC_MATCHED) { + ++count; + } + + return count; +} + +static void kzt_guest_dynamic_summary_set_field( + kzt_guest_dynamic_diagnostic_summary_t *summary, + const kzt_guest_dynamic_diagnostic_field_t *field) +{ + summary->first_difference_kind = + KZT_GUEST_DYNAMIC_DIAGNOSTIC_DIFFERENCE_FIELD; + summary->first_difference_name = field->name; + summary->first_difference_match = field->match; + summary->first_old_present = field->old_present; + summary->first_new_present = field->new_present; + summary->first_old_value = field->old_value; + summary->first_new_value = field->new_value; + summary->first_old_count = field->old_count; + summary->first_new_count = field->new_count; +} + +static void kzt_guest_dynamic_summary_set_status( + kzt_guest_dynamic_diagnostic_summary_t *summary, + const kzt_guest_dynamic_diagnostic_report_t *report) +{ + summary->first_difference_kind = + KZT_GUEST_DYNAMIC_DIAGNOSTIC_DIFFERENCE_STATUS; + summary->first_difference_name = "status"; + summary->first_difference_match = report->status_match; + summary->first_old_present = 1; + summary->first_new_present = 1; + summary->first_old_value = report->old_status; + summary->first_new_value = report->new_status; +} + +static void kzt_guest_dynamic_summary_set_entry_count( + kzt_guest_dynamic_diagnostic_summary_t *summary, + const kzt_guest_dynamic_diagnostic_report_t *report) +{ + summary->first_difference_kind = + KZT_GUEST_DYNAMIC_DIAGNOSTIC_DIFFERENCE_ENTRY_COUNT; + summary->first_difference_name = "entry_count"; + summary->first_difference_match = report->entry_count_match; + summary->first_old_present = 1; + summary->first_new_present = 1; + summary->first_old_count = report->old_entry_count; + summary->first_new_count = report->new_entry_count; +} + +static void kzt_guest_dynamic_summary_set_unknown_tags( + kzt_guest_dynamic_diagnostic_summary_t *summary, + const kzt_guest_dynamic_diagnostic_report_t *report) +{ + summary->first_difference_kind = + KZT_GUEST_DYNAMIC_DIAGNOSTIC_DIFFERENCE_UNKNOWN_TAGS; + summary->first_difference_name = "unknown_tags"; + summary->first_difference_match = report->unknown_tags_match; + summary->first_old_present = report->old_unknown_tag_count > 0; + summary->first_new_present = report->new_unknown_tag_count > 0; + summary->first_old_count = report->old_unknown_tag_count; + summary->first_new_count = report->new_unknown_tag_count; + summary->first_old_tag = report->old_first_unknown_tag; + summary->first_new_tag = report->new_first_unknown_tag; + summary->first_old_tag_index = report->old_first_unknown_tag_index; + summary->first_new_tag_index = report->new_first_unknown_tag_index; +} + +int kzt_guest_dynamic_diagnostics_summarize( + const kzt_guest_dynamic_diagnostic_report_t *report, + uintptr_t link_map_addr, + unsigned long generation, + kzt_guest_dynamic_diagnostic_summary_t *summary) +{ + size_t i; + + if (!report || !summary) { + return -1; + } + + memset(summary, 0, sizeof(*summary)); + summary->link_map_addr = link_map_addr; + summary->generation = generation; + summary->matched = report->difference_count == 0; + summary->blocking = report->blocking_count > 0; + summary->difference_count = report->difference_count; + summary->blocking_count = report->blocking_count; + summary->old_status = report->old_status; + summary->new_status = report->new_status; + summary->old_entry_count = report->old_entry_count; + summary->new_entry_count = report->new_entry_count; + summary->old_unknown_tag_count = report->old_unknown_tag_count; + summary->new_unknown_tag_count = report->new_unknown_tag_count; + summary->old_first_unknown_tag = report->old_first_unknown_tag; + summary->new_first_unknown_tag = report->new_first_unknown_tag; + summary->old_first_unknown_tag_index = + report->old_first_unknown_tag_index; + summary->new_first_unknown_tag_index = + report->new_first_unknown_tag_index; + summary->first_difference_kind = + KZT_GUEST_DYNAMIC_DIAGNOSTIC_DIFFERENCE_NONE; + summary->first_difference_name = "none"; + summary->first_difference_match = + KZT_GUEST_DYNAMIC_DIAGNOSTIC_MATCHED; + + if (report->status_match != KZT_GUEST_DYNAMIC_DIAGNOSTIC_MATCHED || + report->blocking_count > 0) { + kzt_guest_dynamic_summary_set_status(summary, report); + return 0; + } + + if (report->entry_count_match != KZT_GUEST_DYNAMIC_DIAGNOSTIC_MATCHED) { + kzt_guest_dynamic_summary_set_entry_count(summary, report); + return 0; + } + + if (report->unknown_tags_match != + KZT_GUEST_DYNAMIC_DIAGNOSTIC_MATCHED) { + kzt_guest_dynamic_summary_set_unknown_tags(summary, report); + return 0; + } + + for (i = 0; i < report->field_count; ++i) { + if (report->fields[i].match != + KZT_GUEST_DYNAMIC_DIAGNOSTIC_MATCHED) { + kzt_guest_dynamic_summary_set_field(summary, + &report->fields[i]); + return 0; + } + } + + return 0; +} + +int kzt_guest_dynamic_diagnostics_format_summary( + const kzt_guest_dynamic_diagnostic_summary_t *summary, + char *buffer, + size_t buffer_size) +{ + int written; + const char *first_name; + + if (!summary || !buffer || buffer_size == 0) { + return -1; + } + + first_name = summary->first_difference_name ? + summary->first_difference_name : "none"; + written = snprintf( + buffer, buffer_size, + "kzt_guest_dynamic_compare link_map=0x%lx generation=%lu " + "matched=%d blocking=%d differences=%lu blocking_count=%lu " + "first=%s kind=%d match=%d old_present=%d new_present=%d " + "old_value=0x%llx new_value=0x%llx old_count=%lu new_count=%lu " + "old_tag=%lld new_tag=%lld old_tag_index=%lu new_tag_index=%lu " + "old_status=%d new_status=%d old_entries=%lu new_entries=%lu " + "old_unknown_tags=%lu new_unknown_tags=%lu", + (unsigned long)summary->link_map_addr, summary->generation, + summary->matched, summary->blocking, + (unsigned long)summary->difference_count, + (unsigned long)summary->blocking_count, first_name, + summary->first_difference_kind, summary->first_difference_match, + summary->first_old_present, summary->first_new_present, + (unsigned long long)summary->first_old_value, + (unsigned long long)summary->first_new_value, + (unsigned long)summary->first_old_count, + (unsigned long)summary->first_new_count, + (long long)summary->first_old_tag, + (long long)summary->first_new_tag, + (unsigned long)summary->first_old_tag_index, + (unsigned long)summary->first_new_tag_index, + summary->old_status, summary->new_status, + (unsigned long)summary->old_entry_count, + (unsigned long)summary->new_entry_count, + (unsigned long)summary->old_unknown_tag_count, + (unsigned long)summary->new_unknown_tag_count); + + if (written < 0 || (size_t)written >= buffer_size) { + return -1; + } + + return 0; +} + +int kzt_guest_dynamic_diagnostics_compare( + const kzt_guest_dynamic_parse_result_t *old_result, + const kzt_guest_dynamic_parse_result_t *new_result, + kzt_guest_dynamic_diagnostic_report_t *report) +{ + const kzt_guest_dynamic_view_t *old_view; + const kzt_guest_dynamic_view_t *new_view; + const kzt_guest_dynamic_field_spec_t fields[] = { +#define KZT_DYNAMIC_FIELD(name) \ + { #name, offsetof(kzt_guest_dynamic_view_t, name) } + KZT_DYNAMIC_FIELD(symtab), + KZT_DYNAMIC_FIELD(strtab), + KZT_DYNAMIC_FIELD(syment), + KZT_DYNAMIC_FIELD(strsz), + KZT_DYNAMIC_FIELD(hash), + KZT_DYNAMIC_FIELD(gnu_hash), + KZT_DYNAMIC_FIELD(versym), + KZT_DYNAMIC_FIELD(verneed), + KZT_DYNAMIC_FIELD(verneednum), + KZT_DYNAMIC_FIELD(verdef), + KZT_DYNAMIC_FIELD(verdefnum), + KZT_DYNAMIC_FIELD(rela), + KZT_DYNAMIC_FIELD(relasz), + KZT_DYNAMIC_FIELD(relaent), + KZT_DYNAMIC_FIELD(rel), + KZT_DYNAMIC_FIELD(relsz), + KZT_DYNAMIC_FIELD(relent), + KZT_DYNAMIC_FIELD(jmprel), + KZT_DYNAMIC_FIELD(pltrelsz), + KZT_DYNAMIC_FIELD(pltrel), + KZT_DYNAMIC_FIELD(pltgot), +#undef KZT_DYNAMIC_FIELD + }; + size_t i; + + if (!old_result || !new_result || !report) { + return -1; + } + + memset(report, 0, sizeof(*report)); + + old_view = &old_result->view; + new_view = &new_result->view; + + report->old_status = old_result->status; + report->new_status = new_result->status; + report->old_error = old_result->error; + report->new_error = new_result->error; + report->old_read_error_addr = old_result->read_error_addr; + report->new_read_error_addr = new_result->read_error_addr; + report->old_entry_count = old_result->entry_count; + report->new_entry_count = new_result->entry_count; + report->old_unknown_tag_count = old_result->unknown_tag_count; + report->new_unknown_tag_count = new_result->unknown_tag_count; + report->old_first_unknown_tag = old_result->first_unknown_tag; + report->new_first_unknown_tag = new_result->first_unknown_tag; + report->old_first_unknown_tag_index = old_result->first_unknown_tag_index; + report->new_first_unknown_tag_index = new_result->first_unknown_tag_index; + report->old_truncated = old_result->status == + KZT_GUEST_DYNAMIC_TRUNCATED_NO_NULL; + report->new_truncated = new_result->status == + KZT_GUEST_DYNAMIC_TRUNCATED_NO_NULL; + report->old_read_error = old_result->status == KZT_GUEST_DYNAMIC_READ_ERROR; + report->new_read_error = new_result->status == KZT_GUEST_DYNAMIC_READ_ERROR; + report->status_match = kzt_guest_dynamic_compare_status(old_result->status, + new_result->status); + report->entry_count_match = kzt_guest_dynamic_compare_size( + old_result->entry_count, new_result->entry_count); + report->unknown_tags_match = kzt_guest_dynamic_compare_unknown(old_result, + new_result); + + if (kzt_guest_dynamic_status_is_blocking(old_result->status)) { + ++report->blocking_count; + } + if (kzt_guest_dynamic_status_is_blocking(new_result->status)) { + ++report->blocking_count; + } + + for (i = 0; i < sizeof(fields) / sizeof(fields[0]); ++i) { + kzt_guest_dynamic_compare_named_field(report, old_view, new_view, + &fields[i]); + } + kzt_guest_dynamic_compare_needed_offsets(report, old_view, new_view); + report->difference_count = kzt_guest_dynamic_count_summary_differences( + report); + + return 0; +} + +const kzt_guest_dynamic_diagnostic_field_t * +kzt_guest_dynamic_diagnostic_find_field( + const kzt_guest_dynamic_diagnostic_report_t *report, + const char *name) +{ + size_t i; + + if (!report || !name) { + return NULL; + } + + for (i = 0; i < report->field_count; ++i) { + if (!strcmp(report->fields[i].name, name)) { + return &report->fields[i]; + } + } + + return NULL; +} diff --git a/target/i386/latx/context/kzt_guest_library_adapter.c b/target/i386/latx/context/kzt_guest_library_adapter.c new file mode 100644 index 00000000000..328e6f420fa --- /dev/null +++ b/target/i386/latx/context/kzt_guest_library_adapter.c @@ -0,0 +1,75 @@ +#include "kzt_guest_library_adapter.h" + +#include "box64context.h" +#include "kzt_guest_library_binding.h" +#include "library.h" +#include "library_private.h" + +static kzt_guest_library_object_type_t loader_object_type(library_t *library) +{ + return library && library->type == LIB_WRAPPED + ? KZT_GUEST_LIBRARY_OBJECT_WRAPPED + : library && library->type == LIB_EMULATED + ? KZT_GUEST_LIBRARY_OBJECT_EMULATED + : KZT_GUEST_LIBRARY_OBJECT_UNSUPPORTED; +} + +void kzt_guest_library_note_loader_pair(box64context_t *context, + uintptr_t link_map_addr, + library_t *library) +{ +#ifdef CONFIG_LATX_KZT + kzt_guest_library_object_type_t type; + if (!context || !link_map_addr || !library) return; + type = loader_object_type(library); + (void)kzt_guest_library_publish_loader_pair( + KztGuestLibraryBindingsForContext(context), link_map_addr, library, + type); +#else + (void)context; + (void)link_map_addr; + (void)library; +#endif +} + +void kzt_guest_library_note_loader_pair_pending( + box64context_t *context, + const kzt_guest_library_loader_scope_t *scope, + uintptr_t link_map_addr, library_t *library) +{ +#ifdef CONFIG_LATX_KZT + if (!context || !scope || !link_map_addr || !library) return; + (void)kzt_guest_library_loader_scope_note_pair( + scope, link_map_addr, library, loader_object_type(library)); +#else + (void)context; (void)scope; (void)link_map_addr; (void)library; +#endif +} + +void kzt_guest_library_publish_loader_pair_scoped( + box64context_t *context, + const kzt_guest_library_loader_scope_t *scope, + uintptr_t link_map_addr, library_t *library) +{ +#ifdef CONFIG_LATX_KZT + if (!context || !scope || !link_map_addr || !library) return; + (void)kzt_guest_library_loader_scope_publish_pair( + scope, link_map_addr, library, loader_object_type(library)); +#else + (void)context; (void)scope; (void)link_map_addr; (void)library; +#endif +} + +void kzt_guest_library_publish_loader_observed_scoped( + box64context_t *context, + const kzt_guest_library_loader_scope_t *scope, + uintptr_t link_map_addr) +{ +#ifdef CONFIG_LATX_KZT + if (!context || !scope || !link_map_addr) return; + (void)kzt_guest_library_loader_scope_publish_observed( + scope, link_map_addr); +#else + (void)context; (void)scope; (void)link_map_addr; +#endif +} diff --git a/target/i386/latx/context/kzt_guest_library_binding.c b/target/i386/latx/context/kzt_guest_library_binding.c new file mode 100644 index 00000000000..41d25dd73d4 --- /dev/null +++ b/target/i386/latx/context/kzt_guest_library_binding.c @@ -0,0 +1,1598 @@ +#include "kzt_guest_library_binding.h" + +#include +#include +#include +#include +#include + +#include "kzt_guest_registry.h" + +typedef struct kzt_guest_library_binding_entry { + kzt_guest_library_binding_key_t key; + library_t *library; + kzt_guest_library_object_type_t object_type; + kzt_guest_library_binding_state_t state; + unsigned int references; + int retire_started; +} kzt_guest_library_binding_entry_t; + +typedef struct kzt_guest_library_lifecycle { + library_t *library; + kzt_guest_library_binding_state_t state; + int destroy_started; + uintptr_t fallback_closed_addr; + unsigned long fallback_closed_epoch; +} kzt_guest_library_lifecycle_t; + +typedef struct kzt_guest_library_callback_gate { + struct kzt_guest_library_callback_gate *next; + uintptr_t link_map_addr; + library_t *closed_by; + unsigned long state; +} kzt_guest_library_callback_gate_t; + +#define KZT_CALLBACK_GATE_CLOSED \ + (1UL << (sizeof(unsigned long) * 8 - 1)) +#define KZT_CALLBACK_GATE_READERS (KZT_CALLBACK_GATE_CLOSED - 1) + +typedef struct kzt_guest_library_pending { + uintptr_t link_map_addr; + library_t *library; + kzt_guest_library_object_type_t object_type; + int active; +} kzt_guest_library_pending_t; + +typedef struct kzt_guest_library_observed { + kzt_guest_library_binding_key_t key; + int claimed; + library_t *retire_owner; + int retire_started; +} kzt_guest_library_observed_t; + +#define KZT_LOADER_ATTEMPT_SLOTS 16 +#define KZT_LOADER_ATTEMPT_OBJECTS 16 + +typedef enum kzt_guest_library_loader_pair_state { + KZT_LOADER_PAIR_EMPTY = 0, + KZT_LOADER_PAIR_PREPARED, + KZT_LOADER_PAIR_PUBLISHED, +} kzt_guest_library_loader_pair_state_t; + +typedef struct kzt_guest_library_loader_object { + uintptr_t link_map_addr; + library_t *library; + kzt_guest_library_object_type_t object_type; + kzt_guest_library_loader_pair_state_t pair_state; + kzt_guest_library_callback_gate_t *transition_gate; + kzt_guest_library_lifecycle_t *transition_fallback; + library_t *next_closed_by; + int transition_pending; + int reopen; +} kzt_guest_library_loader_object_t; + +typedef struct kzt_guest_library_loader_attempt { + unsigned long identity; + unsigned long cookie; + pthread_t owner; + size_t object_count; + int active; + kzt_guest_library_loader_object_t objects[KZT_LOADER_ATTEMPT_OBJECTS]; +} kzt_guest_library_loader_attempt_t; + +typedef struct kzt_guest_library_loader_state { + unsigned long epoch; + kzt_guest_library_loader_attempt_t attempts[KZT_LOADER_ATTEMPT_SLOTS]; +} kzt_guest_library_loader_state_t; + +struct kzt_guest_library_bindings { + pthread_mutex_t lock; + pthread_cond_t idle; + int shutting_down; + kzt_guest_library_binding_entry_t *entries; + size_t count; + size_t capacity; + kzt_guest_library_lifecycle_t *lifecycles; + size_t lifecycle_count; + size_t lifecycle_capacity; + kzt_guest_library_pending_t *pending; + size_t pending_count; + size_t pending_capacity; + kzt_guest_library_observed_t *observed; + size_t observed_count; + size_t observed_capacity; + kzt_guest_library_callback_gate_t *callback_gates; + unsigned int fallback_callback_readers; + kzt_guest_library_loader_state_t *loader_state; + struct { + unsigned long registry_missing; + unsigned long retire_unprovable; + } diagnostics; +}; + +#ifdef KZT_GUEST_LIBRARY_BINDING_TEST +static long fail_after = -1; +static kzt_guest_library_binding_test_retire_fn test_before_registry_retire; +static void *test_before_registry_retire_opaque; +static kzt_guest_library_binding_test_lifecycle_wait_fn + test_before_lifecycle_wait; +static void *test_before_lifecycle_wait_opaque; + +void kzt_guest_library_binding_test_set_alloc_failure_after(long allocations) +{ + fail_after = allocations; +} +void kzt_guest_library_binding_test_set_before_registry_retire( + kzt_guest_library_binding_test_retire_fn hook, void *opaque) +{ + test_before_registry_retire = hook; + test_before_registry_retire_opaque = opaque; +} +void kzt_guest_library_binding_test_set_before_lifecycle_wait( + kzt_guest_library_binding_test_lifecycle_wait_fn hook, void *opaque) +{ + test_before_lifecycle_wait = hook; + test_before_lifecycle_wait_opaque = opaque; +} +static int should_fail_alloc(void) +{ + if (fail_after < 0) return 0; + if (fail_after == 0) return 1; + --fail_after; + return 0; +} +#else +static int should_fail_alloc(void) { return 0; } +#endif + +static void *binding_calloc(size_t count, size_t size) +{ + return should_fail_alloc() ? NULL : calloc(count, size); +} + +static kzt_guest_library_loader_object_t *find_gate_transition_locked( + kzt_guest_library_bindings_t *bindings, + kzt_guest_library_callback_gate_t *gate); + +static int grow_array(void **array, size_t *capacity, size_t element_size) +{ + size_t next = *capacity ? *capacity * 2 : 8; + void *grown; + if (next < *capacity || next > SIZE_MAX / element_size || + should_fail_alloc()) + return -1; + grown = realloc(*array, next * element_size); + if (!grown) return -1; + *array = grown; + *capacity = next; + return 0; +} + +static int same_key(const kzt_guest_library_binding_key_t *a, + const kzt_guest_library_binding_key_t *b) +{ + return a->link_map_addr == b->link_map_addr && + a->generation == b->generation && + a->namespace_id == b->namespace_id && + a->namespace_kind == b->namespace_kind; +} + +static int supported_key(const kzt_guest_library_binding_key_t *key) +{ + return key && key->link_map_addr && key->generation && + key->namespace_kind == KZT_GUEST_LIBRARY_NAMESPACE_MAIN && + key->namespace_id == 0; +} + +static kzt_guest_library_lifecycle_t *find_lifecycle_locked( + kzt_guest_library_bindings_t *bindings, library_t *library) +{ + for (size_t i = 0; i < bindings->lifecycle_count; ++i) + if (bindings->lifecycles[i].library == library) + return &bindings->lifecycles[i]; + return NULL; +} + +static kzt_guest_library_observed_t *find_observed_locked( + kzt_guest_library_bindings_t *bindings, uintptr_t link_map_addr) +{ + for (size_t i = 0; i < bindings->observed_count; ++i) + if (bindings->observed[i].key.link_map_addr == link_map_addr) + return &bindings->observed[i]; + return NULL; +} + +static kzt_guest_library_observed_t *find_observed_key_locked( + kzt_guest_library_bindings_t *bindings, + const kzt_guest_library_binding_key_t *key) +{ + for (size_t i = 0; i < bindings->observed_count; ++i) + if (same_key(&bindings->observed[i].key, key)) + return &bindings->observed[i]; + return NULL; +} + +static kzt_guest_library_callback_gate_t *find_callback_gate_locked( + kzt_guest_library_bindings_t *bindings, uintptr_t link_map_addr) +{ + for (kzt_guest_library_callback_gate_t *gate = bindings->callback_gates; + gate; gate = gate->next) + if (gate->link_map_addr == link_map_addr) + return gate; + return NULL; +} + +static kzt_guest_library_callback_gate_t *find_callback_gate( + kzt_guest_library_bindings_t *bindings, uintptr_t link_map_addr) +{ + kzt_guest_library_callback_gate_t *gate = + __atomic_load_n(&bindings->callback_gates, __ATOMIC_ACQUIRE); + for (; gate; gate = gate->next) + if (gate->link_map_addr == link_map_addr) + return gate; + return NULL; +} + +static kzt_guest_library_callback_gate_t *add_callback_gate_locked( + kzt_guest_library_bindings_t *bindings, uintptr_t link_map_addr) +{ + kzt_guest_library_callback_gate_t *gate = + binding_calloc(1, sizeof(*gate)); + if (!gate) return NULL; + gate->link_map_addr = link_map_addr; + gate->next = bindings->callback_gates; + __atomic_store_n(&bindings->callback_gates, gate, __ATOMIC_RELEASE); + return gate; +} + +static void close_callback_addr_locked( + kzt_guest_library_bindings_t *bindings, + kzt_guest_library_lifecycle_t *lifecycle, library_t *library, + uintptr_t link_map_addr) +{ + kzt_guest_library_callback_gate_t *gate; + if (!link_map_addr) return; + lifecycle->fallback_closed_epoch = + bindings->loader_state ? bindings->loader_state->epoch : 0; + gate = find_callback_gate_locked(bindings, link_map_addr); + if (!gate) + gate = add_callback_gate_locked(bindings, link_map_addr); + if (!gate) { + /* A lifecycle normally has one current link_map. Keep its closed + * address inline so allocation failure cannot admit a late callback + * or block unrelated libraries. */ + lifecycle->fallback_closed_addr = link_map_addr; + return; + } + unsigned long state = __atomic_load_n(&gate->state, __ATOMIC_ACQUIRE); + if ((state & KZT_CALLBACK_GATE_CLOSED) && + (state & KZT_CALLBACK_GATE_READERS) && gate->closed_by && + gate->closed_by != library) { + kzt_guest_library_loader_object_t *transition = + find_gate_transition_locked(bindings, gate); + if (transition) { + transition->next_closed_by = library; + transition->reopen = 0; + } else { + lifecycle->fallback_closed_addr = link_map_addr; + } + } else { + gate->closed_by = library; + } + __atomic_fetch_or(&gate->state, KZT_CALLBACK_GATE_CLOSED, + __ATOMIC_ACQ_REL); +} + +static int shutting_down(kzt_guest_library_bindings_t *bindings) +{ + return __atomic_load_n(&bindings->shutting_down, __ATOMIC_ACQUIRE); +} + +static kzt_guest_library_loader_attempt_t *find_loader_attempt_locked( + kzt_guest_library_bindings_t *bindings, + const kzt_guest_library_loader_scope_t *scope) +{ + if (!scope || scope->bindings != bindings || !scope->identity || + !bindings->loader_state || + !scope->cookie) + return NULL; + for (size_t i = 0; i < KZT_LOADER_ATTEMPT_SLOTS; ++i) { + kzt_guest_library_loader_attempt_t *attempt = + &bindings->loader_state->attempts[i]; + if (attempt->active && attempt->identity == scope->identity && + attempt->cookie == scope->cookie) + return attempt; + } + return NULL; +} + +static kzt_guest_library_loader_attempt_t *loader_scope_valid_locked( + kzt_guest_library_bindings_t *bindings, + const kzt_guest_library_loader_scope_t *scope) +{ + kzt_guest_library_loader_attempt_t *attempt = + find_loader_attempt_locked(bindings, scope); + unsigned long newest = 0; + + if (!attempt || !pthread_equal(attempt->owner, pthread_self())) + return NULL; + for (size_t i = 0; i < KZT_LOADER_ATTEMPT_SLOTS; ++i) { + kzt_guest_library_loader_attempt_t *candidate = + &bindings->loader_state->attempts[i]; + if (candidate->active && + pthread_equal(candidate->owner, attempt->owner) && + candidate->identity > newest) + newest = candidate->identity; + } + return newest == attempt->identity ? attempt : NULL; +} + +static kzt_guest_library_loader_object_t *find_loader_object_locked( + kzt_guest_library_loader_attempt_t *attempt, uintptr_t link_map_addr) +{ + for (size_t i = 0; i < attempt->object_count; ++i) + if (attempt->objects[i].link_map_addr == link_map_addr) + return &attempt->objects[i]; + return NULL; +} + +static kzt_guest_library_loader_object_t *find_gate_transition_locked( + kzt_guest_library_bindings_t *bindings, + kzt_guest_library_callback_gate_t *gate) +{ + if (!bindings->loader_state) return NULL; + for (size_t i = 0; i < KZT_LOADER_ATTEMPT_SLOTS; ++i) { + kzt_guest_library_loader_attempt_t *attempt = + &bindings->loader_state->attempts[i]; + for (size_t j = 0; j < attempt->object_count; ++j) { + kzt_guest_library_loader_object_t *object = &attempt->objects[j]; + if (object->transition_pending && + object->transition_gate == gate) + return object; + } + } + return NULL; +} + +static void clear_loader_transition_locked( + kzt_guest_library_bindings_t *bindings, + kzt_guest_library_loader_object_t *object) +{ + kzt_guest_library_loader_attempt_t *attempt = NULL; + if (!bindings->loader_state || !object) return; + for (size_t i = 0; i < KZT_LOADER_ATTEMPT_SLOTS; ++i) { + kzt_guest_library_loader_attempt_t *candidate = + &bindings->loader_state->attempts[i]; + if (object >= candidate->objects && + object < candidate->objects + KZT_LOADER_ATTEMPT_OBJECTS) { + attempt = candidate; + break; + } + } + object->transition_gate = NULL; + object->transition_fallback = NULL; + object->next_closed_by = NULL; + object->transition_pending = 0; + object->reopen = 0; + if (attempt && !attempt->active) { + int pending = 0; + for (size_t i = 0; i < attempt->object_count; ++i) + pending |= attempt->objects[i].transition_pending; + if (!pending) memset(attempt, 0, sizeof(*attempt)); + } +} + +static int observe_loader_object_locked( + kzt_guest_library_loader_attempt_t *attempt, uintptr_t link_map_addr) +{ + if (find_loader_object_locked(attempt, link_map_addr)) + return 0; + if (attempt->object_count == KZT_LOADER_ATTEMPT_OBJECTS) + return -1; + attempt->objects[attempt->object_count++].link_map_addr = link_map_addr; + return 0; +} + +static void reopen_callback_addr_locked( + kzt_guest_library_bindings_t *bindings, uintptr_t link_map_addr, + library_t *library, kzt_guest_library_loader_attempt_t *attempt) +{ + kzt_guest_library_callback_gate_t *gate = + find_callback_gate_locked(bindings, link_map_addr); + (void)library; + + if (gate && (__atomic_load_n(&gate->state, __ATOMIC_ACQUIRE) & + KZT_CALLBACK_GATE_CLOSED)) { + unsigned long state = __atomic_load_n(&gate->state, __ATOMIC_ACQUIRE); + if (state & KZT_CALLBACK_GATE_READERS) { + kzt_guest_library_loader_object_t *object = + attempt ? find_loader_object_locked(attempt, link_map_addr) + : NULL; + if (object) { + object->transition_gate = gate; + object->transition_pending = 1; + object->reopen = 1; + } + } else { + kzt_guest_library_lifecycle_t *owner = + find_lifecycle_locked(bindings, gate->closed_by); + if (owner) owner->fallback_closed_epoch = 0; + gate->closed_by = NULL; + __atomic_fetch_and(&gate->state, ~KZT_CALLBACK_GATE_CLOSED, + __ATOMIC_ACQ_REL); + } + } + for (size_t i = 0; i < bindings->lifecycle_count; ++i) { + if (bindings->lifecycles[i].fallback_closed_addr == link_map_addr) { + if (bindings->fallback_callback_readers) { + kzt_guest_library_loader_object_t *object = + attempt ? find_loader_object_locked( + attempt, link_map_addr) : NULL; + if (object) { + object->transition_fallback = &bindings->lifecycles[i]; + object->transition_pending = 1; + object->reopen = 1; + } + } else { + bindings->lifecycles[i].fallback_closed_addr = 0; + bindings->lifecycles[i].fallback_closed_epoch = 0; + } + } + } +} + +static int callback_access_busy_locked( + kzt_guest_library_bindings_t *bindings, + const kzt_guest_library_lifecycle_t *lifecycle, library_t *library) +{ + if (bindings->fallback_callback_readers) + return 1; + for (kzt_guest_library_callback_gate_t *gate = bindings->callback_gates; + gate; gate = gate->next) { + if (gate->closed_by == library && + (__atomic_load_n(&gate->state, __ATOMIC_ACQUIRE) & + KZT_CALLBACK_GATE_READERS)) + return 1; + } + return 0; +} + +static kzt_guest_library_binding_result_t bind_locked( + kzt_guest_library_bindings_t *bindings, + const kzt_guest_library_binding_key_t *key, library_t *library, + kzt_guest_library_object_type_t object_type) +{ + kzt_guest_library_lifecycle_t *lifecycle; + if (shutting_down(bindings) || !supported_key(key) || !library || + object_type == KZT_GUEST_LIBRARY_OBJECT_MAIN || + object_type == KZT_GUEST_LIBRARY_OBJECT_UNSUPPORTED) + return KZT_GUEST_LIBRARY_BINDING_ERROR; + lifecycle = find_lifecycle_locked(bindings, library); + if (!lifecycle || lifecycle->state != KZT_GUEST_LIBRARY_BINDING_LIVE) + return KZT_GUEST_LIBRARY_BINDING_ERROR; + for (size_t i = 0; i < bindings->count; ++i) { + kzt_guest_library_binding_entry_t *entry = &bindings->entries[i]; + if (!same_key(&entry->key, key) || + entry->state == KZT_GUEST_LIBRARY_BINDING_DEAD) + continue; + return entry->state == KZT_GUEST_LIBRARY_BINDING_LIVE && + entry->library == library && + entry->object_type == object_type + ? KZT_GUEST_LIBRARY_BINDING_UNCHANGED + : KZT_GUEST_LIBRARY_BINDING_CONFLICT; + } + if (bindings->count == bindings->capacity && + grow_array((void **)&bindings->entries, &bindings->capacity, + sizeof(*bindings->entries)) != 0) + return KZT_GUEST_LIBRARY_BINDING_ERROR; + bindings->entries[bindings->count++] = + (kzt_guest_library_binding_entry_t){ + .key = *key, + .library = library, + .object_type = object_type, + .state = KZT_GUEST_LIBRARY_BINDING_LIVE, + }; + return KZT_GUEST_LIBRARY_BINDING_ADDED; +} + +kzt_guest_library_bindings_t *kzt_guest_library_bindings_init(void) +{ + kzt_guest_library_bindings_t *bindings = + binding_calloc(1, sizeof(*bindings)); + if (!bindings) return NULL; + if (pthread_mutex_init(&bindings->lock, NULL) != 0) { + free(bindings); + return NULL; + } + if (pthread_cond_init(&bindings->idle, NULL) != 0) { + pthread_mutex_destroy(&bindings->lock); + free(bindings); + return NULL; + } + return bindings; +} + +void kzt_guest_library_bindings_begin_teardown( + kzt_guest_library_bindings_t *bindings) +{ + if (!bindings) return; + pthread_mutex_lock(&bindings->lock); + __atomic_store_n(&bindings->shutting_down, 1, __ATOMIC_RELEASE); + for (size_t i = 0; i < bindings->pending_count; ++i) + bindings->pending[i].active = 0; + for (size_t i = 0; i < bindings->count; ++i) + bindings->entries[i].state = KZT_GUEST_LIBRARY_BINDING_UNLOADING; + for (;;) { + int busy = 0; + for (size_t i = 0; i < bindings->count; ++i) + busy |= bindings->entries[i].references != 0; + for (size_t i = 0; i < bindings->lifecycle_count; ++i) + busy |= bindings->lifecycles[i].state == + KZT_GUEST_LIBRARY_BINDING_UNLOADING; + busy |= bindings->fallback_callback_readers != 0; + for (kzt_guest_library_callback_gate_t *gate = + bindings->callback_gates; + gate; gate = gate->next) + busy |= (__atomic_load_n(&gate->state, __ATOMIC_ACQUIRE) & + KZT_CALLBACK_GATE_READERS) != 0; + if (!busy) break; + pthread_cond_wait(&bindings->idle, &bindings->lock); + } + pthread_mutex_unlock(&bindings->lock); +} + +void kzt_guest_library_bindings_destroy(kzt_guest_library_bindings_t **slot) +{ + kzt_guest_library_bindings_t *bindings; + if (!slot || !(bindings = *slot)) return; + kzt_guest_library_bindings_begin_teardown(bindings); + pthread_cond_destroy(&bindings->idle); + pthread_mutex_destroy(&bindings->lock); + while (bindings->callback_gates) { + kzt_guest_library_callback_gate_t *next = + bindings->callback_gates->next; + free(bindings->callback_gates); + bindings->callback_gates = next; + } + free(bindings->observed); + free(bindings->pending); + if (bindings->loader_state) free(bindings->loader_state); + free(bindings->lifecycles); + free(bindings->entries); + free(bindings); + *slot = NULL; +} + +int kzt_guest_library_access_init(kzt_guest_library_access_t *access) +{ + if (!access) return -1; + memset(access, 0, sizeof(*access)); + if (pthread_mutex_init(&access->lock, NULL) != 0) + return -1; + access->initialized = 1; + access->bindings = kzt_guest_library_bindings_init(); + access->accepting = 1; + return 0; +} + +void kzt_guest_library_access_begin_teardown( + kzt_guest_library_access_t *access) +{ + if (!access || !access->initialized) return; + pthread_mutex_lock(&access->lock); + access->accepting = 0; + /* Closing the context-owned gate under its lock drains any lookup already + * inside the gate and prevents new acquisitions. Drop the gate before + * waiting for binding handles or unload owners so a source-lease holder + * can fast-fail provider lookup and release its lease. */ + pthread_mutex_unlock(&access->lock); + kzt_guest_library_bindings_begin_teardown(access->bindings); +} + +void kzt_guest_library_access_destroy(kzt_guest_library_access_t *access) +{ + if (!access || !access->initialized) return; + kzt_guest_library_access_begin_teardown(access); + kzt_guest_library_bindings_destroy(&access->bindings); + pthread_mutex_destroy(&access->lock); + memset(access, 0, sizeof(*access)); +} + +int kzt_guest_library_track(kzt_guest_library_bindings_t *bindings, + library_t *library) +{ + kzt_guest_library_lifecycle_t *lifecycle; + int result = -1; + if (!bindings || !library) return -1; + pthread_mutex_lock(&bindings->lock); + if (shutting_down(bindings)) { + goto out; + } + lifecycle = find_lifecycle_locked(bindings, library); + if (lifecycle) { + result = lifecycle->state == KZT_GUEST_LIBRARY_BINDING_LIVE ? 0 : -1; + goto out; + } + if (bindings->lifecycle_count == bindings->lifecycle_capacity && + grow_array((void **)&bindings->lifecycles, + &bindings->lifecycle_capacity, + sizeof(*bindings->lifecycles)) != 0) { + goto out; + } + bindings->lifecycles[bindings->lifecycle_count++] = + (kzt_guest_library_lifecycle_t){ + .library = library, + .state = KZT_GUEST_LIBRARY_BINDING_LIVE, + }; + result = 0; +out: + pthread_mutex_unlock(&bindings->lock); + return result; +} + +int kzt_guest_library_reactivate(kzt_guest_library_bindings_t *bindings, + library_t *library) +{ + kzt_guest_library_lifecycle_t *lifecycle; + int result = -1; + if (!bindings || !library) return -1; + pthread_mutex_lock(&bindings->lock); + lifecycle = find_lifecycle_locked(bindings, library); + if (shutting_down(bindings) || !lifecycle || lifecycle->destroy_started) { + goto out; + } + if (lifecycle->state == KZT_GUEST_LIBRARY_BINDING_LIVE) { + result = 0; + goto out; + } + if (lifecycle->state != KZT_GUEST_LIBRARY_BINDING_DEAD) + goto out; + lifecycle->state = KZT_GUEST_LIBRARY_BINDING_LIVE; + result = 0; +out: + pthread_mutex_unlock(&bindings->lock); + return result; +} + +int kzt_guest_library_loader_scope_begin( + kzt_guest_library_bindings_t *bindings, + kzt_guest_library_loader_scope_t *scope) +{ + int result = -1; + pthread_t owner = pthread_self(); + + if (scope) memset(scope, 0, sizeof(*scope)); + if (!bindings || !scope) return -1; + pthread_mutex_lock(&bindings->lock); + if (!shutting_down(bindings) && + (!bindings->loader_state || bindings->loader_state->epoch != ULONG_MAX)) { + kzt_guest_library_loader_attempt_t *slot = NULL; + if (!bindings->loader_state) + bindings->loader_state = binding_calloc( + 1, sizeof(*bindings->loader_state)); + if (!bindings->loader_state) + goto out; + for (size_t i = 0; i < KZT_LOADER_ATTEMPT_SLOTS; ++i) { + kzt_guest_library_loader_attempt_t *attempt = + &bindings->loader_state->attempts[i]; + if (!attempt->active && !attempt->identity && !slot) + slot = attempt; + } + if (slot) { + unsigned long identity = ++bindings->loader_state->epoch; + unsigned long cookie = identity ^ (unsigned long)(uintptr_t)bindings ^ + (unsigned long)(uintptr_t)slot; + if (!cookie) cookie = ~identity; + memset(slot, 0, sizeof(*slot)); + slot->identity = identity; + slot->cookie = cookie; + slot->owner = owner; + slot->active = 1; + scope->bindings = bindings; + scope->identity = identity; + scope->cookie = cookie; + result = 0; + } + } +out: + pthread_mutex_unlock(&bindings->lock); + return result; +} + +void kzt_guest_library_loader_scope_end( + kzt_guest_library_loader_scope_t *scope) +{ + kzt_guest_library_bindings_t *bindings; + if (!scope || !(bindings = scope->bindings)) return; + pthread_mutex_lock(&bindings->lock); + kzt_guest_library_loader_attempt_t *attempt = + find_loader_attempt_locked(bindings, scope); + if (attempt && pthread_equal(attempt->owner, pthread_self())) { + int pending = 0; + for (size_t i = 0; i < attempt->object_count; ++i) + pending |= attempt->objects[i].transition_pending; + if (pending) + attempt->active = 0; + else + memset(attempt, 0, sizeof(*attempt)); + } + pthread_mutex_unlock(&bindings->lock); + memset(scope, 0, sizeof(*scope)); +} + +static int callback_access_begin( + kzt_guest_library_bindings_t *bindings, uintptr_t link_map_addr, + const kzt_guest_library_loader_scope_t *scope, + kzt_guest_library_callback_access_t *access) +{ + kzt_guest_library_callback_gate_t *gate; + kzt_guest_library_loader_attempt_t *attempt; + kzt_guest_library_lifecycle_t *fallback_owner = NULL; + int closed_seen = 0; + + if (access) memset(access, 0, sizeof(*access)); + if (!bindings || !link_map_addr || !access) return -1; + if (shutting_down(bindings)) return -1; + gate = find_callback_gate(bindings, link_map_addr); + if (gate) { + unsigned long state = + __atomic_load_n(&gate->state, __ATOMIC_ACQUIRE); + if (state & KZT_CALLBACK_GATE_CLOSED) + goto slow; + do { + if ((state & KZT_CALLBACK_GATE_READERS) == + KZT_CALLBACK_GATE_READERS) + return -1; + } while (!__atomic_compare_exchange_n( + &gate->state, &state, state + 1, 1, + __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)); + if (scope && scope->bindings) { + int valid; + pthread_mutex_lock(&bindings->lock); + kzt_guest_library_loader_attempt_t *fast_attempt = + loader_scope_valid_locked(bindings, scope); + valid = fast_attempt && + observe_loader_object_locked( + fast_attempt, link_map_addr) == 0; + pthread_mutex_unlock(&bindings->lock); + if (!valid) { + __atomic_fetch_sub(&gate->state, 1, __ATOMIC_ACQ_REL); + return -1; + } + } + if (shutting_down(bindings)) { + unsigned long previous = __atomic_fetch_sub( + &gate->state, 1, __ATOMIC_ACQ_REL); + if ((previous & KZT_CALLBACK_GATE_READERS) == 1) { + pthread_mutex_lock(&bindings->lock); + pthread_cond_broadcast(&bindings->idle); + pthread_mutex_unlock(&bindings->lock); + } + return -1; + } + access->bindings = bindings; + access->link_map_addr = link_map_addr; + access->gate = gate; + return 0; + } + +slow: + pthread_mutex_lock(&bindings->lock); + if (shutting_down(bindings)) { + pthread_mutex_unlock(&bindings->lock); + return -1; + } + if (!scope || !scope->bindings) { + for (size_t i = 0; i < bindings->lifecycle_count; ++i) { + if (bindings->lifecycles[i].fallback_closed_addr == + link_map_addr) { + pthread_mutex_unlock(&bindings->lock); + return -1; + } + } + gate = find_callback_gate_locked(bindings, link_map_addr); + if (gate && (__atomic_load_n(&gate->state, __ATOMIC_ACQUIRE) & + KZT_CALLBACK_GATE_CLOSED)) { + pthread_mutex_unlock(&bindings->lock); + return -1; + } + if (!gate) + gate = add_callback_gate_locked(bindings, link_map_addr); + goto acquire_locked; + } + attempt = loader_scope_valid_locked(bindings, scope); + if (!attempt) { + pthread_mutex_unlock(&bindings->lock); + return -1; + } + for (size_t i = 0; i < bindings->lifecycle_count; ++i) { + kzt_guest_library_lifecycle_t *lifecycle = &bindings->lifecycles[i]; + if (lifecycle->fallback_closed_addr != link_map_addr) + continue; + closed_seen = 1; + fallback_owner = lifecycle; + if (!attempt || attempt->identity <= lifecycle->fallback_closed_epoch) { + pthread_mutex_unlock(&bindings->lock); + return -1; + } + } + gate = find_callback_gate_locked(bindings, link_map_addr); + if (gate && (__atomic_load_n(&gate->state, __ATOMIC_ACQUIRE) & + KZT_CALLBACK_GATE_CLOSED)) { + kzt_guest_library_lifecycle_t *owner = + find_lifecycle_locked(bindings, gate->closed_by); + unsigned long closed_epoch = owner ? owner->fallback_closed_epoch : 0; + + closed_seen = 1; + if (!attempt || attempt->identity <= closed_epoch) { + pthread_mutex_unlock(&bindings->lock); + return -1; + } + } + if (attempt && observe_loader_object_locked(attempt, link_map_addr) != 0) { + pthread_mutex_unlock(&bindings->lock); + return -1; + } + if (!gate) + gate = add_callback_gate_locked(bindings, link_map_addr); + if (gate && closed_seen && fallback_owner && + !(__atomic_load_n(&gate->state, __ATOMIC_ACQUIRE) & + KZT_CALLBACK_GATE_CLOSED)) { + gate->closed_by = fallback_owner->library; + __atomic_fetch_or(&gate->state, KZT_CALLBACK_GATE_CLOSED, + __ATOMIC_ACQ_REL); + } +acquire_locked: + access->bindings = bindings; + access->link_map_addr = link_map_addr; + if (gate) { + unsigned long state = __atomic_load_n(&gate->state, __ATOMIC_ACQUIRE); + if ((state & KZT_CALLBACK_GATE_READERS) == KZT_CALLBACK_GATE_READERS) { + pthread_mutex_unlock(&bindings->lock); + memset(access, 0, sizeof(*access)); + return -1; + } + __atomic_add_fetch(&gate->state, 1, __ATOMIC_ACQ_REL); + access->gate = gate; + } else { + ++bindings->fallback_callback_readers; + access->fallback = 1; + } + pthread_mutex_unlock(&bindings->lock); + return 0; +} + +int kzt_guest_library_callback_access_begin( + kzt_guest_library_bindings_t *bindings, uintptr_t link_map_addr, + kzt_guest_library_callback_access_t *access) +{ + return callback_access_begin(bindings, link_map_addr, NULL, access); +} + +int kzt_guest_library_callback_access_begin_scoped( + kzt_guest_library_bindings_t *bindings, uintptr_t link_map_addr, + const kzt_guest_library_loader_scope_t *scope, + kzt_guest_library_callback_access_t *access) +{ + return callback_access_begin(bindings, link_map_addr, scope, access); +} + +void kzt_guest_library_callback_access_end( + kzt_guest_library_callback_access_t *access) +{ + kzt_guest_library_bindings_t *bindings; + if (!access || !(bindings = access->bindings)) return; + if (access->gate) { + kzt_guest_library_callback_gate_t *gate = access->gate; + unsigned long previous = __atomic_fetch_sub( + &gate->state, 1, __ATOMIC_ACQ_REL); + int last = (previous & KZT_CALLBACK_GATE_READERS) == 1; + if (last && ((previous & KZT_CALLBACK_GATE_CLOSED) || + shutting_down(bindings))) { + pthread_mutex_lock(&bindings->lock); + if (!shutting_down(bindings) && + (__atomic_load_n(&gate->state, __ATOMIC_ACQUIRE) & + KZT_CALLBACK_GATE_READERS) == 0) { + kzt_guest_library_loader_object_t *transition = + find_gate_transition_locked(bindings, gate); + if (transition && transition->next_closed_by) { + gate->closed_by = transition->next_closed_by; + clear_loader_transition_locked(bindings, transition); + } else if (transition && transition->reopen) { + kzt_guest_library_lifecycle_t *owner = + find_lifecycle_locked(bindings, gate->closed_by); + if (owner) owner->fallback_closed_epoch = 0; + gate->closed_by = NULL; + __atomic_fetch_and(&gate->state, + ~KZT_CALLBACK_GATE_CLOSED, + __ATOMIC_ACQ_REL); + clear_loader_transition_locked(bindings, transition); + } + } + pthread_cond_broadcast(&bindings->idle); + pthread_mutex_unlock(&bindings->lock); + } + memset(access, 0, sizeof(*access)); + return; + } + pthread_mutex_lock(&bindings->lock); + if (access->fallback) { + if (bindings->fallback_callback_readers) + --bindings->fallback_callback_readers; + if (!bindings->fallback_callback_readers && + !shutting_down(bindings)) { + if (bindings->loader_state) { + for (size_t i = 0; i < KZT_LOADER_ATTEMPT_SLOTS; ++i) { + kzt_guest_library_loader_attempt_t *attempt = + &bindings->loader_state->attempts[i]; + for (size_t j = 0; j < attempt->object_count; ++j) { + kzt_guest_library_loader_object_t *object = + &attempt->objects[j]; + if (object->transition_pending && object->reopen && + object->transition_fallback) { + object->transition_fallback->fallback_closed_addr = 0; + object->transition_fallback->fallback_closed_epoch = 0; + clear_loader_transition_locked(bindings, object); + } + } + } + } + } + } + pthread_cond_broadcast(&bindings->idle); + pthread_mutex_unlock(&bindings->lock); + memset(access, 0, sizeof(*access)); +} + +kzt_guest_library_binding_result_t kzt_guest_library_bind( + kzt_guest_library_bindings_t *bindings, + const kzt_guest_library_binding_key_t *key, library_t *library, + kzt_guest_library_object_type_t object_type) +{ + kzt_guest_library_binding_result_t result; + if (!bindings) return KZT_GUEST_LIBRARY_BINDING_DISABLED; + pthread_mutex_lock(&bindings->lock); + result = bind_locked(bindings, key, library, object_type); + pthread_mutex_unlock(&bindings->lock); + return result; +} + +static kzt_guest_library_binding_result_t note_exact_pair_locked( + kzt_guest_library_bindings_t *bindings, uintptr_t link_map_addr, + library_t *library, kzt_guest_library_object_type_t object_type) +{ + kzt_guest_library_observed_t *observed; + kzt_guest_library_lifecycle_t *lifecycle; + kzt_guest_library_binding_result_t result; + lifecycle = find_lifecycle_locked(bindings, library); + if (shutting_down(bindings) || !lifecycle || + lifecycle->state != KZT_GUEST_LIBRARY_BINDING_LIVE) { + result = KZT_GUEST_LIBRARY_BINDING_ERROR; + return result; + } + observed = find_observed_locked(bindings, link_map_addr); + if (observed) { + if (observed->retire_owner) { + return KZT_GUEST_LIBRARY_BINDING_CANCELLED; + } + result = bind_locked(bindings, &observed->key, library, object_type); + if (result == KZT_GUEST_LIBRARY_BINDING_ADDED || + result == KZT_GUEST_LIBRARY_BINDING_UNCHANGED) + observed->claimed = 1; + return result; + } + for (size_t i = 0; i < bindings->pending_count; ++i) { + kzt_guest_library_pending_t *pending = &bindings->pending[i]; + if (!pending->active || pending->link_map_addr != link_map_addr) + continue; + result = pending->library == library && + pending->object_type == object_type + ? KZT_GUEST_LIBRARY_BINDING_PENDING + : KZT_GUEST_LIBRARY_BINDING_CONFLICT; + return result; + } + if (bindings->pending_count == bindings->pending_capacity && + grow_array((void **)&bindings->pending, &bindings->pending_capacity, + sizeof(*bindings->pending)) != 0) { + return KZT_GUEST_LIBRARY_BINDING_ERROR; + } + bindings->pending[bindings->pending_count++] = + (kzt_guest_library_pending_t){ + .link_map_addr = link_map_addr, + .library = library, + .object_type = object_type, + .active = 1, + }; + return KZT_GUEST_LIBRARY_BINDING_PENDING; +} + +kzt_guest_library_binding_result_t kzt_guest_library_note_exact_pair( + kzt_guest_library_bindings_t *bindings, uintptr_t link_map_addr, + library_t *library, kzt_guest_library_object_type_t object_type) +{ + kzt_guest_library_binding_result_t result; + if (!link_map_addr || !library || + object_type == KZT_GUEST_LIBRARY_OBJECT_MAIN || + object_type == KZT_GUEST_LIBRARY_OBJECT_UNSUPPORTED) + return KZT_GUEST_LIBRARY_BINDING_ERROR; + if (!bindings) return KZT_GUEST_LIBRARY_BINDING_DISABLED; + pthread_mutex_lock(&bindings->lock); + result = note_exact_pair_locked(bindings, link_map_addr, library, + object_type); + pthread_mutex_unlock(&bindings->lock); + return result; +} + +kzt_guest_library_binding_result_t +kzt_guest_library_loader_scope_note_pair( + const kzt_guest_library_loader_scope_t *scope, + uintptr_t link_map_addr, library_t *library, + kzt_guest_library_object_type_t object_type) +{ + kzt_guest_library_bindings_t *bindings; + kzt_guest_library_loader_attempt_t *attempt; + kzt_guest_library_loader_object_t *object; + kzt_guest_library_lifecycle_t *lifecycle; + + if (!scope || !(bindings = scope->bindings) || !link_map_addr || !library || + object_type == KZT_GUEST_LIBRARY_OBJECT_MAIN || + object_type == KZT_GUEST_LIBRARY_OBJECT_UNSUPPORTED) + return KZT_GUEST_LIBRARY_BINDING_ERROR; + pthread_mutex_lock(&bindings->lock); + attempt = loader_scope_valid_locked(bindings, scope); + object = attempt ? find_loader_object_locked(attempt, link_map_addr) : NULL; + lifecycle = find_lifecycle_locked(bindings, library); + if (shutting_down(bindings) || !object || !lifecycle || + lifecycle->state != KZT_GUEST_LIBRARY_BINDING_LIVE || + lifecycle->destroy_started || + (object->pair_state != KZT_LOADER_PAIR_EMPTY && + (object->library != library || + object->object_type != object_type))) { + pthread_mutex_unlock(&bindings->lock); + return KZT_GUEST_LIBRARY_BINDING_ERROR; + } + object->library = library; + object->object_type = object_type; + if (object->pair_state == KZT_LOADER_PAIR_EMPTY) + object->pair_state = KZT_LOADER_PAIR_PREPARED; + pthread_mutex_unlock(&bindings->lock); + return KZT_GUEST_LIBRARY_BINDING_PENDING; +} + +kzt_guest_library_binding_result_t +kzt_guest_library_loader_scope_publish_pair( + const kzt_guest_library_loader_scope_t *scope, + uintptr_t link_map_addr, library_t *library, + kzt_guest_library_object_type_t object_type) +{ + kzt_guest_library_binding_result_t result; + kzt_guest_library_bindings_t *bindings; + kzt_guest_library_loader_attempt_t *attempt; + kzt_guest_library_loader_object_t *object; + kzt_guest_library_lifecycle_t *lifecycle; + + if (!scope || !(bindings = scope->bindings) || !link_map_addr || !library) + return KZT_GUEST_LIBRARY_BINDING_ERROR; + pthread_mutex_lock(&bindings->lock); + attempt = loader_scope_valid_locked(bindings, scope); + object = attempt ? find_loader_object_locked(attempt, link_map_addr) : NULL; + lifecycle = find_lifecycle_locked(bindings, library); + if (shutting_down(bindings) || !object || + object->pair_state != KZT_LOADER_PAIR_PREPARED || + object->library != library || + object->object_type != object_type || !lifecycle || + lifecycle->state != KZT_GUEST_LIBRARY_BINDING_LIVE || + lifecycle->destroy_started) { + pthread_mutex_unlock(&bindings->lock); + return KZT_GUEST_LIBRARY_BINDING_ERROR; + } + result = note_exact_pair_locked(bindings, link_map_addr, library, + object_type); + if (result != KZT_GUEST_LIBRARY_BINDING_ADDED && + result != KZT_GUEST_LIBRARY_BINDING_UNCHANGED && + result != KZT_GUEST_LIBRARY_BINDING_PENDING) { + pthread_mutex_unlock(&bindings->lock); + return result; + } + object->pair_state = KZT_LOADER_PAIR_PUBLISHED; + reopen_callback_addr_locked(bindings, link_map_addr, library, attempt); + pthread_mutex_unlock(&bindings->lock); + return result; +} + +kzt_guest_library_binding_result_t +kzt_guest_library_loader_scope_publish_observed( + const kzt_guest_library_loader_scope_t *scope, + uintptr_t link_map_addr) +{ + kzt_guest_library_bindings_t *bindings; + library_t *library = NULL; + kzt_guest_library_object_type_t object_type = + KZT_GUEST_LIBRARY_OBJECT_UNSUPPORTED; + + if (!scope || !(bindings = scope->bindings)) + return KZT_GUEST_LIBRARY_BINDING_ERROR; + pthread_mutex_lock(&bindings->lock); + kzt_guest_library_loader_attempt_t *attempt = + loader_scope_valid_locked(bindings, scope); + kzt_guest_library_loader_object_t *object = + attempt ? find_loader_object_locked(attempt, link_map_addr) : NULL; + if (object) { + library = object->library; + object_type = object->object_type; + } + pthread_mutex_unlock(&bindings->lock); + if (!library) + return KZT_GUEST_LIBRARY_BINDING_ERROR; + return kzt_guest_library_loader_scope_publish_pair( + scope, link_map_addr, library, object_type); +} + +kzt_guest_library_binding_result_t kzt_guest_library_publish_loader_pair( + kzt_guest_library_bindings_t *bindings, uintptr_t link_map_addr, + library_t *library, kzt_guest_library_object_type_t object_type) +{ + kzt_guest_library_binding_result_t result = + kzt_guest_library_note_exact_pair(bindings, link_map_addr, library, + object_type); + + if (!bindings || (result != KZT_GUEST_LIBRARY_BINDING_ADDED && + result != KZT_GUEST_LIBRARY_BINDING_UNCHANGED && + result != KZT_GUEST_LIBRARY_BINDING_PENDING)) + return result; + pthread_mutex_lock(&bindings->lock); + kzt_guest_library_lifecycle_t *lifecycle = + find_lifecycle_locked(bindings, library); + if (!shutting_down(bindings) && lifecycle && + lifecycle->state == KZT_GUEST_LIBRARY_BINDING_LIVE && + !lifecycle->destroy_started) { + kzt_guest_library_callback_gate_t *gate = + find_callback_gate_locked(bindings, link_map_addr); + int same_reactivated_library = + gate && gate->closed_by == library; + for (size_t i = 0; i < bindings->lifecycle_count; ++i) + if (bindings->lifecycles[i].library == library && + bindings->lifecycles[i].fallback_closed_addr == link_map_addr) + same_reactivated_library = 1; + if (same_reactivated_library) + reopen_callback_addr_locked(bindings, link_map_addr, library, + NULL); + } + pthread_mutex_unlock(&bindings->lock); + return result; +} + +kzt_guest_library_binding_result_t kzt_guest_library_note_observation( + kzt_guest_library_bindings_t *bindings, + const kzt_guest_library_binding_key_t *key) +{ + kzt_guest_library_observed_t *observed; + kzt_guest_library_binding_result_t result = + KZT_GUEST_LIBRARY_BINDING_UNCHANGED; + if (!supported_key(key)) return KZT_GUEST_LIBRARY_BINDING_ERROR; + if (!bindings) return KZT_GUEST_LIBRARY_BINDING_DISABLED; + pthread_mutex_lock(&bindings->lock); + if (shutting_down(bindings)) { + result = KZT_GUEST_LIBRARY_BINDING_DISABLED; + goto out; + } + for (size_t i = 0; i < bindings->count; ++i) { + kzt_guest_library_binding_entry_t *entry = &bindings->entries[i]; + if (entry->state == KZT_GUEST_LIBRARY_BINDING_UNLOADING && + same_key(&entry->key, key)) { + /* Phase 1 has assigned this exact generation to the binding + * lifecycle owner. This check deliberately precedes the + * address-only pending cancellation path. */ + result = KZT_GUEST_LIBRARY_BINDING_RETIRE_OWNED; + goto out; + } + } + observed = find_observed_key_locked(bindings, key); + if (observed && observed->retire_owner) { + result = KZT_GUEST_LIBRARY_BINDING_RETIRE_OWNED; + goto out; + } + { + int active_pending = 0; + int cancelled_pending = 0; + for (size_t i = 0; i < bindings->pending_count; ++i) { + kzt_guest_library_pending_t *pending = &bindings->pending[i]; + if (pending->link_map_addr != key->link_map_addr) + continue; + if (pending->active) + active_pending = 1; + else { + kzt_guest_library_lifecycle_t *lifecycle = + find_lifecycle_locked(bindings, pending->library); + if (lifecycle && + lifecycle->state != KZT_GUEST_LIBRARY_BINDING_LIVE) + cancelled_pending = 1; + } + } + if (cancelled_pending && !active_pending) { + for (size_t i = 0; i < bindings->pending_count; ++i) { + kzt_guest_library_pending_t *pending = &bindings->pending[i]; + kzt_guest_library_lifecycle_t *lifecycle; + if (pending->active || + pending->link_map_addr != key->link_map_addr) + continue; + lifecycle = find_lifecycle_locked(bindings, + pending->library); + if (lifecycle && + lifecycle->state != KZT_GUEST_LIBRARY_BINDING_LIVE) + pending->link_map_addr = 0; + } + result = KZT_GUEST_LIBRARY_BINDING_CANCELLED; + goto out; + } + } + observed = find_observed_key_locked(bindings, key); + if (!observed) { + kzt_guest_library_observed_t *same_addr = + find_observed_locked(bindings, key->link_map_addr); + if (same_addr && !same_addr->retire_owner) + observed = same_addr; + } + if (observed) { + observed->key = *key; + observed->claimed = 0; + } else { + if (bindings->observed_count == bindings->observed_capacity && + grow_array((void **)&bindings->observed, + &bindings->observed_capacity, + sizeof(*bindings->observed)) != 0) { + result = KZT_GUEST_LIBRARY_BINDING_ERROR; + goto out; + } + bindings->observed[bindings->observed_count++] = + (kzt_guest_library_observed_t){ .key = *key }; + observed = &bindings->observed[bindings->observed_count - 1]; + } + for (size_t i = 0; i < bindings->pending_count; ++i) { + kzt_guest_library_pending_t *pending = &bindings->pending[i]; + kzt_guest_library_binding_result_t one; + if (!pending->active || pending->link_map_addr != key->link_map_addr) + continue; + one = bind_locked(bindings, key, pending->library, + pending->object_type); + if (one == KZT_GUEST_LIBRARY_BINDING_ADDED || + one == KZT_GUEST_LIBRARY_BINDING_UNCHANGED) { + pending->active = 0; + observed->claimed = 1; + if (one == KZT_GUEST_LIBRARY_BINDING_ADDED) + result = one; + } else if (one == KZT_GUEST_LIBRARY_BINDING_CONFLICT) { + result = one; + } + } +out: + pthread_mutex_unlock(&bindings->lock); + return result; +} + +static int lookup_bindings(kzt_guest_library_bindings_t *bindings, + const kzt_guest_library_binding_key_t *key, + kzt_guest_library_handle_t *handle) +{ + if (handle) memset(handle, 0, sizeof(*handle)); + if (!bindings || !key || !handle) return -1; + pthread_mutex_lock(&bindings->lock); + if (shutting_down(bindings)) { + pthread_mutex_unlock(&bindings->lock); + return -1; + } + for (size_t i = 0; i < bindings->count; ++i) { + kzt_guest_library_binding_entry_t *entry = &bindings->entries[i]; + kzt_guest_library_lifecycle_t *lifecycle; + if (entry->state != KZT_GUEST_LIBRARY_BINDING_LIVE || + !same_key(&entry->key, key)) + continue; + lifecycle = find_lifecycle_locked(bindings, entry->library); + if (!lifecycle || lifecycle->state != KZT_GUEST_LIBRARY_BINDING_LIVE) + continue; + ++entry->references; + handle->bindings = bindings; + /* A 1-based index remains stable when forced growth reallocates the + * backing array while this handle protects library lifetime. */ + handle->entry = (void *)(uintptr_t)(i + 1); + handle->library = entry->library; + handle->object_type = entry->object_type; + pthread_mutex_unlock(&bindings->lock); + return 0; + } + pthread_mutex_unlock(&bindings->lock); + return -1; +} + +#ifdef KZT_GUEST_LIBRARY_BINDING_TEST +int kzt_guest_library_lookup(kzt_guest_library_bindings_t *bindings, + const kzt_guest_library_binding_key_t *key, + kzt_guest_library_handle_t *handle) +{ + return lookup_bindings(bindings, key, handle); +} +#endif + +int kzt_guest_library_access_lookup( + kzt_guest_library_access_t *access, + const kzt_guest_library_binding_key_t *key, + kzt_guest_library_handle_t *handle) +{ + int result = -1; + if (handle) memset(handle, 0, sizeof(*handle)); + if (!access || !access->initialized || !key || !handle) return -1; + pthread_mutex_lock(&access->lock); + if (access->accepting && access->bindings) + result = lookup_bindings(access->bindings, key, handle); + pthread_mutex_unlock(&access->lock); + return result; +} + +void kzt_guest_library_handle_release(kzt_guest_library_handle_t *handle) +{ + kzt_guest_library_bindings_t *bindings; + if (!handle || !(bindings = handle->bindings) || !handle->entry) return; + pthread_mutex_lock(&bindings->lock); + size_t index = (size_t)(uintptr_t)handle->entry - 1; + if (index < bindings->count) { + kzt_guest_library_binding_entry_t *entry = + &bindings->entries[index]; + if (entry->references && --entry->references == 0) + pthread_cond_broadcast(&bindings->idle); + } + pthread_mutex_unlock(&bindings->lock); + memset(handle, 0, sizeof(*handle)); +} + +static void unload_library(kzt_guest_library_bindings_t *bindings, + kzt_guest_registry_t *registry, + library_t *library, + uintptr_t guest_link_map_hint, int permanent) +{ + kzt_guest_library_lifecycle_t *lifecycle; + int owns_lifecycle = 0; + int waits_for_lifecycle = 0; + if (!bindings || !library) return; + pthread_mutex_lock(&bindings->lock); + + /* Phase 1 closes every binding-side path without allocation. Only the + * caller that changes this library from LIVE to UNLOADING may attach the + * library as owner of an exact, unclaimed observation. Registry + * retirement is deliberately deferred: it may wait for a source lease, + * and a lease holder may need bindings->lock for provider lookup. */ + lifecycle = find_lifecycle_locked(bindings, library); + if (!lifecycle) { + goto out; + } + if (permanent) lifecycle->destroy_started = 1; + if (lifecycle->state != KZT_GUEST_LIBRARY_BINDING_LIVE) { + waits_for_lifecycle = + lifecycle->state == KZT_GUEST_LIBRARY_BINDING_UNLOADING; + goto wait_or_out; + } + lifecycle->state = KZT_GUEST_LIBRARY_BINDING_UNLOADING; + owns_lifecycle = 1; + if (guest_link_map_hint) { + for (size_t i = 0; i < bindings->observed_count; ++i) { + kzt_guest_library_observed_t *observed = + &bindings->observed[i]; + if (observed->key.link_map_addr == guest_link_map_hint && + !observed->claimed && !observed->retire_owner) { + observed->retire_owner = library; + break; + } + } + close_callback_addr_locked(bindings, lifecycle, library, + guest_link_map_hint); + } + for (size_t i = 0; i < bindings->pending_count; ++i) + if (bindings->pending[i].library == library) + bindings->pending[i].active = 0; + for (size_t i = 0; i < bindings->count; ++i) { + kzt_guest_library_binding_entry_t *entry = &bindings->entries[i]; + if (entry->library != library || + entry->state == KZT_GUEST_LIBRARY_BINDING_DEAD) + continue; + entry->state = KZT_GUEST_LIBRARY_BINDING_UNLOADING; + entry->retire_started = 0; + close_callback_addr_locked(bindings, lifecycle, library, + entry->key.link_map_addr); + for (size_t j = 0; j < bindings->observed_count; ++j) + if (same_key(&bindings->observed[j].key, &entry->key)) { + memset(&bindings->observed[j], 0, + sizeof(bindings->observed[j])); + } + } + + /* A callback that entered first may still need this lock for observation + * publication and loader-pair binding, so wait with pthread_cond_wait. + * New callbacks for the closed address are rejected before their first + * guest-memory read. */ + while (callback_access_busy_locked(bindings, lifecycle, library)) + pthread_cond_wait(&bindings->idle, &bindings->lock); + + /* Phase 2 claims one exact key by value, drops bindings->lock, and only + * then enters the registry. Re-scanning after every lock acquisition is + * safe across concurrent array growth/realloc and needs no allocation. */ + for (;;) { + kzt_guest_library_binding_key_t retire_key = { 0 }; + int from_observation = 0; + + if (owns_lifecycle) { + for (size_t i = 0; i < bindings->count; ++i) { + kzt_guest_library_binding_entry_t *entry = + &bindings->entries[i]; + if (entry->library == library && + entry->state == KZT_GUEST_LIBRARY_BINDING_UNLOADING && + !entry->retire_started) { + retire_key = entry->key; + entry->retire_started = 1; + break; + } + } + } + if (!retire_key.link_map_addr) { + for (size_t i = 0; i < bindings->observed_count; ++i) { + kzt_guest_library_observed_t *observed = + &bindings->observed[i]; + if (observed->retire_owner == library && + !observed->retire_started) { + retire_key = observed->key; + observed->retire_started = 1; + from_observation = 1; + break; + } + } + } + if (!retire_key.link_map_addr) + break; + + pthread_mutex_unlock(&bindings->lock); +#ifdef KZT_GUEST_LIBRARY_BINDING_TEST + if (test_before_registry_retire) + test_before_registry_retire( + bindings, &retire_key, library, from_observation, + test_before_registry_retire_opaque); +#endif + if (!registry) { + fprintf(stderr, + "KZT binding retire unavailable (registry missing): link_map=%p generation=%lu; continuing legacy flow\n", + (void *)retire_key.link_map_addr, + retire_key.generation); + pthread_mutex_lock(&bindings->lock); + ++bindings->diagnostics.registry_missing; + pthread_mutex_unlock(&bindings->lock); + } else if (kzt_guest_registry_retire( + registry, retire_key.link_map_addr, + retire_key.generation) != 0 && + kzt_guest_registry_wait_retired( + registry, retire_key.link_map_addr, + retire_key.generation) != 0) { + /* An exact generation already in UNLOADING is waited to DEAD by + * wait_retired(). Disabled/missing/replaced/unprovable state is + * a new-KZT failure and must leave the legacy process alive. */ + fprintf(stderr, + "KZT binding retire state unprovable: link_map=%p generation=%lu; continuing legacy flow\n", + (void *)retire_key.link_map_addr, + retire_key.generation); + pthread_mutex_lock(&bindings->lock); + ++bindings->diagnostics.retire_unprovable; + pthread_mutex_unlock(&bindings->lock); + } + pthread_mutex_lock(&bindings->lock); + + if (from_observation) { + for (size_t i = 0; i < bindings->observed_count; ++i) { + kzt_guest_library_observed_t *observed = + &bindings->observed[i]; + if (observed->retire_owner == library && + observed->retire_started && + same_key(&observed->key, &retire_key)) { + memset(observed, 0, sizeof(*observed)); + break; + } + } + } + } + +wait_or_out: + if (!owns_lifecycle) { + while (waits_for_lifecycle) { + lifecycle = find_lifecycle_locked(bindings, library); + if (!lifecycle || + lifecycle->state != KZT_GUEST_LIBRARY_BINDING_UNLOADING) + break; +#ifdef KZT_GUEST_LIBRARY_BINDING_TEST + if (test_before_lifecycle_wait) + test_before_lifecycle_wait( + bindings, library, test_before_lifecycle_wait_opaque); +#endif + pthread_cond_wait(&bindings->idle, &bindings->lock); + } + goto out; + } + + /* Phase 3 waits only for binding handles. Handle release takes + * bindings->lock but no registry or access lock. */ + for (;;) { + int busy = 0; + for (size_t i = 0; i < bindings->count; ++i) + if (bindings->entries[i].library == library && + bindings->entries[i].references) + busy = 1; + if (!busy) break; + pthread_cond_wait(&bindings->idle, &bindings->lock); + } + for (size_t i = 0; i < bindings->count; ++i) + if (bindings->entries[i].library == library) + bindings->entries[i].state = KZT_GUEST_LIBRARY_BINDING_DEAD; + lifecycle = find_lifecycle_locked(bindings, library); + if (lifecycle) + lifecycle->state = KZT_GUEST_LIBRARY_BINDING_DEAD; + pthread_cond_broadcast(&bindings->idle); +out: + pthread_mutex_unlock(&bindings->lock); +} + +#ifdef KZT_GUEST_LIBRARY_BINDING_TEST +int kzt_guest_library_binding_test_get_diagnostics( + kzt_guest_library_bindings_t *bindings, + unsigned long *registry_missing, + unsigned long *retire_unprovable) +{ + if (registry_missing) *registry_missing = 0; + if (retire_unprovable) *retire_unprovable = 0; + if (!bindings) return -1; + pthread_mutex_lock(&bindings->lock); + if (registry_missing) + *registry_missing = bindings->diagnostics.registry_missing; + if (retire_unprovable) + *retire_unprovable = bindings->diagnostics.retire_unprovable; + pthread_mutex_unlock(&bindings->lock); + return 0; +} + +int kzt_guest_library_binding_test_snapshot( + kzt_guest_library_bindings_t *bindings, library_t *library, + kzt_guest_library_binding_state_t *lifecycle_state, + size_t *active_pending, size_t *live_entries) +{ + kzt_guest_library_lifecycle_t *lifecycle; + if (lifecycle_state) + *lifecycle_state = KZT_GUEST_LIBRARY_BINDING_DEAD; + if (active_pending) *active_pending = 0; + if (live_entries) *live_entries = 0; + if (!bindings || !library) return -1; + pthread_mutex_lock(&bindings->lock); + lifecycle = find_lifecycle_locked(bindings, library); + if (lifecycle_state && lifecycle) + *lifecycle_state = lifecycle->state; + for (size_t i = 0; i < bindings->pending_count; ++i) + if (bindings->pending[i].library == library && + bindings->pending[i].active && active_pending) + ++*active_pending; + for (size_t i = 0; i < bindings->count; ++i) + if (bindings->entries[i].library == library && + bindings->entries[i].state == KZT_GUEST_LIBRARY_BINDING_LIVE && + live_entries) + ++*live_entries; + pthread_mutex_unlock(&bindings->lock); + return lifecycle ? 0 : -1; +} +#endif + +void kzt_guest_library_unbind(kzt_guest_library_bindings_t *bindings, + kzt_guest_registry_t *registry, + library_t *library, + uintptr_t guest_link_map_hint) +{ + unload_library(bindings, registry, library, guest_link_map_hint, 1); +} + +void kzt_guest_library_inactivate(kzt_guest_library_bindings_t *bindings, + kzt_guest_registry_t *registry, + library_t *library, + uintptr_t guest_link_map_hint) +{ + unload_library(bindings, registry, library, guest_link_map_hint, 0); +} diff --git a/target/i386/latx/context/kzt_guest_link_map_reader.c b/target/i386/latx/context/kzt_guest_link_map_reader.c new file mode 100644 index 00000000000..3e5207942d8 --- /dev/null +++ b/target/i386/latx/context/kzt_guest_link_map_reader.c @@ -0,0 +1,387 @@ +#include +#include +#include +#include + +#include "kzt_guest_link_map_reader.h" + +#define KZT_GUEST_LINK_MAP_WALK_LIMIT 256 + +/* The public x86_64 link_map prefix is stable. Fields after l_prev are glibc + * implementation details and must not be used as registry evidence. */ +typedef struct kzt_guest_link_map_public_prefix { + uint64_t l_addr; + uint64_t l_name; + uint64_t l_ld; + uint64_t l_next; + uint64_t l_prev; +} kzt_guest_link_map_public_prefix_t; + +#ifdef KZT_GUEST_LINK_MAP_READER_TEST +static long test_alloc_failure_after = -1; + +void kzt_guest_link_map_reader_test_set_alloc_failure_after(long allocations) +{ + test_alloc_failure_after = allocations; +} +#endif + +static void *kzt_link_map_reader_malloc(size_t size) +{ +#ifdef KZT_GUEST_LINK_MAP_READER_TEST + if (test_alloc_failure_after == 0) { + return NULL; + } + if (test_alloc_failure_after > 0) { + --test_alloc_failure_after; + } +#endif + return malloc(size); +} + +static void kzt_link_map_reader_free(void *ptr) +{ + free(ptr); +} + +static void kzt_guest_scalar_set_unknown(kzt_guest_scalar_field_t *field) +{ + field->value = 0; + field->status = KZT_GUEST_FIELD_UNKNOWN; +} + +static void kzt_guest_string_set_unknown(kzt_guest_string_field_t *field) +{ + field->value = NULL; + field->status = KZT_GUEST_FIELD_UNKNOWN; +} + +static void kzt_guest_observation_init(kzt_guest_object_observation_t *observation) +{ + memset(observation, 0, sizeof(*observation)); + kzt_guest_scalar_set_unknown(&observation->load_bias); + kzt_guest_scalar_set_unknown(&observation->dynamic_addr); + kzt_guest_scalar_set_unknown(&observation->map_start); + kzt_guest_scalar_set_unknown(&observation->map_end); + kzt_guest_scalar_set_unknown(&observation->namespace_id); + kzt_guest_string_set_unknown(&observation->path); + observation->soname.value = NULL; + observation->soname.status = KZT_GUEST_FIELD_NOT_PARSED; + observation->dynamic_view_status = KZT_GUEST_FIELD_NOT_PARSED; +} + +static int kzt_guest_read_memory(uintptr_t guest_addr, + void *dst, + size_t size, + const kzt_guest_link_map_reader_ops_t *ops) +{ + if (!ops || !ops->read_memory) { + return -1; + } + return ops->read_memory(guest_addr, dst, size, ops->opaque) == 0 ? 0 : -1; +} + +static int kzt_guest_field_addr(uintptr_t base, size_t offset, uintptr_t *addr) +{ + if (base > UINTPTR_MAX - offset) { + return -1; + } + + *addr = base + offset; + return 0; +} + +static int kzt_guest_read_uintptr_field( + uintptr_t base, + size_t offset, + size_t field_size, + const kzt_guest_link_map_reader_ops_t *ops, + uintptr_t *value) +{ + uintptr_t addr; + uint64_t raw = 0; + + if (field_size > sizeof(raw) || + kzt_guest_field_addr(base, offset, &addr) != 0) { + return -1; + } + + if (kzt_guest_read_memory(addr, &raw, field_size, ops) != 0) { + return -1; + } + + *value = (uintptr_t)raw; + return 0; +} + +static void kzt_guest_read_scalar_field( + uintptr_t base, + size_t offset, + size_t field_size, + const kzt_guest_link_map_reader_ops_t *ops, + kzt_guest_scalar_field_t *field) +{ + uintptr_t value = 0; + + field->value = 0; + if (kzt_guest_read_uintptr_field(base, offset, field_size, ops, &value) != 0) { + field->status = KZT_GUEST_FIELD_READ_ERROR; + return; + } + + field->value = value; + field->status = KZT_GUEST_FIELD_OK; +} + +static int kzt_guest_read_link_map_pointer( + uintptr_t link_map_addr, + size_t offset, + size_t field_size, + const kzt_guest_link_map_reader_ops_t *ops, + uintptr_t *value) +{ + *value = 0; + return kzt_guest_read_uintptr_field(link_map_addr, offset, field_size, + ops, value); +} + +int kzt_guest_link_map_read_identity( + uintptr_t link_map_addr, + const kzt_guest_link_map_reader_ops_t *ops, + kzt_guest_link_map_identity_t *identity) +{ + if (!identity) { + return -1; + } + memset(identity, 0, sizeof(*identity)); + if (!link_map_addr || !ops || !ops->read_memory || + kzt_guest_read_uintptr_field( + link_map_addr, + offsetof(kzt_guest_link_map_public_prefix_t, l_addr), + sizeof(((kzt_guest_link_map_public_prefix_t *)0)->l_addr), + ops, &identity->load_bias) != 0 || + kzt_guest_read_uintptr_field( + link_map_addr, + offsetof(kzt_guest_link_map_public_prefix_t, l_ld), + sizeof(((kzt_guest_link_map_public_prefix_t *)0)->l_ld), + ops, &identity->dynamic_addr) != 0) { + memset(identity, 0, sizeof(*identity)); + return -1; + } + return 0; +} + +int kzt_guest_link_map_identity_matches( + const kzt_guest_link_map_identity_t *identity, + uintptr_t expected_load_bias, + uintptr_t expected_dynamic_addr) +{ + return identity && expected_dynamic_addr && + identity->load_bias == expected_load_bias && + identity->dynamic_addr == expected_dynamic_addr; +} + +int kzt_guest_link_map_read_predecessor( + uintptr_t link_map_addr, + const kzt_guest_link_map_reader_ops_t *ops, + uintptr_t *predecessor) +{ + if (!predecessor) { + return -1; + } + *predecessor = 0; + if (!link_map_addr || !ops || !ops->read_memory) { + return -1; + } + return kzt_guest_read_uintptr_field( + link_map_addr, + offsetof(kzt_guest_link_map_public_prefix_t, l_prev), + sizeof(((kzt_guest_link_map_public_prefix_t *)0)->l_prev), + ops, predecessor); +} + +int kzt_guest_link_map_classify_namespace( + uintptr_t link_map_addr, + const kzt_guest_link_map_identity_t *main_identity, + uintptr_t confirmed_main_head, + const kzt_guest_link_map_reader_ops_t *ops, + uintptr_t *namespace_head) +{ + uintptr_t visited[KZT_GUEST_LINK_MAP_WALK_LIMIT]; + uintptr_t current = link_map_addr; + size_t count = 0; + + if (namespace_head) { + *namespace_head = 0; + } + + if (!current || !ops || !ops->read_memory || + (!confirmed_main_head && + (!main_identity || !main_identity->dynamic_addr))) { + return -1; + } + + while (count < KZT_GUEST_LINK_MAP_WALK_LIMIT) { + uintptr_t previous = 0; + size_t i; + + for (i = 0; i < count; ++i) { + if (visited[i] == current) { + return -1; + } + } + visited[count++] = current; + if (kzt_guest_read_uintptr_field( + current, + offsetof(kzt_guest_link_map_public_prefix_t, l_prev), + sizeof(((kzt_guest_link_map_public_prefix_t *)0)->l_prev), + ops, &previous) != 0) { + return -1; + } + + if (!previous) { + kzt_guest_link_map_identity_t head_identity; + + if (namespace_head) { + *namespace_head = current; + } + if (confirmed_main_head) { + return current == confirmed_main_head ? 1 : 0; + } + if (kzt_guest_link_map_read_identity(current, ops, + &head_identity) != 0) { + if (namespace_head) { + *namespace_head = 0; + } + return -1; + } + return kzt_guest_link_map_identity_matches( + &head_identity, main_identity->load_bias, + main_identity->dynamic_addr) ? 1 : 0; + } + current = previous; + } + + return -1; +} + +int kzt_guest_link_map_read_name_snapshot( + uintptr_t guest_name_addr, + const kzt_guest_link_map_reader_ops_t *ops, + size_t max_len, + kzt_guest_string_field_t *name) +{ + char *snapshot; + size_t i; + + if (!name) { + return -1; + } + + kzt_guest_string_set_unknown(name); + if (!guest_name_addr) { + return 0; + } + + if (max_len == 0) { + name->status = KZT_GUEST_FIELD_TRUNCATED; + return 0; + } + + snapshot = kzt_link_map_reader_malloc(max_len + 1); + if (!snapshot) { + name->status = KZT_GUEST_FIELD_READ_ERROR; + return 0; + } + + for (i = 0; i < max_len; ++i) { + if (kzt_guest_read_memory(guest_name_addr + i, &snapshot[i], 1, ops) != 0) { + kzt_link_map_reader_free(snapshot); + name->status = KZT_GUEST_FIELD_READ_ERROR; + return 0; + } + + if (snapshot[i] == '\0') { + name->value = snapshot; + name->status = KZT_GUEST_FIELD_OK; + return 0; + } + } + + snapshot[max_len] = '\0'; + name->value = snapshot; + name->status = KZT_GUEST_FIELD_TRUNCATED; + return 0; +} + +int kzt_guest_link_map_read_observation( + uintptr_t link_map_addr, + const kzt_guest_link_map_reader_ops_t *ops, + kzt_guest_object_observation_t *observation) +{ + uintptr_t guest_name_addr = 0; + int name_ptr_read; + + if (!observation) { + return -1; + } + + kzt_guest_observation_init(observation); + + if (!link_map_addr || !ops || !ops->read_memory) { + return -1; + } + + observation->link_map_addr = link_map_addr; + kzt_guest_read_scalar_field( + link_map_addr, + offsetof(kzt_guest_link_map_public_prefix_t, l_addr), + sizeof(((kzt_guest_link_map_public_prefix_t *)0)->l_addr), ops, + &observation->load_bias); + kzt_guest_read_scalar_field( + link_map_addr, + offsetof(kzt_guest_link_map_public_prefix_t, l_ld), + sizeof(((kzt_guest_link_map_public_prefix_t *)0)->l_ld), ops, + &observation->dynamic_addr); + + name_ptr_read = kzt_guest_read_link_map_pointer( + link_map_addr, + offsetof(kzt_guest_link_map_public_prefix_t, l_name), + sizeof(((kzt_guest_link_map_public_prefix_t *)0)->l_name), ops, + &guest_name_addr); + if (name_ptr_read != 0) { + observation->path.value = NULL; + observation->path.status = KZT_GUEST_FIELD_READ_ERROR; + return 0; + } + + if (kzt_guest_link_map_read_name_snapshot( + guest_name_addr, ops, KZT_GUEST_LINK_MAP_NAME_LIMIT, + &observation->path) != 0) { + return -1; + } + + return 0; +} + +void kzt_guest_link_map_observation_clear( + kzt_guest_object_observation_t *observation) +{ + if (!observation) { + return; + } + + kzt_guest_link_map_string_clear(&observation->path); + kzt_guest_link_map_string_clear(&observation->soname); + kzt_guest_observation_init(observation); +} + +void kzt_guest_link_map_string_clear(kzt_guest_string_field_t *field) +{ + if (!field) { + return; + } + + kzt_link_map_reader_free((void *)field->value); + kzt_guest_string_set_unknown(field); +} diff --git a/target/i386/latx/context/kzt_guest_registry.c b/target/i386/latx/context/kzt_guest_registry.c new file mode 100644 index 00000000000..6f374ced3ee --- /dev/null +++ b/target/i386/latx/context/kzt_guest_registry.c @@ -0,0 +1,1551 @@ +#include +#include +#include +#include +#include +#include + +#include "kzt_guest_registry.h" + +#define KZT_GUEST_REGISTRY_INITIAL_CAPACITY 8 + +struct kzt_guest_registry { + pthread_mutex_t lock; + pthread_cond_t leases_idle; + int lock_ready; + int disabled; + int destroying; + unsigned long active_api_users; + kzt_guest_object_snapshot_t *objects; + size_t count; + size_t capacity; + unsigned long next_generation; + kzt_guest_registry_diagnostics_t diagnostics; + kzt_guest_registry_diagnostic_config_t diagnostic_config; + kzt_guest_registry_event_summary_t diagnostic_events[ + KZT_GUEST_REGISTRY_RESULT_COUNT]; +}; + +#ifdef KZT_GUEST_REGISTRY_TEST +static kzt_guest_registry_test_hook_fn test_after_api_enter; +static void *test_after_api_enter_opaque; +static kzt_guest_registry_test_hook_fn test_before_retire_wait; +static void *test_before_retire_wait_opaque; +static kzt_guest_registry_test_hook_fn test_after_retire_wake; +static void *test_after_retire_wake_opaque; +static kzt_guest_registry_test_hook_fn test_after_destroy_disable; +static void *test_after_destroy_disable_opaque; +#endif + +static int kzt_registry_api_enter(kzt_guest_registry_t *registry) +{ + if (!registry) { + return -1; + } + + __atomic_add_fetch(®istry->active_api_users, 1, __ATOMIC_ACQ_REL); +#ifdef KZT_GUEST_REGISTRY_TEST + if (test_after_api_enter) { + test_after_api_enter(test_after_api_enter_opaque); + } +#endif + if (__atomic_load_n(®istry->destroying, __ATOMIC_ACQUIRE)) { + pthread_mutex_lock(®istry->lock); + if (__atomic_sub_fetch(®istry->active_api_users, 1, + __ATOMIC_ACQ_REL) == 0) { + pthread_cond_broadcast(®istry->leases_idle); + } + pthread_mutex_unlock(®istry->lock); + return -1; + } + + return 0; +} + +static int kzt_registry_api_lock(kzt_guest_registry_t *registry) +{ + if (kzt_registry_api_enter(registry) != 0) { + return -1; + } + pthread_mutex_lock(®istry->lock); + return 0; +} + +static void kzt_registry_api_unlock(kzt_guest_registry_t *registry) +{ + if (__atomic_sub_fetch(®istry->active_api_users, 1, + __ATOMIC_ACQ_REL) == 0) { + pthread_cond_broadcast(®istry->leases_idle); + } + pthread_mutex_unlock(®istry->lock); +} + +static void kzt_registry_api_leave(kzt_guest_registry_t *registry) +{ + pthread_mutex_lock(®istry->lock); + kzt_registry_api_unlock(registry); +} + +#ifdef KZT_GUEST_REGISTRY_TEST +static long test_alloc_failure_after = -1; +static long test_dynamic_commit_failure_after = -1; +static int test_fail_next_cond_init; + +void kzt_guest_registry_test_set_after_api_enter( + kzt_guest_registry_test_hook_fn hook, void *opaque) +{ + test_after_api_enter = hook; + test_after_api_enter_opaque = opaque; +} + +void kzt_guest_registry_test_set_after_retire_wake( + kzt_guest_registry_test_hook_fn hook, void *opaque) +{ + test_after_retire_wake = hook; + test_after_retire_wake_opaque = opaque; +} + +void kzt_guest_registry_test_set_before_retire_wait( + kzt_guest_registry_test_hook_fn hook, void *opaque) +{ + test_before_retire_wait = hook; + test_before_retire_wait_opaque = opaque; +} + +void kzt_guest_registry_test_set_after_destroy_disable( + kzt_guest_registry_test_hook_fn hook, void *opaque) +{ + test_after_destroy_disable = hook; + test_after_destroy_disable_opaque = opaque; +} + +void kzt_guest_registry_test_set_alloc_failure_after(long allocations) +{ + test_alloc_failure_after = allocations; +} + +void kzt_guest_registry_test_set_dynamic_commit_failure_after(long commits) +{ + test_dynamic_commit_failure_after = commits; +} + +void kzt_guest_registry_test_fail_next_cond_init(void) +{ + test_fail_next_cond_init = 1; +} + +static int kzt_registry_test_should_fail_dynamic_commit(void) +{ + if (test_dynamic_commit_failure_after == 0) { + return 1; + } + if (test_dynamic_commit_failure_after > 0) { + --test_dynamic_commit_failure_after; + } + return 0; +} +#endif + +static void *kzt_registry_calloc(size_t count, size_t size) +{ +#ifdef KZT_GUEST_REGISTRY_TEST + if (test_alloc_failure_after == 0) { + return NULL; + } + if (test_alloc_failure_after > 0) { + --test_alloc_failure_after; + } +#endif + return calloc(count, size); +} + +static void kzt_registry_free(void *ptr) +{ + free(ptr); +} + +static char *kzt_registry_strdup(const char *value) +{ + size_t len; + char *copy; + + if (!value) { + value = ""; + } + + len = strlen(value); + copy = kzt_registry_calloc(len + 1, 1); + if (!copy) { + return NULL; + } + + memcpy(copy, value, len); + return copy; +} + +static int kzt_field_is_reliable(kzt_guest_field_status_t status) +{ + return status == KZT_GUEST_FIELD_OK; +} + +static int kzt_string_status_has_snapshot(kzt_guest_field_status_t status) +{ + return status == KZT_GUEST_FIELD_OK || + status == KZT_GUEST_FIELD_TRUNCATED; +} + +static void kzt_free_string_field(kzt_guest_string_field_t *field) +{ + if (!field) { + return; + } + + kzt_registry_free((void *)field->value); + field->value = NULL; +} + +static int kzt_copy_string_field(kzt_guest_string_field_t *dst, + const kzt_guest_string_field_t *src) +{ + dst->status = src->status; + dst->value = NULL; + + if (!kzt_string_status_has_snapshot(src->status)) { + return 0; + } + + dst->value = kzt_registry_strdup(src->value); + return dst->value ? 0 : -1; +} + +static int kzt_dynamic_field_equal( + const kzt_guest_dynamic_field_t *left, + const kzt_guest_dynamic_field_t *right) +{ + return left->present == right->present && + left->value == right->value && + left->address_semantics == right->address_semantics; +} + +static int kzt_dynamic_needed_equal( + const kzt_guest_dynamic_view_t *left, + const kzt_guest_dynamic_view_t *right) +{ + size_t i; + + if (left->needed_count != right->needed_count) { + return 0; + } + + if (left->needed_count > 0 && + left->needed_address_semantics != right->needed_address_semantics) { + return 0; + } + + for (i = 0; i < left->needed_count; ++i) { + if (left->needed_offsets[i] != right->needed_offsets[i]) { + return 0; + } + } + + return 1; +} + +static int kzt_dynamic_view_equal( + const kzt_guest_dynamic_view_t *left, + const kzt_guest_dynamic_view_t *right) +{ + return left->dynamic_addr == right->dynamic_addr && + left->load_bias == right->load_bias && + left->status == right->status && + left->entry_count == right->entry_count && + left->has_null == right->has_null && + left->scan_limit == right->scan_limit && + left->unknown_tag_count == right->unknown_tag_count && + left->first_unknown_tag == right->first_unknown_tag && + left->first_unknown_tag_index == right->first_unknown_tag_index && + kzt_dynamic_field_equal(&left->symtab, &right->symtab) && + kzt_dynamic_field_equal(&left->strtab, &right->strtab) && + kzt_dynamic_field_equal(&left->syment, &right->syment) && + kzt_dynamic_field_equal(&left->strsz, &right->strsz) && + kzt_dynamic_field_equal(&left->hash, &right->hash) && + kzt_dynamic_field_equal(&left->gnu_hash, &right->gnu_hash) && + kzt_dynamic_field_equal(&left->versym, &right->versym) && + kzt_dynamic_field_equal(&left->verneed, &right->verneed) && + kzt_dynamic_field_equal(&left->verneednum, &right->verneednum) && + kzt_dynamic_field_equal(&left->verdef, &right->verdef) && + kzt_dynamic_field_equal(&left->verdefnum, &right->verdefnum) && + kzt_dynamic_field_equal(&left->rela, &right->rela) && + kzt_dynamic_field_equal(&left->relasz, &right->relasz) && + kzt_dynamic_field_equal(&left->relaent, &right->relaent) && + kzt_dynamic_field_equal(&left->rel, &right->rel) && + kzt_dynamic_field_equal(&left->relsz, &right->relsz) && + kzt_dynamic_field_equal(&left->relent, &right->relent) && + kzt_dynamic_field_equal(&left->jmprel, &right->jmprel) && + kzt_dynamic_field_equal(&left->pltrelsz, &right->pltrelsz) && + kzt_dynamic_field_equal(&left->pltrel, &right->pltrel) && + kzt_dynamic_field_equal(&left->pltgot, &right->pltgot) && + kzt_dynamic_needed_equal(left, right); +} + +static kzt_guest_field_status_t kzt_dynamic_view_field_status( + const kzt_guest_dynamic_view_t *view) +{ + switch (view->status) { + case KZT_GUEST_DYNAMIC_COMPLETE: + return KZT_GUEST_FIELD_OK; + case KZT_GUEST_DYNAMIC_TRUNCATED_NO_NULL: + return KZT_GUEST_FIELD_TRUNCATED; + case KZT_GUEST_DYNAMIC_READ_ERROR: + return KZT_GUEST_FIELD_READ_ERROR; + case KZT_GUEST_DYNAMIC_ERROR: + return KZT_GUEST_FIELD_READ_ERROR; + } + + return KZT_GUEST_FIELD_UNKNOWN; +} + +static void kzt_free_snapshot_strings(kzt_guest_object_snapshot_t *snapshot) +{ + if (!snapshot) { + return; + } + + kzt_free_string_field(&snapshot->path); + kzt_free_string_field(&snapshot->soname); + memset(&snapshot->dynamic_view, 0, sizeof(snapshot->dynamic_view)); +} + +static void kzt_free_snapshot_array(kzt_guest_object_snapshot_t *objects, + size_t count) +{ + size_t i; + + if (!objects) { + return; + } + + for (i = 0; i < count; ++i) { + kzt_free_snapshot_strings(&objects[i]); + } + kzt_registry_free(objects); +} + +static int kzt_copy_snapshot(kzt_guest_object_snapshot_t *dst, + const kzt_guest_object_snapshot_t *src) +{ + *dst = *src; + dst->path.value = NULL; + dst->soname.value = NULL; + + if (kzt_copy_string_field(&dst->path, &src->path) != 0) { + return -1; + } + if (kzt_copy_string_field(&dst->soname, &src->soname) != 0) { + kzt_free_string_field(&dst->path); + return -1; + } + + return 0; +} + +static int kzt_snapshot_from_observation( + kzt_guest_object_snapshot_t *snapshot, + const kzt_guest_object_observation_t *observation, + unsigned long generation) +{ + memset(snapshot, 0, sizeof(*snapshot)); + + snapshot->link_map_addr = observation->link_map_addr; + snapshot->load_bias = observation->load_bias; + snapshot->dynamic_addr = observation->dynamic_addr; + snapshot->map_start = observation->map_start; + snapshot->map_end = observation->map_end; + snapshot->namespace_id = observation->namespace_id; + snapshot->dynamic_view_status = observation->dynamic_view_status; + snapshot->state = KZT_GUEST_OBJECT_DISCOVERED; + snapshot->generation = generation; + + if (kzt_copy_string_field(&snapshot->path, &observation->path) != 0) { + return -1; + } + if (kzt_copy_string_field(&snapshot->soname, &observation->soname) != 0) { + kzt_free_string_field(&snapshot->path); + return -1; + } + + return 0; +} + +static int kzt_scalar_conflicts(const kzt_guest_scalar_field_t *current, + const kzt_guest_scalar_field_t *incoming) +{ + return kzt_field_is_reliable(current->status) && + kzt_field_is_reliable(incoming->status) && + current->value != incoming->value; +} + +static int kzt_string_conflicts(const kzt_guest_string_field_t *current, + const kzt_guest_string_field_t *incoming) +{ + const char *left; + const char *right; + + if (!kzt_field_is_reliable(current->status) || + !kzt_field_is_reliable(incoming->status)) { + return 0; + } + + left = current->value ? current->value : ""; + right = incoming->value ? incoming->value : ""; + return strcmp(left, right) != 0; +} + +static int kzt_update_scalar_field(kzt_guest_scalar_field_t *current, + const kzt_guest_scalar_field_t *incoming) +{ + if (kzt_field_is_reliable(current->status) || + !kzt_field_is_reliable(incoming->status)) { + return 0; + } + + *current = *incoming; + return 1; +} + +static int kzt_update_string_field(kzt_guest_string_field_t *current, + const kzt_guest_string_field_t *incoming) +{ + char *copy; + + if (kzt_field_is_reliable(current->status) || + !kzt_field_is_reliable(incoming->status)) { + return 0; + } + + copy = kzt_registry_strdup(incoming->value); + if (!copy) { + return -1; + } + + kzt_free_string_field(current); + current->value = copy; + current->status = incoming->status; + return 1; +} + +static int kzt_update_status_field(kzt_guest_field_status_t *current, + kzt_guest_field_status_t incoming) +{ + if (kzt_field_is_reliable(*current) || + !kzt_field_is_reliable(incoming)) { + return 0; + } + + *current = incoming; + return 1; +} + +static int kzt_observation_conflicts( + const kzt_guest_object_snapshot_t *current, + const kzt_guest_object_observation_t *incoming) +{ + return kzt_scalar_conflicts(¤t->load_bias, + &incoming->load_bias) || + kzt_scalar_conflicts(¤t->dynamic_addr, + &incoming->dynamic_addr) || + kzt_scalar_conflicts(¤t->map_start, + &incoming->map_start) || + kzt_scalar_conflicts(¤t->map_end, + &incoming->map_end) || + kzt_scalar_conflicts(¤t->namespace_id, + &incoming->namespace_id) || + kzt_string_conflicts(¤t->path, &incoming->path) || + kzt_string_conflicts(¤t->soname, &incoming->soname); +} + +static int kzt_update_snapshot(kzt_guest_object_snapshot_t *current, + const kzt_guest_object_observation_t *incoming, + int *updated) +{ + int ret; + + *updated |= kzt_update_scalar_field(¤t->load_bias, + &incoming->load_bias); + *updated |= kzt_update_scalar_field(¤t->dynamic_addr, + &incoming->dynamic_addr); + *updated |= kzt_update_scalar_field(¤t->map_start, + &incoming->map_start); + *updated |= kzt_update_scalar_field(¤t->map_end, + &incoming->map_end); + *updated |= kzt_update_scalar_field(¤t->namespace_id, + &incoming->namespace_id); + + ret = kzt_update_string_field(¤t->path, &incoming->path); + if (ret < 0) { + return -1; + } + *updated |= ret; + + ret = kzt_update_string_field(¤t->soname, &incoming->soname); + if (ret < 0) { + return -1; + } + *updated |= ret; + + *updated |= kzt_update_status_field(¤t->dynamic_view_status, + incoming->dynamic_view_status); + return 0; +} + +static ssize_t kzt_find_object_index(kzt_guest_registry_t *registry, + uintptr_t link_map_addr) +{ + size_t i; + + for (i = 0; i < registry->count; ++i) { + if (registry->objects[i].link_map_addr == link_map_addr) { + return (ssize_t)i; + } + } + + return -1; +} + +static int kzt_registry_ensure_capacity(kzt_guest_registry_t *registry) +{ + kzt_guest_object_snapshot_t *objects; + size_t new_capacity; + + if (registry->count < registry->capacity) { + return 0; + } + + new_capacity = registry->capacity ? + registry->capacity * 2 : KZT_GUEST_REGISTRY_INITIAL_CAPACITY; + objects = kzt_registry_calloc(new_capacity, sizeof(*objects)); + if (!objects) { + ++registry->diagnostics.allocation_failures; + return -1; + } + + if (registry->objects) { + memcpy(objects, registry->objects, + registry->count * sizeof(*registry->objects)); + kzt_registry_free(registry->objects); + } + + registry->objects = objects; + registry->capacity = new_capacity; + return 0; +} + +static const char *kzt_registry_result_name(kzt_guest_registry_result_t result) +{ + switch (result) { + case KZT_GUEST_REGISTRY_ADDED: + return "added"; + case KZT_GUEST_REGISTRY_UNCHANGED: + return "unchanged"; + case KZT_GUEST_REGISTRY_UPDATED: + return "updated"; + case KZT_GUEST_REGISTRY_CONFLICT: + return "conflict"; + case KZT_GUEST_REGISTRY_DISABLED: + return "disabled"; + case KZT_GUEST_REGISTRY_ERROR: + return "error"; + case KZT_GUEST_REGISTRY_RESULT_COUNT: + break; + } + + return "unknown"; +} + +static const char *kzt_guest_field_status_name( + kzt_guest_field_status_t status) +{ + switch (status) { + case KZT_GUEST_FIELD_OK: + return "ok"; + case KZT_GUEST_FIELD_UNKNOWN: + return "unknown"; + case KZT_GUEST_FIELD_READ_ERROR: + return "read_error"; + case KZT_GUEST_FIELD_TRUNCATED: + return "truncated"; + case KZT_GUEST_FIELD_NOT_PARSED: + return "not_parsed"; + } + + return "invalid"; +} + +static void kzt_registry_note_counter(kzt_guest_registry_t *registry, + kzt_guest_registry_result_t result) +{ + switch (result) { + case KZT_GUEST_REGISTRY_ADDED: + ++registry->diagnostics.added; + break; + case KZT_GUEST_REGISTRY_UNCHANGED: + ++registry->diagnostics.unchanged; + break; + case KZT_GUEST_REGISTRY_UPDATED: + ++registry->diagnostics.updated; + break; + case KZT_GUEST_REGISTRY_CONFLICT: + ++registry->diagnostics.conflicts; + break; + case KZT_GUEST_REGISTRY_DISABLED: + ++registry->diagnostics.disabled; + break; + case KZT_GUEST_REGISTRY_ERROR: + ++registry->diagnostics.errors; + break; + case KZT_GUEST_REGISTRY_RESULT_COUNT: + break; + } +} + +static void kzt_registry_init_empty_diagnostic( + kzt_guest_registry_observation_diagnostic_t *diagnostic, + kzt_guest_registry_result_t result, + uintptr_t link_map_addr) +{ + if (!diagnostic) { + return; + } + + memset(diagnostic, 0, sizeof(*diagnostic)); + diagnostic->result = result; + diagnostic->link_map_addr = link_map_addr; +} + +static void kzt_registry_note_result( + kzt_guest_registry_t *registry, + kzt_guest_registry_result_t result, + uintptr_t link_map_addr, + unsigned long generation, + kzt_guest_registry_observation_diagnostic_t *diagnostic) +{ + kzt_guest_registry_event_summary_t *event = NULL; + int emitted = 0; + + kzt_registry_note_counter(registry, result); + + if (registry->diagnostic_config.enabled && + result < KZT_GUEST_REGISTRY_RESULT_COUNT) { + event = ®istry->diagnostic_events[result]; + event->result = result; + ++event->observed; + event->last_link_map_addr = link_map_addr; + event->last_generation = generation; + if (event->emitted < registry->diagnostic_config.throttle_limit) { + ++event->emitted; + emitted = 1; + } else { + ++event->suppressed; + } + } + + if (!diagnostic) { + return; + } + + memset(diagnostic, 0, sizeof(*diagnostic)); + diagnostic->enabled = registry->diagnostic_config.enabled; + diagnostic->emitted = emitted; + diagnostic->result = result; + diagnostic->link_map_addr = link_map_addr; + diagnostic->generation = generation; + diagnostic->object_count = registry->count; + diagnostic->counters = registry->diagnostics; + if (event) { + diagnostic->result_observations = event->observed; + diagnostic->result_suppressed = event->suppressed; + } +} + +kzt_guest_registry_t *kzt_guest_registry_init(void) +{ + kzt_guest_registry_t *registry; + + registry = kzt_registry_calloc(1, sizeof(*registry)); + if (!registry) { + return NULL; + } + + if (pthread_mutex_init(®istry->lock, NULL) != 0) { + kzt_registry_free(registry); + return NULL; + } + + registry->lock_ready = 1; +#ifdef KZT_GUEST_REGISTRY_TEST + if (test_fail_next_cond_init) { + test_fail_next_cond_init = 0; + pthread_mutex_destroy(®istry->lock); + kzt_registry_free(registry); + return NULL; + } +#endif + if (pthread_cond_init(®istry->leases_idle, NULL) != 0) { + pthread_mutex_destroy(®istry->lock); + kzt_registry_free(registry); + return NULL; + } + registry->next_generation = 1; + registry->capacity = KZT_GUEST_REGISTRY_INITIAL_CAPACITY; + registry->objects = kzt_registry_calloc(registry->capacity, + sizeof(*registry->objects)); + if (!registry->objects) { + registry->capacity = 0; + registry->disabled = 1; + ++registry->diagnostics.init_failures; + ++registry->diagnostics.allocation_failures; + } + + return registry; +} + +void kzt_guest_registry_destroy(kzt_guest_registry_t **registry_ptr) +{ + kzt_guest_registry_t *registry; + + if (!registry_ptr || !*registry_ptr) { + return; + } + + registry = *registry_ptr; + *registry_ptr = NULL; + + if (registry->lock_ready) { + __atomic_store_n(®istry->destroying, 1, __ATOMIC_RELEASE); + pthread_mutex_lock(®istry->lock); + registry->disabled = 1; + pthread_cond_broadcast(®istry->leases_idle); +#ifdef KZT_GUEST_REGISTRY_TEST + if (test_after_destroy_disable) { + test_after_destroy_disable(test_after_destroy_disable_opaque); + } +#endif + for (;;) { + size_t i; + int busy = __atomic_load_n(®istry->active_api_users, + __ATOMIC_ACQUIRE) != 0; + + for (i = 0; i < registry->count; ++i) { + if (registry->objects[i].active_source_leases) { + busy = 1; + break; + } + } + if (!busy) { + break; + } + pthread_cond_wait(®istry->leases_idle, ®istry->lock); + } + pthread_mutex_unlock(®istry->lock); + } + + kzt_free_snapshot_array(registry->objects, registry->count); + registry->objects = NULL; + registry->count = 0; + registry->capacity = 0; + + pthread_cond_destroy(®istry->leases_idle); + if (registry->lock_ready) { + pthread_mutex_destroy(®istry->lock); + } + + kzt_registry_free(registry); +} + +kzt_guest_registry_result_t kzt_guest_registry_observe( + kzt_guest_registry_t *registry, + const kzt_guest_object_observation_t *observation) +{ + return kzt_guest_registry_observe_with_diagnostic(registry, observation, + NULL); +} + +kzt_guest_registry_result_t kzt_guest_registry_observe_with_diagnostic( + kzt_guest_registry_t *registry, + const kzt_guest_object_observation_t *observation, + kzt_guest_registry_observation_diagnostic_t *diagnostic) +{ + kzt_guest_registry_result_t result; + ssize_t index; + int updated = 0; + uintptr_t link_map_addr = observation ? observation->link_map_addr : 0; + unsigned long generation = 0; + + if (!registry) { + kzt_registry_init_empty_diagnostic(diagnostic, + KZT_GUEST_REGISTRY_DISABLED, + link_map_addr); + return KZT_GUEST_REGISTRY_DISABLED; + } + + if (kzt_registry_api_lock(registry) != 0) { + kzt_registry_init_empty_diagnostic(diagnostic, + KZT_GUEST_REGISTRY_DISABLED, + link_map_addr); + return KZT_GUEST_REGISTRY_DISABLED; + } + ++registry->diagnostics.observations; + + if (registry->disabled) { + result = KZT_GUEST_REGISTRY_DISABLED; + goto out; + } + + if (!observation || observation->link_map_addr == 0) { + result = KZT_GUEST_REGISTRY_ERROR; + goto out; + } + + index = kzt_find_object_index(registry, observation->link_map_addr); + if (index < 0) { + if (kzt_registry_ensure_capacity(registry) != 0) { + result = KZT_GUEST_REGISTRY_ERROR; + goto out; + } + generation = registry->next_generation; + if (kzt_snapshot_from_observation(®istry->objects[registry->count], + observation, generation) != 0) { + ++registry->diagnostics.allocation_failures; + result = KZT_GUEST_REGISTRY_ERROR; + goto out; + } + ++registry->next_generation; + ++registry->count; + result = KZT_GUEST_REGISTRY_ADDED; + goto out; + } + + generation = registry->objects[index].generation; + if (registry->objects[index].state == KZT_GUEST_OBJECT_DEAD) { + kzt_guest_object_snapshot_t replacement; + generation = registry->next_generation; + if (kzt_snapshot_from_observation(&replacement, observation, + generation) != 0) { + ++registry->diagnostics.allocation_failures; + result = KZT_GUEST_REGISTRY_ERROR; + goto out; + } + kzt_free_snapshot_strings(®istry->objects[index]); + registry->objects[index] = replacement; + ++registry->next_generation; + result = KZT_GUEST_REGISTRY_ADDED; + goto out; + } + if (kzt_observation_conflicts(®istry->objects[index], observation)) { + result = KZT_GUEST_REGISTRY_CONFLICT; + goto out; + } + + if (kzt_update_snapshot(®istry->objects[index], observation, + &updated) != 0) { + ++registry->diagnostics.allocation_failures; + result = KZT_GUEST_REGISTRY_ERROR; + goto out; + } + + result = updated ? KZT_GUEST_REGISTRY_UPDATED : + KZT_GUEST_REGISTRY_UNCHANGED; + +out: + kzt_registry_note_result(registry, result, link_map_addr, generation, + diagnostic); + kzt_registry_api_unlock(registry); + return result; +} + +int kzt_guest_registry_retire(kzt_guest_registry_t *registry, + uintptr_t link_map_addr, + unsigned long generation) +{ + ssize_t index; + if (!registry || !link_map_addr || !generation || + kzt_registry_api_lock(registry) != 0) return -1; + index = kzt_find_object_index(registry, link_map_addr); + if (registry->disabled || index < 0 || + registry->objects[index].generation != generation || + registry->objects[index].state == KZT_GUEST_OBJECT_UNLOADING || + registry->objects[index].state == KZT_GUEST_OBJECT_DEAD) { + kzt_registry_api_unlock(registry); + return -1; + } + registry->objects[index].state = KZT_GUEST_OBJECT_UNLOADING; + for (;;) { + index = kzt_find_object_index(registry, link_map_addr); + if (registry->disabled || index < 0 || + registry->objects[index].generation != generation || + registry->objects[index].state != KZT_GUEST_OBJECT_UNLOADING) { + kzt_registry_api_unlock(registry); + return -1; + } + if (!registry->objects[index].active_source_leases) { + break; + } +#ifdef KZT_GUEST_REGISTRY_TEST + if (test_before_retire_wait) { + test_before_retire_wait(test_before_retire_wait_opaque); + } +#endif + pthread_cond_wait(®istry->leases_idle, ®istry->lock); +#ifdef KZT_GUEST_REGISTRY_TEST + if (test_after_retire_wake) { + pthread_mutex_unlock(®istry->lock); + test_after_retire_wake(test_after_retire_wake_opaque); + pthread_mutex_lock(®istry->lock); + } +#endif + } + registry->objects[index].state = KZT_GUEST_OBJECT_DEAD; + pthread_cond_broadcast(®istry->leases_idle); + kzt_registry_api_unlock(registry); + return 0; +} + +int kzt_guest_registry_wait_retired(kzt_guest_registry_t *registry, + uintptr_t link_map_addr, + unsigned long generation) +{ + ssize_t index; + if (!registry || !link_map_addr || !generation || + kzt_registry_api_lock(registry) != 0) + return -1; + for (;;) { + index = kzt_find_object_index(registry, link_map_addr); + if (registry->disabled || index < 0 || + registry->objects[index].generation != generation) { + kzt_registry_api_unlock(registry); + return -1; + } + if (registry->objects[index].state == KZT_GUEST_OBJECT_DEAD) { + kzt_registry_api_unlock(registry); + return 0; + } + if (registry->objects[index].state != KZT_GUEST_OBJECT_UNLOADING) { + kzt_registry_api_unlock(registry); + return -1; + } + pthread_cond_wait(®istry->leases_idle, ®istry->lock); + } +} + +int kzt_guest_registry_source_lease_acquire( + kzt_guest_registry_t *registry, + uintptr_t link_map_addr, + unsigned long generation, + uintptr_t namespace_id, + kzt_guest_registry_source_lease_t *lease) +{ + ssize_t index; + + if (lease) { + memset(lease, 0, sizeof(*lease)); + } + if (!registry || !link_map_addr || !generation || namespace_id != 0 || + !lease) { + return -1; + } + + if (kzt_registry_api_lock(registry) != 0) { + return -1; + } + index = kzt_find_object_index(registry, link_map_addr); + if (registry->disabled || index < 0 || + registry->objects[index].generation != generation || + registry->objects[index].namespace_id.status != KZT_GUEST_FIELD_OK || + registry->objects[index].namespace_id.value != namespace_id || + registry->objects[index].state == KZT_GUEST_OBJECT_UNLOADING || + registry->objects[index].state == KZT_GUEST_OBJECT_DEAD) { + kzt_registry_api_unlock(registry); + return -1; + } + + ++registry->objects[index].active_source_leases; + lease->registry = registry; + lease->link_map_addr = link_map_addr; + lease->generation = generation; + lease->namespace_id = namespace_id; + lease->active = 1; + kzt_registry_api_unlock(registry); + return 0; +} + +void kzt_guest_registry_source_lease_release( + kzt_guest_registry_source_lease_t *lease) +{ + kzt_guest_registry_t *registry; + ssize_t index; + + if (!lease || !lease->active || !(registry = lease->registry)) { + return; + } + + pthread_mutex_lock(®istry->lock); + index = kzt_find_object_index(registry, lease->link_map_addr); + if (index >= 0 && + registry->objects[index].generation == lease->generation && + registry->objects[index].active_source_leases) { + --registry->objects[index].active_source_leases; + if (!registry->objects[index].active_source_leases) { + pthread_cond_broadcast(®istry->leases_idle); + } + } + pthread_mutex_unlock(®istry->lock); + memset(lease, 0, sizeof(*lease)); +} + +int kzt_guest_registry_find_by_link_map( + kzt_guest_registry_t *registry, + uintptr_t link_map_addr, + kzt_guest_object_snapshot_t **snapshot) +{ + ssize_t index; + + if (snapshot) { + *snapshot = NULL; + } + if (!registry || !snapshot || link_map_addr == 0) { + return -1; + } + + if (kzt_registry_api_lock(registry) != 0) { + return -1; + } + if (registry->disabled) { + kzt_registry_api_unlock(registry); + return -1; + } + + index = kzt_find_object_index(registry, link_map_addr); + if (index < 0 || + registry->objects[index].state == KZT_GUEST_OBJECT_UNLOADING || + registry->objects[index].state == KZT_GUEST_OBJECT_DEAD) { + kzt_registry_api_unlock(registry); + return -1; + } + + *snapshot = kzt_registry_calloc(1, sizeof(**snapshot)); + if (!*snapshot) { + ++registry->diagnostics.allocation_failures; + kzt_registry_api_unlock(registry); + return -1; + } + + if (kzt_copy_snapshot(*snapshot, ®istry->objects[index]) != 0) { + ++registry->diagnostics.allocation_failures; + kzt_registry_free(*snapshot); + *snapshot = NULL; + kzt_registry_api_unlock(registry); + return -1; + } + + kzt_registry_api_unlock(registry); + return 0; +} + +kzt_guest_registry_result_t kzt_guest_registry_commit_dynamic_view( + kzt_guest_registry_t *registry, + uintptr_t link_map_addr, + unsigned long generation, + const kzt_guest_dynamic_view_t *view) +{ + kzt_guest_object_snapshot_t *object; + kzt_guest_field_status_t status; + kzt_guest_registry_result_t result; + ssize_t index; + + if (!registry) { + return KZT_GUEST_REGISTRY_DISABLED; + } + + if (!view || link_map_addr == 0 || generation == 0) { + return KZT_GUEST_REGISTRY_ERROR; + } + +#ifdef KZT_GUEST_REGISTRY_TEST + if (kzt_registry_test_should_fail_dynamic_commit()) { + return KZT_GUEST_REGISTRY_ERROR; + } +#endif + + if (kzt_registry_api_lock(registry) != 0) { + return KZT_GUEST_REGISTRY_DISABLED; + } + if (registry->disabled) { + result = KZT_GUEST_REGISTRY_DISABLED; + goto out; + } + + index = kzt_find_object_index(registry, link_map_addr); + if (index < 0 || + registry->objects[index].state == KZT_GUEST_OBJECT_UNLOADING || + registry->objects[index].state == KZT_GUEST_OBJECT_DEAD) { + result = KZT_GUEST_REGISTRY_ERROR; + goto out; + } + + object = ®istry->objects[index]; + if (object->generation != generation || + object->state == KZT_GUEST_OBJECT_UNLOADING || + object->state == KZT_GUEST_OBJECT_DEAD) { + result = KZT_GUEST_REGISTRY_ERROR; + goto out; + } + status = kzt_dynamic_view_field_status(view); + /* A complete view is the only source suitable for relocation decisions. + * Keep it when a later best-effort read is incomplete, while allowing the + * first incomplete view to remain available for diagnostics. */ + if (object->dynamic_view_status == KZT_GUEST_FIELD_OK && + status != KZT_GUEST_FIELD_OK) { + result = KZT_GUEST_REGISTRY_UNCHANGED; + goto out; + } + if (object->dynamic_view_status == status && + kzt_dynamic_view_equal(&object->dynamic_view, view)) { + result = KZT_GUEST_REGISTRY_UNCHANGED; + goto out; + } + + object->dynamic_view = *view; + object->dynamic_view_status = status; + if (status == KZT_GUEST_FIELD_OK) { + object->state = KZT_GUEST_OBJECT_PARSED; + } else if (object->state == KZT_GUEST_OBJECT_PARSED) { + object->state = KZT_GUEST_OBJECT_DISCOVERED; + } + result = KZT_GUEST_REGISTRY_UPDATED; + +out: + kzt_registry_api_unlock(registry); + return result; +} + +int kzt_guest_registry_find_dynamic_view( + kzt_guest_registry_t *registry, + uintptr_t link_map_addr, + kzt_guest_dynamic_view_t *view, + kzt_guest_field_status_t *status, + unsigned long *generation) +{ + ssize_t index; + + if (view) { + memset(view, 0, sizeof(*view)); + } + if (status) { + *status = KZT_GUEST_FIELD_NOT_PARSED; + } + if (generation) { + *generation = 0; + } + if (!registry || !view || link_map_addr == 0) { + return -1; + } + + if (kzt_registry_api_lock(registry) != 0) { + return -1; + } + if (registry->disabled) { + kzt_registry_api_unlock(registry); + return -1; + } + + index = kzt_find_object_index(registry, link_map_addr); + if (index < 0 || + registry->objects[index].state == KZT_GUEST_OBJECT_UNLOADING || + registry->objects[index].state == KZT_GUEST_OBJECT_DEAD) { + kzt_registry_api_unlock(registry); + return -1; + } + + *view = registry->objects[index].dynamic_view; + if (status) { + *status = registry->objects[index].dynamic_view_status; + } + if (generation) { + *generation = registry->objects[index].generation; + } + + kzt_registry_api_unlock(registry); + return 0; +} + +int kzt_guest_registry_publish_lazy_resolver( + kzt_guest_registry_t *registry, + uintptr_t link_map_addr, + unsigned long generation, + uintptr_t namespace_id, + const kzt_guest_lazy_resolver_t *resolver) +{ + kzt_guest_object_snapshot_t *object; + ssize_t index; + + if (!registry || !link_map_addr || !generation || !resolver || + !resolver->link_map_slot || !resolver->resolver_slot || + !resolver->guest_link_map || !resolver->guest_resolver || + resolver->guest_link_map != link_map_addr || + namespace_id != 0) { + return -1; + } + + if (kzt_registry_api_lock(registry) != 0) { + return -1; + } + index = kzt_find_object_index(registry, link_map_addr); + if (registry->disabled || index < 0) { + kzt_registry_api_unlock(registry); + return -1; + } + object = ®istry->objects[index]; + if (object->generation != generation || + object->namespace_id.status != KZT_GUEST_FIELD_OK || + object->namespace_id.value != namespace_id || + object->state == KZT_GUEST_OBJECT_WRAPPER_READY || + object->state == KZT_GUEST_OBJECT_PATCHED || + object->state == KZT_GUEST_OBJECT_UNLOADING || + object->state == KZT_GUEST_OBJECT_DEAD) { + kzt_registry_api_unlock(registry); + return -1; + } + if (object->lazy_resolver.valid) { + int same = object->lazy_resolver.link_map_slot == + resolver->link_map_slot && + object->lazy_resolver.resolver_slot == + resolver->resolver_slot && + object->lazy_resolver.guest_link_map == + resolver->guest_link_map && + object->lazy_resolver.guest_resolver == + resolver->guest_resolver; + kzt_registry_api_unlock(registry); + return same ? 0 : -1; + } + object->lazy_resolver = *resolver; + object->lazy_resolver.valid = 1; + kzt_registry_api_unlock(registry); + return 0; +} + +int kzt_guest_registry_find_lazy_resolver( + kzt_guest_registry_t *registry, + uintptr_t link_map_addr, + unsigned long generation, + uintptr_t namespace_id, + kzt_guest_lazy_resolver_t *resolver) +{ + kzt_guest_object_snapshot_t *object; + ssize_t index; + + if (resolver) { + memset(resolver, 0, sizeof(*resolver)); + } + if (!registry || !link_map_addr || !generation || !resolver || + namespace_id != 0) { + return -1; + } + + if (kzt_registry_api_lock(registry) != 0) { + return -1; + } + index = kzt_find_object_index(registry, link_map_addr); + if (registry->disabled || index < 0) { + kzt_registry_api_unlock(registry); + return -1; + } + object = ®istry->objects[index]; + if (object->generation != generation || + object->namespace_id.status != KZT_GUEST_FIELD_OK || + object->namespace_id.value != namespace_id || + object->state == KZT_GUEST_OBJECT_UNLOADING || + object->state == KZT_GUEST_OBJECT_DEAD || + !object->lazy_resolver.valid) { + kzt_registry_api_unlock(registry); + return -1; + } + *resolver = object->lazy_resolver; + kzt_registry_api_unlock(registry); + return 0; +} + +int kzt_guest_registry_dump_snapshot( + kzt_guest_registry_t *registry, + kzt_guest_registry_dump_t *dump) +{ + size_t i; + + if (dump) { + dump->objects = NULL; + dump->count = 0; + } + if (!registry || !dump) { + return -1; + } + + if (kzt_registry_api_lock(registry) != 0) { + return -1; + } + if (registry->disabled) { + kzt_registry_api_unlock(registry); + return -1; + } + + if (registry->count == 0) { + kzt_registry_api_unlock(registry); + return 0; + } + + dump->objects = kzt_registry_calloc(registry->count, + sizeof(*dump->objects)); + if (!dump->objects) { + ++registry->diagnostics.allocation_failures; + kzt_registry_api_unlock(registry); + return -1; + } + + for (i = 0; i < registry->count; ++i) { + if (kzt_copy_snapshot(&dump->objects[i], ®istry->objects[i]) != 0) { + ++registry->diagnostics.allocation_failures; + kzt_free_snapshot_array(dump->objects, i + 1); + dump->objects = NULL; + dump->count = 0; + kzt_registry_api_unlock(registry); + return -1; + } + } + dump->count = registry->count; + + kzt_registry_api_unlock(registry); + return 0; +} + +int kzt_guest_registry_get_diagnostics( + kzt_guest_registry_t *registry, + kzt_guest_registry_diagnostics_t *diagnostics) +{ + if (!registry || !diagnostics) { + return -1; + } + + if (kzt_registry_api_lock(registry) != 0) { + return -1; + } + *diagnostics = registry->diagnostics; + kzt_registry_api_unlock(registry); + return 0; +} + +int kzt_guest_registry_configure_diagnostics( + kzt_guest_registry_t *registry, + const kzt_guest_registry_diagnostic_config_t *config) +{ + if (!registry || !config) { + return -1; + } + + if (kzt_registry_api_lock(registry) != 0) { + return -1; + } + registry->diagnostic_config.enabled = !!config->enabled; + registry->diagnostic_config.throttle_limit = + config->throttle_limit ? config->throttle_limit : 1; + kzt_registry_api_unlock(registry); + return 0; +} + +int kzt_guest_registry_get_diagnostic_report( + kzt_guest_registry_t *registry, + kzt_guest_registry_diagnostic_report_t *report) +{ + if (!registry || !report) { + return -1; + } + + if (kzt_registry_api_lock(registry) != 0) { + return -1; + } + memset(report, 0, sizeof(*report)); + report->config = registry->diagnostic_config; + report->counters = registry->diagnostics; + memcpy(report->events, registry->diagnostic_events, + sizeof(report->events)); + report->event_count = KZT_GUEST_REGISTRY_RESULT_COUNT; + kzt_registry_api_unlock(registry); + return 0; +} + +int kzt_guest_registry_note_diagnostic( + kzt_guest_registry_t *registry, + kzt_guest_registry_result_t result, + uintptr_t link_map_addr, + kzt_guest_registry_observation_diagnostic_t *diagnostic) +{ + if (!registry || result >= KZT_GUEST_REGISTRY_RESULT_COUNT) { + kzt_registry_init_empty_diagnostic(diagnostic, result, link_map_addr); + return -1; + } + + if (kzt_registry_api_lock(registry) != 0) { + kzt_registry_init_empty_diagnostic(diagnostic, result, + link_map_addr); + return -1; + } + kzt_registry_note_result(registry, result, link_map_addr, 0, diagnostic); + kzt_registry_api_unlock(registry); + return 0; +} + +static int kzt_guest_registry_dump_emit( + kzt_guest_registry_dump_sink_fn sink, + void *opaque, + const char *fmt, + ...) +{ + char line[1024]; + va_list ap; + int len; + + va_start(ap, fmt); + len = vsnprintf(line, sizeof(line), fmt, ap); + va_end(ap); + if (len < 0) { + return -1; + } + + line[sizeof(line) - 1] = '\0'; + return sink(line, opaque); +} + +static int kzt_guest_registry_dump_emit_scalar( + kzt_guest_registry_dump_sink_fn sink, + void *opaque, + const char *name, + kzt_guest_scalar_field_t field) +{ + return kzt_guest_registry_dump_emit( + sink, opaque, "%s=0x%lx(%s)", name, (unsigned long)field.value, + kzt_guest_field_status_name(field.status)); +} + +int kzt_guest_registry_dump_text( + kzt_guest_registry_t *registry, + kzt_guest_registry_dump_sink_fn sink, + void *opaque) +{ + kzt_guest_registry_diagnostic_report_t report; + kzt_guest_registry_dump_t dump = { 0 }; + size_t i; + int ret = -1; + + if (!registry || !sink || kzt_registry_api_enter(registry) != 0) { + return -1; + } + + if (kzt_guest_registry_get_diagnostic_report(registry, &report) != 0) { + goto out; + } + if (kzt_guest_registry_dump_snapshot(registry, &dump) != 0) { + goto out; + } + + if (kzt_guest_registry_dump_emit( + sink, opaque, + "kzt_guest_registry diagnostics enabled=%d throttle_limit=%lu " + "observations=%lu added=%lu unchanged=%lu updated=%lu " + "conflicts=%lu disabled=%lu errors=%lu init_failures=%lu " + "allocation_failures=%lu objects=%lu", + report.config.enabled, report.config.throttle_limit, + report.counters.observations, report.counters.added, + report.counters.unchanged, report.counters.updated, + report.counters.conflicts, report.counters.disabled, + report.counters.errors, report.counters.init_failures, + report.counters.allocation_failures, + (unsigned long)dump.count) != 0) { + goto out; + } + + for (i = 0; i < report.event_count; ++i) { + const kzt_guest_registry_event_summary_t *event = &report.events[i]; + + if (event->observed == 0) { + continue; + } + if (kzt_guest_registry_dump_emit( + sink, opaque, + "kzt_guest_registry event result=%s observed=%lu " + "emitted=%lu suppressed=%lu last_link_map=0x%lx " + "last_generation=%lu", + kzt_registry_result_name(event->result), event->observed, + event->emitted, event->suppressed, + (unsigned long)event->last_link_map_addr, + event->last_generation) != 0) { + goto out; + } + } + + for (i = 0; i < dump.count; ++i) { + const kzt_guest_object_snapshot_t *object = &dump.objects[i]; + + if (kzt_guest_registry_dump_emit( + sink, opaque, + "kzt_guest_registry object link_map=0x%lx generation=%lu " + "state=%d ", + (unsigned long)object->link_map_addr, object->generation, + object->state) != 0 || + kzt_guest_registry_dump_emit_scalar(sink, opaque, "load_bias", + object->load_bias) != 0 || + kzt_guest_registry_dump_emit_scalar(sink, opaque, " dynamic_addr", + object->dynamic_addr) != 0 || + kzt_guest_registry_dump_emit_scalar(sink, opaque, " map_start", + object->map_start) != 0 || + kzt_guest_registry_dump_emit_scalar(sink, opaque, " map_end", + object->map_end) != 0 || + kzt_guest_registry_dump_emit( + sink, opaque, + " namespace_id=0x%lx(%s) dynamic_view=%s path_status=%s " + "path=\"%s\" soname_status=%s soname=\"%s\"", + (unsigned long)object->namespace_id.value, + kzt_guest_field_status_name(object->namespace_id.status), + kzt_guest_field_status_name(object->dynamic_view_status), + kzt_guest_field_status_name(object->path.status), + object->path.value ? object->path.value : "", + kzt_guest_field_status_name(object->soname.status), + object->soname.value ? object->soname.value : "") != 0) { + goto out; + } + } + + ret = 0; + +out: + kzt_guest_registry_dump_free(&dump); + kzt_registry_api_leave(registry); + return ret; +} + +void kzt_guest_object_snapshot_free(kzt_guest_object_snapshot_t *snapshot) +{ + if (!snapshot) { + return; + } + + kzt_free_snapshot_strings(snapshot); + kzt_registry_free(snapshot); +} + +void kzt_guest_registry_dump_free(kzt_guest_registry_dump_t *dump) +{ + if (!dump) { + return; + } + + kzt_free_snapshot_array(dump->objects, dump->count); + dump->objects = NULL; + dump->count = 0; +} diff --git a/target/i386/latx/context/kzt_guest_registry_context.c b/target/i386/latx/context/kzt_guest_registry_context.c new file mode 100644 index 00000000000..ecc5b9731ad --- /dev/null +++ b/target/i386/latx/context/kzt_guest_registry_context.c @@ -0,0 +1,147 @@ +#include "kzt_guest_registry_context.h" + +enum kzt_guest_registry_context_state { + KZT_GUEST_REGISTRY_CONTEXT_UNINITIALIZED = 0, + KZT_GUEST_REGISTRY_CONTEXT_READY, + KZT_GUEST_REGISTRY_CONTEXT_UNAVAILABLE, + KZT_GUEST_REGISTRY_CONTEXT_DESTROYING, +}; + +kzt_guest_registry_t *kzt_guest_registry_context_get( + kzt_guest_registry_context_t *context, + pthread_mutex_t *context_lock) +{ + kzt_guest_registry_t *registry; + int state; + + if (!context || !context_lock) { + return NULL; + } + + state = __atomic_load_n(&context->state, __ATOMIC_ACQUIRE); + if (state == KZT_GUEST_REGISTRY_CONTEXT_READY) { + return __atomic_load_n(&context->registry, __ATOMIC_ACQUIRE); + } + if (state != KZT_GUEST_REGISTRY_CONTEXT_UNINITIALIZED) { + return NULL; + } + + pthread_mutex_lock(context_lock); + state = __atomic_load_n(&context->state, __ATOMIC_RELAXED); + if (state == KZT_GUEST_REGISTRY_CONTEXT_UNINITIALIZED) { + registry = kzt_guest_registry_init(); + __atomic_store_n(&context->registry, registry, __ATOMIC_RELEASE); + __atomic_store_n( + &context->state, + registry ? KZT_GUEST_REGISTRY_CONTEXT_READY : + KZT_GUEST_REGISTRY_CONTEXT_UNAVAILABLE, + __ATOMIC_RELEASE); + } else { + registry = state == KZT_GUEST_REGISTRY_CONTEXT_READY ? + __atomic_load_n(&context->registry, __ATOMIC_ACQUIRE) : NULL; + } + pthread_mutex_unlock(context_lock); + return registry; +} + +void kzt_guest_registry_context_destroy( + kzt_guest_registry_context_t *context, + pthread_mutex_t *context_lock) +{ + kzt_guest_registry_t *registry; + + if (!context || !context_lock) { + return; + } + + pthread_mutex_lock(context_lock); + __atomic_store_n(&context->state, + KZT_GUEST_REGISTRY_CONTEXT_DESTROYING, + __ATOMIC_RELEASE); + registry = __atomic_exchange_n(&context->registry, NULL, + __ATOMIC_ACQ_REL); + __atomic_store_n(&context->main_namespace_head, 0, __ATOMIC_RELEASE); + pthread_mutex_unlock(context_lock); + + kzt_guest_registry_destroy(®istry); +} + +int kzt_guest_registry_context_get_main_namespace_head( + const kzt_guest_registry_context_t *context, + uintptr_t *head) +{ + uintptr_t value; + + if (head) { + *head = 0; + } + if (!context || !head || + __atomic_load_n(&context->state, __ATOMIC_ACQUIRE) == + KZT_GUEST_REGISTRY_CONTEXT_DESTROYING) { + return -1; + } + value = __atomic_load_n(&context->main_namespace_head, + __ATOMIC_ACQUIRE); + if (!value) { + return -1; + } + *head = value; + return 0; +} + +int kzt_guest_registry_context_confirm_main_namespace_head( + kzt_guest_registry_context_t *context, + pthread_mutex_t *context_lock, + uintptr_t head) +{ + uintptr_t current; + int result = -1; + + if (!context || !context_lock || !head) { + return -1; + } + + pthread_mutex_lock(context_lock); + if (__atomic_load_n(&context->state, __ATOMIC_RELAXED) != + KZT_GUEST_REGISTRY_CONTEXT_DESTROYING) { + current = __atomic_load_n(&context->main_namespace_head, + __ATOMIC_RELAXED); + if (!current || current == head) { + __atomic_store_n(&context->main_namespace_head, head, + __ATOMIC_RELEASE); + result = 0; + } + } + pthread_mutex_unlock(context_lock); + return result; +} + +int kzt_guest_registry_context_has_main_namespace_evidence( + const kzt_guest_registry_context_t *context, + kzt_guest_registry_t *registry, + uintptr_t link_map_addr, + uintptr_t load_bias, + uintptr_t dynamic_addr) +{ + kzt_guest_object_snapshot_t *snapshot = NULL; + uintptr_t main_head = 0; + int matches = 0; + + if (!registry || !link_map_addr || !dynamic_addr || + kzt_guest_registry_context_get_main_namespace_head( + context, &main_head) != 0 || !main_head) { + return 0; + } + if (kzt_guest_registry_find_by_link_map( + registry, link_map_addr, &snapshot) == 0 && snapshot && + snapshot->load_bias.status == KZT_GUEST_FIELD_OK && + snapshot->load_bias.value == load_bias && + snapshot->dynamic_addr.status == KZT_GUEST_FIELD_OK && + snapshot->dynamic_addr.value == dynamic_addr && + snapshot->namespace_id.status == KZT_GUEST_FIELD_OK && + snapshot->namespace_id.value == 0) { + matches = 1; + } + kzt_guest_object_snapshot_free(snapshot); + return matches; +} diff --git a/target/i386/latx/context/kzt_jump_slot_production.c b/target/i386/latx/context/kzt_jump_slot_production.c new file mode 100644 index 00000000000..69d9dac8adb --- /dev/null +++ b/target/i386/latx/context/kzt_jump_slot_production.c @@ -0,0 +1,1189 @@ +#include "qemu/osdep.h" + +#include "kzt_jump_slot_production.h" + +#ifdef CONFIG_LATX_KZT + +#include + +#if defined(CONFIG_USER_ONLY) && !defined(KZT_JUMP_SLOT_PRODUCTION_TEST) +#include "qemu.h" +#endif + +#include "box64context.h" +#include "debug.h" +#include "elfload_dump.h" +#include "elfloader_private.h" +#include "kzt_rela_diagnostics.h" +#include "library.h" +#include "librarian.h" +#include "kzt_rela_request_enricher.h" +#include "kzt_rela_runtime_bridge.h" +#include "kzt_owner_resolver.h" +#include "kzt_runtime_candidate_shadow.h" + +typedef struct kzt_production_jump_slot_state { + box64context_t *context; + int slot_current_value_is_unresolved_stub; + uintptr_t resolved_target; + kzt_wrapper_bridge_provider_t wrapper_provider; + kzt_rela_immediate_candidate_request_t initial_request; + kzt_rela_request_enricher_result_t base_enrich_result; + kzt_rela_request_enricher_result_t bridge_enrich_result; + kzt_rela_immediate_candidate_request_t last_request; + kzt_rela_immediate_writer_result_t writer_result; + kzt_guest_registry_source_lease_t source_lease; + kzt_patch_candidate_t runtime_candidate; + kzt_runtime_got_plt_candidate_result_t runtime_candidate_result; + char runtime_candidate_strings[512]; + uintptr_t required_source_link_map; + unsigned long required_source_generation; + int discover_bridge_from_provider; + const char *failure_stage; +} kzt_production_jump_slot_state_t; + +#ifdef KZT_JUMP_SLOT_PRODUCTION_TEST +void kzt_jump_slot_production_test_before_source_lease_acquire(void); +void kzt_jump_slot_production_test_before_source_memory_access(void); +void kzt_jump_slot_production_test_before_slot_load(void); +void kzt_jump_slot_production_test_after_slot_load(uintptr_t *value); +void kzt_jump_slot_production_test_after_slot_cas(int exchanged); +void kzt_jump_slot_production_test_shadow_run(void); +int kzt_jump_slot_production_test_begin_slot_write( + uintptr_t slot_addr, kzt_patch_spike_permission_lease_t *lease); +int kzt_jump_slot_production_test_end_slot_write( + kzt_patch_spike_permission_lease_t *lease); +#endif + +static int production_read_guest_memory(uintptr_t address, void *dst, + size_t size, void *opaque) +{ + (void)opaque; + if (!address || !dst || !size) { + return -1; + } +#ifdef KZT_JUMP_SLOT_PRODUCTION_TEST + memcpy(dst, (const void *)address, size); +#else + void *host_ptr; + + host_ptr = lock_user(VERIFY_READ, (abi_ulong)address, size, true); + if (!host_ptr) { + return -1; + } + memcpy(dst, host_ptr, size); + unlock_user(host_ptr, (abi_ulong)address, 0); +#endif + return 0; +} + +static int production_shadow_expected_target( + const kzt_patch_candidate_t *candidate, uintptr_t *expected, + void *opaque) +{ + const kzt_production_jump_slot_state_t *state = opaque; + + if (!candidate || !expected || !state || + !state->last_request.expected_guest_target) { + return -1; + } + *expected = state->last_request.expected_guest_target; + return 0; +} + +static kzt_runtime_candidate_shadow_stub_classification_t +production_shadow_classify_stub(const kzt_patch_candidate_t *candidate, + void *opaque) +{ + const kzt_production_jump_slot_state_t *state = opaque; + + (void)candidate; + return state && state->slot_current_value_is_unresolved_stub ? + KZT_RUNTIME_CANDIDATE_SHADOW_STUB_MATCH : + KZT_RUNTIME_CANDIDATE_SHADOW_STUB_NO_MATCH; +} + +static void production_shadow_runtime_candidate( + kzt_production_jump_slot_state_t *state, + const kzt_rela_immediate_candidate_request_t *request, + const kzt_wrapper_bridge_provider_t *wrapper_provider) +{ + kzt_guest_dynamic_view_t view; + kzt_guest_field_status_t status; + unsigned long generation; + kzt_guest_link_map_reader_ops_t reader_ops = { + .read_memory = production_read_guest_memory, + }; + kzt_patch_candidate_t candidate; + kzt_runtime_candidate_shadow_record_t record; + char strings[512]; + kzt_runtime_got_plt_candidate_request_t collector; + kzt_runtime_candidate_shadow_input_t input; + kzt_runtime_candidate_shadow_result_t result; + + /* Shadow is diagnostic-only. With diagnostics disabled, avoid a second + * candidate read plus planner formatting and logging. */ + if (!kzt_registry_diagnostics_enabled() || !state || !request || + !request->source.known || !request->source.link_map_addr || + !request->source.generation || request->table_kind == + KZT_PATCH_TABLE_UNKNOWN || !request->entry_addr) { + return; + } +#ifdef KZT_JUMP_SLOT_PRODUCTION_TEST + kzt_jump_slot_production_test_shadow_run(); +#endif + if (kzt_guest_registry_find_dynamic_view( + KztGuestRegistryForContext(state->context), + request->source.link_map_addr, &view, &status, &generation) != 0 || + status != KZT_GUEST_FIELD_OK || generation != request->source.generation || + view.status != KZT_GUEST_DYNAMIC_COMPLETE) { + return; + } + + memset(&candidate, 0, sizeof(candidate)); + memset(&record, 0, sizeof(record)); + memset(strings, 0, sizeof(strings)); + collector = (kzt_runtime_got_plt_candidate_request_t) { + .view = &view, + .reader_ops = &reader_ops, + .source = &request->source, + .dynamic_view_generation = generation, + .only_entry = 1, + .only_table_kind = request->table_kind, + .only_entry_index = request->entry_index, + .candidates = &candidate, + .candidate_capacity = 1, + .string_storage = strings, + .string_storage_size = sizeof(strings), + }; + input = (kzt_runtime_candidate_shadow_input_t) { + .collector_request = &collector, + .registry = KztGuestRegistryForContext(state->context), + .wrapper_manifest = wrapper_provider ? &wrapper_provider->manifest : + NULL, + .bridge_ops = wrapper_provider ? &wrapper_provider->bridge_ops : NULL, + .resolve_expected_guest_target = production_shadow_expected_target, + .expected_target_opaque = state, + .classify_stub = production_shadow_classify_stub, + .stub_classifier_opaque = state, + .records = &record, + .record_capacity = 1, + }; + (void)kzt_runtime_candidate_shadow_run(&input, &result); +} + +static int production_slot_load(uintptr_t slot_addr, uintptr_t *value, + void *opaque) +{ + (void)opaque; + if (!slot_addr || !value) { + return -1; + } +#ifdef KZT_JUMP_SLOT_PRODUCTION_TEST + kzt_jump_slot_production_test_before_slot_load(); +#endif + *value = __atomic_load_n((uintptr_t *)slot_addr, __ATOMIC_ACQUIRE); +#ifdef KZT_JUMP_SLOT_PRODUCTION_TEST + kzt_jump_slot_production_test_after_slot_load(value); +#endif + return 0; +} + +static int production_slot_cas(uintptr_t slot_addr, uintptr_t *expected, + uintptr_t replacement, void *opaque) +{ + int exchanged; + + (void)opaque; + if (!slot_addr || !expected) { + return -1; + } + exchanged = __atomic_compare_exchange_n( + (uintptr_t *)slot_addr, expected, replacement, 0, + __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE) + ? 1 + : 0; +#ifdef KZT_JUMP_SLOT_PRODUCTION_TEST + kzt_jump_slot_production_test_after_slot_cas(exchanged); +#endif + return exchanged; +} + +static int production_slot_fits_page(uintptr_t slot_addr, uintptr_t page_mask) +{ + uintptr_t slot_last; + + if (!slot_addr || (slot_addr & (sizeof(uintptr_t) - 1)) || + slot_addr > UINTPTR_MAX - (sizeof(uintptr_t) - 1)) { + return 0; + } + slot_last = slot_addr + sizeof(uintptr_t) - 1; + return (slot_addr & page_mask) == (slot_last & page_mask); +} + +/* Use linux-user bookkeeping, never a host mprotect on a guest slot. */ +static int production_slot_begin_write( + uintptr_t slot_addr, kzt_patch_spike_permission_lease_t *lease, + void *opaque) +{ + (void)opaque; + if (!slot_addr || !lease) { + return -1; + } + memset(lease, 0, sizeof(*lease)); +#ifdef KZT_JUMP_SLOT_PRODUCTION_TEST + if (!production_slot_fits_page(slot_addr, ~(uintptr_t)0xfff)) { + return -1; + } + lease->guest_page_length = 0x1000; + return kzt_jump_slot_production_test_begin_slot_write(slot_addr, lease); +#elif defined(CONFIG_USER_ONLY) + { + abi_ulong guest_addr; + abi_ulong guest_last; + int flags; + int permissions; + + if (!production_slot_fits_page(slot_addr, TARGET_PAGE_MASK) || + !h2g_valid((void *)slot_addr) || + !h2g_valid((void *)(slot_addr + sizeof(uintptr_t) - 1))) { + return -1; + } + guest_addr = h2g((void *)slot_addr); + guest_last = h2g((void *)(slot_addr + sizeof(uintptr_t) - 1)); + if (guest_last < guest_addr || + guest_last - guest_addr != sizeof(uintptr_t) - 1 || + (guest_addr & TARGET_PAGE_MASK) != + (guest_last & TARGET_PAGE_MASK)) { + return -1; + } + lease->guest_page = guest_addr & TARGET_PAGE_MASK; + lease->guest_page_length = TARGET_PAGE_SIZE; + flags = page_get_flags(guest_addr); + permissions = flags & PAGE_BITS; + lease->checked = 1; + lease->original_permissions = permissions; + lease->was_writable = (flags & PAGE_WRITE) != 0; + if (!(flags & PAGE_VALID) || !permissions) { + return -1; + } + if (lease->was_writable) { + return 0; + } + if (target_mprotect(lease->guest_page, TARGET_PAGE_SIZE, + permissions | PAGE_WRITE) != 0) { + return -1; + } + lease->write_enabled = 1; + return 0; + } +#else + return -1; +#endif +} + +static int production_slot_end_write(kzt_patch_spike_permission_lease_t *lease, + void *opaque) +{ + (void)opaque; + if (!lease) { + return -1; + } +#ifdef KZT_JUMP_SLOT_PRODUCTION_TEST + return kzt_jump_slot_production_test_end_slot_write(lease); +#elif defined(CONFIG_USER_ONLY) + if (!lease->checked || !lease->guest_page_length || + (lease->guest_page & ~TARGET_PAGE_MASK) || + lease->guest_page_length != TARGET_PAGE_SIZE || + (lease->original_permissions & ~PAGE_BITS)) { + return -1; + } + if (!lease->write_enabled) { + return 0; + } + return target_mprotect(lease->guest_page, lease->guest_page_length, + lease->original_permissions); +#else + return -1; +#endif +} + +static int production_validate_write_generation( + const kzt_patch_decision_t *decision, void *opaque) +{ + kzt_production_jump_slot_state_t *state = opaque; + kzt_guest_object_snapshot_t *snapshot = NULL; + kzt_guest_object_snapshot_t *owner_snapshot = NULL; + kzt_guest_dynamic_view_t view; + kzt_guest_field_status_t dynamic_status; + unsigned long dynamic_generation = 0; + int valid = 0; + + if (!state || !decision || !decision->source.known || + !decision->source.link_map_addr || !decision->source.generation || + !decision->current_owner.known || + !decision->current_owner.link_map_addr || + !decision->current_owner.generation || + !decision->dynamic_view_available || + !decision->dynamic_view_generation || + (state->required_source_link_map && + decision->source.link_map_addr != state->required_source_link_map) || + (state->required_source_generation && + decision->source.generation != state->required_source_generation) || + kzt_guest_registry_find_by_link_map( + KztGuestRegistryForContext(state->context), + decision->source.link_map_addr, &snapshot) != 0 || !snapshot || + snapshot->generation != decision->source.generation || + kzt_guest_registry_find_dynamic_view( + KztGuestRegistryForContext(state->context), + decision->source.link_map_addr, &view, &dynamic_status, + &dynamic_generation) != 0 || + dynamic_status != KZT_GUEST_FIELD_OK || + dynamic_generation != decision->dynamic_view_generation || + kzt_guest_registry_find_by_link_map( + KztGuestRegistryForContext(state->context), + decision->current_owner.link_map_addr, &owner_snapshot) != 0 || + !owner_snapshot || + owner_snapshot->generation != decision->current_owner.generation) { + goto out; + } + valid = 1; +out: + kzt_guest_object_snapshot_free(snapshot); + kzt_guest_object_snapshot_free(owner_snapshot); + return valid ? 0 : -1; +} + +static int production_enrich( + kzt_rela_immediate_candidate_request_t *request, + const kzt_wrapper_bridge_provider_t *wrapper_provider, + kzt_rela_request_enricher_result_t *enrich_result, + kzt_production_jump_slot_state_t *state) +{ + kzt_rela_request_enricher_input_t enrich_input = { + .registry = KztGuestRegistryForContext(state->context), + .slot_current_value_is_unresolved_stub = + state->slot_current_value_is_unresolved_stub, + .wrapper_manifest = wrapper_provider ? &wrapper_provider->manifest : + NULL, + .bridge_ops = wrapper_provider ? &wrapper_provider->bridge_ops : NULL, + }; + return kzt_rela_immediate_request_enrich(request, &enrich_input, + enrich_result); +} + +static int production_request_is_main_namespace( + kzt_production_jump_slot_state_t *state, + const kzt_rela_immediate_candidate_request_t *request) +{ + kzt_guest_object_snapshot_t *snapshot = NULL; + int is_main = 0; + + if (!state || !request || !request->source.known || + !request->source.link_map_addr || !request->source.generation || + kzt_guest_registry_find_by_link_map( + KztGuestRegistryForContext(state->context), + request->source.link_map_addr, &snapshot) != 0 || !snapshot) { + goto out; + } + is_main = snapshot->generation == request->source.generation && + snapshot->namespace_id.status == KZT_GUEST_FIELD_OK && + snapshot->namespace_id.value == 0; +out: + kzt_guest_object_snapshot_free(snapshot); + return is_main ? 0 : -1; +} + +static int production_runtime_candidate_matches_request( + const kzt_patch_candidate_t *candidate, + const kzt_rela_immediate_candidate_request_t *request) +{ + return candidate && request && + request->relocation_type == R_X86_64_JUMP_SLOT && + candidate->reloc_type == KZT_PATCH_RELOCATION_JUMP_SLOT && + candidate->source.known && request->source.known && + candidate->source.link_map_addr == request->source.link_map_addr && + candidate->source.generation == request->source.generation && + candidate->dynamic_view_available && + candidate->dynamic_view_generation == request->source.generation && + candidate->table_kind == request->table_kind && + candidate->entry_index == request->entry_index && + candidate->entry_addr == request->entry_addr && + candidate->slot_addr == request->slot_addr && + candidate->slot_current_value_present && + request->slot_current_value_present && + candidate->slot_current_value == request->slot_current_value && + candidate->symbol_index == request->symbol_index && + candidate->symbol_name && request->symbol_name && + strcmp(candidate->symbol_name, request->symbol_name) == 0 && + kzt_symbol_version_evidence_matches( + candidate->version_evidence, candidate->version, + request->version_evidence, request->version); +} + +static int production_collect_runtime_candidate( + kzt_production_jump_slot_state_t *state, + kzt_rela_immediate_candidate_request_t *request) +{ + kzt_guest_dynamic_view_t view; + kzt_guest_field_status_t status; + unsigned long generation; + kzt_guest_link_map_reader_ops_t reader_ops = { + .read_memory = production_read_guest_memory, + }; + kzt_runtime_got_plt_candidate_request_t collector; + + if (!state || !request || !request->source.link_map_addr || + !request->source.generation || + kzt_guest_registry_find_dynamic_view( + KztGuestRegistryForContext(state->context), + request->source.link_map_addr, &view, &status, &generation) != 0 || + status != KZT_GUEST_FIELD_OK || + generation != request->source.generation || + view.status != KZT_GUEST_DYNAMIC_COMPLETE) { + return -1; + } + + memset(&state->runtime_candidate, 0, sizeof(state->runtime_candidate)); + memset(state->runtime_candidate_strings, 0, + sizeof(state->runtime_candidate_strings)); + collector = (kzt_runtime_got_plt_candidate_request_t) { + .view = &view, + .reader_ops = &reader_ops, + .source = &request->source, + .dynamic_view_generation = generation, + .only_entry = 1, + .only_table_kind = request->table_kind, + .only_entry_index = request->entry_index, + .candidates = &state->runtime_candidate, + .candidate_capacity = 1, + .string_storage = state->runtime_candidate_strings, + .string_storage_size = sizeof(state->runtime_candidate_strings), + }; +#ifdef KZT_JUMP_SLOT_PRODUCTION_TEST + kzt_jump_slot_production_test_before_source_memory_access(); +#endif + if (kzt_runtime_got_plt_candidates_collect( + &collector, &state->runtime_candidate_result) != 0 || + state->runtime_candidate_result.status != + KZT_RUNTIME_GOT_PLT_CANDIDATE_OK || + state->runtime_candidate_result.candidate_count != 1 || + !production_runtime_candidate_matches_request( + &state->runtime_candidate, request)) { + return -1; + } + + request->dynamic_addr = state->runtime_candidate.dynamic_addr; + request->load_bias = state->runtime_candidate.load_bias; + request->dynamic_view_generation = + state->runtime_candidate.dynamic_view_generation; + request->dynamic_view_available = 1; + request->entry_addr = state->runtime_candidate.entry_addr; + request->slot_current_value = + state->runtime_candidate.slot_current_value; + request->symbol_name = state->runtime_candidate.symbol_name; + request->version_evidence = + state->runtime_candidate.version_evidence; + request->version = state->runtime_candidate.version; + return 0; +} + +static int production_enrich_base( + kzt_rela_immediate_candidate_request_t *request, void *opaque) +{ + kzt_production_jump_slot_state_t *state = opaque; + + state->failure_stage = "BASE_EVIDENCE"; + int status = production_enrich(request, NULL, &state->base_enrich_result, + state); + + if (status == 0 && + production_request_is_main_namespace(state, request) != 0) { + /* Namespace evidence is not optional: only namespace zero has a + * binding/lease contract today, so other or unknown namespaces never + * reach the planner or a native write. */ + status = -1; + } + if (status == 0 && state->required_source_link_map && + (request->source.link_map_addr != state->required_source_link_map || + request->source.generation != state->required_source_generation)) { + status = -1; + } + if (status == 0 && !state->required_source_link_map) { +#ifdef KZT_JUMP_SLOT_PRODUCTION_TEST + kzt_jump_slot_production_test_before_source_lease_acquire(); +#endif + status = kzt_guest_registry_source_lease_acquire( + KztGuestRegistryForContext(state->context), + request->source.link_map_addr, request->source.generation, 0, + &state->source_lease); + } + if (status == 0) { + status = production_collect_runtime_candidate(state, request); + } + if (status == 0) { + state->last_request = *request; + state->failure_stage = "EXACT_LIBRARY_BINDING"; + } + return status; +} + +static int production_acquire_exact( + const kzt_patch_object_ref_t *owner, library_t *resolved_provider, + kzt_guest_library_handle_t *handle, void *opaque) +{ + kzt_production_jump_slot_state_t *state = opaque; + kzt_guest_library_binding_key_t key; + kzt_guest_object_snapshot_t *snapshot = NULL; + int result = -1; + + state->failure_stage = "EXACT_LIBRARY_BINDING"; + if (!owner || !owner->known || !owner->link_map_addr || + !owner->generation || !resolved_provider || !handle) { + return -1; + } + if (kzt_guest_registry_find_by_link_map( + KztGuestRegistryForContext(state->context), + owner->link_map_addr, &snapshot) != 0 || !snapshot || + snapshot->generation != owner->generation || + snapshot->namespace_id.status != KZT_GUEST_FIELD_OK || + snapshot->namespace_id.value != 0) { + goto out; + } + key = (kzt_guest_library_binding_key_t){ + .link_map_addr = owner->link_map_addr, + .generation = owner->generation, + .namespace_id = snapshot->namespace_id.value, + .namespace_kind = KZT_GUEST_LIBRARY_NAMESPACE_MAIN, + }; + if (KztGuestLibraryLookupForContext(state->context, &key, handle) != 0) { + goto out; + } + if (handle->library != resolved_provider) { + kzt_guest_library_handle_release(handle); + goto out; + } + result = 0; + state->failure_stage = "BRIDGE_EVIDENCE"; +out: + kzt_guest_object_snapshot_free(snapshot); + return result; +} + +static void production_release_exact(kzt_guest_library_handle_t *handle, + void *opaque) +{ + (void)opaque; + kzt_guest_library_handle_release(handle); +} + +static int production_enrich_bridge( + kzt_rela_immediate_candidate_request_t *request, + library_t *held_provider, void *opaque) +{ + kzt_production_jump_slot_state_t *state = opaque; + int status; + + state->failure_stage = "BRIDGE_EVIDENCE"; + memset(&state->wrapper_provider, 0, sizeof(state->wrapper_provider)); + status = state->discover_bridge_from_provider ? + kzt_rela_runtime_wrapper_provider_discover_with_version_evidence( + state->context, held_provider, request->symbol_name, + request->version_evidence, request->version, + &state->wrapper_provider) : + kzt_rela_runtime_wrapper_provider_prepare_with_version_evidence( + state->context, held_provider, state->resolved_target, + request->symbol_name, request->version_evidence, + request->version, + &state->wrapper_provider); + + if (status <= 0) { + /* Keep source, dynamic, current-owner, and symbol evidence in the + * same request. The planner records the missing wrapper/provider as + * unsupported and the route performs the legacy fallback once. */ + status = production_enrich(request, NULL, + &state->bridge_enrich_result, state); + if (status == 0) { + state->last_request = *request; + production_shadow_runtime_candidate(state, request, NULL); + } + return status; + } + pthread_mutex_lock(&state->context->kzt_bridge_mutex); + status = production_enrich(request, &state->wrapper_provider, + &state->bridge_enrich_result, state); + pthread_mutex_unlock(&state->context->kzt_bridge_mutex); + if (status == 0) { + state->last_request = *request; + state->failure_stage = "SOURCE_IDENTITY"; + production_shadow_runtime_candidate( + state, request, &state->wrapper_provider); + } + return status; +} + +static int production_validate_source_identity( + const kzt_rela_immediate_candidate_request_t *request, void *opaque) +{ + kzt_production_jump_slot_state_t *state = opaque; + kzt_guest_object_snapshot_t *snapshot = NULL; + int valid = 0; + + state->failure_stage = "SOURCE_IDENTITY"; + if (!request || !request->source.known || + !request->source.link_map_addr || !request->source.generation || + (state->required_source_link_map && + (request->source.link_map_addr != state->required_source_link_map || + request->source.generation != state->required_source_generation)) || + kzt_guest_registry_find_by_link_map( + KztGuestRegistryForContext(state->context), + request->source.link_map_addr, &snapshot) != 0 || !snapshot || + snapshot->generation != request->source.generation || + snapshot->namespace_id.status != KZT_GUEST_FIELD_OK || + snapshot->namespace_id.value != 0) { + goto out; + } + valid = 1; + state->failure_stage = "WRITER"; +out: + kzt_guest_object_snapshot_free(snapshot); + return valid; +} + +static kzt_jump_slot_route_writer_status_t production_try_writer( + const kzt_rela_immediate_candidate_request_t *request, + const kzt_patch_spike_slot_ops_t *slot_ops, void *opaque) +{ + kzt_production_jump_slot_state_t *state = opaque; + kzt_patch_spike_guard_t *guard = + KztPatchSpikeGuardForContext(state->context); + int status; + + if (!slot_ops || !slot_ops->begin_write || !slot_ops->end_write || + !slot_ops->validate_generation) { + return KZT_JUMP_SLOT_ROUTE_WRITER_PRESERVE; + } + if (kzt_rela_immediate_jump_slot_try_write( + request, guard, slot_ops, &state->writer_result) != 0) { + return KZT_JUMP_SLOT_ROUTE_WRITER_ERROR; + } + state->last_request = *request; + if (state->writer_result.record.action == + KZT_PATCH_SPIKE_ACTION_PRESERVE_GUEST) { + return KZT_JUMP_SLOT_ROUTE_WRITER_PRESERVE; + } + if (!state->writer_result.writer_called) { + return KZT_JUMP_SLOT_ROUTE_WRITER_DECLINED; + } + if (state->writer_result.record.failure == + KZT_PATCH_SPIKE_FAILURE_PERMISSION_ENABLE_FAILED || + state->writer_result.record.failure == + KZT_PATCH_SPIKE_FAILURE_PERMISSION_RESTORE_FAILED || + state->writer_result.record.failure == + KZT_PATCH_SPIKE_FAILURE_ROLLBACK_FAILED || + state->writer_result.record.failure == + KZT_PATCH_SPIKE_FAILURE_GENERATION_MISMATCH) { + return KZT_JUMP_SLOT_ROUTE_WRITER_PRESERVE; + } + status = state->writer_result.skip_legacy_write + ? KZT_JUMP_SLOT_ROUTE_WRITER_APPLIED + : KZT_JUMP_SLOT_ROUTE_WRITER_ERROR; + return status; +} + +static int production_diagnostic_sink(const char *line, size_t length, + void *opaque) +{ + (void)opaque; + printf_kzt_registry_diagnostics("%.*s\n", (int)length, line); + return 0; +} + +static void production_emit_diagnostic( + kzt_production_jump_slot_state_t *state, + const kzt_jump_slot_route_result_t *route_result) +{ + char buffer[KZT_RELA_DIAGNOSTIC_LINE_LIMIT]; + kzt_rela_diagnostic_throttle_t throttle; + kzt_rela_immediate_diagnostic_input_t input; + kzt_rela_immediate_diagnostic_result_t result; + const kzt_rela_immediate_candidate_request_t *request; + + if (!kzt_registry_diagnostics_enabled() || !state || !route_result) { + return; + } + request = state->last_request.slot_addr ? &state->last_request : + &state->initial_request; + if (!state->last_request.slot_addr) { + printf_kzt_registry_diagnostics( + "kzt_rela_fail_open stage=%s route_status=%d writer_status=%d " + "source_link_map=0x%lx slot=0x%lx symbol=%s version=%s " + "legacy_fallback=%d\n", + state->failure_stage ? state->failure_stage : "UNKNOWN", + route_result->status, route_result->writer_status, + (unsigned long)(request ? request->source.link_map_addr : 0), + (unsigned long)(request ? request->slot_addr : 0), + request && request->symbol_name ? request->symbol_name : "(none)", + request && request->version ? request->version : "(none)", + route_result->legacy_fallback_attempted); + return; + } + if (kzt_rela_diagnostic_throttle_init(&throttle, 1) != 0) { + return; + } + input = (kzt_rela_immediate_diagnostic_input_t) { + .mode = KZT_RELA_DIAGNOSTIC_MODE_DIAGNOSTICS_WRITE_ENABLED, + .request = &state->last_request, + .result = &state->writer_result, + .legacy_fallback = route_result->legacy_fallback_attempted, + .throttle = &throttle, + .buffer = buffer, + .buffer_size = sizeof(buffer), + .sink = production_diagnostic_sink, + }; + (void)kzt_rela_immediate_diagnostic_emit(&input, &result); +} + +static int production_jump_slot_route( + box64context_t *context, library_t *resolved_provider, + uintptr_t resolved_target, elfheader_t *head, int need_resolv_present, + int entry_index, Elf64_Rela *rela, uint64_t *slot, + uintptr_t slot_current_value, + int slot_current_value_is_unresolved_stub, unsigned long symbol_index, + const char *symbol_name, + kzt_symbol_version_evidence_t version_evidence, + const char *version, + int expected_guest_target_present, uintptr_t expected_guest_target, + uintptr_t legacy_target, int preserve_observed_on_failure, + uintptr_t required_source_link_map, + unsigned long required_source_generation, + int discover_bridge_from_provider, + kzt_jump_slot_route_result_t *route_result) +{ + kzt_jump_slot_route_input_t input; + kzt_jump_slot_route_ops_t ops; + kzt_production_jump_slot_state_t state; + int status; + + if (!context || !head || !rela || !slot || !route_result) { + return -1; + } + + memset(&input, 0, sizeof(input)); + input.enabled = 1; + input.preserve_observed_on_failure = preserve_observed_on_failure; + input.expected_guest_target_present = expected_guest_target_present; + input.resolved_target_matches_legacy = + resolved_target == legacy_target; + input.resolved_provider = resolved_provider; + input.request.relocation_type = ELF64_R_TYPE(rela->r_info); + input.request.table_kind = need_resolv_present ? KZT_PATCH_TABLE_PLT_RELA : + KZT_PATCH_TABLE_RELA; + input.request.entry_index = entry_index; + input.request.entry_addr = (uintptr_t)rela; + input.request.source.known = 1; + input.request.source.link_map_addr = required_source_link_map; + input.request.source.generation = required_source_generation; + input.request.source.map_start = (uintptr_t)head->memory; + input.request.source.map_end = (uintptr_t)head->memory + head->memsz; + input.request.source.soname = head->name; + input.request.source.path = head->path; + input.request.dynamic_addr = (uintptr_t)head->Dynamic; + input.request.load_bias = head->delta; + input.request.slot_addr = (uintptr_t)slot; + input.request.slot_current_value_present = 1; + input.request.slot_current_value = slot_current_value; + input.request.expected_guest_target = expected_guest_target; + input.request.legacy_target = legacy_target; + input.request.symbol_index = symbol_index; + input.request.symbol_name = symbol_name; + input.request.version_evidence = version_evidence; + input.request.version = version; + state = (kzt_production_jump_slot_state_t){ + .context = context, + .initial_request = input.request, + .slot_current_value_is_unresolved_stub = + slot_current_value_is_unresolved_stub, + .resolved_target = resolved_target, + .required_source_link_map = required_source_link_map, + .required_source_generation = required_source_generation, + .discover_bridge_from_provider = discover_bridge_from_provider, + .failure_stage = "ROUTE_PRECONDITIONS", + }; + ops = (kzt_jump_slot_route_ops_t){ + .enrich_base = production_enrich_base, + .acquire_exact_provider = production_acquire_exact, + .release_exact_provider = production_release_exact, + .enrich_bridge = production_enrich_bridge, + .validate_source_identity = production_validate_source_identity, + .try_native_writer = production_try_writer, + .load_slot = production_slot_load, + .compare_exchange_slot = production_slot_cas, + .begin_slot_write = production_slot_begin_write, + .end_slot_write = production_slot_end_write, + .validate_write_generation = production_validate_write_generation, + .opaque = &state, + }; + status = kzt_jump_slot_route_apply(&input, &ops, route_result); + if (status != 0) { + kzt_guest_registry_source_lease_release(&state.source_lease); + return -1; + } + production_emit_diagnostic(&state, route_result); + kzt_guest_registry_source_lease_release(&state.source_lease); + + printf_log(LOG_DEBUG, + "KZT: shared JUMP_SLOT route status=%d writer=%d " + "slot=%p observed=%p expected_guest=%p selected=%p " + "legacy=%p exact=%d/%d sym=%s\n", + route_result->status, route_result->writer_status, + (void *)slot, (void *)route_result->observed_value, + (void *)expected_guest_target, + (void *)route_result->selected_target, (void *)legacy_target, + route_result->exact_provider_acquired, + route_result->exact_provider_matched, + symbol_name ? symbol_name : "(none)"); + return 0; +} + +int kzt_production_jump_slot_route( + box64context_t *context, library_t *resolved_provider, + uintptr_t resolved_target, elfheader_t *head, int need_resolv_present, + int entry_index, Elf64_Rela *rela, uint64_t *slot, + uintptr_t slot_current_value, + int slot_current_value_is_unresolved_stub, unsigned long symbol_index, + const char *symbol_name, const char *version, + int expected_guest_target_present, uintptr_t expected_guest_target, + uintptr_t legacy_target, kzt_jump_slot_route_result_t *route_result) +{ + return production_jump_slot_route( + context, resolved_provider, resolved_target, head, + need_resolv_present, entry_index, rela, slot, slot_current_value, + slot_current_value_is_unresolved_stub, symbol_index, symbol_name, + version && version[0] ? KZT_SYMBOL_VERSION_VERSIONED : + KZT_SYMBOL_VERSION_UNKNOWN, + version, expected_guest_target_present, expected_guest_target, + legacy_target, 0, 0, 0, 0, route_result); +} + +int kzt_production_jump_slot_route_with_version_evidence( + box64context_t *context, library_t *resolved_provider, + uintptr_t resolved_target, elfheader_t *head, int need_resolv_present, + int entry_index, Elf64_Rela *rela, uint64_t *slot, + uintptr_t slot_current_value, + int slot_current_value_is_unresolved_stub, unsigned long symbol_index, + const char *symbol_name, + kzt_symbol_version_evidence_t version_evidence, + const char *version, int expected_guest_target_present, + uintptr_t expected_guest_target, uintptr_t legacy_target, + kzt_jump_slot_route_result_t *route_result) +{ + return production_jump_slot_route( + context, resolved_provider, resolved_target, head, + need_resolv_present, entry_index, rela, slot, slot_current_value, + slot_current_value_is_unresolved_stub, symbol_index, symbol_name, + version_evidence, version, expected_guest_target_present, + expected_guest_target, legacy_target, 0, 0, 0, 0, route_result); +} + +static elfheader_t *production_find_source_head( + box64context_t *context, uintptr_t link_map) +{ + int i; + + if (!context || !link_map) { + return NULL; + } + for (i = 0; i < context->elfsize; ++i) { + if (context->elfs[i] && context->elfs[i]->self_link_map == link_map) { + return context->elfs[i]; + } + } + return NULL; +} + +int kzt_production_lazy_route_guest_target_leased( + box64context_t *context, + const kzt_lazy_binding_pending_t *pending, + uintptr_t guest_target, + const kzt_guest_registry_source_lease_t *source_lease, + kzt_lazy_binding_route_result_t *result) +{ + kzt_jump_slot_route_result_t route_result; + elfheader_t *head; + Elf64_Rela *rela; + Elf64_Sym *sym; + const char *symbol_name; + const char *version_name; + kzt_symbol_version_evidence_t version_evidence; + kzt_owner_resolution_t owner_resolution; + kzt_guest_library_binding_key_t provider_key; + kzt_guest_library_handle_t provider_handle; + library_t *resolved_provider = NULL; + unsigned long symbol_index; + int version; + size_t count; + + if (!result) { + return -1; + } + memset(result, 0, sizeof(*result)); + result->status = KZT_LAZY_BINDING_ROUTE_GUEST_PRESERVED; + result->selected_target = guest_target; + result->final_value = guest_target; + if (!context || !pending || !pending->armed || !guest_target || + !pending->symbol || + !kzt_symbol_version_evidence_valid(pending->version_evidence, + pending->version)) { + return 0; + } + if (!source_lease || !source_lease->active || + source_lease->registry != KztGuestRegistryForContext(context) || + source_lease->link_map_addr != pending->source_link_map || + source_lease->generation != pending->source_generation || + source_lease->namespace_id != pending->namespace_id || + pending->namespace_kind != KZT_GUEST_LIBRARY_NAMESPACE_MAIN || + pending->namespace_id != 0) { + return 0; + } +#ifdef KZT_JUMP_SLOT_PRODUCTION_TEST + kzt_jump_slot_production_test_before_source_memory_access(); +#endif + head = production_find_source_head(context, pending->source_link_map); + if (!head || !head->jmprel || !head->pltent || !head->DynSym) { + return 0; + } + count = head->pltsz / head->pltent; + if (pending->relocation_index >= count) { + return 0; + } + rela = (Elf64_Rela *)(head->jmprel + head->delta) + + pending->relocation_index; + if (ELF64_R_TYPE(rela->r_info) != R_X86_64_JUMP_SLOT || + rela->r_offset + head->delta != pending->slot_addr) { + return 0; + } + symbol_index = ELF64_R_SYM(rela->r_info); + sym = &head->DynSym[symbol_index]; + symbol_name = SymName(head, sym); + version = head->VerSym ? + ((Elf64_Half *)((uintptr_t)head->VerSym + head->delta))[symbol_index] : + -1; + if (version == -1 || (version & 0x7fff) < 2) { + version_evidence = KZT_SYMBOL_VERSION_CONFIRMED_UNVERSIONED; + version_name = NULL; + } else { + version &= 0x7fff; + version_evidence = KZT_SYMBOL_VERSION_VERSIONED; + version_name = GetSymbolVersion(head, version); + } + if (!symbol_name || strcmp(symbol_name, pending->symbol) != 0 || + !kzt_symbol_version_evidence_matches( + version_evidence, version_name, pending->version_evidence, + pending->version)) { + return 0; + } + + kzt_owner_resolver_init(&owner_resolution); + if (kzt_owner_resolver_resolve_current( + KztGuestRegistryForContext(context), guest_target, guest_target, + &owner_resolution) != 0 || + owner_resolution.status != KZT_OWNER_RESOLVER_RESOLVED || + owner_resolution.owner_match != KZT_PATCH_OWNER_MATCH || + !owner_resolution.current_owner.known) { + return 0; + } + provider_key = (kzt_guest_library_binding_key_t) { + .link_map_addr = owner_resolution.current_owner.link_map_addr, + .generation = owner_resolution.current_owner.generation, + .namespace_id = 0, + .namespace_kind = KZT_GUEST_LIBRARY_NAMESPACE_MAIN, + }; + memset(&provider_handle, 0, sizeof(provider_handle)); + if (KztGuestLibraryLookupForContext(context, &provider_key, + &provider_handle) != 0 || + !provider_handle.library) { + kzt_guest_library_handle_release(&provider_handle); + return 0; + } + resolved_provider = provider_handle.library; + /* guest_target is only the guest loader's observed bridge/CAS evidence. + * Bridge enrichment resolves and verifies the provider's native symbol + * independently with dlvsym. */ + if (production_jump_slot_route( + context, resolved_provider, guest_target, head, 1, + pending->relocation_index, rela, + (uint64_t *)pending->slot_addr, guest_target, 0, symbol_index, + symbol_name, version_evidence, version_name, 1, guest_target, + guest_target, 1, + pending->source_link_map, pending->source_generation, + 1, + &route_result) != 0) { + kzt_guest_library_handle_release(&provider_handle); + result->status = KZT_LAZY_BINDING_ROUTE_ERROR; + return -1; + } + kzt_guest_library_handle_release(&provider_handle); + result->selected_target = route_result.selected_target; + result->final_value = route_result.final_value; + switch (route_result.status) { + case KZT_JUMP_SLOT_ROUTE_NATIVE_APPLIED: + result->status = KZT_LAZY_BINDING_ROUTE_NATIVE_APPLIED; + break; + case KZT_JUMP_SLOT_ROUTE_CAS_MISMATCH: + result->status = KZT_LAZY_BINDING_ROUTE_CAS_MISMATCH; + break; + case KZT_JUMP_SLOT_ROUTE_GUEST_PRESERVED: + default: + result->status = KZT_LAZY_BINDING_ROUTE_GUEST_PRESERVED; + break; + } + return 0; +} + +int kzt_production_lazy_source_lease_acquire( + box64context_t *context, + const kzt_lazy_binding_pending_t *pending, + kzt_guest_registry_source_lease_t *source_lease) +{ + kzt_guest_registry_t *registry; + + if (!context || !pending || !source_lease || !pending->armed || + !pending->source_link_map || !pending->source_generation || + pending->namespace_kind != KZT_GUEST_LIBRARY_NAMESPACE_MAIN || + pending->namespace_id != 0) { + return -1; + } + memset(source_lease, 0, sizeof(*source_lease)); + registry = KztGuestRegistryForContext(context); + if (!registry) { + return -1; + } +#ifdef KZT_JUMP_SLOT_PRODUCTION_TEST + kzt_jump_slot_production_test_before_source_lease_acquire(); +#endif + return kzt_guest_registry_source_lease_acquire( + registry, pending->source_link_map, + pending->source_generation, pending->namespace_id, source_lease); +} + +int kzt_production_lazy_load_slot_with_lease( + box64context_t *context, + const kzt_lazy_binding_pending_t *pending, + uintptr_t slot_addr, + uintptr_t *value, + kzt_guest_registry_source_lease_t *source_lease) +{ + if (!slot_addr || !value || !source_lease || + kzt_production_lazy_source_lease_acquire( + context, pending, source_lease) != 0) { + return -1; + } +#ifdef KZT_JUMP_SLOT_PRODUCTION_TEST + kzt_jump_slot_production_test_before_slot_load(); +#endif + *value = __atomic_load_n((uintptr_t *)slot_addr, __ATOMIC_ACQUIRE); +#ifdef KZT_JUMP_SLOT_PRODUCTION_TEST + kzt_jump_slot_production_test_after_slot_load(value); +#endif + return 0; +} + +int kzt_production_lazy_route_guest_target( + box64context_t *context, + const kzt_lazy_binding_pending_t *pending, + uintptr_t guest_target, + kzt_lazy_binding_route_result_t *result) +{ + kzt_guest_registry_source_lease_t source_lease = { 0 }; + int status; + + if (!result) { + return -1; + } + memset(result, 0, sizeof(*result)); + result->status = KZT_LAZY_BINDING_ROUTE_GUEST_PRESERVED; + result->selected_target = guest_target; + result->final_value = guest_target; + if (!context || !pending || !pending->armed || !guest_target || + kzt_production_lazy_source_lease_acquire( + context, pending, &source_lease) != 0) { + return 0; + } + status = kzt_production_lazy_route_guest_target_leased( + context, pending, guest_target, &source_lease, result); + kzt_guest_registry_source_lease_release(&source_lease); + return status; +} + +typedef struct kzt_lazy_completion_production_state { + box64context_t *context; + const kzt_lazy_binding_pending_t *pending; + kzt_guest_registry_source_lease_t source_lease; +} kzt_lazy_completion_production_state_t; + +static int kzt_lazy_production_load_slot(uintptr_t slot_addr, + uintptr_t *value, void *opaque) +{ + kzt_lazy_completion_production_state_t *state = opaque; + + return state ? kzt_production_lazy_load_slot_with_lease( + state->context, state->pending, slot_addr, value, + &state->source_lease) : + -1; +} + +static int kzt_lazy_production_validate( + const kzt_lazy_binding_pending_t *pending, uintptr_t guest_target, + void *opaque) +{ + kzt_lazy_completion_production_state_t *state = opaque; + box64context_t *context = state ? state->context : NULL; + kzt_guest_lazy_resolver_t resolver; + + if (!pending || !context || pending->context_id != (uintptr_t)context || + !guest_target || !state->source_lease.active || + state->source_lease.namespace_id != pending->namespace_id || + kzt_guest_registry_find_lazy_resolver( + KztGuestRegistryForContext(context), pending->source_link_map, + pending->source_generation, pending->namespace_id, + &resolver) != 0) { + return 0; + } + return resolver.guest_link_map == pending->source_link_map && + resolver.guest_resolver == pending->guest_resolver; +} + +static int kzt_lazy_production_route( + const kzt_lazy_binding_pending_t *pending, uintptr_t guest_target, + kzt_lazy_binding_route_result_t *result, void *opaque) +{ + kzt_lazy_completion_production_state_t *state = opaque; + + return state ? kzt_production_lazy_route_guest_target_leased( + state->context, pending, guest_target, + &state->source_lease, result) : + -1; +} + +int kzt_production_lazy_complete( + box64context_t *context, kzt_lazy_binding_pending_t *pending, + kzt_lazy_binding_result_t *result) +{ + kzt_lazy_completion_production_state_t state = { + .context = context, + .pending = pending, + }; + kzt_lazy_binding_ops_t ops = { + .load_slot = kzt_lazy_production_load_slot, + .validate_post_bind = kzt_lazy_production_validate, + .route_guest_target = kzt_lazy_production_route, + .opaque = &state, + }; + int status = kzt_lazy_binding_complete(pending, &ops, result); + + kzt_guest_registry_source_lease_release(&state.source_lease); + return status; +} + +#endif diff --git a/target/i386/latx/context/kzt_jump_slot_route.c b/target/i386/latx/context/kzt_jump_slot_route.c new file mode 100644 index 00000000000..b695411693f --- /dev/null +++ b/target/i386/latx/context/kzt_jump_slot_route.c @@ -0,0 +1,250 @@ +#include "kzt_jump_slot_route.h" + +#include + +typedef struct kzt_jump_slot_route_slot_state { + const kzt_jump_slot_route_ops_t *ops; + uintptr_t last_read; + uintptr_t committed_value; + int last_read_present; + int write_succeeded; + int cas_mismatch; +} kzt_jump_slot_route_slot_state_t; + +static int route_slot_read(uintptr_t slot_addr, uintptr_t *value, void *opaque) +{ + kzt_jump_slot_route_slot_state_t *state = opaque; + + if (!state || !state->ops || !state->ops->load_slot || !value || + state->ops->load_slot(slot_addr, value, state->ops->opaque) != 0) { + return -1; + } + state->last_read = *value; + state->last_read_present = 1; + return 0; +} + +static int route_slot_write(uintptr_t slot_addr, uintptr_t value, void *opaque) +{ + kzt_jump_slot_route_slot_state_t *state = opaque; + uintptr_t expected; + int exchanged; + + if (!state || !state->ops || !state->ops->compare_exchange_slot || + !state->last_read_present) { + return -1; + } + expected = state->write_succeeded ? state->committed_value : + state->last_read; + exchanged = state->ops->compare_exchange_slot( + slot_addr, &expected, value, state->ops->opaque); + if (exchanged > 0) { + state->last_read = value; + state->committed_value = value; + state->write_succeeded = 1; + } else { + state->last_read = expected; + state->cas_mismatch = exchanged == 0; + } + return exchanged > 0 ? 0 : -1; +} + +static int route_slot_begin_write( + uintptr_t slot_addr, kzt_patch_spike_permission_lease_t *lease, + void *opaque) +{ + kzt_jump_slot_route_slot_state_t *state = opaque; + + if (!state || !state->ops || !state->ops->begin_slot_write) { + return -1; + } + return state->ops->begin_slot_write(slot_addr, lease, state->ops->opaque); +} + +static int route_slot_end_write(kzt_patch_spike_permission_lease_t *lease, + void *opaque) +{ + kzt_jump_slot_route_slot_state_t *state = opaque; + + if (!state || !state->ops || !state->ops->end_slot_write) { + return -1; + } + return state->ops->end_slot_write(lease, state->ops->opaque); +} + +static int route_slot_validate_generation(const kzt_patch_decision_t *decision, + void *opaque) +{ + kzt_jump_slot_route_slot_state_t *state = opaque; + + if (!state || !state->ops || !state->ops->validate_write_generation) { + return -1; + } + return state->ops->validate_write_generation(decision, state->ops->opaque); +} + +static int route_has_owner_evidence( + const kzt_rela_immediate_candidate_request_t *request) +{ + return request && request->owner_match == KZT_PATCH_OWNER_MATCH && + request->current_owner.known && + request->current_owner.link_map_addr && + request->current_owner.generation; +} + +static int route_owner_identity_matches( + const kzt_rela_immediate_candidate_request_t *request, + const kzt_patch_object_ref_t *acquired_owner) +{ + return route_has_owner_evidence(request) && acquired_owner && + request->current_owner.link_map_addr == + acquired_owner->link_map_addr && + request->current_owner.generation == acquired_owner->generation; +} + +static int route_legacy_fallback( + const kzt_jump_slot_route_input_t *input, + const kzt_jump_slot_route_ops_t *ops, + kzt_jump_slot_route_result_t *result) +{ + uintptr_t expected = result->observed_value; + int exchanged; + + if (input->preserve_observed_on_failure) { + result->status = KZT_JUMP_SLOT_ROUTE_GUEST_PRESERVED; + result->selected_target = result->observed_value; + result->final_value = result->observed_value; + return 0; + } + + result->legacy_fallback_attempted = 1; + result->selected_target = input->request.legacy_target; + exchanged = ops->compare_exchange_slot( + input->request.slot_addr, &expected, input->request.legacy_target, + ops->opaque); + if (exchanged < 0) { + result->status = KZT_JUMP_SLOT_ROUTE_WRITE_ERROR; + result->final_value = expected; + return 0; + } + if (exchanged == 0) { + result->status = KZT_JUMP_SLOT_ROUTE_CAS_MISMATCH; + result->final_value = expected; + return 0; + } + result->status = KZT_JUMP_SLOT_ROUTE_LEGACY_APPLIED; + result->final_value = input->request.legacy_target; + return 0; +} + +int kzt_jump_slot_route_apply(const kzt_jump_slot_route_input_t *input, + const kzt_jump_slot_route_ops_t *ops, + kzt_jump_slot_route_result_t *result) +{ + kzt_rela_immediate_candidate_request_t request; + kzt_patch_object_ref_t acquired_owner; + kzt_guest_library_handle_t handle; + kzt_jump_slot_route_slot_state_t slot_state; + kzt_patch_spike_slot_ops_t slot_ops; + int acquired = 0; + + if (!result) { + return -1; + } + memset(result, 0, sizeof(*result)); + result->status = KZT_JUMP_SLOT_ROUTE_WRITE_ERROR; + if (!input || !ops || !ops->load_slot || + !ops->compare_exchange_slot || !input->request.slot_addr) { + return -1; + } + result->expected_guest_target = input->request.expected_guest_target; + if (!input->enabled) { + result->status = KZT_JUMP_SLOT_ROUTE_BYPASS; + return 0; + } + if (ops->load_slot(input->request.slot_addr, &result->observed_value, + ops->opaque) != 0) { + return 0; + } + if (input->request.slot_current_value_present && + result->observed_value != input->request.slot_current_value) { + result->status = KZT_JUMP_SLOT_ROUTE_CAS_MISMATCH; + result->final_value = result->observed_value; + return 0; + } + + request = input->request; + request.slot_current_value_present = 1; + request.slot_current_value = result->observed_value; + memset(&handle, 0, sizeof(handle)); + memset(&acquired_owner, 0, sizeof(acquired_owner)); + memset(&slot_state, 0, sizeof(slot_state)); + + if (input->resolved_target_matches_legacy && + input->expected_guest_target_present && + request.expected_guest_target && input->resolved_provider && + ops->enrich_base && ops->acquire_exact_provider && + ops->release_exact_provider && ops->enrich_bridge && + ops->try_native_writer && + ops->enrich_base(&request, ops->opaque) == 0 && + route_has_owner_evidence(&request)) { + acquired_owner = request.current_owner; + if (ops->acquire_exact_provider(&acquired_owner, + input->resolved_provider, &handle, + ops->opaque) == 0) { + acquired = 1; + result->exact_provider_acquired = 1; + if (handle.library == input->resolved_provider) { + result->exact_provider_matched = 1; + if (ops->enrich_bridge(&request, handle.library, + ops->opaque) == 0 && + route_owner_identity_matches(&request, + &acquired_owner) && + (!ops->validate_source_identity || + (result->source_identity_rechecked = 1, + ops->validate_source_identity(&request, + ops->opaque) > 0))) { + slot_state = (kzt_jump_slot_route_slot_state_t){ + .ops = ops, + }; + slot_ops = (kzt_patch_spike_slot_ops_t){ + .read_slot = route_slot_read, + .write_slot = route_slot_write, + .begin_write = route_slot_begin_write, + .end_write = route_slot_end_write, + .validate_generation = route_slot_validate_generation, + .opaque = &slot_state, + }; + result->native_writer_called = 1; + result->writer_status = ops->try_native_writer( + &request, &slot_ops, ops->opaque); + } + } + } + } + + if (acquired) { + ops->release_exact_provider(&handle, ops->opaque); + } + if (result->native_writer_called && slot_state.cas_mismatch) { + result->status = KZT_JUMP_SLOT_ROUTE_CAS_MISMATCH; + result->selected_target = slot_state.last_read; + result->final_value = slot_state.last_read; + return 0; + } + if (result->writer_status == KZT_JUMP_SLOT_ROUTE_WRITER_APPLIED) { + result->status = KZT_JUMP_SLOT_ROUTE_NATIVE_APPLIED; + result->selected_target = request.native_bridge_target; + result->final_value = request.native_bridge_target; + return 0; + } + if (result->writer_status == KZT_JUMP_SLOT_ROUTE_WRITER_PRESERVE) { + result->status = KZT_JUMP_SLOT_ROUTE_GUEST_PRESERVED; + result->selected_target = slot_state.last_read_present ? + slot_state.last_read : result->observed_value; + result->final_value = result->selected_target; + return 0; + } + + return route_legacy_fallback(input, ops, result); +} diff --git a/target/i386/latx/context/kzt_lazy_binding.c b/target/i386/latx/context/kzt_lazy_binding.c new file mode 100644 index 00000000000..0948367c45a --- /dev/null +++ b/target/i386/latx/context/kzt_lazy_binding.c @@ -0,0 +1,201 @@ +#include "kzt_lazy_binding.h" + +#include + +static void lazy_result_init(kzt_lazy_binding_result_t *result) +{ + if (!result) { + return; + } + memset(result, 0, sizeof(*result)); + result->status = KZT_LAZY_BINDING_ERROR; + result->reason = KZT_LAZY_BINDING_REASON_INVALID_REQUEST; +} + +static int lazy_copy_text(char *dst, size_t size, const char *src, + const char **stored) +{ + size_t length; + + if (stored) { + *stored = NULL; + } + if (!dst || !size || !stored) { + return -1; + } + if (!src || !src[0]) { + return 0; + } + length = strlen(src); + if (length >= size) { + return -1; + } + memcpy(dst, src, length + 1); + *stored = dst; + return 0; +} + +static void lazy_consume(kzt_lazy_binding_pending_t *pending, + kzt_lazy_binding_result_t *result) +{ + if (pending) { + memset(pending, 0, sizeof(*pending)); + } + if (result) { + result->pending_armed = 0; + result->pending_cleared = 1; + } +} + +int kzt_lazy_binding_begin( + const kzt_lazy_binding_begin_request_t *request, + kzt_lazy_binding_pending_t *pending, + kzt_lazy_binding_result_t *result) +{ + kzt_lazy_binding_pending_t candidate; + + lazy_result_init(result); + if (!request || !pending || !result) { + return -1; + } + result->selected_target = request->guest_resolver; + result->slot_before = request->unresolved_stub; + result->slot_after = request->unresolved_stub; + if (!request->enabled) { + result->status = KZT_LAZY_BINDING_BYPASS; + result->reason = KZT_LAZY_BINDING_REASON_DISABLED; + return 0; + } + /* Nested resolvers share a context. A pending owned by another context + * cannot complete this request and must not block its fail-open path. */ + if (pending->armed && pending->context_id == request->context_id) { + result->status = KZT_LAZY_BINDING_GUEST_PRESERVED; + result->reason = KZT_LAZY_BINDING_REASON_PENDING_BUSY; + return 0; + } + if (request->namespace_kind != KZT_GUEST_LIBRARY_NAMESPACE_MAIN || + request->namespace_id != 0) { + result->status = KZT_LAZY_BINDING_GUEST_PRESERVED; + result->reason = KZT_LAZY_BINDING_REASON_NON_MAIN_NAMESPACE; + return 0; + } + if (!request->context_id || !request->source_link_map || + !request->source_generation || !request->slot_addr || + !request->unresolved_stub || !request->guest_resolver) { + result->status = KZT_LAZY_BINDING_GUEST_PRESERVED; + result->reason = request->guest_resolver ? + KZT_LAZY_BINDING_REASON_INVALID_REQUEST : + KZT_LAZY_BINDING_REASON_RESOLVER_MISSING; + return 0; + } + + memset(&candidate, 0, sizeof(candidate)); + candidate.context_id = request->context_id; + candidate.source_link_map = request->source_link_map; + candidate.source_generation = request->source_generation; + candidate.namespace_id = request->namespace_id; + candidate.namespace_kind = request->namespace_kind; + candidate.relocation_index = request->relocation_index; + candidate.slot_addr = request->slot_addr; + candidate.unresolved_stub = request->unresolved_stub; + candidate.guest_resolver = request->guest_resolver; + candidate.addend = request->addend; + candidate.version_evidence = request->version_evidence; + if (lazy_copy_text(candidate.symbol_storage, + sizeof(candidate.symbol_storage), request->symbol, + &candidate.symbol) != 0 || + lazy_copy_text(candidate.version_storage, + sizeof(candidate.version_storage), request->version, + &candidate.version) != 0) { + result->status = KZT_LAZY_BINDING_GUEST_PRESERVED; + result->reason = KZT_LAZY_BINDING_REASON_INVALID_REQUEST; + return 0; + } + candidate.armed = 1; + *pending = candidate; + /* Repair the pointers after the structure copy. */ + pending->symbol = candidate.symbol ? pending->symbol_storage : NULL; + pending->version = candidate.version ? pending->version_storage : NULL; + result->status = KZT_LAZY_BINDING_HANDOFF_GUEST; + result->reason = KZT_LAZY_BINDING_REASON_NONE; + result->pending_armed = 1; + return 0; +} + +int kzt_lazy_binding_complete( + kzt_lazy_binding_pending_t *pending, + const kzt_lazy_binding_ops_t *ops, + kzt_lazy_binding_result_t *result) +{ + uintptr_t guest_target = 0; + kzt_lazy_binding_route_result_t route_result; + + lazy_result_init(result); + if (!pending || !ops || !result || !pending->armed || !ops->load_slot) { + return -1; + } + if (ops->load_slot(pending->slot_addr, &guest_target, ops->opaque) != 0) { + result->status = KZT_LAZY_BINDING_GUEST_PRESERVED; + result->reason = KZT_LAZY_BINDING_REASON_SLOT_READ_ERROR; + lazy_consume(pending, result); + return 0; + } + result->slot_before = guest_target; + result->slot_after = guest_target; + result->selected_target = guest_target; + if (guest_target == pending->unresolved_stub) { + result->status = KZT_LAZY_BINDING_GUEST_PRESERVED; + result->reason = KZT_LAZY_BINDING_REASON_SLOT_UNCHANGED; + lazy_consume(pending, result); + return 0; + } + if (!kzt_symbol_version_evidence_valid(pending->version_evidence, + pending->version)) { + result->status = KZT_LAZY_BINDING_GUEST_PRESERVED; + result->reason = KZT_LAZY_BINDING_REASON_MISSING_VERSION; + lazy_consume(pending, result); + return 0; + } + if (!ops->validate_post_bind || + ops->validate_post_bind(pending, guest_target, ops->opaque) <= 0) { + result->status = KZT_LAZY_BINDING_GUEST_PRESERVED; + result->reason = KZT_LAZY_BINDING_REASON_POST_BIND_INVALID; + lazy_consume(pending, result); + return 0; + } + if (!ops->route_guest_target) { + result->status = KZT_LAZY_BINDING_GUEST_PRESERVED; + result->reason = KZT_LAZY_BINDING_REASON_NATIVE_UNAVAILABLE; + lazy_consume(pending, result); + return 0; + } + memset(&route_result, 0, sizeof(route_result)); + if (ops->route_guest_target(pending, guest_target, &route_result, + ops->opaque) != 0 || + route_result.status == KZT_LAZY_BINDING_ROUTE_ERROR) { + result->status = KZT_LAZY_BINDING_GUEST_PRESERVED; + result->reason = KZT_LAZY_BINDING_REASON_NATIVE_UNAVAILABLE; + } else if (route_result.status == KZT_LAZY_BINDING_ROUTE_NATIVE_APPLIED) { + result->status = KZT_LAZY_BINDING_NATIVE_APPLIED; + result->reason = KZT_LAZY_BINDING_REASON_NATIVE_APPLIED; + } else if (route_result.status == KZT_LAZY_BINDING_ROUTE_CAS_MISMATCH) { + result->status = KZT_LAZY_BINDING_CAS_MISMATCH; + result->reason = KZT_LAZY_BINDING_REASON_CAS_MISMATCH; + } else { + result->status = KZT_LAZY_BINDING_GUEST_PRESERVED; + result->reason = KZT_LAZY_BINDING_REASON_NATIVE_UNAVAILABLE; + } + result->selected_target = route_result.selected_target ? + route_result.selected_target : guest_target; + result->slot_after = route_result.final_value ? + route_result.final_value : guest_target; + lazy_consume(pending, result); + return 0; +} + +void kzt_lazy_binding_cancel(kzt_lazy_binding_pending_t *pending) +{ + if (pending) { + memset(pending, 0, sizeof(*pending)); + } +} diff --git a/target/i386/latx/context/kzt_lazy_diagnostics.c b/target/i386/latx/context/kzt_lazy_diagnostics.c new file mode 100644 index 00000000000..9464edfae45 --- /dev/null +++ b/target/i386/latx/context/kzt_lazy_diagnostics.c @@ -0,0 +1,247 @@ +#include "kzt_lazy_diagnostics.h" + +#include +#include + +#define KZT_LAZY_DIAGNOSTIC_SCHEMA 1UL + +static void kzt_lazy_diagnostic_copy_token(char *dst, size_t dst_size, + const char *src) +{ + size_t i; + + if (!dst || dst_size == 0) { + return; + } + if (!src || !src[0]) { + src = "(none)"; + } + + for (i = 0; i + 1 < dst_size && src[i]; ++i) { + unsigned char value = (unsigned char)src[i]; + + if (value <= ' ' || value == '"' || value == '\\' || value == '=') { + dst[i] = '_'; + } else { + dst[i] = (char)value; + } + } + dst[i] = '\0'; +} + +static const char *kzt_lazy_diagnostic_status_name( + const kzt_lazy_binding_result_t *binding_result) +{ + if (!binding_result) { + return "INVALID_RESULT"; + } + + switch (binding_result->status) { + case KZT_LAZY_BINDING_BYPASS: + return "BYPASS"; + case KZT_LAZY_BINDING_HANDOFF_GUEST: + return "HANDOFF_GUEST"; + case KZT_LAZY_BINDING_WAITING_GUEST_TARGET: + return "WAITING_GUEST_TARGET"; + case KZT_LAZY_BINDING_NATIVE_APPLIED: + return "NATIVE_APPLIED"; + case KZT_LAZY_BINDING_CAS_MISMATCH: + return "CAS_MISMATCH"; + case KZT_LAZY_BINDING_ERROR: + return "ERROR"; + case KZT_LAZY_BINDING_GUEST_PRESERVED: + switch (binding_result->reason) { + case KZT_LAZY_BINDING_REASON_SLOT_UNCHANGED: + return "SLOT_UNCHANGED"; + case KZT_LAZY_BINDING_REASON_MISSING_VERSION: + return "MISSING_VERSION"; + default: + return "GUEST_PRESERVED"; + } + } + + return "UNKNOWN"; +} + +static const char *kzt_lazy_diagnostic_reason_name( + kzt_lazy_binding_reason_t reason) +{ + switch (reason) { + case KZT_LAZY_BINDING_REASON_NONE: + return "NONE"; + case KZT_LAZY_BINDING_REASON_DISABLED: + return "DISABLED"; + case KZT_LAZY_BINDING_REASON_INVALID_REQUEST: + return "INVALID_REQUEST"; + case KZT_LAZY_BINDING_REASON_PENDING_OCCUPIED: + return "PENDING_OCCUPIED"; + case KZT_LAZY_BINDING_REASON_GENERATION_CHANGED: + return "GENERATION_CHANGED"; + case KZT_LAZY_BINDING_REASON_NON_MAIN_NAMESPACE: + return "NON_MAIN_NAMESPACE"; + case KZT_LAZY_BINDING_REASON_RESOLVER_MISSING: + return "RESOLVER_MISSING"; + case KZT_LAZY_BINDING_REASON_SLOT_UNCHANGED: + return "SLOT_UNCHANGED"; + case KZT_LAZY_BINDING_REASON_MISSING_VERSION: + return "MISSING_VERSION"; + case KZT_LAZY_BINDING_REASON_POST_BIND_INVALID: + return "POST_BIND_INVALID"; + case KZT_LAZY_BINDING_REASON_NATIVE_UNAVAILABLE: + return "NATIVE_UNAVAILABLE"; + case KZT_LAZY_BINDING_REASON_NATIVE_APPLIED: + return "NATIVE_APPLIED"; + case KZT_LAZY_BINDING_REASON_CAS_MISMATCH: + return "CAS_MISMATCH"; + case KZT_LAZY_BINDING_REASON_SLOT_READ_ERROR: + return "SLOT_READ_ERROR"; + case KZT_LAZY_BINDING_REASON_CAS_ERROR: + return "CAS_ERROR"; + } + + return "UNKNOWN"; +} + +int kzt_lazy_diagnostic_record_build( + const kzt_lazy_binding_pending_t *pending, + const kzt_lazy_binding_result_t *binding_result, + kzt_lazy_diagnostic_record_t *record) +{ + if (!pending || !binding_result || !record) { + return -1; + } + + memset(record, 0, sizeof(*record)); + record->schema = KZT_LAZY_DIAGNOSTIC_SCHEMA; + kzt_lazy_diagnostic_copy_token(record->symbol, sizeof(record->symbol), + pending->symbol); + kzt_lazy_diagnostic_copy_token( + record->first_execution_route, + sizeof(record->first_execution_route), "guest"); + kzt_lazy_diagnostic_copy_token( + record->completion_route_status, + sizeof(record->completion_route_status), + kzt_lazy_diagnostic_status_name(binding_result)); + record->slot_before = pending->unresolved_stub; + record->slot_after_guest = binding_result->slot_before; + record->selected_second_target = binding_result->slot_after; + kzt_lazy_diagnostic_copy_token(record->reason, sizeof(record->reason), + kzt_lazy_diagnostic_reason_name( + binding_result->reason)); + return 0; +} + +kzt_lazy_diagnostic_format_status_t kzt_lazy_diagnostic_format( + const kzt_lazy_diagnostic_record_t *record, + char *buffer, + size_t buffer_size) +{ + int written; + + if (!record || !buffer || buffer_size == 0) { + return KZT_LAZY_DIAGNOSTIC_FORMAT_ERROR; + } + + written = snprintf( + buffer, buffer_size, + "kzt_lazy_diagnostic schema=%lu symbol=%s first_execution_route=%s " + "completion_route_status=%s slot_before=0x%lx " + "slot_after_guest=0x%lx selected_second_target=0x%lx reason=%s", + record->schema, record->symbol, record->first_execution_route, + record->completion_route_status, + (unsigned long)record->slot_before, + (unsigned long)record->slot_after_guest, + (unsigned long)record->selected_second_target, record->reason); + if (written < 0) { + return KZT_LAZY_DIAGNOSTIC_FORMAT_ERROR; + } + if ((size_t)written >= buffer_size) { + return KZT_LAZY_DIAGNOSTIC_FORMAT_TRUNCATED; + } + + return KZT_LAZY_DIAGNOSTIC_FORMAT_OK; +} + +int kzt_lazy_diagnostics_stderr_sink(const char *line, + size_t line_length, + void *opaque) +{ + FILE *stream = opaque ? (FILE *)opaque : stderr; + + if (!line || !stream) { + return -1; + } + if (fwrite(line, 1, line_length, stream) != line_length) { + return -1; + } + if (fputc('\n', stream) == EOF) { + return -1; + } + return fflush(stream); +} + +int kzt_lazy_diagnostics_emit( + const kzt_lazy_diagnostic_input_t *input, + kzt_lazy_diagnostic_emit_result_t *result) +{ + if (!input || !result) { + return -1; + } + + memset(result, 0, sizeof(*result)); + result->status = KZT_LAZY_DIAGNOSTIC_EMIT_DISABLED; + result->format_status = KZT_LAZY_DIAGNOSTIC_FORMAT_ERROR; + if (!input->enabled) { + return 0; + } + if (kzt_lazy_diagnostic_record_build(input->pending, + input->binding_result, + &result->record) != 0) { + result->status = KZT_LAZY_DIAGNOSTIC_EMIT_FORMAT_FAILED; + return 0; + } + result->record_present = 1; + result->format_status = kzt_lazy_diagnostic_format( + &result->record, input->buffer, input->buffer_size); + if (result->format_status == KZT_LAZY_DIAGNOSTIC_FORMAT_TRUNCATED) { + result->status = KZT_LAZY_DIAGNOSTIC_EMIT_FORMAT_TRUNCATED; + return 0; + } + if (result->format_status != KZT_LAZY_DIAGNOSTIC_FORMAT_OK) { + result->status = KZT_LAZY_DIAGNOSTIC_EMIT_FORMAT_FAILED; + return 0; + } + if (!input->sink) { + result->status = KZT_LAZY_DIAGNOSTIC_EMIT_SINK_FAILED; + result->sink_status = -1; + return 0; + } + + result->sink_status = input->sink(input->buffer, strlen(input->buffer), + input->sink_opaque); + if (result->sink_status != 0) { + result->status = KZT_LAZY_DIAGNOSTIC_EMIT_SINK_FAILED; + return 0; + } + + result->status = KZT_LAZY_DIAGNOSTIC_EMIT_EMITTED; + return 0; +} + +int kzt_lazy_diagnostics_emit_production( + const kzt_lazy_binding_pending_t *pending, + const kzt_lazy_binding_result_t *binding_result, + kzt_lazy_diagnostic_emit_result_t *result) +{ + char buffer[KZT_LAZY_DIAGNOSTIC_LINE_LIMIT]; + kzt_lazy_diagnostic_input_t input; + + memset(&input, 0, sizeof(input)); + input.enabled = 1; + input.pending = pending; + input.binding_result = binding_result; + input.buffer = buffer; + input.buffer_size = sizeof(buffer); + input.sink = kzt_lazy_diagnostics_stderr_sink; + return kzt_lazy_diagnostics_emit(&input, result); +} diff --git a/target/i386/latx/context/kzt_observation_adapter.c b/target/i386/latx/context/kzt_observation_adapter.c new file mode 100644 index 00000000000..da9b78e9e7f --- /dev/null +++ b/target/i386/latx/context/kzt_observation_adapter.c @@ -0,0 +1,371 @@ +#include "kzt_observation_adapter.h" + +#include + +#include "kzt_guest_library_binding.h" + +static kzt_observation_adapter_result_t kzt_adapter_result_from_registry( + kzt_guest_registry_result_t registry_result) +{ + switch (registry_result) { + case KZT_GUEST_REGISTRY_ADDED: + return KZT_OBSERVATION_ADAPTER_ADDED; + case KZT_GUEST_REGISTRY_UNCHANGED: + return KZT_OBSERVATION_ADAPTER_UNCHANGED; + case KZT_GUEST_REGISTRY_UPDATED: + return KZT_OBSERVATION_ADAPTER_UPDATED; + case KZT_GUEST_REGISTRY_CONFLICT: + return KZT_OBSERVATION_ADAPTER_CONFLICT; + case KZT_GUEST_REGISTRY_DISABLED: + case KZT_GUEST_REGISTRY_ERROR: + return KZT_OBSERVATION_ADAPTER_REGISTRY_FAILED; + case KZT_GUEST_REGISTRY_RESULT_COUNT: + break; + } + + return KZT_OBSERVATION_ADAPTER_REGISTRY_FAILED; +} + +static int kzt_adapter_registry_result_allows_dynamic_parse( + kzt_guest_registry_result_t registry_result) +{ + return registry_result == KZT_GUEST_REGISTRY_ADDED || + registry_result == KZT_GUEST_REGISTRY_UNCHANGED || + registry_result == KZT_GUEST_REGISTRY_UPDATED; +} + +static void kzt_adapter_note_dynamic_failure( + const kzt_observation_adapter_request_t *request, + kzt_guest_registry_result_t result, + kzt_observation_adapter_dynamic_diagnostic_t *dynamic_diagnostic) +{ + kzt_guest_registry_observation_diagnostic_t *registry_diagnostic = NULL; + + if (dynamic_diagnostic) { + registry_diagnostic = &dynamic_diagnostic->registry; + } + + (void)kzt_guest_registry_note_diagnostic( + request ? request->registry : NULL, result, + request ? request->link_map_addr : 0, registry_diagnostic); +} + +static void kzt_adapter_compare_dynamic_views( + const kzt_observation_adapter_request_t *request, + const kzt_guest_dynamic_parse_result_t *parse_result, + unsigned long generation, + kzt_observation_adapter_dynamic_diagnostic_t *dynamic_diagnostic) +{ + kzt_guest_dynamic_view_t existing_view = { 0 }; + kzt_guest_dynamic_parse_result_t existing_result = { 0 }; + kzt_guest_dynamic_diagnostic_report_t report; + kzt_guest_field_status_t existing_status = KZT_GUEST_FIELD_NOT_PARSED; + unsigned long existing_generation = 0; + + if (!request || !request->diagnostics_enabled || !parse_result || + !dynamic_diagnostic || generation == 0 || + kzt_guest_registry_find_dynamic_view( + request->registry, request->link_map_addr, &existing_view, + &existing_status, &existing_generation) != 0 || + existing_status != KZT_GUEST_FIELD_OK || + existing_generation != generation) { + return; + } + + existing_result.status = existing_view.status; + existing_result.error = KZT_GUEST_DYNAMIC_ERROR_NONE; + existing_result.entry_count = existing_view.entry_count; + existing_result.scan_limit = existing_view.scan_limit; + existing_result.unknown_tag_count = existing_view.unknown_tag_count; + existing_result.first_unknown_tag = existing_view.first_unknown_tag; + existing_result.first_unknown_tag_index = + existing_view.first_unknown_tag_index; + existing_result.view = existing_view; + if (kzt_guest_dynamic_diagnostics_compare(&existing_result, parse_result, + &report) != 0 || + kzt_guest_dynamic_diagnostics_summarize( + &report, request->link_map_addr, generation, + &dynamic_diagnostic->comparison) != 0) { + return; + } + + dynamic_diagnostic->comparison_attempted = 1; +} + +static int kzt_adapter_reuse_complete_dynamic_view( + const kzt_observation_adapter_request_t *request, + const kzt_guest_object_observation_t *observation, + kzt_guest_registry_result_t registry_result, + unsigned long generation, + kzt_observation_adapter_dynamic_diagnostic_t *dynamic_diagnostic) +{ + kzt_guest_dynamic_view_t view = { 0 }; + kzt_guest_field_status_t status = KZT_GUEST_FIELD_NOT_PARSED; + unsigned long existing_generation = 0; + + if (!request || !observation || request->library_bindings || + request->dynamic_diagnostics_force_compare || + registry_result != KZT_GUEST_REGISTRY_UNCHANGED || + generation == 0 || + kzt_guest_registry_find_dynamic_view( + request->registry, observation->link_map_addr, &view, &status, + &existing_generation) != 0 || + status != KZT_GUEST_FIELD_OK || + existing_generation != generation || + view.status != KZT_GUEST_DYNAMIC_COMPLETE || + view.dynamic_addr != observation->dynamic_addr.value || + view.load_bias != observation->load_bias.value) { + return 0; + } + + if (dynamic_diagnostic) { + dynamic_diagnostic->cache_hit = 1; + dynamic_diagnostic->parse_return = 0; + dynamic_diagnostic->dynamic_addr = view.dynamic_addr; + dynamic_diagnostic->status = view.status; + dynamic_diagnostic->error = KZT_GUEST_DYNAMIC_ERROR_NONE; + dynamic_diagnostic->entry_count = view.entry_count; + dynamic_diagnostic->commit_result = KZT_GUEST_REGISTRY_UNCHANGED; + } + return 1; +} + +static void kzt_observe_guest_dynamic_view( + const kzt_observation_adapter_request_t *request, + const kzt_guest_object_observation_t *observation, + kzt_guest_registry_result_t registry_result, + unsigned long generation, + kzt_observation_adapter_dynamic_diagnostic_t *dynamic_diagnostic) +{ + kzt_guest_dynamic_parse_result_t parse_result = { 0 }; + kzt_guest_registry_result_t commit_result; + int parse_return; + + if (dynamic_diagnostic) { + memset(dynamic_diagnostic, 0, sizeof(*dynamic_diagnostic)); + dynamic_diagnostic->commit_result = KZT_GUEST_REGISTRY_RESULT_COUNT; + } + + if (!request || !observation || + !kzt_adapter_registry_result_allows_dynamic_parse(registry_result)) { + return; + } + + if (observation->dynamic_addr.status != KZT_GUEST_FIELD_OK || + observation->dynamic_addr.value == 0 || + observation->load_bias.status != KZT_GUEST_FIELD_OK) { + return; + } + + if (kzt_adapter_reuse_complete_dynamic_view( + request, observation, registry_result, generation, + dynamic_diagnostic)) { + return; + } + + if (dynamic_diagnostic) { + dynamic_diagnostic->attempted = 1; + dynamic_diagnostic->dynamic_addr = observation->dynamic_addr.value; + } + + parse_return = kzt_guest_dynamic_parse(observation->dynamic_addr.value, + observation->load_bias.value, + request->reader_ops, + &parse_result); + if (dynamic_diagnostic) { + dynamic_diagnostic->parse_return = parse_return; + dynamic_diagnostic->status = parse_result.status; + dynamic_diagnostic->error = parse_result.error; + dynamic_diagnostic->entry_count = parse_result.entry_count; + dynamic_diagnostic->read_error_addr = parse_result.read_error_addr; + } + + kzt_adapter_compare_dynamic_views(request, &parse_result, generation, + dynamic_diagnostic); + + if (parse_return != 0) { + kzt_adapter_note_dynamic_failure( + request, KZT_GUEST_REGISTRY_ERROR, dynamic_diagnostic); + kzt_guest_dynamic_parse_result_clear(&parse_result); + return; + } + + if (parse_result.status != KZT_GUEST_DYNAMIC_COMPLETE) { + kzt_adapter_note_dynamic_failure( + request, KZT_GUEST_REGISTRY_ERROR, dynamic_diagnostic); + } + + commit_result = kzt_guest_registry_commit_dynamic_view( + request->registry, observation->link_map_addr, generation, + &parse_result.view); + if (dynamic_diagnostic) { + dynamic_diagnostic->commit_attempted = 1; + dynamic_diagnostic->commit_result = commit_result; + } + if (commit_result == KZT_GUEST_REGISTRY_DISABLED || + commit_result == KZT_GUEST_REGISTRY_ERROR) { + kzt_adapter_note_dynamic_failure(request, commit_result, + dynamic_diagnostic); + } + + kzt_guest_dynamic_parse_result_clear(&parse_result); +} + +static kzt_observation_adapter_result_t kzt_observe_guest_object( + const kzt_observation_adapter_request_t *request, + kzt_guest_registry_observation_diagnostic_t *registry_diagnostic, + kzt_observation_adapter_dynamic_diagnostic_t *dynamic_diagnostic) +{ + kzt_guest_object_observation_t observation; + kzt_guest_registry_result_t registry_result; + + if (!request || !request->enabled) { + if (request) { + kzt_guest_registry_note_diagnostic( + request->registry, KZT_GUEST_REGISTRY_DISABLED, + request->link_map_addr, registry_diagnostic); + } + return KZT_OBSERVATION_ADAPTER_DISABLED; + } + + if (kzt_guest_link_map_read_observation(request->link_map_addr, + request->reader_ops, + &observation) != 0) { + kzt_guest_registry_note_diagnostic( + request->registry, KZT_GUEST_REGISTRY_ERROR, + request->link_map_addr, registry_diagnostic); + return KZT_OBSERVATION_ADAPTER_READER_FAILED; + } + + if (observation.link_map_addr == 0) { + kzt_guest_link_map_observation_clear(&observation); + kzt_guest_registry_note_diagnostic( + request->registry, KZT_GUEST_REGISTRY_ERROR, + request->link_map_addr, registry_diagnostic); + return KZT_OBSERVATION_ADAPTER_READER_FAILED; + } + + if (request->namespace_id_present && + observation.namespace_id.status == KZT_GUEST_FIELD_UNKNOWN) { + observation.namespace_id.value = request->namespace_id; + observation.namespace_id.status = KZT_GUEST_FIELD_OK; + } + if (request->map_range_present && request->map_start < request->map_end && + observation.map_start.status == KZT_GUEST_FIELD_UNKNOWN && + observation.map_end.status == KZT_GUEST_FIELD_UNKNOWN) { + observation.map_start.value = request->map_start; + observation.map_start.status = KZT_GUEST_FIELD_OK; + observation.map_end.value = request->map_end; + observation.map_end.status = KZT_GUEST_FIELD_OK; + } + + registry_result = kzt_guest_registry_observe_with_diagnostic( + request->registry, &observation, registry_diagnostic); + if (kzt_adapter_registry_result_allows_dynamic_parse(registry_result) && + registry_diagnostic && registry_diagnostic->generation && + observation.namespace_id.status == KZT_GUEST_FIELD_OK && + observation.namespace_id.value == 0) { + kzt_guest_library_binding_key_t key = { + .link_map_addr = observation.link_map_addr, + .generation = registry_diagnostic->generation, + .namespace_id = observation.namespace_id.value, + .namespace_kind = KZT_GUEST_LIBRARY_NAMESPACE_MAIN, + }; + kzt_guest_library_binding_result_t binding_result = + kzt_guest_library_note_observation( + request->library_bindings, &key); + if (binding_result == KZT_GUEST_LIBRARY_BINDING_CANCELLED) { + /* A loader pair was canceled by unload before this delayed + * observation arrived. Retire this generation so address reuse + * receives fresh evidence on retry. */ + (void)kzt_guest_registry_retire( + request->registry, key.link_map_addr, key.generation); + } + } + kzt_observe_guest_dynamic_view(request, &observation, registry_result, + registry_diagnostic + ? registry_diagnostic->generation + : 0, + dynamic_diagnostic); + kzt_guest_link_map_observation_clear(&observation); + + return kzt_adapter_result_from_registry(registry_result); +} + +static void kzt_observation_adapter_emit_diagnostic( + const kzt_observation_adapter_request_t *request, + kzt_observation_adapter_result_t result, + const kzt_guest_registry_observation_diagnostic_t *registry_diagnostic, + const kzt_observation_adapter_dynamic_diagnostic_t *dynamic_diagnostic) +{ + kzt_observation_adapter_diagnostic_t diagnostic = { 0 }; + + if (!request || !request->diagnostics_enabled || !request->diagnostic) { + return; + } + + diagnostic.enabled = 1; + diagnostic.result = result; + diagnostic.link_map_addr = request->link_map_addr; + diagnostic.emitted = 0; + if (registry_diagnostic) { + diagnostic.registry = *registry_diagnostic; + diagnostic.emitted = registry_diagnostic->enabled && + registry_diagnostic->emitted; + } + if (dynamic_diagnostic) { + diagnostic.dynamic = *dynamic_diagnostic; + if (dynamic_diagnostic->registry.enabled && + dynamic_diagnostic->registry.emitted) { + diagnostic.emitted = 1; + } + } + + if (!diagnostic.emitted) { + return; + } + + request->diagnostic(&diagnostic, request->diagnostic_opaque); +} + +int kzt_observe_guest_object_from_callback( + const kzt_observation_adapter_request_t *request, + kzt_observation_adapter_result_t *result) +{ + kzt_observation_adapter_result_t observation_result; + kzt_guest_registry_observation_diagnostic_t registry_diagnostic = { 0 }; + kzt_observation_adapter_dynamic_diagnostic_t dynamic_diagnostic = { + .commit_result = KZT_GUEST_REGISTRY_RESULT_COUNT, + }; + kzt_guest_library_callback_access_t callback_access = { 0 }; + int legacy_ret = 0; + + if (request && request->library_bindings && + kzt_guest_library_callback_access_begin_scoped( + request->library_bindings, request->link_map_addr, + request->loader_scope, + &callback_access) != 0) { + /* Unload won the address gate. No reader, parser, diagnostic, or + * legacy loader flow may touch this guest object after this point. */ + if (result) *result = KZT_OBSERVATION_ADAPTER_DISABLED; + return 0; + } + + observation_result = kzt_observe_guest_object(request, + ®istry_diagnostic, + &dynamic_diagnostic); + if (result) { + *result = observation_result; + } + + if (request && request->legacy_flow) { + legacy_ret = request->legacy_flow(request->link_map_addr, + request->legacy_opaque); + } + + kzt_observation_adapter_emit_diagnostic(request, observation_result, + ®istry_diagnostic, + &dynamic_diagnostic); + kzt_guest_library_callback_access_end(&callback_access); + return legacy_ret; +} diff --git a/target/i386/latx/context/kzt_owner_resolver.c b/target/i386/latx/context/kzt_owner_resolver.c new file mode 100644 index 00000000000..e23b45d833c --- /dev/null +++ b/target/i386/latx/context/kzt_owner_resolver.c @@ -0,0 +1,304 @@ +#include "kzt_owner_resolver.h" + +#include +#include + +typedef enum kzt_owner_lookup_status { + KZT_OWNER_LOOKUP_RESOLVED = 0, + KZT_OWNER_LOOKUP_NOT_FOUND, + KZT_OWNER_LOOKUP_AMBIGUOUS, +} kzt_owner_lookup_status_t; + +void kzt_owner_resolver_init(kzt_owner_resolution_t *resolution) +{ + if (!resolution) { + return; + } + + memset(resolution, 0, sizeof(*resolution)); + resolution->status = KZT_OWNER_RESOLVER_INVALID_ARGUMENT; + resolution->owner_match = KZT_PATCH_OWNER_UNKNOWN; +} + +static int kzt_owner_field_ok(kzt_guest_field_status_t status) +{ + return status == KZT_GUEST_FIELD_OK; +} + +static int kzt_owner_string_has_value(kzt_guest_field_status_t status) +{ + return status == KZT_GUEST_FIELD_OK || + status == KZT_GUEST_FIELD_TRUNCATED; +} + +static const char *kzt_owner_snapshot_string( + const kzt_guest_string_field_t *field) +{ + if (!field || !kzt_owner_string_has_value(field->status)) { + return NULL; + } + + return field->value; +} + +static void kzt_owner_copy_text(char *dst, size_t dst_size, + const char *src) +{ + if (!dst || dst_size == 0) { + return; + } + + if (!src) { + dst[0] = '\0'; + return; + } + + snprintf(dst, dst_size, "%s", src); +} + +static int kzt_owner_snapshot_has_valid_range( + const kzt_guest_object_snapshot_t *snapshot) +{ + if (!snapshot) { + return 0; + } + if (!kzt_owner_field_ok(snapshot->map_start.status) || + !kzt_owner_field_ok(snapshot->map_end.status)) { + return 0; + } + + return snapshot->map_start.value < snapshot->map_end.value; +} + +static int kzt_owner_snapshot_contains( + const kzt_guest_object_snapshot_t *snapshot, + uintptr_t address) +{ + if (!kzt_owner_snapshot_has_valid_range(snapshot)) { + return 0; + } + + return address >= snapshot->map_start.value && + address < snapshot->map_end.value; +} + +static void kzt_owner_ref_from_snapshot( + const kzt_guest_object_snapshot_t *snapshot, + kzt_owner_resolver_text_t *text, + kzt_patch_object_ref_t *ref) +{ + const char *soname; + const char *path; + + memset(ref, 0, sizeof(*ref)); + if (!snapshot || !text) { + return; + } + + soname = kzt_owner_snapshot_string(&snapshot->soname); + path = kzt_owner_snapshot_string(&snapshot->path); + kzt_owner_copy_text(text->soname, sizeof(text->soname), soname); + kzt_owner_copy_text(text->path, sizeof(text->path), path); + + ref->known = 1; + ref->link_map_addr = snapshot->link_map_addr; + ref->map_start = snapshot->map_start.value; + ref->map_end = snapshot->map_end.value; + ref->generation = snapshot->generation; + ref->soname = text->soname[0] ? text->soname : NULL; + ref->path = text->path[0] ? text->path : NULL; +} + +static kzt_owner_lookup_status_t kzt_owner_find_by_address( + const kzt_guest_registry_dump_t *dump, + uintptr_t address, + kzt_owner_resolver_text_t *text, + kzt_patch_object_ref_t *ref, + size_t *match_count) +{ + const kzt_guest_object_snapshot_t *match = NULL; + size_t i; + size_t count = 0; + + memset(ref, 0, sizeof(*ref)); + if (text) { + memset(text, 0, sizeof(*text)); + } + if (match_count) { + *match_count = 0; + } + + if (!dump || !ref || address == 0) { + return KZT_OWNER_LOOKUP_NOT_FOUND; + } + + for (i = 0; i < dump->count; ++i) { + if (dump->objects[i].state == KZT_GUEST_OBJECT_UNLOADING || + dump->objects[i].state == KZT_GUEST_OBJECT_DEAD) { + continue; + } + if (!kzt_owner_snapshot_contains(&dump->objects[i], address)) { + continue; + } + + ++count; + if (!match) { + match = &dump->objects[i]; + } + } + + if (match_count) { + *match_count = count; + } + if (count == 0) { + return KZT_OWNER_LOOKUP_NOT_FOUND; + } + if (count > 1) { + return KZT_OWNER_LOOKUP_AMBIGUOUS; + } + + kzt_owner_ref_from_snapshot(match, text, ref); + return KZT_OWNER_LOOKUP_RESOLVED; +} + +kzt_patch_owner_match_t kzt_owner_resolver_match_refs( + const kzt_patch_object_ref_t *current_owner, + const kzt_patch_object_ref_t *expected_owner) +{ + if (!current_owner || !expected_owner || + !current_owner->known || !expected_owner->known || + current_owner->link_map_addr == 0 || + expected_owner->link_map_addr == 0 || + current_owner->generation == 0 || expected_owner->generation == 0) { + return KZT_PATCH_OWNER_UNKNOWN; + } + + if (current_owner->link_map_addr == expected_owner->link_map_addr && + current_owner->generation == expected_owner->generation) { + return KZT_PATCH_OWNER_MATCH; + } + + return KZT_PATCH_OWNER_MISMATCH; +} + +static kzt_owner_resolver_status_t kzt_owner_lookup_status_to_current( + kzt_owner_lookup_status_t status) +{ + switch (status) { + case KZT_OWNER_LOOKUP_RESOLVED: + return KZT_OWNER_RESOLVER_RESOLVED; + case KZT_OWNER_LOOKUP_AMBIGUOUS: + return KZT_OWNER_RESOLVER_CURRENT_AMBIGUOUS; + case KZT_OWNER_LOOKUP_NOT_FOUND: + return KZT_OWNER_RESOLVER_CURRENT_NOT_FOUND; + } + + return KZT_OWNER_RESOLVER_CURRENT_NOT_FOUND; +} + +static kzt_owner_resolver_status_t kzt_owner_lookup_status_to_expected( + kzt_owner_lookup_status_t status) +{ + switch (status) { + case KZT_OWNER_LOOKUP_RESOLVED: + return KZT_OWNER_RESOLVER_RESOLVED; + case KZT_OWNER_LOOKUP_AMBIGUOUS: + return KZT_OWNER_RESOLVER_EXPECTED_AMBIGUOUS; + case KZT_OWNER_LOOKUP_NOT_FOUND: + return KZT_OWNER_RESOLVER_EXPECTED_NOT_FOUND; + } + + return KZT_OWNER_RESOLVER_EXPECTED_NOT_FOUND; +} + +int kzt_owner_resolver_resolve_current( + kzt_guest_registry_t *registry, + uintptr_t current_address, + uintptr_t expected_address, + kzt_owner_resolution_t *resolution) +{ + kzt_guest_registry_dump_t dump = { 0 }; + kzt_owner_lookup_status_t current_status; + kzt_owner_lookup_status_t expected_status; + + if (!resolution) { + return -1; + } + + kzt_owner_resolver_init(resolution); + resolution->status = KZT_OWNER_RESOLVER_RESOLVED; + + if (!registry) { + resolution->status = KZT_OWNER_RESOLVER_REGISTRY_UNAVAILABLE; + return 0; + } + if (current_address == 0) { + resolution->status = KZT_OWNER_RESOLVER_CURRENT_ADDRESS_MISSING; + return 0; + } + if (expected_address == 0) { + resolution->status = KZT_OWNER_RESOLVER_EXPECTED_ADDRESS_MISSING; + return 0; + } + if (kzt_guest_registry_dump_snapshot(registry, &dump) != 0) { + resolution->status = KZT_OWNER_RESOLVER_REGISTRY_UNAVAILABLE; + return 0; + } + + current_status = kzt_owner_find_by_address( + &dump, current_address, &resolution->current_text, + &resolution->current_owner, &resolution->current_match_count); + expected_status = kzt_owner_find_by_address( + &dump, expected_address, &resolution->expected_text, + &resolution->expected_owner, &resolution->expected_match_count); + + if (current_status != KZT_OWNER_LOOKUP_RESOLVED) { + resolution->status = kzt_owner_lookup_status_to_current( + current_status); + goto out; + } + if (expected_status != KZT_OWNER_LOOKUP_RESOLVED) { + resolution->status = kzt_owner_lookup_status_to_expected( + expected_status); + goto out; + } + + resolution->owner_match = kzt_owner_resolver_match_refs( + &resolution->current_owner, &resolution->expected_owner); + if (resolution->owner_match == KZT_PATCH_OWNER_UNKNOWN) { + resolution->status = KZT_OWNER_RESOLVER_GENERATION_UNKNOWN; + } + +out: + kzt_guest_registry_dump_free(&dump); + return 0; +} + +const char *kzt_owner_resolver_status_name( + kzt_owner_resolver_status_t status) +{ + switch (status) { + case KZT_OWNER_RESOLVER_RESOLVED: + return "RESOLVED"; + case KZT_OWNER_RESOLVER_INVALID_ARGUMENT: + return "INVALID_ARGUMENT"; + case KZT_OWNER_RESOLVER_REGISTRY_UNAVAILABLE: + return "REGISTRY_UNAVAILABLE"; + case KZT_OWNER_RESOLVER_CURRENT_ADDRESS_MISSING: + return "CURRENT_ADDRESS_MISSING"; + case KZT_OWNER_RESOLVER_EXPECTED_ADDRESS_MISSING: + return "EXPECTED_ADDRESS_MISSING"; + case KZT_OWNER_RESOLVER_CURRENT_NOT_FOUND: + return "CURRENT_NOT_FOUND"; + case KZT_OWNER_RESOLVER_EXPECTED_NOT_FOUND: + return "EXPECTED_NOT_FOUND"; + case KZT_OWNER_RESOLVER_CURRENT_AMBIGUOUS: + return "CURRENT_AMBIGUOUS"; + case KZT_OWNER_RESOLVER_EXPECTED_AMBIGUOUS: + return "EXPECTED_AMBIGUOUS"; + case KZT_OWNER_RESOLVER_GENERATION_UNKNOWN: + return "GENERATION_UNKNOWN"; + } + + return "UNKNOWN"; +} diff --git a/target/i386/latx/context/kzt_patch_planner.c b/target/i386/latx/context/kzt_patch_planner.c new file mode 100644 index 00000000000..2995f2f91d4 --- /dev/null +++ b/target/i386/latx/context/kzt_patch_planner.c @@ -0,0 +1,397 @@ +#include "kzt_patch_planner.h" + +#include +#include + +static int kzt_patch_string_is_empty(const char *value) +{ + return !value || value[0] == '\0'; +} + +static const char *kzt_patch_string_or_none(const char *value) +{ + return kzt_patch_string_is_empty(value) ? "(none)" : value; +} + +static int kzt_patch_relocation_is_supported( + kzt_patch_relocation_type_t reloc_type) +{ + return reloc_type == KZT_PATCH_RELOCATION_JUMP_SLOT || + reloc_type == KZT_PATCH_RELOCATION_GLOB_DAT; +} + +static void kzt_patch_decision_copy_candidate( + const kzt_patch_candidate_t *candidate, + kzt_patch_decision_t *decision) +{ + decision->source = candidate->source; + decision->dynamic_addr = candidate->dynamic_addr; + decision->load_bias = candidate->load_bias; + decision->dynamic_view_generation = candidate->dynamic_view_generation; + decision->dynamic_view_available = candidate->dynamic_view_available; + decision->table_kind = candidate->table_kind; + decision->entry_index = candidate->entry_index; + decision->entry_addr = candidate->entry_addr; + decision->reloc_type = candidate->reloc_type; + decision->slot_addr = candidate->slot_addr; + decision->slot_current_value_present = + candidate->slot_current_value_present; + decision->slot_current_value = candidate->slot_current_value; + decision->lazy_binding_deferred = candidate->lazy_binding_deferred; + decision->symbol_index = candidate->symbol_index; + decision->symbol_name = candidate->symbol_name; + decision->version_evidence = candidate->version_evidence; + decision->version = candidate->version; + decision->current_owner = candidate->current_owner; + decision->owner_match = candidate->owner_match; + decision->wrapper_match = candidate->wrapper_match; + decision->wrapper_name = candidate->wrapper_name; + decision->wrapper_version_evidence = + candidate->wrapper_version_evidence; + decision->wrapper_symbol_version = candidate->wrapper_symbol_version; + decision->bridge_target = candidate->bridge_target; +} + +static int kzt_patch_decision_set(kzt_patch_decision_t *decision, + kzt_patch_decision_kind_t kind, + kzt_patch_reason_t reason) +{ + decision->kind = kind; + decision->reason = reason; + decision->allow_native_bridge = kind == KZT_PATCH_DECISION_APPROVED; + return 0; +} + +const char *kzt_patch_decision_kind_name(kzt_patch_decision_kind_t kind) +{ + switch (kind) { + case KZT_PATCH_DECISION_APPROVED: + return "APPROVED"; + case KZT_PATCH_DECISION_REJECTED: + return "REJECTED"; + case KZT_PATCH_DECISION_UNSUPPORTED: + return "UNSUPPORTED"; + case KZT_PATCH_DECISION_DEFERRED: + return "DEFERRED"; + case KZT_PATCH_DECISION_ERROR: + return "ERROR"; + } + + return "UNKNOWN"; +} + +const char *kzt_patch_reason_name(kzt_patch_reason_t reason) +{ + switch (reason) { + case KZT_PATCH_REASON_APPROVED_NATIVE_BRIDGE: + return "APPROVED_NATIVE_BRIDGE"; + case KZT_PATCH_REASON_ERROR_INVALID_ARGUMENT: + return "ERROR_INVALID_ARGUMENT"; + case KZT_PATCH_REASON_INPUT_UNSUPPORTED_RELOCATION: + return "INPUT_UNSUPPORTED_RELOCATION"; + case KZT_PATCH_REASON_INPUT_MALFORMED_TABLE: + return "INPUT_MALFORMED_TABLE"; + case KZT_PATCH_REASON_INPUT_MALFORMED_SLOT: + return "INPUT_MALFORMED_SLOT"; + case KZT_PATCH_REASON_INPUT_MALFORMED_SYMBOL_NAME: + return "INPUT_MALFORMED_SYMBOL_NAME"; + case KZT_PATCH_REASON_INPUT_MALFORMED_SYMBOL_VERSION: + return "INPUT_MALFORMED_SYMBOL_VERSION"; + case KZT_PATCH_REASON_INPUT_UNAVAILABLE_DYNAMIC_VIEW: + return "INPUT_UNAVAILABLE_DYNAMIC_VIEW"; + case KZT_PATCH_REASON_INPUT_UNAVAILABLE_CURRENT_GOT: + return "INPUT_UNAVAILABLE_CURRENT_GOT"; + case KZT_PATCH_REASON_INPUT_UNAVAILABLE_OWNER: + return "INPUT_UNAVAILABLE_OWNER"; + case KZT_PATCH_REASON_INPUT_UNAVAILABLE_WRAPPER_MANIFEST: + return "INPUT_UNAVAILABLE_WRAPPER_MANIFEST"; + case KZT_PATCH_REASON_INPUT_UNAVAILABLE_BRIDGE_TARGET: + return "INPUT_UNAVAILABLE_BRIDGE_TARGET"; + case KZT_PATCH_REASON_POLICY_KEEP_GUEST: + return "POLICY_KEEP_GUEST"; + case KZT_PATCH_REASON_POLICY_OWNER_MISMATCH: + return "POLICY_OWNER_MISMATCH"; + case KZT_PATCH_REASON_POLICY_NO_WRAPPER: + return "POLICY_NO_WRAPPER"; + case KZT_PATCH_REASON_POLICY_WRAPPER_SYMBOL_ONLY: + return "POLICY_WRAPPER_SYMBOL_ONLY"; + case KZT_PATCH_REASON_POLICY_VERSION_MISMATCH: + return "POLICY_VERSION_MISMATCH"; + case KZT_PATCH_REASON_DEFERRED_LAZY_BINDING: + return "DEFERRED_LAZY_BINDING"; + } + + return "UNKNOWN"; +} + +const char *kzt_patch_table_kind_name(kzt_patch_table_kind_t table_kind) +{ + switch (table_kind) { + case KZT_PATCH_TABLE_UNKNOWN: + return "UNKNOWN"; + case KZT_PATCH_TABLE_RELA: + return "RELA"; + case KZT_PATCH_TABLE_REL: + return "REL"; + case KZT_PATCH_TABLE_PLT_RELA: + return "PLT_RELA"; + case KZT_PATCH_TABLE_PLT_REL: + return "PLT_REL"; + case KZT_PATCH_TABLE_OTHER: + return "OTHER"; + } + + return "UNKNOWN"; +} + +const char *kzt_patch_relocation_type_name( + kzt_patch_relocation_type_t reloc_type) +{ + switch (reloc_type) { + case KZT_PATCH_RELOCATION_UNKNOWN: + return "UNKNOWN"; + case KZT_PATCH_RELOCATION_JUMP_SLOT: + return "JUMP_SLOT"; + case KZT_PATCH_RELOCATION_GLOB_DAT: + return "GLOB_DAT"; + case KZT_PATCH_RELOCATION_RELATIVE: + return "RELATIVE"; + case KZT_PATCH_RELOCATION_COPY: + return "COPY"; + case KZT_PATCH_RELOCATION_IRELATIVE: + return "IRELATIVE"; + case KZT_PATCH_RELOCATION_OTHER: + return "OTHER"; + } + + return "UNKNOWN"; +} + +const char *kzt_patch_owner_match_name(kzt_patch_owner_match_t match) +{ + switch (match) { + case KZT_PATCH_OWNER_UNKNOWN: + return "UNKNOWN"; + case KZT_PATCH_OWNER_MATCH: + return "MATCH"; + case KZT_PATCH_OWNER_MISMATCH: + return "MISMATCH"; + } + + return "UNKNOWN"; +} + +const char *kzt_patch_wrapper_match_name(kzt_patch_wrapper_match_t match) +{ + switch (match) { + case KZT_PATCH_WRAPPER_NO_MANIFEST: + return "NO_MANIFEST"; + case KZT_PATCH_WRAPPER_NO_WRAPPER: + return "NO_WRAPPER"; + case KZT_PATCH_WRAPPER_SYMBOL_ONLY: + return "SYMBOL_ONLY"; + case KZT_PATCH_WRAPPER_VERSION_MISMATCH: + return "VERSION_MISMATCH"; + case KZT_PATCH_WRAPPER_VERSION_MATCH: + return "VERSION_MATCH"; + case KZT_PATCH_WRAPPER_UNVERSIONED_MATCH: + return "UNVERSIONED_MATCH"; + } + + return "UNKNOWN"; +} + +const char *kzt_symbol_version_evidence_name( + kzt_symbol_version_evidence_t evidence) +{ + switch (evidence) { + case KZT_SYMBOL_VERSION_VERSIONED: + return "VERSIONED"; + case KZT_SYMBOL_VERSION_CONFIRMED_UNVERSIONED: + return "CONFIRMED_UNVERSIONED"; + case KZT_SYMBOL_VERSION_UNKNOWN: + return "UNKNOWN"; + case KZT_SYMBOL_VERSION_ERROR: + return "ERROR"; + } + return "UNKNOWN"; +} + +int kzt_patch_planner_decide(const kzt_patch_candidate_t *candidate, + kzt_patch_decision_t *decision) +{ + if (!decision) { + return -1; + } + + memset(decision, 0, sizeof(*decision)); + if (!candidate) { + return kzt_patch_decision_set(decision, KZT_PATCH_DECISION_ERROR, + KZT_PATCH_REASON_ERROR_INVALID_ARGUMENT); + } + kzt_patch_decision_copy_candidate(candidate, decision); + + if (!kzt_patch_relocation_is_supported(candidate->reloc_type)) { + return kzt_patch_decision_set( + decision, KZT_PATCH_DECISION_UNSUPPORTED, + KZT_PATCH_REASON_INPUT_UNSUPPORTED_RELOCATION); + } + + if (!candidate->dynamic_view_available) { + return kzt_patch_decision_set( + decision, KZT_PATCH_DECISION_UNSUPPORTED, + KZT_PATCH_REASON_INPUT_UNAVAILABLE_DYNAMIC_VIEW); + } + + if (candidate->table_kind == KZT_PATCH_TABLE_UNKNOWN) { + return kzt_patch_decision_set( + decision, KZT_PATCH_DECISION_UNSUPPORTED, + KZT_PATCH_REASON_INPUT_MALFORMED_TABLE); + } + + if (candidate->slot_addr == 0) { + return kzt_patch_decision_set( + decision, KZT_PATCH_DECISION_UNSUPPORTED, + KZT_PATCH_REASON_INPUT_MALFORMED_SLOT); + } + + if (kzt_patch_string_is_empty(candidate->symbol_name)) { + return kzt_patch_decision_set( + decision, KZT_PATCH_DECISION_UNSUPPORTED, + KZT_PATCH_REASON_INPUT_MALFORMED_SYMBOL_NAME); + } + + if (!kzt_symbol_version_evidence_valid(candidate->version_evidence, + candidate->version)) { + return kzt_patch_decision_set( + decision, KZT_PATCH_DECISION_UNSUPPORTED, + KZT_PATCH_REASON_INPUT_MALFORMED_SYMBOL_VERSION); + } + + if (!candidate->slot_current_value_present) { + return kzt_patch_decision_set( + decision, KZT_PATCH_DECISION_UNSUPPORTED, + KZT_PATCH_REASON_INPUT_UNAVAILABLE_CURRENT_GOT); + } + + if (candidate->lazy_binding_deferred) { + return kzt_patch_decision_set(decision, KZT_PATCH_DECISION_DEFERRED, + KZT_PATCH_REASON_DEFERRED_LAZY_BINDING); + } + + if (!candidate->current_owner.known || + candidate->owner_match == KZT_PATCH_OWNER_UNKNOWN) { + return kzt_patch_decision_set( + decision, KZT_PATCH_DECISION_UNSUPPORTED, + KZT_PATCH_REASON_INPUT_UNAVAILABLE_OWNER); + } + + if (candidate->owner_match == KZT_PATCH_OWNER_MISMATCH) { + return kzt_patch_decision_set(decision, KZT_PATCH_DECISION_REJECTED, + KZT_PATCH_REASON_POLICY_OWNER_MISMATCH); + } + + switch (candidate->wrapper_match) { + case KZT_PATCH_WRAPPER_NO_MANIFEST: + return kzt_patch_decision_set( + decision, KZT_PATCH_DECISION_UNSUPPORTED, + KZT_PATCH_REASON_INPUT_UNAVAILABLE_WRAPPER_MANIFEST); + case KZT_PATCH_WRAPPER_NO_WRAPPER: + return kzt_patch_decision_set(decision, KZT_PATCH_DECISION_REJECTED, + KZT_PATCH_REASON_POLICY_NO_WRAPPER); + case KZT_PATCH_WRAPPER_SYMBOL_ONLY: + return kzt_patch_decision_set( + decision, KZT_PATCH_DECISION_REJECTED, + KZT_PATCH_REASON_POLICY_WRAPPER_SYMBOL_ONLY); + case KZT_PATCH_WRAPPER_VERSION_MISMATCH: + return kzt_patch_decision_set(decision, KZT_PATCH_DECISION_REJECTED, + KZT_PATCH_REASON_POLICY_VERSION_MISMATCH); + case KZT_PATCH_WRAPPER_VERSION_MATCH: + case KZT_PATCH_WRAPPER_UNVERSIONED_MATCH: + break; + } + + if (!kzt_symbol_version_evidence_matches( + candidate->version_evidence, candidate->version, + candidate->wrapper_version_evidence, + candidate->wrapper_symbol_version) || + (candidate->version_evidence == KZT_SYMBOL_VERSION_VERSIONED && + candidate->wrapper_match != KZT_PATCH_WRAPPER_VERSION_MATCH) || + (candidate->version_evidence == + KZT_SYMBOL_VERSION_CONFIRMED_UNVERSIONED && + candidate->wrapper_match != + KZT_PATCH_WRAPPER_UNVERSIONED_MATCH)) { + return kzt_patch_decision_set(decision, KZT_PATCH_DECISION_REJECTED, + KZT_PATCH_REASON_POLICY_VERSION_MISMATCH); + } + + if (candidate->bridge_target == 0) { + return kzt_patch_decision_set( + decision, KZT_PATCH_DECISION_UNSUPPORTED, + KZT_PATCH_REASON_INPUT_UNAVAILABLE_BRIDGE_TARGET); + } + + return kzt_patch_decision_set(decision, KZT_PATCH_DECISION_APPROVED, + KZT_PATCH_REASON_APPROVED_NATIVE_BRIDGE); +} + +int kzt_patch_decision_format_summary(const kzt_patch_decision_t *decision, + char *buffer, + size_t buffer_size) +{ + int written; + + if (!decision || !buffer || buffer_size == 0) { + return -1; + } + + written = snprintf( + buffer, buffer_size, + "kzt_patch_decision kind=%s reason=%s allow=%d " + "link_map=0x%lx source_generation=%lu dynamic_addr=0x%lx " + "load_bias=0x%lx dynamic_view_generation=%lu " + "dynamic_view_available=%d table=%s entry_index=%lu " + "entry_addr=0x%lx reloc=%s slot=0x%lx " + "slot_current_present=%d slot_current=0x%lx lazy_deferred=%d " + "symbol_index=%lu symbol=%s version_evidence=%s version=%s " + "current_owner=0x%lx " + "current_owner_generation=%lu owner_match=%s wrapper=%s " + "wrapper_match=%s wrapper_version_evidence=%s " + "wrapper_version=%s bridge=0x%lx", + kzt_patch_decision_kind_name(decision->kind), + kzt_patch_reason_name(decision->reason), + decision->allow_native_bridge, + (unsigned long)decision->source.link_map_addr, + decision->source.generation, + (unsigned long)decision->dynamic_addr, + (unsigned long)decision->load_bias, + decision->dynamic_view_generation, + decision->dynamic_view_available, + kzt_patch_table_kind_name(decision->table_kind), + (unsigned long)decision->entry_index, + (unsigned long)decision->entry_addr, + kzt_patch_relocation_type_name(decision->reloc_type), + (unsigned long)decision->slot_addr, + decision->slot_current_value_present, + (unsigned long)decision->slot_current_value, + decision->lazy_binding_deferred, + decision->symbol_index, + kzt_patch_string_or_none(decision->symbol_name), + kzt_symbol_version_evidence_name(decision->version_evidence), + kzt_patch_string_or_none(decision->version), + (unsigned long)decision->current_owner.link_map_addr, + decision->current_owner.generation, + kzt_patch_owner_match_name(decision->owner_match), + kzt_patch_string_or_none(decision->wrapper_name), + kzt_patch_wrapper_match_name(decision->wrapper_match), + kzt_symbol_version_evidence_name( + decision->wrapper_version_evidence), + kzt_patch_string_or_none(decision->wrapper_symbol_version), + (unsigned long)decision->bridge_target); + + if (written < 0 || (size_t)written >= buffer_size) { + return -1; + } + + return 0; +} diff --git a/target/i386/latx/context/kzt_patch_spike_guard.c b/target/i386/latx/context/kzt_patch_spike_guard.c new file mode 100644 index 00000000000..a391a3fb7db --- /dev/null +++ b/target/i386/latx/context/kzt_patch_spike_guard.c @@ -0,0 +1,388 @@ +#include "kzt_patch_spike_guard.h" + +#include + +#if defined(CONFIG_LATX_KZT) +extern int option_kzt_patch_spike; +extern int option_kzt_patch_spike_write; +extern unsigned long option_kzt_patch_spike_budget; +#endif + +static void kzt_patch_spike_outcome_set( + kzt_patch_spike_outcome_t *outcome, + kzt_patch_spike_result_t result, + kzt_patch_spike_failure_t failure, + kzt_patch_spike_action_t action, + const kzt_patch_spike_guard_t *guard) +{ + outcome->result = result; + outcome->failure = failure; + outcome->action = action; + outcome->skip_legacy_write = + action != KZT_PATCH_SPIKE_ACTION_KEEP_LEGACY; + outcome->writes_remaining = + kzt_patch_spike_guard_budget_remaining(guard); +} + +static int kzt_patch_spike_writer_ready( + const kzt_patch_spike_writer_ops_t *writer) +{ + return writer && writer->write_slot && writer->verify_slot && + writer->rollback_slot; +} + +static void kzt_patch_spike_guard_lock(kzt_patch_spike_guard_t *guard) +{ + while (__atomic_exchange_n(&guard->transaction_gate, 1, + __ATOMIC_ACQUIRE)) { + while (__atomic_load_n(&guard->transaction_gate, __ATOMIC_RELAXED)) { + } + } +} + +static void kzt_patch_spike_guard_unlock(kzt_patch_spike_guard_t *guard) +{ + __atomic_store_n(&guard->transaction_gate, 0, __ATOMIC_RELEASE); +} + +static kzt_patch_spike_failure_t kzt_patch_spike_writer_failure( + kzt_patch_spike_writer_status_t status) +{ + switch (status) { + case KZT_PATCH_SPIKE_WRITER_READ_FAILED: + return KZT_PATCH_SPIKE_FAILURE_READ_FAILED; + case KZT_PATCH_SPIKE_WRITER_EXPECTED_MISMATCH: + return KZT_PATCH_SPIKE_FAILURE_EXPECTED_MISMATCH; + case KZT_PATCH_SPIKE_WRITER_WRITE_FAILED: + return KZT_PATCH_SPIKE_FAILURE_WRITE_FAILED; + case KZT_PATCH_SPIKE_WRITER_PERMISSION_ENABLE_FAILED: + return KZT_PATCH_SPIKE_FAILURE_PERMISSION_ENABLE_FAILED; + case KZT_PATCH_SPIKE_WRITER_PERMISSION_RESTORE_FAILED: + return KZT_PATCH_SPIKE_FAILURE_PERMISSION_RESTORE_FAILED; + case KZT_PATCH_SPIKE_WRITER_GENERATION_MISMATCH: + return KZT_PATCH_SPIKE_FAILURE_GENERATION_MISMATCH; + case KZT_PATCH_SPIKE_WRITER_OK: + return KZT_PATCH_SPIKE_FAILURE_NONE; + } + + return KZT_PATCH_SPIKE_FAILURE_WRITE_FAILED; +} + +void kzt_patch_spike_config_from_options(kzt_patch_spike_config_t *config) +{ + if (!config) { + return; + } + +#if defined(CONFIG_LATX_KZT) + config->enabled = option_kzt_patch_spike > 0; + config->write_enabled = option_kzt_patch_spike_write > 0; + config->budget = option_kzt_patch_spike_budget; +#else + config->enabled = 0; + config->write_enabled = 0; + config->budget = 0; +#endif +} + +void kzt_patch_spike_guard_init(kzt_patch_spike_guard_t *guard, + const kzt_patch_spike_config_t *config) +{ + if (!guard) { + return; + } + + memset(guard, 0, sizeof(*guard)); + if (config) { + guard->config = *config; + } +} + +int kzt_patch_spike_guard_should_plan( + const kzt_patch_spike_guard_t *guard) +{ + return guard && guard->config.enabled && + !__atomic_load_n(&guard->circuit_open, __ATOMIC_ACQUIRE); +} + +int kzt_patch_spike_guard_circuit_open( + const kzt_patch_spike_guard_t *guard) +{ + return guard && __atomic_load_n(&guard->circuit_open, __ATOMIC_ACQUIRE); +} + +unsigned long kzt_patch_spike_guard_budget_remaining( + const kzt_patch_spike_guard_t *guard) +{ + unsigned long attempts; + + if (!guard) { + return 0; + } + + attempts = __atomic_load_n(&guard->write_attempts, __ATOMIC_ACQUIRE); + if (guard->config.budget <= attempts) { + return 0; + } + + return guard->config.budget - attempts; +} + +static int kzt_patch_spike_guard_reserve_budget(kzt_patch_spike_guard_t *guard) +{ + unsigned long attempts; + + for (;;) { + attempts = __atomic_load_n(&guard->write_attempts, __ATOMIC_ACQUIRE); + if (attempts >= guard->config.budget) { + return 0; + } + if (__atomic_compare_exchange_n(&guard->write_attempts, &attempts, + attempts + 1, 0, __ATOMIC_ACQ_REL, + __ATOMIC_ACQUIRE)) { + return 1; + } + } +} + +static int kzt_patch_spike_failure_preserves_guest( + kzt_patch_spike_failure_t failure) +{ + return failure == KZT_PATCH_SPIKE_FAILURE_PERMISSION_ENABLE_FAILED || + failure == KZT_PATCH_SPIKE_FAILURE_PERMISSION_RESTORE_FAILED || + failure == KZT_PATCH_SPIKE_FAILURE_ROLLBACK_FAILED || + failure == KZT_PATCH_SPIKE_FAILURE_GENERATION_MISMATCH; +} + +static int kzt_patch_spike_decision_allows_write( + const kzt_patch_decision_t *decision) +{ + return decision && decision->kind == KZT_PATCH_DECISION_APPROVED && + decision->allow_native_bridge; +} + +int kzt_patch_spike_guard_try_write( + kzt_patch_spike_guard_t *guard, + const kzt_patch_decision_t *decision, + const kzt_patch_spike_writer_ops_t *writer, + kzt_patch_spike_outcome_t *outcome) +{ + kzt_patch_spike_writer_status_t writer_status; + uintptr_t previous_value = 0; + + if (!guard || !outcome) { + return -1; + } + + memset(outcome, 0, sizeof(*outcome)); + outcome->action = KZT_PATCH_SPIKE_ACTION_KEEP_LEGACY; + kzt_patch_spike_guard_lock(guard); + outcome->writes_remaining = + kzt_patch_spike_guard_budget_remaining(guard); + + if (__atomic_load_n(&guard->circuit_open, __ATOMIC_ACQUIRE)) { + kzt_patch_spike_outcome_set( + outcome, KZT_PATCH_SPIKE_RESULT_CIRCUIT_OPEN, + KZT_PATCH_SPIKE_FAILURE_CIRCUIT_BREAKER_OPEN, + KZT_PATCH_SPIKE_ACTION_PRESERVE_GUEST, guard); + kzt_patch_spike_guard_unlock(guard); + return 0; + } + + if (!guard->config.enabled) { + kzt_patch_spike_outcome_set( + outcome, KZT_PATCH_SPIKE_RESULT_DISABLED, + KZT_PATCH_SPIKE_FAILURE_NONE, + KZT_PATCH_SPIKE_ACTION_KEEP_LEGACY, guard); + kzt_patch_spike_guard_unlock(guard); + return 0; + } + + if (!kzt_patch_spike_decision_allows_write(decision)) { + kzt_patch_spike_outcome_set( + outcome, KZT_PATCH_SPIKE_RESULT_FAIL_OPEN, + KZT_PATCH_SPIKE_FAILURE_DECISION_NOT_APPROVED, + KZT_PATCH_SPIKE_ACTION_KEEP_LEGACY, guard); + kzt_patch_spike_guard_unlock(guard); + return 0; + } + + if (!guard->config.write_enabled) { + kzt_patch_spike_outcome_set( + outcome, KZT_PATCH_SPIKE_RESULT_DIAGNOSTICS_ONLY, + KZT_PATCH_SPIKE_FAILURE_WRITE_NOT_AUTHORIZED, + KZT_PATCH_SPIKE_ACTION_KEEP_LEGACY, guard); + kzt_patch_spike_guard_unlock(guard); + return 0; + } + + if (!kzt_patch_spike_writer_ready(writer) || + !decision->slot_current_value_present) { + kzt_patch_spike_outcome_set( + outcome, KZT_PATCH_SPIKE_RESULT_FAIL_OPEN, + KZT_PATCH_SPIKE_FAILURE_INVALID_ARGUMENT, + KZT_PATCH_SPIKE_ACTION_KEEP_LEGACY, guard); + kzt_patch_spike_guard_unlock(guard); + return 0; + } + + if (!kzt_patch_spike_guard_reserve_budget(guard)) { + kzt_patch_spike_outcome_set( + outcome, KZT_PATCH_SPIKE_RESULT_BUDGET_EXHAUSTED, + KZT_PATCH_SPIKE_FAILURE_BUDGET_EXHAUSTED, + KZT_PATCH_SPIKE_ACTION_KEEP_LEGACY, guard); + kzt_patch_spike_guard_unlock(guard); + return 0; + } + + outcome->writer_called = 1; + outcome->writes_remaining = + kzt_patch_spike_guard_budget_remaining(guard); + + writer_status = writer->write_slot(decision, + decision->slot_current_value, + decision->bridge_target, + &previous_value, + writer->opaque); + outcome->previous_value = previous_value; + if (writer_status != KZT_PATCH_SPIKE_WRITER_OK) { + kzt_patch_spike_failure_t failure = + kzt_patch_spike_writer_failure(writer_status); + int preserve_guest = kzt_patch_spike_failure_preserves_guest(failure); + + if (failure == KZT_PATCH_SPIKE_FAILURE_PERMISSION_RESTORE_FAILED) { + __atomic_store_n(&guard->circuit_open, 1, __ATOMIC_RELEASE); + } + kzt_patch_spike_outcome_set( + outcome, preserve_guest ? KZT_PATCH_SPIKE_RESULT_GUEST_PRESERVED : + KZT_PATCH_SPIKE_RESULT_FAIL_OPEN, + failure, preserve_guest ? + KZT_PATCH_SPIKE_ACTION_PRESERVE_GUEST : + KZT_PATCH_SPIKE_ACTION_KEEP_LEGACY, guard); + kzt_patch_spike_guard_unlock(guard); + return 0; + } + + if (writer->verify_slot(decision, decision->bridge_target, + writer->opaque) != 0) { + outcome->rollback_called = 1; + if (writer->rollback_slot(decision, previous_value, + writer->opaque) != 0) { + if (writer->finish_slot && + writer->finish_slot(decision, writer->opaque) != + KZT_PATCH_SPIKE_WRITER_OK) { + __atomic_store_n(&guard->circuit_open, 1, __ATOMIC_RELEASE); + kzt_patch_spike_outcome_set( + outcome, KZT_PATCH_SPIKE_RESULT_GUEST_PRESERVED, + KZT_PATCH_SPIKE_FAILURE_PERMISSION_RESTORE_FAILED, + KZT_PATCH_SPIKE_ACTION_PRESERVE_GUEST, guard); + kzt_patch_spike_guard_unlock(guard); + return 0; + } + __atomic_store_n(&guard->circuit_open, 1, __ATOMIC_RELEASE); + kzt_patch_spike_outcome_set( + outcome, KZT_PATCH_SPIKE_RESULT_GUEST_PRESERVED, + KZT_PATCH_SPIKE_FAILURE_ROLLBACK_FAILED, + KZT_PATCH_SPIKE_ACTION_PRESERVE_GUEST, guard); + kzt_patch_spike_guard_unlock(guard); + return 0; + } + + if (writer->finish_slot && + writer->finish_slot(decision, writer->opaque) != + KZT_PATCH_SPIKE_WRITER_OK) { + __atomic_store_n(&guard->circuit_open, 1, __ATOMIC_RELEASE); + kzt_patch_spike_outcome_set( + outcome, KZT_PATCH_SPIKE_RESULT_GUEST_PRESERVED, + KZT_PATCH_SPIKE_FAILURE_PERMISSION_RESTORE_FAILED, + KZT_PATCH_SPIKE_ACTION_PRESERVE_GUEST, guard); + kzt_patch_spike_guard_unlock(guard); + return 0; + } + + kzt_patch_spike_outcome_set( + outcome, KZT_PATCH_SPIKE_RESULT_FAIL_OPEN, + KZT_PATCH_SPIKE_FAILURE_VERIFY_FAILED, + KZT_PATCH_SPIKE_ACTION_KEEP_LEGACY, guard); + kzt_patch_spike_guard_unlock(guard); + return 0; + } + + if (writer->finish_slot && + writer->finish_slot(decision, writer->opaque) != + KZT_PATCH_SPIKE_WRITER_OK) { + __atomic_store_n(&guard->circuit_open, 1, __ATOMIC_RELEASE); + kzt_patch_spike_outcome_set( + outcome, KZT_PATCH_SPIKE_RESULT_GUEST_PRESERVED, + KZT_PATCH_SPIKE_FAILURE_PERMISSION_RESTORE_FAILED, + KZT_PATCH_SPIKE_ACTION_PRESERVE_GUEST, guard); + kzt_patch_spike_guard_unlock(guard); + return 0; + } + + __atomic_add_fetch(&guard->write_successes, 1, __ATOMIC_ACQ_REL); + kzt_patch_spike_outcome_set( + outcome, KZT_PATCH_SPIKE_RESULT_APPLIED, + KZT_PATCH_SPIKE_FAILURE_NONE, + KZT_PATCH_SPIKE_ACTION_USE_NATIVE_BRIDGE, guard); + kzt_patch_spike_guard_unlock(guard); + return 0; +} + +const char *kzt_patch_spike_result_name(kzt_patch_spike_result_t result) +{ + switch (result) { + case KZT_PATCH_SPIKE_RESULT_DISABLED: + return "DISABLED"; + case KZT_PATCH_SPIKE_RESULT_DIAGNOSTICS_ONLY: + return "DIAGNOSTICS_ONLY"; + case KZT_PATCH_SPIKE_RESULT_BUDGET_EXHAUSTED: + return "BUDGET_EXHAUSTED"; + case KZT_PATCH_SPIKE_RESULT_APPLIED: + return "APPLIED"; + case KZT_PATCH_SPIKE_RESULT_FAIL_OPEN: + return "FAIL_OPEN"; + case KZT_PATCH_SPIKE_RESULT_GUEST_PRESERVED: + return "GUEST_PRESERVED"; + case KZT_PATCH_SPIKE_RESULT_CIRCUIT_OPEN: + return "CIRCUIT_OPEN"; + } + + return "UNKNOWN"; +} + +const char *kzt_patch_spike_failure_name(kzt_patch_spike_failure_t failure) +{ + switch (failure) { + case KZT_PATCH_SPIKE_FAILURE_NONE: + return "NONE"; + case KZT_PATCH_SPIKE_FAILURE_INVALID_ARGUMENT: + return "INVALID_ARGUMENT"; + case KZT_PATCH_SPIKE_FAILURE_DECISION_NOT_APPROVED: + return "DECISION_NOT_APPROVED"; + case KZT_PATCH_SPIKE_FAILURE_WRITE_NOT_AUTHORIZED: + return "WRITE_NOT_AUTHORIZED"; + case KZT_PATCH_SPIKE_FAILURE_BUDGET_EXHAUSTED: + return "BUDGET_EXHAUSTED"; + case KZT_PATCH_SPIKE_FAILURE_EXPECTED_MISMATCH: + return "EXPECTED_MISMATCH"; + case KZT_PATCH_SPIKE_FAILURE_READ_FAILED: + return "READ_FAILED"; + case KZT_PATCH_SPIKE_FAILURE_WRITE_FAILED: + return "WRITE_FAILED"; + case KZT_PATCH_SPIKE_FAILURE_VERIFY_FAILED: + return "VERIFY_FAILED"; + case KZT_PATCH_SPIKE_FAILURE_ROLLBACK_FAILED: + return "ROLLBACK_FAILED"; + case KZT_PATCH_SPIKE_FAILURE_PERMISSION_ENABLE_FAILED: + return "PERMISSION_ENABLE_FAILED"; + case KZT_PATCH_SPIKE_FAILURE_PERMISSION_RESTORE_FAILED: + return "PERMISSION_RESTORE_FAILED"; + case KZT_PATCH_SPIKE_FAILURE_GENERATION_MISMATCH: + return "GENERATION_MISMATCH"; + case KZT_PATCH_SPIKE_FAILURE_CIRCUIT_BREAKER_OPEN: + return "CIRCUIT_BREAKER_OPEN"; + } + + return "UNKNOWN"; +} diff --git a/target/i386/latx/context/kzt_patch_spike_writer.c b/target/i386/latx/context/kzt_patch_spike_writer.c new file mode 100644 index 00000000000..06121e18641 --- /dev/null +++ b/target/i386/latx/context/kzt_patch_spike_writer.c @@ -0,0 +1,384 @@ +#include "kzt_patch_spike_writer.h" + +#include + +typedef struct kzt_patch_spike_writer_state { + const kzt_patch_spike_slot_ops_t *slot_ops; + kzt_patch_spike_record_t *record; + kzt_patch_spike_permission_lease_t permission_lease; + int permission_active; +} kzt_patch_spike_writer_state_t; + +static int kzt_patch_spike_direct_read(uintptr_t slot_addr, + uintptr_t *value, + void *opaque) +{ + volatile uintptr_t *slot = (volatile uintptr_t *)slot_addr; + + (void)opaque; + if (!slot || !value) { + return -1; + } + + *value = *slot; + return 0; +} + +static int kzt_patch_spike_direct_write(uintptr_t slot_addr, + uintptr_t value, + void *opaque) +{ + volatile uintptr_t *slot = (volatile uintptr_t *)slot_addr; + + (void)opaque; + if (!slot) { + return -1; + } + + *slot = value; + return 0; +} + +static const kzt_patch_spike_slot_ops_t kzt_patch_spike_direct_slot_ops = { + .read_slot = kzt_patch_spike_direct_read, + .write_slot = kzt_patch_spike_direct_write, +}; + +static int kzt_patch_spike_slot_ops_ready( + const kzt_patch_spike_slot_ops_t *slot_ops) +{ + return slot_ops && slot_ops->read_slot && slot_ops->write_slot && + (slot_ops->begin_write != NULL) == + (slot_ops->end_write != NULL); +} + +void kzt_patch_spike_record_init(kzt_patch_spike_record_t *record, + const kzt_patch_decision_t *decision) +{ + if (!record) { + return; + } + + memset(record, 0, sizeof(*record)); + record->valid = 1; + record->result = KZT_PATCH_SPIKE_RESULT_FAIL_OPEN; + record->failure = KZT_PATCH_SPIKE_FAILURE_INVALID_ARGUMENT; + record->action = KZT_PATCH_SPIKE_ACTION_KEEP_LEGACY; + + if (!decision) { + return; + } + + record->decision_kind = decision->kind; + record->decision_reason = decision->reason; + record->allow_native_bridge = decision->allow_native_bridge; + record->table_kind = decision->table_kind; + record->reloc_type = decision->reloc_type; + record->entry_index = decision->entry_index; + record->entry_addr = decision->entry_addr; + record->slot_addr = decision->slot_addr; + record->source_link_map = decision->source.link_map_addr; + record->current_owner_link_map = decision->current_owner.link_map_addr; + record->source_generation = decision->source.generation; + record->current_owner_generation = decision->current_owner.generation; + record->dynamic_view_generation = decision->dynamic_view_generation; + record->expected_value_present = decision->slot_current_value_present; + record->expected_value = decision->slot_current_value; + record->replacement_value = decision->bridge_target; + record->symbol_name = decision->symbol_name; + record->wrapper_name = decision->wrapper_name; +} + +static void kzt_patch_spike_record_permission( + kzt_patch_spike_record_t *record, + const kzt_patch_spike_permission_lease_t *lease) +{ + if (!record || !lease) { + return; + } + + record->permission_checked = lease->checked; + record->permission_guest_page = lease->guest_page; + record->permission_guest_page_length = lease->guest_page_length; + record->permission_original_permissions = lease->original_permissions; + record->permission_was_writable = lease->was_writable; + record->permission_write_enabled = lease->write_enabled; + record->permission_restore_attempted = lease->restore_attempted; + record->permission_restored = lease->restored; +} + +static kzt_patch_spike_writer_status_t kzt_patch_spike_writer_finish_slot( + const kzt_patch_decision_t *decision, void *opaque) +{ + kzt_patch_spike_writer_state_t *state = opaque; + + (void)decision; + if (!state || !state->record) { + return KZT_PATCH_SPIKE_WRITER_READ_FAILED; + } + if (!state->permission_active) { + return KZT_PATCH_SPIKE_WRITER_OK; + } + + state->permission_lease.restore_attempted = 1; + if (state->slot_ops->end_write && + state->slot_ops->end_write(&state->permission_lease, + state->slot_ops->opaque) != 0) { + kzt_patch_spike_record_permission(state->record, + &state->permission_lease); + state->permission_active = 0; + return KZT_PATCH_SPIKE_WRITER_PERMISSION_RESTORE_FAILED; + } + state->permission_lease.restored = 1; + kzt_patch_spike_record_permission(state->record, + &state->permission_lease); + state->permission_active = 0; + return KZT_PATCH_SPIKE_WRITER_OK; +} + +static kzt_patch_spike_writer_status_t kzt_patch_spike_writer_abort_slot( + const kzt_patch_decision_t *decision, void *opaque, + kzt_patch_spike_writer_status_t status) +{ + if (kzt_patch_spike_writer_finish_slot(decision, opaque) != + KZT_PATCH_SPIKE_WRITER_OK) { + return KZT_PATCH_SPIKE_WRITER_PERMISSION_RESTORE_FAILED; + } + return status; +} + +static int kzt_patch_spike_writer_decision_supported( + const kzt_patch_decision_t *decision) +{ + return decision && decision->kind == KZT_PATCH_DECISION_APPROVED && + decision->reason == KZT_PATCH_REASON_APPROVED_NATIVE_BRIDGE && + decision->allow_native_bridge && + decision->reloc_type == KZT_PATCH_RELOCATION_JUMP_SLOT && + decision->slot_addr && decision->slot_current_value_present && + decision->bridge_target && decision->source.known && + decision->source.link_map_addr && decision->source.generation && + decision->dynamic_view_available && + decision->dynamic_view_generation && + decision->current_owner.known && + decision->current_owner.link_map_addr && + decision->current_owner.generation && + decision->owner_match == KZT_PATCH_OWNER_MATCH; +} + +static kzt_patch_spike_writer_status_t kzt_patch_spike_writer_write_slot( + const kzt_patch_decision_t *decision, + uintptr_t expected_value, + uintptr_t replacement_value, + uintptr_t *previous_value, + void *opaque) +{ + kzt_patch_spike_writer_state_t *state = opaque; + kzt_patch_spike_record_t *record; + uintptr_t observed_value = 0; + + if (!state || !kzt_patch_spike_slot_ops_ready(state->slot_ops) || + !state->record || + !kzt_patch_spike_writer_decision_supported(decision)) { + return KZT_PATCH_SPIKE_WRITER_READ_FAILED; + } + + record = state->record; + if (state->slot_ops->validate_generation) { + record->generation_checked = 1; + if (state->slot_ops->validate_generation(decision, + state->slot_ops->opaque) != 0) { + record->generation_matched = 0; + return KZT_PATCH_SPIKE_WRITER_GENERATION_MISMATCH; + } + record->generation_matched = 1; + } + if (state->slot_ops->begin_write) { + if (state->slot_ops->begin_write(decision->slot_addr, + &state->permission_lease, + state->slot_ops->opaque) != 0) { + state->permission_active = + state->permission_lease.write_enabled != 0; + kzt_patch_spike_record_permission(record, + &state->permission_lease); + if (state->permission_active) { + return kzt_patch_spike_writer_abort_slot( + decision, state, + KZT_PATCH_SPIKE_WRITER_PERMISSION_ENABLE_FAILED); + } + return KZT_PATCH_SPIKE_WRITER_PERMISSION_ENABLE_FAILED; + } + state->permission_active = 1; + kzt_patch_spike_record_permission(record, &state->permission_lease); + } + record->read_attempted = 1; + if (state->slot_ops->read_slot(decision->slot_addr, &observed_value, + state->slot_ops->opaque) != 0) { + return kzt_patch_spike_writer_abort_slot( + decision, state, KZT_PATCH_SPIKE_WRITER_READ_FAILED); + } + + record->observed_value = observed_value; + record->previous_value = observed_value; + if (previous_value) { + *previous_value = observed_value; + } + + if (observed_value != expected_value) { + record->expected_current_matched = 0; + return kzt_patch_spike_writer_abort_slot( + decision, state, KZT_PATCH_SPIKE_WRITER_EXPECTED_MISMATCH); + } + + record->expected_current_matched = 1; + record->write_attempted = 1; + if (state->slot_ops->write_slot(decision->slot_addr, replacement_value, + state->slot_ops->opaque) != 0) { + return kzt_patch_spike_writer_abort_slot( + decision, state, KZT_PATCH_SPIKE_WRITER_WRITE_FAILED); + } + + record->write_succeeded = 1; + return KZT_PATCH_SPIKE_WRITER_OK; +} + +static int kzt_patch_spike_writer_verify_slot( + const kzt_patch_decision_t *decision, + uintptr_t expected_value, + void *opaque) +{ + kzt_patch_spike_writer_state_t *state = opaque; + kzt_patch_spike_record_t *record; + uintptr_t observed_value = 0; + + if (!state || !kzt_patch_spike_slot_ops_ready(state->slot_ops) || + !state->record || !decision || !decision->slot_addr) { + return -1; + } + + record = state->record; + record->verify_attempted = 1; + if (state->slot_ops->read_slot(decision->slot_addr, &observed_value, + state->slot_ops->opaque) != 0) { + return -1; + } + + record->verified_value = observed_value; + if (observed_value != expected_value) { + return -1; + } + + record->verify_succeeded = 1; + return 0; +} + +static int kzt_patch_spike_writer_rollback_slot( + const kzt_patch_decision_t *decision, + uintptr_t previous_value, + void *opaque) +{ + kzt_patch_spike_writer_state_t *state = opaque; + kzt_patch_spike_record_t *record; + uintptr_t observed_value = 0; + + if (!state || !kzt_patch_spike_slot_ops_ready(state->slot_ops) || + !state->record || !decision || !decision->slot_addr) { + return -1; + } + + record = state->record; + record->rollback_called = 1; + record->rollback_value = previous_value; + if (state->slot_ops->write_slot(decision->slot_addr, previous_value, + state->slot_ops->opaque) != 0) { + return -1; + } + + record->rollback_succeeded = 1; + record->rollback_verify_attempted = 1; + if (state->slot_ops->read_slot(decision->slot_addr, &observed_value, + state->slot_ops->opaque) != 0) { + return -1; + } + record->rollback_verified_value = observed_value; + if (observed_value != previous_value) { + return -1; + } + record->rollback_verify_succeeded = 1; + return 0; +} + +static void kzt_patch_spike_record_finish( + kzt_patch_spike_record_t *record, + const kzt_patch_spike_outcome_t *outcome) +{ + if (!record || !outcome) { + return; + } + + record->result = outcome->result; + record->failure = outcome->failure; + record->action = outcome->action; + record->skip_legacy_write = outcome->skip_legacy_write; + record->writes_remaining = outcome->writes_remaining; + record->writer_called = outcome->writer_called; + record->rollback_called = record->rollback_called || + outcome->rollback_called; + record->previous_value = outcome->previous_value; +} + +int kzt_patch_spike_writer_try_apply_with_slot_ops( + kzt_patch_spike_guard_t *guard, + const kzt_patch_decision_t *decision, + const kzt_patch_spike_slot_ops_t *slot_ops, + kzt_patch_spike_record_t *record) +{ + kzt_patch_spike_writer_state_t state; + kzt_patch_spike_writer_ops_t writer_ops; + kzt_patch_spike_outcome_t outcome; + const kzt_patch_spike_slot_ops_t *effective_slot_ops = slot_ops; + + if (!effective_slot_ops) { + effective_slot_ops = &kzt_patch_spike_direct_slot_ops; + } + + kzt_patch_spike_record_init(record, decision); + if (!guard || !record) { + return -1; + } + + if (!decision || + (decision->kind == KZT_PATCH_DECISION_APPROVED && + !kzt_patch_spike_writer_decision_supported(decision))) { + record->result = KZT_PATCH_SPIKE_RESULT_FAIL_OPEN; + record->failure = KZT_PATCH_SPIKE_FAILURE_INVALID_ARGUMENT; + record->action = KZT_PATCH_SPIKE_ACTION_KEEP_LEGACY; + record->writes_remaining = + kzt_patch_spike_guard_budget_remaining(guard); + return 0; + } + + memset(&state, 0, sizeof(state)); + state.slot_ops = effective_slot_ops; + state.record = record; + writer_ops.write_slot = kzt_patch_spike_writer_write_slot; + writer_ops.verify_slot = kzt_patch_spike_writer_verify_slot; + writer_ops.rollback_slot = kzt_patch_spike_writer_rollback_slot; + writer_ops.finish_slot = kzt_patch_spike_writer_finish_slot; + writer_ops.opaque = &state; + + if (kzt_patch_spike_guard_try_write(guard, decision, &writer_ops, + &outcome) != 0) { + return -1; + } + + kzt_patch_spike_record_finish(record, &outcome); + return 0; +} + +int kzt_patch_spike_writer_try_apply(kzt_patch_spike_guard_t *guard, + const kzt_patch_decision_t *decision, + kzt_patch_spike_record_t *record) +{ + return kzt_patch_spike_writer_try_apply_with_slot_ops(guard, decision, + NULL, record); +} diff --git a/target/i386/latx/context/kzt_plt_resolver_adapter.c b/target/i386/latx/context/kzt_plt_resolver_adapter.c new file mode 100644 index 00000000000..7e4fd5e537e --- /dev/null +++ b/target/i386/latx/context/kzt_plt_resolver_adapter.c @@ -0,0 +1,85 @@ +#ifndef KZT_PLT_RESOLVER_ADAPTER_TEST +#include "qemu/osdep.h" +#endif + +#include "kzt_plt_resolver_adapter.h" + +#include + +#ifndef KZT_PLT_RESOLVER_ADAPTER_TEST +#include "target/i386/cpu.h" +#endif + +static void resolver_push64(CPUX86State *cpu, uintptr_t value) +{ + cpu->regs[R_ESP] -= sizeof(uint64_t); + *(uint64_t *)cpu->regs[R_ESP] = value; +} + +int kzt_plt_resolver_enter( + CPUX86State *cpu, const kzt_plt_resolver_runtime_ops_t *ops, + kzt_plt_resolver_enter_result_t *result) +{ + const uint64_t *frame; + kzt_plt_resolver_source_t source; + kzt_lazy_binding_begin_request_t request; + kzt_lazy_binding_result_t begin_result; + + if (result) { + memset(result, 0, sizeof(*result)); + result->status = KZT_PLT_RESOLVER_ERROR; + } + if (!cpu || !ops || !result || !ops->lookup_source || + !ops->begin_lazy_binding || !ops->pending || !cpu->regs[R_ESP]) { + return -1; + } + + frame = (const uint64_t *)cpu->regs[R_ESP]; + result->object_head = frame[0]; + result->relocation_slot = frame[1]; + result->return_address = frame[2]; + memset(&source, 0, sizeof(source)); + if (ops->lookup_source(result->object_head, &source, ops->opaque) != 0 || + !source.guest_resolver || !source.source_link_map) { + result->status = KZT_PLT_RESOLVER_LEGACY_FRAME_RESTORED; + return 0; + } + + cpu->regs[R_ESP] += 2 * sizeof(uint64_t); + memset(&request, 0, sizeof(request)); + request.enabled = source.enabled; + request.context_id = source.context_id; + request.source_link_map = source.source_link_map; + request.source_generation = source.source_generation; + request.namespace_id = source.namespace_id; + request.namespace_kind = source.namespace_kind; + request.relocation_index = result->relocation_slot; + request.slot_addr = source.slot_addr; + request.unresolved_stub = source.unresolved_stub; + request.symbol = source.symbol; + request.version_evidence = source.version_evidence; + request.version = source.version; + request.addend = source.addend; + request.guest_resolver = source.guest_resolver; + memset(&begin_result, 0, sizeof(begin_result)); + if (ops->begin_lazy_binding(&request, ops->pending, &begin_result, + ops->opaque) != 0) { + begin_result.status = KZT_LAZY_BINDING_GUEST_PRESERVED; + } + + result->selected_resolver = source.guest_resolver; + result->pending_armed = begin_result.pending_armed; + if (begin_result.status == KZT_LAZY_BINDING_HANDOFF_GUEST) { + result->status = KZT_PLT_RESOLVER_HANDOFF_GUEST; + if (ops->completion_bridge && ops->original_return) { + *ops->original_return = *(uintptr_t *)cpu->regs[R_ESP]; + *(uintptr_t *)cpu->regs[R_ESP] = ops->completion_bridge; + } + } else { + result->status = KZT_PLT_RESOLVER_GUEST_PRESERVED; + } + resolver_push64(cpu, result->relocation_slot); + resolver_push64(cpu, source.source_link_map); + resolver_push64(cpu, source.guest_resolver); + return 0; +} diff --git a/target/i386/latx/context/kzt_rela_diagnostics.c b/target/i386/latx/context/kzt_rela_diagnostics.c new file mode 100644 index 00000000000..74317768482 --- /dev/null +++ b/target/i386/latx/context/kzt_rela_diagnostics.c @@ -0,0 +1,439 @@ +#include "kzt_rela_diagnostics.h" + +#include +#include +#include + +#define KZT_RELA_DIAGNOSTIC_THROTTLE_MAGIC 0x4b5a5444U + +static int kzt_rela_diagnostic_mode_enabled( + kzt_rela_diagnostic_mode_t mode) +{ + return mode == KZT_RELA_DIAGNOSTIC_MODE_DIAGNOSTICS || + mode == KZT_RELA_DIAGNOSTIC_MODE_DIAGNOSTICS_WRITE_ENABLED; +} + +static int kzt_rela_diagnostic_mode_records_writer( + kzt_rela_diagnostic_mode_t mode) +{ + return mode == KZT_RELA_DIAGNOSTIC_MODE_DIAGNOSTICS_WRITE_ENABLED; +} + +kzt_rela_diagnostic_mode_t kzt_rela_diagnostic_mode_from_flags( + int diagnostics_enabled, + int write_enabled) +{ + if (diagnostics_enabled && write_enabled) { + return KZT_RELA_DIAGNOSTIC_MODE_DIAGNOSTICS_WRITE_ENABLED; + } + if (diagnostics_enabled) { + return KZT_RELA_DIAGNOSTIC_MODE_DIAGNOSTICS; + } + if (write_enabled) { + return KZT_RELA_DIAGNOSTIC_MODE_WRITE_ENABLED_ONLY; + } + + return KZT_RELA_DIAGNOSTIC_MODE_DEFAULT; +} + +const char *kzt_rela_diagnostic_reason_domain_name( + kzt_rela_diagnostic_reason_domain_t domain) +{ + switch (domain) { + case KZT_RELA_DIAGNOSTIC_REASON_CANDIDATE: + return "candidate"; + case KZT_RELA_DIAGNOSTIC_REASON_PLANNER: + return "planner"; + case KZT_RELA_DIAGNOSTIC_REASON_WRITER: + return "writer"; + } + + return "unknown"; +} + +static const char *kzt_rela_candidate_status_name( + kzt_rela_immediate_candidate_status_t status) +{ + switch (status) { + case KZT_RELA_IMMEDIATE_CANDIDATE_SKIPPED: + return "SKIPPED"; + case KZT_RELA_IMMEDIATE_CANDIDATE_PLANNED: + return "PLANNED"; + case KZT_RELA_IMMEDIATE_CANDIDATE_FAIL_OPEN: + return "FAIL_OPEN"; + } + + return "UNKNOWN"; +} + +static const char *kzt_rela_candidate_reason_name( + kzt_rela_immediate_candidate_reason_t reason) +{ + switch (reason) { + case KZT_RELA_IMMEDIATE_CANDIDATE_REASON_NONE: + return "NONE"; + case KZT_RELA_IMMEDIATE_CANDIDATE_REASON_INVALID_ARGUMENT: + return "INVALID_ARGUMENT"; + case KZT_RELA_IMMEDIATE_CANDIDATE_REASON_NON_TARGET_RELOCATION: + return "NON_TARGET_RELOCATION"; + case KZT_RELA_IMMEDIATE_CANDIDATE_REASON_DEFERRED_LAZY_BINDING: + return "DEFERRED_LAZY_BINDING"; + case KZT_RELA_IMMEDIATE_CANDIDATE_REASON_MISSING_SLOT: + return "MISSING_SLOT"; + case KZT_RELA_IMMEDIATE_CANDIDATE_REASON_MISSING_CURRENT_VALUE: + return "MISSING_CURRENT_VALUE"; + case KZT_RELA_IMMEDIATE_CANDIDATE_REASON_MISSING_SYMBOL_NAME: + return "MISSING_SYMBOL_NAME"; + case KZT_RELA_IMMEDIATE_CANDIDATE_REASON_MISSING_SYMBOL_VERSION: + return "MISSING_SYMBOL_VERSION"; + case KZT_RELA_IMMEDIATE_CANDIDATE_REASON_PLANNER_ERROR: + return "PLANNER_ERROR"; + } + + return "UNKNOWN"; +} + +static void kzt_rela_diagnostic_copy_name(char *dst, size_t dst_size, + const char *src) +{ + if (!dst || dst_size == 0) { + return; + } + + snprintf(dst, dst_size, "%s", src ? src : "UNKNOWN"); +} + +static void kzt_rela_diagnostic_copy_source(char *dst, size_t dst_size, + const char *src) +{ + size_t i; + + if (!dst || dst_size == 0) { + return; + } + if (!src || !src[0]) { + src = "(unknown)"; + } + + for (i = 0; i + 1 < dst_size && src[i]; ++i) { + unsigned char value = (unsigned char)src[i]; + + if (value <= ' ' || value == '"' || value == '\\' || value == '=') { + dst[i] = '_'; + } else { + dst[i] = (char)value; + } + } + dst[i] = '\0'; +} + +static const char *kzt_rela_diagnostic_object_name( + const kzt_patch_object_ref_t *object) +{ + if (!object) { + return NULL; + } + if (object->path && object->path[0]) { + return object->path; + } + if (object->soname && object->soname[0]) { + return object->soname; + } + + return NULL; +} + +static void kzt_rela_diagnostic_apply_request( + const kzt_rela_immediate_candidate_request_t *request, + kzt_rela_diagnostic_record_t *record) +{ + if (!request || !record) { + return; + } + + kzt_rela_diagnostic_copy_source( + record->source, sizeof(record->source), + kzt_rela_diagnostic_object_name(&request->source)); + record->source_link_map = request->source.link_map_addr; + record->current_owner = request->current_owner.link_map_addr; + record->source_generation = request->source.generation; + record->current_owner_generation = request->current_owner.generation; + record->owner_match = request->owner_match; + record->wrapper_match = request->wrapper_match; + record->bridge_target = request->native_bridge_target; + kzt_rela_diagnostic_copy_name( + record->symbol, sizeof(record->symbol), request->symbol_name); + kzt_rela_diagnostic_copy_name( + record->version, sizeof(record->version), request->version); +} + +static void kzt_rela_diagnostic_apply_decision( + const kzt_patch_decision_t *decision, + kzt_rela_diagnostic_record_t *record) +{ + if (!decision || !record) { + return; + } + + kzt_rela_diagnostic_copy_source( + record->source, sizeof(record->source), + kzt_rela_diagnostic_object_name(&decision->source)); + record->source_link_map = decision->source.link_map_addr; + record->current_owner = decision->current_owner.link_map_addr; + record->source_generation = decision->source.generation; + record->current_owner_generation = decision->current_owner.generation; + record->owner_match = decision->owner_match; + record->wrapper_match = decision->wrapper_match; + record->bridge_target = decision->bridge_target; + kzt_rela_diagnostic_copy_name( + record->symbol, sizeof(record->symbol), decision->symbol_name); + kzt_rela_diagnostic_copy_name( + record->version, sizeof(record->version), decision->version); +} + +int kzt_rela_immediate_diagnostic_record( + kzt_rela_diagnostic_mode_t mode, + const kzt_rela_immediate_candidate_request_t *request, + const kzt_rela_immediate_writer_result_t *result, + int legacy_fallback, + kzt_rela_diagnostic_record_t *record) +{ + const kzt_rela_immediate_candidate_result_t *plan = NULL; + + if (!record) { + return -1; + } + + memset(record, 0, sizeof(*record)); + kzt_rela_diagnostic_copy_source(record->source, sizeof(record->source), + NULL); + record->owner_match = KZT_PATCH_OWNER_UNKNOWN; + record->wrapper_match = KZT_PATCH_WRAPPER_NO_MANIFEST; + record->reason_domain = KZT_RELA_DIAGNOSTIC_REASON_CANDIDATE; + kzt_rela_diagnostic_copy_name(record->reason, sizeof(record->reason), + "UNAVAILABLE"); + kzt_rela_diagnostic_copy_name(record->decision, + sizeof(record->decision), "UNAVAILABLE"); + kzt_rela_diagnostic_copy_name(record->writer_result, + sizeof(record->writer_result), + "NOT_RECORDED"); + record->legacy_fallback = legacy_fallback != 0; + kzt_rela_diagnostic_apply_request(request, record); + + if (result) { + plan = &result->plan; + kzt_rela_diagnostic_copy_name( + record->decision, sizeof(record->decision), + kzt_rela_candidate_status_name(plan->status)); + kzt_rela_diagnostic_copy_name( + record->reason, sizeof(record->reason), + kzt_rela_candidate_reason_name(plan->reason)); + } + + if (plan && plan->decision_present) { + record->reason_domain = KZT_RELA_DIAGNOSTIC_REASON_PLANNER; + kzt_rela_diagnostic_apply_decision(&plan->decision, record); + kzt_rela_diagnostic_copy_name( + record->decision, sizeof(record->decision), + kzt_patch_decision_kind_name(plan->decision.kind)); + kzt_rela_diagnostic_copy_name( + record->reason, sizeof(record->reason), + kzt_patch_reason_name(plan->decision.reason)); + } + + if (kzt_rela_diagnostic_mode_records_writer(mode) && result && + result->record.valid) { + record->reason_domain = KZT_RELA_DIAGNOSTIC_REASON_WRITER; + kzt_rela_diagnostic_copy_name( + record->reason, sizeof(record->reason), + kzt_patch_spike_failure_name(result->record.failure)); + kzt_rela_diagnostic_copy_name( + record->writer_result, sizeof(record->writer_result), + kzt_patch_spike_result_name(result->record.result)); + } + + return 0; +} + +kzt_rela_diagnostic_format_status_t kzt_rela_diagnostic_format( + const kzt_rela_diagnostic_record_t *record, + char *buffer, + size_t buffer_size) +{ + int written; + + if (!record || !buffer || buffer_size == 0) { + return KZT_RELA_DIAGNOSTIC_FORMAT_ERROR; + } + + written = snprintf( + buffer, buffer_size, + "kzt_rela_diagnostic source=%s source_link_map=0x%lx " + "source_generation=%lu current_owner=0x%lx " + "current_owner_generation=%lu owner_match=%s " + "wrapper_match=%s bridge_target=0x%lx symbol=%s version=%s " + "reason_domain=%s " + "reason=%s decision=%s writer_result=%s legacy_fallback=%d", + record->source, + (unsigned long)record->source_link_map, + record->source_generation, + (unsigned long)record->current_owner, + record->current_owner_generation, + kzt_patch_owner_match_name(record->owner_match), + kzt_patch_wrapper_match_name(record->wrapper_match), + (unsigned long)record->bridge_target, + record->symbol, + record->version, + kzt_rela_diagnostic_reason_domain_name(record->reason_domain), + record->reason, + record->decision, + record->writer_result, + record->legacy_fallback); + if (written < 0) { + return KZT_RELA_DIAGNOSTIC_FORMAT_ERROR; + } + if ((size_t)written >= buffer_size) { + return KZT_RELA_DIAGNOSTIC_FORMAT_TRUNCATED; + } + + return KZT_RELA_DIAGNOSTIC_FORMAT_OK; +} + +static int kzt_rela_diagnostic_throttle_valid( + const kzt_rela_diagnostic_throttle_t *throttle) +{ + return throttle && + __atomic_load_n(&throttle->initialized, __ATOMIC_ACQUIRE) == + KZT_RELA_DIAGNOSTIC_THROTTLE_MAGIC; +} + +static void kzt_rela_diagnostic_throttle_lock( + kzt_rela_diagnostic_throttle_t *throttle) +{ + while (__atomic_exchange_n(&throttle->lock, 1U, __ATOMIC_ACQUIRE) != 0U) { + } +} + +static void kzt_rela_diagnostic_throttle_unlock( + kzt_rela_diagnostic_throttle_t *throttle) +{ + __atomic_store_n(&throttle->lock, 0U, __ATOMIC_RELEASE); +} + +int kzt_rela_diagnostic_throttle_init( + kzt_rela_diagnostic_throttle_t *throttle, + unsigned long capacity) +{ + if (!throttle) { + return -1; + } + + memset(throttle, 0, sizeof(*throttle)); + throttle->capacity = capacity; + __atomic_store_n(&throttle->initialized, + KZT_RELA_DIAGNOSTIC_THROTTLE_MAGIC, + __ATOMIC_RELEASE); + return 0; +} + +int kzt_rela_diagnostic_throttle_try_admit( + kzt_rela_diagnostic_throttle_t *throttle) +{ + int result; + + if (!kzt_rela_diagnostic_throttle_valid(throttle)) { + return -1; + } + + kzt_rela_diagnostic_throttle_lock(throttle); + if (throttle->admitted >= throttle->capacity) { + if (throttle->suppressed != ULONG_MAX) { + ++throttle->suppressed; + } + result = 0; + } else { + ++throttle->admitted; + result = 1; + } + kzt_rela_diagnostic_throttle_unlock(throttle); + return result; +} + +int kzt_rela_diagnostic_throttle_snapshot( + kzt_rela_diagnostic_throttle_t *throttle, + kzt_rela_diagnostic_throttle_snapshot_t *snapshot) +{ + if (!snapshot || !kzt_rela_diagnostic_throttle_valid(throttle)) { + return -1; + } + + kzt_rela_diagnostic_throttle_lock(throttle); + snapshot->capacity = throttle->capacity; + snapshot->admitted = throttle->admitted; + snapshot->suppressed = throttle->suppressed; + kzt_rela_diagnostic_throttle_unlock(throttle); + return 0; +} + +int kzt_rela_immediate_diagnostic_emit( + const kzt_rela_immediate_diagnostic_input_t *input, + kzt_rela_immediate_diagnostic_result_t *result) +{ + int admitted; + + if (!input || !result) { + return -1; + } + + memset(result, 0, sizeof(*result)); + result->status = KZT_RELA_DIAGNOSTIC_EMIT_DISABLED; + result->format_status = KZT_RELA_DIAGNOSTIC_FORMAT_ERROR; + if (!kzt_rela_diagnostic_mode_enabled(input->mode)) { + return 0; + } + + admitted = kzt_rela_diagnostic_throttle_try_admit(input->throttle); + if (admitted < 0) { + result->status = KZT_RELA_DIAGNOSTIC_EMIT_THROTTLE_FAILED; + return 0; + } + if (admitted == 0) { + result->status = KZT_RELA_DIAGNOSTIC_EMIT_SUPPRESSED; + return 0; + } + + if (kzt_rela_immediate_diagnostic_record( + input->mode, input->request, input->result, + input->legacy_fallback, &result->record) != 0) { + result->status = KZT_RELA_DIAGNOSTIC_EMIT_FORMAT_FAILED; + return 0; + } + result->record_present = 1; + + result->format_status = kzt_rela_diagnostic_format( + &result->record, input->buffer, input->buffer_size); + if (result->format_status == KZT_RELA_DIAGNOSTIC_FORMAT_TRUNCATED) { + result->status = KZT_RELA_DIAGNOSTIC_EMIT_FORMAT_TRUNCATED; + return 0; + } + if (result->format_status != KZT_RELA_DIAGNOSTIC_FORMAT_OK) { + result->status = KZT_RELA_DIAGNOSTIC_EMIT_FORMAT_FAILED; + return 0; + } + + if (!input->sink) { + result->status = KZT_RELA_DIAGNOSTIC_EMIT_SINK_FAILED; + result->sink_status = -1; + return 0; + } + + result->sink_status = input->sink( + input->buffer, strlen(input->buffer), input->sink_opaque); + if (result->sink_status != 0) { + result->status = KZT_RELA_DIAGNOSTIC_EMIT_SINK_FAILED; + return 0; + } + + result->status = KZT_RELA_DIAGNOSTIC_EMIT_EMITTED; + return 0; +} diff --git a/target/i386/latx/context/kzt_rela_immediate_candidate.c b/target/i386/latx/context/kzt_rela_immediate_candidate.c new file mode 100644 index 00000000000..c81f5caf9fc --- /dev/null +++ b/target/i386/latx/context/kzt_rela_immediate_candidate.c @@ -0,0 +1,174 @@ +#include "kzt_rela_immediate_candidate.h" + +#include + +#include "elf.h" + +static int kzt_rela_immediate_string_empty(const char *value) +{ + return !value || !value[0]; +} + +static void kzt_rela_immediate_result_set( + kzt_rela_immediate_candidate_result_t *result, + kzt_rela_immediate_candidate_status_t status, + kzt_rela_immediate_candidate_reason_t reason) +{ + result->status = status; + result->reason = reason; +} + +static int kzt_rela_immediate_fail_open( + kzt_rela_immediate_candidate_result_t *result, + kzt_rela_immediate_candidate_reason_t reason) +{ + kzt_rela_immediate_result_set( + result, KZT_RELA_IMMEDIATE_CANDIDATE_FAIL_OPEN, reason); + return 0; +} + +static void kzt_rela_immediate_copy_request( + const kzt_rela_immediate_candidate_request_t *request, + kzt_patch_candidate_t *candidate) +{ + memset(candidate, 0, sizeof(*candidate)); + candidate->source = request->source; + candidate->dynamic_addr = request->dynamic_addr; + candidate->load_bias = request->load_bias; + candidate->dynamic_view_generation = request->dynamic_view_generation; + candidate->dynamic_view_available = request->dynamic_view_available; + candidate->table_kind = request->table_kind; + candidate->entry_index = request->entry_index; + candidate->entry_addr = request->entry_addr; + candidate->reloc_type = KZT_PATCH_RELOCATION_JUMP_SLOT; + candidate->slot_addr = request->slot_addr; + candidate->slot_current_value_present = + request->slot_current_value_present; + candidate->slot_current_value = request->slot_current_value; + candidate->lazy_binding_deferred = request->lazy_binding_deferred; + candidate->symbol_index = request->symbol_index; + candidate->symbol_name = request->symbol_name; + candidate->version_evidence = request->version_evidence; + candidate->version = request->version; + candidate->current_owner = request->current_owner; + candidate->owner_match = request->owner_match; + candidate->wrapper_match = request->wrapper_match; + candidate->wrapper_name = request->wrapper_name; + candidate->wrapper_version_evidence = + request->wrapper_version_evidence; + candidate->wrapper_symbol_version = request->wrapper_symbol_version; + candidate->bridge_target = request->native_bridge_target; +} + +int kzt_rela_immediate_jump_slot_plan( + const kzt_rela_immediate_candidate_request_t *request, + kzt_rela_immediate_candidate_result_t *result) +{ + kzt_patch_decision_t decision; + + if (!result) { + return -1; + } + + memset(result, 0, sizeof(*result)); + if (!request) { + return kzt_rela_immediate_fail_open( + result, KZT_RELA_IMMEDIATE_CANDIDATE_REASON_INVALID_ARGUMENT); + } + + if (request->relocation_type != R_X86_64_JUMP_SLOT) { + kzt_rela_immediate_result_set( + result, KZT_RELA_IMMEDIATE_CANDIDATE_SKIPPED, + KZT_RELA_IMMEDIATE_CANDIDATE_REASON_NON_TARGET_RELOCATION); + return 0; + } + + if (request->lazy_binding_deferred) { + kzt_rela_immediate_result_set( + result, KZT_RELA_IMMEDIATE_CANDIDATE_SKIPPED, + KZT_RELA_IMMEDIATE_CANDIDATE_REASON_DEFERRED_LAZY_BINDING); + return 0; + } + + if (request->slot_addr == 0) { + return kzt_rela_immediate_fail_open( + result, KZT_RELA_IMMEDIATE_CANDIDATE_REASON_MISSING_SLOT); + } + + if (!request->slot_current_value_present) { + return kzt_rela_immediate_fail_open( + result, + KZT_RELA_IMMEDIATE_CANDIDATE_REASON_MISSING_CURRENT_VALUE); + } + + if (kzt_rela_immediate_string_empty(request->symbol_name)) { + return kzt_rela_immediate_fail_open( + result, KZT_RELA_IMMEDIATE_CANDIDATE_REASON_MISSING_SYMBOL_NAME); + } + + if (!kzt_symbol_version_evidence_valid(request->version_evidence, + request->version)) { + return kzt_rela_immediate_fail_open( + result, + KZT_RELA_IMMEDIATE_CANDIDATE_REASON_MISSING_SYMBOL_VERSION); + } + + kzt_rela_immediate_copy_request(request, &result->candidate); + result->candidate_present = 1; + + if (kzt_patch_planner_decide(&result->candidate, &decision) != 0) { + result->candidate_present = 0; + return kzt_rela_immediate_fail_open( + result, KZT_RELA_IMMEDIATE_CANDIDATE_REASON_PLANNER_ERROR); + } + + result->decision = decision; + result->decision_present = 1; + kzt_rela_immediate_result_set( + result, KZT_RELA_IMMEDIATE_CANDIDATE_PLANNED, + KZT_RELA_IMMEDIATE_CANDIDATE_REASON_NONE); + return 0; +} + +static int kzt_rela_immediate_decision_allows_writer( + const kzt_rela_immediate_candidate_result_t *plan) +{ + return plan && plan->status == KZT_RELA_IMMEDIATE_CANDIDATE_PLANNED && + plan->decision_present && + plan->decision.kind == KZT_PATCH_DECISION_APPROVED && + plan->decision.allow_native_bridge; +} + +int kzt_rela_immediate_jump_slot_try_write( + const kzt_rela_immediate_candidate_request_t *request, + kzt_patch_spike_guard_t *guard, + const kzt_patch_spike_slot_ops_t *slot_ops, + kzt_rela_immediate_writer_result_t *result) +{ + kzt_rela_immediate_writer_result_t local_result; + + if (!result) { + result = &local_result; + } + + memset(result, 0, sizeof(*result)); + result->planner_called = 1; + if (kzt_rela_immediate_jump_slot_plan(request, &result->plan) != 0) { + return 0; + } + + if (!kzt_rela_immediate_decision_allows_writer(&result->plan)) { + return 0; + } + + if (kzt_patch_spike_writer_try_apply_with_slot_ops( + guard, &result->plan.decision, slot_ops, &result->record) != 0) { + return 0; + } + + result->writer_called = result->record.writer_called; + result->skip_legacy_write = + result->record.result == KZT_PATCH_SPIKE_RESULT_APPLIED && + result->record.skip_legacy_write; + return 0; +} diff --git a/target/i386/latx/context/kzt_rela_request_enricher.c b/target/i386/latx/context/kzt_rela_request_enricher.c new file mode 100644 index 00000000000..7fe2db5ab46 --- /dev/null +++ b/target/i386/latx/context/kzt_rela_request_enricher.c @@ -0,0 +1,371 @@ +#include "kzt_rela_request_enricher.h" +#include "kzt_rela_stub_detector.h" + +#include +#include + +static int kzt_rela_add_load_bias(uintptr_t address, intptr_t load_bias, + uintptr_t *runtime_address) +{ + uintptr_t magnitude; + + if (!runtime_address) { + return 0; + } + if (load_bias >= 0) { + magnitude = (uintptr_t)load_bias; + if (address > UINTPTR_MAX - magnitude) { + return 0; + } + *runtime_address = address + magnitude; + return 1; + } + + magnitude = (uintptr_t)(-(load_bias + 1)) + 1; + if (address < magnitude) { + return 0; + } + *runtime_address = address - magnitude; + return 1; +} + +static int kzt_rela_value_in_range(uintptr_t value, uintptr_t start, + uintptr_t end) +{ + return start < end && value >= start && value < end; +} + +static int kzt_rela_value_in_loaded_range(uintptr_t value, + intptr_t load_bias, + uintptr_t start, + uintptr_t end) +{ + uintptr_t runtime_start; + uintptr_t runtime_end; + + if (start >= end || + !kzt_rela_add_load_bias(start, load_bias, &runtime_start) || + !kzt_rela_add_load_bias(end, load_bias, &runtime_end)) { + return 0; + } + + return kzt_rela_value_in_range(value, runtime_start, runtime_end); +} + +int kzt_rela_slot_current_is_unresolved_stub( + uintptr_t slot_current_value, kzt_rela_stub_coordinate_t coordinate, + intptr_t load_bias, + uintptr_t plt_start, uintptr_t plt_end, + uintptr_t gotplt_start, uintptr_t gotplt_end) +{ + if (!slot_current_value || + coordinate == KZT_RELA_STUB_COORDINATE_UNKNOWN) { + return 0; + } + + if (coordinate == KZT_RELA_STUB_COORDINATE_LINK_TIME_RAW) { + return kzt_rela_value_in_range(slot_current_value, + plt_start, plt_end) || + kzt_rela_value_in_range(slot_current_value, + gotplt_start, gotplt_end); + } + if (coordinate == KZT_RELA_STUB_COORDINATE_RUNTIME_REBASED) { + return kzt_rela_value_in_loaded_range(slot_current_value, load_bias, + plt_start, plt_end) || + kzt_rela_value_in_loaded_range(slot_current_value, load_bias, + gotplt_start, gotplt_end); + } + + return 0; +} + +static int kzt_rela_enricher_field_ok(kzt_guest_field_status_t status) +{ + return status == KZT_GUEST_FIELD_OK; +} + +static int kzt_rela_enricher_string_has_value( + kzt_guest_field_status_t status) +{ + return status == KZT_GUEST_FIELD_OK || + status == KZT_GUEST_FIELD_TRUNCATED; +} + +static void kzt_rela_enricher_copy_text(char *dst, size_t dst_size, + const char *src) +{ + if (!dst || dst_size == 0) { + return; + } + if (!src) { + dst[0] = '\0'; + return; + } + + snprintf(dst, dst_size, "%s", src); +} + +static int kzt_rela_enricher_snapshot_has_range( + const kzt_guest_object_snapshot_t *snapshot) +{ + if (!snapshot) { + return 0; + } + if (!kzt_rela_enricher_field_ok(snapshot->map_start.status) || + !kzt_rela_enricher_field_ok(snapshot->map_end.status)) { + return 0; + } + + return snapshot->map_start.value < snapshot->map_end.value; +} + +static int kzt_rela_enricher_snapshot_contains( + const kzt_guest_object_snapshot_t *snapshot, uintptr_t address) +{ + if (!kzt_rela_enricher_snapshot_has_range(snapshot)) { + return 0; + } + + return address >= snapshot->map_start.value && + address < snapshot->map_end.value; +} + +static const char *kzt_rela_enricher_snapshot_string( + const kzt_guest_string_field_t *field) +{ + if (!field || !kzt_rela_enricher_string_has_value(field->status)) { + return NULL; + } + + return field->value; +} + +static void kzt_rela_enricher_ref_from_snapshot( + const kzt_guest_object_snapshot_t *snapshot, + kzt_rela_request_enricher_text_t *text, + kzt_patch_object_ref_t *ref) +{ + const char *soname; + const char *path; + + memset(ref, 0, sizeof(*ref)); + if (!snapshot || !text) { + return; + } + + soname = kzt_rela_enricher_snapshot_string(&snapshot->soname); + path = kzt_rela_enricher_snapshot_string(&snapshot->path); + kzt_rela_enricher_copy_text(text->soname, sizeof(text->soname), soname); + kzt_rela_enricher_copy_text(text->path, sizeof(text->path), path); + + ref->known = 1; + ref->link_map_addr = snapshot->link_map_addr; + ref->map_start = snapshot->map_start.value; + ref->map_end = snapshot->map_end.value; + ref->generation = snapshot->generation; + ref->soname = text->soname[0] ? text->soname : NULL; + ref->path = text->path[0] ? text->path : NULL; +} + +static int kzt_rela_enricher_find_unique_source( + kzt_guest_registry_t *registry, uintptr_t address, + kzt_rela_request_enricher_result_t *result) +{ + kzt_guest_registry_dump_t dump = { 0 }; + const kzt_guest_object_snapshot_t *match = NULL; + size_t i; + size_t count = 0; + + if (!registry || !address || !result) { + return -1; + } + if (kzt_guest_registry_dump_snapshot(registry, &dump) != 0) { + return -1; + } + + for (i = 0; i < dump.count; ++i) { + if (dump.objects[i].state == KZT_GUEST_OBJECT_UNLOADING || + dump.objects[i].state == KZT_GUEST_OBJECT_DEAD) { + continue; + } + if (!kzt_rela_enricher_snapshot_contains(&dump.objects[i], address)) { + continue; + } + ++count; + if (!match) { + match = &dump.objects[i]; + } + } + + if (count == 1 && match) { + kzt_rela_enricher_ref_from_snapshot( + match, &result->source_text, &result->source); + result->source_present = result->source.known; + } + + kzt_guest_registry_dump_free(&dump); + return result->source_present ? 0 : -1; +} + +static void kzt_rela_enricher_apply_source( + kzt_rela_immediate_candidate_request_t *request, + const kzt_patch_object_ref_t *source) +{ + const char *fallback_soname; + const char *fallback_path; + + if (!request || !source || !source->known) { + return; + } + + fallback_soname = request->source.soname; + fallback_path = request->source.path; + request->source = *source; + if (!request->source.soname) { + request->source.soname = fallback_soname; + } + if (!request->source.path) { + request->source.path = fallback_path; + } +} + +static void kzt_rela_enricher_apply_dynamic_view( + kzt_rela_immediate_candidate_request_t *request, + const kzt_rela_request_enricher_input_t *input, + kzt_rela_request_enricher_result_t *result) +{ + kzt_guest_dynamic_view_t view; + kzt_guest_field_status_t status; + unsigned long generation; + + request->dynamic_view_available = 0; + request->dynamic_view_generation = 0; + if (!input || !input->registry || !request->source.link_map_addr) { + return; + } + + if (kzt_guest_registry_find_dynamic_view( + input->registry, request->source.link_map_addr, &view, &status, + &generation) != 0) { + return; + } + if (status != KZT_GUEST_FIELD_OK || + view.status != KZT_GUEST_DYNAMIC_COMPLETE) { + return; + } + + request->dynamic_addr = view.dynamic_addr; + request->load_bias = view.load_bias; + request->dynamic_view_available = 1; + request->dynamic_view_generation = generation; + if (result) { + result->dynamic_view_present = 1; + } +} + +static void kzt_rela_enricher_apply_owner( + kzt_rela_immediate_candidate_request_t *request, + const kzt_rela_request_enricher_input_t *input, + kzt_rela_request_enricher_result_t *result) +{ + if (!request || !input || !result) { + return; + } + + request->owner_match = KZT_PATCH_OWNER_UNKNOWN; + memset(&request->current_owner, 0, sizeof(request->current_owner)); + kzt_owner_resolver_init(&result->owner_resolution); + if (input->slot_current_value_is_unresolved_stub) { + request->lazy_binding_deferred = 1; + return; + } + + if (kzt_owner_resolver_resolve_current( + input->registry, request->slot_current_value, + request->expected_guest_target, &result->owner_resolution) != 0) { + return; + } + + request->current_owner = result->owner_resolution.current_owner; + request->owner_match = result->owner_resolution.owner_match; + result->owner_present = request->current_owner.known; +} + +static void kzt_rela_enricher_apply_wrapper( + kzt_rela_immediate_candidate_request_t *request, + const kzt_rela_request_enricher_input_t *input, + kzt_rela_request_enricher_result_t *result) +{ + const kzt_wrapper_probe_bridge_ops_t *bridge_ops; + kzt_wrapper_probe_bridge_ops_t restricted_bridge_ops; + kzt_wrapper_probe_request_t probe_request; + + if (!request || !input || !result) { + return; + } + + request->wrapper_match = KZT_PATCH_WRAPPER_NO_MANIFEST; + request->wrapper_name = NULL; + request->wrapper_version_evidence = KZT_SYMBOL_VERSION_UNKNOWN; + request->wrapper_symbol_version = NULL; + request->native_bridge_target = 0; + + probe_request.symbol_name = request->symbol_name; + probe_request.symbol_version_evidence = request->version_evidence; + probe_request.symbol_version = request->version; + bridge_ops = input->bridge_ops; + if (bridge_ops && + (!request->dynamic_view_available || + request->owner_match != KZT_PATCH_OWNER_MATCH)) { + restricted_bridge_ops = *bridge_ops; + restricted_bridge_ops.add_bridge = NULL; + bridge_ops = &restricted_bridge_ops; + } + if (kzt_wrapper_probe_minimal_manifest( + input->wrapper_manifest, &probe_request, bridge_ops, + &result->wrapper_probe) != 0) { + return; + } + + kzt_wrapper_probe_apply_to_decision_request( + &result->wrapper_probe, &request->wrapper_match, + &request->wrapper_name, &request->wrapper_symbol_version, + &request->native_bridge_target); + request->wrapper_version_evidence = + result->wrapper_probe.wrapper_version_evidence; + result->wrapper_present = 1; +} + +void kzt_rela_request_enricher_result_init( + kzt_rela_request_enricher_result_t *result) +{ + if (!result) { + return; + } + + memset(result, 0, sizeof(*result)); + kzt_owner_resolver_init(&result->owner_resolution); + result->wrapper_probe.wrapper_match = KZT_PATCH_WRAPPER_NO_MANIFEST; +} + +int kzt_rela_immediate_request_enrich( + kzt_rela_immediate_candidate_request_t *request, + const kzt_rela_request_enricher_input_t *input, + kzt_rela_request_enricher_result_t *result) +{ + if (!request || !input || !result) { + return -1; + } + kzt_rela_request_enricher_result_init(result); + + if (request->source.known && request->source.map_start && + kzt_rela_enricher_find_unique_source( + input->registry, request->source.map_start, result) == 0) { + kzt_rela_enricher_apply_source(request, &result->source); + } + + kzt_rela_enricher_apply_dynamic_view(request, input, result); + kzt_rela_enricher_apply_owner(request, input, result); + kzt_rela_enricher_apply_wrapper(request, input, result); + return 0; +} diff --git a/target/i386/latx/context/kzt_rela_runtime_bridge.c b/target/i386/latx/context/kzt_rela_runtime_bridge.c new file mode 100644 index 00000000000..821f12b3b67 --- /dev/null +++ b/target/i386/latx/context/kzt_rela_runtime_bridge.c @@ -0,0 +1,476 @@ +#include "kzt_rela_runtime_bridge.h" + +#include +#include +#include + +#include "box64context.h" +#include "kzt_bridge_exact.h" +#include "khash.h" +#include "librarian_private.h" +#include "library.h" +#include "library_private.h" + +extern uintptr_t CheckBridged(bridge_t *bridge, void *fnc); +extern uintptr_t AddCheckBridge(bridge_t *bridge, wrapper_t wrapper, + void *fnc, int stack_bytes, + const char *name) __attribute__((weak)); +extern void *GetNativeSymbolUnversionned( + void *lib, const char *name) __attribute__((weak)); + +static int kzt_rela_runtime_wrapper_map_entry( + kh_symbolmap_t *map, const char *symbol_name, wrapper_t *wrapper) +{ + khint_t key; + + if (!map || !symbol_name || !wrapper) { + return 0; + } + key = kh_get(symbolmap, map, symbol_name); + if (key == kh_end(map)) { + return 0; + } + + *wrapper = kh_value(map, key); + return *wrapper ? 1 : -1; +} + +typedef struct kzt_rela_runtime_wrapper_candidate { + wrapper_t wrapper; + const char *native_name; + int custom_wrapper; + int stack_bytes; +} kzt_rela_runtime_wrapper_candidate_t; + +typedef struct kzt_rela_runtime_provider_state { + box64context_t *context; + uintptr_t resolved_target; + int discover_bridge; +} kzt_rela_runtime_provider_state_t; + +static void *kzt_rela_runtime_lookup_native( + void *handle, const char *native_name, int custom_wrapper) +{ + void *native_symbol; + + if (!handle || !native_name || !native_name[0]) { + return NULL; + } + native_symbol = dlsym(handle, native_name); + if (!native_symbol && !custom_wrapper && GetNativeSymbolUnversionned) { + native_symbol = GetNativeSymbolUnversionned(handle, native_name); + } + return native_symbol; +} + +static int kzt_rela_runtime_custom_native_name( + const library_t *lib, const char *symbol_name, char *buffer, + size_t buffer_size) +{ + const char *prefix; + + if (!lib || !symbol_name || !buffer || buffer_size == 0) { + return -1; + } + prefix = lib->altmy ? lib->altmy : "my_"; + if (snprintf(buffer, buffer_size, "%s%s", prefix, symbol_name) >= + (int)buffer_size) { + return -1; + } + return 0; +} + +static uintptr_t kzt_rela_runtime_provider_exact_bridge( + library_t *provider, uintptr_t resolved_target, wrapper_t wrapper, + void *native_symbol) +{ + uintptr_t provider_target; + + if (!provider || !provider->priv.w.bridge || !wrapper || !native_symbol) { + return 0; + } + + /* CheckBridged is a read-only lookup keyed by the native symbol. Do not + inspect resolved_target as a onebridge_t until the provider map proves + that the target belongs to this provider. */ + provider_target = CheckBridged(provider->priv.w.bridge, native_symbol); + if (!provider_target || + (resolved_target && provider_target != resolved_target)) { + return 0; + } + + return kzt_bridge_is_exact(provider_target, wrapper, native_symbol) ? + provider_target : 0; +} + +static int kzt_rela_runtime_context_owns_library( + box64context_t *context, library_t *library) +{ + lib_t *scopes[2]; + size_t i; + int j; + + if (!context || !library) { + return 0; + } + scopes[0] = context->maplib; + scopes[1] = context->local_maplib; + for (i = 0; i < sizeof(scopes) / sizeof(scopes[0]); ++i) { + if (!scopes[i]) { + continue; + } + for (j = 0; j < scopes[i]->libsz; ++j) { + if (scopes[i]->libraries[j] == library) { + return 1; + } + } + } + return 0; +} + +static int kzt_rela_runtime_match_lifetime_valid( + const kzt_wrapper_bridge_provider_match_t *match) +{ + box64context_t *context; + library_t *provider; + struct link_map *lookup_owner = NULL; + struct link_map *native_owner = NULL; + Dl_info symbol_info; + void *expected_handle; + void *native_symbol; + + if (!match || !match->context_owner || !match->wrapper_provider || + !match->native_lookup_handle || !match->native_owner || + !match->bridge_owner || !match->bridge_storage || + !match->native_name[0] || !match->abi_wrapper || + !match->native_symbol || + !match->wrapper_provider_lifetime_bound || + !match->native_owner_lifetime_bound || + !match->bridge_owner_lifetime_bound) { + return 0; + } + + context = match->context_owner; + provider = match->wrapper_provider; + expected_handle = match->custom_wrapper ? provider->priv.w.box64lib : + provider->priv.w.lib; + if (!provider->active || provider->type != LIB_WRAPPED || + provider->context != context || + !kzt_rela_runtime_context_owns_library(context, provider) || + match->bridge_owner != provider || + match->bridge_storage != provider->priv.w.bridge || + expected_handle != match->native_lookup_handle || + dlinfo(expected_handle, RTLD_DI_LINKMAP, &lookup_owner) != 0 || + !lookup_owner) { + return 0; + } + + native_symbol = kzt_rela_runtime_lookup_native( + expected_handle, match->native_name, match->custom_wrapper); + if ((uintptr_t)native_symbol != match->native_symbol || + dladdr1(native_symbol, &symbol_info, (void **)&native_owner, + RTLD_DL_LINKMAP) == 0 || + native_owner != match->native_owner) { + return 0; + } + return 1; +} + +static int kzt_rela_runtime_provider_inspect( + void *library, const char *symbol_name, const char *symbol_version, + kzt_wrapper_bridge_provider_match_t *match, void *opaque) +{ + library_t *lib = library; + kzt_rela_runtime_provider_state_t *state = opaque; + wrapper_t wrapper = NULL; + wrapper_t candidate = NULL; + const char *native_name = symbol_name; + char prefixed_name[256]; + char custom_name[256]; + struct link_map *handle_map = NULL; + struct link_map *symbol_map = NULL; + Dl_info symbol_info; + khint_t key; + uintptr_t bridge_target; + int matches = 0; + int allow_altprefix = 1; + + (void)symbol_version; + if (!lib || !state || !match || !symbol_name || + !lib->active || lib->type != LIB_WRAPPED || !lib->priv.w.lib || + !lib->priv.w.bridge || lib->context != state->context || + !kzt_rela_runtime_context_owns_library(state->context, lib) || + (!state->resolved_target && !state->discover_bridge)) { + return 0; + } + + if (kzt_rela_runtime_wrapper_map_entry( + lib->mysymbolmap, symbol_name, &candidate)) { + if (kzt_rela_runtime_custom_native_name( + lib, symbol_name, custom_name, sizeof(custom_name)) != 0) { + return -1; + } + wrapper = candidate; + native_name = custom_name; + ++matches; + allow_altprefix = 0; + match->custom_wrapper = 1; + } + if (kzt_rela_runtime_wrapper_map_entry( + lib->wmysymbolmap, symbol_name, &candidate)) { + if (kzt_rela_runtime_custom_native_name( + lib, symbol_name, custom_name, sizeof(custom_name)) != 0) { + return -1; + } + wrapper = candidate; + native_name = custom_name; + ++matches; + allow_altprefix = 0; + match->custom_wrapper = 1; + } + if (kzt_rela_runtime_wrapper_map_entry( + lib->stsymbolmap, symbol_name, &candidate)) { + if (kzt_rela_runtime_custom_native_name( + lib, symbol_name, custom_name, sizeof(custom_name)) != 0) { + return -1; + } + wrapper = candidate; + native_name = custom_name; + ++matches; + allow_altprefix = 0; + match->custom_wrapper = 1; + match->stack_bytes = sizeof(void *); + } + + if (kzt_rela_runtime_wrapper_map_entry( + lib->symbolmap, symbol_name, &candidate)) { + wrapper = candidate; + ++matches; + } + if (kzt_rela_runtime_wrapper_map_entry( + lib->wsymbolmap, symbol_name, &candidate)) { + wrapper = candidate; + ++matches; + } + if (lib->symbol2map) { + key = kh_get(symbol2map, lib->symbol2map, symbol_name); + if (key != kh_end(lib->symbol2map)) { + wrapper = kh_value(lib->symbol2map, key).w; + native_name = kh_value(lib->symbol2map, key).name; + allow_altprefix = 0; + ++matches; + } + } + if (matches == 0) { + return 0; + } + if (matches != 1 || !wrapper || !native_name || !native_name[0]) { + return -1; + } + + if (lib->priv.w.altprefix && allow_altprefix) { + if (snprintf(prefixed_name, sizeof(prefixed_name), "%s%s", + lib->priv.w.altprefix, symbol_name) >= + (int)sizeof(prefixed_name)) { + return -1; + } + native_name = prefixed_name; + } + + /* symbol_version describes the guest relocation. The host provider may + legitimately export the same ABI wrapper under a different GLIBC + version, so native lookup keeps the wrapped-library semantics. */ + match->native_symbol = (uintptr_t)kzt_rela_runtime_lookup_native( + match->custom_wrapper ? lib->priv.w.box64lib : lib->priv.w.lib, + native_name, match->custom_wrapper); + if (symbol_version && symbol_version[0] && + (strstr(symbol_version, "NOT_REAL") || + strstr(symbol_version, "UNSUPPORTED"))) { + return -1; + } + if (symbol_version && symbol_version[0] && !match->custom_wrapper && + state->resolved_target && + !kzt_rela_runtime_provider_exact_bridge( + lib, state->resolved_target, wrapper, + (void *)match->native_symbol)) { + uintptr_t versioned_symbol = (uintptr_t)dlvsym( + lib->priv.w.lib, native_name, symbol_version); + + if (!versioned_symbol || + !kzt_rela_runtime_provider_exact_bridge( + lib, state->resolved_target, wrapper, + (void *)versioned_symbol)) { + return -1; + } + match->native_symbol = versioned_symbol; + } + if (!match->native_symbol) { + return -1; + } + if (dlinfo(match->custom_wrapper ? lib->priv.w.box64lib : lib->priv.w.lib, + RTLD_DI_LINKMAP, &handle_map) != 0 || + !handle_map || + dladdr1((void *)match->native_symbol, &symbol_info, + (void **)&symbol_map, RTLD_DL_LINKMAP) == 0 || + symbol_map != handle_map) { + return -1; + } + bridge_target = kzt_rela_runtime_provider_exact_bridge( + lib, state->resolved_target, wrapper, (void *)match->native_symbol); + if (!bridge_target && + (CheckBridged(lib->priv.w.bridge, (void *)match->native_symbol) || + !state->discover_bridge)) { + return -1; + } + + match->wrapper_name = lib->name; + snprintf(match->native_name, sizeof(match->native_name), "%s", + native_name); + match->abi_wrapper = wrapper; + match->resolved_bridge_target = bridge_target; + match->context_owner = state->context; + match->wrapper_provider = lib; + match->native_lookup_handle = match->custom_wrapper ? lib->priv.w.box64lib : + lib->priv.w.lib; + match->native_owner = symbol_map; + match->bridge_owner = lib; + match->bridge_storage = lib->priv.w.bridge; + match->resolved_bridge_exact = bridge_target != 0; + /* Wrapped native handles stay live in a root context scope until + NativeLib_FinishFini runs during context teardown. */ + match->wrapper_provider_lifetime_bound = 1; + match->native_owner_lifetime_bound = 1; + match->bridge_owner_lifetime_bound = 1; + return 1; +} + +static uintptr_t kzt_rela_runtime_provider_check( + const kzt_wrapper_bridge_provider_match_t *match, void *opaque) +{ + library_t *provider; + + (void)opaque; + if (!match || !match->bridge_owner || !match->abi_wrapper || + !match->native_symbol || + !kzt_rela_runtime_match_lifetime_valid(match) || + (!!match->resolved_bridge_target != !!match->resolved_bridge_exact)) { + return 0; + } + provider = match->bridge_owner; + return kzt_rela_runtime_provider_exact_bridge( + provider, match->resolved_bridge_target, match->abi_wrapper, + (void *)match->native_symbol); +} + +static uintptr_t kzt_rela_runtime_provider_add( + const kzt_wrapper_bridge_provider_match_t *match, + const kzt_wrapper_probe_bridge_request_t *request, void *opaque) +{ + library_t *provider; + uintptr_t target; + + (void)opaque; + if (!match || !request || !match->bridge_owner || !match->abi_wrapper || + !match->native_symbol || + !kzt_rela_runtime_match_lifetime_valid(match) || + match->resolved_bridge_target || match->resolved_bridge_exact || + request->native_symbol != match->native_symbol || + !request->symbol_name || !request->symbol_name[0]) { + return 0; + } + + provider = match->bridge_owner; + if (!provider->priv.w.bridge || !AddCheckBridge) { + return 0; + } + target = AddCheckBridge(provider->priv.w.bridge, match->abi_wrapper, + (void *)match->native_symbol, match->stack_bytes, + request->symbol_name); + if (!target || + kzt_rela_runtime_provider_exact_bridge( + provider, target, match->abi_wrapper, + (void *)match->native_symbol) != target) { + return 0; + } + return target; +} + +static int kzt_rela_runtime_wrapper_provider_prepare_mode( + box64context_t *context, library_t *resolved_provider, + uintptr_t resolved_target, const char *symbol_name, + kzt_symbol_version_evidence_t version_evidence, + const char *symbol_version, int discover_bridge, + kzt_wrapper_bridge_provider_t *provider) +{ + kzt_rela_runtime_provider_state_t state = { + .context = context, + .resolved_target = resolved_target, + .discover_bridge = discover_bridge, + }; + kzt_wrapper_bridge_provider_runtime_ops_t runtime_ops = { + .inspect_library = kzt_rela_runtime_provider_inspect, + .check_bridge = kzt_rela_runtime_provider_check, + .add_bridge = kzt_rela_runtime_provider_add, + .opaque = &state, + }; + void *libraries[] = { resolved_provider }; + int status; + + if (!provider) { + return -1; + } + status = kzt_wrapper_bridge_provider_prepare_with_version_evidence( + provider, libraries, 1, symbol_name, version_evidence, + symbol_version, &runtime_ops); + provider->runtime_ops.opaque = NULL; + return status; +} + +int kzt_rela_runtime_wrapper_provider_prepare( + box64context_t *context, library_t *resolved_provider, + uintptr_t resolved_target, const char *symbol_name, + const char *symbol_version, + kzt_wrapper_bridge_provider_t *provider) +{ + return kzt_rela_runtime_wrapper_provider_prepare_mode( + context, resolved_provider, resolved_target, symbol_name, + symbol_version && symbol_version[0] ? KZT_SYMBOL_VERSION_VERSIONED : + KZT_SYMBOL_VERSION_UNKNOWN, + symbol_version, 0, provider); +} + +int kzt_rela_runtime_wrapper_provider_prepare_with_version_evidence( + box64context_t *context, library_t *resolved_provider, + uintptr_t resolved_target, const char *symbol_name, + kzt_symbol_version_evidence_t version_evidence, + const char *symbol_version, + kzt_wrapper_bridge_provider_t *provider) +{ + return kzt_rela_runtime_wrapper_provider_prepare_mode( + context, resolved_provider, resolved_target, symbol_name, + version_evidence, symbol_version, 0, provider); +} + +int kzt_rela_runtime_wrapper_provider_discover( + box64context_t *context, library_t *resolved_provider, + const char *symbol_name, const char *symbol_version, + kzt_wrapper_bridge_provider_t *provider) +{ + return kzt_rela_runtime_wrapper_provider_prepare_mode( + context, resolved_provider, 0, symbol_name, + symbol_version && symbol_version[0] ? KZT_SYMBOL_VERSION_VERSIONED : + KZT_SYMBOL_VERSION_UNKNOWN, + symbol_version, 1, provider); +} + +int kzt_rela_runtime_wrapper_provider_discover_with_version_evidence( + box64context_t *context, library_t *resolved_provider, + const char *symbol_name, + kzt_symbol_version_evidence_t version_evidence, + const char *symbol_version, + kzt_wrapper_bridge_provider_t *provider) +{ + return kzt_rela_runtime_wrapper_provider_prepare_mode( + context, resolved_provider, 0, symbol_name, version_evidence, + symbol_version, 1, provider); +} diff --git a/target/i386/latx/context/kzt_runtime_candidate_shadow.c b/target/i386/latx/context/kzt_runtime_candidate_shadow.c new file mode 100644 index 00000000000..c294be625af --- /dev/null +++ b/target/i386/latx/context/kzt_runtime_candidate_shadow.c @@ -0,0 +1,388 @@ +#include "kzt_runtime_candidate_shadow.h" + +#include + +static void kzt_runtime_candidate_shadow_result_init( + kzt_runtime_candidate_shadow_result_t *result) +{ + memset(result, 0, sizeof(*result)); + result->status = KZT_RUNTIME_CANDIDATE_SHADOW_OK; + result->reason = KZT_RUNTIME_CANDIDATE_SHADOW_REASON_NONE; +} + +static int kzt_runtime_candidate_shadow_output_sizes_valid( + const kzt_runtime_candidate_shadow_input_t *input) +{ + const kzt_runtime_got_plt_candidate_request_t *collector; + + if (!input || !input->collector_request) { + return 0; + } + + collector = input->collector_request; + if ((!input->records && input->record_capacity > 0) || + input->record_capacity > + SIZE_MAX / sizeof(kzt_runtime_candidate_shadow_record_t)) { + return 0; + } + + if ((!collector->candidates && collector->candidate_capacity > 0) || + collector->candidate_capacity > + SIZE_MAX / sizeof(kzt_patch_candidate_t)) { + return 0; + } + + return 1; +} + +static void kzt_runtime_candidate_shadow_clear_outputs( + const kzt_runtime_candidate_shadow_input_t *input) +{ + const kzt_runtime_got_plt_candidate_request_t *collector; + + if (!input || !input->collector_request) { + return; + } + + collector = input->collector_request; + if (collector->candidates && collector->candidate_capacity > 0) { + memset(collector->candidates, 0, + collector->candidate_capacity * + sizeof(*collector->candidates)); + } + if (collector->string_storage && collector->string_storage_size > 0) { + memset(collector->string_storage, 0, + collector->string_storage_size); + } + if (input->records && input->record_capacity > 0) { + memset(input->records, 0, + input->record_capacity * sizeof(*input->records)); + } +} + +static int kzt_runtime_candidate_shadow_registry_generation( + kzt_guest_registry_t *registry, + uintptr_t link_map_addr, + unsigned long *generation) +{ + kzt_guest_registry_dump_t dump = { 0 }; + size_t matches = 0; + size_t i; + + if (!registry || link_map_addr == 0 || !generation) { + return -1; + } + + if (kzt_guest_registry_dump_snapshot(registry, &dump) != 0) { + return -1; + } + + for (i = 0; i < dump.count; ++i) { + if (dump.objects[i].link_map_addr != link_map_addr) { + continue; + } + if (dump.objects[i].state == KZT_GUEST_OBJECT_UNLOADING || + dump.objects[i].state == KZT_GUEST_OBJECT_DEAD) { + continue; + } + + *generation = dump.objects[i].generation; + ++matches; + } + + kzt_guest_registry_dump_free(&dump); + return matches == 1 && *generation != 0 ? 0 : -1; +} + +static int kzt_runtime_candidate_shadow_query_generation( + const kzt_runtime_candidate_shadow_input_t *input, + uintptr_t link_map_addr, + unsigned long *generation) +{ + *generation = 0; + if (input->query_generation) { + return input->query_generation( + link_map_addr, generation, + input->generation_query_opaque); + } + + return kzt_runtime_candidate_shadow_registry_generation( + input->registry, link_map_addr, generation); +} + +static int kzt_runtime_candidate_shadow_generation_valid( + const kzt_runtime_candidate_shadow_input_t *input, + const kzt_patch_candidate_t *candidate) +{ + unsigned long current_generation = 0; + + if (!candidate || !candidate->source.known || + candidate->source.link_map_addr == 0 || + candidate->source.generation == 0 || + candidate->dynamic_view_generation == 0) { + return 0; + } + + if (kzt_runtime_candidate_shadow_query_generation( + input, candidate->source.link_map_addr, + ¤t_generation) != 0 || + current_generation == 0) { + return 0; + } + + return candidate->source.generation == current_generation && + candidate->dynamic_view_generation == current_generation; +} + +static int kzt_runtime_candidate_shadow_batch_generation_valid( + const kzt_runtime_candidate_shadow_input_t *input, + size_t candidate_count) +{ + size_t i; + + for (i = 0; i < candidate_count; ++i) { + if (!kzt_runtime_candidate_shadow_generation_valid( + input, + &input->collector_request->candidates[i])) { + return 0; + } + } + + return 1; +} + +static int kzt_runtime_candidate_shadow_is_precise_stub( + const kzt_runtime_candidate_shadow_input_t *input, + const kzt_patch_candidate_t *candidate) +{ + if (!candidate || !candidate->slot_current_value_present || + !input->classify_stub) { + return 0; + } + + if (candidate->reloc_type != KZT_PATCH_RELOCATION_JUMP_SLOT && + candidate->reloc_type != KZT_PATCH_RELOCATION_GLOB_DAT) { + return 0; + } + + return input->classify_stub( + candidate, input->stub_classifier_opaque) == + KZT_RUNTIME_CANDIDATE_SHADOW_STUB_MATCH; +} + +static int kzt_runtime_candidate_shadow_decision_valid( + const kzt_patch_decision_t *decision) +{ + return decision && + decision->kind >= KZT_PATCH_DECISION_ERROR && + (size_t)decision->kind < + KZT_RUNTIME_CANDIDATE_SHADOW_DECISION_BUCKETS && + decision->reason >= KZT_PATCH_REASON_ERROR_INVALID_ARGUMENT && + (size_t)decision->reason < + KZT_RUNTIME_CANDIDATE_SHADOW_REASON_BUCKETS; +} + +static void kzt_runtime_candidate_shadow_enrich_owner( + const kzt_runtime_candidate_shadow_input_t *input, + kzt_patch_candidate_t *candidate, + kzt_runtime_candidate_shadow_record_t *record) +{ + uintptr_t expected_guest_target = 0; + + memset(&candidate->current_owner, 0, sizeof(candidate->current_owner)); + candidate->owner_match = KZT_PATCH_OWNER_UNKNOWN; + kzt_owner_resolver_init(&record->owner_resolution); + + if (candidate->lazy_binding_deferred || !input->registry || + !input->resolve_expected_guest_target) { + return; + } + + if (input->resolve_expected_guest_target( + candidate, &expected_guest_target, + input->expected_target_opaque) != 0 || + expected_guest_target == 0) { + return; + } + + if (kzt_owner_resolver_resolve_current( + input->registry, candidate->slot_current_value, + expected_guest_target, &record->owner_resolution) != 0) { + return; + } + + candidate->current_owner = + record->owner_resolution.current_owner; + candidate->owner_match = record->owner_resolution.owner_match; +} + +static int kzt_runtime_candidate_shadow_enrich_wrapper( + const kzt_runtime_candidate_shadow_input_t *input, + kzt_patch_candidate_t *candidate, + kzt_runtime_candidate_shadow_record_t *record) +{ + kzt_wrapper_probe_bridge_ops_t readonly_bridge_ops = { 0 }; + const kzt_wrapper_probe_bridge_ops_t *bridge_ops = NULL; + kzt_wrapper_probe_request_t probe_request = { + .symbol_name = candidate->symbol_name, + .symbol_version_evidence = candidate->version_evidence, + .symbol_version = candidate->version, + }; + + candidate->wrapper_match = KZT_PATCH_WRAPPER_NO_MANIFEST; + candidate->wrapper_name = NULL; + candidate->wrapper_version_evidence = KZT_SYMBOL_VERSION_UNKNOWN; + candidate->wrapper_symbol_version = NULL; + candidate->bridge_target = 0; + + if (input->bridge_ops) { + readonly_bridge_ops.check_bridge = + input->bridge_ops->check_bridge; + readonly_bridge_ops.add_bridge = NULL; + readonly_bridge_ops.opaque = input->bridge_ops->opaque; + bridge_ops = &readonly_bridge_ops; + } + + if (kzt_wrapper_probe_minimal_manifest( + input->wrapper_manifest, &probe_request, bridge_ops, + &record->wrapper_probe) != 0) { + return -1; + } + + kzt_wrapper_probe_apply_to_candidate(&record->wrapper_probe, + candidate); + return 0; +} + +static void kzt_runtime_candidate_shadow_fail_open( + const kzt_runtime_candidate_shadow_input_t *input, + kzt_runtime_candidate_shadow_result_t *result, + kzt_runtime_candidate_shadow_reason_t reason) +{ + kzt_runtime_candidate_shadow_clear_outputs(input); + result->status = KZT_RUNTIME_CANDIDATE_SHADOW_FAIL_OPEN; + result->reason = reason; + result->collector_result.candidate_count = 0; + result->candidate_count = 0; + result->record_count = 0; + result->eligible_count = 0; + result->observe_only_count = 0; + memset(result->decision_histogram, 0, + sizeof(result->decision_histogram)); + memset(result->reason_histogram, 0, + sizeof(result->reason_histogram)); +} + +int kzt_runtime_candidate_shadow_run( + const kzt_runtime_candidate_shadow_input_t *input, + kzt_runtime_candidate_shadow_result_t *result) +{ + const kzt_runtime_got_plt_candidate_request_t *collector; + size_t candidate_count; + size_t i; + int collect_status; + + if (!result) { + return -1; + } + + kzt_runtime_candidate_shadow_result_init(result); + if (!kzt_runtime_candidate_shadow_output_sizes_valid(input)) { + result->status = KZT_RUNTIME_CANDIDATE_SHADOW_ERROR; + result->reason = + KZT_RUNTIME_CANDIDATE_SHADOW_REASON_INVALID_ARGUMENT; + return -1; + } + + collector = input->collector_request; + kzt_runtime_candidate_shadow_clear_outputs(input); + collect_status = kzt_runtime_got_plt_candidates_collect( + collector, &result->collector_result); + if (collect_status != 0 || + result->collector_result.status == + KZT_RUNTIME_GOT_PLT_CANDIDATE_ERROR) { + kzt_runtime_candidate_shadow_clear_outputs(input); + result->status = KZT_RUNTIME_CANDIDATE_SHADOW_ERROR; + result->reason = + KZT_RUNTIME_CANDIDATE_SHADOW_REASON_COLLECTOR_ERROR; + return -1; + } + + if (result->collector_result.status == + KZT_RUNTIME_GOT_PLT_CANDIDATE_FAIL_OPEN) { + kzt_runtime_candidate_shadow_fail_open( + input, result, + KZT_RUNTIME_CANDIDATE_SHADOW_REASON_COLLECTOR_FAIL_OPEN); + return 0; + } + + candidate_count = result->collector_result.candidate_count; + if (candidate_count > input->record_capacity) { + kzt_runtime_candidate_shadow_fail_open( + input, result, + KZT_RUNTIME_CANDIDATE_SHADOW_REASON_RECORD_CAPACITY_EXCEEDED); + return 0; + } + + if (!kzt_runtime_candidate_shadow_batch_generation_valid( + input, candidate_count)) { + kzt_runtime_candidate_shadow_fail_open( + input, result, + KZT_RUNTIME_CANDIDATE_SHADOW_REASON_OBJECT_GENERATION_CHANGED); + return 0; + } + + for (i = 0; i < candidate_count; ++i) { + kzt_patch_candidate_t *candidate = &collector->candidates[i]; + kzt_runtime_candidate_shadow_record_t *record = + &input->records[i]; + + record->candidate_index = i; + candidate->lazy_binding_deferred = + kzt_runtime_candidate_shadow_is_precise_stub(input, + candidate); + kzt_runtime_candidate_shadow_enrich_owner(input, candidate, + record); + if (kzt_runtime_candidate_shadow_enrich_wrapper( + input, candidate, record) != 0 || + kzt_patch_planner_decide(candidate, + &record->decision) != 0) { + kzt_runtime_candidate_shadow_fail_open( + input, result, + KZT_RUNTIME_CANDIDATE_SHADOW_REASON_PLANNER_ERROR); + return 0; + } + + if (!kzt_runtime_candidate_shadow_decision_valid( + &record->decision)) { + kzt_runtime_candidate_shadow_fail_open( + input, result, + KZT_RUNTIME_CANDIDATE_SHADOW_REASON_PLANNER_ERROR); + return 0; + } + + record->audit_only = 1; + record->legacy_target_consumed = 0; + record->observe_only = + candidate->reloc_type == KZT_PATCH_RELOCATION_GLOB_DAT; + record->eligible = + record->decision.kind == KZT_PATCH_DECISION_APPROVED && + !record->observe_only; + result->decision_histogram[record->decision.kind]++; + result->reason_histogram[record->decision.reason]++; + result->eligible_count += record->eligible; + result->observe_only_count += record->observe_only; + } + + if (!kzt_runtime_candidate_shadow_batch_generation_valid( + input, candidate_count)) { + kzt_runtime_candidate_shadow_fail_open( + input, result, + KZT_RUNTIME_CANDIDATE_SHADOW_REASON_OBJECT_GENERATION_CHANGED); + return 0; + } + + result->candidate_count = candidate_count; + result->record_count = candidate_count; + return 0; +} diff --git a/target/i386/latx/context/kzt_runtime_got_plt_candidate.c b/target/i386/latx/context/kzt_runtime_got_plt_candidate.c new file mode 100644 index 00000000000..b329acb2e97 --- /dev/null +++ b/target/i386/latx/context/kzt_runtime_got_plt_candidate.c @@ -0,0 +1,995 @@ +#include "kzt_runtime_got_plt_candidate.h" + +#include + +#include "elf.h" + +#define KZT_RUNTIME_GOT_PLT_VERSION_SCAN_LIMIT 128 + +typedef struct kzt_runtime_got_plt_string_pool { + char *storage; + size_t size; + size_t used; +} kzt_runtime_got_plt_string_pool_t; + +static void kzt_runtime_got_plt_result_clear( + kzt_runtime_got_plt_candidate_result_t *result) +{ + memset(result, 0, sizeof(*result)); + result->status = KZT_RUNTIME_GOT_PLT_CANDIDATE_OK; + result->reason = KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_NONE; + result->table_kind = KZT_PATCH_TABLE_UNKNOWN; + result->version_evidence = KZT_SYMBOL_VERSION_UNKNOWN; +} + +static int kzt_runtime_got_plt_result_set( + kzt_runtime_got_plt_candidate_result_t *result, + kzt_runtime_got_plt_candidate_status_t status, + kzt_runtime_got_plt_candidate_reason_t reason, + kzt_patch_reason_t patch_reason, + kzt_patch_table_kind_t table_kind, + size_t entry_index, + uintptr_t entry_addr, + uintptr_t slot_addr, + uintptr_t read_error_addr) +{ + result->status = status; + result->reason = reason; + result->patch_reason_present = 1; + result->patch_reason = patch_reason; + result->candidate_count = 0; + result->table_kind = table_kind; + result->entry_index = entry_index; + result->entry_addr = entry_addr; + result->slot_addr = slot_addr; + result->read_error_addr = read_error_addr; + return 0; +} + +static int kzt_runtime_got_plt_fail_open( + kzt_runtime_got_plt_candidate_result_t *result, + kzt_runtime_got_plt_candidate_reason_t reason, + kzt_patch_reason_t patch_reason, + kzt_patch_table_kind_t table_kind, + size_t entry_index, + uintptr_t entry_addr, + uintptr_t slot_addr, + uintptr_t read_error_addr) +{ + return kzt_runtime_got_plt_result_set( + result, KZT_RUNTIME_GOT_PLT_CANDIDATE_FAIL_OPEN, reason, + patch_reason, table_kind, entry_index, entry_addr, slot_addr, + read_error_addr); +} + +static int kzt_runtime_got_plt_u64_to_size(uint64_t value, size_t *out) +{ + if (value > (uint64_t)SIZE_MAX) { + return -1; + } + + *out = (size_t)value; + return 0; +} + +static int kzt_runtime_got_plt_add_u64(uintptr_t base, + uint64_t offset, + uintptr_t *out) +{ + if (offset > (uint64_t)UINTPTR_MAX) { + return -1; + } + + if (base > UINTPTR_MAX - (uintptr_t)offset) { + return -1; + } + + *out = base + (uintptr_t)offset; + return 0; +} + +static int kzt_runtime_got_plt_entry_addr(uintptr_t table_addr, + size_t entry_size, + size_t index, + uintptr_t *entry_addr) +{ + uintptr_t offset; + + if (entry_size == 0 || index > UINTPTR_MAX / entry_size) { + return -1; + } + + offset = index * entry_size; + if (table_addr > UINTPTR_MAX - offset) { + return -1; + } + + *entry_addr = table_addr + offset; + return 0; +} + +static int kzt_runtime_got_plt_table_bounds_valid(uintptr_t table_addr, + size_t table_size) +{ + if (table_size == 0) { + return 0; + } + + if (table_addr > UINTPTR_MAX - (table_size - 1)) { + return -1; + } + + return 0; +} + +static int kzt_runtime_got_plt_has_rel_table( + const kzt_guest_dynamic_view_t *view) +{ + return view->rel.present || view->relsz.present || view->relent.present; +} + +static int kzt_runtime_got_plt_has_plt_table( + const kzt_guest_dynamic_view_t *view) +{ + return view->jmprel.present || view->pltrelsz.present || + view->pltrel.present; +} + +static int kzt_runtime_got_plt_has_rela_table( + const kzt_guest_dynamic_view_t *view) +{ + return view->rela.present || view->relasz.present || + view->relaent.present; +} + +static int kzt_runtime_got_plt_read_rela( + const kzt_guest_link_map_reader_ops_t *reader_ops, + uintptr_t entry_addr, + Elf64_Rela *rela) +{ + return reader_ops->read_memory(entry_addr, rela, sizeof(*rela), + reader_ops->opaque); +} + +static int kzt_runtime_got_plt_read_slot( + const kzt_guest_link_map_reader_ops_t *reader_ops, + uintptr_t slot_addr, + uintptr_t *slot_value) +{ + uint64_t raw_value = 0; + + if (reader_ops->read_memory(slot_addr, &raw_value, sizeof(raw_value), + reader_ops->opaque) != 0) { + return -1; + } + + if (raw_value > (uint64_t)UINTPTR_MAX) { + return -1; + } + + *slot_value = (uintptr_t)raw_value; + return 0; +} + +static int kzt_runtime_got_plt_read_sym( + const kzt_guest_link_map_reader_ops_t *reader_ops, + uintptr_t entry_addr, + Elf64_Sym *sym) +{ + return reader_ops->read_memory(entry_addr, sym, sizeof(*sym), + reader_ops->opaque); +} + +static int kzt_runtime_got_plt_read_half( + const kzt_guest_link_map_reader_ops_t *reader_ops, + uintptr_t entry_addr, + Elf64_Half *value) +{ + return reader_ops->read_memory(entry_addr, value, sizeof(*value), + reader_ops->opaque); +} + +static int kzt_runtime_got_plt_read_verneed( + const kzt_guest_link_map_reader_ops_t *reader_ops, + uintptr_t entry_addr, + Elf64_Verneed *verneed) +{ + return reader_ops->read_memory(entry_addr, verneed, sizeof(*verneed), + reader_ops->opaque); +} + +static int kzt_runtime_got_plt_read_vernaux( + const kzt_guest_link_map_reader_ops_t *reader_ops, + uintptr_t entry_addr, + Elf64_Vernaux *vernaux) +{ + return reader_ops->read_memory(entry_addr, vernaux, sizeof(*vernaux), + reader_ops->opaque); +} + +static int kzt_runtime_got_plt_read_verdef( + const kzt_guest_link_map_reader_ops_t *reader_ops, + uintptr_t entry_addr, + Elf64_Verdef *verdef) +{ + return reader_ops->read_memory(entry_addr, verdef, sizeof(*verdef), + reader_ops->opaque); +} + +static int kzt_runtime_got_plt_read_verdaux( + const kzt_guest_link_map_reader_ops_t *reader_ops, + uintptr_t entry_addr, + Elf64_Verdaux *verdaux) +{ + return reader_ops->read_memory(entry_addr, verdaux, sizeof(*verdaux), + reader_ops->opaque); +} + +static int kzt_runtime_got_plt_read_string( + const kzt_runtime_got_plt_candidate_request_t *request, + kzt_runtime_got_plt_string_pool_t *pool, + uint64_t string_offset, + const char **out) +{ + uintptr_t string_addr = 0; + uint64_t remaining; + char *terminator; + size_t readable; + size_t start; + + if (!request->view->strtab.present || !request->view->strsz.present || + string_offset >= request->view->strsz.value) { + return -1; + } + + if (kzt_runtime_got_plt_add_u64((uintptr_t)request->view->strtab.value, + string_offset, &string_addr) != 0) { + return -1; + } + + remaining = request->view->strsz.value - string_offset; + if (remaining > (uint64_t)SIZE_MAX) { + return -1; + } + + if (!pool->storage || pool->used >= pool->size) { + return -1; + } + start = pool->used; + readable = pool->size - start; + if (remaining < readable) { + readable = (size_t)remaining; + } + if (request->reader_ops->read_memory( + string_addr, &pool->storage[start], readable, + request->reader_ops->opaque) != 0) { + return -1; + } + terminator = memchr(&pool->storage[start], '\0', readable); + if (!terminator || terminator == &pool->storage[start]) { + return -1; + } + pool->used = (size_t)(terminator - pool->storage) + 1; + *out = &pool->storage[start]; + return 0; +} + +static int kzt_runtime_got_plt_read_symbol_name( + const kzt_runtime_got_plt_candidate_request_t *request, + kzt_runtime_got_plt_candidate_result_t *result, + kzt_runtime_got_plt_string_pool_t *pool, + unsigned long symbol_index, + kzt_patch_table_kind_t table_kind, + size_t entry_index, + uintptr_t entry_addr, + const char **symbol_name) +{ + uintptr_t sym_addr = 0; + size_t syment = 0; + Elf64_Sym sym; + + if (!request->view->symtab.present || !request->view->syment.present || + !request->view->strtab.present || !request->view->strsz.present) { + kzt_runtime_got_plt_fail_open( + result, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_MISSING_DYNAMIC_FIELD, + KZT_PATCH_REASON_INPUT_MALFORMED_SYMBOL_NAME, table_kind, + entry_index, entry_addr, 0, 0); + return -1; + } + + if (kzt_runtime_got_plt_u64_to_size(request->view->syment.value, + &syment) != 0 || + syment != sizeof(Elf64_Sym) || + kzt_runtime_got_plt_entry_addr((uintptr_t)request->view->symtab.value, + syment, symbol_index, + &sym_addr) != 0) { + kzt_runtime_got_plt_fail_open( + result, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_MALFORMED_SYMBOL_NAME, + KZT_PATCH_REASON_INPUT_MALFORMED_SYMBOL_NAME, table_kind, + entry_index, entry_addr, 0, 0); + return -1; + } + + if (kzt_runtime_got_plt_read_sym(request->reader_ops, sym_addr, + &sym) != 0) { + kzt_runtime_got_plt_fail_open( + result, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_SYMBOL_READ_FAILED, + KZT_PATCH_REASON_INPUT_MALFORMED_SYMBOL_NAME, table_kind, + entry_index, entry_addr, 0, sym_addr); + return -1; + } + + if (kzt_runtime_got_plt_read_string(request, pool, sym.st_name, + symbol_name) != 0) { + kzt_runtime_got_plt_fail_open( + result, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_MALFORMED_SYMBOL_NAME, + KZT_PATCH_REASON_INPUT_MALFORMED_SYMBOL_NAME, table_kind, + entry_index, entry_addr, 0, 0); + return -1; + } + + return 0; +} + +static int kzt_runtime_got_plt_read_version_string_from_verneed( + const kzt_runtime_got_plt_candidate_request_t *request, + kzt_runtime_got_plt_string_pool_t *pool, + unsigned int version, + const char **version_name, + uintptr_t *read_error_addr) +{ + uintptr_t verneed_addr; + size_t verneed_count; + size_t i; + + if (!request->view->verneed.present) { + return 1; + } + + verneed_addr = (uintptr_t)request->view->verneed.value; + if (request->view->verneednum.present && + request->view->verneednum.value < KZT_RUNTIME_GOT_PLT_VERSION_SCAN_LIMIT) { + verneed_count = (size_t)request->view->verneednum.value; + } else { + verneed_count = KZT_RUNTIME_GOT_PLT_VERSION_SCAN_LIMIT; + } + + for (i = 0; i < verneed_count; ++i) { + Elf64_Verneed verneed; + uintptr_t aux_addr; + size_t j; + + if (kzt_runtime_got_plt_read_verneed(request->reader_ops, + verneed_addr, &verneed) != 0) { + *read_error_addr = verneed_addr; + return -1; + } + + if (kzt_runtime_got_plt_add_u64(verneed_addr, verneed.vn_aux, + &aux_addr) != 0) { + return -1; + } + + for (j = 0; j < verneed.vn_cnt && + j < KZT_RUNTIME_GOT_PLT_VERSION_SCAN_LIMIT; ++j) { + Elf64_Vernaux aux; + + if (kzt_runtime_got_plt_read_vernaux(request->reader_ops, + aux_addr, &aux) != 0) { + *read_error_addr = aux_addr; + return -1; + } + + if ((aux.vna_other & 0x7fff) == version) { + return kzt_runtime_got_plt_read_string( + request, pool, aux.vna_name, version_name) == 0 + ? 0 + : -1; + } + + if (aux.vna_next == 0) { + break; + } + if (kzt_runtime_got_plt_add_u64(aux_addr, aux.vna_next, + &aux_addr) != 0) { + return -1; + } + } + + if (verneed.vn_next == 0) { + break; + } + if (kzt_runtime_got_plt_add_u64(verneed_addr, verneed.vn_next, + &verneed_addr) != 0) { + return -1; + } + } + + return 1; +} + +static int kzt_runtime_got_plt_read_version_string_from_verdef( + const kzt_runtime_got_plt_candidate_request_t *request, + kzt_runtime_got_plt_string_pool_t *pool, + unsigned int version, + const char **version_name, + uintptr_t *read_error_addr) +{ + uintptr_t verdef_addr; + size_t verdef_count; + size_t i; + + if (!request->view->verdef.present) { + return 1; + } + + verdef_addr = (uintptr_t)request->view->verdef.value; + if (request->view->verdefnum.present && + request->view->verdefnum.value < KZT_RUNTIME_GOT_PLT_VERSION_SCAN_LIMIT) { + verdef_count = (size_t)request->view->verdefnum.value; + } else { + verdef_count = KZT_RUNTIME_GOT_PLT_VERSION_SCAN_LIMIT; + } + + for (i = 0; i < verdef_count; ++i) { + Elf64_Verdef verdef; + + if (kzt_runtime_got_plt_read_verdef(request->reader_ops, + verdef_addr, &verdef) != 0) { + *read_error_addr = verdef_addr; + return -1; + } + + if (verdef.vd_ndx == version) { + Elf64_Verdaux aux; + uintptr_t aux_addr; + + if (verdef.vd_cnt < 1 || + kzt_runtime_got_plt_add_u64(verdef_addr, verdef.vd_aux, + &aux_addr) != 0) { + return -1; + } + + if (kzt_runtime_got_plt_read_verdaux(request->reader_ops, + aux_addr, &aux) != 0) { + *read_error_addr = aux_addr; + return -1; + } + + return kzt_runtime_got_plt_read_string( + request, pool, aux.vda_name, version_name) == 0 + ? 0 + : -1; + } + + if (verdef.vd_next == 0) { + break; + } + if (kzt_runtime_got_plt_add_u64(verdef_addr, verdef.vd_next, + &verdef_addr) != 0) { + return -1; + } + } + + return 1; +} + +static int kzt_runtime_got_plt_read_symbol_version( + const kzt_runtime_got_plt_candidate_request_t *request, + kzt_runtime_got_plt_candidate_result_t *result, + kzt_runtime_got_plt_string_pool_t *pool, + unsigned long symbol_index, + kzt_patch_table_kind_t table_kind, + size_t entry_index, + uintptr_t entry_addr, + kzt_symbol_version_evidence_t *version_evidence, + const char **version_name) +{ + uintptr_t versym_addr = 0; + uintptr_t read_error_addr = 0; + Elf64_Half raw_version = 0; + unsigned int version; + int lookup_status; + + *version_evidence = KZT_SYMBOL_VERSION_UNKNOWN; + *version_name = NULL; + if (!request->view->versym.present) { + *version_evidence = KZT_SYMBOL_VERSION_CONFIRMED_UNVERSIONED; + result->version_evidence = *version_evidence; + return 0; + } + + if (kzt_runtime_got_plt_entry_addr((uintptr_t)request->view->versym.value, + sizeof(raw_version), symbol_index, + &versym_addr) != 0) { + *version_evidence = KZT_SYMBOL_VERSION_ERROR; + result->version_evidence = *version_evidence; + kzt_runtime_got_plt_fail_open( + result, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_MALFORMED_SYMBOL_VERSION, + KZT_PATCH_REASON_INPUT_MALFORMED_SYMBOL_VERSION, table_kind, + entry_index, entry_addr, 0, 0); + return -1; + } + + if (kzt_runtime_got_plt_read_half(request->reader_ops, versym_addr, + &raw_version) != 0) { + *version_evidence = KZT_SYMBOL_VERSION_ERROR; + result->version_evidence = *version_evidence; + kzt_runtime_got_plt_fail_open( + result, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_VERSION_READ_FAILED, + KZT_PATCH_REASON_INPUT_MALFORMED_SYMBOL_VERSION, table_kind, + entry_index, entry_addr, 0, versym_addr); + return -1; + } + + version = raw_version & 0x7fff; + if (version < 2) { + *version_evidence = KZT_SYMBOL_VERSION_CONFIRMED_UNVERSIONED; + result->version_evidence = *version_evidence; + return 0; + } + + lookup_status = kzt_runtime_got_plt_read_version_string_from_verneed( + request, pool, version, version_name, &read_error_addr); + if (lookup_status == 1) { + lookup_status = kzt_runtime_got_plt_read_version_string_from_verdef( + request, pool, version, version_name, &read_error_addr); + } + + if (lookup_status == 0) { + *version_evidence = KZT_SYMBOL_VERSION_VERSIONED; + result->version_evidence = *version_evidence; + return 0; + } + + *version_evidence = KZT_SYMBOL_VERSION_ERROR; + result->version_evidence = *version_evidence; + kzt_runtime_got_plt_fail_open( + result, + lookup_status < 0 + ? KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_VERSION_READ_FAILED + : KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_MALFORMED_SYMBOL_VERSION, + KZT_PATCH_REASON_INPUT_MALFORMED_SYMBOL_VERSION, table_kind, + entry_index, entry_addr, 0, read_error_addr); + return -1; +} + +static kzt_patch_relocation_type_t kzt_runtime_got_plt_reloc_type( + unsigned int elf_reloc_type) +{ + switch (elf_reloc_type) { + case R_X86_64_JUMP_SLOT: + return KZT_PATCH_RELOCATION_JUMP_SLOT; + case R_X86_64_GLOB_DAT: + return KZT_PATCH_RELOCATION_GLOB_DAT; + case R_X86_64_RELATIVE: + return KZT_PATCH_RELOCATION_RELATIVE; + case R_X86_64_COPY: + return KZT_PATCH_RELOCATION_COPY; + case R_X86_64_IRELATIVE: + return KZT_PATCH_RELOCATION_IRELATIVE; + } + + return KZT_PATCH_RELOCATION_OTHER; +} + +static int kzt_runtime_got_plt_target_relocation( + kzt_patch_table_kind_t table_kind, + unsigned int elf_reloc_type) +{ + if (table_kind == KZT_PATCH_TABLE_PLT_RELA) { + return elf_reloc_type == R_X86_64_JUMP_SLOT; + } + + if (table_kind == KZT_PATCH_TABLE_RELA) { + return elf_reloc_type == R_X86_64_GLOB_DAT; + } + + return 0; +} + +static int kzt_runtime_got_plt_append_candidate( + const kzt_runtime_got_plt_candidate_request_t *request, + kzt_runtime_got_plt_candidate_result_t *result, + kzt_runtime_got_plt_string_pool_t *pool, + size_t *candidate_count, + kzt_patch_table_kind_t table_kind, + size_t entry_index, + uintptr_t entry_addr, + const Elf64_Rela *rela) +{ + kzt_patch_candidate_t candidate; + uintptr_t slot_addr = 0; + uintptr_t slot_value = 0; + unsigned int elf_reloc_type = ELF64_R_TYPE(rela->r_info); + unsigned long symbol_index = ELF64_R_SYM(rela->r_info); + const char *symbol_name = NULL; + const char *version_name = NULL; + kzt_symbol_version_evidence_t version_evidence = + KZT_SYMBOL_VERSION_UNKNOWN; + + if (kzt_runtime_got_plt_add_u64(request->view->load_bias, + rela->r_offset, &slot_addr) != 0) { + kzt_runtime_got_plt_fail_open( + result, KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_SLOT_OVERFLOW, + KZT_PATCH_REASON_INPUT_MALFORMED_SLOT, table_kind, entry_index, + entry_addr, 0, 0); + return -1; + } + + if (*candidate_count >= request->candidate_capacity) { + kzt_runtime_got_plt_fail_open( + result, KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_CAPACITY_EXCEEDED, + KZT_PATCH_REASON_INPUT_MALFORMED_TABLE, table_kind, entry_index, + entry_addr, slot_addr, 0); + return -1; + } + + if (kzt_runtime_got_plt_read_slot(request->reader_ops, slot_addr, + &slot_value) != 0) { + kzt_runtime_got_plt_fail_open( + result, KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_SLOT_READ_FAILED, + KZT_PATCH_REASON_INPUT_UNAVAILABLE_CURRENT_GOT, table_kind, + entry_index, entry_addr, slot_addr, slot_addr); + return -1; + } + + if (kzt_runtime_got_plt_read_symbol_name( + request, result, pool, symbol_index, table_kind, entry_index, + entry_addr, &symbol_name) != 0) { + return -1; + } + + if (kzt_runtime_got_plt_read_symbol_version( + request, result, pool, symbol_index, table_kind, entry_index, + entry_addr, &version_evidence, &version_name) != 0) { + return -1; + } + + memset(&candidate, 0, sizeof(candidate)); + if (request->source) { + candidate.source = *request->source; + } + candidate.dynamic_addr = request->view->dynamic_addr; + candidate.load_bias = request->view->load_bias; + candidate.dynamic_view_generation = request->dynamic_view_generation; + candidate.dynamic_view_available = 1; + candidate.table_kind = table_kind; + candidate.entry_index = entry_index; + candidate.entry_addr = entry_addr; + candidate.reloc_type = kzt_runtime_got_plt_reloc_type(elf_reloc_type); + candidate.slot_addr = slot_addr; + candidate.slot_current_value_present = 1; + candidate.slot_current_value = slot_value; + candidate.lazy_binding_deferred = 0; + candidate.symbol_index = symbol_index; + candidate.symbol_name = symbol_name; + candidate.version_evidence = version_evidence; + candidate.version = version_name; + candidate.owner_match = KZT_PATCH_OWNER_UNKNOWN; + + request->candidates[*candidate_count] = candidate; + ++*candidate_count; + return 0; +} + +static int kzt_runtime_got_plt_enumerate_rela_table( + const kzt_runtime_got_plt_candidate_request_t *request, + kzt_runtime_got_plt_candidate_result_t *result, + kzt_runtime_got_plt_string_pool_t *pool, + kzt_patch_table_kind_t table_kind, + uintptr_t table_addr, + size_t table_size, + size_t entry_size, + size_t *candidate_count) +{ + size_t entry_count; + size_t i; + + if (request->only_entry && request->only_table_kind != table_kind) { + return 0; + } + + if (entry_size != sizeof(Elf64_Rela) || + table_size % entry_size != 0) { + kzt_runtime_got_plt_fail_open( + result, KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_MALFORMED_TABLE, + KZT_PATCH_REASON_INPUT_MALFORMED_TABLE, table_kind, 0, + table_addr, 0, 0); + return -1; + } + + if (kzt_runtime_got_plt_table_bounds_valid(table_addr, table_size) != 0) { + kzt_runtime_got_plt_fail_open( + result, KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_TABLE_OVERFLOW, + KZT_PATCH_REASON_INPUT_MALFORMED_TABLE, table_kind, 0, + table_addr, 0, 0); + return -1; + } + + entry_count = table_size / entry_size; + if (request->only_entry) { + if (request->only_entry_index >= entry_count) { + kzt_runtime_got_plt_fail_open( + result, KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_MALFORMED_TABLE, + KZT_PATCH_REASON_INPUT_MALFORMED_TABLE, table_kind, + request->only_entry_index, table_addr, 0, 0); + return -1; + } + i = request->only_entry_index; + } else { + i = 0; + } + for (; i < entry_count; ++i) { + Elf64_Rela rela; + uintptr_t entry_addr = 0; + unsigned int elf_reloc_type; + + if (kzt_runtime_got_plt_entry_addr(table_addr, entry_size, i, + &entry_addr) != 0) { + kzt_runtime_got_plt_fail_open( + result, KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_TABLE_OVERFLOW, + KZT_PATCH_REASON_INPUT_MALFORMED_TABLE, table_kind, i, + table_addr, 0, 0); + return -1; + } + + if (kzt_runtime_got_plt_read_rela(request->reader_ops, entry_addr, + &rela) != 0) { + kzt_runtime_got_plt_fail_open( + result, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_RELOCATION_READ_FAILED, + KZT_PATCH_REASON_INPUT_MALFORMED_TABLE, table_kind, i, + entry_addr, 0, entry_addr); + return -1; + } + + elf_reloc_type = ELF64_R_TYPE(rela.r_info); + if (!kzt_runtime_got_plt_target_relocation(table_kind, + elf_reloc_type)) { + if (request->only_entry) { + return 0; + } + continue; + } + + if (kzt_runtime_got_plt_append_candidate( + request, result, pool, candidate_count, table_kind, i, + entry_addr, &rela) != 0) { + return -1; + } + if (request->only_entry) { + return 0; + } + } + + return 0; +} + +static int kzt_runtime_got_plt_enumerate_plt_rela( + const kzt_runtime_got_plt_candidate_request_t *request, + kzt_runtime_got_plt_candidate_result_t *result, + kzt_runtime_got_plt_string_pool_t *pool, + size_t *candidate_count) +{ + const kzt_guest_dynamic_view_t *view = request->view; + size_t table_size = 0; + + if (request->only_entry && + request->only_table_kind != KZT_PATCH_TABLE_PLT_RELA) { + return 0; + } + + if (!kzt_runtime_got_plt_has_plt_table(view)) { + return 0; + } + + if (!view->jmprel.present || !view->pltrelsz.present || + !view->pltrel.present) { + kzt_runtime_got_plt_fail_open( + result, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_MISSING_DYNAMIC_FIELD, + KZT_PATCH_REASON_INPUT_MALFORMED_TABLE, + KZT_PATCH_TABLE_PLT_RELA, 0, 0, 0, 0); + return -1; + } + + if (view->pltrel.value == DT_REL) { + kzt_runtime_got_plt_fail_open( + result, KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_DT_REL_UNSUPPORTED, + KZT_PATCH_REASON_INPUT_UNSUPPORTED_RELOCATION, + KZT_PATCH_TABLE_PLT_REL, 0, view->jmprel.value, 0, 0); + return -1; + } + + if (view->pltrel.value != DT_RELA || + kzt_runtime_got_plt_u64_to_size(view->pltrelsz.value, + &table_size) != 0) { + kzt_runtime_got_plt_fail_open( + result, KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_MALFORMED_TABLE, + KZT_PATCH_REASON_INPUT_MALFORMED_TABLE, + KZT_PATCH_TABLE_PLT_RELA, 0, view->jmprel.value, 0, 0); + return -1; + } + + return kzt_runtime_got_plt_enumerate_rela_table( + request, result, pool, KZT_PATCH_TABLE_PLT_RELA, + (uintptr_t)view->jmprel.value, table_size, sizeof(Elf64_Rela), + candidate_count); +} + +static int kzt_runtime_got_plt_enumerate_rela( + const kzt_runtime_got_plt_candidate_request_t *request, + kzt_runtime_got_plt_candidate_result_t *result, + kzt_runtime_got_plt_string_pool_t *pool, + size_t *candidate_count) +{ + const kzt_guest_dynamic_view_t *view = request->view; + size_t table_size = 0; + size_t entry_size = 0; + + if (request->only_entry && + request->only_table_kind != KZT_PATCH_TABLE_RELA) { + return 0; + } + + if (!kzt_runtime_got_plt_has_rela_table(view)) { + return 0; + } + + if (!view->rela.present || !view->relasz.present || + !view->relaent.present) { + kzt_runtime_got_plt_fail_open( + result, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_MISSING_DYNAMIC_FIELD, + KZT_PATCH_REASON_INPUT_MALFORMED_TABLE, + KZT_PATCH_TABLE_RELA, 0, 0, 0, 0); + return -1; + } + + if (kzt_runtime_got_plt_u64_to_size(view->relasz.value, + &table_size) != 0 || + kzt_runtime_got_plt_u64_to_size(view->relaent.value, + &entry_size) != 0 || + entry_size == 0) { + kzt_runtime_got_plt_fail_open( + result, KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_MALFORMED_TABLE, + KZT_PATCH_REASON_INPUT_MALFORMED_TABLE, KZT_PATCH_TABLE_RELA, + 0, view->rela.value, 0, 0); + return -1; + } + + return kzt_runtime_got_plt_enumerate_rela_table( + request, result, pool, KZT_PATCH_TABLE_RELA, (uintptr_t)view->rela.value, + table_size, entry_size, candidate_count); +} + +int kzt_runtime_got_plt_candidates_collect( + const kzt_runtime_got_plt_candidate_request_t *request, + kzt_runtime_got_plt_candidate_result_t *result) +{ + size_t candidate_count = 0; + kzt_runtime_got_plt_string_pool_t pool = { 0 }; + + if (!result) { + return -1; + } + + kzt_runtime_got_plt_result_clear(result); + + if (!request || !request->view || !request->reader_ops || + !request->reader_ops->read_memory || + (!request->candidates && request->candidate_capacity > 0) || + ((!request->string_storage || request->string_storage_size == 0) && + request->candidate_capacity > 0)) { + kzt_runtime_got_plt_result_set( + result, KZT_RUNTIME_GOT_PLT_CANDIDATE_ERROR, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_INVALID_ARGUMENT, + KZT_PATCH_REASON_ERROR_INVALID_ARGUMENT, + KZT_PATCH_TABLE_UNKNOWN, 0, 0, 0, 0); + return -1; + } + + pool.storage = request->string_storage; + pool.size = request->string_storage_size; + + if (request->view->status != KZT_GUEST_DYNAMIC_COMPLETE) { + return kzt_runtime_got_plt_fail_open( + result, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_DYNAMIC_VIEW_UNAVAILABLE, + KZT_PATCH_REASON_INPUT_UNAVAILABLE_DYNAMIC_VIEW, + KZT_PATCH_TABLE_UNKNOWN, 0, 0, 0, 0); + } + + if ((!request->only_entry || + request->only_table_kind == KZT_PATCH_TABLE_PLT_RELA) && + request->view->pltrel.present && + request->view->pltrel.value == DT_REL) { + return kzt_runtime_got_plt_fail_open( + result, KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_DT_REL_UNSUPPORTED, + KZT_PATCH_REASON_INPUT_UNSUPPORTED_RELOCATION, + KZT_PATCH_TABLE_PLT_REL, 0, request->view->jmprel.value, 0, 0); + } + + if ((!request->only_entry || + request->only_table_kind == KZT_PATCH_TABLE_REL) && + kzt_runtime_got_plt_has_rel_table(request->view)) { + return kzt_runtime_got_plt_fail_open( + result, KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_DT_REL_UNSUPPORTED, + KZT_PATCH_REASON_INPUT_UNSUPPORTED_RELOCATION, + KZT_PATCH_TABLE_REL, 0, request->view->rel.value, 0, 0); + } + + if (kzt_runtime_got_plt_enumerate_plt_rela( + request, result, &pool, &candidate_count) != 0) { + return 0; + } + + if (kzt_runtime_got_plt_enumerate_rela( + request, result, &pool, &candidate_count) != 0) { + return 0; + } + + result->candidate_count = candidate_count; + return 0; +} + +const char *kzt_runtime_got_plt_candidate_status_name( + kzt_runtime_got_plt_candidate_status_t status) +{ + switch (status) { + case KZT_RUNTIME_GOT_PLT_CANDIDATE_OK: + return "OK"; + case KZT_RUNTIME_GOT_PLT_CANDIDATE_FAIL_OPEN: + return "FAIL_OPEN"; + case KZT_RUNTIME_GOT_PLT_CANDIDATE_ERROR: + return "ERROR"; + } + + return "UNKNOWN"; +} + +const char *kzt_runtime_got_plt_candidate_reason_name( + kzt_runtime_got_plt_candidate_reason_t reason) +{ + switch (reason) { + case KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_NONE: + return "NONE"; + case KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_INVALID_ARGUMENT: + return "INVALID_ARGUMENT"; + case KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_DYNAMIC_VIEW_UNAVAILABLE: + return "DYNAMIC_VIEW_UNAVAILABLE"; + case KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_DT_REL_UNSUPPORTED: + return "DT_REL_UNSUPPORTED"; + case KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_MISSING_DYNAMIC_FIELD: + return "MISSING_DYNAMIC_FIELD"; + case KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_MALFORMED_TABLE: + return "MALFORMED_TABLE"; + case KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_TABLE_OVERFLOW: + return "TABLE_OVERFLOW"; + case KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_RELOCATION_READ_FAILED: + return "RELOCATION_READ_FAILED"; + case KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_SLOT_OVERFLOW: + return "SLOT_OVERFLOW"; + case KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_SLOT_READ_FAILED: + return "SLOT_READ_FAILED"; + case KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_SYMBOL_READ_FAILED: + return "SYMBOL_READ_FAILED"; + case KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_MALFORMED_SYMBOL_NAME: + return "MALFORMED_SYMBOL_NAME"; + case KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_VERSION_READ_FAILED: + return "VERSION_READ_FAILED"; + case KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_MALFORMED_SYMBOL_VERSION: + return "MALFORMED_SYMBOL_VERSION"; + case KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_CAPACITY_EXCEEDED: + return "CAPACITY_EXCEEDED"; + } + + return "UNKNOWN"; +} diff --git a/target/i386/latx/context/kzt_wrapper_bridge_provider.c b/target/i386/latx/context/kzt_wrapper_bridge_provider.c new file mode 100644 index 00000000000..e8b5ce84889 --- /dev/null +++ b/target/i386/latx/context/kzt_wrapper_bridge_provider.c @@ -0,0 +1,183 @@ +#include "kzt_wrapper_bridge_provider.h" + +#include + +static int kzt_wrapper_bridge_provider_string_empty(const char *value) +{ + return !value || !value[0]; +} + +static int kzt_wrapper_bridge_provider_string_equal(const char *left, + const char *right) +{ + return !kzt_wrapper_bridge_provider_string_empty(left) && + !kzt_wrapper_bridge_provider_string_empty(right) && + strcmp(left, right) == 0; +} + +static uintptr_t kzt_wrapper_bridge_provider_check_bridge( + uintptr_t native_symbol, void *opaque) +{ + kzt_wrapper_bridge_provider_t *provider = opaque; + + if (!provider || !provider->runtime_ops.check_bridge || + native_symbol != provider->match.native_symbol) { + return 0; + } + + return provider->runtime_ops.check_bridge( + &provider->match, provider->runtime_ops.opaque); +} + +static uintptr_t kzt_wrapper_bridge_provider_add_bridge( + const kzt_wrapper_probe_bridge_request_t *request, void *opaque) +{ + kzt_wrapper_bridge_provider_t *provider = opaque; + uintptr_t created; + uintptr_t verified; + + if (!provider || !request || !provider->runtime_ops.add_bridge || + !provider->runtime_ops.check_bridge || + request->native_symbol != provider->match.native_symbol || + !kzt_wrapper_bridge_provider_string_equal( + request->symbol_name, provider->entry.symbol_name) || + !kzt_symbol_version_evidence_matches( + request->symbol_version_evidence, request->symbol_version, + provider->entry.symbol_version_evidence, + provider->entry.symbol_version) || + !kzt_wrapper_bridge_provider_string_equal( + request->wrapper_name, provider->entry.wrapper_name) || + !kzt_symbol_version_evidence_matches( + request->wrapper_version_evidence, + request->wrapper_symbol_version, + provider->entry.wrapper_version_evidence, + provider->entry.wrapper_symbol_version)) { + return 0; + } + + created = provider->runtime_ops.add_bridge( + &provider->match, request, provider->runtime_ops.opaque); + if (!created) { + return 0; + } + verified = provider->runtime_ops.check_bridge( + &provider->match, provider->runtime_ops.opaque); + return verified == created ? created : 0; +} + +int kzt_wrapper_bridge_provider_prepare_with_version_evidence( + kzt_wrapper_bridge_provider_t *provider, void *const *libraries, + size_t library_count, const char *symbol_name, + kzt_symbol_version_evidence_t version_evidence, + const char *symbol_version, + const kzt_wrapper_bridge_provider_runtime_ops_t *runtime_ops) +{ + kzt_wrapper_bridge_provider_match_t selected; + size_t selected_count = 0; + size_t i; + + if (!provider) { + return -1; + } + memset(provider, 0, sizeof(*provider)); + memset(&selected, 0, sizeof(selected)); + + if (!libraries || !runtime_ops || !runtime_ops->inspect_library || + !runtime_ops->check_bridge || + kzt_wrapper_bridge_provider_string_empty(symbol_name) || + !kzt_symbol_version_evidence_valid(version_evidence, + symbol_version)) { + return 0; + } + + for (i = 0; i < library_count; ++i) { + kzt_wrapper_bridge_provider_match_t candidate; + size_t j; + int duplicate = 0; + int status; + + if (!libraries[i]) { + continue; + } + for (j = 0; j < i; ++j) { + if (libraries[j] == libraries[i]) { + duplicate = 1; + break; + } + } + if (duplicate) { + continue; + } + + memset(&candidate, 0, sizeof(candidate)); + status = runtime_ops->inspect_library( + libraries[i], symbol_name, symbol_version, &candidate, + runtime_ops->opaque); + if (status < 0) { + return 0; + } + if (status == 0) { + continue; + } + if (!candidate.abi_wrapper || !candidate.native_symbol || + !candidate.context_owner || !candidate.wrapper_provider || + !candidate.native_lookup_handle || !candidate.native_owner || + !candidate.bridge_owner || !candidate.bridge_storage || + candidate.stack_bytes < 0 || !candidate.native_name[0] || + (!!candidate.resolved_bridge_target != + !!candidate.resolved_bridge_exact) || + (!candidate.resolved_bridge_target && + !runtime_ops->add_bridge) || + !candidate.wrapper_provider_lifetime_bound || + !candidate.native_owner_lifetime_bound || + !candidate.bridge_owner_lifetime_bound || + kzt_wrapper_bridge_provider_string_empty(candidate.wrapper_name)) { + return 0; + } + + selected = candidate; + if (++selected_count != 1) { + return 0; + } + } + + if (selected_count != 1) { + return 0; + } + + provider->match = selected; + provider->runtime_ops = *runtime_ops; + provider->entry.symbol_name = symbol_name; + provider->entry.symbol_version_evidence = version_evidence; + provider->entry.symbol_version = symbol_version; + provider->entry.wrapper_name = selected.wrapper_name; + provider->entry.wrapper_version_evidence = version_evidence; + provider->entry.wrapper_symbol_version = symbol_version; + provider->entry.native_symbol = selected.native_symbol; + provider->manifest.available = 1; + provider->manifest.manifest_name = selected.wrapper_name; + provider->manifest.entries = &provider->entry; + provider->manifest.entry_count = 1; + provider->bridge_ops.check_bridge = + kzt_wrapper_bridge_provider_check_bridge; + if (runtime_ops->add_bridge && !selected.resolved_bridge_exact) { + provider->bridge_ops.add_bridge = + kzt_wrapper_bridge_provider_add_bridge; + } + provider->bridge_ops.opaque = provider; + return 1; +} + +int kzt_wrapper_bridge_provider_prepare( + kzt_wrapper_bridge_provider_t *provider, void *const *libraries, + size_t library_count, const char *symbol_name, + const char *symbol_version, + const kzt_wrapper_bridge_provider_runtime_ops_t *runtime_ops) +{ + return kzt_wrapper_bridge_provider_prepare_with_version_evidence( + provider, libraries, library_count, symbol_name, + kzt_wrapper_bridge_provider_string_empty(symbol_version) + ? KZT_SYMBOL_VERSION_UNKNOWN + : KZT_SYMBOL_VERSION_VERSIONED, + symbol_version, runtime_ops); +} diff --git a/target/i386/latx/context/kzt_wrapper_probe.c b/target/i386/latx/context/kzt_wrapper_probe.c new file mode 100644 index 00000000000..010310c7dda --- /dev/null +++ b/target/i386/latx/context/kzt_wrapper_probe.c @@ -0,0 +1,240 @@ +#include "kzt_wrapper_probe.h" + +#include + +static int kzt_wrapper_probe_string_empty(const char *value) +{ + return !value || !value[0]; +} + +static int kzt_wrapper_probe_string_equal(const char *left, + const char *right) +{ + if (kzt_wrapper_probe_string_empty(left) || + kzt_wrapper_probe_string_empty(right)) { + return 0; + } + + return strcmp(left, right) == 0; +} + +static int kzt_wrapper_probe_entry_matches_symbol( + const kzt_wrapper_probe_entry_t *entry, + const kzt_wrapper_probe_request_t *request) +{ + return entry && request && + kzt_wrapper_probe_string_equal(entry->symbol_name, + request->symbol_name); +} + +static int kzt_wrapper_probe_entry_has_valid_version_evidence( + const kzt_wrapper_probe_entry_t *entry) +{ + return entry && kzt_symbol_version_evidence_valid( + entry->symbol_version_evidence, + entry->symbol_version); +} + +static int kzt_wrapper_probe_entry_matches_version( + const kzt_wrapper_probe_entry_t *entry, + const kzt_wrapper_probe_request_t *request) +{ + return entry && request && kzt_symbol_version_evidence_matches( + entry->symbol_version_evidence, entry->symbol_version, + request->symbol_version_evidence, request->symbol_version); +} + +static void kzt_wrapper_probe_result_reset(kzt_wrapper_probe_result_t *result) +{ + memset(result, 0, sizeof(*result)); + result->wrapper_match = KZT_PATCH_WRAPPER_NO_MANIFEST; + result->wrapper_version_evidence = KZT_SYMBOL_VERSION_UNKNOWN; + result->bridge_source = KZT_WRAPPER_PROBE_BRIDGE_NONE; +} + +static void kzt_wrapper_probe_result_from_entry( + kzt_wrapper_probe_result_t *result, + kzt_patch_wrapper_match_t match, + const kzt_wrapper_probe_entry_t *entry) +{ + result->wrapper_match = match; + result->wrapper_name = entry ? entry->wrapper_name : NULL; + result->wrapper_version_evidence = entry ? + entry->wrapper_version_evidence : KZT_SYMBOL_VERSION_UNKNOWN; + result->wrapper_symbol_version = entry ? entry->wrapper_symbol_version : NULL; + result->native_symbol = entry ? entry->native_symbol : 0; +} + +static void kzt_wrapper_probe_fill_bridge_request( + const kzt_wrapper_probe_entry_t *entry, + const kzt_wrapper_probe_request_t *request, + kzt_wrapper_probe_bridge_request_t *bridge_request) +{ + memset(bridge_request, 0, sizeof(*bridge_request)); + bridge_request->symbol_name = request->symbol_name; + bridge_request->symbol_version_evidence = + request->symbol_version_evidence; + bridge_request->symbol_version = request->symbol_version; + bridge_request->wrapper_name = entry->wrapper_name; + bridge_request->wrapper_version_evidence = + entry->wrapper_version_evidence; + bridge_request->wrapper_symbol_version = entry->wrapper_symbol_version; + bridge_request->native_symbol = entry->native_symbol; +} + +static void kzt_wrapper_probe_resolve_bridge( + const kzt_wrapper_probe_entry_t *entry, + const kzt_wrapper_probe_request_t *request, + const kzt_wrapper_probe_bridge_ops_t *bridge_ops, + kzt_wrapper_probe_result_t *result) +{ + kzt_wrapper_probe_bridge_request_t bridge_request; + uintptr_t target; + + if (!entry || !entry->native_symbol || !bridge_ops) { + return; + } + + if (bridge_ops->check_bridge) { + target = bridge_ops->check_bridge(entry->native_symbol, + bridge_ops->opaque); + if (target) { + result->bridge_target = target; + result->bridge_source = KZT_WRAPPER_PROBE_BRIDGE_CACHE; + return; + } + } + + if (!bridge_ops->add_bridge) { + return; + } + + kzt_wrapper_probe_fill_bridge_request(entry, request, &bridge_request); + target = bridge_ops->add_bridge(&bridge_request, bridge_ops->opaque); + if (target) { + result->bridge_target = target; + result->bridge_source = KZT_WRAPPER_PROBE_BRIDGE_ADD_BRIDGE; + } +} + +int kzt_wrapper_probe_minimal_manifest( + const kzt_wrapper_probe_manifest_t *manifest, + const kzt_wrapper_probe_request_t *request, + const kzt_wrapper_probe_bridge_ops_t *bridge_ops, + kzt_wrapper_probe_result_t *result) +{ + const kzt_wrapper_probe_entry_t *first_symbol_only = NULL; + const kzt_wrapper_probe_entry_t *first_version_mismatch = NULL; + const kzt_wrapper_probe_entry_t *version_match = NULL; + size_t i; + + if (!result) { + return -1; + } + + kzt_wrapper_probe_result_reset(result); + if (!request || kzt_wrapper_probe_string_empty(request->symbol_name)) { + return -1; + } + + if (!manifest || !manifest->available || !manifest->entries) { + result->wrapper_match = KZT_PATCH_WRAPPER_NO_MANIFEST; + return 0; + } + + for (i = 0; i < manifest->entry_count; ++i) { + const kzt_wrapper_probe_entry_t *entry = &manifest->entries[i]; + + if (!kzt_wrapper_probe_entry_matches_symbol(entry, request)) { + continue; + } + + if (!kzt_wrapper_probe_entry_has_valid_version_evidence(entry) || + !kzt_symbol_version_evidence_valid( + entry->wrapper_version_evidence, + entry->wrapper_symbol_version)) { + if (!first_symbol_only) { + first_symbol_only = entry; + } + continue; + } + + if (!first_version_mismatch) { + first_version_mismatch = entry; + } + + if (kzt_wrapper_probe_entry_matches_version(entry, request)) { + version_match = entry; + break; + } + } + + if (version_match) { + kzt_wrapper_probe_result_from_entry( + result, + request->symbol_version_evidence == + KZT_SYMBOL_VERSION_CONFIRMED_UNVERSIONED + ? KZT_PATCH_WRAPPER_UNVERSIONED_MATCH + : KZT_PATCH_WRAPPER_VERSION_MATCH, + version_match); + kzt_wrapper_probe_resolve_bridge(version_match, request, bridge_ops, + result); + return 0; + } + + if (first_version_mismatch) { + kzt_wrapper_probe_result_from_entry( + result, KZT_PATCH_WRAPPER_VERSION_MISMATCH, + first_version_mismatch); + return 0; + } + + if (first_symbol_only) { + kzt_wrapper_probe_result_from_entry( + result, KZT_PATCH_WRAPPER_SYMBOL_ONLY, first_symbol_only); + return 0; + } + + result->wrapper_match = KZT_PATCH_WRAPPER_NO_WRAPPER; + return 0; +} + +void kzt_wrapper_probe_apply_to_candidate( + const kzt_wrapper_probe_result_t *probe, + kzt_patch_candidate_t *candidate) +{ + if (!probe || !candidate) { + return; + } + + candidate->wrapper_match = probe->wrapper_match; + candidate->wrapper_name = probe->wrapper_name; + candidate->wrapper_version_evidence = + probe->wrapper_version_evidence; + candidate->wrapper_symbol_version = probe->wrapper_symbol_version; + candidate->bridge_target = probe->bridge_target; +} + +void kzt_wrapper_probe_apply_to_decision_request( + const kzt_wrapper_probe_result_t *probe, + kzt_patch_wrapper_match_t *wrapper_match, + const char **wrapper_name, + const char **wrapper_symbol_version, + uintptr_t *bridge_target) +{ + if (!probe) { + return; + } + if (wrapper_match) { + *wrapper_match = probe->wrapper_match; + } + if (wrapper_name) { + *wrapper_name = probe->wrapper_name; + } + if (wrapper_symbol_version) { + *wrapper_symbol_version = probe->wrapper_symbol_version; + } + if (bridge_target) { + *bridge_target = probe->bridge_target; + } +} diff --git a/target/i386/latx/context/librarian.c b/target/i386/latx/context/librarian.c index b6fdba0ea61..210da588e1d 100755 --- a/target/i386/latx/context/librarian.c +++ b/target/i386/latx/context/librarian.c @@ -221,12 +221,17 @@ static void MapLibRemoveMapLib(lib_t* dest, lib_t* src) } } -int AddNeededLib_add(lib_t* maplib, needed_libs_t* neededlibs, library_t* deplib, int local, const char* path, box64context_t* box64) +static int AddNeededLib_add_exact(lib_t* maplib, needed_libs_t* neededlibs, + library_t* deplib, int local, + const char* path, box64context_t* box64, + library_t **exact_library) { + if (exact_library) *exact_library = NULL; printf_log(LOG_INFO, "Trying to add \"%s\" to maplib%s\n", path, local?" (local)":""); // first check if lib is already loaded library_t *lib = getLib(my_context->maplib, path); if(lib) { + if (exact_library) *exact_library = lib; add_neededlib(neededlibs, lib); if (lib && deplib) add_dependedlib(&lib->depended, deplib); printf_log(LOG_INFO, "Already present in maplib => success\n"); @@ -235,6 +240,7 @@ int AddNeededLib_add(lib_t* maplib, needed_libs_t* neededlibs, library_t* deplib // check also in the local loaded lib lib = getLib(my_context->local_maplib, path); if(lib) { + if (exact_library) *exact_library = lib; printf_log(LOG_INFO, "Already present in local_maplib => success\n"); if(local) { // add lib to maplib... @@ -304,9 +310,18 @@ int AddNeededLib_add(lib_t* maplib, needed_libs_t* neededlibs, library_t* deplib lm->l_name = lib->name; lm->l_ld = GetDynamicSection(my_context->elfs[lib->priv.n.elf_index]); } + if (exact_library) *exact_library = lib; return 0; } +int AddNeededLib_add(lib_t* maplib, needed_libs_t* neededlibs, + library_t* deplib, int local, const char* path, + box64context_t* box64) +{ + return AddNeededLib_add_exact(maplib, neededlibs, deplib, local, path, + box64, NULL); +} + int AddNeededLib(lib_t* maplib, needed_libs_t* neededlibs, library_t* deplib, int local, int bindnow, const char** paths, int npath, box64context_t* box64) { if(!neededlibs) { @@ -322,6 +337,26 @@ int AddNeededLib(lib_t* maplib, needed_libs_t* neededlibs, library_t* deplib, in return 0; } +int AddNeededLibWithLibrary(lib_t* maplib, needed_libs_t* neededlibs, + library_t* deplib, int local, int bindnow, + const char* path, box64context_t* box64, + library_t **exact_library) +{ + library_t *selected = NULL; + int add_result; + (void)bindnow; + if (exact_library) *exact_library = NULL; + if (!neededlibs) + neededlibs = box_calloc(1, sizeof(needed_libs_t)); + add_result = AddNeededLib_add_exact(maplib, neededlibs, deplib, local, + path, box64, &selected); + if (!add_result && exact_library) + *exact_library = selected; + /* Preserve AddNeededLib's historical public return behavior. Exact + * callers distinguish failure through the reliably cleared output. */ + return 0; +} + library_t* GetLibMapLib(lib_t* maplib, const char* name) { printf_log(LOG_DEBUG, "Trying to Get \"%s\" to maplib\n", name); @@ -392,7 +427,7 @@ int GetNoSelfSymbolStartEnd(lib_t *maplib, const char* name, uintptr_t* start, u // nope, not found return 0; } -static int GetGlobalSymbolStartEnd_internal(lib_t *maplib, const char* name, uintptr_t* start, uintptr_t* end, elfheader_t* self, int version, const char* vername) +static int GetGlobalSymbolStartEnd_internal(lib_t *maplib, const char* name, uintptr_t* start, uintptr_t* end, elfheader_t* self, int version, const char* vername, library_t **provider) { khint_t pre_k = kh_str_hash_func(name); @@ -406,24 +441,29 @@ static int GetGlobalSymbolStartEnd_internal(lib_t *maplib, const char* name, uin //noweak=0 for(int i=0; ilibsz; ++i) { if(GetLibSymbolStartEnd(maplib->libraries[i], name, pre_k, start, end, version, vername, isLocal(self, maplib->libraries[i]))) // only weak symbol haven't been found yet - if(*start) + if(*start) { + if(provider) + *provider = maplib->libraries[i]; return 1; + } } // nope, not found return 0; } -int GetGlobalSymbolStartEnd(lib_t *maplib, const char* name, uintptr_t* start, uintptr_t* end, elfheader_t* self, int version, const char* vername) +int GetGlobalSymbolStartEndWithProvider(lib_t *maplib, const char* name, uintptr_t* start, uintptr_t* end, elfheader_t* self, int version, const char* vername, library_t **provider) { + if(provider) + *provider = NULL; if(!maplib) return 0; - if(GetGlobalSymbolStartEnd_internal(maplib, name, start, end, self, version, vername)) { + if(GetGlobalSymbolStartEnd_internal(maplib, name, start, end, self, version, vername, provider)) { if(start && end && *end==*start) { // object is of 0 sized, try to see an "_END" object of null size uintptr_t start2, end2; char* buff = (char*)box_malloc(strlen(name) + strlen("_END") + 1); strcpy(buff, name); strcat(buff, "_END"); - if(GetGlobalSymbolStartEnd_internal(maplib, buff, &start2, &end2, self, version, vername)) { + if(GetGlobalSymbolStartEnd_internal(maplib, buff, &start2, &end2, self, version, vername, NULL)) { if(end2>*end && start2==end2) *end = end2; } @@ -448,6 +488,12 @@ int GetGlobalSymbolStartEnd(lib_t *maplib, const char* name, uintptr_t* start, u return 0; } +int GetGlobalSymbolStartEnd(lib_t *maplib, const char* name, uintptr_t* start, uintptr_t* end, elfheader_t* self, int version, const char* vername) +{ + return GetGlobalSymbolStartEndWithProvider( + maplib, name, start, end, self, version, vername, NULL); +} + elfheader_t* GetGlobalSymbolElf(lib_t *maplib, const char* name, int version, const char* vername) { uintptr_t start = 0; diff --git a/target/i386/latx/context/library.c b/target/i386/latx/context/library.c index 378099df1ea..67d7f48c422 100755 --- a/target/i386/latx/context/library.c +++ b/target/i386/latx/context/library.c @@ -29,6 +29,10 @@ #include "librarian.h" #include "librarian_private.h" #include "pathcoll.h" +#ifdef CONFIG_LATX_KZT +#include "kzt_guest_library_adapter.h" +#include "kzt_guest_library_binding.h" +#endif #define GO(P, N) int wrapped##N##_init(library_t* lib, box64context_t *box64); \ void wrapped##N##_fini(library_t* lib); \ @@ -352,7 +356,6 @@ int AddSymbolsLibrary(lib_t *maplib, library_t* lib) int ReloadLibrary(library_t* lib) { - lib->active = 1; if(lib->type == LIB_EMULATED) { elfheader_t *elf_header = lib->context->elfs[lib->priv.n.elf_index]; // reload image in memory and re-run the mapping @@ -383,8 +386,21 @@ int ReloadLibrary(library_t* lib) printf_log(LOG_NONE, "Error: relocating symbols in elf %s\n", lib->name); return 1; } - RelocateElfPlt(lib->context->maplib, lib->maplib, 0, elf_header); + if(RelocateElfPlt(lib->context->maplib, lib->maplib, 0, elf_header)) { + printf_log(LOG_NONE, "Error: relocating PLT symbols in elf %s\n", lib->name); + return 1; + } } + lib->active = 1; +#ifdef CONFIG_LATX_KZT + /* Publish only after every reload step has succeeded. On failure the + * binding lifecycle remains DEAD and no exact pair is left pending. */ + if (kzt_guest_library_reactivate( + KztGuestLibraryBindingsForContext(lib->context), lib) == 0 && + lib->x86linkmap) + kzt_guest_library_note_loader_pair( + lib->context, (uintptr_t)lib->x86linkmap, lib); +#endif return 0; } void InactiveLibrary(library_t* lib) diff --git a/target/i386/latx/context/meson.build b/target/i386/latx/context/meson.build index f07cf9b44a0..b24ade609f2 100644 --- a/target/i386/latx/context/meson.build +++ b/target/i386/latx/context/meson.build @@ -61,13 +61,42 @@ my_file = files( 'library.c', 'pathcoll.c', 'fileutils.c', + 'dlopen_recycle_transaction.c', 'elfparser.c', + 'elf_plt_relocation.c', + 'elfmap.c', 'elfloader.c', 'elfload_dump.c', 'mallochook.c', 'bridge.c', 'wrapperdebug.c', 'wrapper.c', + 'kzt_guest_registry.c', + 'kzt_guest_registry_context.c', + 'kzt_guest_library_binding.c', + 'kzt_jump_slot_route.c', + 'kzt_jump_slot_production.c', + 'kzt_lazy_binding.c', + 'kzt_lazy_diagnostics.c', + 'kzt_plt_resolver_adapter.c', + 'kzt_guest_library_adapter.c', + 'kzt_bridge_exact.c', + 'kzt_guest_link_map_reader.c', + 'kzt_guest_dynamic.c', + 'kzt_guest_dynamic_diagnostics.c', + 'kzt_observation_adapter.c', + 'kzt_patch_planner.c', + 'kzt_runtime_got_plt_candidate.c', + 'kzt_runtime_candidate_shadow.c', + 'kzt_rela_immediate_candidate.c', + 'kzt_rela_request_enricher.c', + 'kzt_rela_diagnostics.c', + 'kzt_rela_runtime_bridge.c', + 'kzt_owner_resolver.c', + 'kzt_wrapper_probe.c', + 'kzt_wrapper_bridge_provider.c', + 'kzt_patch_spike_guard.c', + 'kzt_patch_spike_writer.c', 'myalign.c', 'obstack.c', 'globalsymbols.c', diff --git a/target/i386/latx/context/myalign.c b/target/i386/latx/context/myalign.c index 9e4981a9d57..c6d6a31aa85 100644 --- a/target/i386/latx/context/myalign.c +++ b/target/i386/latx/context/myalign.c @@ -9,8 +9,14 @@ #include "config-host.h" #include "lsenv.h" #include "myalign.h" +#include "debug.h" +#include "elfmap.h" #include "elfloader.h" #include "elfloader_private.h" +#ifdef CONFIG_LATX_KZT +#include "kzt_observation_adapter.h" +#include "kzt_guest_library_adapter.h" +#endif #include #include #include @@ -41,6 +47,78 @@ typedef union { } mmx87_regs_t; static int regs_abi[] = {R_EDI, R_ESI, R_EDX, R_ECX, R_R8, R_R9}; + +#ifdef CONFIG_LATX_KZT +static int kzt_registry_debug_dump_line(const char *line, void *opaque) +{ + (void)opaque; + + printf_kzt_registry_diagnostics("%s\n", line); + return 0; +} + +static int kzt_main_elf_identity( + const elfheader_t *head, + kzt_guest_link_map_identity_t *identity) +{ + size_t i; + + if (!head || !identity || !head->PHEntries) { + return -1; + } + memset(identity, 0, sizeof(*identity)); + for (i = 0; i < head->numPHEntries; ++i) { + const Elf64_Phdr *entry = &head->PHEntries[i]; + + if (entry->p_type != PT_DYNAMIC) { + continue; + } + if (head->delta < 0 || + entry->p_vaddr > UINTPTR_MAX - (uintptr_t)head->delta) { + return -1; + } + identity->load_bias = (uintptr_t)head->delta; + identity->dynamic_addr = entry->p_vaddr + identity->load_bias; + return identity->dynamic_addr ? 0 : -1; + } + return -1; +} + +static void kzt_callback_diagnostic_log( + const kzt_observation_adapter_diagnostic_t *diagnostic, + void *opaque) +{ + if (!diagnostic) { + return; + } + + printf_kzt_registry_diagnostics( + "KZT registry callback result=%d link_map=0x%lx " + "registry_result=%d generation=%lu objects=%lu " + "observed=%lu suppressed=%lu\n", + diagnostic->result, (unsigned long)diagnostic->link_map_addr, + diagnostic->registry.result, diagnostic->registry.generation, + diagnostic->registry.object_count, + diagnostic->registry.result_observations, + diagnostic->registry.result_suppressed); + + if (opaque) { + (void)kzt_guest_registry_dump_text(opaque, kzt_registry_debug_dump_line, + NULL); + } + + if (diagnostic->dynamic.comparison_attempted) { + char summary[1024]; + + if (kzt_guest_dynamic_diagnostics_format_summary( + &diagnostic->dynamic.comparison, summary, + sizeof(summary)) == 0) { + printf_kzt_registry_diagnostics("%s\n", summary); + } + } +} +#endif + uintptr_t getVArgs(int pos, uintptr_t* b, int N) { CPUX86State *cpu = (CPUX86State *)lsenv->cpu_state; @@ -2222,30 +2300,105 @@ static char* kzt_find_realsofilepath(char * filepath, char *filetmp) return filepath; } extern const char* libcName; -static void kzt_tb_callback(CPUX86State *env) +#ifdef CONFIG_LATX_KZT +static int kzt_callback_read_memory(uintptr_t guest_addr, + void *dst, + size_t size, + void *opaque) +{ + void *host_ptr; + + (void)opaque; + if (!dst && size) { + return -1; + } + if (!size) { + return 0; + } + + host_ptr = lock_user(VERIFY_READ, (abi_ulong)guest_addr, size, true); + if (!host_ptr) { + return -1; + } + + memcpy(dst, host_ptr, size); + unlock_user(host_ptr, (abi_ulong)guest_addr, 0); + return 0; +} +#endif + +static int kzt_tb_callback_legacy(uintptr_t link_map_addr, void *opaque) { - struct link_map_x64 * my_lm = (struct link_map_x64 *)env->regs[R_EAX + ld_info->reg]; + const kzt_guest_library_loader_scope_t *loader_scope = opaque; +#ifdef CONFIG_LATX_KZT + int loader_scope_active = loader_scope && loader_scope->bindings && + loader_scope->identity && loader_scope->cookie; +#endif + struct link_map_x64 * my_lm = (struct link_map_x64 *)link_map_addr; elfheader_t *h = NULL; - if ((!my_lm ||!my_lm->l_name ||!strlen(my_lm->l_name) || ! my_lm->l_addr)&& my_lm->l_addr != info1.load_addr) { +#ifdef CONFIG_LATX_KZT + int relocate_result; + int relocate_plt_result; +#endif + uintptr_t map_start = 0; + uintptr_t map_end = 0; + if (!my_lm) { + printf_log(LOG_DEBUG, "error %d debug %s link_map = %p\n", + getpid(), __func__, my_lm); + return 0; + } + if (!my_lm->l_name) { + printf_log(LOG_DEBUG, + "error %d debug %s link_map = %p{0x%lx, }\n", + getpid(), __func__, my_lm, my_lm->l_addr); + return 0; + } + if ((!my_lm->l_name[0] || !my_lm->l_addr) && + my_lm->l_addr != info1.load_addr) { printf_log(LOG_DEBUG, "error %d debug %s link_map = %p{0x%lx, %s}\n", getpid(), __func__, my_lm, my_lm->l_addr, my_lm->l_name); - return; + return 0; } printf_log(LOG_DEBUG, "%d debug %s link_map = %p{0x%lx, %s}\n", getpid(), __func__, my_lm, my_lm->l_addr, my_lm->l_name); char * rfilename = my_lm->l_name; - if (strstr(basename(rfilename), "ld-linux-x86-64.so.2")) { - AddDebugInfo(LIB_EMULATED, my_lm->l_name, my_lm->l_map_start, my_lm->l_map_end); - return; - } char filetmp[PATH_MAX] = {0}; if (rfilename[0] == '/') { rfilename = kzt_find_realsofilepath(rfilename, filetmp); } + if (strstr(basename(rfilename), "ld-linux-x86-64.so.2")) { + FILE *loader_file = fopen(rfilename, "rb"); + elfheader_t *loader_header = loader_file ? + LoadAndCheckElfHeader(loader_file, rfilename, 0) : NULL; + + if (loader_file) { + fclose(loader_file); + } + if (loader_header && + GetElfLoadRange(loader_header->PHEntries, + loader_header->numPHEntries, my_lm->l_addr, + TARGET_PAGE_SIZE, &map_start, &map_end) == 0) { + AddDebugInfo(LIB_EMULATED, my_lm->l_name, + map_start, map_end); + } else { + printf_log(LOG_INFO, + "KZT: cannot determine loader range for %s\n", + my_lm->l_name); + } + FreeElfHeader(&loader_header); + return 0; + } char * rbasename = basename(rfilename); - library_t* lib = NewLibrary(rbasename, my_context); - if (lib) { - const char* libs[] = {rbasename}; - AddNeededLib(my_context->maplib, &my_context->neededlibs, NULL, 0, 1, libs, 1, my_context); + library_t* lib = NULL; + (void)AddNeededLibWithLibrary( + my_context->maplib, &my_context->neededlibs, NULL, 0, 1, + rbasename, my_context, &lib); +#ifdef CONFIG_LATX_KZT + /* AddNeededLibWithLibrary returns the exact instance selected/created by + * this callback operation; no basename lookup is used for binding. */ + if (loader_scope_active) { + kzt_guest_library_note_loader_pair_pending( + my_context, loader_scope, link_map_addr, lib); } +#endif if (!lib && (!strncmp(rbasename, "libSDL", 6)||!strncmp(rbasename, "libCgGL.so", strlen("libCgGL.so")))) { printf_log(LOG_DEBUG, "%s libSDL need libGL.so.1\n", __func__); const char* libs[] = {"libGL.so.1"}; @@ -2254,9 +2407,15 @@ static void kzt_tb_callback(CPUX86State *env) FILE *f = fopen(rfilename, "rb"); if(!f) { printf_log(LOG_INFO, "%s Error: Cannot open \"%s\"\n", __func__, rfilename); - return; + return 0; } h = LoadAndCheckElfHeader(f, rfilename, 0); + if (!h) { + fclose(f); + printf_log(LOG_INFO, "%s Error: Cannot load ELF header for \"%s\"\n", + __func__, rfilename); + return 0; + } ElfHeadReFix(h, my_lm->l_addr); fclose(f); collectX86free(h); @@ -2271,13 +2430,109 @@ static void kzt_tb_callback(CPUX86State *env) } AddElfHeader(my_context, h); LoadNeededLibs(h, my_context->maplib, &my_context->neededlibs, NULL, 0, 0, my_context); +#ifdef CONFIG_LATX_KZT + relocate_result = RelocateElf(my_context->maplib, NULL, 0, h); + relocate_plt_result = RelocateElfPlt(my_context->maplib, NULL, 0, h); + /* Startup loading has no dlopen scope to publish the pair later. The + callback gate remains held until this function returns. Failed + relocation keeps the legacy fail-open path without publishing. */ + if (lib && !loader_scope_active && relocate_result == 0 && + relocate_plt_result == 0) { + kzt_guest_library_note_loader_pair(my_context, link_map_addr, lib); + } +#else RelocateElf(my_context->maplib, NULL, 0, h); RelocateElfPlt(my_context->maplib, NULL, 0, h); - if (lib) { - AddDebugInfo(LIB_WRAPPED, my_lm->l_name, my_lm->l_map_start, my_lm->l_map_end); +#endif + if (GetElfLoadRange(h->PHEntries, h->numPHEntries, h->delta, + TARGET_PAGE_SIZE, &map_start, &map_end) == 0) { + AddDebugInfo(lib ? LIB_WRAPPED : LIB_EMULATED, my_lm->l_name, + map_start, map_end); + } else { + printf_log(LOG_INFO, + "KZT: cannot determine ELF load range for %s\n", + my_lm->l_name); + } + return 0; +} + +static void kzt_tb_callback(CPUX86State *env) +{ + uintptr_t link_map_addr = env->regs[R_EAX + ld_info->reg]; +#ifdef CONFIG_LATX_KZT + kzt_guest_registry_t *registry = KztGuestRegistryForContext(my_context); + int diagnostics_enabled = kzt_registry_diagnostics_enabled(); + kzt_guest_registry_diagnostic_config_t diagnostic_config = { + .enabled = diagnostics_enabled, + .throttle_limit = 1, + }; + const kzt_guest_link_map_reader_ops_t reader_ops = { + .read_memory = kzt_callback_read_memory, + .opaque = NULL, + }; + kzt_guest_link_map_identity_t main_identity = { 0 }; + kzt_guest_link_map_identity_t object_identity = { 0 }; + uintptr_t confirmed_main_head = 0; + uintptr_t namespace_head = 0; + uintptr_t predecessor = 0; + int main_namespace; + + (void)kzt_guest_registry_context_get_main_namespace_head( + &my_context->kzt_guest_registry_context, &confirmed_main_head); + if (kzt_guest_link_map_read_identity( + link_map_addr, &reader_ops, &object_identity) != 0) { + main_namespace = -1; + } else if (confirmed_main_head && + kzt_guest_registry_context_has_main_namespace_evidence( + &my_context->kzt_guest_registry_context, registry, + link_map_addr, object_identity.load_bias, + object_identity.dynamic_addr)) { + main_namespace = 1; } else { - AddDebugInfo(LIB_EMULATED, my_lm->l_name, my_lm->l_map_start, my_lm->l_map_end); + if (confirmed_main_head && + kzt_guest_link_map_read_predecessor( + link_map_addr, &reader_ops, &predecessor) == 0 && + predecessor == confirmed_main_head) { + main_namespace = 1; + } else if (confirmed_main_head || + kzt_main_elf_identity(elf_header, &main_identity) == 0) { + main_namespace = kzt_guest_link_map_classify_namespace( + link_map_addr, &main_identity, confirmed_main_head, &reader_ops, + &namespace_head); + } else { + main_namespace = -1; + } } + if (main_namespace == 1 && !confirmed_main_head && + kzt_guest_registry_context_confirm_main_namespace_head( + &my_context->kzt_guest_registry_context, + &my_context->mutex_lock, namespace_head) != 0) { + main_namespace = -1; + } + kzt_observation_adapter_request_t request = { + .enabled = option_kzt || wine_option_kzt, + .diagnostics_enabled = diagnostics_enabled, + .link_map_addr = link_map_addr, + .registry = registry, + .library_bindings = KztGuestLibraryBindingsForContext(my_context), + .loader_scope = &env->kzt_guest_library_loader_scope, + .reader_ops = &reader_ops, + .namespace_id_present = main_namespace == 1, + .namespace_id = 0, + .legacy_flow = kzt_tb_callback_legacy, + .legacy_opaque = &env->kzt_guest_library_loader_scope, + .diagnostic = kzt_callback_diagnostic_log, + .diagnostic_opaque = registry, + }; + + if (registry && diagnostics_enabled) { + (void)kzt_guest_registry_configure_diagnostics(registry, + &diagnostic_config); + } + (void)kzt_observe_guest_object_from_callback(&request, NULL); +#else + (void)kzt_tb_callback_legacy(link_map_addr, env); +#endif } static TranslationBlock* test_tb; static void test_x86free(CPUX86State *env) diff --git a/target/i386/latx/context/wrappedlibc.c b/target/i386/latx/context/wrappedlibc.c index c6bced3cbd0..27b6bd88578 100644 --- a/target/i386/latx/context/wrappedlibc.c +++ b/target/i386/latx/context/wrappedlibc.c @@ -68,6 +68,12 @@ #include "elfloader_private.h" #include "bridge.h" #include "globalsymbols.h" +#include "dlopen_recycle_transaction.h" +#ifdef CONFIG_LATX_KZT +#include "kzt_guest_library_binding.h" +#include "kzt_guest_library_adapter.h" +#endif +#include "kzt_loader_callback_scope.h" #define LIBNAME libc const char* libcName = @@ -77,6 +83,49 @@ const char* libcName = "libc.so.6" #endif ; + +static uint64_t run_guest_dlopen_scoped(uintptr_t function, + void *filename, int flag, + kzt_guest_library_loader_scope_t *scope) +{ +#ifdef CONFIG_LATX_KZT + __MY_CPU; + kzt_guest_library_loader_scope_t previous = + cpu->kzt_guest_library_loader_scope; + kzt_guest_library_bindings_t *bindings = + KztGuestLibraryBindingsForContext(my_context); + int scoped = scope && + kzt_guest_library_loader_scope_begin(bindings, scope) == 0; + uint64_t result; + + if (scoped) cpu->kzt_guest_library_loader_scope = *scope; + result = RunFunctionWithState(function, 2, filename, flag); + cpu->kzt_guest_library_loader_scope = previous; + return result; +#else + (void)scope; + return RunFunctionWithState(function, 2, filename, flag); +#endif +} + +static void finish_guest_dlopen_scoped( + kzt_guest_library_loader_scope_t *scope, uintptr_t link_map_addr, + library_t *library, int publish) +{ +#ifdef CONFIG_LATX_KZT + if (publish && link_map_addr) { + if (library) + kzt_guest_library_publish_loader_pair_scoped( + my_context, scope, link_map_addr, library); + else + kzt_guest_library_publish_loader_observed_scoped( + my_context, scope, link_map_addr); + } + kzt_guest_library_loader_scope_end(scope); +#else + (void)scope; (void)link_map_addr; (void)library; (void)publish; +#endif +} #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wmissing-prototypes" void* x86free; @@ -3562,13 +3611,19 @@ int init_x86dlfun(void) kzt_wine_init_x86(); return 0; } -static int callx86dlopen(void *filename, int flag, elfheader_t * h, int is_local) { - struct link_map* ret = (struct link_map*)(uintptr_t)RunFunctionWithState((uintptr_t)my_context->dlprivate->x86dlopen, 2, filename, flag); +static int callx86dlopen(void *filename, int flag, elfheader_t * h, + int is_local, int publish_pair) { + kzt_guest_library_loader_scope_t scope = { 0 }; + struct link_map* ret = (struct link_map*)(uintptr_t) + run_guest_dlopen_scoped( + (uintptr_t)my_context->dlprivate->x86dlopen, filename, flag, + &scope); if (ret) { printf_dlsym(LOG_DEBUG, "latx RunFunctionWithState dlopen %s addr %p\n", (char *)filename, (void *)ret->l_addr); h->lib->x86linkmap = ret; } else { //open error + finish_guest_dlopen_scoped(&scope, 0, NULL, 0); return -1; } h->delta = ret->l_addr; @@ -3582,6 +3637,8 @@ static int callx86dlopen(void *filename, int flag, elfheader_t * h, int is_local printf_dlsym(LOG_INFO, "Failure to Add lib => fail\n"); lsassert(0); } + finish_guest_dlopen_scoped(&scope, (uintptr_t)ret, h->lib, + publish_pair); return 0; } static void LatxResetElf(elfheader_t * h) @@ -3592,6 +3649,72 @@ static void LatxResetElf(elfheader_t * h) h->latx_type = 0; h->latx_hasfix = 0; } + +typedef struct dlopen_recycle_context { + void *filename; + int flag; + elfheader_t *header; + library_t *library; + int is_local; + int guest_open_required; +} dlopen_recycle_context_t; + +static void recycle_reset_loader_state(dlopen_recycle_context_t *recycle) +{ + linkmap_t *lm; + + recycle->library->x86linkmap = NULL; + if (!recycle->header) + return; + LatxResetElf(recycle->header); + recycle->header->delta = 0; + lm = getLinkMapLib(recycle->library); + if (lm) + lm->l_addr = 0; +} + +static void recycle_prepare(void *opaque) +{ + dlopen_recycle_context_t *recycle = opaque; + if (!recycle->guest_open_required) + return; + recycle_reset_loader_state(recycle); +} + +static int recycle_guest_open(void *opaque) +{ + dlopen_recycle_context_t *recycle = opaque; +#ifdef CONFIG_LATX_KZT + if (recycle->guest_open_required && + kzt_guest_library_reactivate( + KztGuestLibraryBindingsForContext(recycle->library->context), + recycle->library) != 0) + return -1; +#endif + return recycle->guest_open_required + ? callx86dlopen(recycle->filename, recycle->flag, + recycle->header, recycle->is_local, 0) + : 0; +} + +static int recycle_reload(void *opaque) +{ + dlopen_recycle_context_t *recycle = opaque; + return ReloadLibrary(recycle->library); +} + +static void recycle_rollback(void *opaque, int guest_opened) +{ + dlopen_recycle_context_t *recycle = opaque; + InactiveLibrary(recycle->library); + if (guest_opened && recycle->guest_open_required && + recycle->library->x86linkmap && + my_context->dlprivate->x86dlclose) + (void)RunFunctionWithState( + (uintptr_t)my_context->dlprivate->x86dlclose, 1, + recycle->library->x86linkmap); + recycle_reset_loader_state(recycle); +} void* my_dlopen(void *filename, int flag){ // TODO, handling special values for filename, like RTLD_SELF? // TODO, handling flags? @@ -3647,14 +3770,31 @@ void* my_dlopen(void *filename, int flag){ int idx = GetElfIndex(dl->libs[i]); if(idx!=-1) { printf_dlsym(LOG_DEBUG, "dlopen: Recycling, calling Init for %p (%s)\n", (void*)(i+1), rfilename); - //TODO + dlopen_recycle_context_t recycle = { + .filename = rfilename, + .flag = flag, + .library = dl->libs[i], + .is_local = is_local, + }; if (IsEmuLib(dl->libs[i])) { - elfheader_t * h = my_context->elfs[idx]; - lsassert(h); - LatxResetElf(h); - callx86dlopen(rfilename, flag, h, is_local); + recycle.header = my_context->elfs[idx]; + lsassert(recycle.header); + recycle.guest_open_required = 1; + } + void *handle = dlopen_recycle_transaction( + (void *)(i + 1), &dl->count[i], !(flag & 0x4), + &recycle, recycle_prepare, recycle_guest_open, + recycle_reload, recycle_rollback); + if (!handle) { + if(!dl->last_error) + dl->last_error = box_malloc(129); + snprintf(dl->last_error, 129, + "Cannot recycle dlopen(\"%s\"/%p, %X)\n", + rfilename, filename, flag); + return NULL; } - ReloadLibrary(dl->libs[i]); // reset memory image, redo reloc, run inits + printf_dlsym(LOG_DEBUG, "dlopen: Recycling %s/%p count=%ld (dlopened=%ld, elf_index=%d)\n", rfilename, (void*)(i+1), dl->count[i], dl->dlopened[i], GetElfIndex(dl->libs[i])); + return handle; } } if(!(flag&0x4)) @@ -3679,7 +3819,12 @@ void* my_dlopen(void *filename, int flag){ printf_dlsym(LOG_DEBUG, "warning call x86dlopen filename is %s %x\n", (char *)filename, flag); return NULL; #else - uint64_t ret = RunFunctionWithState((uintptr_t)my_context->dlprivate->x86dlopen, 2, filename, flag); + kzt_guest_library_loader_scope_t scope = { 0 }; + uint64_t ret = run_guest_dlopen_scoped( + (uintptr_t)my_context->dlprivate->x86dlopen, filename, flag, + &scope); + finish_guest_dlopen_scoped(&scope, (uintptr_t)ret, NULL, + ret != 0); printf_dlsym(LOG_DEBUG, "warning call call x86dlopen filename %s %x ret=0x%lx\n", (char *)filename, flag, ret); //lsassert(0); if (ret) { @@ -3692,15 +3837,14 @@ void* my_dlopen(void *filename, int flag){ return NULL; #endif } - if(AddNeededLib(NULL, NULL, NULL, is_local, bindnow, libs, 1, my_context)) { + if(AddNeededLibWithLibrary(NULL, NULL, NULL, is_local, bindnow, + rfilename, my_context, &lib) || !lib) { printf_dlsym(strchr(rfilename,'/')?LOG_DEBUG:LOG_INFO, "Warning: Cannot dlopen(\"%s\"/%p, %X)\n", rfilename, filename, flag); if(!dl->last_error) dl->last_error = box_malloc(129); snprintf(dl->last_error, 129, "Cannot dlopen(\"%s\"/%p, %X)\n", rfilename, filename, flag); return NULL; } - lib = GetLibInternal(rfilename); - if (!lib) return NULL; lib->x86dlopenflag = flag; if (lib && lib->type == LIB_EMULATED) { // if dlopened = 0 ---> lib added but not loaded @@ -3709,7 +3853,7 @@ void* my_dlopen(void *filename, int flag){ elfheader_t * h = my_context->elfs[libidx]; lsassert(h); if (!h->latx_hasfix || !lib->x86linkmap) {//lib->x86linkmap is null ---- this lib has been needed by other elf and opened - callx86dlopen(rfilename, flag, h, is_local); + callx86dlopen(rfilename, flag, h, is_local, 1); } } //TODO:RunDeferedElfInit; @@ -3928,13 +4072,20 @@ void* my_dlsym(void *handle, void *symbol){ #if 1 if(!dl->libs[nlib]->x86linkmap) { //redlopen - uint64_t ret = RunFunctionWithState((uintptr_t)my_context->dlprivate->x86dlopen, 2, dl->libs[nlib]->name, dl->libs[nlib]->x86dlopenflag); + kzt_guest_library_loader_scope_t scope = { 0 }; + uint64_t ret = run_guest_dlopen_scoped( + (uintptr_t)my_context->dlprivate->x86dlopen, + dl->libs[nlib]->name, dl->libs[nlib]->x86dlopenflag, + &scope); if (!ret) {//user sometime test for finding a func. + finish_guest_dlopen_scoped(&scope, 0, NULL, 0); printf_dlsym(LOG_NEVER, "redlopen %p return %p\n", rsymbol, (void*)NULL); return NULL; } lsassert(ret); dl->libs[nlib]->x86linkmap = (void *)ret; + finish_guest_dlopen_scoped( + &scope, (uintptr_t)ret, dl->libs[nlib], 1); ret = RunFunctionWithState((uintptr_t)my_context->dlprivate->x86dlsym, 2, dl->libs[nlib]->x86linkmap , cpu->regs[R_ESI]); printf_dlsym(LOG_DEBUG, "call x86dlsym filename %s is wrapped but not find symbol, dlsym(%p, %s) ret=0x%lx\n", dl->libs[nlib]->name, dl->libs[nlib]->x86linkmap, (char *)cpu->regs[R_ESI], ret); diff --git a/target/i386/latx/context/wrappedlibdl.c b/target/i386/latx/context/wrappedlibdl.c index 1f7c3ce8626..611c4f5c637 100644 --- a/target/i386/latx/context/wrappedlibdl.c +++ b/target/i386/latx/context/wrappedlibdl.c @@ -27,6 +27,12 @@ #include "callback.h" #include "myalign.h" #include "fileutils.h" +#include "dlopen_recycle_transaction.h" +#ifdef CONFIG_LATX_KZT +#include "kzt_guest_library_adapter.h" +#include "kzt_guest_library_binding.h" +#endif +#include "kzt_loader_callback_scope.h" #define FORWORDBACK 0 dlprivate_t *NewDLPrivate(void) { @@ -81,13 +87,62 @@ int init_x86dlfun(void) my_context->dlprivate->x86dlinfo = rsyms[5]; return 0; } -static int callx86dlopen(void *filename, int flag, elfheader_t * h, int is_local) { - struct link_map* ret = (struct link_map*)(uintptr_t)RunFunctionWithState((uintptr_t)my_context->dlprivate->x86dlopen, 2, filename, flag); +static uint64_t run_guest_dlopen_scoped(uintptr_t function, + void *filename, int flag, + kzt_guest_library_loader_scope_t *scope) +{ +#ifdef CONFIG_LATX_KZT + __MY_CPU; + kzt_guest_library_loader_scope_t previous = + cpu->kzt_guest_library_loader_scope; + kzt_guest_library_bindings_t *bindings = + KztGuestLibraryBindingsForContext(my_context); + int scoped = scope && + kzt_guest_library_loader_scope_begin(bindings, scope) == 0; + uint64_t result; + + if (scoped) cpu->kzt_guest_library_loader_scope = *scope; + result = RunFunctionWithState(function, 2, filename, flag); + cpu->kzt_guest_library_loader_scope = previous; + return result; +#else + (void)scope; + return RunFunctionWithState(function, 2, filename, flag); +#endif +} + +static void finish_guest_dlopen_scoped( + kzt_guest_library_loader_scope_t *scope, uintptr_t link_map_addr, + library_t *library, int publish) +{ +#ifdef CONFIG_LATX_KZT + if (publish && link_map_addr) { + if (library) + kzt_guest_library_publish_loader_pair_scoped( + my_context, scope, link_map_addr, library); + else + kzt_guest_library_publish_loader_observed_scoped( + my_context, scope, link_map_addr); + } + kzt_guest_library_loader_scope_end(scope); +#else + (void)scope; (void)link_map_addr; (void)library; (void)publish; +#endif +} + +static int callx86dlopen(void *filename, int flag, elfheader_t * h, + int is_local, int publish_pair) { + kzt_guest_library_loader_scope_t scope = { 0 }; + struct link_map* ret = (struct link_map*)(uintptr_t) + run_guest_dlopen_scoped( + (uintptr_t)my_context->dlprivate->x86dlopen, filename, flag, + &scope); if (ret) { printf_dlsym(LOG_DEBUG, "latx RunFunctionWithState dlopen %s addr %p\n", (char *)filename, (void *)ret->l_addr); h->lib->x86linkmap = ret; } else { //open error + finish_guest_dlopen_scoped(&scope, 0, NULL, 0); return -1; } h->delta = ret->l_addr; @@ -101,6 +156,8 @@ static int callx86dlopen(void *filename, int flag, elfheader_t * h, int is_local printf_dlsym(LOG_INFO, "Failure to Add lib => fail\n"); lsassert(0); } + finish_guest_dlopen_scoped(&scope, (uintptr_t)ret, h->lib, + publish_pair); return 0; } static void LatxResetElf(elfheader_t * h) @@ -111,6 +168,72 @@ static void LatxResetElf(elfheader_t * h) h->latx_type = 0; h->latx_hasfix = 0; } + +typedef struct dlopen_recycle_context { + void *filename; + int flag; + elfheader_t *header; + library_t *library; + int is_local; + int guest_open_required; +} dlopen_recycle_context_t; + +static void recycle_reset_loader_state(dlopen_recycle_context_t *recycle) +{ + linkmap_t *lm; + + recycle->library->x86linkmap = NULL; + if (!recycle->header) + return; + LatxResetElf(recycle->header); + recycle->header->delta = 0; + lm = getLinkMapLib(recycle->library); + if (lm) + lm->l_addr = 0; +} + +static void recycle_prepare(void *opaque) +{ + dlopen_recycle_context_t *recycle = opaque; + if (!recycle->guest_open_required) + return; + recycle_reset_loader_state(recycle); +} + +static int recycle_guest_open(void *opaque) +{ + dlopen_recycle_context_t *recycle = opaque; +#ifdef CONFIG_LATX_KZT + if (recycle->guest_open_required && + kzt_guest_library_reactivate( + KztGuestLibraryBindingsForContext(recycle->library->context), + recycle->library) != 0) + return -1; +#endif + return recycle->guest_open_required + ? callx86dlopen(recycle->filename, recycle->flag, + recycle->header, recycle->is_local, 0) + : 0; +} + +static int recycle_reload(void *opaque) +{ + dlopen_recycle_context_t *recycle = opaque; + return ReloadLibrary(recycle->library); +} + +static void recycle_rollback(void *opaque, int guest_opened) +{ + dlopen_recycle_context_t *recycle = opaque; + InactiveLibrary(recycle->library); + if (guest_opened && recycle->guest_open_required && + recycle->library->x86linkmap && + my_context->dlprivate->x86dlclose) + (void)RunFunctionWithState( + (uintptr_t)my_context->dlprivate->x86dlclose, 1, + recycle->library->x86linkmap); + recycle_reset_loader_state(recycle); +} void* my_dlopen(void *filename, int flag){ // TODO, handling special values for filename, like RTLD_SELF? // TODO, handling flags? @@ -166,14 +289,31 @@ void* my_dlopen(void *filename, int flag){ int idx = GetElfIndex(dl->libs[i]); if(idx!=-1) { printf_dlsym(LOG_DEBUG, "dlopen: Recycling, calling Init for %p (%s)\n", (void*)(i+1), rfilename); - //TODO + dlopen_recycle_context_t recycle = { + .filename = rfilename, + .flag = flag, + .library = dl->libs[i], + .is_local = is_local, + }; if (IsEmuLib(dl->libs[i])) { - elfheader_t * h = my_context->elfs[idx]; - lsassert(h); - LatxResetElf(h); - callx86dlopen(rfilename, flag, h, is_local); + recycle.header = my_context->elfs[idx]; + lsassert(recycle.header); + recycle.guest_open_required = 1; } - ReloadLibrary(dl->libs[i]); // reset memory image, redo reloc, run inits + void *handle = dlopen_recycle_transaction( + (void *)(i + 1), &dl->count[i], !(flag & 0x4), + &recycle, recycle_prepare, recycle_guest_open, + recycle_reload, recycle_rollback); + if (!handle) { + if(!dl->last_error) + dl->last_error = box_malloc(129); + snprintf(dl->last_error, 129, + "Cannot recycle dlopen(\"%s\"/%p, %X)\n", + rfilename, filename, flag); + return NULL; + } + printf_dlsym(LOG_DEBUG, "dlopen: Recycling %s/%p count=%ld (dlopened=%ld, elf_index=%d)\n", rfilename, (void*)(i+1), dl->count[i], dl->dlopened[i], GetElfIndex(dl->libs[i])); + return handle; } } if(!(flag&0x4)) @@ -198,7 +338,12 @@ void* my_dlopen(void *filename, int flag){ printf_dlsym(LOG_DEBUG, "warning call x86dlopen filename is %s %x\n", (char *)filename, flag); return NULL; #else - uint64_t ret = RunFunctionWithState((uintptr_t)my_context->dlprivate->x86dlopen, 2, filename, flag); + kzt_guest_library_loader_scope_t scope = { 0 }; + uint64_t ret = run_guest_dlopen_scoped( + (uintptr_t)my_context->dlprivate->x86dlopen, filename, flag, + &scope); + finish_guest_dlopen_scoped(&scope, (uintptr_t)ret, NULL, + ret != 0); printf_dlsym(LOG_DEBUG, "warning call call x86dlopen filename %s %x ret=0x%lx\n", (char *)filename, flag, ret); //lsassert(0); if (ret) { @@ -211,15 +356,14 @@ void* my_dlopen(void *filename, int flag){ return NULL; #endif } - if(AddNeededLib(NULL, NULL, NULL, is_local, bindnow, libs, 1, my_context)) { + if(AddNeededLibWithLibrary(NULL, NULL, NULL, is_local, bindnow, + rfilename, my_context, &lib) || !lib) { printf_dlsym(strchr(rfilename,'/')?LOG_DEBUG:LOG_INFO, "Warning: Cannot dlopen(\"%s\"/%p, %X)\n", rfilename, filename, flag); if(!dl->last_error) dl->last_error = box_malloc(129); snprintf(dl->last_error, 129, "Cannot dlopen(\"%s\"/%p, %X)\n", rfilename, filename, flag); return NULL; } - lib = GetLibInternal(rfilename); - if (!lib) return NULL; lib->x86dlopenflag = flag; if (lib && lib->type == LIB_EMULATED) { // if dlopened = 0 ---> lib added but not loaded @@ -228,7 +372,7 @@ void* my_dlopen(void *filename, int flag){ elfheader_t * h = my_context->elfs[libidx]; lsassert(h); if (!h->latx_hasfix || !lib->x86linkmap) {//lib->x86linkmap is null ---- this lib has been needed by other elf and opened - callx86dlopen(rfilename, flag, h, is_local); + callx86dlopen(rfilename, flag, h, is_local, 1); } } //TODO:RunDeferedElfInit; @@ -446,13 +590,20 @@ void* my_dlsym(void *handle, void *symbol){ #if 1 if(!dl->libs[nlib]->x86linkmap) { //redlopen - uint64_t ret = RunFunctionWithState((uintptr_t)my_context->dlprivate->x86dlopen, 2, dl->libs[nlib]->name, dl->libs[nlib]->x86dlopenflag); + kzt_guest_library_loader_scope_t scope = { 0 }; + uint64_t ret = run_guest_dlopen_scoped( + (uintptr_t)my_context->dlprivate->x86dlopen, + dl->libs[nlib]->name, dl->libs[nlib]->x86dlopenflag, + &scope); if (!ret) {//user sometime test for finding a func. + finish_guest_dlopen_scoped(&scope, 0, NULL, 0); printf_dlsym(LOG_NEVER, "redlopen %p return %p\n", rsymbol, (void*)NULL); return NULL; } lsassert(ret); dl->libs[nlib]->x86linkmap = (void *)ret; + finish_guest_dlopen_scoped( + &scope, (uintptr_t)ret, dl->libs[nlib], 1); ret = RunFunctionWithState((uintptr_t)my_context->dlprivate->x86dlsym, 2, dl->libs[nlib]->x86linkmap , cpu->regs[R_ESI]); printf_dlsym(LOG_DEBUG, "call x86dlsym filename %s is wrapped but not find symbol, dlsym(%p, %s) ret=0x%lx\n", dl->libs[nlib]->name, dl->libs[nlib]->x86linkmap, (char *)cpu->regs[R_ESI], ret); @@ -643,4 +794,3 @@ int my_dlinfo(void* handle, int request, void* info) } #include "wrappedlib_init.h" - diff --git a/target/i386/latx/include/box64context.h b/target/i386/latx/include/box64context.h index 489d1b79ad4..af02bfe7e37 100755 --- a/target/i386/latx/include/box64context.h +++ b/target/i386/latx/include/box64context.h @@ -5,6 +5,9 @@ #include "dictionnary.h" #include #include "debug.h" +#include "kzt_guest_library_binding.h" +#include "kzt_guest_registry_context.h" +#include "kzt_patch_spike_guard.h" typedef struct elfheader_s elfheader_t; typedef struct cleanup_s cleanup_t; @@ -346,12 +349,29 @@ typedef struct box64context_s { int latx_kzt_debugcap; int latx_kzt_debugsize; // number of latx_kzt_debug #endif +#ifdef CONFIG_LATX_KZT + kzt_guest_registry_context_t kzt_guest_registry_context; + kzt_guest_library_access_t kzt_guest_library_access; + kzt_patch_spike_guard_t kzt_patch_spike_guard; + pthread_mutex_t kzt_bridge_mutex; + uintptr_t kzt_lazy_completion_bridge; +#endif } box64context_t; extern box64context_t *my_context; // global context box64context_t *NewBox64Context(int argc); void FreeBox64Context(box64context_t** context); +#ifdef CONFIG_LATX_KZT +kzt_guest_registry_t *KztGuestRegistryForContext(box64context_t *context); +kzt_guest_library_bindings_t *KztGuestLibraryBindingsForContext( + box64context_t *context); +int KztGuestLibraryLookupForContext( + box64context_t *context, + const kzt_guest_library_binding_key_t *key, + kzt_guest_library_handle_t *handle); +kzt_patch_spike_guard_t *KztPatchSpikeGuardForContext(box64context_t *context); +#endif // return the index of the added header int AddElfHeader(box64context_t* ctx, elfheader_t* head); diff --git a/target/i386/latx/include/debug.h b/target/i386/latx/include/debug.h index 2f1ec47042f..8560d18bc5d 100755 --- a/target/i386/latx/include/debug.h +++ b/target/i386/latx/include/debug.h @@ -19,6 +19,7 @@ extern int box64_pagesize; extern uintptr_t box64_load_addr; extern int dlsym_error; // log dlsym error extern int kzt_call_log; +extern int kzt_registry_diagnostics; extern int allow_missing_libs; extern int box64_nogtk; extern int box64_prefer_wrapped; @@ -56,6 +57,19 @@ extern char* libGL; #define printf_dlsym(L, ...) ((void)0) #define printf_kzt_call(L, ...) ((void)0) #endif +#if defined(CONFIG_LATX_KZT) +#define kzt_registry_diagnostics_enabled() \ + (kzt_registry_diagnostics || (LOG_DEBUG <= relocation_log)) +#define printf_kzt_registry_diagnostics(...) \ + do { \ + if (kzt_registry_diagnostics_enabled()) { \ + fprintf(stderr, __VA_ARGS__); \ + } \ + } while (0) +#else +#define kzt_registry_diagnostics_enabled() 0 +#define printf_kzt_registry_diagnostics(...) ((void)0) +#endif #define EXPORT __attribute__((visibility("default"))) #define EXPORTDYN diff --git a/target/i386/latx/include/dlopen_recycle_transaction.h b/target/i386/latx/include/dlopen_recycle_transaction.h new file mode 100644 index 00000000000..89ac4df90f5 --- /dev/null +++ b/target/i386/latx/include/dlopen_recycle_transaction.h @@ -0,0 +1,16 @@ +#ifndef DLOPEN_RECYCLE_TRANSACTION_H +#define DLOPEN_RECYCLE_TRANSACTION_H + +#include + +typedef int (*dlopen_recycle_step_fn)(void *opaque); +typedef void (*dlopen_recycle_prepare_fn)(void *opaque); +typedef void (*dlopen_recycle_rollback_fn)(void *opaque, int guest_opened); + +void *dlopen_recycle_transaction( + void *success_handle, size_t *count, int increment_count, void *opaque, + dlopen_recycle_prepare_fn prepare, dlopen_recycle_step_fn guest_open, + dlopen_recycle_step_fn reload, + dlopen_recycle_rollback_fn rollback); + +#endif diff --git a/target/i386/latx/include/elf_plt_relocation.h b/target/i386/latx/include/elf_plt_relocation.h new file mode 100644 index 00000000000..49f0f860247 --- /dev/null +++ b/target/i386/latx/include/elf_plt_relocation.h @@ -0,0 +1,10 @@ +#ifndef ELF_PLT_RELOCATION_H +#define ELF_PLT_RELOCATION_H + +typedef int (*elf_plt_relocation_apply_fn)(void *opaque, + int *need_resolver); + +int elf_plt_relocation_apply(elf_plt_relocation_apply_fn apply, void *opaque, + int *need_resolver); + +#endif diff --git a/target/i386/latx/include/elfmap.h b/target/i386/latx/include/elfmap.h new file mode 100644 index 00000000000..9ef2d8f5f30 --- /dev/null +++ b/target/i386/latx/include/elfmap.h @@ -0,0 +1,21 @@ +#ifndef LATX_ELFMAP_H +#define LATX_ELFMAP_H + +#include +#include + +#include "elf.h" + +int GetElfLoadRange(const Elf64_Phdr *program_headers, + size_t program_header_count, + uintptr_t load_bias, + uintptr_t page_size, + uintptr_t *map_start, + uintptr_t *map_end); + +int GetElfDynamicAddress(const Elf64_Phdr *program_headers, + size_t program_header_count, + uintptr_t load_bias, + uintptr_t *dynamic_addr); + +#endif diff --git a/target/i386/latx/include/kzt_bridge_exact.h b/target/i386/latx/include/kzt_bridge_exact.h new file mode 100644 index 00000000000..42c9b027bef --- /dev/null +++ b/target/i386/latx/include/kzt_bridge_exact.h @@ -0,0 +1,11 @@ +#ifndef KZT_BRIDGE_EXACT_H +#define KZT_BRIDGE_EXACT_H + +#include + +typedef void (*kzt_bridge_wrapper_t)(uintptr_t fnc); + +int kzt_bridge_is_exact(uintptr_t target, kzt_bridge_wrapper_t wrapper, + void *native_symbol); + +#endif diff --git a/target/i386/latx/include/kzt_guest_dynamic.h b/target/i386/latx/include/kzt_guest_dynamic.h new file mode 100644 index 00000000000..69d2579414a --- /dev/null +++ b/target/i386/latx/include/kzt_guest_dynamic.h @@ -0,0 +1,47 @@ +#ifndef KZT_GUEST_DYNAMIC_H +#define KZT_GUEST_DYNAMIC_H + +#include +#include + +#include "elf.h" +#include "kzt_guest_dynamic_view.h" +#include "kzt_guest_link_map_reader.h" + +#ifndef DT_GNU_HASH +#define DT_GNU_HASH 0x6ffffef5 +#endif + + +typedef enum kzt_guest_dynamic_error { + KZT_GUEST_DYNAMIC_ERROR_NONE = 0, + KZT_GUEST_DYNAMIC_ERROR_INVALID_ARGUMENT, + KZT_GUEST_DYNAMIC_ERROR_ALLOCATION_FAILURE, + KZT_GUEST_DYNAMIC_ERROR_READ_FAILURE, + KZT_GUEST_DYNAMIC_ERROR_SCAN_LIMIT_EXCEEDED, + KZT_GUEST_DYNAMIC_ERROR_TOO_MANY_NEEDED, +} kzt_guest_dynamic_error_t; + +typedef struct kzt_guest_dynamic_parse_result { + kzt_guest_dynamic_status_t status; + kzt_guest_dynamic_error_t error; + size_t entry_count; + uintptr_t read_error_addr; + size_t scan_limit; + size_t unknown_tag_count; + int64_t first_unknown_tag; + size_t first_unknown_tag_index; + kzt_guest_dynamic_view_t view; +} kzt_guest_dynamic_parse_result_t; + +int kzt_guest_dynamic_parse( + uintptr_t dynamic_addr, + uintptr_t load_bias, + const kzt_guest_link_map_reader_ops_t *reader_ops, + kzt_guest_dynamic_parse_result_t *result); + +void kzt_guest_dynamic_view_destroy(kzt_guest_dynamic_view_t *view); +void kzt_guest_dynamic_parse_result_clear( + kzt_guest_dynamic_parse_result_t *result); + +#endif diff --git a/target/i386/latx/include/kzt_guest_dynamic_diagnostics.h b/target/i386/latx/include/kzt_guest_dynamic_diagnostics.h new file mode 100644 index 00000000000..1225ff90a5e --- /dev/null +++ b/target/i386/latx/include/kzt_guest_dynamic_diagnostics.h @@ -0,0 +1,130 @@ +#ifndef KZT_GUEST_DYNAMIC_DIAGNOSTICS_H +#define KZT_GUEST_DYNAMIC_DIAGNOSTICS_H + +#include +#include + +#include "kzt_guest_dynamic.h" + +#define KZT_GUEST_DYNAMIC_DIAGNOSTIC_FIELD_LIMIT 32 + +typedef enum kzt_guest_dynamic_diagnostic_match { + KZT_GUEST_DYNAMIC_DIAGNOSTIC_MATCHED = 0, + KZT_GUEST_DYNAMIC_DIAGNOSTIC_MISSING_OLD, + KZT_GUEST_DYNAMIC_DIAGNOSTIC_MISSING_NEW, + KZT_GUEST_DYNAMIC_DIAGNOSTIC_MISMATCH, +} kzt_guest_dynamic_diagnostic_match_t; + +typedef enum kzt_guest_dynamic_diagnostic_difference_kind { + KZT_GUEST_DYNAMIC_DIAGNOSTIC_DIFFERENCE_NONE = 0, + KZT_GUEST_DYNAMIC_DIAGNOSTIC_DIFFERENCE_STATUS, + KZT_GUEST_DYNAMIC_DIAGNOSTIC_DIFFERENCE_ENTRY_COUNT, + KZT_GUEST_DYNAMIC_DIAGNOSTIC_DIFFERENCE_UNKNOWN_TAGS, + KZT_GUEST_DYNAMIC_DIAGNOSTIC_DIFFERENCE_FIELD, +} kzt_guest_dynamic_diagnostic_difference_kind_t; + +typedef struct kzt_guest_dynamic_diagnostic_field { + const char *name; + kzt_guest_dynamic_diagnostic_match_t match; + int old_present; + uint64_t old_value; + kzt_guest_dynamic_address_semantics_t old_address_semantics; + size_t old_count; + int new_present; + uint64_t new_value; + kzt_guest_dynamic_address_semantics_t new_address_semantics; + size_t new_count; +} kzt_guest_dynamic_diagnostic_field_t; + +typedef struct kzt_guest_dynamic_diagnostic_report { + kzt_guest_dynamic_status_t old_status; + kzt_guest_dynamic_status_t new_status; + kzt_guest_dynamic_error_t old_error; + kzt_guest_dynamic_error_t new_error; + uintptr_t old_read_error_addr; + uintptr_t new_read_error_addr; + size_t old_entry_count; + size_t new_entry_count; + size_t old_unknown_tag_count; + size_t new_unknown_tag_count; + int64_t old_first_unknown_tag; + int64_t new_first_unknown_tag; + size_t old_first_unknown_tag_index; + size_t new_first_unknown_tag_index; + int old_truncated; + int new_truncated; + int old_read_error; + int new_read_error; + + kzt_guest_dynamic_diagnostic_match_t status_match; + kzt_guest_dynamic_diagnostic_match_t entry_count_match; + kzt_guest_dynamic_diagnostic_match_t unknown_tags_match; + + size_t field_count; + size_t matched_count; + size_t missing_old_count; + size_t missing_new_count; + size_t mismatch_count; + size_t difference_count; + size_t blocking_count; + + kzt_guest_dynamic_diagnostic_field_t + fields[KZT_GUEST_DYNAMIC_DIAGNOSTIC_FIELD_LIMIT]; +} kzt_guest_dynamic_diagnostic_report_t; + +typedef struct kzt_guest_dynamic_diagnostic_summary { + uintptr_t link_map_addr; + unsigned long generation; + int matched; + int blocking; + size_t difference_count; + size_t blocking_count; + + kzt_guest_dynamic_status_t old_status; + kzt_guest_dynamic_status_t new_status; + size_t old_entry_count; + size_t new_entry_count; + size_t old_unknown_tag_count; + size_t new_unknown_tag_count; + int64_t old_first_unknown_tag; + int64_t new_first_unknown_tag; + size_t old_first_unknown_tag_index; + size_t new_first_unknown_tag_index; + + kzt_guest_dynamic_diagnostic_difference_kind_t first_difference_kind; + const char *first_difference_name; + kzt_guest_dynamic_diagnostic_match_t first_difference_match; + int first_old_present; + int first_new_present; + uint64_t first_old_value; + uint64_t first_new_value; + size_t first_old_count; + size_t first_new_count; + int64_t first_old_tag; + int64_t first_new_tag; + size_t first_old_tag_index; + size_t first_new_tag_index; +} kzt_guest_dynamic_diagnostic_summary_t; + +int kzt_guest_dynamic_diagnostics_summarize( + const kzt_guest_dynamic_diagnostic_report_t *report, + uintptr_t link_map_addr, + unsigned long generation, + kzt_guest_dynamic_diagnostic_summary_t *summary); + +int kzt_guest_dynamic_diagnostics_compare( + const kzt_guest_dynamic_parse_result_t *old_result, + const kzt_guest_dynamic_parse_result_t *new_result, + kzt_guest_dynamic_diagnostic_report_t *report); + +const kzt_guest_dynamic_diagnostic_field_t * +kzt_guest_dynamic_diagnostic_find_field( + const kzt_guest_dynamic_diagnostic_report_t *report, + const char *name); + +int kzt_guest_dynamic_diagnostics_format_summary( + const kzt_guest_dynamic_diagnostic_summary_t *summary, + char *buffer, + size_t buffer_size); + +#endif diff --git a/target/i386/latx/include/kzt_guest_dynamic_view.h b/target/i386/latx/include/kzt_guest_dynamic_view.h new file mode 100644 index 00000000000..0bd61f53032 --- /dev/null +++ b/target/i386/latx/include/kzt_guest_dynamic_view.h @@ -0,0 +1,68 @@ +#ifndef KZT_GUEST_DYNAMIC_VIEW_H +#define KZT_GUEST_DYNAMIC_VIEW_H + +#include +#include + +#define KZT_GUEST_DYNAMIC_SCAN_LIMIT 512 +#define KZT_GUEST_DYNAMIC_NEEDED_LIMIT 32 + +typedef enum kzt_guest_dynamic_status { + KZT_GUEST_DYNAMIC_COMPLETE = 0, + KZT_GUEST_DYNAMIC_TRUNCATED_NO_NULL, + KZT_GUEST_DYNAMIC_READ_ERROR, + KZT_GUEST_DYNAMIC_ERROR, +} kzt_guest_dynamic_status_t; + +typedef enum kzt_guest_dynamic_address_semantics { + KZT_GUEST_DYNAMIC_ADDRESS_UNKNOWN = 0, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS, + KZT_GUEST_DYNAMIC_STRING_TABLE_OFFSET, + KZT_GUEST_DYNAMIC_SCALAR, +} kzt_guest_dynamic_address_semantics_t; + +typedef struct kzt_guest_dynamic_field { + int present; + uint64_t value; + kzt_guest_dynamic_address_semantics_t address_semantics; +} kzt_guest_dynamic_field_t; + +typedef struct kzt_guest_dynamic_view { + uintptr_t dynamic_addr; + uintptr_t load_bias; + kzt_guest_dynamic_status_t status; + size_t entry_count; + int has_null; + size_t scan_limit; + size_t unknown_tag_count; + int64_t first_unknown_tag; + size_t first_unknown_tag_index; + + kzt_guest_dynamic_field_t symtab; + kzt_guest_dynamic_field_t strtab; + kzt_guest_dynamic_field_t syment; + kzt_guest_dynamic_field_t strsz; + kzt_guest_dynamic_field_t hash; + kzt_guest_dynamic_field_t gnu_hash; + kzt_guest_dynamic_field_t versym; + kzt_guest_dynamic_field_t verneed; + kzt_guest_dynamic_field_t verneednum; + kzt_guest_dynamic_field_t verdef; + kzt_guest_dynamic_field_t verdefnum; + kzt_guest_dynamic_field_t rela; + kzt_guest_dynamic_field_t relasz; + kzt_guest_dynamic_field_t relaent; + kzt_guest_dynamic_field_t rel; + kzt_guest_dynamic_field_t relsz; + kzt_guest_dynamic_field_t relent; + kzt_guest_dynamic_field_t jmprel; + kzt_guest_dynamic_field_t pltrelsz; + kzt_guest_dynamic_field_t pltrel; + kzt_guest_dynamic_field_t pltgot; + + uint64_t needed_offsets[KZT_GUEST_DYNAMIC_NEEDED_LIMIT]; + size_t needed_count; + kzt_guest_dynamic_address_semantics_t needed_address_semantics; +} kzt_guest_dynamic_view_t; + +#endif diff --git a/target/i386/latx/include/kzt_guest_library_adapter.h b/target/i386/latx/include/kzt_guest_library_adapter.h new file mode 100644 index 00000000000..04138e188df --- /dev/null +++ b/target/i386/latx/include/kzt_guest_library_adapter.h @@ -0,0 +1,29 @@ +#ifndef KZT_GUEST_LIBRARY_ADAPTER_H +#define KZT_GUEST_LIBRARY_ADAPTER_H + +#include + +#include "kzt_loader_callback_scope.h" + +typedef struct box64context_s box64context_t; +typedef struct library_s library_t; + +/* Shared production adapter used by wrappedlibc, wrappedlibdl, and loader + * callback paths whenever one operation owns both exact values. */ +void kzt_guest_library_note_loader_pair(box64context_t *context, + uintptr_t link_map_addr, + library_t *library); +void kzt_guest_library_note_loader_pair_pending( + box64context_t *context, + const kzt_guest_library_loader_scope_t *scope, + uintptr_t link_map_addr, library_t *library); +void kzt_guest_library_publish_loader_pair_scoped( + box64context_t *context, + const kzt_guest_library_loader_scope_t *scope, + uintptr_t link_map_addr, library_t *library); +void kzt_guest_library_publish_loader_observed_scoped( + box64context_t *context, + const kzt_guest_library_loader_scope_t *scope, + uintptr_t link_map_addr); + +#endif diff --git a/target/i386/latx/include/kzt_guest_library_binding.h b/target/i386/latx/include/kzt_guest_library_binding.h new file mode 100644 index 00000000000..ec5946f5cd0 --- /dev/null +++ b/target/i386/latx/include/kzt_guest_library_binding.h @@ -0,0 +1,214 @@ +#ifndef KZT_GUEST_LIBRARY_BINDING_H +#define KZT_GUEST_LIBRARY_BINDING_H + +#include +#include +#include + +#include "kzt_loader_callback_scope.h" + +typedef struct library_s library_t; +typedef struct kzt_guest_registry kzt_guest_registry_t; +typedef struct kzt_guest_library_bindings kzt_guest_library_bindings_t; + +/* Embedded in box64context_t. This is the only lifetime gate for lookup: + * context teardown closes it before waiting for lookup handles. */ +typedef struct kzt_guest_library_access { + pthread_mutex_t lock; + kzt_guest_library_bindings_t *bindings; + int accepting; + int initialized; +} kzt_guest_library_access_t; + +typedef enum kzt_guest_library_object_type { + KZT_GUEST_LIBRARY_OBJECT_MAIN = 0, + KZT_GUEST_LIBRARY_OBJECT_WRAPPED, + KZT_GUEST_LIBRARY_OBJECT_EMULATED, + KZT_GUEST_LIBRARY_OBJECT_UNSUPPORTED, +} kzt_guest_library_object_type_t; + +typedef enum kzt_guest_library_namespace_kind { + KZT_GUEST_LIBRARY_NAMESPACE_MAIN = 0, + KZT_GUEST_LIBRARY_NAMESPACE_EXPLICIT, + KZT_GUEST_LIBRARY_NAMESPACE_UNSUPPORTED, +} kzt_guest_library_namespace_kind_t; + +typedef enum kzt_guest_library_binding_state { + KZT_GUEST_LIBRARY_BINDING_LIVE = 0, + KZT_GUEST_LIBRARY_BINDING_UNLOADING, + KZT_GUEST_LIBRARY_BINDING_DEAD, +} kzt_guest_library_binding_state_t; + +typedef enum kzt_guest_library_binding_result { + KZT_GUEST_LIBRARY_BINDING_ADDED = 0, + KZT_GUEST_LIBRARY_BINDING_UNCHANGED, + KZT_GUEST_LIBRARY_BINDING_PENDING, + KZT_GUEST_LIBRARY_BINDING_CANCELLED, + KZT_GUEST_LIBRARY_BINDING_RETIRE_OWNED, + KZT_GUEST_LIBRARY_BINDING_CONFLICT, + KZT_GUEST_LIBRARY_BINDING_DISABLED, + KZT_GUEST_LIBRARY_BINDING_ERROR, +} kzt_guest_library_binding_result_t; + +typedef struct kzt_guest_library_binding_key { + uintptr_t link_map_addr; + unsigned long generation; + uintptr_t namespace_id; + kzt_guest_library_namespace_kind_t namespace_kind; +} kzt_guest_library_binding_key_t; + +typedef struct kzt_guest_library_handle { + kzt_guest_library_bindings_t *bindings; + void *entry; + library_t *library; + kzt_guest_library_object_type_t object_type; +} kzt_guest_library_handle_t; + +typedef struct kzt_guest_library_callback_access { + kzt_guest_library_bindings_t *bindings; + uintptr_t link_map_addr; + void *gate; + int fallback; +} kzt_guest_library_callback_access_t; + +kzt_guest_library_bindings_t *kzt_guest_library_bindings_init(void); +void kzt_guest_library_bindings_begin_teardown( + kzt_guest_library_bindings_t *bindings); +void kzt_guest_library_bindings_destroy(kzt_guest_library_bindings_t **bindings); + +int kzt_guest_library_access_init(kzt_guest_library_access_t *access); +void kzt_guest_library_access_begin_teardown( + kzt_guest_library_access_t *access); +void kzt_guest_library_access_destroy(kzt_guest_library_access_t *access); + +/* Libraries must be tracked before they become visible to loader users. If + * tracking cannot be established, exact binding remains disabled for that + * library and the legacy loader continues unchanged. */ +int kzt_guest_library_track(kzt_guest_library_bindings_t *bindings, + library_t *library); +int kzt_guest_library_reactivate(kzt_guest_library_bindings_t *bindings, + library_t *library); + +/* Guest loader calls are synchronous. A context-local scope proves that a + * callback came from a loader invocation issued after an older address was + * closed. note_pair only prepares a scope-local pair and never makes it + * visible to lookup; publish_pair commits it after the loader succeeds. + * Ending a scope without publication cancels every prepared pair. */ +int kzt_guest_library_loader_scope_begin( + kzt_guest_library_bindings_t *bindings, + kzt_guest_library_loader_scope_t *scope); +void kzt_guest_library_loader_scope_end( + kzt_guest_library_loader_scope_t *scope); +kzt_guest_library_binding_result_t +kzt_guest_library_loader_scope_note_pair( + const kzt_guest_library_loader_scope_t *scope, + uintptr_t link_map_addr, library_t *library, + kzt_guest_library_object_type_t object_type); +kzt_guest_library_binding_result_t +kzt_guest_library_loader_scope_publish_pair( + const kzt_guest_library_loader_scope_t *scope, + uintptr_t link_map_addr, library_t *library, + kzt_guest_library_object_type_t object_type); +kzt_guest_library_binding_result_t +kzt_guest_library_loader_scope_publish_observed( + const kzt_guest_library_loader_scope_t *scope, + uintptr_t link_map_addr); + +/* Pins one link_map address before the callback's first guest-memory read and + * through dynamic parsing, legacy processing, and diagnostics. Unload closes + * that address under the same lock and waits for every admitted callback. + * A closed address rejects late callbacks before any guest work. */ +int kzt_guest_library_callback_access_begin( + kzt_guest_library_bindings_t *bindings, uintptr_t link_map_addr, + kzt_guest_library_callback_access_t *access); +int kzt_guest_library_callback_access_begin_scoped( + kzt_guest_library_bindings_t *bindings, uintptr_t link_map_addr, + const kzt_guest_library_loader_scope_t *scope, + kzt_guest_library_callback_access_t *access); +void kzt_guest_library_callback_access_end( + kzt_guest_library_callback_access_t *access); + +/* Record the exact pair produced by one causal loader operation. This never + * searches by name/path/SONAME. A pair remains pending until registry + * observation supplies its generation, so either arrival order works. */ +kzt_guest_library_binding_result_t kzt_guest_library_note_exact_pair( + kzt_guest_library_bindings_t *bindings, + uintptr_t link_map_addr, + library_t *library, + kzt_guest_library_object_type_t object_type); + +/* The caller must use only the exact (address, library) returned by one + * successfully completed guest loader operation. This causal publication + * may reopen an address whose replacement is now known valid. */ +kzt_guest_library_binding_result_t kzt_guest_library_publish_loader_pair( + kzt_guest_library_bindings_t *bindings, uintptr_t link_map_addr, + library_t *library, kzt_guest_library_object_type_t object_type); + +/* Publish successful registry evidence to the exact-pair handshake. WI-254 + * intentionally supports only ordinary shared objects in LM_ID_BASE. Main + * executable objects and non-main namespaces fail open. */ +kzt_guest_library_binding_result_t kzt_guest_library_note_observation( + kzt_guest_library_bindings_t *bindings, + const kzt_guest_library_binding_key_t *key); + +kzt_guest_library_binding_result_t kzt_guest_library_bind( + kzt_guest_library_bindings_t *bindings, + const kzt_guest_library_binding_key_t *key, + library_t *library, + kzt_guest_library_object_type_t object_type); + +/* Production lookup must enter through the box64context-owned access object. + * The raw lookup is exposed only for binding white-box tests, where the test + * owns and stops all callers before destroying bindings. */ +int kzt_guest_library_access_lookup( + kzt_guest_library_access_t *access, + const kzt_guest_library_binding_key_t *key, + kzt_guest_library_handle_t *handle); +#ifdef KZT_GUEST_LIBRARY_BINDING_TEST +int kzt_guest_library_lookup(kzt_guest_library_bindings_t *bindings, + const kzt_guest_library_binding_key_t *key, + kzt_guest_library_handle_t *handle); +#endif +void kzt_guest_library_handle_release(kzt_guest_library_handle_t *handle); + +/* Closes attachment under the bindings lock, retires exact registry + * generations without that lock, then waits for acquired lookup handles. + * guest_link_map_hint identifies only this library's unclaimed observation; + * a missing hint leaves unclaimed observations live rather than guessing. + * This is the only permitted order: access -> bindings for lookup entry; + * registry is never entered while bindings is held; handle release takes only + * bindings. */ +void kzt_guest_library_unbind(kzt_guest_library_bindings_t *bindings, + kzt_guest_registry_t *registry, + library_t *library, + uintptr_t guest_link_map_hint); +void kzt_guest_library_inactivate(kzt_guest_library_bindings_t *bindings, + kzt_guest_registry_t *registry, + library_t *library, + uintptr_t guest_link_map_hint); + +#ifdef KZT_GUEST_LIBRARY_BINDING_TEST +typedef void (*kzt_guest_library_binding_test_retire_fn)( + kzt_guest_library_bindings_t *bindings, + const kzt_guest_library_binding_key_t *key, + library_t *library, int from_observation, void *opaque); +typedef void (*kzt_guest_library_binding_test_lifecycle_wait_fn)( + kzt_guest_library_bindings_t *bindings, library_t *library, + void *opaque); + +void kzt_guest_library_binding_test_set_alloc_failure_after(long allocations); +void kzt_guest_library_binding_test_set_before_registry_retire( + kzt_guest_library_binding_test_retire_fn hook, void *opaque); +void kzt_guest_library_binding_test_set_before_lifecycle_wait( + kzt_guest_library_binding_test_lifecycle_wait_fn hook, void *opaque); +int kzt_guest_library_binding_test_snapshot( + kzt_guest_library_bindings_t *bindings, library_t *library, + kzt_guest_library_binding_state_t *lifecycle_state, + size_t *active_pending, size_t *live_entries); +int kzt_guest_library_binding_test_get_diagnostics( + kzt_guest_library_bindings_t *bindings, + unsigned long *registry_missing, + unsigned long *retire_unprovable); +#endif + +#endif diff --git a/target/i386/latx/include/kzt_guest_link_map_reader.h b/target/i386/latx/include/kzt_guest_link_map_reader.h new file mode 100644 index 00000000000..cdd362569bf --- /dev/null +++ b/target/i386/latx/include/kzt_guest_link_map_reader.h @@ -0,0 +1,72 @@ +#ifndef KZT_GUEST_LINK_MAP_READER_H +#define KZT_GUEST_LINK_MAP_READER_H + +#include +#include + +#include "kzt_guest_registry.h" + +#define KZT_GUEST_LINK_MAP_NAME_LIMIT 4096 + +typedef int (*kzt_guest_memory_read_fn)(uintptr_t guest_addr, + void *dst, + size_t size, + void *opaque); + +typedef struct kzt_guest_link_map_reader_ops { + kzt_guest_memory_read_fn read_memory; + void *opaque; +} kzt_guest_link_map_reader_ops_t; + +typedef struct kzt_guest_link_map_identity { + uintptr_t load_bias; + uintptr_t dynamic_addr; +} kzt_guest_link_map_identity_t; + +int kzt_guest_link_map_read_identity( + uintptr_t link_map_addr, + const kzt_guest_link_map_reader_ops_t *ops, + kzt_guest_link_map_identity_t *identity); + +int kzt_guest_link_map_identity_matches( + const kzt_guest_link_map_identity_t *identity, + uintptr_t expected_load_bias, + uintptr_t expected_dynamic_addr); + +int kzt_guest_link_map_read_predecessor( + uintptr_t link_map_addr, + const kzt_guest_link_map_reader_ops_t *ops, + uintptr_t *predecessor); + +/* Walk the public x86_64 link_map l_prev chain to its exact head. With a + * previously confirmed main head, classification is based only on head + * identity. Otherwise both l_addr and l_ld must match the main executable. + * Returns 1 for main, 0 for a complete non-main chain, and -1 when unknown. */ +int kzt_guest_link_map_classify_namespace( + uintptr_t link_map_addr, + const kzt_guest_link_map_identity_t *main_identity, + uintptr_t confirmed_main_head, + const kzt_guest_link_map_reader_ops_t *ops, + uintptr_t *namespace_head); + +int kzt_guest_link_map_read_name_snapshot( + uintptr_t guest_name_addr, + const kzt_guest_link_map_reader_ops_t *ops, + size_t max_len, + kzt_guest_string_field_t *name); + +int kzt_guest_link_map_read_observation( + uintptr_t link_map_addr, + const kzt_guest_link_map_reader_ops_t *ops, + kzt_guest_object_observation_t *observation); + +void kzt_guest_link_map_observation_clear( + kzt_guest_object_observation_t *observation); + +void kzt_guest_link_map_string_clear(kzt_guest_string_field_t *field); + +#ifdef KZT_GUEST_LINK_MAP_READER_TEST +void kzt_guest_link_map_reader_test_set_alloc_failure_after(long allocations); +#endif + +#endif diff --git a/target/i386/latx/include/kzt_guest_registry.h b/target/i386/latx/include/kzt_guest_registry.h new file mode 100644 index 00000000000..2b8140e6e1d --- /dev/null +++ b/target/i386/latx/include/kzt_guest_registry.h @@ -0,0 +1,269 @@ +#ifndef KZT_GUEST_REGISTRY_H +#define KZT_GUEST_REGISTRY_H + +#include +#include + +#include "kzt_guest_dynamic_view.h" + +typedef struct kzt_guest_registry kzt_guest_registry_t; + +typedef struct kzt_guest_registry_source_lease { + kzt_guest_registry_t *registry; + uintptr_t link_map_addr; + unsigned long generation; + uintptr_t namespace_id; + int active; +} kzt_guest_registry_source_lease_t; + +typedef enum kzt_guest_object_state { + KZT_GUEST_OBJECT_DISCOVERED = 0, + KZT_GUEST_OBJECT_PARSED, + KZT_GUEST_OBJECT_WRAPPER_READY, + KZT_GUEST_OBJECT_PATCHED, + KZT_GUEST_OBJECT_UNLOADING, + KZT_GUEST_OBJECT_DEAD, +} kzt_guest_object_state_t; + +typedef enum kzt_guest_field_status { + KZT_GUEST_FIELD_OK = 0, + KZT_GUEST_FIELD_UNKNOWN, + KZT_GUEST_FIELD_READ_ERROR, + KZT_GUEST_FIELD_TRUNCATED, + KZT_GUEST_FIELD_NOT_PARSED, +} kzt_guest_field_status_t; + +typedef enum kzt_guest_registry_result { + KZT_GUEST_REGISTRY_ADDED = 0, + KZT_GUEST_REGISTRY_UNCHANGED, + KZT_GUEST_REGISTRY_UPDATED, + KZT_GUEST_REGISTRY_CONFLICT, + KZT_GUEST_REGISTRY_DISABLED, + KZT_GUEST_REGISTRY_ERROR, + KZT_GUEST_REGISTRY_RESULT_COUNT, +} kzt_guest_registry_result_t; + +typedef struct kzt_guest_scalar_field { + uintptr_t value; + kzt_guest_field_status_t status; +} kzt_guest_scalar_field_t; + +typedef struct kzt_guest_string_field { + const char *value; + kzt_guest_field_status_t status; +} kzt_guest_string_field_t; + +typedef struct kzt_guest_lazy_resolver { + uintptr_t link_map_slot; + uintptr_t resolver_slot; + uintptr_t guest_link_map; + uintptr_t guest_resolver; + int valid; +} kzt_guest_lazy_resolver_t; + +typedef struct kzt_guest_object_observation { + uintptr_t link_map_addr; + kzt_guest_scalar_field_t load_bias; + kzt_guest_scalar_field_t dynamic_addr; + kzt_guest_scalar_field_t map_start; + kzt_guest_scalar_field_t map_end; + kzt_guest_scalar_field_t namespace_id; + kzt_guest_string_field_t path; + kzt_guest_string_field_t soname; + kzt_guest_field_status_t dynamic_view_status; +} kzt_guest_object_observation_t; + +typedef struct kzt_guest_object_snapshot { + uintptr_t link_map_addr; + kzt_guest_scalar_field_t load_bias; + kzt_guest_scalar_field_t dynamic_addr; + kzt_guest_scalar_field_t map_start; + kzt_guest_scalar_field_t map_end; + kzt_guest_scalar_field_t namespace_id; + kzt_guest_string_field_t path; + kzt_guest_string_field_t soname; + kzt_guest_field_status_t dynamic_view_status; + kzt_guest_dynamic_view_t dynamic_view; + kzt_guest_lazy_resolver_t lazy_resolver; + kzt_guest_object_state_t state; + unsigned long generation; + unsigned long active_source_leases; +} kzt_guest_object_snapshot_t; + +typedef struct kzt_guest_registry_dump { + kzt_guest_object_snapshot_t *objects; + size_t count; +} kzt_guest_registry_dump_t; + +typedef struct kzt_guest_registry_diagnostics { + unsigned long observations; + unsigned long added; + unsigned long unchanged; + unsigned long updated; + unsigned long conflicts; + unsigned long disabled; + unsigned long errors; + unsigned long init_failures; + unsigned long allocation_failures; +} kzt_guest_registry_diagnostics_t; + +typedef struct kzt_guest_registry_diagnostic_config { + int enabled; + unsigned long throttle_limit; +} kzt_guest_registry_diagnostic_config_t; + +typedef struct kzt_guest_registry_observation_diagnostic { + int enabled; + int emitted; + kzt_guest_registry_result_t result; + uintptr_t link_map_addr; + unsigned long generation; + unsigned long object_count; + unsigned long result_observations; + unsigned long result_suppressed; + kzt_guest_registry_diagnostics_t counters; +} kzt_guest_registry_observation_diagnostic_t; + +typedef struct kzt_guest_registry_event_summary { + kzt_guest_registry_result_t result; + unsigned long observed; + unsigned long emitted; + unsigned long suppressed; + uintptr_t last_link_map_addr; + unsigned long last_generation; +} kzt_guest_registry_event_summary_t; + +typedef struct kzt_guest_registry_diagnostic_report { + kzt_guest_registry_diagnostic_config_t config; + kzt_guest_registry_diagnostics_t counters; + kzt_guest_registry_event_summary_t events[KZT_GUEST_REGISTRY_RESULT_COUNT]; + size_t event_count; +} kzt_guest_registry_diagnostic_report_t; + +typedef int (*kzt_guest_registry_dump_sink_fn)(const char *line, + void *opaque); + +kzt_guest_registry_t *kzt_guest_registry_init(void); +/* The owner must stop starting new registry calls before destroy begins. + * Calls that have entered the API, including cond waiters and active source + * leases, are drained before the mutex/condition and registry storage die. */ +void kzt_guest_registry_destroy(kzt_guest_registry_t **registry); + +#ifdef KZT_GUEST_REGISTRY_TEST +typedef void (*kzt_guest_registry_test_hook_fn)(void *opaque); +void kzt_guest_registry_test_set_after_api_enter( + kzt_guest_registry_test_hook_fn hook, void *opaque); +void kzt_guest_registry_test_set_before_retire_wait( + kzt_guest_registry_test_hook_fn hook, void *opaque); +void kzt_guest_registry_test_set_after_retire_wake( + kzt_guest_registry_test_hook_fn hook, void *opaque); +void kzt_guest_registry_test_set_after_destroy_disable( + kzt_guest_registry_test_hook_fn hook, void *opaque); +#endif + +kzt_guest_registry_result_t kzt_guest_registry_observe( + kzt_guest_registry_t *registry, + const kzt_guest_object_observation_t *observation); + +kzt_guest_registry_result_t kzt_guest_registry_observe_with_diagnostic( + kzt_guest_registry_t *registry, + const kzt_guest_object_observation_t *observation, + kzt_guest_registry_observation_diagnostic_t *diagnostic); + +int kzt_guest_registry_find_by_link_map( + kzt_guest_registry_t *registry, + uintptr_t link_map_addr, + kzt_guest_object_snapshot_t **snapshot); + +int kzt_guest_registry_retire(kzt_guest_registry_t *registry, + uintptr_t link_map_addr, + unsigned long generation); +/* Defensive handoff for a violated single-retire protocol: wait for the + * already-started exact generation to become DEAD instead of allowing its + * binding owner to return before source leases drain. */ +int kzt_guest_registry_wait_retired(kzt_guest_registry_t *registry, + uintptr_t link_map_addr, + unsigned long generation); + +/* Pin an exact live (link_map, generation, namespace) source across a writer + * transaction. Only the main namespace is currently supported. Retire has + * a single owner: it rejects an already UNLOADING/DEAD generation, then waits + * for every active lease before completing DEAD. The guest loader therefore + * cannot unmap source slots while a lease is held. Guest memory must never be + * accessed while the registry mutex is held. */ +int kzt_guest_registry_source_lease_acquire( + kzt_guest_registry_t *registry, + uintptr_t link_map_addr, + unsigned long generation, + uintptr_t namespace_id, + kzt_guest_registry_source_lease_t *lease); +void kzt_guest_registry_source_lease_release( + kzt_guest_registry_source_lease_t *lease); + +kzt_guest_registry_result_t kzt_guest_registry_commit_dynamic_view( + kzt_guest_registry_t *registry, + uintptr_t link_map_addr, + unsigned long generation, + const kzt_guest_dynamic_view_t *view); + +int kzt_guest_registry_find_dynamic_view( + kzt_guest_registry_t *registry, + uintptr_t link_map_addr, + kzt_guest_dynamic_view_t *view, + kzt_guest_field_status_t *status, + unsigned long *generation); + +/* Resolver metadata is published only while the object is not callable. + * The generation and namespace checks make stale/non-main observations fail + * open without changing the guest resolver slots. */ +int kzt_guest_registry_publish_lazy_resolver( + kzt_guest_registry_t *registry, + uintptr_t link_map_addr, + unsigned long generation, + uintptr_t namespace_id, + const kzt_guest_lazy_resolver_t *resolver); + +int kzt_guest_registry_find_lazy_resolver( + kzt_guest_registry_t *registry, + uintptr_t link_map_addr, + unsigned long generation, + uintptr_t namespace_id, + kzt_guest_lazy_resolver_t *resolver); + +int kzt_guest_registry_dump_snapshot( + kzt_guest_registry_t *registry, + kzt_guest_registry_dump_t *dump); + +int kzt_guest_registry_get_diagnostics( + kzt_guest_registry_t *registry, + kzt_guest_registry_diagnostics_t *diagnostics); + +int kzt_guest_registry_configure_diagnostics( + kzt_guest_registry_t *registry, + const kzt_guest_registry_diagnostic_config_t *config); + +int kzt_guest_registry_get_diagnostic_report( + kzt_guest_registry_t *registry, + kzt_guest_registry_diagnostic_report_t *report); + +int kzt_guest_registry_note_diagnostic( + kzt_guest_registry_t *registry, + kzt_guest_registry_result_t result, + uintptr_t link_map_addr, + kzt_guest_registry_observation_diagnostic_t *diagnostic); + +int kzt_guest_registry_dump_text( + kzt_guest_registry_t *registry, + kzt_guest_registry_dump_sink_fn sink, + void *opaque); + +void kzt_guest_object_snapshot_free(kzt_guest_object_snapshot_t *snapshot); +void kzt_guest_registry_dump_free(kzt_guest_registry_dump_t *dump); + +#ifdef KZT_GUEST_REGISTRY_TEST +void kzt_guest_registry_test_set_alloc_failure_after(long allocations); +void kzt_guest_registry_test_set_dynamic_commit_failure_after(long commits); +void kzt_guest_registry_test_fail_next_cond_init(void); +#endif + +#endif diff --git a/target/i386/latx/include/kzt_guest_registry_context.h b/target/i386/latx/include/kzt_guest_registry_context.h new file mode 100644 index 00000000000..c0e020623d1 --- /dev/null +++ b/target/i386/latx/include/kzt_guest_registry_context.h @@ -0,0 +1,42 @@ +#ifndef KZT_GUEST_REGISTRY_CONTEXT_H +#define KZT_GUEST_REGISTRY_CONTEXT_H + +#include + +#include "kzt_guest_registry.h" + +typedef struct kzt_guest_registry_context { + kzt_guest_registry_t *registry; + int state; + uintptr_t main_namespace_head; +} kzt_guest_registry_context_t; + +/* The caller owns the surrounding context lifetime. The context lock is + * used only for first publication and teardown; established lookups use an + * atomic fast path and registry calls never run while the context lock is + * held. */ +kzt_guest_registry_t *kzt_guest_registry_context_get( + kzt_guest_registry_context_t *context, + pthread_mutex_t *context_lock); + +void kzt_guest_registry_context_destroy( + kzt_guest_registry_context_t *context, + pthread_mutex_t *context_lock); + +int kzt_guest_registry_context_get_main_namespace_head( + const kzt_guest_registry_context_t *context, + uintptr_t *head); + +int kzt_guest_registry_context_confirm_main_namespace_head( + kzt_guest_registry_context_t *context, + pthread_mutex_t *context_lock, + uintptr_t head); + +int kzt_guest_registry_context_has_main_namespace_evidence( + const kzt_guest_registry_context_t *context, + kzt_guest_registry_t *registry, + uintptr_t link_map_addr, + uintptr_t load_bias, + uintptr_t dynamic_addr); + +#endif diff --git a/target/i386/latx/include/kzt_jump_slot_production.h b/target/i386/latx/include/kzt_jump_slot_production.h new file mode 100644 index 00000000000..2a7a8bca58e --- /dev/null +++ b/target/i386/latx/include/kzt_jump_slot_production.h @@ -0,0 +1,71 @@ +#ifndef KZT_JUMP_SLOT_PRODUCTION_H +#define KZT_JUMP_SLOT_PRODUCTION_H + +#include + +#include "elf.h" +#include "kzt_guest_registry.h" +#include "kzt_jump_slot_route.h" +#include "kzt_lazy_binding.h" + +typedef struct box64context_s box64context_t; +typedef struct elfheader_s elfheader_t; + +int kzt_production_jump_slot_route( + box64context_t *context, library_t *resolved_provider, + uintptr_t resolved_target, elfheader_t *head, int need_resolv_present, + int entry_index, Elf64_Rela *rela, uint64_t *slot, + uintptr_t slot_current_value, + int slot_current_value_is_unresolved_stub, unsigned long symbol_index, + const char *symbol_name, const char *version, + int expected_guest_target_present, uintptr_t expected_guest_target, + uintptr_t legacy_target, kzt_jump_slot_route_result_t *route_result); + +int kzt_production_jump_slot_route_with_version_evidence( + box64context_t *context, library_t *resolved_provider, + uintptr_t resolved_target, elfheader_t *head, int need_resolv_present, + int entry_index, Elf64_Rela *rela, uint64_t *slot, + uintptr_t slot_current_value, + int slot_current_value_is_unresolved_stub, unsigned long symbol_index, + const char *symbol_name, + kzt_symbol_version_evidence_t version_evidence, + const char *version, int expected_guest_target_present, + uintptr_t expected_guest_target, uintptr_t legacy_target, + kzt_jump_slot_route_result_t *route_result); + +int kzt_production_lazy_route_guest_target( + box64context_t *context, + const kzt_lazy_binding_pending_t *pending, + uintptr_t guest_target, + kzt_lazy_binding_route_result_t *result); + +/* Production resolver orchestration. One exact source lease covers the + * first slot load, post-bind validation, route transaction, and final + * release. */ +int kzt_production_lazy_complete( + box64context_t *context, + kzt_lazy_binding_pending_t *pending, + kzt_lazy_binding_result_t *result); + +int kzt_production_lazy_source_lease_acquire( + box64context_t *context, + const kzt_lazy_binding_pending_t *pending, + kzt_guest_registry_source_lease_t *source_lease); + +int kzt_production_lazy_load_slot_with_lease( + box64context_t *context, + const kzt_lazy_binding_pending_t *pending, + uintptr_t slot_addr, + uintptr_t *value, + kzt_guest_registry_source_lease_t *source_lease); + +/* The caller-held exact (link_map, generation, namespace) source lease must + * cover the first source slot read through the complete route transaction. */ +int kzt_production_lazy_route_guest_target_leased( + box64context_t *context, + const kzt_lazy_binding_pending_t *pending, + uintptr_t guest_target, + const kzt_guest_registry_source_lease_t *source_lease, + kzt_lazy_binding_route_result_t *result); + +#endif diff --git a/target/i386/latx/include/kzt_jump_slot_route.h b/target/i386/latx/include/kzt_jump_slot_route.h new file mode 100644 index 00000000000..bc5d244fc80 --- /dev/null +++ b/target/i386/latx/include/kzt_jump_slot_route.h @@ -0,0 +1,85 @@ +#ifndef KZT_JUMP_SLOT_ROUTE_H +#define KZT_JUMP_SLOT_ROUTE_H + +#include + +#include "kzt_guest_library_binding.h" +#include "kzt_patch_spike_writer.h" +#include "kzt_rela_immediate_candidate.h" + +typedef enum kzt_jump_slot_route_writer_status { + KZT_JUMP_SLOT_ROUTE_WRITER_DECLINED = 0, + KZT_JUMP_SLOT_ROUTE_WRITER_APPLIED, + KZT_JUMP_SLOT_ROUTE_WRITER_ERROR, + KZT_JUMP_SLOT_ROUTE_WRITER_PRESERVE, +} kzt_jump_slot_route_writer_status_t; + +typedef enum kzt_jump_slot_route_status { + KZT_JUMP_SLOT_ROUTE_BYPASS = 0, + KZT_JUMP_SLOT_ROUTE_NATIVE_APPLIED, + KZT_JUMP_SLOT_ROUTE_LEGACY_APPLIED, + KZT_JUMP_SLOT_ROUTE_GUEST_PRESERVED, + KZT_JUMP_SLOT_ROUTE_CAS_MISMATCH, + KZT_JUMP_SLOT_ROUTE_WRITE_ERROR, +} kzt_jump_slot_route_status_t; + +typedef int (*kzt_jump_slot_route_load_fn)(uintptr_t slot_addr, + uintptr_t *value, + void *opaque); +typedef int (*kzt_jump_slot_route_cas_fn)(uintptr_t slot_addr, + uintptr_t *expected, + uintptr_t replacement, + void *opaque); + +typedef struct kzt_jump_slot_route_ops { + int (*enrich_base)(kzt_rela_immediate_candidate_request_t *request, + void *opaque); + int (*acquire_exact_provider)( + const kzt_patch_object_ref_t *owner, library_t *resolved_provider, + kzt_guest_library_handle_t *handle, void *opaque); + void (*release_exact_provider)(kzt_guest_library_handle_t *handle, + void *opaque); + int (*enrich_bridge)(kzt_rela_immediate_candidate_request_t *request, + library_t *held_provider, void *opaque); + int (*validate_source_identity)( + const kzt_rela_immediate_candidate_request_t *request, + void *opaque); + kzt_jump_slot_route_writer_status_t (*try_native_writer)( + const kzt_rela_immediate_candidate_request_t *request, + const kzt_patch_spike_slot_ops_t *slot_ops, void *opaque); + kzt_jump_slot_route_load_fn load_slot; + kzt_jump_slot_route_cas_fn compare_exchange_slot; + kzt_patch_spike_slot_permission_begin_fn begin_slot_write; + kzt_patch_spike_slot_permission_end_fn end_slot_write; + kzt_patch_spike_slot_generation_validate_fn validate_write_generation; + void *opaque; +} kzt_jump_slot_route_ops_t; + +typedef struct kzt_jump_slot_route_input { + int enabled; + int preserve_observed_on_failure; + int expected_guest_target_present; + int resolved_target_matches_legacy; + library_t *resolved_provider; + kzt_rela_immediate_candidate_request_t request; +} kzt_jump_slot_route_input_t; + +typedef struct kzt_jump_slot_route_result { + kzt_jump_slot_route_status_t status; + kzt_jump_slot_route_writer_status_t writer_status; + uintptr_t observed_value; + uintptr_t expected_guest_target; + uintptr_t selected_target; + uintptr_t final_value; + int exact_provider_acquired; + int exact_provider_matched; + int native_writer_called; + int source_identity_rechecked; + int legacy_fallback_attempted; +} kzt_jump_slot_route_result_t; + +int kzt_jump_slot_route_apply(const kzt_jump_slot_route_input_t *input, + const kzt_jump_slot_route_ops_t *ops, + kzt_jump_slot_route_result_t *result); + +#endif diff --git a/target/i386/latx/include/kzt_lazy_binding.h b/target/i386/latx/include/kzt_lazy_binding.h new file mode 100644 index 00000000000..458659d0c0e --- /dev/null +++ b/target/i386/latx/include/kzt_lazy_binding.h @@ -0,0 +1,125 @@ +#ifndef KZT_LAZY_BINDING_H +#define KZT_LAZY_BINDING_H + +#include + +#include "kzt_guest_library_binding.h" +#include "kzt_patch_planner.h" + +#define KZT_LAZY_BINDING_SYMBOL_MAX 128 +#define KZT_LAZY_BINDING_VERSION_MAX 128 + +typedef enum kzt_lazy_binding_status { + KZT_LAZY_BINDING_BYPASS = 0, + KZT_LAZY_BINDING_HANDOFF_GUEST, + KZT_LAZY_BINDING_WAITING_GUEST_TARGET, + KZT_LAZY_BINDING_GUEST_PRESERVED, + KZT_LAZY_BINDING_NATIVE_APPLIED, + KZT_LAZY_BINDING_CAS_MISMATCH, + KZT_LAZY_BINDING_ERROR, +} kzt_lazy_binding_status_t; + +typedef enum kzt_lazy_binding_reason { + KZT_LAZY_BINDING_REASON_NONE = 0, + KZT_LAZY_BINDING_REASON_DISABLED, + KZT_LAZY_BINDING_REASON_INVALID_REQUEST, + KZT_LAZY_BINDING_REASON_PENDING_OCCUPIED, + KZT_LAZY_BINDING_REASON_GENERATION_CHANGED, + KZT_LAZY_BINDING_REASON_NON_MAIN_NAMESPACE, + KZT_LAZY_BINDING_REASON_RESOLVER_MISSING, + KZT_LAZY_BINDING_REASON_SLOT_UNCHANGED, + KZT_LAZY_BINDING_REASON_MISSING_VERSION, + KZT_LAZY_BINDING_REASON_POST_BIND_INVALID, + KZT_LAZY_BINDING_REASON_NATIVE_UNAVAILABLE, + KZT_LAZY_BINDING_REASON_NATIVE_APPLIED, + KZT_LAZY_BINDING_REASON_CAS_MISMATCH, + KZT_LAZY_BINDING_REASON_SLOT_READ_ERROR, + KZT_LAZY_BINDING_REASON_CAS_ERROR, +} kzt_lazy_binding_reason_t; + +#define KZT_LAZY_BINDING_REASON_PENDING_BUSY \ + KZT_LAZY_BINDING_REASON_PENDING_OCCUPIED + +typedef struct kzt_lazy_binding_begin_request { + int enabled; + uintptr_t context_id; + uintptr_t source_link_map; + unsigned long source_generation; + uintptr_t namespace_id; + kzt_guest_library_namespace_kind_t namespace_kind; + unsigned long relocation_index; + uintptr_t slot_addr; + uintptr_t unresolved_stub; + const char *symbol; + kzt_symbol_version_evidence_t version_evidence; + const char *version; + long addend; + uintptr_t guest_resolver; +} kzt_lazy_binding_begin_request_t; + +typedef struct kzt_lazy_binding_pending { + int armed; + uintptr_t context_id; + uintptr_t source_link_map; + unsigned long source_generation; + uintptr_t namespace_id; + kzt_guest_library_namespace_kind_t namespace_kind; + unsigned long relocation_index; + uintptr_t slot_addr; + uintptr_t unresolved_stub; + uintptr_t guest_resolver; + long addend; + const char *symbol; + kzt_symbol_version_evidence_t version_evidence; + const char *version; + char symbol_storage[KZT_LAZY_BINDING_SYMBOL_MAX]; + char version_storage[KZT_LAZY_BINDING_VERSION_MAX]; +} kzt_lazy_binding_pending_t; + +typedef enum kzt_lazy_binding_route_status { + KZT_LAZY_BINDING_ROUTE_GUEST_PRESERVED = 0, + KZT_LAZY_BINDING_ROUTE_NATIVE_APPLIED, + KZT_LAZY_BINDING_ROUTE_CAS_MISMATCH, + KZT_LAZY_BINDING_ROUTE_ERROR, +} kzt_lazy_binding_route_status_t; + +typedef struct kzt_lazy_binding_route_result { + kzt_lazy_binding_route_status_t status; + uintptr_t selected_target; + uintptr_t final_value; +} kzt_lazy_binding_route_result_t; + +typedef struct kzt_lazy_binding_ops { + int (*load_slot)(uintptr_t slot_addr, uintptr_t *value, void *opaque); + int (*validate_post_bind)(const kzt_lazy_binding_pending_t *pending, + uintptr_t guest_target, void *opaque); + int (*route_guest_target)(const kzt_lazy_binding_pending_t *pending, + uintptr_t guest_target, + kzt_lazy_binding_route_result_t *result, + void *opaque); + void *opaque; +} kzt_lazy_binding_ops_t; + +typedef struct kzt_lazy_binding_result { + kzt_lazy_binding_status_t status; + kzt_lazy_binding_reason_t reason; + uintptr_t slot_before; + uintptr_t slot_after; + uintptr_t selected_target; + int pending_armed; + int pending_cleared; +} kzt_lazy_binding_result_t; + +int kzt_lazy_binding_begin( + const kzt_lazy_binding_begin_request_t *request, + kzt_lazy_binding_pending_t *pending, + kzt_lazy_binding_result_t *result); + +int kzt_lazy_binding_complete( + kzt_lazy_binding_pending_t *pending, + const kzt_lazy_binding_ops_t *ops, + kzt_lazy_binding_result_t *result); + +void kzt_lazy_binding_cancel(kzt_lazy_binding_pending_t *pending); + +#endif diff --git a/target/i386/latx/include/kzt_lazy_diagnostics.h b/target/i386/latx/include/kzt_lazy_diagnostics.h new file mode 100644 index 00000000000..9a1ef59231b --- /dev/null +++ b/target/i386/latx/include/kzt_lazy_diagnostics.h @@ -0,0 +1,84 @@ +#ifndef KZT_LAZY_DIAGNOSTICS_H +#define KZT_LAZY_DIAGNOSTICS_H + +#include +#include + +#include "kzt_lazy_binding.h" + +#define KZT_LAZY_DIAGNOSTIC_SYMBOL_LIMIT KZT_LAZY_BINDING_SYMBOL_MAX +#define KZT_LAZY_DIAGNOSTIC_ROUTE_LIMIT 64 +#define KZT_LAZY_DIAGNOSTIC_REASON_LIMIT 64 +#define KZT_LAZY_DIAGNOSTIC_LINE_LIMIT 1024 + +typedef enum kzt_lazy_diagnostic_format_status { + KZT_LAZY_DIAGNOSTIC_FORMAT_ERROR = -1, + KZT_LAZY_DIAGNOSTIC_FORMAT_OK = 0, + KZT_LAZY_DIAGNOSTIC_FORMAT_TRUNCATED, +} kzt_lazy_diagnostic_format_status_t; + +typedef enum kzt_lazy_diagnostic_emit_status { + KZT_LAZY_DIAGNOSTIC_EMIT_DISABLED = 0, + KZT_LAZY_DIAGNOSTIC_EMIT_EMITTED, + KZT_LAZY_DIAGNOSTIC_EMIT_FORMAT_FAILED, + KZT_LAZY_DIAGNOSTIC_EMIT_FORMAT_TRUNCATED, + KZT_LAZY_DIAGNOSTIC_EMIT_SINK_FAILED, +} kzt_lazy_diagnostic_emit_status_t; + +typedef struct kzt_lazy_diagnostic_record { + unsigned long schema; + char symbol[KZT_LAZY_DIAGNOSTIC_SYMBOL_LIMIT]; + char first_execution_route[KZT_LAZY_DIAGNOSTIC_ROUTE_LIMIT]; + char completion_route_status[KZT_LAZY_DIAGNOSTIC_ROUTE_LIMIT]; + uintptr_t slot_before; + uintptr_t slot_after_guest; + uintptr_t selected_second_target; + char reason[KZT_LAZY_DIAGNOSTIC_REASON_LIMIT]; +} kzt_lazy_diagnostic_record_t; + +typedef int (*kzt_lazy_diagnostic_sink_fn)(const char *line, + size_t line_length, + void *opaque); + +typedef struct kzt_lazy_diagnostic_input { + int enabled; + const kzt_lazy_binding_pending_t *pending; + const kzt_lazy_binding_result_t *binding_result; + char *buffer; + size_t buffer_size; + kzt_lazy_diagnostic_sink_fn sink; + void *sink_opaque; +} kzt_lazy_diagnostic_input_t; + +typedef struct kzt_lazy_diagnostic_emit_result { + kzt_lazy_diagnostic_emit_status_t status; + int record_present; + kzt_lazy_diagnostic_record_t record; + kzt_lazy_diagnostic_format_status_t format_status; + int sink_status; +} kzt_lazy_diagnostic_emit_result_t; + +int kzt_lazy_diagnostic_record_build( + const kzt_lazy_binding_pending_t *pending, + const kzt_lazy_binding_result_t *binding_result, + kzt_lazy_diagnostic_record_t *record); + +kzt_lazy_diagnostic_format_status_t kzt_lazy_diagnostic_format( + const kzt_lazy_diagnostic_record_t *record, + char *buffer, + size_t buffer_size); + +int kzt_lazy_diagnostics_stderr_sink(const char *line, + size_t line_length, + void *opaque); + +int kzt_lazy_diagnostics_emit( + const kzt_lazy_diagnostic_input_t *input, + kzt_lazy_diagnostic_emit_result_t *result); + +int kzt_lazy_diagnostics_emit_production( + const kzt_lazy_binding_pending_t *pending, + const kzt_lazy_binding_result_t *binding_result, + kzt_lazy_diagnostic_emit_result_t *result); + +#endif diff --git a/target/i386/latx/include/kzt_loader_callback_scope.h b/target/i386/latx/include/kzt_loader_callback_scope.h new file mode 100644 index 00000000000..54e5f7e6d3a --- /dev/null +++ b/target/i386/latx/include/kzt_loader_callback_scope.h @@ -0,0 +1,13 @@ +#ifndef KZT_LOADER_CALLBACK_SCOPE_H +#define KZT_LOADER_CALLBACK_SCOPE_H + +typedef struct kzt_guest_library_bindings kzt_guest_library_bindings_t; + +/* A value token issued by one context-owned guest loader invocation. */ +typedef struct kzt_guest_library_loader_scope { + kzt_guest_library_bindings_t *bindings; + unsigned long identity; + unsigned long cookie; +} kzt_guest_library_loader_scope_t; + +#endif diff --git a/target/i386/latx/include/kzt_observation_adapter.h b/target/i386/latx/include/kzt_observation_adapter.h new file mode 100644 index 00000000000..914448bdb0a --- /dev/null +++ b/target/i386/latx/include/kzt_observation_adapter.h @@ -0,0 +1,82 @@ +#ifndef KZT_OBSERVATION_ADAPTER_H +#define KZT_OBSERVATION_ADAPTER_H + +#include +#include + +#include "kzt_guest_link_map_reader.h" +#include "kzt_guest_dynamic.h" +#include "kzt_guest_dynamic_diagnostics.h" +#include "kzt_guest_registry.h" +#include "kzt_loader_callback_scope.h" + +typedef struct kzt_guest_library_bindings kzt_guest_library_bindings_t; + +typedef enum kzt_observation_adapter_result { + KZT_OBSERVATION_ADAPTER_DISABLED = 0, + KZT_OBSERVATION_ADAPTER_ADDED, + KZT_OBSERVATION_ADAPTER_UNCHANGED, + KZT_OBSERVATION_ADAPTER_UPDATED, + KZT_OBSERVATION_ADAPTER_CONFLICT, + KZT_OBSERVATION_ADAPTER_READER_FAILED, + KZT_OBSERVATION_ADAPTER_REGISTRY_FAILED, +} kzt_observation_adapter_result_t; + +typedef int (*kzt_observation_legacy_flow_fn)(uintptr_t link_map_addr, + void *opaque); + +typedef struct kzt_observation_adapter_dynamic_diagnostic { + int attempted; + int cache_hit; + int parse_return; + uintptr_t dynamic_addr; + kzt_guest_dynamic_status_t status; + kzt_guest_dynamic_error_t error; + size_t entry_count; + uintptr_t read_error_addr; + int commit_attempted; + kzt_guest_registry_result_t commit_result; + int comparison_attempted; + kzt_guest_dynamic_diagnostic_summary_t comparison; + kzt_guest_registry_observation_diagnostic_t registry; +} kzt_observation_adapter_dynamic_diagnostic_t; + +typedef struct kzt_observation_adapter_diagnostic { + int enabled; + int emitted; + kzt_observation_adapter_result_t result; + uintptr_t link_map_addr; + kzt_guest_registry_observation_diagnostic_t registry; + kzt_observation_adapter_dynamic_diagnostic_t dynamic; +} kzt_observation_adapter_diagnostic_t; + +typedef void (*kzt_observation_adapter_diagnostic_fn)( + const kzt_observation_adapter_diagnostic_t *diagnostic, + void *opaque); + +typedef struct kzt_observation_adapter_request { + int enabled; + int diagnostics_enabled; + int dynamic_diagnostics_force_compare; + uintptr_t link_map_addr; + kzt_guest_registry_t *registry; + kzt_guest_library_bindings_t *library_bindings; + const kzt_guest_library_loader_scope_t *loader_scope; + const kzt_guest_link_map_reader_ops_t *reader_ops; + /* Caller-verified evidence. Invalid or absent hints are ignored. */ + int namespace_id_present; + uintptr_t namespace_id; + int map_range_present; + uintptr_t map_start; + uintptr_t map_end; + kzt_observation_legacy_flow_fn legacy_flow; + void *legacy_opaque; + kzt_observation_adapter_diagnostic_fn diagnostic; + void *diagnostic_opaque; +} kzt_observation_adapter_request_t; + +int kzt_observe_guest_object_from_callback( + const kzt_observation_adapter_request_t *request, + kzt_observation_adapter_result_t *result); + +#endif diff --git a/target/i386/latx/include/kzt_owner_resolver.h b/target/i386/latx/include/kzt_owner_resolver.h new file mode 100644 index 00000000000..97979482acc --- /dev/null +++ b/target/i386/latx/include/kzt_owner_resolver.h @@ -0,0 +1,56 @@ +#ifndef KZT_OWNER_RESOLVER_H +#define KZT_OWNER_RESOLVER_H + +#include +#include + +#include "kzt_guest_registry.h" +#include "kzt_patch_planner.h" + +#define KZT_OWNER_RESOLVER_TEXT_LIMIT 256 + +typedef enum kzt_owner_resolver_status { + KZT_OWNER_RESOLVER_RESOLVED = 0, + KZT_OWNER_RESOLVER_INVALID_ARGUMENT, + KZT_OWNER_RESOLVER_REGISTRY_UNAVAILABLE, + KZT_OWNER_RESOLVER_CURRENT_ADDRESS_MISSING, + KZT_OWNER_RESOLVER_EXPECTED_ADDRESS_MISSING, + KZT_OWNER_RESOLVER_CURRENT_NOT_FOUND, + KZT_OWNER_RESOLVER_EXPECTED_NOT_FOUND, + KZT_OWNER_RESOLVER_CURRENT_AMBIGUOUS, + KZT_OWNER_RESOLVER_EXPECTED_AMBIGUOUS, + KZT_OWNER_RESOLVER_GENERATION_UNKNOWN, +} kzt_owner_resolver_status_t; + +typedef struct kzt_owner_resolver_text { + char soname[KZT_OWNER_RESOLVER_TEXT_LIMIT]; + char path[KZT_OWNER_RESOLVER_TEXT_LIMIT]; +} kzt_owner_resolver_text_t; + +typedef struct kzt_owner_resolution { + kzt_owner_resolver_status_t status; + kzt_patch_object_ref_t current_owner; + kzt_patch_object_ref_t expected_owner; + kzt_patch_owner_match_t owner_match; + size_t current_match_count; + size_t expected_match_count; + kzt_owner_resolver_text_t current_text; + kzt_owner_resolver_text_t expected_text; +} kzt_owner_resolution_t; + +void kzt_owner_resolver_init(kzt_owner_resolution_t *resolution); + +int kzt_owner_resolver_resolve_current( + kzt_guest_registry_t *registry, + uintptr_t current_address, + uintptr_t expected_address, + kzt_owner_resolution_t *resolution); + +kzt_patch_owner_match_t kzt_owner_resolver_match_refs( + const kzt_patch_object_ref_t *current_owner, + const kzt_patch_object_ref_t *expected_owner); + +const char *kzt_owner_resolver_status_name( + kzt_owner_resolver_status_t status); + +#endif diff --git a/target/i386/latx/include/kzt_patch_planner.h b/target/i386/latx/include/kzt_patch_planner.h new file mode 100644 index 00000000000..176d5911206 --- /dev/null +++ b/target/i386/latx/include/kzt_patch_planner.h @@ -0,0 +1,204 @@ +#ifndef KZT_PATCH_PLANNER_H +#define KZT_PATCH_PLANNER_H + +#include +#include +#include + +typedef enum kzt_patch_table_kind { + KZT_PATCH_TABLE_UNKNOWN = 0, + KZT_PATCH_TABLE_RELA, + KZT_PATCH_TABLE_REL, + KZT_PATCH_TABLE_PLT_RELA, + KZT_PATCH_TABLE_PLT_REL, + KZT_PATCH_TABLE_OTHER, +} kzt_patch_table_kind_t; + +typedef enum kzt_patch_relocation_type { + KZT_PATCH_RELOCATION_UNKNOWN = 0, + KZT_PATCH_RELOCATION_JUMP_SLOT, + KZT_PATCH_RELOCATION_GLOB_DAT, + KZT_PATCH_RELOCATION_RELATIVE, + KZT_PATCH_RELOCATION_COPY, + KZT_PATCH_RELOCATION_IRELATIVE, + KZT_PATCH_RELOCATION_OTHER, +} kzt_patch_relocation_type_t; + +typedef enum kzt_patch_owner_match { + KZT_PATCH_OWNER_UNKNOWN = 0, + KZT_PATCH_OWNER_MATCH, + KZT_PATCH_OWNER_MISMATCH, +} kzt_patch_owner_match_t; + +typedef enum kzt_patch_wrapper_match { + KZT_PATCH_WRAPPER_NO_MANIFEST = 0, + KZT_PATCH_WRAPPER_NO_WRAPPER, + KZT_PATCH_WRAPPER_SYMBOL_ONLY, + KZT_PATCH_WRAPPER_VERSION_MISMATCH, + KZT_PATCH_WRAPPER_VERSION_MATCH, + KZT_PATCH_WRAPPER_UNVERSIONED_MATCH, +} kzt_patch_wrapper_match_t; + +typedef enum kzt_symbol_version_evidence { + /* Keep VERSIONED as zero so existing versioned initializers retain their + * meaning. Producers must still validate that a version string exists. */ + KZT_SYMBOL_VERSION_VERSIONED = 0, + KZT_SYMBOL_VERSION_CONFIRMED_UNVERSIONED, + KZT_SYMBOL_VERSION_UNKNOWN, + KZT_SYMBOL_VERSION_ERROR, +} kzt_symbol_version_evidence_t; + +typedef enum kzt_patch_decision_kind { + KZT_PATCH_DECISION_ERROR = 0, + KZT_PATCH_DECISION_UNSUPPORTED, + KZT_PATCH_DECISION_REJECTED, + KZT_PATCH_DECISION_DEFERRED, + KZT_PATCH_DECISION_APPROVED, +} kzt_patch_decision_kind_t; + +typedef enum kzt_patch_reason { + KZT_PATCH_REASON_ERROR_INVALID_ARGUMENT = 0, + KZT_PATCH_REASON_INPUT_UNSUPPORTED_RELOCATION, + KZT_PATCH_REASON_INPUT_MALFORMED_TABLE, + KZT_PATCH_REASON_INPUT_MALFORMED_SLOT, + KZT_PATCH_REASON_INPUT_MALFORMED_SYMBOL_NAME, + KZT_PATCH_REASON_INPUT_MALFORMED_SYMBOL_VERSION, + KZT_PATCH_REASON_INPUT_UNAVAILABLE_DYNAMIC_VIEW, + KZT_PATCH_REASON_INPUT_UNAVAILABLE_CURRENT_GOT, + KZT_PATCH_REASON_INPUT_UNAVAILABLE_OWNER, + KZT_PATCH_REASON_INPUT_UNAVAILABLE_WRAPPER_MANIFEST, + KZT_PATCH_REASON_INPUT_UNAVAILABLE_BRIDGE_TARGET, + KZT_PATCH_REASON_POLICY_KEEP_GUEST, + KZT_PATCH_REASON_POLICY_OWNER_MISMATCH, + KZT_PATCH_REASON_POLICY_NO_WRAPPER, + KZT_PATCH_REASON_POLICY_WRAPPER_SYMBOL_ONLY, + KZT_PATCH_REASON_POLICY_VERSION_MISMATCH, + KZT_PATCH_REASON_DEFERRED_LAZY_BINDING, + KZT_PATCH_REASON_APPROVED_NATIVE_BRIDGE, +} kzt_patch_reason_t; + +typedef struct kzt_patch_object_ref { + int known; + uintptr_t link_map_addr; + uintptr_t map_start; + uintptr_t map_end; + unsigned long generation; + const char *soname; + const char *path; +} kzt_patch_object_ref_t; + +typedef struct kzt_patch_candidate { + kzt_patch_object_ref_t source; + uintptr_t dynamic_addr; + uintptr_t load_bias; + unsigned long dynamic_view_generation; + int dynamic_view_available; + + kzt_patch_table_kind_t table_kind; + size_t entry_index; + uintptr_t entry_addr; + kzt_patch_relocation_type_t reloc_type; + uintptr_t slot_addr; + + int slot_current_value_present; + uintptr_t slot_current_value; + int lazy_binding_deferred; + + unsigned long symbol_index; + const char *symbol_name; + kzt_symbol_version_evidence_t version_evidence; + const char *version; + + kzt_patch_object_ref_t current_owner; + kzt_patch_owner_match_t owner_match; + + kzt_patch_wrapper_match_t wrapper_match; + const char *wrapper_name; + kzt_symbol_version_evidence_t wrapper_version_evidence; + const char *wrapper_symbol_version; + uintptr_t bridge_target; +} kzt_patch_candidate_t; + +typedef struct kzt_patch_decision { + kzt_patch_decision_kind_t kind; + kzt_patch_reason_t reason; + int allow_native_bridge; + + kzt_patch_object_ref_t source; + uintptr_t dynamic_addr; + uintptr_t load_bias; + unsigned long dynamic_view_generation; + int dynamic_view_available; + + kzt_patch_table_kind_t table_kind; + size_t entry_index; + uintptr_t entry_addr; + kzt_patch_relocation_type_t reloc_type; + uintptr_t slot_addr; + + int slot_current_value_present; + uintptr_t slot_current_value; + int lazy_binding_deferred; + + unsigned long symbol_index; + const char *symbol_name; + kzt_symbol_version_evidence_t version_evidence; + const char *version; + + kzt_patch_object_ref_t current_owner; + kzt_patch_owner_match_t owner_match; + + kzt_patch_wrapper_match_t wrapper_match; + const char *wrapper_name; + kzt_symbol_version_evidence_t wrapper_version_evidence; + const char *wrapper_symbol_version; + uintptr_t bridge_target; +} kzt_patch_decision_t; + +int kzt_patch_planner_decide(const kzt_patch_candidate_t *candidate, + kzt_patch_decision_t *decision); + +const char *kzt_patch_decision_kind_name(kzt_patch_decision_kind_t kind); +const char *kzt_patch_reason_name(kzt_patch_reason_t reason); +const char *kzt_patch_table_kind_name(kzt_patch_table_kind_t table_kind); +const char *kzt_patch_relocation_type_name( + kzt_patch_relocation_type_t reloc_type); +const char *kzt_patch_owner_match_name(kzt_patch_owner_match_t match); +const char *kzt_patch_wrapper_match_name(kzt_patch_wrapper_match_t match); +const char *kzt_symbol_version_evidence_name( + kzt_symbol_version_evidence_t evidence); + +static inline int kzt_symbol_version_evidence_valid( + kzt_symbol_version_evidence_t evidence, const char *version) +{ + switch (evidence) { + case KZT_SYMBOL_VERSION_VERSIONED: + return version && version[0]; + case KZT_SYMBOL_VERSION_CONFIRMED_UNVERSIONED: + return !version || !version[0]; + case KZT_SYMBOL_VERSION_UNKNOWN: + case KZT_SYMBOL_VERSION_ERROR: + return 0; + } + return 0; +} + +static inline int kzt_symbol_version_evidence_matches( + kzt_symbol_version_evidence_t left_evidence, const char *left_version, + kzt_symbol_version_evidence_t right_evidence, const char *right_version) +{ + if (left_evidence != right_evidence || + !kzt_symbol_version_evidence_valid(left_evidence, left_version) || + !kzt_symbol_version_evidence_valid(right_evidence, right_version)) { + return 0; + } + return left_evidence == KZT_SYMBOL_VERSION_CONFIRMED_UNVERSIONED || + strcmp(left_version, right_version) == 0; +} + +int kzt_patch_decision_format_summary( + const kzt_patch_decision_t *decision, + char *buffer, + size_t buffer_size); + +#endif diff --git a/target/i386/latx/include/kzt_patch_spike_guard.h b/target/i386/latx/include/kzt_patch_spike_guard.h new file mode 100644 index 00000000000..c81b43e5a7a --- /dev/null +++ b/target/i386/latx/include/kzt_patch_spike_guard.h @@ -0,0 +1,114 @@ +#ifndef KZT_PATCH_SPIKE_GUARD_H +#define KZT_PATCH_SPIKE_GUARD_H + +#include + +#include "kzt_patch_planner.h" + +typedef enum kzt_patch_spike_action { + KZT_PATCH_SPIKE_ACTION_KEEP_LEGACY = 0, + KZT_PATCH_SPIKE_ACTION_USE_NATIVE_BRIDGE, + KZT_PATCH_SPIKE_ACTION_PRESERVE_GUEST, +} kzt_patch_spike_action_t; + +typedef enum kzt_patch_spike_result { + KZT_PATCH_SPIKE_RESULT_DISABLED = 0, + KZT_PATCH_SPIKE_RESULT_DIAGNOSTICS_ONLY, + KZT_PATCH_SPIKE_RESULT_BUDGET_EXHAUSTED, + KZT_PATCH_SPIKE_RESULT_APPLIED, + KZT_PATCH_SPIKE_RESULT_FAIL_OPEN, + KZT_PATCH_SPIKE_RESULT_GUEST_PRESERVED, + KZT_PATCH_SPIKE_RESULT_CIRCUIT_OPEN, +} kzt_patch_spike_result_t; + +typedef enum kzt_patch_spike_failure { + KZT_PATCH_SPIKE_FAILURE_NONE = 0, + KZT_PATCH_SPIKE_FAILURE_INVALID_ARGUMENT, + KZT_PATCH_SPIKE_FAILURE_DECISION_NOT_APPROVED, + KZT_PATCH_SPIKE_FAILURE_WRITE_NOT_AUTHORIZED, + KZT_PATCH_SPIKE_FAILURE_BUDGET_EXHAUSTED, + KZT_PATCH_SPIKE_FAILURE_EXPECTED_MISMATCH, + KZT_PATCH_SPIKE_FAILURE_READ_FAILED, + KZT_PATCH_SPIKE_FAILURE_WRITE_FAILED, + KZT_PATCH_SPIKE_FAILURE_VERIFY_FAILED, + KZT_PATCH_SPIKE_FAILURE_ROLLBACK_FAILED, + KZT_PATCH_SPIKE_FAILURE_PERMISSION_ENABLE_FAILED, + KZT_PATCH_SPIKE_FAILURE_PERMISSION_RESTORE_FAILED, + KZT_PATCH_SPIKE_FAILURE_GENERATION_MISMATCH, + KZT_PATCH_SPIKE_FAILURE_CIRCUIT_BREAKER_OPEN, +} kzt_patch_spike_failure_t; + +typedef enum kzt_patch_spike_writer_status { + KZT_PATCH_SPIKE_WRITER_OK = 0, + KZT_PATCH_SPIKE_WRITER_READ_FAILED, + KZT_PATCH_SPIKE_WRITER_EXPECTED_MISMATCH, + KZT_PATCH_SPIKE_WRITER_WRITE_FAILED, + KZT_PATCH_SPIKE_WRITER_PERMISSION_ENABLE_FAILED, + KZT_PATCH_SPIKE_WRITER_PERMISSION_RESTORE_FAILED, + KZT_PATCH_SPIKE_WRITER_GENERATION_MISMATCH, +} kzt_patch_spike_writer_status_t; + +typedef struct kzt_patch_spike_config { + int enabled; + int write_enabled; + unsigned long budget; +} kzt_patch_spike_config_t; + +typedef struct kzt_patch_spike_guard { + kzt_patch_spike_config_t config; + unsigned long write_attempts; + unsigned long write_successes; + int circuit_open; + int transaction_gate; +} kzt_patch_spike_guard_t; + +typedef struct kzt_patch_spike_outcome { + kzt_patch_spike_result_t result; + kzt_patch_spike_failure_t failure; + kzt_patch_spike_action_t action; + int skip_legacy_write; + unsigned long writes_remaining; + int writer_called; + int rollback_called; + uintptr_t previous_value; +} kzt_patch_spike_outcome_t; + +typedef struct kzt_patch_spike_writer_ops { + kzt_patch_spike_writer_status_t (*write_slot)( + const kzt_patch_decision_t *decision, + uintptr_t expected_value, + uintptr_t replacement_value, + uintptr_t *previous_value, + void *opaque); + int (*verify_slot)(const kzt_patch_decision_t *decision, + uintptr_t expected_value, + void *opaque); + int (*rollback_slot)(const kzt_patch_decision_t *decision, + uintptr_t previous_value, + void *opaque); + kzt_patch_spike_writer_status_t (*finish_slot)( + const kzt_patch_decision_t *decision, void *opaque); + void *opaque; +} kzt_patch_spike_writer_ops_t; + +void kzt_patch_spike_config_from_options(kzt_patch_spike_config_t *config); +void kzt_patch_spike_guard_init(kzt_patch_spike_guard_t *guard, + const kzt_patch_spike_config_t *config); + +int kzt_patch_spike_guard_should_plan( + const kzt_patch_spike_guard_t *guard); +int kzt_patch_spike_guard_circuit_open( + const kzt_patch_spike_guard_t *guard); +unsigned long kzt_patch_spike_guard_budget_remaining( + const kzt_patch_spike_guard_t *guard); + +int kzt_patch_spike_guard_try_write( + kzt_patch_spike_guard_t *guard, + const kzt_patch_decision_t *decision, + const kzt_patch_spike_writer_ops_t *writer, + kzt_patch_spike_outcome_t *outcome); + +const char *kzt_patch_spike_result_name(kzt_patch_spike_result_t result); +const char *kzt_patch_spike_failure_name(kzt_patch_spike_failure_t failure); + +#endif diff --git a/target/i386/latx/include/kzt_patch_spike_writer.h b/target/i386/latx/include/kzt_patch_spike_writer.h new file mode 100644 index 00000000000..ad5b5cc016e --- /dev/null +++ b/target/i386/latx/include/kzt_patch_spike_writer.h @@ -0,0 +1,116 @@ +#ifndef KZT_PATCH_SPIKE_WRITER_H +#define KZT_PATCH_SPIKE_WRITER_H + +#include +#include + +#include "kzt_patch_spike_guard.h" + +typedef int (*kzt_patch_spike_slot_read_fn)(uintptr_t slot_addr, + uintptr_t *value, + void *opaque); +typedef int (*kzt_patch_spike_slot_write_fn)(uintptr_t slot_addr, + uintptr_t value, + void *opaque); + +typedef struct kzt_patch_spike_permission_lease { + uintptr_t guest_page; + uintptr_t guest_page_length; + int original_permissions; + int checked; + int was_writable; + int write_enabled; + int restore_attempted; + int restored; +} kzt_patch_spike_permission_lease_t; + +typedef int (*kzt_patch_spike_slot_permission_begin_fn)( + uintptr_t slot_addr, kzt_patch_spike_permission_lease_t *lease, + void *opaque); +typedef int (*kzt_patch_spike_slot_permission_end_fn)( + kzt_patch_spike_permission_lease_t *lease, void *opaque); +typedef int (*kzt_patch_spike_slot_generation_validate_fn)( + const kzt_patch_decision_t *decision, void *opaque); + +typedef struct kzt_patch_spike_slot_ops { + kzt_patch_spike_slot_read_fn read_slot; + kzt_patch_spike_slot_write_fn write_slot; + kzt_patch_spike_slot_permission_begin_fn begin_write; + kzt_patch_spike_slot_permission_end_fn end_write; + kzt_patch_spike_slot_generation_validate_fn validate_generation; + void *opaque; +} kzt_patch_spike_slot_ops_t; + +typedef struct kzt_patch_spike_record { + int valid; + + kzt_patch_decision_kind_t decision_kind; + kzt_patch_reason_t decision_reason; + int allow_native_bridge; + kzt_patch_table_kind_t table_kind; + kzt_patch_relocation_type_t reloc_type; + size_t entry_index; + uintptr_t entry_addr; + uintptr_t slot_addr; + uintptr_t source_link_map; + uintptr_t current_owner_link_map; + unsigned long source_generation; + unsigned long current_owner_generation; + unsigned long dynamic_view_generation; + + int expected_value_present; + uintptr_t expected_value; + uintptr_t replacement_value; + uintptr_t previous_value; + uintptr_t observed_value; + uintptr_t verified_value; + uintptr_t rollback_value; + + const char *symbol_name; + const char *wrapper_name; + + kzt_patch_spike_result_t result; + kzt_patch_spike_failure_t failure; + kzt_patch_spike_action_t action; + int skip_legacy_write; + unsigned long writes_remaining; + + int writer_called; + int read_attempted; + int expected_current_matched; + int write_attempted; + int write_succeeded; + int verify_attempted; + int verify_succeeded; + int rollback_called; + int rollback_succeeded; + int rollback_verify_attempted; + uintptr_t rollback_verified_value; + int rollback_verify_succeeded; + int generation_checked; + int generation_matched; + int permission_checked; + uintptr_t permission_guest_page; + uintptr_t permission_guest_page_length; + int permission_original_permissions; + int permission_was_writable; + int permission_write_enabled; + int permission_restore_attempted; + int permission_restored; +} kzt_patch_spike_record_t; + +void kzt_patch_spike_record_init(kzt_patch_spike_record_t *record, + const kzt_patch_decision_t *decision); + +int kzt_patch_spike_writer_try_apply( + kzt_patch_spike_guard_t *guard, + const kzt_patch_decision_t *decision, + kzt_patch_spike_record_t *record); + +int kzt_patch_spike_writer_try_apply_with_slot_ops( + kzt_patch_spike_guard_t *guard, + const kzt_patch_decision_t *decision, + const kzt_patch_spike_slot_ops_t *slot_ops, + kzt_patch_spike_record_t *record); + +#endif diff --git a/target/i386/latx/include/kzt_plt_resolver_adapter.h b/target/i386/latx/include/kzt_plt_resolver_adapter.h new file mode 100644 index 00000000000..35d96c0a219 --- /dev/null +++ b/target/i386/latx/include/kzt_plt_resolver_adapter.h @@ -0,0 +1,67 @@ +#ifndef KZT_PLT_RESOLVER_ADAPTER_H +#define KZT_PLT_RESOLVER_ADAPTER_H + +#include + +#include "kzt_lazy_binding.h" + +#ifdef KZT_PLT_RESOLVER_ADAPTER_TEST +typedef struct CPUX86State { + uint64_t regs[16]; +} CPUX86State; +#define R_ESP 4 +#else +typedef struct CPUX86State CPUX86State; +#endif + +typedef struct kzt_plt_resolver_source { + int enabled; + uintptr_t context_id; + uintptr_t object_head; + uintptr_t source_link_map; + unsigned long source_generation; + uintptr_t namespace_id; + kzt_guest_library_namespace_kind_t namespace_kind; + uintptr_t slot_addr; + uintptr_t unresolved_stub; + const char *symbol; + kzt_symbol_version_evidence_t version_evidence; + const char *version; + long addend; + uintptr_t guest_resolver; +} kzt_plt_resolver_source_t; + +typedef struct kzt_plt_resolver_runtime_ops { + int (*lookup_source)(uintptr_t object_head, + kzt_plt_resolver_source_t *source, void *opaque); + int (*begin_lazy_binding)( + const kzt_lazy_binding_begin_request_t *request, + kzt_lazy_binding_pending_t *pending, + kzt_lazy_binding_result_t *result, void *opaque); + kzt_lazy_binding_pending_t *pending; + uintptr_t completion_bridge; + uintptr_t *original_return; + void *opaque; +} kzt_plt_resolver_runtime_ops_t; + +typedef enum kzt_plt_resolver_enter_status { + KZT_PLT_RESOLVER_ERROR = 0, + KZT_PLT_RESOLVER_HANDOFF_GUEST, + KZT_PLT_RESOLVER_GUEST_PRESERVED, + KZT_PLT_RESOLVER_LEGACY_FRAME_RESTORED, +} kzt_plt_resolver_enter_status_t; + +typedef struct kzt_plt_resolver_enter_result { + kzt_plt_resolver_enter_status_t status; + uintptr_t object_head; + unsigned long relocation_slot; + uintptr_t return_address; + uintptr_t selected_resolver; + int pending_armed; +} kzt_plt_resolver_enter_result_t; + +int kzt_plt_resolver_enter( + CPUX86State *cpu, const kzt_plt_resolver_runtime_ops_t *ops, + kzt_plt_resolver_enter_result_t *result); + +#endif diff --git a/target/i386/latx/include/kzt_rela_diagnostics.h b/target/i386/latx/include/kzt_rela_diagnostics.h new file mode 100644 index 00000000000..016603a2f17 --- /dev/null +++ b/target/i386/latx/include/kzt_rela_diagnostics.h @@ -0,0 +1,140 @@ +#ifndef KZT_RELA_DIAGNOSTICS_H +#define KZT_RELA_DIAGNOSTICS_H + +#include +#include + +#include "kzt_rela_immediate_candidate.h" + +#define KZT_RELA_DIAGNOSTIC_SOURCE_LIMIT 256 +#define KZT_RELA_DIAGNOSTIC_NAME_LIMIT 64 +#define KZT_RELA_DIAGNOSTIC_LINE_LIMIT 1024 + +typedef enum kzt_rela_diagnostic_mode { + KZT_RELA_DIAGNOSTIC_MODE_DEFAULT = 0, + KZT_RELA_DIAGNOSTIC_MODE_WRITE_ENABLED_ONLY, + KZT_RELA_DIAGNOSTIC_MODE_DIAGNOSTICS, + KZT_RELA_DIAGNOSTIC_MODE_DIAGNOSTICS_WRITE_ENABLED, +} kzt_rela_diagnostic_mode_t; + +typedef enum kzt_rela_diagnostic_reason_domain { + KZT_RELA_DIAGNOSTIC_REASON_CANDIDATE = 0, + KZT_RELA_DIAGNOSTIC_REASON_PLANNER, + KZT_RELA_DIAGNOSTIC_REASON_WRITER, +} kzt_rela_diagnostic_reason_domain_t; + +typedef enum kzt_rela_diagnostic_format_status { + KZT_RELA_DIAGNOSTIC_FORMAT_ERROR = -1, + KZT_RELA_DIAGNOSTIC_FORMAT_OK = 0, + KZT_RELA_DIAGNOSTIC_FORMAT_TRUNCATED, +} kzt_rela_diagnostic_format_status_t; + +typedef enum kzt_rela_diagnostic_emit_status { + KZT_RELA_DIAGNOSTIC_EMIT_DISABLED = 0, + KZT_RELA_DIAGNOSTIC_EMIT_EMITTED, + KZT_RELA_DIAGNOSTIC_EMIT_SUPPRESSED, + KZT_RELA_DIAGNOSTIC_EMIT_THROTTLE_FAILED, + KZT_RELA_DIAGNOSTIC_EMIT_FORMAT_FAILED, + KZT_RELA_DIAGNOSTIC_EMIT_FORMAT_TRUNCATED, + KZT_RELA_DIAGNOSTIC_EMIT_SINK_FAILED, +} kzt_rela_diagnostic_emit_status_t; + +typedef struct kzt_rela_diagnostic_record { + char source[KZT_RELA_DIAGNOSTIC_SOURCE_LIMIT]; + uintptr_t source_link_map; + uintptr_t current_owner; + unsigned long source_generation; + unsigned long current_owner_generation; + kzt_patch_owner_match_t owner_match; + kzt_patch_wrapper_match_t wrapper_match; + uintptr_t bridge_target; + char symbol[KZT_RELA_DIAGNOSTIC_NAME_LIMIT]; + char version[KZT_RELA_DIAGNOSTIC_NAME_LIMIT]; + kzt_rela_diagnostic_reason_domain_t reason_domain; + char reason[KZT_RELA_DIAGNOSTIC_NAME_LIMIT]; + char decision[KZT_RELA_DIAGNOSTIC_NAME_LIMIT]; + char writer_result[KZT_RELA_DIAGNOSTIC_NAME_LIMIT]; + int legacy_fallback; +} kzt_rela_diagnostic_record_t; + +/* + * Initialize before publishing to other threads. Capacity is immutable after + * initialization. The embedded lock serializes counter updates and snapshots; + * callers must not mutate the fields directly after publication. + */ +typedef struct kzt_rela_diagnostic_throttle { + unsigned long capacity; + unsigned long admitted; + unsigned long suppressed; + unsigned int lock; + unsigned int initialized; +} kzt_rela_diagnostic_throttle_t; + +typedef struct kzt_rela_diagnostic_throttle_snapshot { + unsigned long capacity; + unsigned long admitted; + unsigned long suppressed; +} kzt_rela_diagnostic_throttle_snapshot_t; + +typedef int (*kzt_rela_diagnostic_sink_fn)(const char *line, + size_t line_length, + void *opaque); + +/* + * The adapter only consumes caller-owned request/result snapshots. It copies + * text into the record and invokes the sink synchronously; it never probes, + * plans, creates bridges, writes a slot, or applies legacy fallback. + */ +typedef struct kzt_rela_immediate_diagnostic_input { + kzt_rela_diagnostic_mode_t mode; + const kzt_rela_immediate_candidate_request_t *request; + const kzt_rela_immediate_writer_result_t *result; + int legacy_fallback; + kzt_rela_diagnostic_throttle_t *throttle; + char *buffer; + size_t buffer_size; + kzt_rela_diagnostic_sink_fn sink; + void *sink_opaque; +} kzt_rela_immediate_diagnostic_input_t; + +typedef struct kzt_rela_immediate_diagnostic_result { + kzt_rela_diagnostic_emit_status_t status; + int record_present; + kzt_rela_diagnostic_record_t record; + kzt_rela_diagnostic_format_status_t format_status; + int sink_status; +} kzt_rela_immediate_diagnostic_result_t; + +kzt_rela_diagnostic_mode_t kzt_rela_diagnostic_mode_from_flags( + int diagnostics_enabled, + int write_enabled); + +const char *kzt_rela_diagnostic_reason_domain_name( + kzt_rela_diagnostic_reason_domain_t domain); + +int kzt_rela_immediate_diagnostic_record( + kzt_rela_diagnostic_mode_t mode, + const kzt_rela_immediate_candidate_request_t *request, + const kzt_rela_immediate_writer_result_t *result, + int legacy_fallback, + kzt_rela_diagnostic_record_t *record); + +kzt_rela_diagnostic_format_status_t kzt_rela_diagnostic_format( + const kzt_rela_diagnostic_record_t *record, + char *buffer, + size_t buffer_size); + +int kzt_rela_diagnostic_throttle_init( + kzt_rela_diagnostic_throttle_t *throttle, + unsigned long capacity); +int kzt_rela_diagnostic_throttle_try_admit( + kzt_rela_diagnostic_throttle_t *throttle); +int kzt_rela_diagnostic_throttle_snapshot( + kzt_rela_diagnostic_throttle_t *throttle, + kzt_rela_diagnostic_throttle_snapshot_t *snapshot); + +int kzt_rela_immediate_diagnostic_emit( + const kzt_rela_immediate_diagnostic_input_t *input, + kzt_rela_immediate_diagnostic_result_t *result); + +#endif diff --git a/target/i386/latx/include/kzt_rela_immediate_candidate.h b/target/i386/latx/include/kzt_rela_immediate_candidate.h new file mode 100644 index 00000000000..63b1217d091 --- /dev/null +++ b/target/i386/latx/include/kzt_rela_immediate_candidate.h @@ -0,0 +1,89 @@ +#ifndef KZT_RELA_IMMEDIATE_CANDIDATE_H +#define KZT_RELA_IMMEDIATE_CANDIDATE_H + +#include +#include + +#include "kzt_patch_planner.h" +#include "kzt_patch_spike_writer.h" + +typedef enum kzt_rela_immediate_candidate_status { + KZT_RELA_IMMEDIATE_CANDIDATE_SKIPPED = 0, + KZT_RELA_IMMEDIATE_CANDIDATE_PLANNED, + KZT_RELA_IMMEDIATE_CANDIDATE_FAIL_OPEN, +} kzt_rela_immediate_candidate_status_t; + +typedef enum kzt_rela_immediate_candidate_reason { + KZT_RELA_IMMEDIATE_CANDIDATE_REASON_NONE = 0, + KZT_RELA_IMMEDIATE_CANDIDATE_REASON_INVALID_ARGUMENT, + KZT_RELA_IMMEDIATE_CANDIDATE_REASON_NON_TARGET_RELOCATION, + KZT_RELA_IMMEDIATE_CANDIDATE_REASON_DEFERRED_LAZY_BINDING, + KZT_RELA_IMMEDIATE_CANDIDATE_REASON_MISSING_SLOT, + KZT_RELA_IMMEDIATE_CANDIDATE_REASON_MISSING_CURRENT_VALUE, + KZT_RELA_IMMEDIATE_CANDIDATE_REASON_MISSING_SYMBOL_NAME, + KZT_RELA_IMMEDIATE_CANDIDATE_REASON_MISSING_SYMBOL_VERSION, + KZT_RELA_IMMEDIATE_CANDIDATE_REASON_PLANNER_ERROR, +} kzt_rela_immediate_candidate_reason_t; + +typedef struct kzt_rela_immediate_candidate_request { + unsigned int relocation_type; + kzt_patch_table_kind_t table_kind; + size_t entry_index; + uintptr_t entry_addr; + + kzt_patch_object_ref_t source; + uintptr_t dynamic_addr; + uintptr_t load_bias; + unsigned long dynamic_view_generation; + int dynamic_view_available; + + uintptr_t slot_addr; + int slot_current_value_present; + uintptr_t slot_current_value; + int lazy_binding_deferred; + + uintptr_t expected_guest_target; + uintptr_t native_bridge_target; + uintptr_t legacy_target; + + unsigned long symbol_index; + const char *symbol_name; + kzt_symbol_version_evidence_t version_evidence; + const char *version; + + kzt_patch_object_ref_t current_owner; + kzt_patch_owner_match_t owner_match; + kzt_patch_wrapper_match_t wrapper_match; + const char *wrapper_name; + kzt_symbol_version_evidence_t wrapper_version_evidence; + const char *wrapper_symbol_version; +} kzt_rela_immediate_candidate_request_t; + +typedef struct kzt_rela_immediate_candidate_result { + kzt_rela_immediate_candidate_status_t status; + kzt_rela_immediate_candidate_reason_t reason; + int candidate_present; + kzt_patch_candidate_t candidate; + int decision_present; + kzt_patch_decision_t decision; +} kzt_rela_immediate_candidate_result_t; + +typedef struct kzt_rela_immediate_writer_result { + int planner_called; + int writer_called; + int skip_legacy_write; + kzt_rela_immediate_candidate_result_t plan; + kzt_patch_spike_record_t record; +} kzt_rela_immediate_writer_result_t; + +int kzt_rela_immediate_jump_slot_plan( + const kzt_rela_immediate_candidate_request_t *request, + kzt_rela_immediate_candidate_result_t *result); + +int kzt_rela_immediate_jump_slot_try_write( + const kzt_rela_immediate_candidate_request_t *request, + kzt_patch_spike_guard_t *guard, + const kzt_patch_spike_slot_ops_t *slot_ops, + kzt_rela_immediate_writer_result_t *result); + +#endif diff --git a/target/i386/latx/include/kzt_rela_request_enricher.h b/target/i386/latx/include/kzt_rela_request_enricher.h new file mode 100644 index 00000000000..c553960bbe3 --- /dev/null +++ b/target/i386/latx/include/kzt_rela_request_enricher.h @@ -0,0 +1,44 @@ +#ifndef KZT_RELA_REQUEST_ENRICHER_H +#define KZT_RELA_REQUEST_ENRICHER_H + +#include + +#include "kzt_guest_registry.h" +#include "kzt_owner_resolver.h" +#include "kzt_rela_immediate_candidate.h" +#include "kzt_wrapper_probe.h" + +#define KZT_RELA_REQUEST_ENRICHER_TEXT_LIMIT 256 + +typedef struct kzt_rela_request_enricher_text { + char soname[KZT_RELA_REQUEST_ENRICHER_TEXT_LIMIT]; + char path[KZT_RELA_REQUEST_ENRICHER_TEXT_LIMIT]; +} kzt_rela_request_enricher_text_t; + +typedef struct kzt_rela_request_enricher_input { + kzt_guest_registry_t *registry; + int slot_current_value_is_unresolved_stub; + const kzt_wrapper_probe_manifest_t *wrapper_manifest; + const kzt_wrapper_probe_bridge_ops_t *bridge_ops; +} kzt_rela_request_enricher_input_t; + +typedef struct kzt_rela_request_enricher_result { + int source_present; + int dynamic_view_present; + int owner_present; + int wrapper_present; + kzt_patch_object_ref_t source; + kzt_rela_request_enricher_text_t source_text; + kzt_owner_resolution_t owner_resolution; + kzt_wrapper_probe_result_t wrapper_probe; +} kzt_rela_request_enricher_result_t; + +void kzt_rela_request_enricher_result_init( + kzt_rela_request_enricher_result_t *result); + +int kzt_rela_immediate_request_enrich( + kzt_rela_immediate_candidate_request_t *request, + const kzt_rela_request_enricher_input_t *input, + kzt_rela_request_enricher_result_t *result); + +#endif diff --git a/target/i386/latx/include/kzt_rela_runtime_bridge.h b/target/i386/latx/include/kzt_rela_runtime_bridge.h new file mode 100644 index 00000000000..8925313a156 --- /dev/null +++ b/target/i386/latx/include/kzt_rela_runtime_bridge.h @@ -0,0 +1,36 @@ +#ifndef KZT_RELA_RUNTIME_BRIDGE_H +#define KZT_RELA_RUNTIME_BRIDGE_H + +#include + +#include "kzt_wrapper_bridge_provider.h" + +typedef struct box64context_s box64context_t; +typedef struct library_s library_t; + +int kzt_rela_runtime_wrapper_provider_prepare( + box64context_t *context, library_t *resolved_provider, + uintptr_t resolved_target, const char *symbol_name, + const char *symbol_version, + kzt_wrapper_bridge_provider_t *provider); + +int kzt_rela_runtime_wrapper_provider_prepare_with_version_evidence( + box64context_t *context, library_t *resolved_provider, + uintptr_t resolved_target, const char *symbol_name, + kzt_symbol_version_evidence_t version_evidence, + const char *symbol_version, + kzt_wrapper_bridge_provider_t *provider); + +int kzt_rela_runtime_wrapper_provider_discover( + box64context_t *context, library_t *resolved_provider, + const char *symbol_name, const char *symbol_version, + kzt_wrapper_bridge_provider_t *provider); + +int kzt_rela_runtime_wrapper_provider_discover_with_version_evidence( + box64context_t *context, library_t *resolved_provider, + const char *symbol_name, + kzt_symbol_version_evidence_t version_evidence, + const char *symbol_version, + kzt_wrapper_bridge_provider_t *provider); + +#endif diff --git a/target/i386/latx/include/kzt_rela_stub_detector.h b/target/i386/latx/include/kzt_rela_stub_detector.h new file mode 100644 index 00000000000..bb7fcc285cb --- /dev/null +++ b/target/i386/latx/include/kzt_rela_stub_detector.h @@ -0,0 +1,18 @@ +#ifndef KZT_RELA_STUB_DETECTOR_H +#define KZT_RELA_STUB_DETECTOR_H + +#include + +typedef enum kzt_rela_stub_coordinate { + KZT_RELA_STUB_COORDINATE_UNKNOWN = 0, + KZT_RELA_STUB_COORDINATE_LINK_TIME_RAW, + KZT_RELA_STUB_COORDINATE_RUNTIME_REBASED, +} kzt_rela_stub_coordinate_t; + +int kzt_rela_slot_current_is_unresolved_stub( + uintptr_t slot_current_value, kzt_rela_stub_coordinate_t coordinate, + intptr_t load_bias, + uintptr_t plt_start, uintptr_t plt_end, + uintptr_t gotplt_start, uintptr_t gotplt_end); + +#endif diff --git a/target/i386/latx/include/kzt_runtime_candidate_shadow.h b/target/i386/latx/include/kzt_runtime_candidate_shadow.h new file mode 100644 index 00000000000..48ad737a690 --- /dev/null +++ b/target/i386/latx/include/kzt_runtime_candidate_shadow.h @@ -0,0 +1,108 @@ +#ifndef KZT_RUNTIME_CANDIDATE_SHADOW_H +#define KZT_RUNTIME_CANDIDATE_SHADOW_H + +#include +#include + +#include "kzt_guest_registry.h" +#include "kzt_owner_resolver.h" +#include "kzt_runtime_got_plt_candidate.h" +#include "kzt_wrapper_probe.h" + +#define KZT_RUNTIME_CANDIDATE_SHADOW_DECISION_BUCKETS \ + ((size_t)KZT_PATCH_DECISION_APPROVED + 1) +#define KZT_RUNTIME_CANDIDATE_SHADOW_REASON_BUCKETS \ + ((size_t)KZT_PATCH_REASON_APPROVED_NATIVE_BRIDGE + 1) + +typedef enum kzt_runtime_candidate_shadow_status { + KZT_RUNTIME_CANDIDATE_SHADOW_OK = 0, + KZT_RUNTIME_CANDIDATE_SHADOW_FAIL_OPEN, + KZT_RUNTIME_CANDIDATE_SHADOW_ERROR, +} kzt_runtime_candidate_shadow_status_t; + +typedef enum kzt_runtime_candidate_shadow_reason { + KZT_RUNTIME_CANDIDATE_SHADOW_REASON_NONE = 0, + KZT_RUNTIME_CANDIDATE_SHADOW_REASON_INVALID_ARGUMENT, + KZT_RUNTIME_CANDIDATE_SHADOW_REASON_COLLECTOR_FAIL_OPEN, + KZT_RUNTIME_CANDIDATE_SHADOW_REASON_COLLECTOR_ERROR, + KZT_RUNTIME_CANDIDATE_SHADOW_REASON_RECORD_CAPACITY_EXCEEDED, + KZT_RUNTIME_CANDIDATE_SHADOW_REASON_OBJECT_GENERATION_CHANGED, + KZT_RUNTIME_CANDIDATE_SHADOW_REASON_PLANNER_ERROR, +} kzt_runtime_candidate_shadow_reason_t; + +typedef int (*kzt_runtime_candidate_shadow_expected_target_fn)( + const kzt_patch_candidate_t *candidate, + uintptr_t *expected_guest_target, + void *opaque); + +typedef enum kzt_runtime_candidate_shadow_stub_classification { + KZT_RUNTIME_CANDIDATE_SHADOW_STUB_NO_MATCH = 0, + KZT_RUNTIME_CANDIDATE_SHADOW_STUB_MATCH, + KZT_RUNTIME_CANDIDATE_SHADOW_STUB_UNKNOWN, +} kzt_runtime_candidate_shadow_stub_classification_t; + +typedef kzt_runtime_candidate_shadow_stub_classification_t +(*kzt_runtime_candidate_shadow_stub_classifier_fn)( + const kzt_patch_candidate_t *candidate, + void *opaque); + +/* Return zero only for one live object with one non-zero generation. */ +typedef int (*kzt_runtime_candidate_shadow_generation_query_fn)( + uintptr_t link_map_addr, + unsigned long *generation, + void *opaque); + +typedef struct kzt_runtime_candidate_shadow_record { + size_t candidate_index; + kzt_owner_resolution_t owner_resolution; + kzt_wrapper_probe_result_t wrapper_probe; + kzt_patch_decision_t decision; + /* Shadow records are audit-only and never consume a legacy target. */ + int audit_only; + int legacy_target_consumed; + int observe_only; + /* Eligibility is audit output only; it never authorizes a write. */ + int eligible; +} kzt_runtime_candidate_shadow_record_t; + +/* + * Contract: E is used only for owner resolution, B only comes from a + * side-effect-free bridge cache query, and the observed current slot is not + * legacy target L. This shadow API has no legacy_target input. + */ +typedef struct kzt_runtime_candidate_shadow_input { + const kzt_runtime_got_plt_candidate_request_t *collector_request; + kzt_guest_registry_t *registry; + const kzt_wrapper_probe_manifest_t *wrapper_manifest; + const kzt_wrapper_probe_bridge_ops_t *bridge_ops; + kzt_runtime_candidate_shadow_expected_target_fn + resolve_expected_guest_target; + void *expected_target_opaque; + /* Missing or UNKNOWN stub evidence is always treated as non-stub. */ + kzt_runtime_candidate_shadow_stub_classifier_fn classify_stub; + void *stub_classifier_opaque; + kzt_runtime_candidate_shadow_generation_query_fn query_generation; + void *generation_query_opaque; + kzt_runtime_candidate_shadow_record_t *records; + size_t record_capacity; +} kzt_runtime_candidate_shadow_input_t; + +typedef struct kzt_runtime_candidate_shadow_result { + kzt_runtime_candidate_shadow_status_t status; + kzt_runtime_candidate_shadow_reason_t reason; + kzt_runtime_got_plt_candidate_result_t collector_result; + size_t candidate_count; + size_t record_count; + size_t eligible_count; + size_t observe_only_count; + size_t decision_histogram[ + KZT_RUNTIME_CANDIDATE_SHADOW_DECISION_BUCKETS]; + size_t reason_histogram[ + KZT_RUNTIME_CANDIDATE_SHADOW_REASON_BUCKETS]; +} kzt_runtime_candidate_shadow_result_t; + +int kzt_runtime_candidate_shadow_run( + const kzt_runtime_candidate_shadow_input_t *input, + kzt_runtime_candidate_shadow_result_t *result); + +#endif diff --git a/target/i386/latx/include/kzt_runtime_got_plt_candidate.h b/target/i386/latx/include/kzt_runtime_got_plt_candidate.h new file mode 100644 index 00000000000..cb5b7c79af6 --- /dev/null +++ b/target/i386/latx/include/kzt_runtime_got_plt_candidate.h @@ -0,0 +1,75 @@ +#ifndef KZT_RUNTIME_GOT_PLT_CANDIDATE_H +#define KZT_RUNTIME_GOT_PLT_CANDIDATE_H + +#include +#include + +#include "kzt_guest_dynamic_view.h" +#include "kzt_guest_link_map_reader.h" +#include "kzt_patch_planner.h" + +typedef enum kzt_runtime_got_plt_candidate_status { + KZT_RUNTIME_GOT_PLT_CANDIDATE_OK = 0, + KZT_RUNTIME_GOT_PLT_CANDIDATE_FAIL_OPEN, + KZT_RUNTIME_GOT_PLT_CANDIDATE_ERROR, +} kzt_runtime_got_plt_candidate_status_t; + +typedef enum kzt_runtime_got_plt_candidate_reason { + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_NONE = 0, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_INVALID_ARGUMENT, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_DYNAMIC_VIEW_UNAVAILABLE, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_DT_REL_UNSUPPORTED, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_MISSING_DYNAMIC_FIELD, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_MALFORMED_TABLE, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_TABLE_OVERFLOW, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_RELOCATION_READ_FAILED, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_SLOT_OVERFLOW, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_SLOT_READ_FAILED, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_SYMBOL_READ_FAILED, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_MALFORMED_SYMBOL_NAME, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_VERSION_READ_FAILED, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_MALFORMED_SYMBOL_VERSION, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_CAPACITY_EXCEEDED, +} kzt_runtime_got_plt_candidate_reason_t; + +typedef struct kzt_runtime_got_plt_candidate_request { + const kzt_guest_dynamic_view_t *view; + const kzt_guest_link_map_reader_ops_t *reader_ops; + const kzt_patch_object_ref_t *source; + unsigned long dynamic_view_generation; + /* Production can audit one already-selected relocation without needing + * storage proportional to the object's complete relocation table. */ + int only_entry; + kzt_patch_table_kind_t only_table_kind; + size_t only_entry_index; + kzt_patch_candidate_t *candidates; + size_t candidate_capacity; + char *string_storage; + size_t string_storage_size; +} kzt_runtime_got_plt_candidate_request_t; + +typedef struct kzt_runtime_got_plt_candidate_result { + kzt_runtime_got_plt_candidate_status_t status; + kzt_runtime_got_plt_candidate_reason_t reason; + int patch_reason_present; + kzt_patch_reason_t patch_reason; + size_t candidate_count; + kzt_patch_table_kind_t table_kind; + size_t entry_index; + uintptr_t entry_addr; + uintptr_t slot_addr; + uintptr_t read_error_addr; + kzt_symbol_version_evidence_t version_evidence; +} kzt_runtime_got_plt_candidate_result_t; + +int kzt_runtime_got_plt_candidates_collect( + const kzt_runtime_got_plt_candidate_request_t *request, + kzt_runtime_got_plt_candidate_result_t *result); + +const char *kzt_runtime_got_plt_candidate_status_name( + kzt_runtime_got_plt_candidate_status_t status); + +const char *kzt_runtime_got_plt_candidate_reason_name( + kzt_runtime_got_plt_candidate_reason_t reason); + +#endif diff --git a/target/i386/latx/include/kzt_wrapper_bridge_provider.h b/target/i386/latx/include/kzt_wrapper_bridge_provider.h new file mode 100644 index 00000000000..e3760eed5c4 --- /dev/null +++ b/target/i386/latx/include/kzt_wrapper_bridge_provider.h @@ -0,0 +1,72 @@ +#ifndef KZT_WRAPPER_BRIDGE_PROVIDER_H +#define KZT_WRAPPER_BRIDGE_PROVIDER_H + +#include +#include + +#include "kzt_wrapper_probe.h" + +typedef void (*kzt_wrapper_bridge_abi_wrapper_t)(uintptr_t fnc); + +#define KZT_WRAPPER_BRIDGE_NATIVE_NAME_MAX 256 + +typedef struct kzt_wrapper_bridge_provider_match { + const char *wrapper_name; + char native_name[KZT_WRAPPER_BRIDGE_NATIVE_NAME_MAX]; + kzt_wrapper_bridge_abi_wrapper_t abi_wrapper; + uintptr_t native_symbol; + uintptr_t resolved_bridge_target; + void *context_owner; + void *wrapper_provider; + void *native_lookup_handle; + void *native_owner; + void *bridge_owner; + void *bridge_storage; + int stack_bytes; + int custom_wrapper; + int resolved_bridge_exact; + int wrapper_provider_lifetime_bound; + int native_owner_lifetime_bound; + int bridge_owner_lifetime_bound; +} kzt_wrapper_bridge_provider_match_t; + +typedef int (*kzt_wrapper_bridge_provider_inspect_fn)( + void *library, const char *symbol_name, const char *symbol_version, + kzt_wrapper_bridge_provider_match_t *match, void *opaque); + +typedef uintptr_t (*kzt_wrapper_bridge_provider_check_fn)( + const kzt_wrapper_bridge_provider_match_t *match, void *opaque); + +typedef uintptr_t (*kzt_wrapper_bridge_provider_add_fn)( + const kzt_wrapper_bridge_provider_match_t *match, + const kzt_wrapper_probe_bridge_request_t *request, void *opaque); + +typedef struct kzt_wrapper_bridge_provider_runtime_ops { + kzt_wrapper_bridge_provider_inspect_fn inspect_library; + kzt_wrapper_bridge_provider_check_fn check_bridge; + kzt_wrapper_bridge_provider_add_fn add_bridge; + void *opaque; +} kzt_wrapper_bridge_provider_runtime_ops_t; + +typedef struct kzt_wrapper_bridge_provider { + kzt_wrapper_probe_entry_t entry; + kzt_wrapper_probe_manifest_t manifest; + kzt_wrapper_probe_bridge_ops_t bridge_ops; + kzt_wrapper_bridge_provider_match_t match; + kzt_wrapper_bridge_provider_runtime_ops_t runtime_ops; +} kzt_wrapper_bridge_provider_t; + +int kzt_wrapper_bridge_provider_prepare( + kzt_wrapper_bridge_provider_t *provider, void *const *libraries, + size_t library_count, const char *symbol_name, + const char *symbol_version, + const kzt_wrapper_bridge_provider_runtime_ops_t *runtime_ops); + +int kzt_wrapper_bridge_provider_prepare_with_version_evidence( + kzt_wrapper_bridge_provider_t *provider, void *const *libraries, + size_t library_count, const char *symbol_name, + kzt_symbol_version_evidence_t version_evidence, + const char *symbol_version, + const kzt_wrapper_bridge_provider_runtime_ops_t *runtime_ops); + +#endif diff --git a/target/i386/latx/include/kzt_wrapper_probe.h b/target/i386/latx/include/kzt_wrapper_probe.h new file mode 100644 index 00000000000..86b56c7777c --- /dev/null +++ b/target/i386/latx/include/kzt_wrapper_probe.h @@ -0,0 +1,87 @@ +#ifndef KZT_WRAPPER_PROBE_H +#define KZT_WRAPPER_PROBE_H + +#include +#include + +#include "kzt_patch_planner.h" + +typedef enum kzt_wrapper_probe_bridge_source { + KZT_WRAPPER_PROBE_BRIDGE_NONE = 0, + KZT_WRAPPER_PROBE_BRIDGE_CACHE, + KZT_WRAPPER_PROBE_BRIDGE_ADD_BRIDGE, +} kzt_wrapper_probe_bridge_source_t; + +typedef struct kzt_wrapper_probe_entry { + const char *symbol_name; + kzt_symbol_version_evidence_t symbol_version_evidence; + const char *symbol_version; + const char *wrapper_name; + kzt_symbol_version_evidence_t wrapper_version_evidence; + const char *wrapper_symbol_version; + uintptr_t native_symbol; +} kzt_wrapper_probe_entry_t; + +typedef struct kzt_wrapper_probe_manifest { + int available; + const char *manifest_name; + const kzt_wrapper_probe_entry_t *entries; + size_t entry_count; +} kzt_wrapper_probe_manifest_t; + +typedef struct kzt_wrapper_probe_request { + const char *symbol_name; + kzt_symbol_version_evidence_t symbol_version_evidence; + const char *symbol_version; +} kzt_wrapper_probe_request_t; + +typedef struct kzt_wrapper_probe_bridge_request { + const char *symbol_name; + kzt_symbol_version_evidence_t symbol_version_evidence; + const char *symbol_version; + const char *wrapper_name; + kzt_symbol_version_evidence_t wrapper_version_evidence; + const char *wrapper_symbol_version; + uintptr_t native_symbol; +} kzt_wrapper_probe_bridge_request_t; + +typedef uintptr_t (*kzt_wrapper_probe_check_bridge_fn)( + uintptr_t native_symbol, void *opaque); + +typedef uintptr_t (*kzt_wrapper_probe_add_bridge_fn)( + const kzt_wrapper_probe_bridge_request_t *request, void *opaque); + +typedef struct kzt_wrapper_probe_bridge_ops { + kzt_wrapper_probe_check_bridge_fn check_bridge; + kzt_wrapper_probe_add_bridge_fn add_bridge; + void *opaque; +} kzt_wrapper_probe_bridge_ops_t; + +typedef struct kzt_wrapper_probe_result { + kzt_patch_wrapper_match_t wrapper_match; + const char *wrapper_name; + kzt_symbol_version_evidence_t wrapper_version_evidence; + const char *wrapper_symbol_version; + uintptr_t native_symbol; + uintptr_t bridge_target; + kzt_wrapper_probe_bridge_source_t bridge_source; +} kzt_wrapper_probe_result_t; + +int kzt_wrapper_probe_minimal_manifest( + const kzt_wrapper_probe_manifest_t *manifest, + const kzt_wrapper_probe_request_t *request, + const kzt_wrapper_probe_bridge_ops_t *bridge_ops, + kzt_wrapper_probe_result_t *result); + +void kzt_wrapper_probe_apply_to_candidate( + const kzt_wrapper_probe_result_t *probe, + kzt_patch_candidate_t *candidate); + +void kzt_wrapper_probe_apply_to_decision_request( + const kzt_wrapper_probe_result_t *probe, + kzt_patch_wrapper_match_t *wrapper_match, + const char **wrapper_name, + const char **wrapper_symbol_version, + uintptr_t *bridge_target); + +#endif diff --git a/target/i386/latx/include/latx-options.h b/target/i386/latx/include/latx-options.h index 96519c8a884..118b7f54650 100644 --- a/target/i386/latx/include/latx-options.h +++ b/target/i386/latx/include/latx-options.h @@ -22,6 +22,13 @@ extern int option_flag_reduction; extern int option_tu_link; #endif +#if defined(CONFIG_LATX_KZT) +extern int option_kzt_lazy_diagnostics; +extern int option_kzt_patch_spike; +extern int option_kzt_patch_spike_write; +extern unsigned long option_kzt_patch_spike_budget; +#endif + #ifdef CONFIG_LATX_AVX_OPT extern int option_avx_cpuid; #endif /* CONFIG_LATX_AVX_OPT */ @@ -131,7 +138,14 @@ extern unsigned long long counter_mips_tr; #if defined(CONFIG_LATX) && defined(CONFIG_LATX_KZT) #define ENVSUP_KZT \ - ENVFUN(LATX_KZT, handle_arg_latx_kzt) + ENVFUN(LATX_KZT, handle_arg_latx_kzt) \ + ENVFUN(LATX_KZT_LAZY_DIAGNOSTICS, \ + handle_arg_latx_kzt_lazy_diagnostics) \ + ENVFUN(LATX_KZT_REGISTRY_DIAGNOSTICS, \ + handle_arg_latx_kzt_registry_diagnostics) \ + ENVFUN(LATX_KZT_PATCH_SPIKE, handle_arg_latx_kzt_patch_spike) \ + ENVFUN(LATX_KZT_PATCH_SPIKE_WRITE, handle_arg_latx_kzt_patch_spike_write) \ + ENVFUN(LATX_KZT_PATCH_SPIKE_BUDGET, handle_arg_latx_kzt_patch_spike_budget) #else #define ENVSUP_KZT #endif diff --git a/target/i386/latx/include/librarian.h b/target/i386/latx/include/librarian.h index bd47496bf3d..413847d495e 100755 --- a/target/i386/latx/include/librarian.h +++ b/target/i386/latx/include/librarian.h @@ -30,6 +30,10 @@ kh_mapsymbols_t* GetWeakSymbol(lib_t* maplib); kh_mapsymbols_t* GetLocalSymbol(lib_t* maplib); kh_mapsymbols_t* GetGlobalData(lib_t* maplib); int AddNeededLib(lib_t* maplib, needed_libs_t* neededlibs, library_t *deplib, int local, int bindnow, const char** paths, int npath, box64context_t* box64); // 0=success, 1=error +int AddNeededLibWithLibrary(lib_t* maplib, needed_libs_t* neededlibs, + library_t *deplib, int local, int bindnow, + const char* path, box64context_t* box64, + library_t **exact_library); // AddNeededLib return semantics; exact_library is NULL on failure int AddNeededLib_add(lib_t* maplib, needed_libs_t* neededlibs, library_t* deplib, int local, const char* path, box64context_t* box64); library_t* GetLibMapLib(lib_t* maplib, const char* name); library_t* GetLibInternal(const char* name); @@ -37,6 +41,7 @@ uintptr_t FindGlobalSymbol(lib_t *maplib, const char* name, int version, const c int GetNoSelfSymbolStartEnd(lib_t *maplib, const char* name, uintptr_t* start, uintptr_t* end, elfheader_t* self, int version, const char* vername); int GetSelfSymbolStartEnd(lib_t *maplib, const char* name, uintptr_t* start, uintptr_t* end, elfheader_t *self, int version, const char* vername); int GetGlobalSymbolStartEnd(lib_t *maplib, const char* name, uintptr_t* start, uintptr_t* end, elfheader_t *self, int version, const char* vername); +int GetGlobalSymbolStartEndWithProvider(lib_t *maplib, const char* name, uintptr_t* start, uintptr_t* end, elfheader_t *self, int version, const char* vername, library_t **provider); int GetGlobalNoWeakSymbolStartEnd(lib_t *maplib, const char* name, uintptr_t* start, uintptr_t* end, int version, const char* vername); int GetLocalSymbolStartEnd(lib_t *maplib, const char* name,khint_t pre_k, uintptr_t* start, uintptr_t* end, elfheader_t *self, int version, const char* vername); int GetNoWeakSymbolStartEnd(lib_t *maplib, const char* name, uintptr_t* start, uintptr_t* end, elfheader_t *self, int version, const char* vername); diff --git a/target/i386/latx/latx-options.c b/target/i386/latx/latx-options.c index afa8c3d1d99..308714cf39c 100644 --- a/target/i386/latx/latx-options.c +++ b/target/i386/latx/latx-options.c @@ -13,6 +13,10 @@ #if defined(CONFIG_LATX_KZT) int option_kzt = 0; +int option_kzt_lazy_diagnostics = 0; +int option_kzt_patch_spike = 0; +int option_kzt_patch_spike_write = 0; +unsigned long option_kzt_patch_spike_budget = 0; #endif #ifdef CONFIG_LATX_AVX_OPT @@ -274,6 +278,12 @@ void options_init(void) option_fast_atomic = 1; else option_fast_atomic = 0; + +#if defined(CONFIG_LATX_KZT) + option_kzt_patch_spike = 0; + option_kzt_patch_spike_write = 0; + option_kzt_patch_spike_budget = 0; +#endif } #define OPTIONS_IMM_REG 0 diff --git a/tests/unit/kzt/guest_e2e/build_guest_probe.sh b/tests/unit/kzt/guest_e2e/build_guest_probe.sh new file mode 100755 index 00000000000..b2aed7302df --- /dev/null +++ b/tests/unit/kzt/guest_e2e/build_guest_probe.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +build_dir=${KZT_GUEST_BUILD_DIR:-${TMPDIR:-/tmp}/kzt-guest-e2e} +guest_cc=${KZT_GUEST_CC:-x86_64-linux-gnu-gcc} +guest_readelf=${KZT_GUEST_READELF:-readelf} +guest_libc=${KZT_GUEST_LIBC:--lc} + +read -r -a cc_flags <<< "${KZT_GUEST_CC_FLAGS:-}" +read -r -a launcher_flags <<< "${KZT_GUEST_CC_LAUNCHER_FLAGS:-}" + +run_cc() +{ + if [[ -n ${KZT_GUEST_CC_LAUNCHER:-} ]]; then + "${KZT_GUEST_CC_LAUNCHER}" "${launcher_flags[@]}" \ + "$guest_cc" "${cc_flags[@]}" "$@" + else + "$guest_cc" "${cc_flags[@]}" "$@" + fi +} + +mkdir -p "$build_dir" + +run_cc -O2 -fPIC -shared -nostdlib \ + -I"$script_dir" \ + "$script_dir/kzt_guest_probe.c" \ + "$guest_libc" \ + -o "$build_dir/libkzt_guest_probe.so" + +run_cc -O2 -nostdlib -Wl,-e,_start \ + -I"$script_dir" \ + "$script_dir/kzt_guest_main.c" \ + -L"$build_dir" -lkzt_guest_probe \ + -o "$build_dir/kzt_guest_main" + +header=$($guest_readelf -h "$build_dir/kzt_guest_main") +if ! grep -Eq 'Machine:.*(Advanced Micro Devices X86-64|X86-64)' \ + <<< "$header"; then + echo "Guest main is not an x86-64 ELF." >&2 + exit 1 +fi + +relocations=$($guest_readelf -rW "$build_dir/libkzt_guest_probe.so") +uname_jump_slots=$(grep -Ec \ + 'R_X86_64_(JUMP_SLOT|JUMP_SLO).*uname(@|[[:space:]])' \ + <<< "$relocations" || true) +if [[ $uname_jump_slots -ne 1 ]]; then + echo "Guest DSO must contain exactly one lazy uname JUMP_SLOT." >&2 + exit 1 +fi + +symbols=$($guest_readelf -Ws "$build_dir/libkzt_guest_probe.so") +uname_versions=$(sed -n \ + 's/.* UND uname@\(GLIBC_[^[:space:]]*\).*/\1/p' \ + <<< "$symbols" | sort -u) +if [[ -z $uname_versions || $uname_versions == *$'\n'* ]]; then + echo "Guest DSO must require one versioned uname symbol." >&2 + exit 1 +fi +printf '%s\n' "$uname_versions" > "$build_dir/guest-symbol-version.txt" + +dynamic=$($guest_readelf -d "$build_dir/libkzt_guest_probe.so") +if grep -Eq '(BIND_NOW|FLAGS.*NOW)' <<< "$dynamic"; then + echo "Guest DSO must use lazy binding." >&2 + exit 1 +fi + +main_dynamic=$($guest_readelf -d "$build_dir/kzt_guest_main") +if ! grep -Fq 'Shared library: [libkzt_guest_probe.so]' \ + <<< "$main_dynamic"; then + echo "Guest main is not linked to the probe DSO." >&2 + exit 1 +fi + +printf 'Guest fixture built in %s\n' "$build_dir" +printf 'uname version: %s\n' "$uname_versions" +printf 'Run: python3 tests/unit/kzt/test_real_guest_e2e.py '\''\n' +printf ' --latx --guest-root '\''\n' +printf ' --host-libc '\''\n' +printf ' --fixture-dir %q\n' "$build_dir" diff --git a/tests/unit/kzt/guest_e2e/kzt_guest_main.c b/tests/unit/kzt/guest_e2e/kzt_guest_main.c new file mode 100644 index 00000000000..8c66034e358 --- /dev/null +++ b/tests/unit/kzt/guest_e2e/kzt_guest_main.c @@ -0,0 +1,79 @@ +#include "kzt_guest_probe.h" + +static long raw_syscall3(long number, long arg1, long arg2, long arg3) +{ + long result; + + __asm__ volatile( + "syscall" + : "=a"(result) + : "a"(number), "D"(arg1), "S"(arg2), "d"(arg3) + : "rcx", "r11", "memory"); + return result; +} + +static void raw_exit(int status) +{ + __asm__ volatile( + "syscall" + : + : "a"(60L), "D"((long)status) + : "rcx", "r11", "memory"); + __builtin_unreachable(); +} + +static char *append_text(char *cursor, const char *text) +{ + while (*text) { + *cursor++ = *text++; + } + return cursor; +} + +static char *append_hex(char *cursor, uintptr_t value) +{ + static const char digits[] = "0123456789abcdef"; + int shift; + + cursor = append_text(cursor, "0x"); + for (shift = (int)(sizeof(value) * 8) - 4; shift > 0; shift -= 4) { + if ((value >> shift) != 0) { + break; + } + } + for (; shift >= 0; shift -= 4) { + *cursor++ = digits[(value >> shift) & 0xf]; + } + return cursor; +} + +void _start(void) +{ + static const char failure[] = "KZT_GUEST_E2E_FAIL\n"; + struct kzt_guest_probe_result result; + char success[256]; + char *cursor = success; + + if (kzt_guest_probe(&result) != 0) { + (void)raw_syscall3(1, 2, (long)failure, sizeof(failure) - 1); + raw_exit(1); + } + cursor = append_text(cursor, "KZT_GUEST_E2E_OK calls=2 slot="); + cursor = append_hex(cursor, result.slot_addr); + cursor = append_text(cursor, " before="); + cursor = append_hex(cursor, result.before); + cursor = append_text(cursor, " after_first="); + cursor = append_hex(cursor, result.after_first); + cursor = append_text(cursor, " after_second="); + cursor = append_hex(cursor, result.after_second); + cursor = append_text(cursor, " first_ns="); + cursor = append_hex(cursor, result.first_call_ns); + cursor = append_text(cursor, " second_ns="); + cursor = append_hex(cursor, result.second_call_ns); + *cursor++ = '\n'; + if (raw_syscall3(1, 1, (long)success, cursor - success) != + cursor - success) { + raw_exit(2); + } + raw_exit(0); +} diff --git a/tests/unit/kzt/guest_e2e/kzt_guest_probe.c b/tests/unit/kzt/guest_e2e/kzt_guest_probe.c new file mode 100644 index 00000000000..9bd722a1f36 --- /dev/null +++ b/tests/unit/kzt/guest_e2e/kzt_guest_probe.c @@ -0,0 +1,85 @@ +#include "kzt_guest_probe.h" + +struct guest_utsname { + char sysname[65]; + char nodename[65]; + char release[65]; + char version[65]; + char machine[65]; + char domainname[65]; +}; + +extern int uname(struct guest_utsname *result); + +struct guest_timespec { + long seconds; + long nanoseconds; +}; + +static uint64_t monotonic_raw_ns(void) +{ + struct guest_timespec value; + long status; + + __asm__ volatile( + "syscall" + : "=a"(status) + : "a"(228L), "D"(4L), "S"((long)&value) + : "rcx", "r11", "memory"); + if (status != 0) { + return 0; + } + return (uint64_t)value.seconds * 1000000000ULL + + (uint64_t)value.nanoseconds; +} + +static uintptr_t *uname_jump_slot(void) +{ + const unsigned char *plt; + uint32_t raw_displacement; + int32_t displacement; + + __asm__ volatile("lea uname@PLT(%%rip), %0" : "=r"(plt)); + if (plt[0] != 0xff || plt[1] != 0x25) { + return 0; + } + raw_displacement = (uint32_t)plt[2] | + ((uint32_t)plt[3] << 8) | + ((uint32_t)plt[4] << 16) | + ((uint32_t)plt[5] << 24); + displacement = (int32_t)raw_displacement; + return (uintptr_t *)(plt + 6 + displacement); +} + +int kzt_guest_probe(struct kzt_guest_probe_result *result) +{ + struct guest_utsname utsname; + uintptr_t *slot; + uint64_t before_call; + uint64_t after_call; + + if (!result || !(slot = uname_jump_slot())) { + return 2; + } + result->slot_addr = (uintptr_t)slot; + result->before = *(volatile uintptr_t *)slot; + + before_call = monotonic_raw_ns(); + if (!before_call || uname(&utsname) != 0 || + utsname.machine[0] == '\0' || + !(after_call = monotonic_raw_ns()) || after_call < before_call) { + return 1; + } + result->first_call_ns = after_call - before_call; + result->after_first = *(volatile uintptr_t *)slot; + + before_call = monotonic_raw_ns(); + if (!before_call || uname(&utsname) != 0 || + utsname.machine[0] == '\0' || + !(after_call = monotonic_raw_ns()) || after_call < before_call) { + return 1; + } + result->second_call_ns = after_call - before_call; + result->after_second = *(volatile uintptr_t *)slot; + return 0; +} diff --git a/tests/unit/kzt/guest_e2e/kzt_guest_probe.h b/tests/unit/kzt/guest_e2e/kzt_guest_probe.h new file mode 100644 index 00000000000..9a676aa42f0 --- /dev/null +++ b/tests/unit/kzt/guest_e2e/kzt_guest_probe.h @@ -0,0 +1,19 @@ +#ifndef KZT_GUEST_PROBE_H +#define KZT_GUEST_PROBE_H + +#include + +#define KZT_GUEST_PROBE_CALL_COUNT 2 + +struct kzt_guest_probe_result { + uintptr_t slot_addr; + uintptr_t before; + uintptr_t after_first; + uintptr_t after_second; + uint64_t first_call_ns; + uint64_t second_call_ns; +}; + +int kzt_guest_probe(struct kzt_guest_probe_result *result); + +#endif diff --git a/tests/unit/kzt/kzt_test_options.c b/tests/unit/kzt/kzt_test_options.c new file mode 100644 index 00000000000..24587e7482b --- /dev/null +++ b/tests/unit/kzt/kzt_test_options.c @@ -0,0 +1,3 @@ +int option_kzt_patch_spike; +int option_kzt_patch_spike_write; +unsigned long option_kzt_patch_spike_budget; diff --git a/tests/unit/kzt/kzt_test_options.h b/tests/unit/kzt/kzt_test_options.h new file mode 100644 index 00000000000..2ca906d5f3f --- /dev/null +++ b/tests/unit/kzt/kzt_test_options.h @@ -0,0 +1,8 @@ +#ifndef KZT_TEST_OPTIONS_H +#define KZT_TEST_OPTIONS_H + +extern int option_kzt_patch_spike; +extern int option_kzt_patch_spike_write; +extern unsigned long option_kzt_patch_spike_budget; + +#endif diff --git a/tests/unit/kzt/test_dlopen_recycle_transaction.c b/tests/unit/kzt/test_dlopen_recycle_transaction.c new file mode 100644 index 00000000000..6d77ea2a0bc --- /dev/null +++ b/tests/unit/kzt/test_dlopen_recycle_transaction.c @@ -0,0 +1,354 @@ +#include +#include +#include + +#include "dlopen_recycle_transaction.h" +#include "elf_plt_relocation.h" +#include "kzt_guest_library_binding.h" +#include "kzt_guest_registry.h" + +typedef struct fake_library { int id; } fake_library_t; + +typedef struct recycle_fixture { + kzt_guest_library_bindings_t *bindings; + kzt_guest_registry_t *registry; + fake_library_t library; + uintptr_t link_map_addr; + unsigned long generation; + int active; + int prepared; + int guest_open_result; + int reload_result; + int plt_relocation_result; + int guest_close_calls; + int guest_open_calls; + uintptr_t delta; + uintptr_t internal_l_addr; + int latx_hasfix; + int had_relocate_elf; + int had_relocate_plt; + int latx_type; +} recycle_fixture_t; + +static int failures; + +#define CHECK(name, expr) do { if (!(expr)) { \ + fprintf(stderr, "FAIL %s\n", name); ++failures; } } while (0) + +static kzt_guest_object_observation_t observation(uintptr_t map) +{ + return (kzt_guest_object_observation_t){ + .link_map_addr = map, + .load_bias = { .value = 0x500000, .status = KZT_GUEST_FIELD_OK }, + .dynamic_addr = { .status = KZT_GUEST_FIELD_UNKNOWN }, + .map_start = { .status = KZT_GUEST_FIELD_UNKNOWN }, + .map_end = { .status = KZT_GUEST_FIELD_UNKNOWN }, + .namespace_id = { .value = 0, .status = KZT_GUEST_FIELD_OK }, + .path = { .status = KZT_GUEST_FIELD_UNKNOWN }, + .soname = { .status = KZT_GUEST_FIELD_UNKNOWN }, + .dynamic_view_status = KZT_GUEST_FIELD_NOT_PARSED, + }; +} + +static kzt_guest_library_binding_key_t binding_key(uintptr_t map, + unsigned long generation) +{ + return (kzt_guest_library_binding_key_t){ + .link_map_addr = map, + .generation = generation, + .namespace_id = 0, + .namespace_kind = KZT_GUEST_LIBRARY_NAMESPACE_MAIN, + }; +} + +static unsigned long current_generation(recycle_fixture_t *fixture) +{ + kzt_guest_object_snapshot_t *snapshot = NULL; + unsigned long generation = 0; + CHECK("transaction registry snapshot", + kzt_guest_registry_find_by_link_map( + fixture->registry, fixture->link_map_addr, &snapshot) == 0 && + snapshot != NULL); + if (snapshot) generation = snapshot->generation; + kzt_guest_object_snapshot_free(snapshot); + return generation; +} + +static void fixture_init(recycle_fixture_t *fixture) +{ + kzt_guest_object_observation_t observed; + kzt_guest_library_binding_key_t key; + *fixture = (recycle_fixture_t){ + .bindings = kzt_guest_library_bindings_init(), + .registry = kzt_guest_registry_init(), + .library = { .id = 1 }, + .link_map_addr = 0xd000, + .active = 1, + .delta = 0x400000, + .internal_l_addr = 0x400000, + .latx_hasfix = 1, + .had_relocate_elf = 1, + .had_relocate_plt = 1, + .latx_type = 3, + }; + observed = observation(fixture->link_map_addr); + CHECK("transaction track", kzt_guest_library_track( + fixture->bindings, (library_t *)&fixture->library) == 0); + CHECK("transaction initial observe", kzt_guest_registry_observe( + fixture->registry, &observed) == KZT_GUEST_REGISTRY_ADDED); + fixture->generation = current_generation(fixture); + key = binding_key(fixture->link_map_addr, fixture->generation); + CHECK("transaction initial binding observation", + kzt_guest_library_note_observation(fixture->bindings, &key) == + KZT_GUEST_LIBRARY_BINDING_UNCHANGED); + CHECK("transaction initial pair", kzt_guest_library_note_exact_pair( + fixture->bindings, fixture->link_map_addr, + (library_t *)&fixture->library, + KZT_GUEST_LIBRARY_OBJECT_EMULATED) == + KZT_GUEST_LIBRARY_BINDING_ADDED); + kzt_guest_library_inactivate(fixture->bindings, fixture->registry, + (library_t *)&fixture->library, + fixture->link_map_addr); + fixture->active = 0; + /* Model the stale pointer retained by the old wrappers after dlclose. */ + fixture->link_map_addr = 0xd000; +} + +static void fixture_destroy(recycle_fixture_t *fixture) +{ + kzt_guest_library_bindings_destroy(&fixture->bindings); + kzt_guest_registry_destroy(&fixture->registry); +} + +static void recycle_prepare(void *opaque) +{ + recycle_fixture_t *fixture = opaque; + fixture->prepared = 1; + fixture->link_map_addr = 0; + fixture->delta = 0; + fixture->internal_l_addr = 0; + fixture->latx_hasfix = 0; + fixture->had_relocate_elf = 0; + fixture->had_relocate_plt = 0; + fixture->latx_type = 0; +} + +static int recycle_guest_open(void *opaque) +{ + recycle_fixture_t *fixture = opaque; + kzt_guest_object_observation_t observed; + kzt_guest_library_binding_key_t key; + if (fixture->guest_open_result != 0) + return fixture->guest_open_result; + ++fixture->guest_open_calls; + fixture->link_map_addr = 0xd000; + fixture->delta = 0x500000 + (uintptr_t)fixture->guest_open_calls * 0x100000; + fixture->internal_l_addr = fixture->delta; + fixture->latx_hasfix = 1; + fixture->active = 1; + observed = observation(fixture->link_map_addr); + CHECK("transaction guest observation", kzt_guest_registry_observe( + fixture->registry, &observed) == KZT_GUEST_REGISTRY_ADDED); + fixture->generation = current_generation(fixture); + key = binding_key(fixture->link_map_addr, fixture->generation); + CHECK("transaction guest binding observation", + kzt_guest_library_note_observation(fixture->bindings, &key) == + KZT_GUEST_LIBRARY_BINDING_UNCHANGED); + CHECK("transaction guest incarnation live", + kzt_guest_library_reactivate( + fixture->bindings, (library_t *)&fixture->library) == 0); + return 0; +} + +static int simulated_plt_relocation(void *opaque, int *need_resolver) +{ + recycle_fixture_t *fixture = opaque; + if (need_resolver) + *need_resolver = 1; + return fixture->plt_relocation_result; +} + +static int recycle_reload(void *opaque) +{ + recycle_fixture_t *fixture = opaque; + kzt_guest_library_binding_result_t pair_result; + int need_resolver = 0; + if (fixture->reload_result != 0) + return fixture->reload_result; + fixture->had_relocate_elf = 1; + if (elf_plt_relocation_apply(simulated_plt_relocation, fixture, + &need_resolver) != 0) + return -1; + fixture->had_relocate_plt = 1; + fixture->active = 1; + CHECK("transaction reactivate", kzt_guest_library_reactivate( + fixture->bindings, (library_t *)&fixture->library) == 0); + pair_result = kzt_guest_library_note_exact_pair( + fixture->bindings, fixture->link_map_addr, + (library_t *)&fixture->library, KZT_GUEST_LIBRARY_OBJECT_EMULATED); + CHECK("transaction publish pair", + pair_result == KZT_GUEST_LIBRARY_BINDING_ADDED || + pair_result == KZT_GUEST_LIBRARY_BINDING_UNCHANGED); + return 0; +} + +static void recycle_rollback(void *opaque, int guest_opened) +{ + recycle_fixture_t *fixture = opaque; + if (guest_opened && fixture->link_map_addr) + ++fixture->guest_close_calls; + kzt_guest_library_inactivate(fixture->bindings, fixture->registry, + (library_t *)&fixture->library, + fixture->link_map_addr); + fixture->active = 0; + fixture->link_map_addr = 0; + fixture->delta = 0; + fixture->internal_l_addr = 0; + fixture->latx_hasfix = 0; + fixture->had_relocate_elf = 0; + fixture->had_relocate_plt = 0; + fixture->latx_type = 0; +} + +static void check_failed_state(const char *prefix, recycle_fixture_t *fixture) +{ + kzt_guest_library_binding_state_t lifecycle; + size_t pending = 99, live_entries = 99; + (void)prefix; + CHECK("failure active false", fixture->active == 0); + CHECK("failure link_map cleared", fixture->link_map_addr == 0); + CHECK("failure delta cleared", fixture->delta == 0); + CHECK("failure internal l_addr cleared", fixture->internal_l_addr == 0); + CHECK("failure latx_hasfix cleared", fixture->latx_hasfix == 0); + CHECK("failure relocate flag cleared", fixture->had_relocate_elf == 0); + CHECK("failure PLT flag cleared", fixture->had_relocate_plt == 0); + CHECK("failure LATX type cleared", fixture->latx_type == 0); + CHECK("failure binding snapshot", + kzt_guest_library_binding_test_snapshot( + fixture->bindings, (library_t *)&fixture->library, + &lifecycle, &pending, &live_entries) == 0); + CHECK("failure lifecycle dead", + lifecycle == KZT_GUEST_LIBRARY_BINDING_DEAD); + CHECK("failure pending empty", pending == 0); + CHECK("failure live pair absent", live_entries == 0); +} + +static void test_guest_open_failure(void) +{ + recycle_fixture_t fixture; + size_t count = 0; + void *result; + fixture_init(&fixture); + fixture.guest_open_result = -1; + result = dlopen_recycle_transaction( + (void *)0x55, &count, 1, &fixture, recycle_prepare, + recycle_guest_open, recycle_reload, recycle_rollback); + CHECK("guest failure returned null", result == NULL); + CHECK("guest failure count unchanged", count == 0); + CHECK("guest failure prepared", fixture.prepared == 1); + CHECK("guest failure no close", fixture.guest_close_calls == 0); + check_failed_state("guest", &fixture); + fixture_destroy(&fixture); +} + +static void test_reload_failure(void) +{ + recycle_fixture_t fixture; + kzt_guest_object_observation_t observed; + unsigned long failed_generation, reused_generation; + size_t count = 0; + void *result; + fixture_init(&fixture); + fixture.reload_result = -1; + result = dlopen_recycle_transaction( + (void *)0x66, &count, 1, &fixture, recycle_prepare, + recycle_guest_open, recycle_reload, recycle_rollback); + failed_generation = fixture.generation; + CHECK("reload failure returned null", result == NULL); + CHECK("reload failure count unchanged", count == 0); + CHECK("reload failure closes guest once", fixture.guest_close_calls == 1); + check_failed_state("reload", &fixture); + + observed = observation(0xd000); + CHECK("reload failure observation retired", + kzt_guest_registry_observe(fixture.registry, &observed) == + KZT_GUEST_REGISTRY_ADDED); + fixture.link_map_addr = 0xd000; + reused_generation = current_generation(&fixture); + CHECK("reload failure reuse generation", + reused_generation > failed_generation); + fixture_destroy(&fixture); +} + +static void test_plt_relocation_failure_rolls_back_and_retry_overwrites(void) +{ + recycle_fixture_t fixture; + size_t count = 0; + void *result; + + fixture_init(&fixture); + fixture.plt_relocation_result = -1; + result = dlopen_recycle_transaction( + (void *)0x76, &count, 1, &fixture, recycle_prepare, + recycle_guest_open, recycle_reload, recycle_rollback); + CHECK("PLT failure returned null", result == NULL); + CHECK("PLT failure count unchanged", count == 0); + CHECK("PLT failure compensates once", fixture.guest_close_calls == 1); + check_failed_state("plt", &fixture); + + fixture.plt_relocation_result = 0; + result = dlopen_recycle_transaction( + (void *)0x76, &count, 1, &fixture, recycle_prepare, + recycle_guest_open, recycle_reload, recycle_rollback); + CHECK("PLT retry returned handle", result == (void *)0x76); + CHECK("PLT retry increments count", count == 1); + CHECK("PLT retry did not compensate", fixture.guest_close_calls == 1); + CHECK("PLT retry overwrote delta", fixture.delta == 0x700000); + CHECK("PLT retry overwrote l_addr", fixture.internal_l_addr == 0x700000); + CHECK("PLT retry set fix flag", fixture.latx_hasfix == 1); + CHECK("PLT retry set relocation flag", fixture.had_relocate_elf == 1); + CHECK("PLT retry set PLT flag", fixture.had_relocate_plt == 1); + fixture_destroy(&fixture); +} + +static void test_success_commits_handle_count_and_pair(void) +{ + recycle_fixture_t fixture; + kzt_guest_library_binding_state_t lifecycle; + kzt_guest_library_binding_key_t key; + kzt_guest_library_handle_t binding_handle; + size_t pending = 99, live_entries = 99, count = 0; + void *result; + fixture_init(&fixture); + result = dlopen_recycle_transaction( + (void *)0x77, &count, 1, &fixture, recycle_prepare, + recycle_guest_open, recycle_reload, recycle_rollback); + CHECK("success returned old handle", result == (void *)0x77); + CHECK("success count incremented", count == 1); + CHECK("success active", fixture.active == 1); + CHECK("success binding snapshot", + kzt_guest_library_binding_test_snapshot( + fixture.bindings, (library_t *)&fixture.library, &lifecycle, + &pending, &live_entries) == 0); + CHECK("success lifecycle live", + lifecycle == KZT_GUEST_LIBRARY_BINDING_LIVE); + CHECK("success pending empty", pending == 0); + CHECK("success exact pair live", live_entries == 1); + key = binding_key(fixture.link_map_addr, fixture.generation); + CHECK("success pair lookup", kzt_guest_library_lookup( + fixture.bindings, &key, &binding_handle) == 0); + kzt_guest_library_handle_release(&binding_handle); + fixture_destroy(&fixture); +} + +int main(void) +{ + test_guest_open_failure(); + test_reload_failure(); + test_plt_relocation_failure_rolls_back_and_retry_overwrites(); + test_success_commits_handle_count_and_pair(); + if (failures) + fprintf(stderr, "dlopen recycle transaction: %d failure(s)\n", + failures); + return failures ? EXIT_FAILURE : EXIT_SUCCESS; +} diff --git a/tests/unit/kzt/test_elf_map_range.c b/tests/unit/kzt/test_elf_map_range.c new file mode 100644 index 00000000000..fb9b16d19d8 --- /dev/null +++ b/tests/unit/kzt/test_elf_map_range.c @@ -0,0 +1,250 @@ +#include +#include + +#include "elfmap.h" + +static int failures; + +static void check_int(const char *name, int got, int expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got %d expected %d\n", name, got, expected); + ++failures; +} + +static void check_address(const char *name, uintptr_t got, + uintptr_t expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got 0x%lx expected 0x%lx\n", name, + (unsigned long)got, (unsigned long)expected); + ++failures; +} + +static Elf64_Phdr load_segment(uint64_t virtual_address, uint64_t memory_size) +{ + return (Elf64_Phdr) { + .p_type = PT_LOAD, + .p_vaddr = virtual_address, + .p_memsz = memory_size, + }; +} + +static void expect_failure(const char *name, const Elf64_Phdr *headers, + size_t count, uintptr_t load_bias, + uintptr_t page_size) +{ + uintptr_t start = 0x11111111; + uintptr_t end = 0x22222222; + + check_int(name, GetElfLoadRange(headers, count, load_bias, page_size, + &start, &end), -1); + check_address("failure-preserves-start", start, 0x11111111); + check_address("failure-preserves-end", end, 0x22222222); +} + +static void test_single_segment_is_page_aligned(void) +{ + Elf64_Phdr header = load_segment(0x1234, 0x2345); + uintptr_t start = 0; + uintptr_t end = 0; + + check_int("single.result", + GetElfLoadRange(&header, 1, 0x400000, 0x1000, + &start, &end), 0); + check_address("single.start", start, 0x401000); + check_address("single.end", end, 0x404000); +} + +static void test_page_aligned_end_is_not_extended(void) +{ + Elf64_Phdr header = load_segment(0x2000, 0x1000); + uintptr_t start = 0; + uintptr_t end = 0; + + check_int("aligned-end.result", + GetElfLoadRange(&header, 1, 0x800000, 0x1000, + &start, &end), 0); + check_address("aligned-end.start", start, 0x802000); + check_address("aligned-end.end", end, 0x803000); +} + +static void test_multiple_segments_use_outer_range(void) +{ + Elf64_Phdr headers[] = { + load_segment(0x9000, 0x1100), + load_segment(0x1234, 0x20), + load_segment(0x5000, 0x2800), + }; + uintptr_t start = 0; + uintptr_t end = 0; + + check_int("multiple.result", + GetElfLoadRange(headers, 3, 0x100000, 0x1000, + &start, &end), 0); + check_address("multiple.start", start, 0x101000); + check_address("multiple.end", end, 0x10b000); +} + +static void test_non_load_and_empty_segments_are_ignored(void) +{ + Elf64_Phdr headers[] = { + { + .p_type = PT_DYNAMIC, + .p_vaddr = UINT64_MAX - 1, + .p_memsz = UINT64_MAX, + }, + load_segment(UINT64_MAX, 0), + load_segment(0x3001, 1), + }; + uintptr_t start = 0; + uintptr_t end = 0; + + check_int("ignored.result", + GetElfLoadRange(headers, 3, 0, 0x1000, &start, &end), 0); + check_address("ignored.start", start, 0x3000); + check_address("ignored.end", end, 0x4000); +} + +static void test_invalid_arguments(void) +{ + Elf64_Phdr header = load_segment(0x1000, 0x1000); + uintptr_t start = 0x11111111; + uintptr_t end = 0x22222222; + + check_int("null-headers", + GetElfLoadRange(NULL, 1, 0, 0x1000, &start, &end), -1); + check_int("zero-count", + GetElfLoadRange(&header, 0, 0, 0x1000, &start, &end), -1); + check_int("null-start", + GetElfLoadRange(&header, 1, 0, 0x1000, NULL, &end), -1); + check_int("null-end", + GetElfLoadRange(&header, 1, 0, 0x1000, &start, NULL), -1); + check_int("aliased-output", + GetElfLoadRange(&header, 1, 0, 0x1000, &start, &start), -1); + expect_failure("zero-page-size", &header, 1, 0, 0); + expect_failure("non-power-of-two-page-size", &header, 1, 0, 0x1800); +} + +static void test_missing_load_segment(void) +{ + Elf64_Phdr non_load = { + .p_type = PT_DYNAMIC, + .p_vaddr = 0x1000, + .p_memsz = 0x1000, + }; + Elf64_Phdr empty_load = load_segment(0x1000, 0); + + expect_failure("no-load", &non_load, 1, 0, 0x1000); + expect_failure("empty-load", &empty_load, 1, 0, 0x1000); +} + +static void test_segment_end_overflow(void) +{ + Elf64_Phdr header = load_segment(UINT64_MAX - 0xfff, 0x1000); + + expect_failure("segment-end-overflow", &header, 1, 0, 0x1000); +} + +static void test_page_rounding_overflow(void) +{ + Elf64_Phdr header = load_segment(UINTPTR_MAX - 0x7ff, 0x400); + + expect_failure("page-rounding-overflow", &header, 1, 0, 0x1000); +} + +static void test_load_bias_overflow(void) +{ + Elf64_Phdr header = load_segment(0x1000, 0x1000); + + expect_failure("load-bias-start-overflow", &header, 1, + UINTPTR_MAX, 0x1000); + expect_failure("load-bias-end-overflow", &header, 1, + UINTPTR_MAX - 0x1000, 0x1000); +} + +static void test_dynamic_runtime_address_ignores_empty_image_info_hint(void) +{ + Elf64_Phdr headers[] = { + load_segment(0, 0x3000), + { + .p_type = PT_DYNAMIC, + .p_vaddr = 0x2f00, + .p_memsz = 0x100, + }, + }; + uintptr_t image_info_pt_dynamic_addr = 0; + uintptr_t dynamic_addr = image_info_pt_dynamic_addr; + + check_int("dynamic.normal-x86-image", + GetElfDynamicAddress(headers, 2, 0x400000, &dynamic_addr), 0); + check_address("dynamic.from-main-elf-phdr", dynamic_addr, 0x402f00); +} + +static void test_dynamic_runtime_address_rejects_bad_evidence(void) +{ + Elf64_Phdr missing = load_segment(0, 0x1000); + Elf64_Phdr duplicate[] = { + { .p_type = PT_DYNAMIC, .p_vaddr = 0x1000 }, + { .p_type = PT_DYNAMIC, .p_vaddr = 0x2000 }, + }; + Elf64_Phdr overflow = { + .p_type = PT_DYNAMIC, + .p_vaddr = UINTPTR_MAX, + }; + uintptr_t dynamic_addr = 0x11111111; + + check_int("dynamic.missing", + GetElfDynamicAddress(&missing, 1, 0x400000, + &dynamic_addr), -1); + check_int("dynamic.duplicate", + GetElfDynamicAddress(duplicate, 2, 0x400000, + &dynamic_addr), -1); + check_int("dynamic.overflow", + GetElfDynamicAddress(&overflow, 1, 1, &dynamic_addr), -1); + check_address("dynamic.failure-preserves-output", dynamic_addr, + 0x11111111); +} + +#if UINTPTR_MAX < UINT64_MAX +static void test_elf_address_too_wide_for_host(void) +{ + Elf64_Phdr start_too_wide = load_segment((uint64_t)UINTPTR_MAX + 1, 1); + Elf64_Phdr end_too_wide = load_segment(UINTPTR_MAX - 0x100, 0x200); + + expect_failure("start-too-wide", &start_too_wide, 1, 0, 1); + expect_failure("end-too-wide", &end_too_wide, 1, 0, 1); +} +#endif + +int main(void) +{ + test_single_segment_is_page_aligned(); + test_page_aligned_end_is_not_extended(); + test_multiple_segments_use_outer_range(); + test_non_load_and_empty_segments_are_ignored(); + test_invalid_arguments(); + test_missing_load_segment(); + test_segment_end_overflow(); + test_page_rounding_overflow(); + test_load_bias_overflow(); + test_dynamic_runtime_address_ignores_empty_image_info_hint(); + test_dynamic_runtime_address_rejects_bad_evidence(); +#if UINTPTR_MAX < UINT64_MAX + test_elf_address_too_wide_for_host(); +#endif + + if (failures) { + fprintf(stderr, "kzt-elf-map-range: %d failure(s)\n", failures); + return 1; + } + + puts("kzt-elf-map-range: all tests passed"); + return 0; +} diff --git a/tests/unit/kzt/test_guest_dynamic_diagnostics.c b/tests/unit/kzt/test_guest_dynamic_diagnostics.c new file mode 100644 index 00000000000..9ce831257e8 --- /dev/null +++ b/tests/unit/kzt/test_guest_dynamic_diagnostics.c @@ -0,0 +1,496 @@ +#include +#include + +#include "target/i386/latx/include/kzt_guest_dynamic_diagnostics.h" + +#define TEST_ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0])) +#define KZT_TEST_UNKNOWN_DYNAMIC_TAG 0x6000000d + +static int failures; + +static void check_true(const char *name, int condition) +{ + if (condition) { + return; + } + + fprintf(stderr, "%s: condition failed\n", name); + ++failures; +} + +static void check_int(const char *name, int got, int expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got %d expected %d\n", name, got, expected); + ++failures; +} + +static void check_size(const char *name, size_t got, size_t expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got %lu expected %lu\n", name, + (unsigned long)got, (unsigned long)expected); + ++failures; +} + +static void check_ulong(const char *name, unsigned long got, + unsigned long expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got %lu expected %lu\n", name, got, expected); + ++failures; +} + +static void check_u64(const char *name, uint64_t got, uint64_t expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got 0x%llx expected 0x%llx\n", name, + (unsigned long long)got, (unsigned long long)expected); + ++failures; +} + +static void check_uintptr(const char *name, uintptr_t got, uintptr_t expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got 0x%lx expected 0x%lx\n", name, + (unsigned long)got, (unsigned long)expected); + ++failures; +} + +static void check_str_contains(const char *name, const char *value, + const char *expected) +{ + if (value && expected && strstr(value, expected)) { + return; + } + + fprintf(stderr, "%s: '%s' does not contain '%s'\n", name, + value ? value : "(null)", expected ? expected : "(null)"); + ++failures; +} + +static kzt_guest_dynamic_field_t make_field( + uint64_t value, + kzt_guest_dynamic_address_semantics_t semantics) +{ + return (kzt_guest_dynamic_field_t) { + .present = 1, + .value = value, + .address_semantics = semantics, + }; +} + +static kzt_guest_dynamic_parse_result_t make_complete_result(void) +{ + kzt_guest_dynamic_parse_result_t result = { + .status = KZT_GUEST_DYNAMIC_COMPLETE, + .error = KZT_GUEST_DYNAMIC_ERROR_NONE, + .entry_count = 12, + .scan_limit = KZT_GUEST_DYNAMIC_SCAN_LIMIT, + .view = { + .dynamic_addr = 0x7000001000, + .load_bias = 0x7000000000, + .status = KZT_GUEST_DYNAMIC_COMPLETE, + .entry_count = 12, + .has_null = 1, + .scan_limit = KZT_GUEST_DYNAMIC_SCAN_LIMIT, + .symtab = make_field(0x7000010000, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS), + .strtab = make_field(0x7000020000, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS), + .syment = make_field(24, KZT_GUEST_DYNAMIC_SCALAR), + .strsz = make_field(0x220, KZT_GUEST_DYNAMIC_SCALAR), + .hash = make_field(0x7000030000, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS), + .gnu_hash = make_field(0x7000040000, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS), + .versym = make_field(0x7000050000, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS), + .verneed = make_field(0x7000060000, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS), + .verneednum = make_field(2, KZT_GUEST_DYNAMIC_SCALAR), + .verdef = make_field(0x7000070000, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS), + .verdefnum = make_field(1, KZT_GUEST_DYNAMIC_SCALAR), + .rela = make_field(0x7000080000, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS), + .relasz = make_field(0x60, KZT_GUEST_DYNAMIC_SCALAR), + .relaent = make_field(24, KZT_GUEST_DYNAMIC_SCALAR), + .rel = make_field(0x7000090000, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS), + .relsz = make_field(0x40, KZT_GUEST_DYNAMIC_SCALAR), + .relent = make_field(16, KZT_GUEST_DYNAMIC_SCALAR), + .jmprel = make_field(0x70000a0000, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS), + .pltrelsz = make_field(0x30, KZT_GUEST_DYNAMIC_SCALAR), + .pltrel = make_field(DT_RELA, KZT_GUEST_DYNAMIC_SCALAR), + .pltgot = make_field(0x70000b0000, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS), + .needed_offsets = { 0x10, 0x38 }, + .needed_count = 2, + .needed_address_semantics = + KZT_GUEST_DYNAMIC_STRING_TABLE_OFFSET, + }, + }; + + return result; +} + +static const kzt_guest_dynamic_diagnostic_field_t *require_field( + const kzt_guest_dynamic_diagnostic_report_t *report, + const char *name) +{ + const kzt_guest_dynamic_diagnostic_field_t *field = + kzt_guest_dynamic_diagnostic_find_field(report, name); + + check_true(name, field != NULL); + return field; +} + +static void assert_field_match( + const kzt_guest_dynamic_diagnostic_report_t *report, + const char *name, + kzt_guest_dynamic_diagnostic_match_t expected) +{ + const kzt_guest_dynamic_diagnostic_field_t *field = require_field(report, + name); + + if (!field) { + return; + } + + check_int(name, field->match, expected); +} + +static void test_identical_views_are_matched(void) +{ + kzt_guest_dynamic_parse_result_t old_result = make_complete_result(); + kzt_guest_dynamic_parse_result_t new_result = make_complete_result(); + kzt_guest_dynamic_diagnostic_report_t report; + + check_int("identical.compare", + kzt_guest_dynamic_diagnostics_compare(&old_result, &new_result, + &report), + 0); + check_int("identical.status", report.status_match, + KZT_GUEST_DYNAMIC_DIAGNOSTIC_MATCHED); + check_int("identical.entry-count", report.entry_count_match, + KZT_GUEST_DYNAMIC_DIAGNOSTIC_MATCHED); + check_int("identical.unknown-tags", report.unknown_tags_match, + KZT_GUEST_DYNAMIC_DIAGNOSTIC_MATCHED); + check_size("identical.field-count", report.field_count, 22); + check_size("identical.matched", report.matched_count, report.field_count); + check_size("identical.missing-old", report.missing_old_count, 0); + check_size("identical.missing-new", report.missing_new_count, 0); + check_size("identical.mismatch", report.mismatch_count, 0); + check_size("identical.difference", report.difference_count, 0); + check_size("identical.blocking", report.blocking_count, 0); + assert_field_match(&report, "symtab", + KZT_GUEST_DYNAMIC_DIAGNOSTIC_MATCHED); + assert_field_match(&report, "needed_offsets", + KZT_GUEST_DYNAMIC_DIAGNOSTIC_MATCHED); +} + +static void test_old_field_missing_from_new_is_reported(void) +{ + kzt_guest_dynamic_parse_result_t old_result = make_complete_result(); + kzt_guest_dynamic_parse_result_t new_result = make_complete_result(); + kzt_guest_dynamic_diagnostic_report_t report; + const kzt_guest_dynamic_diagnostic_field_t *field; + + memset(&new_result.view.strtab, 0, sizeof(new_result.view.strtab)); + check_int("missing-new.compare", + kzt_guest_dynamic_diagnostics_compare(&old_result, &new_result, + &report), + 0); + + field = require_field(&report, "strtab"); + if (!field) { + return; + } + + check_int("missing-new.match", field->match, + KZT_GUEST_DYNAMIC_DIAGNOSTIC_MISSING_NEW); + check_true("missing-new.old-present", field->old_present); + check_true("missing-new.new-present", !field->new_present); + check_u64("missing-new.old-value", field->old_value, 0x7000020000); + check_size("missing-new.count", report.missing_new_count, 1); + check_size("missing-new.difference", report.difference_count, 1); + check_size("missing-new.blocking", report.blocking_count, 0); +} + +static void test_new_parser_incomplete_states_are_reported(void) +{ + kzt_guest_dynamic_parse_result_t old_result = make_complete_result(); + kzt_guest_dynamic_parse_result_t new_result = make_complete_result(); + kzt_guest_dynamic_diagnostic_report_t report; + + new_result.status = KZT_GUEST_DYNAMIC_TRUNCATED_NO_NULL; + new_result.error = KZT_GUEST_DYNAMIC_ERROR_SCAN_LIMIT_EXCEEDED; + new_result.entry_count = KZT_GUEST_DYNAMIC_SCAN_LIMIT; + new_result.view.status = KZT_GUEST_DYNAMIC_TRUNCATED_NO_NULL; + new_result.view.entry_count = KZT_GUEST_DYNAMIC_SCAN_LIMIT; + new_result.view.has_null = 0; + + check_int("truncated.compare", + kzt_guest_dynamic_diagnostics_compare(&old_result, &new_result, + &report), + 0); + check_int("truncated.status-match", report.status_match, + KZT_GUEST_DYNAMIC_DIAGNOSTIC_MISMATCH); + check_int("truncated.new-status", report.new_status, + KZT_GUEST_DYNAMIC_TRUNCATED_NO_NULL); + check_int("truncated.new-error", report.new_error, + KZT_GUEST_DYNAMIC_ERROR_SCAN_LIMIT_EXCEEDED); + check_true("truncated.flag", report.new_truncated); + check_size("truncated.blocking", report.blocking_count, 1); + + new_result = make_complete_result(); + new_result.status = KZT_GUEST_DYNAMIC_READ_ERROR; + new_result.error = KZT_GUEST_DYNAMIC_ERROR_READ_FAILURE; + new_result.read_error_addr = 0x7000001080; + new_result.entry_count = 8; + new_result.view.status = KZT_GUEST_DYNAMIC_READ_ERROR; + new_result.view.entry_count = 8; + new_result.view.has_null = 0; + + check_int("read-error.compare", + kzt_guest_dynamic_diagnostics_compare(&old_result, &new_result, + &report), + 0); + check_int("read-error.status-match", report.status_match, + KZT_GUEST_DYNAMIC_DIAGNOSTIC_MISMATCH); + check_int("read-error.new-status", report.new_status, + KZT_GUEST_DYNAMIC_READ_ERROR); + check_int("read-error.new-error", report.new_error, + KZT_GUEST_DYNAMIC_ERROR_READ_FAILURE); + check_true("read-error.flag", report.new_read_error); + check_uintptr("read-error.addr", report.new_read_error_addr, + 0x7000001080); + check_size("read-error.blocking", report.blocking_count, 1); +} + +static void test_unknown_tag_difference_is_diagnostic_only(void) +{ + kzt_guest_dynamic_parse_result_t old_result = make_complete_result(); + kzt_guest_dynamic_parse_result_t new_result = make_complete_result(); + kzt_guest_dynamic_diagnostic_report_t report; + kzt_guest_dynamic_diagnostic_summary_t summary; + char line[512]; + + new_result.unknown_tag_count = 1; + new_result.first_unknown_tag = KZT_TEST_UNKNOWN_DYNAMIC_TAG; + new_result.first_unknown_tag_index = 3; + new_result.view.unknown_tag_count = 1; + new_result.view.first_unknown_tag = KZT_TEST_UNKNOWN_DYNAMIC_TAG; + new_result.view.first_unknown_tag_index = 3; + + check_int("unknown.compare", + kzt_guest_dynamic_diagnostics_compare(&old_result, &new_result, + &report), + 0); + check_int("unknown.match", report.unknown_tags_match, + KZT_GUEST_DYNAMIC_DIAGNOSTIC_MISMATCH); + check_size("unknown.old-count", report.old_unknown_tag_count, 0); + check_size("unknown.new-count", report.new_unknown_tag_count, 1); + check_size("unknown.field-mismatch", report.mismatch_count, 0); + check_size("unknown.difference", report.difference_count, 1); + check_size("unknown.blocking", report.blocking_count, 0); + + check_int("unknown.summary", + kzt_guest_dynamic_diagnostics_summarize(&report, 0xabc000, 9, + &summary), + 0); + check_int("unknown.summary-kind", summary.first_difference_kind, + KZT_GUEST_DYNAMIC_DIAGNOSTIC_DIFFERENCE_UNKNOWN_TAGS); + check_true("unknown.summary-new-present", summary.first_new_present); + check_true("unknown.summary-new-tag", + summary.first_new_tag == KZT_TEST_UNKNOWN_DYNAMIC_TAG); + check_size("unknown.summary-new-index", summary.first_new_tag_index, 3); + check_int("unknown.format", + kzt_guest_dynamic_diagnostics_format_summary(&summary, line, + sizeof(line)), 0); + check_str_contains("unknown.format-first", line, "first=unknown_tags"); +} + +static void test_needed_offsets_difference_is_reported(void) +{ + kzt_guest_dynamic_parse_result_t old_result = make_complete_result(); + kzt_guest_dynamic_parse_result_t new_result = make_complete_result(); + kzt_guest_dynamic_diagnostic_report_t report; + const kzt_guest_dynamic_diagnostic_field_t *field; + + new_result.view.needed_offsets[1] = 0x58; + check_int("needed.compare", + kzt_guest_dynamic_diagnostics_compare(&old_result, &new_result, + &report), + 0); + + field = require_field(&report, "needed_offsets"); + if (!field) { + return; + } + + check_int("needed.match", field->match, + KZT_GUEST_DYNAMIC_DIAGNOSTIC_MISMATCH); + check_size("needed.old-count", field->old_count, 2); + check_size("needed.new-count", field->new_count, 2); + check_u64("needed.old-first", field->old_value, 0x10); + check_u64("needed.new-first", field->new_value, 0x10); + check_size("needed.mismatch", report.mismatch_count, 1); + check_size("needed.difference", report.difference_count, 1); + check_size("needed.blocking", report.blocking_count, 0); +} + +static void test_error_status_is_blocking(void) +{ + kzt_guest_dynamic_parse_result_t old_result = make_complete_result(); + kzt_guest_dynamic_parse_result_t new_result = make_complete_result(); + kzt_guest_dynamic_diagnostic_report_t report; + kzt_guest_dynamic_diagnostic_summary_t summary; + char line[512]; + + old_result.status = KZT_GUEST_DYNAMIC_ERROR; + old_result.error = KZT_GUEST_DYNAMIC_ERROR_INVALID_ARGUMENT; + old_result.view.status = KZT_GUEST_DYNAMIC_ERROR; + new_result.status = KZT_GUEST_DYNAMIC_ERROR; + new_result.error = KZT_GUEST_DYNAMIC_ERROR_INVALID_ARGUMENT; + new_result.view.status = KZT_GUEST_DYNAMIC_ERROR; + + check_int("error.compare", + kzt_guest_dynamic_diagnostics_compare(&old_result, &new_result, + &report), + 0); + check_int("error.status-match", report.status_match, + KZT_GUEST_DYNAMIC_DIAGNOSTIC_MATCHED); + check_size("error.difference", report.difference_count, 0); + check_size("error.blocking", report.blocking_count, 2); + + check_int("error.summary", + kzt_guest_dynamic_diagnostics_summarize(&report, 0xabc100, 10, + &summary), + 0); + check_true("error.summary-blocking", summary.blocking); + check_int("error.summary-kind", summary.first_difference_kind, + KZT_GUEST_DYNAMIC_DIAGNOSTIC_DIFFERENCE_STATUS); + check_int("error.summary-match", summary.first_difference_match, + KZT_GUEST_DYNAMIC_DIAGNOSTIC_MATCHED); + check_int("error.format", + kzt_guest_dynamic_diagnostics_format_summary(&summary, line, + sizeof(line)), 0); + check_str_contains("error.format-blocking", line, "blocking=1"); + check_str_contains("error.format-first", line, "first=status"); +} + +static void test_summary_includes_identity_and_first_field(void) +{ + kzt_guest_dynamic_parse_result_t old_result = make_complete_result(); + kzt_guest_dynamic_parse_result_t new_result = make_complete_result(); + kzt_guest_dynamic_diagnostic_report_t report; + kzt_guest_dynamic_diagnostic_summary_t summary; + char line[512]; + + new_result.view.pltgot.value = 0x70000c0000; + check_int("summary.compare", + kzt_guest_dynamic_diagnostics_compare(&old_result, &new_result, + &report), + 0); + check_int("summary.create", + kzt_guest_dynamic_diagnostics_summarize(&report, 0xabcdef00, + 17, &summary), + 0); + + check_uintptr("summary.link-map", summary.link_map_addr, 0xabcdef00); + check_ulong("summary.generation", summary.generation, 17); + check_true("summary.not-matched", !summary.matched); + check_true("summary.not-blocking", !summary.blocking); + check_size("summary.difference", summary.difference_count, 1); + check_int("summary.kind", summary.first_difference_kind, + KZT_GUEST_DYNAMIC_DIAGNOSTIC_DIFFERENCE_FIELD); + check_true("summary.name", !strcmp(summary.first_difference_name, + "pltgot")); + check_int("summary.match", summary.first_difference_match, + KZT_GUEST_DYNAMIC_DIAGNOSTIC_MISMATCH); + check_true("summary.old-present", summary.first_old_present); + check_true("summary.new-present", summary.first_new_present); + check_u64("summary.old-value", summary.first_old_value, 0x70000b0000); + check_u64("summary.new-value", summary.first_new_value, 0x70000c0000); + + check_int("summary.format", + kzt_guest_dynamic_diagnostics_format_summary(&summary, line, + sizeof(line)), 0); + check_str_contains("summary.format-object", line, + "link_map=0xabcdef00"); + check_str_contains("summary.format-generation", line, + "generation=17"); + check_str_contains("summary.format-first", line, "first=pltgot"); + check_str_contains("summary.format-differences", line, + "differences=1"); +} + +static int test_matches_filter(const char *name, int argc, char **argv) +{ + int i; + + for (i = 1; i < argc; ++i) { + if (!strcmp(argv[i], "--filter") && i + 1 < argc) { + return strcmp(name, argv[i + 1]) == 0; + } + } + + return 1; +} + +int main(int argc, char **argv) +{ + if (test_matches_filter("identical_views_are_matched", argc, argv)) { + test_identical_views_are_matched(); + } + if (test_matches_filter("old_field_missing_from_new_is_reported", + argc, argv)) { + test_old_field_missing_from_new_is_reported(); + } + if (test_matches_filter("new_parser_incomplete_states_are_reported", + argc, argv)) { + test_new_parser_incomplete_states_are_reported(); + } + if (test_matches_filter("unknown_tag_difference_is_diagnostic_only", + argc, argv)) { + test_unknown_tag_difference_is_diagnostic_only(); + } + if (test_matches_filter("needed_offsets_difference_is_reported", + argc, argv)) { + test_needed_offsets_difference_is_reported(); + } + if (test_matches_filter("error_status_is_blocking", argc, argv)) { + test_error_status_is_blocking(); + } + if (test_matches_filter("summary_includes_identity_and_first_field", + argc, argv)) { + test_summary_includes_identity_and_first_field(); + } + + if (failures) { + fprintf(stderr, "kzt-guest-dynamic-diagnostics: %d failure(s)\n", + failures); + return 1; + } + + puts("kzt-guest-dynamic-diagnostics: selected contract tests passed"); + return 0; +} diff --git a/tests/unit/kzt/test_guest_dynamic_parser.c b/tests/unit/kzt/test_guest_dynamic_parser.c new file mode 100644 index 00000000000..816219ddb2c --- /dev/null +++ b/tests/unit/kzt/test_guest_dynamic_parser.c @@ -0,0 +1,450 @@ +#include +#include + +#include "target/i386/latx/include/kzt_guest_dynamic.h" + +#define TEST_ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0])) +#define KZT_TEST_UNKNOWN_DYNAMIC_TAG 0x6000000d + +typedef struct fake_dynamic_memory { + uintptr_t base; + size_t size; + int read_calls; +} fake_dynamic_memory_t; + +static int failures; + +static void check_true(const char *name, int condition) +{ + if (condition) { + return; + } + + fprintf(stderr, "%s: condition failed\n", name); + ++failures; +} + +static void check_int(const char *name, int got, int expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got %d expected %d\n", name, got, expected); + ++failures; +} + +static void check_size(const char *name, size_t got, size_t expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got %lu expected %lu\n", name, + (unsigned long)got, (unsigned long)expected); + ++failures; +} + +static void check_u64(const char *name, uint64_t got, uint64_t expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got 0x%llx expected 0x%llx\n", name, + (unsigned long long)got, (unsigned long long)expected); + ++failures; +} + +static void check_i64(const char *name, int64_t got, int64_t expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got %lld expected %lld\n", name, + (long long)got, (long long)expected); + ++failures; +} + +static int fake_read_memory(uintptr_t guest_addr, void *dst, size_t size, + void *opaque) +{ + fake_dynamic_memory_t *memory = opaque; + + ++memory->read_calls; + if (guest_addr < memory->base || + size > memory->size || + guest_addr - memory->base > memory->size - size) { + return -1; + } + + memcpy(dst, (const void *)guest_addr, size); + return 0; +} + +static kzt_guest_link_map_reader_ops_t fake_ops(fake_dynamic_memory_t *memory) +{ + kzt_guest_link_map_reader_ops_t ops = { + .read_memory = fake_read_memory, + .opaque = memory, + }; + + return ops; +} + +static void check_field(const char *name, + const kzt_guest_dynamic_field_t *field, + uint64_t value, + kzt_guest_dynamic_address_semantics_t semantics) +{ + check_true(name, field->present); + check_u64(name, field->value, value); + check_int(name, field->address_semantics, semantics); +} + +static void test_complete_runtime_dynamic_view(void) +{ + Elf64_Dyn dynamic[] = { + { .d_tag = DT_NEEDED, .d_un.d_val = 0x10 }, + { .d_tag = DT_NEEDED, .d_un.d_val = 0x38 }, + { .d_tag = DT_SYMTAB, .d_un.d_ptr = 0x7000010000 }, + { .d_tag = DT_STRTAB, .d_un.d_ptr = 0x7000020000 }, + { .d_tag = DT_SYMENT, .d_un.d_val = sizeof(Elf64_Sym) }, + { .d_tag = DT_STRSZ, .d_un.d_val = 0x220 }, + { .d_tag = DT_HASH, .d_un.d_ptr = 0x7000030000 }, + { .d_tag = DT_GNU_HASH, .d_un.d_ptr = 0x7000040000 }, + { .d_tag = DT_VERSYM, .d_un.d_ptr = 0x7000050000 }, + { .d_tag = DT_VERNEED, .d_un.d_ptr = 0x7000060000 }, + { .d_tag = DT_VERNEEDNUM, .d_un.d_val = 2 }, + { .d_tag = DT_VERDEF, .d_un.d_ptr = 0x7000070000 }, + { .d_tag = DT_VERDEFNUM, .d_un.d_val = 1 }, + { .d_tag = DT_RELA, .d_un.d_ptr = 0x7000080000 }, + { .d_tag = DT_RELASZ, .d_un.d_val = 0x60 }, + { .d_tag = DT_RELAENT, .d_un.d_val = sizeof(Elf64_Rela) }, + { .d_tag = DT_REL, .d_un.d_ptr = 0x7000090000 }, + { .d_tag = DT_RELSZ, .d_un.d_val = 0x40 }, + { .d_tag = DT_RELENT, .d_un.d_val = sizeof(Elf64_Rel) }, + { .d_tag = DT_JMPREL, .d_un.d_ptr = 0x70000a0000 }, + { .d_tag = DT_PLTRELSZ, .d_un.d_val = 0x30 }, + { .d_tag = DT_PLTREL, .d_un.d_val = DT_RELA }, + { .d_tag = DT_PLTGOT, .d_un.d_ptr = 0x70000b0000 }, + { .d_tag = DT_NULL, .d_un.d_val = 0 }, + }; + fake_dynamic_memory_t memory = { + .base = (uintptr_t)dynamic, + .size = sizeof(dynamic), + }; + kzt_guest_link_map_reader_ops_t ops = fake_ops(&memory); + kzt_guest_dynamic_parse_result_t result = { 0 }; + + check_int("dynamic.complete.parse", + kzt_guest_dynamic_parse((uintptr_t)dynamic, 0x400000, + &ops, &result), + 0); + check_int("dynamic.complete.status", result.status, + KZT_GUEST_DYNAMIC_COMPLETE); + check_int("dynamic.complete.view-status", result.view.status, + KZT_GUEST_DYNAMIC_COMPLETE); + check_true("dynamic.complete.has-null", result.view.has_null); + check_size("dynamic.complete.entry-count", result.entry_count, + TEST_ARRAY_SIZE(dynamic) - 1); + check_size("dynamic.complete.view-entry-count", result.view.entry_count, + TEST_ARRAY_SIZE(dynamic) - 1); + check_int("dynamic.complete.reader-calls", memory.read_calls, + TEST_ARRAY_SIZE(dynamic)); + + check_field("dynamic.symtab", &result.view.symtab, 0x7000010000, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS); + check_field("dynamic.strtab", &result.view.strtab, 0x7000020000, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS); + check_field("dynamic.syment", &result.view.syment, sizeof(Elf64_Sym), + KZT_GUEST_DYNAMIC_SCALAR); + check_field("dynamic.strsz", &result.view.strsz, 0x220, + KZT_GUEST_DYNAMIC_SCALAR); + check_field("dynamic.hash", &result.view.hash, 0x7000030000, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS); + check_field("dynamic.gnu-hash", &result.view.gnu_hash, 0x7000040000, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS); + check_field("dynamic.versym", &result.view.versym, 0x7000050000, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS); + check_field("dynamic.verneed", &result.view.verneed, 0x7000060000, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS); + check_field("dynamic.verneednum", &result.view.verneednum, 2, + KZT_GUEST_DYNAMIC_SCALAR); + check_field("dynamic.verdef", &result.view.verdef, 0x7000070000, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS); + check_field("dynamic.verdefnum", &result.view.verdefnum, 1, + KZT_GUEST_DYNAMIC_SCALAR); + check_field("dynamic.rela", &result.view.rela, 0x7000080000, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS); + check_field("dynamic.relasz", &result.view.relasz, 0x60, + KZT_GUEST_DYNAMIC_SCALAR); + check_field("dynamic.relaent", &result.view.relaent, sizeof(Elf64_Rela), + KZT_GUEST_DYNAMIC_SCALAR); + check_field("dynamic.rel", &result.view.rel, 0x7000090000, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS); + check_field("dynamic.relsz", &result.view.relsz, 0x40, + KZT_GUEST_DYNAMIC_SCALAR); + check_field("dynamic.relent", &result.view.relent, sizeof(Elf64_Rel), + KZT_GUEST_DYNAMIC_SCALAR); + check_field("dynamic.jmprel", &result.view.jmprel, 0x70000a0000, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS); + check_field("dynamic.pltrelsz", &result.view.pltrelsz, 0x30, + KZT_GUEST_DYNAMIC_SCALAR); + check_field("dynamic.pltrel", &result.view.pltrel, DT_RELA, + KZT_GUEST_DYNAMIC_SCALAR); + check_field("dynamic.pltgot", &result.view.pltgot, 0x70000b0000, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS); + + check_size("dynamic.needed.count", result.view.needed_count, 2); + check_int("dynamic.needed.semantics", + result.view.needed_address_semantics, + KZT_GUEST_DYNAMIC_STRING_TABLE_OFFSET); + check_u64("dynamic.needed.0", result.view.needed_offsets[0], 0x10); + check_u64("dynamic.needed.1", result.view.needed_offsets[1], 0x38); + + kzt_guest_dynamic_parse_result_clear(&result); +} + +static void test_dynamic_address_semantics(void) +{ + Elf64_Dyn dynamic[] = { + { .d_tag = DT_SYMTAB, .d_un.d_ptr = 0x5000010000 }, + { .d_tag = DT_STRTAB, .d_un.d_ptr = 0x5000020000 }, + { .d_tag = DT_JMPREL, .d_un.d_ptr = 0x5000030000 }, + { .d_tag = DT_PLTGOT, .d_un.d_ptr = 0x5000040000 }, + { .d_tag = DT_NEEDED, .d_un.d_val = 0x84 }, + { .d_tag = DT_NULL, .d_un.d_val = 0 }, + }; + fake_dynamic_memory_t memory = { + .base = (uintptr_t)dynamic, + .size = sizeof(dynamic), + }; + kzt_guest_link_map_reader_ops_t ops = fake_ops(&memory); + kzt_guest_dynamic_parse_result_t result = { 0 }; + + check_int("semantics.parse", + kzt_guest_dynamic_parse((uintptr_t)dynamic, 0x100000, + &ops, &result), + 0); + check_int("semantics.status", result.status, + KZT_GUEST_DYNAMIC_COMPLETE); + check_field("semantics.symtab", &result.view.symtab, 0x5000010000, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS); + check_field("semantics.strtab", &result.view.strtab, 0x5000020000, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS); + check_field("semantics.jmprel", &result.view.jmprel, 0x5000030000, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS); + check_field("semantics.pltgot", &result.view.pltgot, 0x5000040000, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS); + check_size("semantics.needed.count", result.view.needed_count, 1); + check_int("semantics.needed.semantics", + result.view.needed_address_semantics, + KZT_GUEST_DYNAMIC_STRING_TABLE_OFFSET); + check_u64("semantics.needed.offset", result.view.needed_offsets[0], + 0x84); + check_u64("semantics.load-bias-preserved", result.view.load_bias, + 0x100000); + + kzt_guest_dynamic_parse_result_clear(&result); +} + +static void test_read_failure_reports_parser_state(void) +{ + Elf64_Dyn dynamic[] = { + { .d_tag = DT_SYMTAB, .d_un.d_ptr = 0x5000010000 }, + }; + fake_dynamic_memory_t memory = { + .base = (uintptr_t)dynamic, + .size = sizeof(dynamic), + }; + kzt_guest_link_map_reader_ops_t ops = fake_ops(&memory); + kzt_guest_dynamic_parse_result_t result = { 0 }; + uintptr_t expected_error_addr = (uintptr_t)&dynamic[1]; + + check_int("read-failure.parse", + kzt_guest_dynamic_parse((uintptr_t)dynamic, 0x100000, + &ops, &result), + 0); + check_int("read-failure.status", result.status, + KZT_GUEST_DYNAMIC_READ_ERROR); + check_int("read-failure.view-status", result.view.status, + KZT_GUEST_DYNAMIC_READ_ERROR); + check_int("read-failure.error", result.error, + KZT_GUEST_DYNAMIC_ERROR_READ_FAILURE); + check_size("read-failure.entry-count", result.entry_count, 1); + check_size("read-failure.view-entry-count", result.view.entry_count, 1); + check_u64("read-failure.addr", result.read_error_addr, + expected_error_addr); + check_size("read-failure.scan-limit", result.scan_limit, + KZT_GUEST_DYNAMIC_SCAN_LIMIT); + check_size("read-failure.view-scan-limit", result.view.scan_limit, + KZT_GUEST_DYNAMIC_SCAN_LIMIT); + check_int("read-failure.reader-calls", memory.read_calls, 2); + check_true("read-failure.no-null", !result.view.has_null); + + kzt_guest_dynamic_parse_result_clear(&result); +} + +static void test_missing_null_stops_at_scan_limit(void) +{ + Elf64_Dyn dynamic[KZT_GUEST_DYNAMIC_SCAN_LIMIT]; + fake_dynamic_memory_t memory = { + .base = (uintptr_t)dynamic, + .size = sizeof(dynamic), + }; + kzt_guest_link_map_reader_ops_t ops = fake_ops(&memory); + kzt_guest_dynamic_parse_result_t result = { 0 }; + size_t i; + + for (i = 0; i < TEST_ARRAY_SIZE(dynamic); ++i) { + dynamic[i].d_tag = DT_SYMENT; + dynamic[i].d_un.d_val = sizeof(Elf64_Sym); + } + + check_int("scan-limit.parse", + kzt_guest_dynamic_parse((uintptr_t)dynamic, 0x100000, + &ops, &result), + 0); + check_int("scan-limit.status", result.status, + KZT_GUEST_DYNAMIC_TRUNCATED_NO_NULL); + check_int("scan-limit.view-status", result.view.status, + KZT_GUEST_DYNAMIC_TRUNCATED_NO_NULL); + check_int("scan-limit.error", result.error, + KZT_GUEST_DYNAMIC_ERROR_SCAN_LIMIT_EXCEEDED); + check_size("scan-limit.entry-count", result.entry_count, + KZT_GUEST_DYNAMIC_SCAN_LIMIT); + check_size("scan-limit.view-entry-count", result.view.entry_count, + KZT_GUEST_DYNAMIC_SCAN_LIMIT); + check_size("scan-limit.scan-limit", result.scan_limit, + KZT_GUEST_DYNAMIC_SCAN_LIMIT); + check_int("scan-limit.reader-calls", memory.read_calls, + KZT_GUEST_DYNAMIC_SCAN_LIMIT); + check_true("scan-limit.no-null", !result.view.has_null); + + kzt_guest_dynamic_parse_result_clear(&result); +} + +static void test_unknown_tag_is_diagnostic_only(void) +{ + Elf64_Dyn dynamic[] = { + { .d_tag = KZT_TEST_UNKNOWN_DYNAMIC_TAG, .d_un.d_val = 0x44 }, + { .d_tag = DT_STRTAB, .d_un.d_ptr = 0x5000020000 }, + { .d_tag = DT_NULL, .d_un.d_val = 0 }, + }; + fake_dynamic_memory_t memory = { + .base = (uintptr_t)dynamic, + .size = sizeof(dynamic), + }; + kzt_guest_link_map_reader_ops_t ops = fake_ops(&memory); + kzt_guest_dynamic_parse_result_t result = { 0 }; + + check_int("unknown-tag.parse", + kzt_guest_dynamic_parse((uintptr_t)dynamic, 0x100000, + &ops, &result), + 0); + check_int("unknown-tag.status", result.status, + KZT_GUEST_DYNAMIC_COMPLETE); + check_int("unknown-tag.error", result.error, + KZT_GUEST_DYNAMIC_ERROR_NONE); + check_size("unknown-tag.count", result.unknown_tag_count, 1); + check_size("unknown-tag.view-count", result.view.unknown_tag_count, 1); + check_i64("unknown-tag.first", result.first_unknown_tag, + KZT_TEST_UNKNOWN_DYNAMIC_TAG); + check_i64("unknown-tag.view-first", result.view.first_unknown_tag, + KZT_TEST_UNKNOWN_DYNAMIC_TAG); + check_size("unknown-tag.index", result.first_unknown_tag_index, 0); + check_size("unknown-tag.view-index", result.view.first_unknown_tag_index, + 0); + check_field("unknown-tag.strtab", &result.view.strtab, 0x5000020000, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS); + check_true("unknown-tag.has-null", result.view.has_null); + + kzt_guest_dynamic_parse_result_clear(&result); +} + +static void test_too_many_needed_reports_resource_limit(void) +{ + Elf64_Dyn dynamic[KZT_GUEST_DYNAMIC_NEEDED_LIMIT + 2]; + fake_dynamic_memory_t memory = { + .base = (uintptr_t)dynamic, + .size = sizeof(dynamic), + }; + kzt_guest_link_map_reader_ops_t ops = fake_ops(&memory); + kzt_guest_dynamic_parse_result_t result = { 0 }; + size_t i; + + for (i = 0; i < KZT_GUEST_DYNAMIC_NEEDED_LIMIT + 1; ++i) { + dynamic[i].d_tag = DT_NEEDED; + dynamic[i].d_un.d_val = i * 0x10; + } + dynamic[KZT_GUEST_DYNAMIC_NEEDED_LIMIT + 1].d_tag = DT_NULL; + dynamic[KZT_GUEST_DYNAMIC_NEEDED_LIMIT + 1].d_un.d_val = 0; + + check_int("needed-limit.parse", + kzt_guest_dynamic_parse((uintptr_t)dynamic, 0x100000, + &ops, &result), + 0); + check_int("needed-limit.status", result.status, + KZT_GUEST_DYNAMIC_ERROR); + check_int("needed-limit.view-status", result.view.status, + KZT_GUEST_DYNAMIC_ERROR); + check_int("needed-limit.error", result.error, + KZT_GUEST_DYNAMIC_ERROR_TOO_MANY_NEEDED); + check_size("needed-limit.entry-count", result.entry_count, + KZT_GUEST_DYNAMIC_NEEDED_LIMIT); + check_size("needed-limit.view-entry-count", result.view.entry_count, + KZT_GUEST_DYNAMIC_NEEDED_LIMIT); + check_size("needed-limit.needed-count", result.view.needed_count, + KZT_GUEST_DYNAMIC_NEEDED_LIMIT); + check_true("needed-limit.no-null", !result.view.has_null); + + kzt_guest_dynamic_parse_result_clear(&result); +} + +static int test_matches_filter(const char *name, int argc, char **argv) +{ + int i; + + for (i = 1; i < argc; ++i) { + if (!strcmp(argv[i], "--filter") && i + 1 < argc) { + return strcmp(name, argv[i + 1]) == 0; + } + } + + return 1; +} + +int main(int argc, char **argv) +{ + if (test_matches_filter("complete_runtime_dynamic_view", argc, argv)) { + test_complete_runtime_dynamic_view(); + } + if (test_matches_filter("dynamic_address_semantics", argc, argv)) { + test_dynamic_address_semantics(); + } + if (test_matches_filter("read_failure_reports_parser_state", + argc, argv)) { + test_read_failure_reports_parser_state(); + } + if (test_matches_filter("missing_null_stops_at_scan_limit", argc, argv)) { + test_missing_null_stops_at_scan_limit(); + } + if (test_matches_filter("unknown_tag_is_diagnostic_only", argc, argv)) { + test_unknown_tag_is_diagnostic_only(); + } + if (test_matches_filter("too_many_needed_reports_resource_limit", + argc, argv)) { + test_too_many_needed_reports_resource_limit(); + } + + if (failures) { + fprintf(stderr, "kzt-guest-dynamic-parser: %d failure(s)\n", + failures); + return 1; + } + + puts("kzt-guest-dynamic-parser: selected contract tests passed"); + return 0; +} diff --git a/tests/unit/kzt/test_guest_dynamic_snapshot.c b/tests/unit/kzt/test_guest_dynamic_snapshot.c new file mode 100644 index 00000000000..b9b723707b1 --- /dev/null +++ b/tests/unit/kzt/test_guest_dynamic_snapshot.c @@ -0,0 +1,500 @@ +#include +#include + +#include "target/i386/latx/include/kzt_guest_registry.h" + +static int failures; + +static void check_true(const char *name, int condition) +{ + if (condition) { + return; + } + + fprintf(stderr, "%s: condition failed\n", name); + ++failures; +} + +static void check_int(const char *name, int got, int expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got %d expected %d\n", name, got, expected); + ++failures; +} + +static void check_ulong(const char *name, unsigned long got, + unsigned long expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got 0x%lx expected 0x%lx\n", name, got, expected); + ++failures; +} + +static void check_uintptr(const char *name, uintptr_t got, uintptr_t expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got 0x%lx expected 0x%lx\n", name, + (unsigned long)got, (unsigned long)expected); + ++failures; +} + +static void check_u64(const char *name, uint64_t got, uint64_t expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got 0x%llx expected 0x%llx\n", name, + (unsigned long long)got, (unsigned long long)expected); + ++failures; +} + +static kzt_guest_object_observation_t make_observation(uintptr_t link_map_addr) +{ + return (kzt_guest_object_observation_t) { + .link_map_addr = link_map_addr, + .load_bias = { 0x100000, KZT_GUEST_FIELD_OK }, + .dynamic_addr = { 0x101000, KZT_GUEST_FIELD_OK }, + .map_start = { 0, KZT_GUEST_FIELD_UNKNOWN }, + .map_end = { 0, KZT_GUEST_FIELD_UNKNOWN }, + .namespace_id = { 0, KZT_GUEST_FIELD_UNKNOWN }, + .path = { "/guest/libfoo.so", KZT_GUEST_FIELD_OK }, + .soname = { NULL, KZT_GUEST_FIELD_NOT_PARSED }, + .dynamic_view_status = KZT_GUEST_FIELD_NOT_PARSED, + }; +} + +static kzt_guest_dynamic_field_t make_field( + uint64_t value, + kzt_guest_dynamic_address_semantics_t semantics) +{ + return (kzt_guest_dynamic_field_t) { + .present = 1, + .value = value, + .address_semantics = semantics, + }; +} + +static kzt_guest_dynamic_view_t make_dynamic_view(uintptr_t dynamic_addr, + uintptr_t load_bias, + uint64_t symtab) +{ + kzt_guest_dynamic_view_t view = { + .dynamic_addr = dynamic_addr, + .load_bias = load_bias, + .status = KZT_GUEST_DYNAMIC_COMPLETE, + .entry_count = 8, + .has_null = 1, + .scan_limit = KZT_GUEST_DYNAMIC_SCAN_LIMIT, + .unknown_tag_count = 1, + .first_unknown_tag = 0x6000000d, + .first_unknown_tag_index = 3, + .symtab = make_field(symtab, KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS), + .strtab = make_field(symtab + 0x1000, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS), + .strsz = make_field(0x240, KZT_GUEST_DYNAMIC_SCALAR), + .gnu_hash = make_field(symtab + 0x2000, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS), + .versym = make_field(symtab + 0x3000, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS), + .jmprel = make_field(symtab + 0x4000, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS), + .pltrelsz = make_field(0x30, KZT_GUEST_DYNAMIC_SCALAR), + .pltgot = make_field(symtab + 0x5000, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS), + .needed_offsets = { 0x10, 0x38 }, + .needed_count = 2, + .needed_address_semantics = KZT_GUEST_DYNAMIC_STRING_TABLE_OFFSET, + }; + + return view; +} + +static int dynamic_field_equal(const kzt_guest_dynamic_field_t *left, + const kzt_guest_dynamic_field_t *right) +{ + return left->present == right->present && + left->value == right->value && + left->address_semantics == right->address_semantics; +} + +static void assert_dynamic_view_equal(const char *name, + const kzt_guest_dynamic_view_t *got, + const kzt_guest_dynamic_view_t *expected) +{ + char field_name[128]; + size_t i; + + check_uintptr(name, got->dynamic_addr, expected->dynamic_addr); + check_uintptr(name, got->load_bias, expected->load_bias); + check_int(name, got->status, expected->status); + check_ulong(name, got->entry_count, expected->entry_count); + check_int(name, got->has_null, expected->has_null); + check_ulong("dynamic.scan_limit", got->scan_limit, expected->scan_limit); + check_ulong("dynamic.unknown_tag_count", got->unknown_tag_count, + expected->unknown_tag_count); + check_ulong("dynamic.first_unknown_tag", + (unsigned long)got->first_unknown_tag, + (unsigned long)expected->first_unknown_tag); + check_ulong("dynamic.first_unknown_tag_index", + got->first_unknown_tag_index, + expected->first_unknown_tag_index); + check_true("dynamic.symtab", dynamic_field_equal(&got->symtab, + &expected->symtab)); + check_true("dynamic.strtab", dynamic_field_equal(&got->strtab, + &expected->strtab)); + check_true("dynamic.strsz", dynamic_field_equal(&got->strsz, + &expected->strsz)); + check_true("dynamic.gnu_hash", dynamic_field_equal(&got->gnu_hash, + &expected->gnu_hash)); + check_true("dynamic.versym", dynamic_field_equal(&got->versym, + &expected->versym)); + check_true("dynamic.jmprel", dynamic_field_equal(&got->jmprel, + &expected->jmprel)); + check_true("dynamic.pltrelsz", dynamic_field_equal(&got->pltrelsz, + &expected->pltrelsz)); + check_true("dynamic.pltgot", dynamic_field_equal(&got->pltgot, + &expected->pltgot)); + check_ulong("dynamic.needed_count", got->needed_count, + expected->needed_count); + check_int("dynamic.needed_semantics", got->needed_address_semantics, + expected->needed_address_semantics); + for (i = 0; i < expected->needed_count; ++i) { + snprintf(field_name, sizeof(field_name), "%s.needed[%lu]", name, + (unsigned long)i); + check_u64(field_name, got->needed_offsets[i], + expected->needed_offsets[i]); + } +} + +static kzt_guest_object_snapshot_t *find_snapshot( + kzt_guest_registry_t *registry, + uintptr_t link_map_addr) +{ + kzt_guest_object_snapshot_t *snapshot = NULL; + + check_int("find_by_link_map", kzt_guest_registry_find_by_link_map( + registry, link_map_addr, &snapshot), 0); + check_true("find_by_link_map.snapshot", snapshot != NULL); + return snapshot; +} + +static void test_commit_and_query_are_per_object(void) +{ + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_object_observation_t first = make_observation(0x1000); + kzt_guest_object_observation_t second = make_observation(0x2000); + kzt_guest_dynamic_view_t first_view = + make_dynamic_view(0x101000, 0x100000, 0x7000010000); + kzt_guest_dynamic_view_t queried = { 0 }; + kzt_guest_field_status_t queried_status = KZT_GUEST_FIELD_UNKNOWN; + unsigned long generation = 0; + kzt_guest_object_snapshot_t *snapshot; + + check_true("registry.init", registry != NULL); + if (!registry) { + return; + } + + second.dynamic_addr.value = 0x201000; + check_int("observe.first", kzt_guest_registry_observe(registry, &first), + KZT_GUEST_REGISTRY_ADDED); + check_int("observe.second", kzt_guest_registry_observe(registry, &second), + KZT_GUEST_REGISTRY_ADDED); + + check_int("commit.first", + kzt_guest_registry_commit_dynamic_view(registry, 0x1000, 1, + &first_view), + KZT_GUEST_REGISTRY_UPDATED); + + snapshot = find_snapshot(registry, 0x1000); + check_int("snapshot.first.status", snapshot->dynamic_view_status, + KZT_GUEST_FIELD_OK); + check_int("snapshot.first.state", snapshot->state, + KZT_GUEST_OBJECT_PARSED); + check_ulong("snapshot.first.generation", snapshot->generation, 1); + assert_dynamic_view_equal("snapshot.first.view", + &snapshot->dynamic_view, &first_view); + kzt_guest_object_snapshot_free(snapshot); + + snapshot = find_snapshot(registry, 0x2000); + check_int("snapshot.second.status", snapshot->dynamic_view_status, + KZT_GUEST_FIELD_NOT_PARSED); + check_int("snapshot.second.state", snapshot->state, + KZT_GUEST_OBJECT_DISCOVERED); + check_ulong("snapshot.second.generation", snapshot->generation, 2); + check_uintptr("snapshot.second.dynamic-view-zero", + snapshot->dynamic_view.dynamic_addr, 0); + kzt_guest_object_snapshot_free(snapshot); + + check_int("find.dynamic-view", + kzt_guest_registry_find_dynamic_view(registry, 0x1000, + &queried, + &queried_status, + &generation), + 0); + check_int("queried.status", queried_status, KZT_GUEST_FIELD_OK); + check_ulong("queried.generation", generation, 1); + assert_dynamic_view_equal("queried.view", &queried, &first_view); + + kzt_guest_registry_destroy(®istry); + check_true("registry.destroy", registry == NULL); +} + +static void test_repeated_commit_and_replacement_semantics(void) +{ + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_object_observation_t observation = make_observation(0x3000); + kzt_guest_dynamic_view_t first_view = + make_dynamic_view(0x301000, 0x300000, 0x7100010000); + kzt_guest_dynamic_view_t replacement = + make_dynamic_view(0x301000, 0x300000, 0x7200010000); + kzt_guest_object_snapshot_t *snapshot; + + check_true("registry.init", registry != NULL); + if (!registry) { + return; + } + + check_int("observe.object", + kzt_guest_registry_observe(registry, &observation), + KZT_GUEST_REGISTRY_ADDED); + check_int("commit.first", + kzt_guest_registry_commit_dynamic_view(registry, 0x3000, 1, + &first_view), + KZT_GUEST_REGISTRY_UPDATED); + check_int("commit.same", + kzt_guest_registry_commit_dynamic_view(registry, 0x3000, 1, + &first_view), + KZT_GUEST_REGISTRY_UNCHANGED); + + replacement.needed_offsets[1] = 0x58; + replacement.entry_count = 9; + replacement.unknown_tag_count = 2; + replacement.first_unknown_tag = 0x6000000e; + replacement.first_unknown_tag_index = 4; + check_int("commit.replacement", + kzt_guest_registry_commit_dynamic_view(registry, 0x3000, 1, + &replacement), + KZT_GUEST_REGISTRY_UPDATED); + + snapshot = find_snapshot(registry, 0x3000); + check_ulong("snapshot.generation-stable", snapshot->generation, 1); + check_int("snapshot.status", snapshot->dynamic_view_status, + KZT_GUEST_FIELD_OK); + check_int("snapshot.state", snapshot->state, KZT_GUEST_OBJECT_PARSED); + assert_dynamic_view_equal("snapshot.replacement", + &snapshot->dynamic_view, &replacement); + kzt_guest_object_snapshot_free(snapshot); + + kzt_guest_registry_destroy(®istry); + check_true("registry.destroy", registry == NULL); +} + +static void test_snapshots_survive_replacement_and_registry_destroy(void) +{ + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_object_observation_t observation = make_observation(0x4000); + kzt_guest_dynamic_view_t first_view = + make_dynamic_view(0x401000, 0x400000, 0x7300010000); + kzt_guest_dynamic_view_t replacement = + make_dynamic_view(0x401000, 0x400000, 0x7400010000); + kzt_guest_object_snapshot_t *before_replace; + kzt_guest_object_snapshot_t *after_replace; + kzt_guest_registry_dump_t dump = { 0 }; + + check_true("registry.init", registry != NULL); + if (!registry) { + return; + } + + check_int("observe.object", + kzt_guest_registry_observe(registry, &observation), + KZT_GUEST_REGISTRY_ADDED); + check_int("commit.first", + kzt_guest_registry_commit_dynamic_view(registry, 0x4000, 1, + &first_view), + KZT_GUEST_REGISTRY_UPDATED); + before_replace = find_snapshot(registry, 0x4000); + + check_int("commit.replacement", + kzt_guest_registry_commit_dynamic_view(registry, 0x4000, 1, + &replacement), + KZT_GUEST_REGISTRY_UPDATED); + after_replace = find_snapshot(registry, 0x4000); + + check_int("dump.snapshot", + kzt_guest_registry_dump_snapshot(registry, &dump), 0); + check_ulong("dump.count", dump.count, 1); + + kzt_guest_registry_destroy(®istry); + check_true("registry.destroy", registry == NULL); + + assert_dynamic_view_equal("before_replace.still-first", + &before_replace->dynamic_view, &first_view); + assert_dynamic_view_equal("after_replace.still-replacement", + &after_replace->dynamic_view, &replacement); + assert_dynamic_view_equal("dump.still-replacement", + &dump.objects[0].dynamic_view, &replacement); + + kzt_guest_object_snapshot_free(before_replace); + kzt_guest_object_snapshot_free(after_replace); + kzt_guest_registry_dump_free(&dump); +} + +static void test_missing_and_destroyed_registry_are_rejected(void) +{ + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_dynamic_view_t view = + make_dynamic_view(0x501000, 0x500000, 0x7500010000); + kzt_guest_dynamic_view_t queried = { + .dynamic_addr = 0xdeadbeef, + }; + kzt_guest_field_status_t status = KZT_GUEST_FIELD_OK; + unsigned long generation = 99; + + check_true("registry.init", registry != NULL); + if (!registry) { + return; + } + + check_int("commit.missing", + kzt_guest_registry_commit_dynamic_view(registry, 0x5000, 1, + &view), + KZT_GUEST_REGISTRY_ERROR); + check_int("find.missing", + kzt_guest_registry_find_dynamic_view(registry, 0x5000, + &queried, &status, + &generation), + -1); + check_uintptr("find.missing.clears-view", queried.dynamic_addr, 0); + check_int("find.missing.status", status, KZT_GUEST_FIELD_NOT_PARSED); + check_ulong("find.missing.generation", generation, 0); + + kzt_guest_registry_destroy(®istry); + check_true("registry.destroy", registry == NULL); + check_int("commit.destroyed", + kzt_guest_registry_commit_dynamic_view(registry, 0x5000, 1, + &view), + KZT_GUEST_REGISTRY_DISABLED); + check_int("find.destroyed", + kzt_guest_registry_find_dynamic_view(registry, 0x5000, + &queried, &status, + &generation), + -1); +} + +static void test_stale_generation_cannot_revive_or_overwrite(void) +{ + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_object_observation_t observation = make_observation(0x6000); + kzt_guest_dynamic_view_t stale = + make_dynamic_view(0x601000, 0x600000, 0x7600010000); + kzt_guest_dynamic_view_t fresh = + make_dynamic_view(0x601000, 0x600000, 0x7700010000); + kzt_guest_dynamic_view_t queried = { 0 }; + kzt_guest_field_status_t status; + unsigned long generation; + + check_int("stale.observe.a", kzt_guest_registry_observe( + registry, &observation), KZT_GUEST_REGISTRY_ADDED); + check_int("stale.retire.a", kzt_guest_registry_retire( + registry, observation.link_map_addr, 1), 0); + check_int("stale.dead-commit", kzt_guest_registry_commit_dynamic_view( + registry, observation.link_map_addr, 1, &stale), + KZT_GUEST_REGISTRY_ERROR); + check_int("stale.observe.b", kzt_guest_registry_observe( + registry, &observation), KZT_GUEST_REGISTRY_ADDED); + check_int("stale.reused-commit", kzt_guest_registry_commit_dynamic_view( + registry, observation.link_map_addr, 1, &stale), + KZT_GUEST_REGISTRY_ERROR); + check_int("stale.fresh-commit", kzt_guest_registry_commit_dynamic_view( + registry, observation.link_map_addr, 2, &fresh), + KZT_GUEST_REGISTRY_UPDATED); + check_int("stale.query", kzt_guest_registry_find_dynamic_view( + registry, observation.link_map_addr, &queried, &status, + &generation), 0); + check_ulong("stale.generation", generation, 2); + assert_dynamic_view_equal("stale.fresh-view", &queried, &fresh); + kzt_guest_registry_destroy(®istry); +} + +static void test_incomplete_dynamic_view_preserves_complete_view(void) +{ + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_object_observation_t observation = make_observation(0x7000); + kzt_guest_dynamic_view_t complete = + make_dynamic_view(0x701000, 0x700000, 0x7800010000); + kzt_guest_dynamic_view_t incomplete = complete; + kzt_guest_dynamic_view_t queried = { 0 }; + kzt_guest_field_status_t status = KZT_GUEST_FIELD_UNKNOWN; + unsigned long generation = 0; + + check_true("preserve-complete.init", registry != NULL); + if (!registry) { + return; + } + + check_int("preserve-complete.observe", kzt_guest_registry_observe( + registry, &observation), KZT_GUEST_REGISTRY_ADDED); + check_int("preserve-complete.commit", kzt_guest_registry_commit_dynamic_view( + registry, observation.link_map_addr, 1, &complete), + KZT_GUEST_REGISTRY_UPDATED); + + incomplete.status = KZT_GUEST_DYNAMIC_READ_ERROR; + incomplete.has_null = 0; + incomplete.entry_count = 1; + check_int("preserve-complete.read-error", + kzt_guest_registry_commit_dynamic_view( + registry, observation.link_map_addr, 1, &incomplete), + KZT_GUEST_REGISTRY_UNCHANGED); + incomplete.status = KZT_GUEST_DYNAMIC_TRUNCATED_NO_NULL; + incomplete.entry_count = KZT_GUEST_DYNAMIC_SCAN_LIMIT; + check_int("preserve-complete.truncated", + kzt_guest_registry_commit_dynamic_view( + registry, observation.link_map_addr, 1, &incomplete), + KZT_GUEST_REGISTRY_UNCHANGED); + incomplete.status = KZT_GUEST_DYNAMIC_ERROR; + incomplete.entry_count = 2; + check_int("preserve-complete.error", + kzt_guest_registry_commit_dynamic_view( + registry, observation.link_map_addr, 1, &incomplete), + KZT_GUEST_REGISTRY_UNCHANGED); + + check_int("preserve-complete.find", kzt_guest_registry_find_dynamic_view( + registry, observation.link_map_addr, &queried, &status, + &generation), 0); + check_int("preserve-complete.status", status, KZT_GUEST_FIELD_OK); + check_ulong("preserve-complete.generation", generation, 1); + assert_dynamic_view_equal("preserve-complete.view", &queried, &complete); + + kzt_guest_registry_destroy(®istry); +} + +int main(void) +{ + test_commit_and_query_are_per_object(); + test_repeated_commit_and_replacement_semantics(); + test_snapshots_survive_replacement_and_registry_destroy(); + test_missing_and_destroyed_registry_are_rejected(); + test_stale_generation_cannot_revive_or_overwrite(); + test_incomplete_dynamic_view_preserves_complete_view(); + + if (failures) { + fprintf(stderr, "kzt-guest-dynamic-snapshot: %d failure(s)\n", + failures); + return 1; + } + + puts("kzt-guest-dynamic-snapshot: all contract tests passed"); + return 0; +} diff --git a/tests/unit/kzt/test_guest_library_binding.c b/tests/unit/kzt/test_guest_library_binding.c new file mode 100644 index 00000000000..bc787480b1b --- /dev/null +++ b/tests/unit/kzt/test_guest_library_binding.c @@ -0,0 +1,1692 @@ +#include +#include +#include +#include +#include + +#include "kzt_guest_library_binding.h" +#include "kzt_guest_registry.h" + +typedef struct fake_library { int id; } fake_library_t; +static int failures; + +#define CHECK(name, expr) do { if (!(expr)) { \ + fprintf(stderr, "FAIL %s\n", name); ++failures; } } while (0) + +static kzt_guest_library_binding_key_t key(uintptr_t map, + unsigned long generation) +{ + return (kzt_guest_library_binding_key_t){ + .link_map_addr = map, + .generation = generation, + .namespace_id = 0, + .namespace_kind = KZT_GUEST_LIBRARY_NAMESPACE_MAIN, + }; +} + +static kzt_guest_object_observation_t observation(uintptr_t map) +{ + return (kzt_guest_object_observation_t){ + .link_map_addr = map, + .load_bias = { .value = 0x400000, .status = KZT_GUEST_FIELD_OK }, + .dynamic_addr = { .status = KZT_GUEST_FIELD_UNKNOWN }, + .map_start = { .status = KZT_GUEST_FIELD_UNKNOWN }, + .map_end = { .status = KZT_GUEST_FIELD_UNKNOWN }, + .namespace_id = { .value = 0, .status = KZT_GUEST_FIELD_OK }, + .path = { .status = KZT_GUEST_FIELD_UNKNOWN }, + .soname = { .status = KZT_GUEST_FIELD_UNKNOWN }, + .dynamic_view_status = KZT_GUEST_FIELD_NOT_PARSED, + }; +} + +static unsigned long registry_generation(kzt_guest_registry_t *registry, + uintptr_t map) +{ + kzt_guest_object_snapshot_t *snapshot = NULL; + unsigned long generation = 0; + CHECK("registry snapshot", kzt_guest_registry_find_by_link_map( + registry, map, &snapshot) == 0 && snapshot != NULL); + if (snapshot) generation = snapshot->generation; + kzt_guest_object_snapshot_free(snapshot); + return generation; +} + +static void test_init_failure_is_fail_open(void) +{ + fake_library_t lib = { 1 }; + kzt_guest_library_binding_key_t k = key(0x1000, 1); + kzt_guest_library_binding_test_set_alloc_failure_after(0); + kzt_guest_library_bindings_t *bindings = kzt_guest_library_bindings_init(); + CHECK("init failure", bindings == NULL); + CHECK("disabled bind", kzt_guest_library_bind(bindings, &k, + (library_t *)&lib, KZT_GUEST_LIBRARY_OBJECT_WRAPPED) == + KZT_GUEST_LIBRARY_BINDING_DISABLED); + kzt_guest_library_binding_test_set_alloc_failure_after(-1); +} + +static void test_both_arrival_orders_and_retry(void) +{ + fake_library_t wrapped = { 2 }, emulated = { 3 }; + kzt_guest_library_bindings_t *bindings = kzt_guest_library_bindings_init(); + kzt_guest_library_binding_key_t a = key(0x2000, 7); + kzt_guest_library_binding_key_t b = key(0x3000, 8); + kzt_guest_library_handle_t handle; + + CHECK("track wrapped", kzt_guest_library_track( + bindings, (library_t *)&wrapped) == 0); + CHECK("track emulated", kzt_guest_library_track( + bindings, (library_t *)&emulated) == 0); + + CHECK("pair first pending", kzt_guest_library_note_exact_pair( + bindings, a.link_map_addr, (library_t *)&wrapped, + KZT_GUEST_LIBRARY_OBJECT_WRAPPED) == + KZT_GUEST_LIBRARY_BINDING_PENDING); + kzt_guest_library_binding_test_set_alloc_failure_after(0); + CHECK("transient observation failure", kzt_guest_library_note_observation( + bindings, &a) == KZT_GUEST_LIBRARY_BINDING_ERROR); + kzt_guest_library_binding_test_set_alloc_failure_after(-1); + CHECK("observation retry binds", kzt_guest_library_note_observation( + bindings, &a) == KZT_GUEST_LIBRARY_BINDING_ADDED); + CHECK("wrapped exact lookup", kzt_guest_library_lookup( + bindings, &a, &handle) == 0 && + handle.library == (library_t *)&wrapped && + handle.object_type == KZT_GUEST_LIBRARY_OBJECT_WRAPPED); + kzt_guest_library_handle_release(&handle); + + CHECK("observation first", kzt_guest_library_note_observation( + bindings, &b) == KZT_GUEST_LIBRARY_BINDING_UNCHANGED); + CHECK("emulated pair binds", kzt_guest_library_note_exact_pair( + bindings, b.link_map_addr, (library_t *)&emulated, + KZT_GUEST_LIBRARY_OBJECT_EMULATED) == + KZT_GUEST_LIBRARY_BINDING_ADDED); + CHECK("emulated exact lookup", kzt_guest_library_lookup( + bindings, &b, &handle) == 0 && + handle.library == (library_t *)&emulated && + handle.object_type == KZT_GUEST_LIBRARY_OBJECT_EMULATED); + kzt_guest_library_handle_release(&handle); + + b.namespace_id = 4; + b.namespace_kind = KZT_GUEST_LIBRARY_NAMESPACE_EXPLICIT; + CHECK("non-main namespace fail open", kzt_guest_library_note_observation( + bindings, &b) == KZT_GUEST_LIBRARY_BINDING_ERROR); + CHECK("main executable type fail open", kzt_guest_library_note_exact_pair( + bindings, 0x4000, (library_t *)&emulated, + KZT_GUEST_LIBRARY_OBJECT_MAIN) == KZT_GUEST_LIBRARY_BINDING_ERROR); + + kzt_guest_library_bindings_destroy(&bindings); +} + +static void test_forced_growth_with_held_handle(void) +{ + fake_library_t libs[24]; + kzt_guest_library_bindings_t *bindings = kzt_guest_library_bindings_init(); + kzt_guest_library_handle_t held; + kzt_guest_library_binding_key_t first = key(0x5000, 1); + + for (size_t i = 0; i < 24; ++i) { + libs[i].id = (int)i; + CHECK("growth track", kzt_guest_library_track( + bindings, (library_t *)&libs[i]) == 0); + } + CHECK("growth first bind", kzt_guest_library_bind( + bindings, &first, (library_t *)&libs[0], + KZT_GUEST_LIBRARY_OBJECT_WRAPPED) == + KZT_GUEST_LIBRARY_BINDING_ADDED); + CHECK("growth hold", kzt_guest_library_lookup( + bindings, &first, &held) == 0); + for (size_t i = 1; i < 24; ++i) { + kzt_guest_library_binding_key_t next = + key(0x5000 + i * 0x100, i + 1); + CHECK("growth bind", kzt_guest_library_bind( + bindings, &next, (library_t *)&libs[i], + KZT_GUEST_LIBRARY_OBJECT_EMULATED) == + KZT_GUEST_LIBRARY_BINDING_ADDED); + } + CHECK("held survives realloc", held.library == (library_t *)&libs[0]); + kzt_guest_library_handle_release(&held); + kzt_guest_library_bindings_destroy(&bindings); +} + +static void test_pending_cancel_and_address_reuse(void) +{ + fake_library_t old = { 4 }, replacement = { 5 }; + kzt_guest_library_bindings_t *bindings = kzt_guest_library_bindings_init(); + kzt_guest_library_binding_key_t old_key = key(0x9000, 11); + kzt_guest_library_binding_key_t new_key = key(0x9000, 12); + kzt_guest_library_handle_t handle; + + CHECK("cancel track", kzt_guest_library_track( + bindings, (library_t *)&old) == 0); + CHECK("cancel pending", kzt_guest_library_note_exact_pair( + bindings, old_key.link_map_addr, (library_t *)&old, + KZT_GUEST_LIBRARY_OBJECT_WRAPPED) == + KZT_GUEST_LIBRARY_BINDING_PENDING); + kzt_guest_library_unbind(bindings, NULL, (library_t *)&old, + old_key.link_map_addr); + CHECK("late attach rejected", kzt_guest_library_note_exact_pair( + bindings, old_key.link_map_addr, (library_t *)&old, + KZT_GUEST_LIBRARY_OBJECT_WRAPPED) == + KZT_GUEST_LIBRARY_BINDING_ERROR); + CHECK("canceled observation cannot bind", kzt_guest_library_note_observation( + bindings, &old_key) == KZT_GUEST_LIBRARY_BINDING_CANCELLED); + CHECK("canceled lookup", kzt_guest_library_lookup( + bindings, &old_key, &handle) != 0); + + CHECK("reuse track", kzt_guest_library_track( + bindings, (library_t *)&replacement) == 0); + CHECK("reuse observation", kzt_guest_library_note_observation( + bindings, &new_key) == KZT_GUEST_LIBRARY_BINDING_UNCHANGED); + CHECK("reuse exact pair", kzt_guest_library_note_exact_pair( + bindings, new_key.link_map_addr, (library_t *)&replacement, + KZT_GUEST_LIBRARY_OBJECT_EMULATED) == + KZT_GUEST_LIBRARY_BINDING_ADDED); + CHECK("old generation absent", kzt_guest_library_lookup( + bindings, &old_key, &handle) != 0); + CHECK("new generation present", kzt_guest_library_lookup( + bindings, &new_key, &handle) == 0 && + handle.library == (library_t *)&replacement); + kzt_guest_library_handle_release(&handle); + kzt_guest_library_bindings_destroy(&bindings); +} + +static void test_unclaimed_observation_address_reuse_pair_first(void) +{ + fake_library_t old = { 40 }, replacement = { 41 }; + kzt_guest_library_bindings_t *bindings = kzt_guest_library_bindings_init(); + kzt_guest_library_binding_key_t stale = key(0x9800, 30); + kzt_guest_library_binding_key_t fresh = key(0x9800, 31); + kzt_guest_library_handle_t handle; + + CHECK("unclaimed old track", kzt_guest_library_track( + bindings, (library_t *)&old) == 0); + CHECK("unclaimed old observation", kzt_guest_library_note_observation( + bindings, &stale) == KZT_GUEST_LIBRARY_BINDING_UNCHANGED); + /* There is deliberately no exact pair/entry for the old observation. */ + kzt_guest_library_inactivate(bindings, NULL, (library_t *)&old, + stale.link_map_addr); + + CHECK("unclaimed replacement track", kzt_guest_library_track( + bindings, (library_t *)&replacement) == 0); + CHECK("replacement pair waits for fresh observation", + kzt_guest_library_note_exact_pair( + bindings, fresh.link_map_addr, (library_t *)&replacement, + KZT_GUEST_LIBRARY_OBJECT_WRAPPED) == + KZT_GUEST_LIBRARY_BINDING_PENDING); + CHECK("pair first remains fail open", kzt_guest_library_lookup( + bindings, &fresh, &handle) != 0); + CHECK("fresh observation binds replacement", + kzt_guest_library_note_observation(bindings, &fresh) == + KZT_GUEST_LIBRARY_BINDING_ADDED); + CHECK("stale generation stays absent", kzt_guest_library_lookup( + bindings, &stale, &handle) != 0); + CHECK("fresh generation lookup", kzt_guest_library_lookup( + bindings, &fresh, &handle) == 0 && + handle.library == (library_t *)&replacement); + kzt_guest_library_handle_release(&handle); + kzt_guest_library_bindings_destroy(&bindings); +} + +static void test_observation_first_unload_retires_registry_generation(void) +{ + fake_library_t lib = { 42 }; + uintptr_t map = 0x9900; + kzt_guest_object_observation_t observed = observation(map); + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_library_bindings_t *bindings = kzt_guest_library_bindings_init(); + unsigned long first, second; + + CHECK("joint track", kzt_guest_library_track( + bindings, (library_t *)&lib) == 0); + CHECK("joint registry first", kzt_guest_registry_observe( + registry, &observed) == KZT_GUEST_REGISTRY_ADDED); + first = registry_generation(registry, map); + CHECK("joint binding observation", kzt_guest_library_note_observation( + bindings, &(kzt_guest_library_binding_key_t){ + .link_map_addr = map, + .generation = first, + .namespace_id = 0, + .namespace_kind = KZT_GUEST_LIBRARY_NAMESPACE_MAIN, + }) == KZT_GUEST_LIBRARY_BINDING_UNCHANGED); + + /* No exact pair is ever supplied. Unload must still retire the registry + * identity attached to the binding-side observation. */ + kzt_guest_library_inactivate(bindings, registry, (library_t *)&lib, map); + CHECK("joint identical reuse added", kzt_guest_registry_observe( + registry, &observed) == KZT_GUEST_REGISTRY_ADDED); + second = registry_generation(registry, map); + CHECK("joint reuse gets new generation", second > first); + + kzt_guest_library_bindings_destroy(&bindings); + kzt_guest_registry_destroy(®istry); +} + +static void test_unload_retires_only_hinted_unclaimed_observation(void) +{ + fake_library_t a = { 44 }, b = { 45 }; + uintptr_t map_a = 0x9b00, map_b = 0x9c00; + kzt_guest_object_observation_t observed_a = observation(map_a); + kzt_guest_object_observation_t observed_b = observation(map_b); + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_library_bindings_t *bindings = kzt_guest_library_bindings_init(); + kzt_guest_library_binding_key_t key_a, key_b; + kzt_guest_object_snapshot_t *snapshot = NULL; + kzt_guest_library_handle_t handle; + + CHECK("two-lib track a", kzt_guest_library_track( + bindings, (library_t *)&a) == 0); + CHECK("two-lib track b", kzt_guest_library_track( + bindings, (library_t *)&b) == 0); + CHECK("two-lib observe registry a", kzt_guest_registry_observe( + registry, &observed_a) == KZT_GUEST_REGISTRY_ADDED); + CHECK("two-lib observe registry b", kzt_guest_registry_observe( + registry, &observed_b) == KZT_GUEST_REGISTRY_ADDED); + key_a = key(map_a, registry_generation(registry, map_a)); + key_b = key(map_b, registry_generation(registry, map_b)); + CHECK("two-lib binding observation a", kzt_guest_library_note_observation( + bindings, &key_a) == KZT_GUEST_LIBRARY_BINDING_UNCHANGED); + CHECK("two-lib binding observation b", kzt_guest_library_note_observation( + bindings, &key_b) == KZT_GUEST_LIBRARY_BINDING_UNCHANGED); + + kzt_guest_library_inactivate(bindings, registry, (library_t *)&a, map_a); + CHECK("two-lib a retired", kzt_guest_registry_find_by_link_map( + registry, map_a, &snapshot) != 0 && snapshot == NULL); + CHECK("two-lib b remains live", kzt_guest_registry_find_by_link_map( + registry, map_b, &snapshot) == 0 && snapshot != NULL && + snapshot->generation == key_b.generation && + snapshot->state != KZT_GUEST_OBJECT_UNLOADING && + snapshot->state != KZT_GUEST_OBJECT_DEAD); + kzt_guest_object_snapshot_free(snapshot); + snapshot = NULL; + CHECK("two-lib b completes exact pair", + kzt_guest_library_note_exact_pair( + bindings, map_b, (library_t *)&b, + KZT_GUEST_LIBRARY_OBJECT_WRAPPED) == + KZT_GUEST_LIBRARY_BINDING_ADDED); + CHECK("two-lib b lookup", kzt_guest_library_lookup( + bindings, &key_b, &handle) == 0 && + handle.library == (library_t *)&b); + kzt_guest_library_handle_release(&handle); + + kzt_guest_library_unbind(bindings, registry, (library_t *)&b, map_b); + kzt_guest_library_unbind(bindings, registry, (library_t *)&a, 0); + kzt_guest_library_bindings_destroy(&bindings); + kzt_guest_registry_destroy(®istry); +} + +static void test_missing_unload_hint_preserves_unclaimed_observation(void) +{ + fake_library_t lib = { 46 }; + uintptr_t map = 0x9d00; + kzt_guest_object_observation_t observed = observation(map); + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_library_bindings_t *bindings = kzt_guest_library_bindings_init(); + kzt_guest_object_snapshot_t *snapshot = NULL; + kzt_guest_library_binding_key_t observed_key; + + CHECK("no-hint track", kzt_guest_library_track( + bindings, (library_t *)&lib) == 0); + CHECK("no-hint registry", kzt_guest_registry_observe( + registry, &observed) == KZT_GUEST_REGISTRY_ADDED); + observed_key = key(map, registry_generation(registry, map)); + CHECK("no-hint observation", kzt_guest_library_note_observation( + bindings, &observed_key) == KZT_GUEST_LIBRARY_BINDING_UNCHANGED); + kzt_guest_library_inactivate(bindings, registry, (library_t *)&lib, 0); + CHECK("no-hint remains live", kzt_guest_registry_find_by_link_map( + registry, map, &snapshot) == 0 && snapshot != NULL && + snapshot->generation == observed_key.generation); + kzt_guest_object_snapshot_free(snapshot); + + CHECK("no-hint explicit cleanup", kzt_guest_registry_retire( + registry, map, observed_key.generation) == 0); + kzt_guest_library_bindings_destroy(&bindings); + kzt_guest_registry_destroy(®istry); +} + +static void test_dead_library_stale_hint_does_not_retire_reused_address(void) +{ + fake_library_t old = { 47 }, replacement = { 48 }; + uintptr_t map = 0x9e00; + kzt_guest_object_observation_t observed = observation(map); + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_library_bindings_t *bindings = kzt_guest_library_bindings_init(); + kzt_guest_object_snapshot_t *snapshot = NULL; + kzt_guest_library_binding_key_t old_key, replacement_key; + + CHECK("stale-hint track old", kzt_guest_library_track( + bindings, (library_t *)&old) == 0); + CHECK("stale-hint registry old", kzt_guest_registry_observe( + registry, &observed) == KZT_GUEST_REGISTRY_ADDED); + old_key = key(map, registry_generation(registry, map)); + CHECK("stale-hint observe old", kzt_guest_library_note_observation( + bindings, &old_key) == KZT_GUEST_LIBRARY_BINDING_UNCHANGED); + kzt_guest_library_unbind(bindings, registry, (library_t *)&old, map); + + CHECK("stale-hint track replacement", kzt_guest_library_track( + bindings, (library_t *)&replacement) == 0); + CHECK("stale-hint registry replacement", kzt_guest_registry_observe( + registry, &observed) == KZT_GUEST_REGISTRY_ADDED); + replacement_key = key(map, registry_generation(registry, map)); + CHECK("stale-hint replacement generation", + replacement_key.generation > old_key.generation); + CHECK("stale-hint observe replacement", + kzt_guest_library_note_observation(bindings, &replacement_key) == + KZT_GUEST_LIBRARY_BINDING_UNCHANGED); + + /* A is already DEAD. Repeating its obsolete address hint must not own + * or retire the observation which now belongs to B's generation. */ + kzt_guest_library_unbind(bindings, registry, (library_t *)&old, map); + CHECK("stale-hint replacement remains live", + kzt_guest_registry_find_by_link_map(registry, map, &snapshot) == 0 && + snapshot && snapshot->generation == replacement_key.generation && + snapshot->state != KZT_GUEST_OBJECT_UNLOADING && + snapshot->state != KZT_GUEST_OBJECT_DEAD); + kzt_guest_object_snapshot_free(snapshot); + + kzt_guest_library_unbind(bindings, registry, + (library_t *)&replacement, map); + kzt_guest_library_bindings_destroy(&bindings); + kzt_guest_registry_destroy(®istry); +} + +static void test_tracking_allocation_failure_unload_is_fail_open(void) +{ + fake_library_t untracked = { 43 }; + uintptr_t map = 0x9a00; + kzt_guest_object_observation_t observed = observation(map); + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_library_bindings_t *bindings = kzt_guest_library_bindings_init(); + kzt_guest_object_snapshot_t *snapshot = NULL; + unsigned long first; + + kzt_guest_library_binding_test_set_alloc_failure_after(0); + CHECK("alloc-fail track", kzt_guest_library_track( + bindings, (library_t *)&untracked) != 0); + kzt_guest_library_binding_test_set_alloc_failure_after(-1); + + CHECK("alloc-fail registry first", kzt_guest_registry_observe( + registry, &observed) == KZT_GUEST_REGISTRY_ADDED); + first = registry_generation(registry, map); + CHECK("alloc-fail binding observation", + kzt_guest_library_note_observation( + bindings, &(kzt_guest_library_binding_key_t){ + .link_map_addr = map, + .generation = first, + .namespace_id = 0, + .namespace_kind = KZT_GUEST_LIBRARY_NAMESPACE_MAIN, + }) == KZT_GUEST_LIBRARY_BINDING_UNCHANGED); + + /* With no lifecycle record there is no provable unload owner. The stale + * address hint must therefore preserve the exact observation fail-open. */ + kzt_guest_library_binding_test_set_alloc_failure_after(0); + kzt_guest_library_unbind(bindings, registry, (library_t *)&untracked, + map); + kzt_guest_library_binding_test_set_alloc_failure_after(-1); + CHECK("alloc-fail observation remains live", + kzt_guest_registry_find_by_link_map(registry, map, &snapshot) == 0 && + snapshot && snapshot->generation == first && + snapshot->state != KZT_GUEST_OBJECT_UNLOADING && + snapshot->state != KZT_GUEST_OBJECT_DEAD); + kzt_guest_object_snapshot_free(snapshot); + CHECK("alloc-fail explicit cleanup", + kzt_guest_registry_retire(registry, map, first) == 0); + + kzt_guest_library_bindings_destroy(&bindings); + kzt_guest_registry_destroy(®istry); +} + +static void test_inactive_can_reload_but_destroyed_cannot(void) +{ + fake_library_t lib = { 7 }; + kzt_guest_library_bindings_t *bindings = kzt_guest_library_bindings_init(); + kzt_guest_library_binding_key_t first = key(0xb000, 20); + kzt_guest_library_binding_key_t second = key(0xb000, 21); + kzt_guest_library_handle_t handle; + + CHECK("reload track", kzt_guest_library_track( + bindings, (library_t *)&lib) == 0); + CHECK("reload observe first", kzt_guest_library_note_observation( + bindings, &first) == KZT_GUEST_LIBRARY_BINDING_UNCHANGED); + CHECK("reload bind first", kzt_guest_library_note_exact_pair( + bindings, first.link_map_addr, (library_t *)&lib, + KZT_GUEST_LIBRARY_OBJECT_EMULATED) == + KZT_GUEST_LIBRARY_BINDING_ADDED); + kzt_guest_library_inactivate(bindings, NULL, (library_t *)&lib, + first.link_map_addr); + CHECK("reload reactivate", kzt_guest_library_reactivate( + bindings, (library_t *)&lib) == 0); + CHECK("reload observe second", kzt_guest_library_note_observation( + bindings, &second) == KZT_GUEST_LIBRARY_BINDING_UNCHANGED); + CHECK("reload bind second", kzt_guest_library_note_exact_pair( + bindings, second.link_map_addr, (library_t *)&lib, + KZT_GUEST_LIBRARY_OBJECT_EMULATED) == + KZT_GUEST_LIBRARY_BINDING_ADDED); + CHECK("reload lookup", kzt_guest_library_lookup( + bindings, &second, &handle) == 0); + kzt_guest_library_handle_release(&handle); + kzt_guest_library_unbind(bindings, NULL, (library_t *)&lib, + second.link_map_addr); + CHECK("destroyed no reactivate", kzt_guest_library_reactivate( + bindings, (library_t *)&lib) != 0); + kzt_guest_library_bindings_destroy(&bindings); +} + +static void run_different_library_address_reuse(int force_gate_alloc_failure, + const char *prefix) +{ + fake_library_t old_library = { 41 }; + fake_library_t new_library = { 42 }; + kzt_guest_library_bindings_t *bindings = + kzt_guest_library_bindings_init(); + kzt_guest_library_loader_scope_t scope = { 0 }; + kzt_guest_library_callback_access_t stale = { 0 }; + kzt_guest_library_callback_access_t current = { 0 }; + uintptr_t map = force_gate_alloc_failure ? 0xa11000 : 0xa10000; + + CHECK(prefix, bindings != NULL); + if (!bindings) return; + CHECK("reuse old track", kzt_guest_library_track( + bindings, (library_t *)&old_library) == 0); + CHECK("reuse old loader pair", kzt_guest_library_publish_loader_pair( + bindings, map, (library_t *)&old_library, + KZT_GUEST_LIBRARY_OBJECT_WRAPPED) == + KZT_GUEST_LIBRARY_BINDING_PENDING); + if (force_gate_alloc_failure) + kzt_guest_library_binding_test_set_alloc_failure_after(0); + kzt_guest_library_inactivate(bindings, NULL, + (library_t *)&old_library, map); + kzt_guest_library_binding_test_set_alloc_failure_after(-1); + + /* A callback left over from the old causal chain has no new loader scope + * and must still be rejected before the first guest read. */ + CHECK("reuse stale callback rejected", + kzt_guest_library_callback_access_begin( + bindings, map, &stale) != 0); + + CHECK("reuse new track", kzt_guest_library_track( + bindings, (library_t *)&new_library) == 0); + CHECK("reuse loader scope", kzt_guest_library_loader_scope_begin( + bindings, &scope) == 0); + CHECK("reuse current callback admitted", + kzt_guest_library_callback_access_begin_scoped( + bindings, map, &scope, ¤t) == 0); + kzt_guest_library_callback_access_end(¤t); + CHECK("reuse pending loader pair", + kzt_guest_library_loader_scope_note_pair( + &scope, map, (library_t *)&new_library, + KZT_GUEST_LIBRARY_OBJECT_WRAPPED) == + KZT_GUEST_LIBRARY_BINDING_PENDING); + CHECK("reuse new loader pair", kzt_guest_library_loader_scope_publish_pair( + &scope, map, (library_t *)&new_library, + KZT_GUEST_LIBRARY_OBJECT_WRAPPED) == + KZT_GUEST_LIBRARY_BINDING_PENDING); + kzt_guest_library_loader_scope_end(&scope); + + kzt_guest_library_bindings_destroy(&bindings); +} + +static void test_different_library_reuses_closed_callback_address(void) +{ + run_different_library_address_reuse(0, "reuse normal init"); + run_different_library_address_reuse(1, "reuse fallback init"); +} + +static void prepare_scoped_pair( + kzt_guest_library_bindings_t *bindings, + kzt_guest_library_loader_scope_t *scope, uintptr_t map, + library_t *library) +{ + kzt_guest_library_callback_access_t access = { 0 }; + + CHECK("transaction scope", kzt_guest_library_loader_scope_begin( + bindings, scope) == 0); + CHECK("transaction callback", kzt_guest_library_callback_access_begin_scoped( + bindings, map, scope, &access) == 0); + kzt_guest_library_callback_access_end(&access); + CHECK("transaction prepare", kzt_guest_library_loader_scope_note_pair( + scope, map, library, KZT_GUEST_LIBRARY_OBJECT_WRAPPED) == + KZT_GUEST_LIBRARY_BINDING_PENDING); +} + +static void test_loader_pair_is_invisible_until_publish(void) +{ + fake_library_t lib = { 61 }; + kzt_guest_library_bindings_t *bindings = + kzt_guest_library_bindings_init(); + kzt_guest_library_loader_scope_t scope = { 0 }; + kzt_guest_library_binding_key_t observed = key(0xc900, 30); + kzt_guest_library_handle_t handle; + size_t active_pending = 99, live_entries = 99; + + CHECK("transaction init", bindings != NULL); + if (!bindings) return; + CHECK("transaction track", kzt_guest_library_track( + bindings, (library_t *)&lib) == 0); + CHECK("transaction observe", kzt_guest_library_note_observation( + bindings, &observed) == KZT_GUEST_LIBRARY_BINDING_UNCHANGED); + prepare_scoped_pair(bindings, &scope, observed.link_map_addr, + (library_t *)&lib); + CHECK("transaction prepared invisible", kzt_guest_library_lookup( + bindings, &observed, &handle) != 0); + CHECK("transaction prepared snapshot", + kzt_guest_library_binding_test_snapshot( + bindings, (library_t *)&lib, NULL, &active_pending, + &live_entries) == 0); + CHECK("transaction prepared has no global state", + active_pending == 0 && live_entries == 0); + CHECK("transaction publish", kzt_guest_library_loader_scope_publish_pair( + &scope, observed.link_map_addr, (library_t *)&lib, + KZT_GUEST_LIBRARY_OBJECT_WRAPPED) == + KZT_GUEST_LIBRARY_BINDING_ADDED); + CHECK("transaction published visible", kzt_guest_library_lookup( + bindings, &observed, &handle) == 0); + kzt_guest_library_handle_release(&handle); + kzt_guest_library_loader_scope_end(&scope); + kzt_guest_library_bindings_destroy(&bindings); +} + +static void test_loader_pair_cancel_and_failed_publish_are_invisible(void) +{ + fake_library_t prepared = { 62 }, wrong = { 63 }; + kzt_guest_library_bindings_t *bindings = + kzt_guest_library_bindings_init(); + kzt_guest_library_loader_scope_t scope = { 0 }; + kzt_guest_library_binding_key_t cancelled = key(0xca00, 31); + kzt_guest_library_binding_key_t failed = key(0xcb00, 32); + kzt_guest_library_handle_t handle; + size_t active_pending = 99, live_entries = 99; + + CHECK("cancel init", bindings != NULL); + if (!bindings) return; + CHECK("cancel track prepared", kzt_guest_library_track( + bindings, (library_t *)&prepared) == 0); + CHECK("cancel track wrong", kzt_guest_library_track( + bindings, (library_t *)&wrong) == 0); + CHECK("cancel observe", kzt_guest_library_note_observation( + bindings, &cancelled) == KZT_GUEST_LIBRARY_BINDING_UNCHANGED); + prepare_scoped_pair(bindings, &scope, cancelled.link_map_addr, + (library_t *)&prepared); + kzt_guest_library_loader_scope_end(&scope); + CHECK("cancelled pair invisible", kzt_guest_library_lookup( + bindings, &cancelled, &handle) != 0); + CHECK("cancelled pair snapshot", + kzt_guest_library_binding_test_snapshot( + bindings, (library_t *)&prepared, NULL, &active_pending, + &live_entries) == 0); + CHECK("cancelled pair has no global state", + active_pending == 0 && live_entries == 0); + + CHECK("failed observe", kzt_guest_library_note_observation( + bindings, &failed) == KZT_GUEST_LIBRARY_BINDING_UNCHANGED); + prepare_scoped_pair(bindings, &scope, failed.link_map_addr, + (library_t *)&prepared); + CHECK("failed publish rejected", + kzt_guest_library_loader_scope_publish_pair( + &scope, failed.link_map_addr, (library_t *)&wrong, + KZT_GUEST_LIBRARY_OBJECT_WRAPPED) == + KZT_GUEST_LIBRARY_BINDING_ERROR); + kzt_guest_library_loader_scope_end(&scope); + CHECK("failed publish invisible", kzt_guest_library_lookup( + bindings, &failed, &handle) != 0); + CHECK("failed publish snapshot", + kzt_guest_library_binding_test_snapshot( + bindings, (library_t *)&prepared, NULL, &active_pending, + &live_entries) == 0); + CHECK("failed publish has no global state", + active_pending == 0 && live_entries == 0); + kzt_guest_library_bindings_destroy(&bindings); +} + +static int callback_rejected(kzt_guest_library_bindings_t *bindings, + uintptr_t map, + const kzt_guest_library_loader_scope_t *scope) +{ + kzt_guest_library_callback_access_t access = { 0 }; + int result = kzt_guest_library_callback_access_begin_scoped( + bindings, map, scope, &access); + if (result == 0) + kzt_guest_library_callback_access_end(&access); + return result != 0; +} + +static void run_failed_loader_keeps_tombstone(int force_gate_alloc_failure) +{ + fake_library_t old_library = { 51 }, new_library = { 52 }; + kzt_guest_library_bindings_t *bindings = + kzt_guest_library_bindings_init(); + kzt_guest_library_loader_scope_t scope = { 0 }; + kzt_guest_library_callback_access_t access = { 0 }; + uintptr_t map = force_gate_alloc_failure ? 0xc200 : 0xc100; + + CHECK("failed-loader init", bindings != NULL); + if (!bindings) return; + CHECK("failed-loader old track", kzt_guest_library_track( + bindings, (library_t *)&old_library) == 0); + CHECK("failed-loader old pair", kzt_guest_library_publish_loader_pair( + bindings, map, (library_t *)&old_library, + KZT_GUEST_LIBRARY_OBJECT_WRAPPED) == + KZT_GUEST_LIBRARY_BINDING_PENDING); + if (force_gate_alloc_failure) + kzt_guest_library_binding_test_set_alloc_failure_after(0); + kzt_guest_library_inactivate(bindings, NULL, + (library_t *)&old_library, map); + kzt_guest_library_binding_test_set_alloc_failure_after(-1); + CHECK("failed-loader new track", kzt_guest_library_track( + bindings, (library_t *)&new_library) == 0); + CHECK("failed-loader scope", kzt_guest_library_loader_scope_begin( + bindings, &scope) == 0); + CHECK("failed-loader scoped temporary read", + kzt_guest_library_callback_access_begin_scoped( + bindings, map, &scope, &access) == 0); + kzt_guest_library_callback_access_end(&access); + kzt_guest_library_loader_scope_end(&scope); + + CHECK("failed-loader tombstone retained", + callback_rejected(bindings, map, NULL)); + kzt_guest_library_bindings_destroy(&bindings); +} + +static void test_failed_loader_keeps_normal_and_fallback_tombstones(void) +{ + run_failed_loader_keeps_tombstone(0); + run_failed_loader_keeps_tombstone(1); +} + +typedef struct scope_transfer_arg { + kzt_guest_library_bindings_t *bindings; + uintptr_t map; + kzt_guest_library_loader_scope_t scope; + int result; +} scope_transfer_arg_t; + +static void *scope_transfer_thread(void *opaque) +{ + scope_transfer_arg_t *arg = opaque; + kzt_guest_library_callback_access_t access = { 0 }; + arg->result = kzt_guest_library_callback_access_begin_scoped( + arg->bindings, arg->map, &arg->scope, &access); + if (arg->result == 0) + kzt_guest_library_callback_access_end(&access); + return NULL; +} + +static void test_loader_scope_identity_and_nesting(void) +{ + fake_library_t old_library = { 53 }; + kzt_guest_library_bindings_t *bindings = + kzt_guest_library_bindings_init(); + kzt_guest_library_loader_scope_t outer = { 0 }, inner = { 0 }; + kzt_guest_library_loader_scope_t stale_inner = { 0 }, forged = { 0 }; + kzt_guest_library_callback_access_t access = { 0 }; + scope_transfer_arg_t transfer = { 0 }; + pthread_t thread; + uintptr_t map = 0xc300; + + CHECK("scope-id init", bindings != NULL); + if (!bindings) return; + CHECK("scope-id track", kzt_guest_library_track( + bindings, (library_t *)&old_library) == 0); + CHECK("scope-id pair", kzt_guest_library_publish_loader_pair( + bindings, map, (library_t *)&old_library, + KZT_GUEST_LIBRARY_OBJECT_WRAPPED) == + KZT_GUEST_LIBRARY_BINDING_PENDING); + kzt_guest_library_inactivate(bindings, NULL, + (library_t *)&old_library, map); + + CHECK("scope-id outer begin", kzt_guest_library_loader_scope_begin( + bindings, &outer) == 0); + CHECK("scope-id inner begin", kzt_guest_library_loader_scope_begin( + bindings, &inner) == 0); + stale_inner = inner; + CHECK("scope-id inner active", kzt_guest_library_callback_access_begin_scoped( + bindings, map, &inner, &access) == 0); + kzt_guest_library_callback_access_end(&access); + kzt_guest_library_loader_scope_end(&inner); + CHECK("scope-id ended copy rejected", + callback_rejected(bindings, map, &stale_inner)); + CHECK("scope-id outer restored", kzt_guest_library_callback_access_begin_scoped( + bindings, map, &outer, &access) == 0); + kzt_guest_library_callback_access_end(&access); + + forged = outer; + forged.cookie++; + CHECK("scope-id forged rejected", + callback_rejected(bindings, map, &forged)); + + transfer.bindings = bindings; + transfer.map = map; + transfer.scope = outer; + transfer.result = 0; + CHECK("scope-id transfer create", pthread_create( + &thread, NULL, scope_transfer_thread, &transfer) == 0); + CHECK("scope-id transfer join", pthread_join(thread, NULL) == 0); + CHECK("scope-id cross-thread rejected", transfer.result != 0); + kzt_guest_library_loader_scope_end(&outer); + kzt_guest_library_bindings_destroy(&bindings); +} + +typedef struct concurrent_scope_arg { + kzt_guest_library_bindings_t *bindings; + pthread_barrier_t *barrier; + uintptr_t map; + int result; +} concurrent_scope_arg_t; + +static void *concurrent_scope_thread(void *opaque) +{ + concurrent_scope_arg_t *arg = opaque; + kzt_guest_library_loader_scope_t scope = { 0 }; + kzt_guest_library_callback_access_t access = { 0 }; + + arg->result = kzt_guest_library_loader_scope_begin( + arg->bindings, &scope); + pthread_barrier_wait(arg->barrier); + if (arg->result == 0) + arg->result = kzt_guest_library_callback_access_begin_scoped( + arg->bindings, arg->map, &scope, &access); + if (arg->result == 0) + kzt_guest_library_callback_access_end(&access); + kzt_guest_library_loader_scope_end(&scope); + return NULL; +} + +static void test_concurrent_loader_scopes_do_not_invalidate_each_other(void) +{ + fake_library_t old_a = { 59 }, old_b = { 60 }; + kzt_guest_library_bindings_t *bindings = + kzt_guest_library_bindings_init(); + pthread_barrier_t barrier; + pthread_t threads[2]; + concurrent_scope_arg_t args[2] = { + { .bindings = bindings, .map = 0xc700 }, + { .bindings = bindings, .map = 0xc800 }, + }; + + CHECK("concurrent-scope init", bindings != NULL); + if (!bindings) return; + CHECK("concurrent-scope track a", kzt_guest_library_track( + bindings, (library_t *)&old_a) == 0); + CHECK("concurrent-scope track b", kzt_guest_library_track( + bindings, (library_t *)&old_b) == 0); + CHECK("concurrent-scope pair a", kzt_guest_library_publish_loader_pair( + bindings, args[0].map, (library_t *)&old_a, + KZT_GUEST_LIBRARY_OBJECT_WRAPPED) == + KZT_GUEST_LIBRARY_BINDING_PENDING); + CHECK("concurrent-scope pair b", kzt_guest_library_publish_loader_pair( + bindings, args[1].map, (library_t *)&old_b, + KZT_GUEST_LIBRARY_OBJECT_WRAPPED) == + KZT_GUEST_LIBRARY_BINDING_PENDING); + kzt_guest_library_inactivate(bindings, NULL, (library_t *)&old_a, + args[0].map); + kzt_guest_library_inactivate(bindings, NULL, (library_t *)&old_b, + args[1].map); + CHECK("concurrent-scope barrier", pthread_barrier_init( + &barrier, NULL, 2) == 0); + args[0].barrier = &barrier; + args[1].barrier = &barrier; + CHECK("concurrent-scope create a", pthread_create( + &threads[0], NULL, concurrent_scope_thread, &args[0]) == 0); + CHECK("concurrent-scope create b", pthread_create( + &threads[1], NULL, concurrent_scope_thread, &args[1]) == 0); + CHECK("concurrent-scope join a", pthread_join(threads[0], NULL) == 0); + CHECK("concurrent-scope join b", pthread_join(threads[1], NULL) == 0); + CHECK("concurrent-scope a active", args[0].result == 0); + CHECK("concurrent-scope b active", args[1].result == 0); + pthread_barrier_destroy(&barrier); + kzt_guest_library_bindings_destroy(&bindings); +} + +static void test_loader_scope_cannot_reopen_unobserved_address(void) +{ + fake_library_t old_a = { 54 }, old_b = { 55 }, replacement = { 56 }; + kzt_guest_library_bindings_t *bindings = + kzt_guest_library_bindings_init(); + kzt_guest_library_loader_scope_t scope = { 0 }; + kzt_guest_library_callback_access_t access = { 0 }; + uintptr_t map_a = 0xc400, map_b = 0xc500; + + CHECK("scope-object init", bindings != NULL); + if (!bindings) return; + CHECK("scope-object track a", kzt_guest_library_track( + bindings, (library_t *)&old_a) == 0); + CHECK("scope-object track b", kzt_guest_library_track( + bindings, (library_t *)&old_b) == 0); + CHECK("scope-object old a", kzt_guest_library_publish_loader_pair( + bindings, map_a, (library_t *)&old_a, + KZT_GUEST_LIBRARY_OBJECT_WRAPPED) == + KZT_GUEST_LIBRARY_BINDING_PENDING); + CHECK("scope-object old b", kzt_guest_library_publish_loader_pair( + bindings, map_b, (library_t *)&old_b, + KZT_GUEST_LIBRARY_OBJECT_WRAPPED) == + KZT_GUEST_LIBRARY_BINDING_PENDING); + kzt_guest_library_inactivate(bindings, NULL, (library_t *)&old_a, map_a); + kzt_guest_library_inactivate(bindings, NULL, (library_t *)&old_b, map_b); + CHECK("scope-object replacement track", kzt_guest_library_track( + bindings, (library_t *)&replacement) == 0); + CHECK("scope-object begin", kzt_guest_library_loader_scope_begin( + bindings, &scope) == 0); + CHECK("scope-object observe a", kzt_guest_library_callback_access_begin_scoped( + bindings, map_a, &scope, &access) == 0); + kzt_guest_library_callback_access_end(&access); + CHECK("scope-object unrelated publication rejected", + kzt_guest_library_loader_scope_publish_pair( + &scope, map_b, (library_t *)&replacement, + KZT_GUEST_LIBRARY_OBJECT_WRAPPED) == + KZT_GUEST_LIBRARY_BINDING_ERROR); + kzt_guest_library_loader_scope_end(&scope); + CHECK("scope-object b remains closed", + callback_rejected(bindings, map_b, NULL)); + kzt_guest_library_bindings_destroy(&bindings); +} + +typedef struct publish_reader_arg { + kzt_guest_library_bindings_t *bindings; + library_t *library; + uintptr_t map; + int done; +} publish_reader_arg_t; + +static void *publish_reader_unload_thread(void *opaque) +{ + publish_reader_arg_t *arg = opaque; + kzt_guest_library_inactivate(arg->bindings, NULL, arg->library, arg->map); + __atomic_store_n(&arg->done, 1, __ATOMIC_RELEASE); + return NULL; +} + +static void test_publish_while_reader_preserves_unload_wait(void) +{ + fake_library_t old_library = { 57 }, replacement = { 58 }; + kzt_guest_library_bindings_t *bindings = + kzt_guest_library_bindings_init(); + kzt_guest_library_callback_access_t held = { 0 }; + kzt_guest_library_callback_access_t scoped = { 0 }; + kzt_guest_library_loader_scope_t scope = { 0 }; + publish_reader_arg_t arg = { .bindings = bindings, + .library = (library_t *)&old_library, + .map = 0xc600 }; + pthread_t thread; + struct timespec delay = { .tv_nsec = 20 * 1000 * 1000 }; + + CHECK("publish-reader init", bindings != NULL); + if (!bindings) return; + CHECK("publish-reader old track", kzt_guest_library_track( + bindings, (library_t *)&old_library) == 0); + CHECK("publish-reader old pair", kzt_guest_library_publish_loader_pair( + bindings, arg.map, (library_t *)&old_library, + KZT_GUEST_LIBRARY_OBJECT_WRAPPED) == + KZT_GUEST_LIBRARY_BINDING_PENDING); + CHECK("publish-reader held", kzt_guest_library_callback_access_begin( + bindings, arg.map, &held) == 0); + CHECK("publish-reader replacement track", kzt_guest_library_track( + bindings, (library_t *)&replacement) == 0); + CHECK("publish-reader create", pthread_create( + &thread, NULL, publish_reader_unload_thread, &arg) == 0); + for (int i = 0; i < 20 && + !callback_rejected(bindings, arg.map, NULL); ++i) + nanosleep(&delay, NULL); + CHECK("publish-reader closed", callback_rejected( + bindings, arg.map, NULL)); + CHECK("publish-reader scope", kzt_guest_library_loader_scope_begin( + bindings, &scope) == 0); + CHECK("publish-reader scoped observation", + kzt_guest_library_callback_access_begin_scoped( + bindings, arg.map, &scope, &scoped) == 0); + kzt_guest_library_callback_access_end(&scoped); + CHECK("publish-reader pending pair", + kzt_guest_library_loader_scope_note_pair( + &scope, arg.map, (library_t *)&replacement, + KZT_GUEST_LIBRARY_OBJECT_WRAPPED) != + KZT_GUEST_LIBRARY_BINDING_ERROR); + CHECK("publish-reader publication", + kzt_guest_library_loader_scope_publish_pair( + &scope, arg.map, (library_t *)&replacement, + KZT_GUEST_LIBRARY_OBJECT_WRAPPED) != + KZT_GUEST_LIBRARY_BINDING_ERROR); + kzt_guest_library_loader_scope_end(&scope); + kzt_guest_library_callback_access_end(&held); + for (int i = 0; i < 20 && + !__atomic_load_n(&arg.done, __ATOMIC_ACQUIRE); ++i) + nanosleep(&delay, NULL); + CHECK("publish-reader unload completes", + __atomic_load_n(&arg.done, __ATOMIC_ACQUIRE)); + if (__atomic_load_n(&arg.done, __ATOMIC_ACQUIRE)) { + pthread_join(thread, NULL); + CHECK("publish-reader reopened after old readers drain", + kzt_guest_library_callback_access_begin( + bindings, arg.map, &held) == 0); + kzt_guest_library_callback_access_end(&held); + kzt_guest_library_bindings_destroy(&bindings); + } +} + +static void test_registry_retire_failures_are_observable_fail_open(void) +{ + fake_library_t missing = { 43 }; + fake_library_t disabled = { 44 }; + fake_library_t replaced = { 45 }; + kzt_guest_library_bindings_t *bindings = + kzt_guest_library_bindings_init(); + kzt_guest_registry_t *registry; + unsigned long registry_missing = 0; + unsigned long retire_unprovable = 0; + kzt_guest_object_observation_t observed = observation(0xa20000); + kzt_guest_library_binding_key_t missing_key = key(0xa21000, 1); + kzt_guest_library_binding_key_t disabled_key = key(0xa22000, 1); + kzt_guest_library_binding_key_t replaced_key; + unsigned long generation; + + CHECK("fail-open bindings", bindings != NULL); + if (!bindings) return; + + CHECK("fail-open missing track", kzt_guest_library_track( + bindings, (library_t *)&missing) == 0); + CHECK("fail-open missing bind", kzt_guest_library_bind( + bindings, &missing_key, (library_t *)&missing, + KZT_GUEST_LIBRARY_OBJECT_WRAPPED) == + KZT_GUEST_LIBRARY_BINDING_ADDED); + kzt_guest_library_inactivate(bindings, NULL, + (library_t *)&missing, 0xa21000); + + kzt_guest_registry_test_set_alloc_failure_after(1); + registry = kzt_guest_registry_init(); + kzt_guest_registry_test_set_alloc_failure_after(-1); + CHECK("fail-open disabled registry", registry != NULL); + CHECK("fail-open disabled track", kzt_guest_library_track( + bindings, (library_t *)&disabled) == 0); + CHECK("fail-open disabled bind", kzt_guest_library_bind( + bindings, &disabled_key, (library_t *)&disabled, + KZT_GUEST_LIBRARY_OBJECT_WRAPPED) == + KZT_GUEST_LIBRARY_BINDING_ADDED); + kzt_guest_library_inactivate(bindings, registry, + (library_t *)&disabled, 0xa22000); + kzt_guest_registry_destroy(®istry); + + registry = kzt_guest_registry_init(); + CHECK("fail-open replacement registry", registry != NULL); + CHECK("fail-open replacement observe", kzt_guest_registry_observe( + registry, &observed) == KZT_GUEST_REGISTRY_ADDED); + generation = registry_generation(registry, observed.link_map_addr); + replaced_key = key(observed.link_map_addr, generation); + CHECK("fail-open replacement track", kzt_guest_library_track( + bindings, (library_t *)&replaced) == 0); + CHECK("fail-open replacement bind", kzt_guest_library_bind( + bindings, &replaced_key, (library_t *)&replaced, + KZT_GUEST_LIBRARY_OBJECT_WRAPPED) == + KZT_GUEST_LIBRARY_BINDING_ADDED); + CHECK("fail-open replacement retire", kzt_guest_registry_retire( + registry, observed.link_map_addr, generation) == 0); + CHECK("fail-open replacement observe new", kzt_guest_registry_observe( + registry, &observed) == KZT_GUEST_REGISTRY_ADDED); + kzt_guest_library_inactivate(bindings, registry, + (library_t *)&replaced, + observed.link_map_addr); + + CHECK("fail-open diagnostics", + kzt_guest_library_binding_test_get_diagnostics( + bindings, ®istry_missing, &retire_unprovable) == 0); + CHECK("fail-open registry missing observed", + registry_missing >= 1); + CHECK("fail-open registry disabled observed", + retire_unprovable >= 1); + CHECK("fail-open generation replacement observed", + retire_unprovable >= 2); + + kzt_guest_registry_destroy(®istry); + kzt_guest_library_bindings_destroy(&bindings); +} + +typedef struct unbind_thread_arg { + kzt_guest_library_bindings_t *bindings; + library_t *library; + pthread_mutex_t lock; + pthread_cond_t cond; + int started; + int done; +} unbind_thread_arg_t; + +static void *unbind_thread(void *opaque) +{ + unbind_thread_arg_t *arg = opaque; + pthread_mutex_lock(&arg->lock); + arg->started = 1; + pthread_cond_broadcast(&arg->cond); + pthread_mutex_unlock(&arg->lock); + kzt_guest_library_unbind(arg->bindings, NULL, arg->library, 0); + pthread_mutex_lock(&arg->lock); + arg->done = 1; + pthread_cond_broadcast(&arg->cond); + pthread_mutex_unlock(&arg->lock); + return NULL; +} + +static void test_lookup_and_unbind_threads(void) +{ + fake_library_t lib = { 6 }; + kzt_guest_library_bindings_t *bindings = kzt_guest_library_bindings_init(); + kzt_guest_library_binding_key_t k = key(0xa000, 13); + kzt_guest_library_handle_t held, probe; + pthread_t thread; + unbind_thread_arg_t arg = { + .bindings = bindings, .library = (library_t *)&lib, + .lock = PTHREAD_MUTEX_INITIALIZER, .cond = PTHREAD_COND_INITIALIZER, + }; + CHECK("thread track", kzt_guest_library_track( + bindings, (library_t *)&lib) == 0); + CHECK("thread add", kzt_guest_library_bind(bindings, &k, + (library_t *)&lib, KZT_GUEST_LIBRARY_OBJECT_WRAPPED) == + KZT_GUEST_LIBRARY_BINDING_ADDED); + CHECK("thread acquire", kzt_guest_library_lookup(bindings, &k, &held) == 0); + CHECK("thread create", pthread_create(&thread, NULL, unbind_thread, &arg) == 0); + pthread_mutex_lock(&arg.lock); + while (!arg.started) pthread_cond_wait(&arg.cond, &arg.lock); + pthread_mutex_unlock(&arg.lock); + while (kzt_guest_library_lookup(bindings, &k, &probe) == 0) + kzt_guest_library_handle_release(&probe); + pthread_mutex_lock(&arg.lock); + CHECK("unbind waits for handle", !arg.done); + pthread_mutex_unlock(&arg.lock); + kzt_guest_library_handle_release(&held); + pthread_join(thread, NULL); + CHECK("no dead return", kzt_guest_library_lookup(bindings, &k, &probe) != 0); + pthread_cond_destroy(&arg.cond); + pthread_mutex_destroy(&arg.lock); + kzt_guest_library_bindings_destroy(&bindings); +} + +typedef struct lock_order_sync { + pthread_mutex_t lock; + pthread_cond_t cond; + int lease_ready; + int start_lookup; + int retire_waiting; + int lookup_done; + int lookup_result; +} lock_order_sync_t; + +typedef struct lock_order_unload_arg { + kzt_guest_library_bindings_t *bindings; + kzt_guest_registry_t *registry; + library_t *library; + uintptr_t link_map_addr; +} lock_order_unload_arg_t; + +typedef struct lock_order_lookup_arg { + kzt_guest_library_access_t *access; + kzt_guest_registry_t *registry; + kzt_guest_library_binding_key_t key; + kzt_guest_registry_source_lease_t *lease; + lock_order_sync_t *sync; +} lock_order_lookup_arg_t; + +static void lock_order_retire_waiting(void *opaque) +{ + lock_order_sync_t *sync = opaque; + + pthread_mutex_lock(&sync->lock); + sync->retire_waiting = 1; + pthread_cond_broadcast(&sync->cond); + pthread_mutex_unlock(&sync->lock); +} + +static void *lock_order_unload_worker(void *opaque) +{ + lock_order_unload_arg_t *arg = opaque; + + kzt_guest_library_inactivate(arg->bindings, arg->registry, arg->library, + arg->link_map_addr); + return NULL; +} + +static void *lock_order_lookup_worker(void *opaque) +{ + lock_order_lookup_arg_t *arg = opaque; + kzt_guest_library_handle_t handle; + int result = kzt_guest_registry_source_lease_acquire( + arg->registry, arg->key.link_map_addr, arg->key.generation, + arg->key.namespace_id, arg->lease); + + pthread_mutex_lock(&arg->sync->lock); + arg->sync->lease_ready = result == 0; + pthread_cond_broadcast(&arg->sync->cond); + while (result == 0 && !arg->sync->start_lookup) { + pthread_cond_wait(&arg->sync->cond, &arg->sync->lock); + } + pthread_mutex_unlock(&arg->sync->lock); + + if (result == 0) { + /* This is the production provider-lookup lock path while the same + * lazy-completion thread owns the exact source lease. */ + result = kzt_guest_library_access_lookup( + arg->access, &arg->key, &handle); + } + + if (result == 0) { + kzt_guest_library_handle_release(&handle); + } + pthread_mutex_lock(&arg->sync->lock); + arg->sync->lookup_result = result; + arg->sync->lookup_done = 1; + pthread_cond_broadcast(&arg->sync->cond); + pthread_mutex_unlock(&arg->sync->lock); + return NULL; +} + +static int timed_wait_for_flag(pthread_cond_t *cond, pthread_mutex_t *lock, + int *flag) +{ + struct timespec deadline; + + clock_gettime(CLOCK_MONOTONIC, &deadline); + deadline.tv_sec += 30; + while (!*flag) { + int result = pthread_cond_clockwait( + cond, lock, CLOCK_MONOTONIC, &deadline); + if (result != 0) { + fprintf(stderr, "timed wait failed: flag=%d error=%d\n", + *flag, result); + return -1; + } + } + return 0; +} + +typedef struct observation_unload_sync { + pthread_mutex_t lock; + pthread_cond_t cond; + int retire_waiters; + int retire_calls; + struct { + kzt_guest_library_bindings_t *bindings; + kzt_guest_library_binding_key_t key; + library_t *library; + int from_observation; + } retire[4]; + int lifecycle_waiters; + kzt_guest_library_bindings_t *lifecycle_wait_bindings; + library_t *lifecycle_wait_library; +} observation_unload_sync_t; + +typedef struct observation_unload_arg { + kzt_guest_library_bindings_t *bindings; + kzt_guest_registry_t *registry; + library_t *library; + uintptr_t link_map_addr; + observation_unload_sync_t *sync; + int started; + int done; +} observation_unload_arg_t; + +static void observation_retire_waiting(void *opaque) +{ + observation_unload_sync_t *sync = opaque; + + pthread_mutex_lock(&sync->lock); + ++sync->retire_waiters; + pthread_cond_broadcast(&sync->cond); + pthread_mutex_unlock(&sync->lock); +} + +static void observation_before_registry_retire( + kzt_guest_library_bindings_t *bindings, + const kzt_guest_library_binding_key_t *key, + library_t *library, int from_observation, void *opaque) +{ + observation_unload_sync_t *sync = opaque; + + pthread_mutex_lock(&sync->lock); + if (sync->retire_calls < (int)(sizeof(sync->retire) / + sizeof(sync->retire[0]))) { + sync->retire[sync->retire_calls].bindings = bindings; + sync->retire[sync->retire_calls].key = *key; + sync->retire[sync->retire_calls].library = library; + sync->retire[sync->retire_calls].from_observation = + from_observation; + } + ++sync->retire_calls; + pthread_cond_broadcast(&sync->cond); + pthread_mutex_unlock(&sync->lock); +} + +static void observation_before_lifecycle_wait( + kzt_guest_library_bindings_t *bindings, library_t *library, + void *opaque) +{ + observation_unload_sync_t *sync = opaque; + + pthread_mutex_lock(&sync->lock); + ++sync->lifecycle_waiters; + sync->lifecycle_wait_bindings = bindings; + sync->lifecycle_wait_library = library; + pthread_cond_broadcast(&sync->cond); + pthread_mutex_unlock(&sync->lock); +} + +static void *observation_unload_worker(void *opaque) +{ + observation_unload_arg_t *arg = opaque; + + pthread_mutex_lock(&arg->sync->lock); + arg->started = 1; + pthread_cond_broadcast(&arg->sync->cond); + pthread_mutex_unlock(&arg->sync->lock); + kzt_guest_library_inactivate(arg->bindings, arg->registry, arg->library, + arg->link_map_addr); + pthread_mutex_lock(&arg->sync->lock); + arg->done = 1; + pthread_cond_broadcast(&arg->sync->cond); + pthread_mutex_unlock(&arg->sync->lock); + return NULL; +} + +static int timed_wait_for_count(pthread_cond_t *cond, pthread_mutex_t *lock, + int *value, int expected) +{ + struct timespec deadline; + + clock_gettime(CLOCK_MONOTONIC, &deadline); + deadline.tv_sec += 30; + while (*value < expected) { + int result = pthread_cond_clockwait( + cond, lock, CLOCK_MONOTONIC, &deadline); + if (result != 0) { + fprintf(stderr, + "timed wait failed: value=%d expected=%d error=%d\n", + *value, expected, result); + return -1; + } + } + return 0; +} + +static void test_concurrent_observation_unloads_keep_exact_owners(void) +{ + fake_library_t a = { 70 }, b = { 71 }; + uintptr_t map_a = 0xe000, map_b = 0xe100; + kzt_guest_object_observation_t observed_a = observation(map_a); + kzt_guest_object_observation_t observed_b = observation(map_b); + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_library_bindings_t *bindings = kzt_guest_library_bindings_init(); + kzt_guest_registry_source_lease_t lease_a = { 0 }, lease_b = { 0 }; + kzt_guest_library_binding_key_t key_a, key_b; + observation_unload_sync_t sync = { + .lock = PTHREAD_MUTEX_INITIALIZER, + .cond = PTHREAD_COND_INITIALIZER, + }; + observation_unload_arg_t arg_a = { + .bindings = bindings, .registry = registry, + .library = (library_t *)&a, .link_map_addr = map_a, .sync = &sync, + }; + observation_unload_arg_t arg_b = { + .bindings = bindings, .registry = registry, + .library = (library_t *)&b, .link_map_addr = map_b, .sync = &sync, + }; + pthread_t thread_a, thread_b; + + CHECK("owner-race track a", kzt_guest_library_track( + bindings, (library_t *)&a) == 0); + CHECK("owner-race track b", kzt_guest_library_track( + bindings, (library_t *)&b) == 0); + CHECK("owner-race registry a", kzt_guest_registry_observe( + registry, &observed_a) == KZT_GUEST_REGISTRY_ADDED); + CHECK("owner-race registry b", kzt_guest_registry_observe( + registry, &observed_b) == KZT_GUEST_REGISTRY_ADDED); + key_a = key(map_a, registry_generation(registry, map_a)); + key_b = key(map_b, registry_generation(registry, map_b)); + CHECK("owner-race observation a", kzt_guest_library_note_observation( + bindings, &key_a) == KZT_GUEST_LIBRARY_BINDING_UNCHANGED); + CHECK("owner-race observation b", kzt_guest_library_note_observation( + bindings, &key_b) == KZT_GUEST_LIBRARY_BINDING_UNCHANGED); + CHECK("owner-race lease a", kzt_guest_registry_source_lease_acquire( + registry, map_a, key_a.generation, 0, &lease_a) == 0); + CHECK("owner-race lease b", kzt_guest_registry_source_lease_acquire( + registry, map_b, key_b.generation, 0, &lease_b) == 0); + + kzt_guest_registry_test_set_before_retire_wait( + observation_retire_waiting, &sync); + kzt_guest_library_binding_test_set_before_registry_retire( + observation_before_registry_retire, &sync); + CHECK("owner-race thread a", pthread_create( + &thread_a, NULL, observation_unload_worker, &arg_a) == 0); + CHECK("owner-race thread b", pthread_create( + &thread_b, NULL, observation_unload_worker, &arg_b) == 0); + pthread_mutex_lock(&sync.lock); + CHECK("owner-race both blocked", timed_wait_for_count( + &sync.cond, &sync.lock, &sync.retire_waiters, 2) == 0); + CHECK("owner-race retire identities", sync.retire_calls == 2 && + sync.retire[0].bindings == bindings && + sync.retire[1].bindings == bindings && + sync.retire[0].from_observation && + sync.retire[1].from_observation && + ((sync.retire[0].library == (library_t *)&a && + sync.retire[0].key.link_map_addr == map_a && + sync.retire[1].library == (library_t *)&b && + sync.retire[1].key.link_map_addr == map_b) || + (sync.retire[0].library == (library_t *)&b && + sync.retire[0].key.link_map_addr == map_b && + sync.retire[1].library == (library_t *)&a && + sync.retire[1].key.link_map_addr == map_a))); + pthread_mutex_unlock(&sync.lock); + + kzt_guest_registry_source_lease_release(&lease_a); + pthread_mutex_lock(&sync.lock); + CHECK("owner-race a completes", timed_wait_for_flag( + &sync.cond, &sync.lock, &arg_a.done) == 0); + CHECK("owner-race b remains blocked", !arg_b.done); + pthread_mutex_unlock(&sync.lock); + + kzt_guest_registry_source_lease_release(&lease_b); + pthread_mutex_lock(&sync.lock); + CHECK("owner-race b completes", timed_wait_for_flag( + &sync.cond, &sync.lock, &arg_b.done) == 0); + pthread_mutex_unlock(&sync.lock); + CHECK("owner-race join a", pthread_join(thread_a, NULL) == 0); + CHECK("owner-race join b", pthread_join(thread_b, NULL) == 0); + + kzt_guest_registry_test_set_before_retire_wait(NULL, NULL); + kzt_guest_library_binding_test_set_before_registry_retire(NULL, NULL); + pthread_cond_destroy(&sync.cond); + pthread_mutex_destroy(&sync.lock); + kzt_guest_library_bindings_destroy(&bindings); + kzt_guest_registry_destroy(®istry); +} + +static void test_duplicate_unload_waits_for_lifecycle_owner(void) +{ + fake_library_t lib = { 72 }; + uintptr_t map = 0xe200, unowned_map = 0xe300; + kzt_guest_object_observation_t observed = observation(map); + kzt_guest_object_observation_t unowned = observation(unowned_map); + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_library_bindings_t *bindings = kzt_guest_library_bindings_init(); + kzt_guest_registry_source_lease_t lease = { 0 }; + kzt_guest_object_snapshot_t *snapshot = NULL; + kzt_guest_library_binding_key_t observed_key, unowned_key; + observation_unload_sync_t sync = { + .lock = PTHREAD_MUTEX_INITIALIZER, + .cond = PTHREAD_COND_INITIALIZER, + }; + observation_unload_arg_t first = { + .bindings = bindings, .registry = registry, + .library = (library_t *)&lib, .link_map_addr = map, .sync = &sync, + }; + observation_unload_arg_t second = { + .bindings = bindings, .registry = registry, + .library = (library_t *)&lib, .link_map_addr = unowned_map, + .sync = &sync, + }; + pthread_t first_thread, second_thread; + + CHECK("duplicate-owner track", kzt_guest_library_track( + bindings, (library_t *)&lib) == 0); + CHECK("duplicate-owner registry", kzt_guest_registry_observe( + registry, &observed) == KZT_GUEST_REGISTRY_ADDED); + CHECK("duplicate-owner unowned registry", kzt_guest_registry_observe( + registry, &unowned) == KZT_GUEST_REGISTRY_ADDED); + observed_key = key(map, registry_generation(registry, map)); + unowned_key = key(unowned_map, + registry_generation(registry, unowned_map)); + CHECK("duplicate-owner observation", kzt_guest_library_note_observation( + bindings, &observed_key) == KZT_GUEST_LIBRARY_BINDING_UNCHANGED); + CHECK("duplicate-owner unowned observation", + kzt_guest_library_note_observation(bindings, &unowned_key) == + KZT_GUEST_LIBRARY_BINDING_UNCHANGED); + CHECK("duplicate-owner lease", kzt_guest_registry_source_lease_acquire( + registry, map, observed_key.generation, 0, &lease) == 0); + + kzt_guest_registry_test_set_before_retire_wait( + observation_retire_waiting, &sync); + kzt_guest_library_binding_test_set_before_registry_retire( + observation_before_registry_retire, &sync); + kzt_guest_library_binding_test_set_before_lifecycle_wait( + observation_before_lifecycle_wait, &sync); + CHECK("duplicate-owner first thread", pthread_create( + &first_thread, NULL, observation_unload_worker, &first) == 0); + pthread_mutex_lock(&sync.lock); + CHECK("duplicate-owner first blocked", timed_wait_for_count( + &sync.cond, &sync.lock, &sync.retire_waiters, 1) == 0); + pthread_mutex_unlock(&sync.lock); + CHECK("duplicate-owner second thread", pthread_create( + &second_thread, NULL, observation_unload_worker, &second) == 0); + pthread_mutex_lock(&sync.lock); + CHECK("duplicate-owner second entered exact wait", timed_wait_for_count( + &sync.cond, &sync.lock, &sync.lifecycle_waiters, 1) == 0); + CHECK("duplicate-owner wait identity", + sync.lifecycle_wait_bindings == bindings && + sync.lifecycle_wait_library == (library_t *)&lib); + CHECK("duplicate-owner second waits", second.started && !second.done); + CHECK("duplicate-owner only first hint retired", + sync.retire_calls == 1 && + sync.retire[0].bindings == bindings && + sync.retire[0].library == (library_t *)&lib && + sync.retire[0].from_observation && + sync.retire[0].key.link_map_addr == map && + sync.retire[0].key.generation == observed_key.generation); + pthread_mutex_unlock(&sync.lock); + + kzt_guest_registry_source_lease_release(&lease); + pthread_mutex_lock(&sync.lock); + CHECK("duplicate-owner first completes", timed_wait_for_flag( + &sync.cond, &sync.lock, &first.done) == 0); + CHECK("duplicate-owner second completes", timed_wait_for_flag( + &sync.cond, &sync.lock, &second.done) == 0); + pthread_mutex_unlock(&sync.lock); + CHECK("duplicate-owner first join", pthread_join(first_thread, NULL) == 0); + CHECK("duplicate-owner second join", pthread_join(second_thread, NULL) == 0); + CHECK("duplicate-owner second hint not claimed", + kzt_guest_registry_find_by_link_map( + registry, unowned_map, &snapshot) == 0 && snapshot && + snapshot->generation == unowned_key.generation && + snapshot->state != KZT_GUEST_OBJECT_UNLOADING && + snapshot->state != KZT_GUEST_OBJECT_DEAD); + kzt_guest_object_snapshot_free(snapshot); + CHECK("duplicate-owner unowned cleanup", kzt_guest_registry_retire( + registry, unowned_map, unowned_key.generation) == 0); + + kzt_guest_registry_test_set_before_retire_wait(NULL, NULL); + kzt_guest_library_binding_test_set_before_lifecycle_wait(NULL, NULL); + kzt_guest_library_binding_test_set_before_registry_retire(NULL, NULL); + pthread_cond_destroy(&sync.cond); + pthread_mutex_destroy(&sync.lock); + kzt_guest_library_bindings_destroy(&bindings); + kzt_guest_registry_destroy(®istry); +} + +static void test_source_lease_unload_allows_provider_lookup(void) +{ + fake_library_t lib = { 60 }; + fake_library_t growth[24]; + uintptr_t map = 0xd000; + kzt_guest_object_observation_t observed = observation(map); + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_library_access_t access; + kzt_guest_registry_source_lease_t lease = { 0 }; + kzt_guest_library_binding_state_t lifecycle_state; + kzt_guest_library_binding_key_t binding_key; + lock_order_sync_t sync = { + .lock = PTHREAD_MUTEX_INITIALIZER, + .cond = PTHREAD_COND_INITIALIZER, + }; + lock_order_unload_arg_t unload_arg; + lock_order_lookup_arg_t lookup_arg; + pthread_t unload_thread; + pthread_t lookup_thread; + size_t live_entries = 1; + + CHECK("lock-order.registry", registry != NULL); + CHECK("lock-order.access", kzt_guest_library_access_init(&access) == 0); + CHECK("lock-order.track", kzt_guest_library_track( + access.bindings, (library_t *)&lib) == 0); + CHECK("lock-order.observe", kzt_guest_registry_observe( + registry, &observed) == KZT_GUEST_REGISTRY_ADDED); + binding_key = key(map, registry_generation(registry, map)); + CHECK("lock-order.bind", kzt_guest_library_bind( + access.bindings, &binding_key, (library_t *)&lib, + KZT_GUEST_LIBRARY_OBJECT_WRAPPED) == + KZT_GUEST_LIBRARY_BINDING_ADDED); + + lookup_arg = (lock_order_lookup_arg_t) { + .access = &access, + .registry = registry, + .key = binding_key, + .lease = &lease, + .sync = &sync, + }; + CHECK("lock-order.lookup-thread", pthread_create( + &lookup_thread, NULL, lock_order_lookup_worker, &lookup_arg) == 0); + pthread_mutex_lock(&sync.lock); + CHECK("lock-order.lease-barrier", timed_wait_for_flag( + &sync.cond, &sync.lock, &sync.lease_ready) == 0); + pthread_mutex_unlock(&sync.lock); + + unload_arg = (lock_order_unload_arg_t) { + .bindings = access.bindings, + .registry = registry, + .library = (library_t *)&lib, + .link_map_addr = map, + }; + kzt_guest_registry_test_set_before_retire_wait( + lock_order_retire_waiting, &sync); + CHECK("lock-order.unload-thread", pthread_create( + &unload_thread, NULL, lock_order_unload_worker, &unload_arg) == 0); + pthread_mutex_lock(&sync.lock); + CHECK("lock-order.retire-wait-barrier", timed_wait_for_flag( + &sync.cond, &sync.lock, &sync.retire_waiting) == 0); + pthread_mutex_unlock(&sync.lock); + + /* Force lifecycle realloc while unload owns no binding-side pointer. */ + for (size_t i = 0; i < 24; ++i) { + growth[i].id = 100 + (int)i; + CHECK("lock-order.concurrent-lifecycle-growth", + kzt_guest_library_track( + access.bindings, (library_t *)&growth[i]) == 0); + } + + CHECK("lock-order.entry-closed", kzt_guest_library_binding_test_snapshot( + access.bindings, (library_t *)&lib, &lifecycle_state, NULL, + &live_entries) == 0 && + lifecycle_state == KZT_GUEST_LIBRARY_BINDING_UNLOADING && + live_entries == 0); + + pthread_mutex_lock(&sync.lock); + sync.start_lookup = 1; + pthread_cond_broadcast(&sync.cond); + CHECK("lock-order.lookup-not-blocked", timed_wait_for_flag( + &sync.cond, &sync.lock, &sync.lookup_done) == 0); + CHECK("lock-order.lookup-fast-fail", sync.lookup_done && + sync.lookup_result != 0); + pthread_mutex_unlock(&sync.lock); + + /* Also releases the old implementation if it deadlocks in lookup, so the + * test reports a bounded failure instead of hanging the suite. */ + kzt_guest_registry_source_lease_release(&lease); + CHECK("lock-order.lookup-join", pthread_join(lookup_thread, NULL) == 0); + CHECK("lock-order.unload-join", pthread_join(unload_thread, NULL) == 0); + CHECK("lock-order.final-dead", kzt_guest_library_binding_test_snapshot( + access.bindings, (library_t *)&lib, &lifecycle_state, NULL, + &live_entries) == 0 && + lifecycle_state == KZT_GUEST_LIBRARY_BINDING_DEAD && + live_entries == 0); + + kzt_guest_registry_test_set_before_retire_wait(NULL, NULL); + pthread_cond_destroy(&sync.cond); + pthread_mutex_destroy(&sync.lock); + kzt_guest_library_access_destroy(&access); + kzt_guest_registry_destroy(®istry); +} + +typedef struct access_teardown_arg { + kzt_guest_library_access_t *access; + pthread_mutex_t lock; + pthread_cond_t cond; + int started; + int done; +} access_teardown_arg_t; + +static void *access_teardown_thread(void *opaque) +{ + access_teardown_arg_t *arg = opaque; + pthread_mutex_lock(&arg->lock); + arg->started = 1; + pthread_cond_broadcast(&arg->cond); + pthread_mutex_unlock(&arg->lock); + kzt_guest_library_access_begin_teardown(arg->access); + pthread_mutex_lock(&arg->lock); + arg->done = 1; + pthread_cond_broadcast(&arg->cond); + pthread_mutex_unlock(&arg->lock); + return NULL; +} + +static void test_context_access_closes_before_destroy(void) +{ + fake_library_t lib = { 50 }; + kzt_guest_library_access_t access; + kzt_guest_library_binding_key_t k = key(0xc000, 50); + kzt_guest_library_handle_t held, probe; + pthread_t thread; + access_teardown_arg_t arg = { + .access = &access, + .lock = PTHREAD_MUTEX_INITIALIZER, + .cond = PTHREAD_COND_INITIALIZER, + }; + + CHECK("access init", kzt_guest_library_access_init(&access) == 0); + CHECK("access track", kzt_guest_library_track( + access.bindings, (library_t *)&lib) == 0); + CHECK("access bind", kzt_guest_library_bind( + access.bindings, &k, (library_t *)&lib, + KZT_GUEST_LIBRARY_OBJECT_WRAPPED) == + KZT_GUEST_LIBRARY_BINDING_ADDED); + CHECK("access acquire", kzt_guest_library_access_lookup( + &access, &k, &held) == 0); + CHECK("access teardown thread", pthread_create( + &thread, NULL, access_teardown_thread, &arg) == 0); + pthread_mutex_lock(&arg.lock); + while (!arg.started) pthread_cond_wait(&arg.cond, &arg.lock); + pthread_mutex_unlock(&arg.lock); + while (kzt_guest_library_lookup(access.bindings, &k, &probe) == 0) + kzt_guest_library_handle_release(&probe); + pthread_mutex_lock(&arg.lock); + CHECK("context teardown waits for held handle", !arg.done); + pthread_mutex_unlock(&arg.lock); + kzt_guest_library_handle_release(&held); + pthread_join(thread, NULL); + CHECK("closed context rejects lookup", kzt_guest_library_access_lookup( + &access, &k, &probe) != 0); + pthread_cond_destroy(&arg.cond); + pthread_mutex_destroy(&arg.lock); + kzt_guest_library_access_destroy(&access); +} + +int main(void) +{ + test_init_failure_is_fail_open(); + test_both_arrival_orders_and_retry(); + test_forced_growth_with_held_handle(); + test_pending_cancel_and_address_reuse(); + test_unclaimed_observation_address_reuse_pair_first(); + test_observation_first_unload_retires_registry_generation(); + test_unload_retires_only_hinted_unclaimed_observation(); + test_missing_unload_hint_preserves_unclaimed_observation(); + test_dead_library_stale_hint_does_not_retire_reused_address(); + test_tracking_allocation_failure_unload_is_fail_open(); + test_inactive_can_reload_but_destroyed_cannot(); + test_different_library_reuses_closed_callback_address(); + test_loader_pair_is_invisible_until_publish(); + test_loader_pair_cancel_and_failed_publish_are_invisible(); + test_failed_loader_keeps_normal_and_fallback_tombstones(); + test_loader_scope_identity_and_nesting(); + test_concurrent_loader_scopes_do_not_invalidate_each_other(); + test_loader_scope_cannot_reopen_unobserved_address(); + test_registry_retire_failures_are_observable_fail_open(); + test_lookup_and_unbind_threads(); + test_concurrent_observation_unloads_keep_exact_owners(); + test_duplicate_unload_waits_for_lifecycle_owner(); + test_source_lease_unload_allows_provider_lookup(); + test_context_access_closes_before_destroy(); + /* Keep last: a broken reader/publication protocol intentionally leaves + * its unload thread blocked so the red run remains bounded. */ + test_publish_while_reader_preserves_unload_wait(); + if (failures) fprintf(stderr, "%d failure(s)\n", failures); + return failures ? EXIT_FAILURE : EXIT_SUCCESS; +} diff --git a/tests/unit/kzt/test_guest_library_binding_teardown.c b/tests/unit/kzt/test_guest_library_binding_teardown.c new file mode 100644 index 00000000000..6232abac048 --- /dev/null +++ b/tests/unit/kzt/test_guest_library_binding_teardown.c @@ -0,0 +1,51 @@ +#include +#include +#include +#include +#include + +#include "kzt_guest_library_binding.h" + +typedef struct teardown_fixture { + kzt_guest_library_bindings_t *bindings; + int stop; + unsigned long admitted; +} teardown_fixture_t; + +static void *callback_worker(void *opaque) +{ + teardown_fixture_t *fixture = opaque; + + while (!__atomic_load_n(&fixture->stop, __ATOMIC_ACQUIRE)) { + kzt_guest_library_callback_access_t access = { 0 }; + if (kzt_guest_library_callback_access_begin( + fixture->bindings, 0xb10000, &access) == 0) { + __atomic_add_fetch(&fixture->admitted, 1, __ATOMIC_RELAXED); + kzt_guest_library_callback_access_end(&access); + } + } + return NULL; +} + +int main(void) +{ + teardown_fixture_t fixture = { + .bindings = kzt_guest_library_bindings_init(), + }; + pthread_t workers[4]; + + if (!fixture.bindings) return EXIT_FAILURE; + for (size_t i = 0; i < sizeof(workers) / sizeof(workers[0]); ++i) + if (pthread_create(&workers[i], NULL, callback_worker, &fixture) != 0) + return EXIT_FAILURE; + + while (__atomic_load_n(&fixture.admitted, __ATOMIC_ACQUIRE) < 1000) + sched_yield(); + kzt_guest_library_bindings_begin_teardown(fixture.bindings); + __atomic_store_n(&fixture.stop, 1, __ATOMIC_RELEASE); + for (size_t i = 0; i < sizeof(workers) / sizeof(workers[0]); ++i) + pthread_join(workers[i], NULL); + kzt_guest_library_bindings_destroy(&fixture.bindings); + puts("kzt-guest-library-binding-teardown: PASS"); + return EXIT_SUCCESS; +} diff --git a/tests/unit/kzt/test_guest_link_map_reader.c b/tests/unit/kzt/test_guest_link_map_reader.c new file mode 100644 index 00000000000..da4fd5f716f --- /dev/null +++ b/tests/unit/kzt/test_guest_link_map_reader.c @@ -0,0 +1,591 @@ +#include +#include +#include + +#include "target/i386/latx/include/kzt_guest_link_map_reader.h" +#include "target/i386/latx/include/kzt_guest_registry.h" + +typedef struct test_guest_link_map { + uint64_t l_addr; + uint64_t l_name; + uint64_t l_ld; + uint64_t l_next; + uint64_t l_prev; + uint64_t private_l_ns; + uint64_t private_l_map_start; + uint64_t private_l_map_end; +} test_guest_link_map_t; + +typedef struct fake_read_failure { + uintptr_t addr; + size_t size; +} fake_read_failure_t; + +typedef struct fake_reader_memory { + uintptr_t base; + size_t size; + const fake_read_failure_t *failures; + size_t failure_count; +} fake_reader_memory_t; + +static int failures; + +static void check_true(const char *name, int condition) +{ + if (condition) { + return; + } + + fprintf(stderr, "%s: condition failed\n", name); + ++failures; +} + +static void check_int(const char *name, int got, int expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got %d expected %d\n", name, got, expected); + ++failures; +} + +static void check_uintptr(const char *name, uintptr_t got, uintptr_t expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got 0x%lx expected 0x%lx\n", name, + (unsigned long)got, (unsigned long)expected); + ++failures; +} + +static void check_string(const char *name, const char *got, + const char *expected) +{ + if ((!got && !expected) || (got && expected && !strcmp(got, expected))) { + return; + } + + fprintf(stderr, "%s: got \"%s\" expected \"%s\"\n", name, + got ? got : "(null)", expected ? expected : "(null)"); + ++failures; +} + +static int ranges_overlap(uintptr_t left_addr, size_t left_size, + uintptr_t right_addr, size_t right_size) +{ + uintptr_t left_end = left_addr + left_size; + uintptr_t right_end = right_addr + right_size; + + return left_addr < right_end && right_addr < left_end; +} + +static int fake_read_memory(uintptr_t guest_addr, void *dst, size_t size, + void *opaque) +{ + fake_reader_memory_t *memory = opaque; + size_t i; + + for (i = 0; i < memory->failure_count; ++i) { + if (ranges_overlap(guest_addr, size, + memory->failures[i].addr, + memory->failures[i].size)) { + return -1; + } + } + + if (guest_addr < memory->base || + size > memory->size || + guest_addr - memory->base > memory->size - size) { + return -1; + } + + memcpy(dst, (const void *)guest_addr, size); + return 0; +} + +static kzt_guest_link_map_reader_ops_t fake_ops(fake_reader_memory_t *memory) +{ + kzt_guest_link_map_reader_ops_t ops = { + .read_memory = fake_read_memory, + .opaque = memory, + }; + + return ops; +} + +static fake_reader_memory_t fake_memory_for(void *base, size_t size, + const fake_read_failure_t *failures, + size_t failure_count) +{ + fake_reader_memory_t memory = { + .base = (uintptr_t)base, + .size = size, + .failures = failures, + .failure_count = failure_count, + }; + + return memory; +} + +static void init_link_map(test_guest_link_map_t *link_map, char *name) +{ + memset(link_map, 0, sizeof(*link_map)); + link_map->l_addr = 0x100000; + link_map->l_name = (uintptr_t)name; + link_map->l_ld = 0x101000; + /* Poison private glibc fields: the reader must never trust them. */ + link_map->private_l_ns = 7; + link_map->private_l_map_start = 0x100000; + link_map->private_l_map_end = 0x120000; +} + +static void test_valid_link_map_reads_complete_observation(void) +{ + struct { + test_guest_link_map_t link_map; + char guest_name[32]; + } guest = { 0 }; + kzt_guest_object_observation_t observation = { 0 }; + fake_reader_memory_t memory; + kzt_guest_link_map_reader_ops_t ops; + + strcpy(guest.guest_name, "/guest/libfoo.so"); + init_link_map(&guest.link_map, guest.guest_name); + memory = fake_memory_for(&guest, sizeof(guest), NULL, 0); + ops = fake_ops(&memory); + + check_int("read_observation.valid", + kzt_guest_link_map_read_observation((uintptr_t)&guest.link_map, + &ops, + &observation), + 0); + check_uintptr("observation.link_map_addr", + observation.link_map_addr, + (uintptr_t)&guest.link_map); + check_uintptr("observation.load_bias", + observation.load_bias.value, + 0x100000); + check_int("observation.load_bias.status", + observation.load_bias.status, + KZT_GUEST_FIELD_OK); + check_uintptr("observation.dynamic_addr", + observation.dynamic_addr.value, + 0x101000); + check_int("observation.dynamic_addr.status", + observation.dynamic_addr.status, + KZT_GUEST_FIELD_OK); + check_int("observation.map_start.status", + observation.map_start.status, + KZT_GUEST_FIELD_UNKNOWN); + check_int("observation.map_end.status", + observation.map_end.status, + KZT_GUEST_FIELD_UNKNOWN); + check_int("observation.namespace_id.status", + observation.namespace_id.status, + KZT_GUEST_FIELD_UNKNOWN); + check_string("observation.path", observation.path.value, + "/guest/libfoo.so"); + check_int("observation.path.status", + observation.path.status, + KZT_GUEST_FIELD_OK); + check_int("observation.soname.status", + observation.soname.status, + KZT_GUEST_FIELD_NOT_PARSED); + check_int("observation.dynamic_view_status", + observation.dynamic_view_status, + KZT_GUEST_FIELD_NOT_PARSED); + + kzt_guest_link_map_observation_clear(&observation); +} + +static void test_invalid_link_map_is_identity_failure(void) +{ + char unrelated[16] = { 0 }; + kzt_guest_object_observation_t observation = { 0 }; + fake_reader_memory_t memory = fake_memory_for(unrelated, + sizeof(unrelated), + NULL, + 0); + kzt_guest_link_map_reader_ops_t ops = fake_ops(&memory); + + check_int("read_observation.null-link-map", + kzt_guest_link_map_read_observation(0, &ops, &observation), + -1); + check_uintptr("null-link-map.identity", observation.link_map_addr, 0); + + check_int("read_observation.out-of-range-link-map", + kzt_guest_link_map_read_observation((uintptr_t)unrelated - 8, + &ops, + &observation), + 0); + check_uintptr("out-of-range.identity", observation.link_map_addr, + (uintptr_t)unrelated - 8); + check_int("out-of-range.load-bias", observation.load_bias.status, + KZT_GUEST_FIELD_READ_ERROR); + kzt_guest_link_map_observation_clear(&observation); +} + +static void test_field_read_failure_forms_partial_observation(void) +{ + struct { + test_guest_link_map_t link_map; + char guest_name[32]; + } guest = { 0 }; + fake_read_failure_t read_failures[] = { + { + .addr = (uintptr_t)&guest.link_map + + offsetof(test_guest_link_map_t, l_ld), + .size = sizeof(guest.link_map.l_ld), + }, + }; + fake_reader_memory_t memory; + kzt_guest_link_map_reader_ops_t ops; + kzt_guest_object_observation_t observation = { 0 }; + + strcpy(guest.guest_name, "/guest/libpartial.so"); + init_link_map(&guest.link_map, guest.guest_name); + memory = fake_memory_for(&guest, + sizeof(guest), + read_failures, + sizeof(read_failures) / sizeof(read_failures[0])); + ops = fake_ops(&memory); + + check_int("read_observation.partial", + kzt_guest_link_map_read_observation((uintptr_t)&guest.link_map, + &ops, + &observation), + 0); + check_uintptr("partial.identity", + observation.link_map_addr, + (uintptr_t)&guest.link_map); + check_int("partial.load-bias-ok", + observation.load_bias.status, + KZT_GUEST_FIELD_OK); + check_int("partial.dynamic-read-error", + observation.dynamic_addr.status, + KZT_GUEST_FIELD_READ_ERROR); + check_int("partial.map-end-unknown", + observation.map_end.status, + KZT_GUEST_FIELD_UNKNOWN); + check_string("partial.path", observation.path.value, + "/guest/libpartial.so"); + + kzt_guest_link_map_observation_clear(&observation); +} + +static void test_name_snapshot_status_matrix(void) +{ + char storage[128]; + fake_read_failure_t read_failures[] = { + { + .addr = (uintptr_t)(storage + 98), + .size = 1, + }, + }; + fake_reader_memory_t memory; + kzt_guest_link_map_reader_ops_t ops; + kzt_guest_string_field_t name = { 0 }; + + memset(storage, 0, sizeof(storage)); + memcpy(storage, "/guest/libok.so", sizeof("/guest/libok.so")); + storage[32] = 0; + memcpy(storage + 64, "abcd", 4); + memcpy(storage + 96, "broken", sizeof("broken")); + + memory = fake_memory_for(storage, + sizeof(storage), + read_failures, + sizeof(read_failures) / sizeof(read_failures[0])); + ops = fake_ops(&memory); + + check_int("name.valid", + kzt_guest_link_map_read_name_snapshot((uintptr_t)storage, + &ops, + 32, + &name), + 0); + check_int("name.valid.status", name.status, KZT_GUEST_FIELD_OK); + check_string("name.valid.value", name.value, "/guest/libok.so"); + kzt_guest_link_map_string_clear(&name); + + check_int("name.empty", + kzt_guest_link_map_read_name_snapshot((uintptr_t)(storage + 32), + &ops, + 32, + &name), + 0); + check_int("name.empty.status", name.status, KZT_GUEST_FIELD_OK); + check_string("name.empty.value", name.value, ""); + kzt_guest_link_map_string_clear(&name); + + check_int("name.truncated", + kzt_guest_link_map_read_name_snapshot((uintptr_t)(storage + 64), + &ops, + 4, + &name), + 0); + check_int("name.truncated.status", name.status, + KZT_GUEST_FIELD_TRUNCATED); + check_string("name.truncated.value", name.value, "abcd"); + kzt_guest_link_map_string_clear(&name); + + check_int("name.read-error", + kzt_guest_link_map_read_name_snapshot((uintptr_t)(storage + 96), + &ops, + 32, + &name), + 0); + check_int("name.read-error.status", name.status, + KZT_GUEST_FIELD_READ_ERROR); + check_true("name.read-error.value", name.value == NULL); + kzt_guest_link_map_string_clear(&name); + + check_int("name.unknown-null", + kzt_guest_link_map_read_name_snapshot(0, &ops, 32, &name), + 0); + check_int("name.unknown-null.status", name.status, + KZT_GUEST_FIELD_UNKNOWN); + check_true("name.unknown-null.value", name.value == NULL); + kzt_guest_link_map_string_clear(&name); +} + +static void test_allocation_failure_is_read_error_not_borrowed_pointer(void) +{ + char guest_name[] = "/guest/liballoc.so"; + fake_reader_memory_t memory = fake_memory_for(guest_name, + sizeof(guest_name), + NULL, + 0); + kzt_guest_link_map_reader_ops_t ops = fake_ops(&memory); + kzt_guest_string_field_t name = { 0 }; + + kzt_guest_link_map_reader_test_set_alloc_failure_after(0); + check_int("name.alloc-failure", + kzt_guest_link_map_read_name_snapshot((uintptr_t)guest_name, + &ops, + 64, + &name), + 0); + kzt_guest_link_map_reader_test_set_alloc_failure_after(-1); + + check_int("name.alloc-failure.status", name.status, + KZT_GUEST_FIELD_READ_ERROR); + check_true("name.alloc-failure.value", name.value == NULL); +} + +static void test_main_namespace_walk_matches_verified_main_identity(void) +{ + test_guest_link_map_t maps[3] = { 0 }; + fake_reader_memory_t memory = fake_memory_for(maps, sizeof(maps), NULL, 0); + kzt_guest_link_map_reader_ops_t ops = fake_ops(&memory); + kzt_guest_link_map_identity_t main_identity = { + .load_bias = 0x400000, + .dynamic_addr = 0x401000, + }; + uintptr_t namespace_head = 0; + + maps[0].l_addr = 0x400000; + maps[0].l_ld = 0x401000; + maps[1].l_addr = 0x700000; + maps[1].l_ld = 0x701000; + maps[1].l_prev = (uintptr_t)&maps[0]; + maps[2].l_addr = 0x900000; + maps[2].l_ld = 0x901000; + maps[2].l_prev = (uintptr_t)&maps[1]; + + check_int("namespace.main-current", + kzt_guest_link_map_classify_namespace( + (uintptr_t)&maps[0], &main_identity, 0, &ops, + &namespace_head), + 1); + check_uintptr("namespace.main-head", namespace_head, + (uintptr_t)&maps[0]); + check_int("namespace.main-via-prev", + kzt_guest_link_map_classify_namespace( + (uintptr_t)&maps[2], &main_identity, 0, &ops, + &namespace_head), + 1); + maps[0].l_ld = 0x402000; + check_int("namespace.same-bias-wrong-dynamic", + kzt_guest_link_map_classify_namespace( + (uintptr_t)&maps[2], &main_identity, 0, &ops, + &namespace_head), + 0); +} + +static void test_cached_main_head_uses_identity_not_load_bias(void) +{ + test_guest_link_map_t maps[4] = { 0 }; + test_guest_link_map_t *main_maps = &maps[0]; + test_guest_link_map_t *other_maps = &maps[2]; + fake_reader_memory_t memory = fake_memory_for(maps, sizeof(maps), NULL, 0); + kzt_guest_link_map_reader_ops_t ops = fake_ops(&memory); + kzt_guest_link_map_identity_t main_identity = { + .load_bias = 0x400000, + .dynamic_addr = 0x401000, + }; + uintptr_t main_head = (uintptr_t)&main_maps[0]; + uintptr_t namespace_head = 0; + + /* Both namespace heads deliberately use the same load bias. */ + main_maps[0].l_addr = main_identity.load_bias; + main_maps[0].l_ld = main_identity.dynamic_addr; + main_maps[1].l_prev = main_head; + other_maps[0].l_addr = main_identity.load_bias; + other_maps[0].l_ld = 0x501000; + other_maps[1].l_prev = (uintptr_t)&other_maps[0]; + + check_int("namespace.cached-main", + kzt_guest_link_map_classify_namespace( + (uintptr_t)&main_maps[1], &main_identity, main_head, &ops, + &namespace_head), + 1); + check_int("namespace.cached-same-bias-non-main", + kzt_guest_link_map_classify_namespace( + (uintptr_t)&other_maps[1], &main_identity, main_head, &ops, + &namespace_head), + 0); +} + +static void test_link_map_identity_rejects_wrong_dynamic_address(void) +{ + test_guest_link_map_t map = { + .l_addr = 0x700000, + .l_ld = 0x701000, + }; + fake_reader_memory_t memory = fake_memory_for(&map, sizeof(map), NULL, 0); + kzt_guest_link_map_reader_ops_t ops = fake_ops(&memory); + kzt_guest_link_map_identity_t identity = { 0 }; + + check_int("identity.read", + kzt_guest_link_map_read_identity((uintptr_t)&map, &ops, + &identity), + 0); + check_uintptr("identity.load-bias", identity.load_bias, map.l_addr); + check_uintptr("identity.dynamic", identity.dynamic_addr, map.l_ld); + check_int("identity.match", + kzt_guest_link_map_identity_matches(&identity, map.l_addr, + map.l_ld), + 1); + check_int("identity.wrong-dynamic", + kzt_guest_link_map_identity_matches(&identity, map.l_addr, + map.l_ld + 0x1000), + 0); +} + +static void test_predecessor_is_read_from_public_prefix(void) +{ + test_guest_link_map_t maps[2] = { 0 }; + fake_read_failure_t failure = { + .addr = (uintptr_t)&maps[1] + + offsetof(test_guest_link_map_t, l_prev), + .size = sizeof(maps[1].l_prev), + }; + fake_reader_memory_t memory = fake_memory_for(maps, sizeof(maps), NULL, 0); + kzt_guest_link_map_reader_ops_t ops = fake_ops(&memory); + uintptr_t predecessor = 0; + + maps[1].l_prev = (uintptr_t)&maps[0]; + check_int("predecessor.read", + kzt_guest_link_map_read_predecessor( + (uintptr_t)&maps[1], &ops, &predecessor), + 0); + check_uintptr("predecessor.value", predecessor, + (uintptr_t)&maps[0]); + + memory.failures = &failure; + memory.failure_count = 1; + predecessor = 1; + check_int("predecessor.read-failure", + kzt_guest_link_map_read_predecessor( + (uintptr_t)&maps[1], &ops, &predecessor), + -1); + check_uintptr("predecessor.failure-clears", predecessor, 0); +} + +static void test_main_namespace_walk_fails_open_on_bad_chain(void) +{ + test_guest_link_map_t maps[2] = { 0 }; + fake_read_failure_t failure = { + .addr = (uintptr_t)&maps[0] + + offsetof(test_guest_link_map_t, l_addr), + .size = sizeof(maps[0].l_addr), + }; + fake_reader_memory_t memory = fake_memory_for( + maps, sizeof(maps), &failure, 1); + kzt_guest_link_map_reader_ops_t ops = fake_ops(&memory); + + maps[0].l_addr = 0x400000; + maps[1].l_addr = 0x700000; + maps[1].l_prev = (uintptr_t)&maps[0]; + + check_int("namespace.read-failure", + kzt_guest_link_map_classify_namespace( + (uintptr_t)&maps[1], + &(kzt_guest_link_map_identity_t) { 0x400000, 0x401000 }, + 0, &ops, NULL), + -1); + + memory.failures = NULL; + memory.failure_count = 0; + maps[0].l_prev = (uintptr_t)&maps[1]; + check_int("namespace.cycle", + kzt_guest_link_map_classify_namespace( + (uintptr_t)&maps[1], + &(kzt_guest_link_map_identity_t) { 0x500000, 0x501000 }, + 0, &ops, NULL), + -1); + + check_int("namespace.invalid-args", + kzt_guest_link_map_classify_namespace( + 0, &(kzt_guest_link_map_identity_t) { 0x400000, 0x401000 }, + 0, &ops, NULL), + -1); +} + +static void test_main_namespace_walk_is_bounded(void) +{ + test_guest_link_map_t maps[257] = { 0 }; + fake_reader_memory_t memory = fake_memory_for(maps, sizeof(maps), NULL, 0); + kzt_guest_link_map_reader_ops_t ops = fake_ops(&memory); + size_t i; + + maps[0].l_addr = 0x400000; + for (i = 1; i < sizeof(maps) / sizeof(maps[0]); ++i) { + maps[i].l_addr = 0x500000 + i * 0x1000; + maps[i].l_prev = (uintptr_t)&maps[i - 1]; + } + + check_int("namespace.walk-limit", + kzt_guest_link_map_classify_namespace( + (uintptr_t)&maps[256], + &(kzt_guest_link_map_identity_t) { 0x400000, 0x401000 }, + 0, &ops, NULL), + -1); +} + +int main(void) +{ + test_valid_link_map_reads_complete_observation(); + test_invalid_link_map_is_identity_failure(); + test_field_read_failure_forms_partial_observation(); + test_name_snapshot_status_matrix(); + test_allocation_failure_is_read_error_not_borrowed_pointer(); + test_main_namespace_walk_matches_verified_main_identity(); + test_cached_main_head_uses_identity_not_load_bias(); + test_link_map_identity_rejects_wrong_dynamic_address(); + test_predecessor_is_read_from_public_prefix(); + test_main_namespace_walk_fails_open_on_bad_chain(); + test_main_namespace_walk_is_bounded(); + + if (failures) { + fprintf(stderr, "kzt-guest-link-map-reader: %d failure(s)\n", failures); + return 1; + } + + puts("kzt-guest-link-map-reader: all contract tests passed"); + return 0; +} diff --git a/tests/unit/kzt/test_guest_registry.c b/tests/unit/kzt/test_guest_registry.c new file mode 100644 index 00000000000..e5e273cf8d4 --- /dev/null +++ b/tests/unit/kzt/test_guest_registry.c @@ -0,0 +1,1261 @@ +#include +#include +#include +#include +#include + +#include "target/i386/latx/include/kzt_guest_registry.h" + +static int failures; + +static void check_true(const char *name, int condition) +{ + if (condition) { + return; + } + + fprintf(stderr, "%s: condition failed\n", name); + ++failures; +} + +static void check_int(const char *name, int got, int expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got %d expected %d\n", name, got, expected); + ++failures; +} + +static void check_not_int(const char *name, int got, int unexpected) +{ + if (got != unexpected) { + return; + } + + fprintf(stderr, "%s: got unexpected %d\n", name, got); + ++failures; +} + +static void check_ulong(const char *name, unsigned long got, + unsigned long expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got 0x%lx expected 0x%lx\n", name, got, expected); + ++failures; +} + +static void check_uintptr(const char *name, uintptr_t got, uintptr_t expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got 0x%lx expected 0x%lx\n", name, + (unsigned long)got, (unsigned long)expected); + ++failures; +} + +static void check_string(const char *name, const char *got, + const char *expected) +{ + if ((!got && !expected) || (got && expected && !strcmp(got, expected))) { + return; + } + + fprintf(stderr, "%s: got \"%s\" expected \"%s\"\n", name, + got ? got : "(null)", expected ? expected : "(null)"); + ++failures; +} + +static void check_contains(const char *name, const char *haystack, + const char *needle) +{ + if (haystack && needle && strstr(haystack, needle)) { + return; + } + + fprintf(stderr, "%s: missing \"%s\" in \"%s\"\n", name, + needle ? needle : "(null)", haystack ? haystack : "(null)"); + ++failures; +} + +static kzt_guest_object_observation_t make_observation(uintptr_t link_map_addr) +{ + return (kzt_guest_object_observation_t) { + .link_map_addr = link_map_addr, + .load_bias = { 0x100000, KZT_GUEST_FIELD_OK }, + .dynamic_addr = { 0x101000, KZT_GUEST_FIELD_OK }, + .map_start = { 0, KZT_GUEST_FIELD_UNKNOWN }, + .map_end = { 0, KZT_GUEST_FIELD_UNKNOWN }, + .namespace_id = { 0, KZT_GUEST_FIELD_UNKNOWN }, + .path = { "/guest/libfoo.so", KZT_GUEST_FIELD_OK }, + .soname = { NULL, KZT_GUEST_FIELD_NOT_PARSED }, + .dynamic_view_status = KZT_GUEST_FIELD_NOT_PARSED, + }; +} + +static kzt_guest_object_snapshot_t *find_snapshot( + kzt_guest_registry_t *registry, + uintptr_t link_map_addr) +{ + kzt_guest_object_snapshot_t *snapshot = NULL; + + check_int("find_by_link_map", kzt_guest_registry_find_by_link_map( + registry, link_map_addr, &snapshot), 0); + check_true("find_by_link_map.snapshot", snapshot != NULL); + return snapshot; +} + +static void assert_not_found(kzt_guest_registry_t *registry, + uintptr_t link_map_addr) +{ + kzt_guest_object_snapshot_t *snapshot = (void *)0x1; + + check_not_int("find_by_link_map.missing", + kzt_guest_registry_find_by_link_map(registry, + link_map_addr, + &snapshot), + 0); + check_true("find_by_link_map.missing-snapshot", snapshot == NULL); +} + +static void assert_snapshot_identity( + const kzt_guest_object_snapshot_t *snapshot, + uintptr_t link_map_addr, + unsigned long generation) +{ + check_uintptr("snapshot.link_map_addr", snapshot->link_map_addr, + link_map_addr); + check_ulong("snapshot.generation", snapshot->generation, generation); + check_int("snapshot.state", snapshot->state, KZT_GUEST_OBJECT_DISCOVERED); +} + +static void test_first_and_repeat_observation_keep_generation(void) +{ + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_object_observation_t observation = make_observation(0x1000); + kzt_guest_object_snapshot_t *snapshot; + + check_true("registry.init", registry != NULL); + if (!registry) { + return; + } + + observation.load_bias.value = 0; + observation.path.value = ""; + + check_int("observe.added", + kzt_guest_registry_observe(registry, &observation), + KZT_GUEST_REGISTRY_ADDED); + + snapshot = find_snapshot(registry, 0x1000); + assert_snapshot_identity(snapshot, 0x1000, 1); + check_int("snapshot.load_bias.status", snapshot->load_bias.status, + KZT_GUEST_FIELD_OK); + check_uintptr("snapshot.load_bias.value", snapshot->load_bias.value, 0); + check_int("snapshot.path.status", snapshot->path.status, + KZT_GUEST_FIELD_OK); + check_string("snapshot.path.value", snapshot->path.value, ""); + check_int("snapshot.soname.status", snapshot->soname.status, + KZT_GUEST_FIELD_NOT_PARSED); + check_int("snapshot.dynamic_view_status", snapshot->dynamic_view_status, + KZT_GUEST_FIELD_NOT_PARSED); + check_int("snapshot.map_start.status", snapshot->map_start.status, + KZT_GUEST_FIELD_UNKNOWN); + check_int("snapshot.namespace_id.status", snapshot->namespace_id.status, + KZT_GUEST_FIELD_UNKNOWN); + kzt_guest_object_snapshot_free(snapshot); + + check_int("observe.unchanged", + kzt_guest_registry_observe(registry, &observation), + KZT_GUEST_REGISTRY_UNCHANGED); + + snapshot = find_snapshot(registry, 0x1000); + assert_snapshot_identity(snapshot, 0x1000, 1); + kzt_guest_object_snapshot_free(snapshot); + + kzt_guest_registry_destroy(®istry); + check_true("registry.destroy", registry == NULL); +} + +static void test_unknown_fields_are_completed_without_generation_change(void) +{ + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_object_observation_t observation = make_observation(0x2000); + kzt_guest_object_snapshot_t *snapshot; + + check_true("registry.init", registry != NULL); + if (!registry) { + return; + } + + observation.load_bias.status = KZT_GUEST_FIELD_UNKNOWN; + observation.dynamic_addr.status = KZT_GUEST_FIELD_READ_ERROR; + observation.path.value = NULL; + observation.path.status = KZT_GUEST_FIELD_UNKNOWN; + + check_int("observe.partial-added", + kzt_guest_registry_observe(registry, &observation), + KZT_GUEST_REGISTRY_ADDED); + + snapshot = find_snapshot(registry, 0x2000); + assert_snapshot_identity(snapshot, 0x2000, 1); + check_int("snapshot.load_bias.unknown", snapshot->load_bias.status, + KZT_GUEST_FIELD_UNKNOWN); + check_int("snapshot.dynamic_addr.read-error", + snapshot->dynamic_addr.status, KZT_GUEST_FIELD_READ_ERROR); + check_int("snapshot.path.unknown", snapshot->path.status, + KZT_GUEST_FIELD_UNKNOWN); + kzt_guest_object_snapshot_free(snapshot); + + observation.load_bias.value = 0x220000; + observation.load_bias.status = KZT_GUEST_FIELD_OK; + observation.dynamic_addr.value = 0x221000; + observation.dynamic_addr.status = KZT_GUEST_FIELD_OK; + observation.path.value = "/guest/libfilled.so"; + observation.path.status = KZT_GUEST_FIELD_OK; + + check_int("observe.updated", + kzt_guest_registry_observe(registry, &observation), + KZT_GUEST_REGISTRY_UPDATED); + + snapshot = find_snapshot(registry, 0x2000); + assert_snapshot_identity(snapshot, 0x2000, 1); + check_int("snapshot.load_bias.ok", snapshot->load_bias.status, + KZT_GUEST_FIELD_OK); + check_uintptr("snapshot.load_bias.filled", snapshot->load_bias.value, + 0x220000); + check_int("snapshot.dynamic_addr.ok", snapshot->dynamic_addr.status, + KZT_GUEST_FIELD_OK); + check_uintptr("snapshot.dynamic_addr.filled", snapshot->dynamic_addr.value, + 0x221000); + check_int("snapshot.path.ok", snapshot->path.status, KZT_GUEST_FIELD_OK); + check_string("snapshot.path.filled", snapshot->path.value, + "/guest/libfilled.so"); + kzt_guest_object_snapshot_free(snapshot); + + observation.load_bias.status = KZT_GUEST_FIELD_READ_ERROR; + observation.dynamic_addr.status = KZT_GUEST_FIELD_UNKNOWN; + observation.path.value = NULL; + observation.path.status = KZT_GUEST_FIELD_READ_ERROR; + + check_int("observe.failed-fields-unchanged", + kzt_guest_registry_observe(registry, &observation), + KZT_GUEST_REGISTRY_UNCHANGED); + + snapshot = find_snapshot(registry, 0x2000); + assert_snapshot_identity(snapshot, 0x2000, 1); + check_int("snapshot.load_bias.still-ok", snapshot->load_bias.status, + KZT_GUEST_FIELD_OK); + check_uintptr("snapshot.load_bias.still-filled", snapshot->load_bias.value, + 0x220000); + check_int("snapshot.dynamic_addr.still-ok", snapshot->dynamic_addr.status, + KZT_GUEST_FIELD_OK); + check_uintptr("snapshot.dynamic_addr.still-filled", + snapshot->dynamic_addr.value, 0x221000); + check_int("snapshot.path.still-ok", snapshot->path.status, + KZT_GUEST_FIELD_OK); + check_string("snapshot.path.still-filled", snapshot->path.value, + "/guest/libfilled.so"); + kzt_guest_object_snapshot_free(snapshot); + + kzt_guest_registry_destroy(®istry); + check_true("registry.destroy", registry == NULL); +} + +static void test_identity_conflict_preserves_original_record(void) +{ + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_object_observation_t observation = make_observation(0x3000); + kzt_guest_object_snapshot_t *snapshot; + kzt_guest_registry_dump_t dump = { 0 }; + + check_true("registry.init", registry != NULL); + if (!registry) { + return; + } + + observation.load_bias.value = 0x300000; + observation.dynamic_addr.value = 0x301000; + observation.path.value = "/guest/liboriginal.so"; + + check_int("observe.added", + kzt_guest_registry_observe(registry, &observation), + KZT_GUEST_REGISTRY_ADDED); + + observation.load_bias.value = 0x310000; + observation.dynamic_addr.value = 0x311000; + observation.path.value = "/guest/libconflict.so"; + + check_int("observe.conflict", + kzt_guest_registry_observe(registry, &observation), + KZT_GUEST_REGISTRY_CONFLICT); + + snapshot = find_snapshot(registry, 0x3000); + assert_snapshot_identity(snapshot, 0x3000, 1); + check_uintptr("snapshot.load_bias.original", snapshot->load_bias.value, + 0x300000); + check_uintptr("snapshot.dynamic_addr.original", + snapshot->dynamic_addr.value, 0x301000); + check_string("snapshot.path.original", snapshot->path.value, + "/guest/liboriginal.so"); + kzt_guest_object_snapshot_free(snapshot); + + check_int("dump.snapshot", + kzt_guest_registry_dump_snapshot(registry, &dump), + 0); + check_ulong("dump.count", dump.count, 1); + assert_snapshot_identity(&dump.objects[0], 0x3000, 1); + check_string("dump.path.original", dump.objects[0].path.value, + "/guest/liboriginal.so"); + kzt_guest_registry_dump_free(&dump); + + kzt_guest_registry_destroy(®istry); + check_true("registry.destroy", registry == NULL); +} + +static void test_partial_observation_and_invalid_identity(void) +{ + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_object_observation_t observation = make_observation(0); + kzt_guest_object_snapshot_t *snapshot; + + check_true("registry.init", registry != NULL); + if (!registry) { + return; + } + + check_int("observe.invalid-link-map", + kzt_guest_registry_observe(registry, &observation), + KZT_GUEST_REGISTRY_ERROR); + assert_not_found(registry, 0); + + observation = make_observation(0x4000); + observation.load_bias.status = KZT_GUEST_FIELD_READ_ERROR; + observation.dynamic_addr.status = KZT_GUEST_FIELD_UNKNOWN; + observation.path.value = "/guest/path-truncated"; + observation.path.status = KZT_GUEST_FIELD_TRUNCATED; + + check_int("observe.partial-added", + kzt_guest_registry_observe(registry, &observation), + KZT_GUEST_REGISTRY_ADDED); + + snapshot = find_snapshot(registry, 0x4000); + assert_snapshot_identity(snapshot, 0x4000, 1); + check_int("snapshot.load_bias.read-error", snapshot->load_bias.status, + KZT_GUEST_FIELD_READ_ERROR); + check_int("snapshot.dynamic_addr.unknown", snapshot->dynamic_addr.status, + KZT_GUEST_FIELD_UNKNOWN); + check_int("snapshot.path.truncated", snapshot->path.status, + KZT_GUEST_FIELD_TRUNCATED); + check_string("snapshot.path.truncated-value", snapshot->path.value, + "/guest/path-truncated"); + kzt_guest_object_snapshot_free(snapshot); + + kzt_guest_registry_destroy(®istry); + check_true("registry.destroy", registry == NULL); +} + +static void test_query_and_dump_snapshots_are_caller_owned(void) +{ + static const char path_literal[] = "/guest/libowned.so"; + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_object_observation_t observation = make_observation(0x5000); + kzt_guest_object_snapshot_t *first; + kzt_guest_object_snapshot_t *second; + kzt_guest_registry_dump_t dump = { 0 }; + + check_true("registry.init", registry != NULL); + if (!registry) { + return; + } + + observation.path.value = path_literal; + check_int("observe.added", + kzt_guest_registry_observe(registry, &observation), + KZT_GUEST_REGISTRY_ADDED); + + first = find_snapshot(registry, 0x5000); + second = find_snapshot(registry, 0x5000); + check_string("first.path", first->path.value, path_literal); + check_string("second.path", second->path.value, path_literal); + check_true("first.path.not-observation", first->path.value != path_literal); + check_true("second.path.not-observation", + second->path.value != path_literal); + check_true("snapshots.path.distinct", + first->path.value != second->path.value); + + check_int("dump.snapshot", + kzt_guest_registry_dump_snapshot(registry, &dump), + 0); + check_ulong("dump.count", dump.count, 1); + check_string("dump.path", dump.objects[0].path.value, path_literal); + check_true("dump.path.not-observation", + dump.objects[0].path.value != path_literal); + check_true("dump.path.not-first", + dump.objects[0].path.value != first->path.value); + check_true("dump.path.not-second", + dump.objects[0].path.value != second->path.value); + + kzt_guest_registry_dump_free(&dump); + kzt_guest_object_snapshot_free(first); + kzt_guest_object_snapshot_free(second); + + kzt_guest_registry_destroy(®istry); + check_true("registry.destroy", registry == NULL); +} + +typedef struct dump_text_sink { + char text[8192]; + size_t used; + unsigned long calls; + kzt_guest_registry_t *registry; +} dump_text_sink_t; + +static int collect_dump_text_line(const char *line, void *opaque) +{ + dump_text_sink_t *sink = opaque; + kzt_guest_registry_diagnostics_t diagnostics = { 0 }; + size_t len; + size_t remaining; + + if (sink->registry) { + check_int("dump.sink-can-query-registry", + kzt_guest_registry_get_diagnostics(sink->registry, + &diagnostics), + 0); + } + + ++sink->calls; + len = strlen(line); + remaining = sizeof(sink->text) - sink->used; + if (remaining <= 2) { + return 0; + } + if (len >= remaining - 1) { + len = remaining - 2; + } + memcpy(sink->text + sink->used, line, len); + sink->used += len; + sink->text[sink->used++] = '\n'; + sink->text[sink->used] = '\0'; + return 0; +} + +static void test_diagnostics_are_opt_in_and_throttled(void) +{ + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_object_observation_t observation = make_observation(0x8000); + kzt_guest_registry_observation_diagnostic_t diagnostic = { 0 }; + kzt_guest_registry_diagnostic_report_t report; + kzt_guest_registry_diagnostic_config_t config = { + .enabled = 1, + .throttle_limit = 2, + }; + dump_text_sink_t sink = { 0 }; + + check_true("registry.init", registry != NULL); + if (!registry) { + return; + } + + check_int("observe.diagnostic-disabled", + kzt_guest_registry_observe_with_diagnostic(registry, + &observation, + &diagnostic), + KZT_GUEST_REGISTRY_ADDED); + check_int("diagnostic.default-enabled", diagnostic.enabled, 0); + check_int("diagnostic.default-emitted", diagnostic.emitted, 0); + check_int("diagnostic.default-result", diagnostic.result, + KZT_GUEST_REGISTRY_ADDED); + + check_int("diagnostic.report.disabled", + kzt_guest_registry_get_diagnostic_report(registry, &report), + 0); + check_int("report.default-enabled", report.config.enabled, 0); + check_ulong("report.default-added-events", + report.events[KZT_GUEST_REGISTRY_ADDED].observed, 0); + check_ulong("report.default-added-counter", report.counters.added, 1); + + check_int("diagnostic.configure", + kzt_guest_registry_configure_diagnostics(registry, &config), + 0); + + check_int("observe.unchanged.first", + kzt_guest_registry_observe_with_diagnostic(registry, + &observation, + &diagnostic), + KZT_GUEST_REGISTRY_UNCHANGED); + check_int("diagnostic.first-enabled", diagnostic.enabled, 1); + check_int("diagnostic.first-emitted", diagnostic.emitted, 1); + check_ulong("diagnostic.first-observations", + diagnostic.result_observations, 1); + check_ulong("diagnostic.first-suppressed", diagnostic.result_suppressed, + 0); + + check_int("observe.unchanged.second", + kzt_guest_registry_observe_with_diagnostic(registry, + &observation, + &diagnostic), + KZT_GUEST_REGISTRY_UNCHANGED); + check_int("diagnostic.second-emitted", diagnostic.emitted, 1); + check_ulong("diagnostic.second-observations", + diagnostic.result_observations, 2); + + check_int("observe.unchanged.third", + kzt_guest_registry_observe_with_diagnostic(registry, + &observation, + &diagnostic), + KZT_GUEST_REGISTRY_UNCHANGED); + check_int("diagnostic.third-emitted", diagnostic.emitted, 0); + check_ulong("diagnostic.third-observations", + diagnostic.result_observations, 3); + check_ulong("diagnostic.third-suppressed", diagnostic.result_suppressed, + 1); + + check_int("diagnostic.report.enabled", + kzt_guest_registry_get_diagnostic_report(registry, &report), + 0); + check_int("report.enabled", report.config.enabled, 1); + check_ulong("report.throttle-limit", report.config.throttle_limit, 2); + check_ulong("report.unchanged-observed", + report.events[KZT_GUEST_REGISTRY_UNCHANGED].observed, 3); + check_ulong("report.unchanged-emitted", + report.events[KZT_GUEST_REGISTRY_UNCHANGED].emitted, 2); + check_ulong("report.unchanged-suppressed", + report.events[KZT_GUEST_REGISTRY_UNCHANGED].suppressed, 1); + check_uintptr("report.unchanged-last-link-map", + report.events[KZT_GUEST_REGISTRY_UNCHANGED] + .last_link_map_addr, + 0x8000); + + sink.registry = registry; + check_int("dump.text", + kzt_guest_registry_dump_text(registry, collect_dump_text_line, + &sink), + 0); + check_true("dump.text-called", sink.calls > 0); + check_contains("dump.text-summary", sink.text, + "enabled=1 throttle_limit=2"); + check_contains("dump.text-event", sink.text, + "result=unchanged observed=3 emitted=2 suppressed=1"); + check_contains("dump.text-object", sink.text, + "object link_map=0x8000 generation=1 state=0"); + + kzt_guest_registry_destroy(®istry); + check_true("registry.destroy", registry == NULL); +} + +static void test_init_failure_creates_disabled_registry_diagnostics(void) +{ + kzt_guest_registry_t *registry; + kzt_guest_object_observation_t observation = make_observation(0x6000); + kzt_guest_registry_diagnostics_t diagnostics = { 0 }; + + kzt_guest_registry_test_set_alloc_failure_after(1); + registry = kzt_guest_registry_init(); + kzt_guest_registry_test_set_alloc_failure_after(-1); + + check_true("registry.disabled-init", registry != NULL); + if (!registry) { + return; + } + + check_int("observe.disabled", + kzt_guest_registry_observe(registry, &observation), + KZT_GUEST_REGISTRY_DISABLED); + + check_int("diagnostics.disabled", + kzt_guest_registry_get_diagnostics(registry, &diagnostics), 0); + check_ulong("diagnostics.init-failures", diagnostics.init_failures, 1); + check_ulong("diagnostics.alloc-failures", diagnostics.allocation_failures, + 1); + check_ulong("diagnostics.disabled-observe", diagnostics.disabled, 1); + check_ulong("diagnostics.observations", diagnostics.observations, 1); + + kzt_guest_registry_destroy(®istry); + check_true("registry.destroy", registry == NULL); +} + +static void test_cond_init_failure_is_clean(void) +{ + kzt_guest_registry_t *registry; + + kzt_guest_registry_test_fail_next_cond_init(); + registry = kzt_guest_registry_init(); + check_true("cond-init.failure", registry == NULL); + + registry = kzt_guest_registry_init(); + check_true("cond-init.next-succeeds", registry != NULL); + kzt_guest_registry_destroy(®istry); +} + +static void test_source_lease_acquire_release(void) +{ + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_object_observation_t observation = make_observation(0x6800); + kzt_guest_object_observation_t unknown_namespace = + make_observation(0x6880); + kzt_guest_object_snapshot_t *snapshot; + kzt_guest_registry_source_lease_t lease = { 0 }; + unsigned long generation; + + check_true("lease.registry", registry != NULL); + if (!registry) { + return; + } + observation.namespace_id = (kzt_guest_scalar_field_t) { + .value = 0, + .status = KZT_GUEST_FIELD_OK, + }; + check_int("lease.observe", kzt_guest_registry_observe( + registry, &observation), KZT_GUEST_REGISTRY_ADDED); + snapshot = find_snapshot(registry, observation.link_map_addr); + generation = snapshot->generation; + kzt_guest_object_snapshot_free(snapshot); + + check_int("lease.acquire", kzt_guest_registry_source_lease_acquire( + registry, observation.link_map_addr, generation, 0, + &lease), 0); + check_true("lease.active", lease.active == 1 && + lease.registry == registry && + lease.namespace_id == 0); + snapshot = find_snapshot(registry, observation.link_map_addr); + check_ulong("lease.snapshot-active", snapshot->active_source_leases, 1); + kzt_guest_object_snapshot_free(snapshot); + + kzt_guest_registry_source_lease_release(&lease); + check_true("lease.cleared", lease.active == 0 && lease.registry == NULL); + snapshot = find_snapshot(registry, observation.link_map_addr); + check_ulong("lease.snapshot-idle", snapshot->active_source_leases, 0); + kzt_guest_object_snapshot_free(snapshot); + kzt_guest_registry_source_lease_release(&lease); + + check_not_int("lease.stale", kzt_guest_registry_source_lease_acquire( + registry, observation.link_map_addr, generation + 1, + 0, &lease), 0); + check_true("lease.stale-cleared", lease.active == 0); + check_not_int("lease.namespace-mismatch", + kzt_guest_registry_source_lease_acquire( + registry, observation.link_map_addr, generation, 1, + &lease), 0); + check_true("lease.namespace-mismatch-cleared", lease.active == 0); + check_int("lease.unknown-namespace-observe", kzt_guest_registry_observe( + registry, &unknown_namespace), KZT_GUEST_REGISTRY_ADDED); + snapshot = find_snapshot(registry, unknown_namespace.link_map_addr); + generation = snapshot->generation; + kzt_guest_object_snapshot_free(snapshot); + check_not_int("lease.unknown-namespace", + kzt_guest_registry_source_lease_acquire( + registry, unknown_namespace.link_map_addr, generation, + 0, &lease), 0); + check_true("lease.unknown-namespace-cleared", lease.active == 0); + kzt_guest_registry_destroy(®istry); +} + +typedef struct retire_wait_fixture { + pthread_mutex_t lock; + kzt_guest_registry_t *registry; + uintptr_t link_map_addr; + unsigned long generation; + int called; + int done; + int result; +} retire_wait_fixture_t; + +typedef struct registry_hook_sync { + pthread_mutex_t lock; + pthread_cond_t cond; + int before_wait_count; + int api_entered; + int release_api; + int destroy_disabled; +} registry_hook_sync_t; + +static void before_retire_wait_hook(void *opaque) +{ + registry_hook_sync_t *sync = opaque; + + pthread_mutex_lock(&sync->lock); + ++sync->before_wait_count; + pthread_cond_broadcast(&sync->cond); + pthread_mutex_unlock(&sync->lock); +} + +static void after_api_enter_hook(void *opaque) +{ + registry_hook_sync_t *sync = opaque; + + pthread_mutex_lock(&sync->lock); + sync->api_entered = 1; + pthread_cond_broadcast(&sync->cond); + while (!sync->release_api) { + pthread_cond_wait(&sync->cond, &sync->lock); + } + pthread_mutex_unlock(&sync->lock); +} + +static void after_destroy_disable_hook(void *opaque) +{ + registry_hook_sync_t *sync = opaque; + + pthread_mutex_lock(&sync->lock); + sync->destroy_disabled = 1; + pthread_cond_broadcast(&sync->cond); + pthread_mutex_unlock(&sync->lock); +} + +static int registry_hook_sync_init(registry_hook_sync_t *sync) +{ + memset(sync, 0, sizeof(*sync)); + if (pthread_mutex_init(&sync->lock, NULL) != 0) { + return -1; + } + if (pthread_cond_init(&sync->cond, NULL) != 0) { + pthread_mutex_destroy(&sync->lock); + return -1; + } + return 0; +} + +static void registry_hook_sync_destroy(registry_hook_sync_t *sync) +{ + pthread_cond_destroy(&sync->cond); + pthread_mutex_destroy(&sync->lock); +} + +static void *retire_wait_worker(void *opaque) +{ + retire_wait_fixture_t *fixture = opaque; + int result; + + pthread_mutex_lock(&fixture->lock); + fixture->called = 1; + pthread_mutex_unlock(&fixture->lock); + result = kzt_guest_registry_retire( + fixture->registry, fixture->link_map_addr, fixture->generation); + pthread_mutex_lock(&fixture->lock); + fixture->result = result; + fixture->done = 1; + pthread_mutex_unlock(&fixture->lock); + return NULL; +} + +static void test_retire_waits_for_source_lease(void) +{ + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_object_observation_t observation = make_observation(0x6900); + kzt_guest_object_snapshot_t *snapshot; + kzt_guest_registry_source_lease_t lease = { 0 }; + retire_wait_fixture_t fixture; + pthread_t thread; + struct timespec start; + int done = 0; + int thread_created; + + check_true("retire-wait.registry", registry != NULL); + if (!registry) { + return; + } + observation.namespace_id = (kzt_guest_scalar_field_t) { + .value = 0, + .status = KZT_GUEST_FIELD_OK, + }; + check_int("retire-wait.observe", kzt_guest_registry_observe( + registry, &observation), KZT_GUEST_REGISTRY_ADDED); + snapshot = find_snapshot(registry, observation.link_map_addr); + memset(&fixture, 0, sizeof(fixture)); + fixture.registry = registry; + fixture.link_map_addr = observation.link_map_addr; + fixture.generation = snapshot->generation; + kzt_guest_object_snapshot_free(snapshot); + check_int("retire-wait.lock", pthread_mutex_init(&fixture.lock, NULL), 0); + check_int("retire-wait.acquire", kzt_guest_registry_source_lease_acquire( + registry, fixture.link_map_addr, fixture.generation, + 0, &lease), 0); + thread_created = pthread_create( + &thread, NULL, retire_wait_worker, &fixture); + check_int("retire-wait.thread", thread_created, 0); + if (thread_created != 0) { + kzt_guest_registry_source_lease_release(&lease); + pthread_mutex_destroy(&fixture.lock); + kzt_guest_registry_destroy(®istry); + return; + } + + clock_gettime(CLOCK_MONOTONIC, &start); + for (;;) { + struct timespec now; + + pthread_mutex_lock(&fixture.lock); + done = fixture.done; + pthread_mutex_unlock(&fixture.lock); + snapshot = NULL; + if (kzt_guest_registry_find_by_link_map( + registry, fixture.link_map_addr, &snapshot) != 0) { + kzt_guest_object_snapshot_free(snapshot); + break; + } + kzt_guest_object_snapshot_free(snapshot); + clock_gettime(CLOCK_MONOTONIC, &now); + if (done || now.tv_sec - start.tv_sec >= 2) { + break; + } + sched_yield(); + } + pthread_mutex_lock(&fixture.lock); + done = fixture.done; + pthread_mutex_unlock(&fixture.lock); + check_true("retire-wait.blocked", done == 0); + + kzt_guest_registry_source_lease_release(&lease); + check_int("retire-wait.join", pthread_join(thread, NULL), 0); + check_true("retire-wait.done", fixture.done == 1); + check_int("retire-wait.result", fixture.result, 0); + check_not_int("retire-wait.dead", kzt_guest_registry_find_by_link_map( + registry, fixture.link_map_addr, &snapshot), 0); + check_true("retire-wait.no-snapshot", snapshot == NULL); + pthread_mutex_destroy(&fixture.lock); + kzt_guest_registry_destroy(®istry); +} + +static void test_double_retire_cannot_kill_reused_generation(void) +{ + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_object_observation_t observation = make_observation(0x6a00); + kzt_guest_registry_source_lease_t lease = { 0 }; + kzt_guest_object_snapshot_t *snapshot; + retire_wait_fixture_t retire = { 0 }; + registry_hook_sync_t sync; + pthread_t thread; + unsigned long old_generation; + unsigned long new_generation; + + check_true("double-retire.registry", registry != NULL); + if (!registry || registry_hook_sync_init(&sync) != 0) { + kzt_guest_registry_destroy(®istry); + return; + } + observation.namespace_id = (kzt_guest_scalar_field_t) { + .value = 0, + .status = KZT_GUEST_FIELD_OK, + }; + check_int("double-retire.observe", kzt_guest_registry_observe( + registry, &observation), KZT_GUEST_REGISTRY_ADDED); + snapshot = find_snapshot(registry, observation.link_map_addr); + old_generation = snapshot->generation; + kzt_guest_object_snapshot_free(snapshot); + check_int("double-retire.lease", kzt_guest_registry_source_lease_acquire( + registry, observation.link_map_addr, old_generation, + 0, &lease), 0); + kzt_guest_registry_test_set_before_retire_wait( + before_retire_wait_hook, &sync); + retire.registry = registry; + retire.link_map_addr = observation.link_map_addr; + retire.generation = old_generation; + check_int("double-retire.lock", pthread_mutex_init( + &retire.lock, NULL), 0); + check_int("double-retire.thread", pthread_create( + &thread, NULL, retire_wait_worker, &retire), 0); + pthread_mutex_lock(&sync.lock); + while (sync.before_wait_count < 1) { + pthread_cond_wait(&sync.cond, &sync.lock); + } + pthread_mutex_unlock(&sync.lock); + + check_not_int("double-retire.second-rejected", + kzt_guest_registry_retire( + registry, observation.link_map_addr, old_generation), + 0); + kzt_guest_registry_source_lease_release(&lease); + check_int("double-retire.join", pthread_join(thread, NULL), 0); + check_int("double-retire.first-success", retire.result, 0); + check_int("double-retire.reobserve", kzt_guest_registry_observe( + registry, &observation), KZT_GUEST_REGISTRY_ADDED); + snapshot = find_snapshot(registry, observation.link_map_addr); + new_generation = snapshot->generation; + kzt_guest_object_snapshot_free(snapshot); + check_true("double-retire.new-generation", + new_generation > old_generation); + check_not_int("double-retire.old-generation-rejected", + kzt_guest_registry_retire( + registry, observation.link_map_addr, old_generation), + 0); + snapshot = find_snapshot(registry, observation.link_map_addr); + check_true("double-retire.reused-live", snapshot != NULL && + snapshot->generation == new_generation); + kzt_guest_object_snapshot_free(snapshot); + kzt_guest_registry_test_set_before_retire_wait(NULL, NULL); + pthread_mutex_destroy(&retire.lock); + registry_hook_sync_destroy(&sync); + kzt_guest_registry_destroy(®istry); +} + +typedef struct simultaneous_retire_fixture { + kzt_guest_registry_t *registry; + uintptr_t link_map_addr; + unsigned long generation; + pthread_barrier_t *barrier; + int result; +} simultaneous_retire_fixture_t; + +static void *simultaneous_retire_worker(void *opaque) +{ + simultaneous_retire_fixture_t *fixture = opaque; + + pthread_barrier_wait(fixture->barrier); + fixture->result = kzt_guest_registry_retire( + fixture->registry, fixture->link_map_addr, fixture->generation); + return NULL; +} + +static void test_simultaneous_retire_has_one_owner(void) +{ + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_object_observation_t observation = make_observation(0x6a80); + kzt_guest_object_snapshot_t *snapshot; + kzt_guest_registry_dump_t dump = { 0 }; + simultaneous_retire_fixture_t fixtures[2]; + pthread_barrier_t barrier; + pthread_t threads[2]; + unsigned long generation; + int successes = 0; + int rejections = 0; + + check_true("one-retire.registry", registry != NULL); + if (!registry) { + return; + } + check_int("one-retire.observe", kzt_guest_registry_observe( + registry, &observation), KZT_GUEST_REGISTRY_ADDED); + snapshot = find_snapshot(registry, observation.link_map_addr); + generation = snapshot->generation; + kzt_guest_object_snapshot_free(snapshot); + check_int("one-retire.barrier", pthread_barrier_init( + &barrier, NULL, 3), 0); + for (int i = 0; i < 2; ++i) { + fixtures[i] = (simultaneous_retire_fixture_t) { + .registry = registry, + .link_map_addr = observation.link_map_addr, + .generation = generation, + .barrier = &barrier, + .result = -2, + }; + check_int("one-retire.thread", pthread_create( + &threads[i], NULL, simultaneous_retire_worker, + &fixtures[i]), 0); + } + pthread_barrier_wait(&barrier); + for (int i = 0; i < 2; ++i) { + check_int("one-retire.join", pthread_join(threads[i], NULL), 0); + successes += fixtures[i].result == 0; + rejections += fixtures[i].result != 0; + } + check_int("one-retire.exactly-one-success", successes, 1); + check_int("one-retire.exactly-one-rejection", rejections, 1); + check_int("one-retire.dump", kzt_guest_registry_dump_snapshot( + registry, &dump), 0); + check_ulong("one-retire.single-object", dump.count, 1); + if (dump.count == 1) { + check_ulong("one-retire.same-generation", + dump.objects[0].generation, generation); + check_int("one-retire.final-dead", dump.objects[0].state, + KZT_GUEST_OBJECT_DEAD); + } + kzt_guest_registry_dump_free(&dump); + pthread_barrier_destroy(&barrier); + kzt_guest_registry_destroy(®istry); +} + +typedef struct registry_observe_worker { + kzt_guest_registry_t *registry; + kzt_guest_object_observation_t observation; + kzt_guest_registry_result_t result; +} registry_observe_worker_t; + +typedef struct registry_destroy_worker { + pthread_mutex_t lock; + pthread_cond_t cond; + kzt_guest_registry_t *registry; + int called; + int done; +} registry_destroy_worker_t; + +static void *registry_observe_worker_main(void *opaque) +{ + registry_observe_worker_t *worker = opaque; + + worker->result = kzt_guest_registry_observe( + worker->registry, &worker->observation); + return NULL; +} + +static void *registry_destroy_worker_main(void *opaque) +{ + registry_destroy_worker_t *worker = opaque; + + pthread_mutex_lock(&worker->lock); + worker->called = 1; + pthread_cond_broadcast(&worker->cond); + pthread_mutex_unlock(&worker->lock); + kzt_guest_registry_destroy(&worker->registry); + pthread_mutex_lock(&worker->lock); + worker->done = 1; + pthread_cond_broadcast(&worker->cond); + pthread_mutex_unlock(&worker->lock); + return NULL; +} + +static void test_destroy_drains_waiter_and_inflight_api(void) +{ + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_object_observation_t observation = make_observation(0x6b00); + kzt_guest_registry_source_lease_t lease = { 0 }; + kzt_guest_object_snapshot_t *snapshot; + retire_wait_fixture_t retire = { 0 }; + registry_observe_worker_t observe_worker; + registry_destroy_worker_t destroy_worker; + registry_hook_sync_t sync; + pthread_t retire_thread; + pthread_t observe_thread; + pthread_t destroy_thread; + + check_true("destroy-race.registry", registry != NULL); + if (!registry || registry_hook_sync_init(&sync) != 0) { + kzt_guest_registry_destroy(®istry); + return; + } + observation.namespace_id = (kzt_guest_scalar_field_t) { + .value = 0, + .status = KZT_GUEST_FIELD_OK, + }; + check_int("destroy-race.observe", kzt_guest_registry_observe( + registry, &observation), KZT_GUEST_REGISTRY_ADDED); + snapshot = find_snapshot(registry, observation.link_map_addr); + retire.registry = registry; + retire.link_map_addr = observation.link_map_addr; + retire.generation = snapshot->generation; + kzt_guest_object_snapshot_free(snapshot); + check_int("destroy-race.retire-lock", pthread_mutex_init( + &retire.lock, NULL), 0); + check_int("destroy-race.lease", kzt_guest_registry_source_lease_acquire( + registry, retire.link_map_addr, retire.generation, + 0, &lease), 0); + kzt_guest_registry_test_set_before_retire_wait( + before_retire_wait_hook, &sync); + check_int("destroy-race.retire-thread", pthread_create( + &retire_thread, NULL, retire_wait_worker, &retire), 0); + pthread_mutex_lock(&sync.lock); + while (sync.before_wait_count < 1) { + pthread_cond_wait(&sync.cond, &sync.lock); + } + pthread_mutex_unlock(&sync.lock); + + observe_worker = (registry_observe_worker_t) { + .registry = registry, + .observation = make_observation(0x6c00), + }; + kzt_guest_registry_test_set_after_api_enter(after_api_enter_hook, &sync); + check_int("destroy-race.observe-thread", pthread_create( + &observe_thread, NULL, registry_observe_worker_main, + &observe_worker), 0); + pthread_mutex_lock(&sync.lock); + while (!sync.api_entered) { + pthread_cond_wait(&sync.cond, &sync.lock); + } + pthread_mutex_unlock(&sync.lock); + + memset(&destroy_worker, 0, sizeof(destroy_worker)); + destroy_worker.registry = registry; + check_int("destroy-race.destroy-lock", pthread_mutex_init( + &destroy_worker.lock, NULL), 0); + check_int("destroy-race.destroy-cond", pthread_cond_init( + &destroy_worker.cond, NULL), 0); + kzt_guest_registry_test_set_after_destroy_disable( + after_destroy_disable_hook, &sync); + check_int("destroy-race.destroy-thread", pthread_create( + &destroy_thread, NULL, registry_destroy_worker_main, + &destroy_worker), 0); + pthread_mutex_lock(&destroy_worker.lock); + while (!destroy_worker.called) { + pthread_cond_wait(&destroy_worker.cond, &destroy_worker.lock); + } + check_true("destroy-race.blocked", destroy_worker.done == 0); + pthread_mutex_unlock(&destroy_worker.lock); + pthread_mutex_lock(&sync.lock); + while (!sync.destroy_disabled) { + pthread_cond_wait(&sync.cond, &sync.lock); + } + pthread_mutex_unlock(&sync.lock); + + pthread_mutex_lock(&sync.lock); + sync.release_api = 1; + pthread_cond_broadcast(&sync.cond); + pthread_mutex_unlock(&sync.lock); + check_int("destroy-race.observe-join", pthread_join( + observe_thread, NULL), 0); + check_int("destroy-race.observe-disabled", observe_worker.result, + KZT_GUEST_REGISTRY_DISABLED); + kzt_guest_registry_source_lease_release(&lease); + check_int("destroy-race.retire-join", pthread_join( + retire_thread, NULL), 0); + check_not_int("destroy-race.retire-cancelled", retire.result, 0); + check_int("destroy-race.destroy-join", pthread_join( + destroy_thread, NULL), 0); + check_true("destroy-race.destroyed", destroy_worker.done == 1 && + destroy_worker.registry == NULL); + + kzt_guest_registry_test_set_after_api_enter(NULL, NULL); + kzt_guest_registry_test_set_before_retire_wait(NULL, NULL); + kzt_guest_registry_test_set_after_destroy_disable(NULL, NULL); + pthread_cond_destroy(&destroy_worker.cond); + pthread_mutex_destroy(&destroy_worker.lock); + pthread_mutex_destroy(&retire.lock); + registry_hook_sync_destroy(&sync); + registry = NULL; +} + +static void test_destroyed_registry_rejects_new_observation(void) +{ + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_object_observation_t observation = make_observation(0x7000); + + check_true("registry.init", registry != NULL); + if (!registry) { + return; + } + + kzt_guest_registry_destroy(®istry); + check_true("registry.destroy", registry == NULL); + check_int("observe.after-destroy", + kzt_guest_registry_observe(registry, &observation), + KZT_GUEST_REGISTRY_DISABLED); +} + +static void test_retired_address_gets_new_generation(void) +{ + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_object_observation_t observation = make_observation(0x7100); + kzt_guest_object_snapshot_t *snapshot; + kzt_guest_registry_dump_t dump = { 0 }; + check_int("reuse.first", kzt_guest_registry_observe(registry, &observation), + KZT_GUEST_REGISTRY_ADDED); + snapshot = find_snapshot(registry, observation.link_map_addr); + unsigned long first_generation = snapshot->generation; + kzt_guest_object_snapshot_free(snapshot); + check_int("reuse.retire", kzt_guest_registry_retire( + registry, observation.link_map_addr, first_generation), 0); + check_int("reuse.dead.filtered", kzt_guest_registry_find_by_link_map( + registry, observation.link_map_addr, &snapshot), -1); + check_true("reuse.dead.no-snapshot", snapshot == NULL); + check_int("reuse.dead.dump", kzt_guest_registry_dump_snapshot( + registry, &dump), 0); + check_ulong("reuse.dead.dump-count", dump.count, 1); + check_int("reuse.dead.diagnostic-state", dump.objects[0].state, + KZT_GUEST_OBJECT_DEAD); + kzt_guest_registry_dump_free(&dump); + check_int("reuse.observe", kzt_guest_registry_observe(registry, &observation), + KZT_GUEST_REGISTRY_ADDED); + snapshot = find_snapshot(registry, observation.link_map_addr); + check_ulong("reuse.new-generation", snapshot->generation, + first_generation + 1); + check_int("reuse.live-state", snapshot->state, + KZT_GUEST_OBJECT_DISCOVERED); + kzt_guest_object_snapshot_free(snapshot); + kzt_guest_registry_destroy(®istry); +} + +static void test_lazy_resolver_publish_find_lifecycle(void) +{ + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_object_observation_t observation = make_observation(0x7200); + kzt_guest_object_snapshot_t *snapshot; + kzt_guest_lazy_resolver_t resolver = { + .link_map_slot = 0x7210, + .resolver_slot = 0x7218, + .guest_link_map = 0x7200, + .guest_resolver = 0x7300, + }; + kzt_guest_lazy_resolver_t found; + unsigned long generation; + + observation.namespace_id = (kzt_guest_scalar_field_t) { + .value = 0, + .status = KZT_GUEST_FIELD_OK, + }; + check_int("lazy.observe", kzt_guest_registry_observe(registry, &observation), + KZT_GUEST_REGISTRY_ADDED); + snapshot = find_snapshot(registry, observation.link_map_addr); + generation = snapshot->generation; + kzt_guest_object_snapshot_free(snapshot); + + check_int("lazy.publish-pair", kzt_guest_registry_publish_lazy_resolver( + registry, observation.link_map_addr, generation, 0, + &resolver), 0); + check_int("lazy.find-pair", kzt_guest_registry_find_lazy_resolver( + registry, observation.link_map_addr, generation, 0, + &found), 0); + check_uintptr("lazy.find-link-map", found.guest_link_map, + resolver.guest_link_map); + check_uintptr("lazy.find-resolver", found.guest_resolver, + resolver.guest_resolver); + + resolver.guest_link_map = 0x7201; + check_not_int("lazy.reject-link-map-mismatch", + kzt_guest_registry_publish_lazy_resolver( + registry, observation.link_map_addr, generation, 0, + &resolver), 0); + resolver.guest_link_map = observation.link_map_addr; + check_not_int("lazy.reject-generation", + kzt_guest_registry_find_lazy_resolver( + registry, observation.link_map_addr, generation + 1, 0, + &found), 0); + check_not_int("lazy.reject-non-main-publish", + kzt_guest_registry_publish_lazy_resolver( + registry, observation.link_map_addr, generation, 1, + &resolver), 0); + check_not_int("lazy.reject-non-main-find", + kzt_guest_registry_find_lazy_resolver( + registry, observation.link_map_addr, generation, 1, + &found), 0); + check_int("lazy.retire", kzt_guest_registry_retire( + registry, observation.link_map_addr, generation), 0); + check_not_int("lazy.retired-invalid", + kzt_guest_registry_find_lazy_resolver( + registry, observation.link_map_addr, generation, 0, + &found), 0); + kzt_guest_registry_destroy(®istry); +} + +int main(void) +{ + test_first_and_repeat_observation_keep_generation(); + test_unknown_fields_are_completed_without_generation_change(); + test_identity_conflict_preserves_original_record(); + test_partial_observation_and_invalid_identity(); + test_query_and_dump_snapshots_are_caller_owned(); + test_diagnostics_are_opt_in_and_throttled(); + test_init_failure_creates_disabled_registry_diagnostics(); + test_cond_init_failure_is_clean(); + test_source_lease_acquire_release(); + test_retire_waits_for_source_lease(); + test_double_retire_cannot_kill_reused_generation(); + test_simultaneous_retire_has_one_owner(); + test_destroy_drains_waiter_and_inflight_api(); + test_destroyed_registry_rejects_new_observation(); + test_retired_address_gets_new_generation(); + test_lazy_resolver_publish_find_lifecycle(); + + if (failures) { + fprintf(stderr, "kzt-guest-registry: %d failure(s)\n", failures); + return 1; + } + + puts("kzt-guest-registry: all contract tests passed"); + return 0; +} diff --git a/tests/unit/kzt/test_guest_registry_concurrency.c b/tests/unit/kzt/test_guest_registry_concurrency.c new file mode 100644 index 00000000000..594e443493d --- /dev/null +++ b/tests/unit/kzt/test_guest_registry_concurrency.c @@ -0,0 +1,529 @@ +#include +#include +#include + +#include "target/i386/latx/include/kzt_guest_registry.h" + +#define SAME_LINK_THREADS 16 +#define DIFFERENT_LINK_THREADS 12 +#define SNAPSHOT_THREADS 8 +#define SNAPSHOT_ITERATIONS 64 +#define SNAPSHOT_OBJECTS 6 + +static int failures; + +static void check_true(const char *name, int condition) +{ + if (condition) { + return; + } + + fprintf(stderr, "%s: condition failed\n", name); + ++failures; +} + +static void check_int(const char *name, int got, int expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got %d expected %d\n", name, got, expected); + ++failures; +} + +static void check_ulong(const char *name, unsigned long got, + unsigned long expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got 0x%lx expected 0x%lx\n", name, got, expected); + ++failures; +} + +static void check_string(const char *name, const char *got, + const char *expected) +{ + if ((!got && !expected) || (got && expected && !strcmp(got, expected))) { + return; + } + + fprintf(stderr, "%s: got \"%s\" expected \"%s\"\n", name, + got ? got : "(null)", expected ? expected : "(null)"); + ++failures; +} + +static void thread_check_true(int *thread_failures, const char *name, + int condition) +{ + if (condition) { + return; + } + + fprintf(stderr, "%s: condition failed\n", name); + ++*thread_failures; +} + +static void thread_check_int(int *thread_failures, const char *name, + int got, int expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got %d expected %d\n", name, got, expected); + ++*thread_failures; +} + +static void thread_check_ulong(int *thread_failures, const char *name, + unsigned long got, unsigned long expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got 0x%lx expected 0x%lx\n", name, got, expected); + ++*thread_failures; +} + +static void thread_check_string(int *thread_failures, const char *name, + const char *got, const char *expected) +{ + if ((!got && !expected) || (got && expected && !strcmp(got, expected))) { + return; + } + + fprintf(stderr, "%s: got \"%s\" expected \"%s\"\n", name, + got ? got : "(null)", expected ? expected : "(null)"); + ++*thread_failures; +} + +static kzt_guest_object_observation_t make_observation( + uintptr_t link_map_addr, + uintptr_t index, + const char *path) +{ + return (kzt_guest_object_observation_t) { + .link_map_addr = link_map_addr, + .load_bias = { 0x100000 + index * 0x1000, KZT_GUEST_FIELD_OK }, + .dynamic_addr = { 0x101000 + index * 0x1000, KZT_GUEST_FIELD_OK }, + .map_start = { 0, KZT_GUEST_FIELD_UNKNOWN }, + .map_end = { 0, KZT_GUEST_FIELD_UNKNOWN }, + .namespace_id = { 0, KZT_GUEST_FIELD_UNKNOWN }, + .path = { path, KZT_GUEST_FIELD_OK }, + .soname = { NULL, KZT_GUEST_FIELD_NOT_PARSED }, + .dynamic_view_status = KZT_GUEST_FIELD_NOT_PARSED, + }; +} + +static int wait_for_barrier(pthread_barrier_t *barrier) +{ + int ret = pthread_barrier_wait(barrier); + + return ret == 0 || ret == PTHREAD_BARRIER_SERIAL_THREAD ? 0 : ret; +} + +typedef struct observe_worker { + kzt_guest_registry_t *registry; + pthread_barrier_t *barrier; + kzt_guest_object_observation_t observation; + kzt_guest_registry_result_t result; + int failures; +} observe_worker_t; + +static void *observe_worker_main(void *opaque) +{ + observe_worker_t *worker = opaque; + + if (wait_for_barrier(worker->barrier) != 0) { + worker->result = KZT_GUEST_REGISTRY_ERROR; + ++worker->failures; + return NULL; + } + + worker->result = kzt_guest_registry_observe(worker->registry, + &worker->observation); + return NULL; +} + +static void join_observe_workers(pthread_t *threads, + observe_worker_t *workers, + size_t count) +{ + size_t i; + + for (i = 0; i < count; ++i) { + check_int("pthread.join", pthread_join(threads[i], NULL), 0); + failures += workers[i].failures; + } +} + +static const kzt_guest_object_snapshot_t *find_dump_object( + const kzt_guest_registry_dump_t *dump, + uintptr_t link_map_addr) +{ + size_t i; + + for (i = 0; i < dump->count; ++i) { + if (dump->objects[i].link_map_addr == link_map_addr) { + return &dump->objects[i]; + } + } + + return NULL; +} + +static void test_concurrent_same_link_map_converges_to_one_generation(void) +{ + static const uintptr_t link_map_addr = 0x710000; + static const char path[] = "/guest/libsame-concurrent.so"; + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + pthread_barrier_t barrier; + pthread_t threads[SAME_LINK_THREADS]; + observe_worker_t workers[SAME_LINK_THREADS]; + kzt_guest_object_snapshot_t *snapshot = NULL; + kzt_guest_registry_dump_t dump = { 0 }; + kzt_guest_registry_diagnostics_t diagnostics = { 0 }; + unsigned long added = 0; + unsigned long unchanged = 0; + size_t i; + + check_true("registry.init.same", registry != NULL); + if (!registry) { + return; + } + + check_int("barrier.init.same", + pthread_barrier_init(&barrier, NULL, SAME_LINK_THREADS), 0); + + for (i = 0; i < SAME_LINK_THREADS; ++i) { + workers[i] = (observe_worker_t) { + .registry = registry, + .barrier = &barrier, + .observation = make_observation(link_map_addr, 0, path), + .result = KZT_GUEST_REGISTRY_ERROR, + }; + check_int("pthread.create.same", + pthread_create(&threads[i], NULL, observe_worker_main, + &workers[i]), 0); + } + + join_observe_workers(threads, workers, SAME_LINK_THREADS); + check_int("barrier.destroy.same", pthread_barrier_destroy(&barrier), 0); + + for (i = 0; i < SAME_LINK_THREADS; ++i) { + if (workers[i].result == KZT_GUEST_REGISTRY_ADDED) { + ++added; + } else if (workers[i].result == KZT_GUEST_REGISTRY_UNCHANGED) { + ++unchanged; + } else { + fprintf(stderr, "same-link worker %lu unexpected result %d\n", + (unsigned long)i, workers[i].result); + ++failures; + } + } + + check_ulong("same.added", added, 1); + check_ulong("same.unchanged", unchanged, SAME_LINK_THREADS - 1); + + check_int("same.find", + kzt_guest_registry_find_by_link_map(registry, link_map_addr, + &snapshot), 0); + check_true("same.snapshot", snapshot != NULL); + if (snapshot) { + check_ulong("same.snapshot.generation", snapshot->generation, 1); + check_string("same.snapshot.path", snapshot->path.value, path); + kzt_guest_object_snapshot_free(snapshot); + } + + check_int("same.dump", kzt_guest_registry_dump_snapshot(registry, &dump), + 0); + check_ulong("same.dump.count", dump.count, 1); + if (dump.count == 1) { + check_ulong("same.dump.generation", dump.objects[0].generation, 1); + check_string("same.dump.path", dump.objects[0].path.value, path); + } + kzt_guest_registry_dump_free(&dump); + + check_int("same.diagnostics", + kzt_guest_registry_get_diagnostics(registry, &diagnostics), 0); + check_ulong("same.diagnostics.observations", diagnostics.observations, + SAME_LINK_THREADS); + check_ulong("same.diagnostics.added", diagnostics.added, 1); + check_ulong("same.diagnostics.unchanged", diagnostics.unchanged, + SAME_LINK_THREADS - 1); + + kzt_guest_registry_destroy(®istry); + check_true("registry.destroy.same", registry == NULL); +} + +static void test_concurrent_different_link_maps_create_distinct_objects(void) +{ + static const uintptr_t base_link_map_addr = 0x720000; + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + pthread_barrier_t barrier; + pthread_t threads[DIFFERENT_LINK_THREADS]; + observe_worker_t workers[DIFFERENT_LINK_THREADS]; + char paths[DIFFERENT_LINK_THREADS][64]; + unsigned char seen_generation[DIFFERENT_LINK_THREADS + 1] = { 0 }; + kzt_guest_registry_dump_t dump = { 0 }; + size_t i; + + check_true("registry.init.different", registry != NULL); + if (!registry) { + return; + } + + check_int("barrier.init.different", + pthread_barrier_init(&barrier, NULL, DIFFERENT_LINK_THREADS), 0); + + for (i = 0; i < DIFFERENT_LINK_THREADS; ++i) { + uintptr_t link_map_addr = base_link_map_addr + i * 0x1000; + + snprintf(paths[i], sizeof(paths[i]), + "/guest/libdifferent-%02lu.so", (unsigned long)i); + workers[i] = (observe_worker_t) { + .registry = registry, + .barrier = &barrier, + .observation = make_observation(link_map_addr, i + 1, paths[i]), + .result = KZT_GUEST_REGISTRY_ERROR, + }; + check_int("pthread.create.different", + pthread_create(&threads[i], NULL, observe_worker_main, + &workers[i]), 0); + } + + join_observe_workers(threads, workers, DIFFERENT_LINK_THREADS); + check_int("barrier.destroy.different", + pthread_barrier_destroy(&barrier), 0); + + for (i = 0; i < DIFFERENT_LINK_THREADS; ++i) { + kzt_guest_object_snapshot_t *snapshot = NULL; + unsigned long generation; + + check_int("different.result", workers[i].result, + KZT_GUEST_REGISTRY_ADDED); + check_int("different.find", + kzt_guest_registry_find_by_link_map( + registry, workers[i].observation.link_map_addr, + &snapshot), + 0); + check_true("different.snapshot", snapshot != NULL); + if (!snapshot) { + continue; + } + + check_string("different.snapshot.path", snapshot->path.value, + paths[i]); + generation = snapshot->generation; + check_true("different.generation.range", + generation >= 1 && generation <= DIFFERENT_LINK_THREADS); + if (generation >= 1 && generation <= DIFFERENT_LINK_THREADS) { + check_true("different.generation.unique", + !seen_generation[generation]); + seen_generation[generation] = 1; + } + kzt_guest_object_snapshot_free(snapshot); + } + + for (i = 1; i <= DIFFERENT_LINK_THREADS; ++i) { + check_true("different.generation.seen", seen_generation[i]); + } + + check_int("different.dump", + kzt_guest_registry_dump_snapshot(registry, &dump), 0); + check_ulong("different.dump.count", dump.count, DIFFERENT_LINK_THREADS); + for (i = 0; i < DIFFERENT_LINK_THREADS; ++i) { + const kzt_guest_object_snapshot_t *object = find_dump_object( + &dump, base_link_map_addr + i * 0x1000); + + check_true("different.dump.object", object != NULL); + if (object) { + check_string("different.dump.path", object->path.value, paths[i]); + } + } + kzt_guest_registry_dump_free(&dump); + + kzt_guest_registry_destroy(®istry); + check_true("registry.destroy.different", registry == NULL); +} + +typedef struct snapshot_worker { + kzt_guest_registry_t *registry; + pthread_barrier_t *barrier; + uintptr_t link_map_addr; + const char *expected_path; + const char *source_path; + int failures; +} snapshot_worker_t; + +static void *snapshot_worker_main(void *opaque) +{ + snapshot_worker_t *worker = opaque; + size_t i; + + if (wait_for_barrier(worker->barrier) != 0) { + ++worker->failures; + return NULL; + } + + for (i = 0; i < SNAPSHOT_ITERATIONS; ++i) { + kzt_guest_object_snapshot_t *first = NULL; + kzt_guest_object_snapshot_t *second = NULL; + kzt_guest_registry_dump_t first_dump = { 0 }; + kzt_guest_registry_dump_t second_dump = { 0 }; + const kzt_guest_object_snapshot_t *first_object; + const kzt_guest_object_snapshot_t *second_object; + + thread_check_int( + &worker->failures, "snapshot.find.first", + kzt_guest_registry_find_by_link_map(worker->registry, + worker->link_map_addr, + &first), + 0); + thread_check_int( + &worker->failures, "snapshot.find.second", + kzt_guest_registry_find_by_link_map(worker->registry, + worker->link_map_addr, + &second), + 0); + thread_check_true(&worker->failures, "snapshot.find.first.ptr", + first != NULL); + thread_check_true(&worker->failures, "snapshot.find.second.ptr", + second != NULL); + if (first && second) { + thread_check_string(&worker->failures, "snapshot.find.path.first", + first->path.value, worker->expected_path); + thread_check_string(&worker->failures, "snapshot.find.path.second", + second->path.value, worker->expected_path); + thread_check_true(&worker->failures, + "snapshot.find.path.not-source", + first->path.value != worker->source_path); + thread_check_true(&worker->failures, + "snapshot.find.path.distinct", + first->path.value != second->path.value); + } + + thread_check_int(&worker->failures, "snapshot.dump.first", + kzt_guest_registry_dump_snapshot(worker->registry, + &first_dump), + 0); + thread_check_int(&worker->failures, "snapshot.dump.second", + kzt_guest_registry_dump_snapshot(worker->registry, + &second_dump), + 0); + thread_check_ulong(&worker->failures, "snapshot.dump.first.count", + first_dump.count, SNAPSHOT_OBJECTS); + thread_check_ulong(&worker->failures, "snapshot.dump.second.count", + second_dump.count, SNAPSHOT_OBJECTS); + + first_object = find_dump_object(&first_dump, worker->link_map_addr); + second_object = find_dump_object(&second_dump, worker->link_map_addr); + thread_check_true(&worker->failures, "snapshot.dump.first.object", + first_object != NULL); + thread_check_true(&worker->failures, "snapshot.dump.second.object", + second_object != NULL); + if (first_object && second_object) { + thread_check_string(&worker->failures, "snapshot.dump.path.first", + first_object->path.value, + worker->expected_path); + thread_check_string(&worker->failures, "snapshot.dump.path.second", + second_object->path.value, + worker->expected_path); + thread_check_true(&worker->failures, + "snapshot.dump.path.not-source", + first_object->path.value != worker->source_path); + thread_check_true(&worker->failures, + "snapshot.dump.path.distinct", + first_object->path.value != + second_object->path.value); + if (first) { + thread_check_true(&worker->failures, + "snapshot.dump.path.not-find", + first_object->path.value != + first->path.value); + } + } + + kzt_guest_registry_dump_free(&second_dump); + kzt_guest_registry_dump_free(&first_dump); + kzt_guest_object_snapshot_free(second); + kzt_guest_object_snapshot_free(first); + } + + return NULL; +} + +static void test_concurrent_query_and_dump_return_owned_snapshots(void) +{ + static const uintptr_t base_link_map_addr = 0x730000; + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + pthread_barrier_t barrier; + pthread_t threads[SNAPSHOT_THREADS]; + snapshot_worker_t workers[SNAPSHOT_THREADS]; + char paths[SNAPSHOT_OBJECTS][64]; + size_t i; + + check_true("registry.init.snapshot", registry != NULL); + if (!registry) { + return; + } + + for (i = 0; i < SNAPSHOT_OBJECTS; ++i) { + kzt_guest_object_observation_t observation; + + snprintf(paths[i], sizeof(paths[i]), "/guest/libsnapshot-%02lu.so", + (unsigned long)i); + observation = make_observation(base_link_map_addr + i * 0x1000, + i + 1, paths[i]); + check_int("snapshot.prepopulate", + kzt_guest_registry_observe(registry, &observation), + KZT_GUEST_REGISTRY_ADDED); + } + + check_int("barrier.init.snapshot", + pthread_barrier_init(&barrier, NULL, SNAPSHOT_THREADS), 0); + + for (i = 0; i < SNAPSHOT_THREADS; ++i) { + size_t object_index = i % SNAPSHOT_OBJECTS; + + workers[i] = (snapshot_worker_t) { + .registry = registry, + .barrier = &barrier, + .link_map_addr = base_link_map_addr + object_index * 0x1000, + .expected_path = paths[object_index], + .source_path = paths[object_index], + }; + check_int("pthread.create.snapshot", + pthread_create(&threads[i], NULL, snapshot_worker_main, + &workers[i]), 0); + } + + for (i = 0; i < SNAPSHOT_THREADS; ++i) { + check_int("pthread.join.snapshot", pthread_join(threads[i], NULL), 0); + failures += workers[i].failures; + } + check_int("barrier.destroy.snapshot", pthread_barrier_destroy(&barrier), + 0); + + kzt_guest_registry_destroy(®istry); + check_true("registry.destroy.snapshot", registry == NULL); +} + +int main(void) +{ + test_concurrent_same_link_map_converges_to_one_generation(); + test_concurrent_different_link_maps_create_distinct_objects(); + test_concurrent_query_and_dump_return_owned_snapshots(); + + if (failures) { + fprintf(stderr, "kzt-guest-registry-concurrency: %d failure(s)\n", + failures); + return 1; + } + + puts("kzt-guest-registry-concurrency: all tests passed"); + return 0; +} diff --git a/tests/unit/kzt/test_guest_registry_context.c b/tests/unit/kzt/test_guest_registry_context.c new file mode 100644 index 00000000000..5f9355e7227 --- /dev/null +++ b/tests/unit/kzt/test_guest_registry_context.c @@ -0,0 +1,378 @@ +#include +#include +#include + +#include "target/i386/latx/include/kzt_guest_registry_context.h" + +#ifdef __APPLE__ +typedef struct kzt_test_barrier { + pthread_mutex_t lock; + pthread_cond_t condition; + unsigned int waiting; + unsigned int participants; + unsigned int generation; +} kzt_test_barrier_t; + +#define pthread_barrier_t kzt_test_barrier_t +#define PTHREAD_BARRIER_SERIAL_THREAD 1 + +static int pthread_barrier_init(kzt_test_barrier_t *barrier, + const void *attributes, + unsigned int participants) +{ + (void)attributes; + memset(barrier, 0, sizeof(*barrier)); + barrier->participants = participants; + return pthread_mutex_init(&barrier->lock, NULL) || + pthread_cond_init(&barrier->condition, NULL); +} + +static int pthread_barrier_wait(kzt_test_barrier_t *barrier) +{ + unsigned int generation; + + pthread_mutex_lock(&barrier->lock); + generation = barrier->generation; + if (++barrier->waiting == barrier->participants) { + barrier->waiting = 0; + ++barrier->generation; + pthread_cond_broadcast(&barrier->condition); + pthread_mutex_unlock(&barrier->lock); + return PTHREAD_BARRIER_SERIAL_THREAD; + } + while (generation == barrier->generation) { + pthread_cond_wait(&barrier->condition, &barrier->lock); + } + pthread_mutex_unlock(&barrier->lock); + return 0; +} + +static int pthread_barrier_destroy(kzt_test_barrier_t *barrier) +{ + int condition_result = pthread_cond_destroy(&barrier->condition); + int mutex_result = pthread_mutex_destroy(&barrier->lock); + + return condition_result ? condition_result : mutex_result; +} +#endif + +#define CONTEXT_THREADS 16 +#define CONTEXT_RACE_ROUNDS 32 + +typedef struct registry_context_fixture { + pthread_mutex_t lock; + pthread_barrier_t barrier; + kzt_guest_registry_context_t context; + kzt_guest_registry_t *results[CONTEXT_THREADS]; + int worker_failures; +} registry_context_fixture_t; + +typedef struct registry_context_worker { + registry_context_fixture_t *fixture; + size_t index; +} registry_context_worker_t; + +static int failures; + +static void check_true(const char *name, int condition) +{ + if (!condition) { + fprintf(stderr, "%s: condition failed\n", name); + ++failures; + } +} + +static int wait_at_barrier(registry_context_fixture_t *fixture) +{ + int result = pthread_barrier_wait(&fixture->barrier); + + if (result != 0 && result != PTHREAD_BARRIER_SERIAL_THREAD) { + __atomic_add_fetch(&fixture->worker_failures, 1, __ATOMIC_RELAXED); + return -1; + } + return 0; +} + +static void *context_get_worker(void *opaque) +{ + registry_context_worker_t *worker = opaque; + + if (wait_at_barrier(worker->fixture) == 0) { + worker->fixture->results[worker->index] = + kzt_guest_registry_context_get(&worker->fixture->context, + &worker->fixture->lock); + } + return NULL; +} + +static void *context_destroy_worker(void *opaque) +{ + registry_context_fixture_t *fixture = opaque; + + if (wait_at_barrier(fixture) == 0) { + kzt_guest_registry_context_destroy(&fixture->context, + &fixture->lock); + } + return NULL; +} + +static void *context_confirm_main_head_worker(void *opaque) +{ + registry_context_fixture_t *fixture = opaque; + + if (wait_at_barrier(fixture) == 0) { + (void)kzt_guest_registry_context_confirm_main_namespace_head( + &fixture->context, &fixture->lock, 0x12340000); + } + return NULL; +} + +static void init_fixture(registry_context_fixture_t *fixture, + unsigned int participants) +{ + memset(fixture, 0, sizeof(*fixture)); + check_true("context.lock-init", + pthread_mutex_init(&fixture->lock, NULL) == 0); + check_true("context.barrier-init", + pthread_barrier_init(&fixture->barrier, NULL, + participants) == 0); +} + +static void destroy_fixture(registry_context_fixture_t *fixture) +{ + check_true("context.barrier-destroy", + pthread_barrier_destroy(&fixture->barrier) == 0); + check_true("context.lock-destroy", + pthread_mutex_destroy(&fixture->lock) == 0); +} + +static void test_concurrent_lazy_init_publishes_one_registry(void) +{ + registry_context_fixture_t fixture; + registry_context_worker_t workers[CONTEXT_THREADS]; + pthread_t threads[CONTEXT_THREADS]; + size_t i; + + init_fixture(&fixture, CONTEXT_THREADS); + for (i = 0; i < CONTEXT_THREADS; ++i) { + workers[i].fixture = &fixture; + workers[i].index = i; + check_true("context.create", + pthread_create(&threads[i], NULL, context_get_worker, + &workers[i]) == 0); + } + for (i = 0; i < CONTEXT_THREADS; ++i) { + check_true("context.join", pthread_join(threads[i], NULL) == 0); + } + check_true("context.worker-results", fixture.worker_failures == 0); + check_true("context.lazy-published", fixture.results[0] != NULL); + for (i = 1; i < CONTEXT_THREADS; ++i) { + check_true("context.single-publication", + fixture.results[i] == fixture.results[0]); + } + + kzt_guest_registry_context_destroy(&fixture.context, &fixture.lock); + check_true("context.no-reinit", + kzt_guest_registry_context_get(&fixture.context, + &fixture.lock) == NULL); + kzt_guest_registry_context_destroy(&fixture.context, &fixture.lock); + destroy_fixture(&fixture); +} + +static void test_init_and_destroy_race_closes_publication_gate(void) +{ + unsigned int round; + + for (round = 0; round < CONTEXT_RACE_ROUNDS; ++round) { + registry_context_fixture_t fixture; + registry_context_worker_t workers[CONTEXT_THREADS]; + pthread_t getters[CONTEXT_THREADS]; + pthread_t destroyer; + kzt_guest_registry_t *published = NULL; + size_t i; + + init_fixture(&fixture, CONTEXT_THREADS + 1); + for (i = 0; i < CONTEXT_THREADS; ++i) { + workers[i].fixture = &fixture; + workers[i].index = i; + check_true("race.create-getter", + pthread_create(&getters[i], NULL, context_get_worker, + &workers[i]) == 0); + } + check_true("race.create-destroyer", + pthread_create(&destroyer, NULL, context_destroy_worker, + &fixture) == 0); + for (i = 0; i < CONTEXT_THREADS; ++i) { + check_true("race.join-getter", + pthread_join(getters[i], NULL) == 0); + } + check_true("race.join-destroyer", + pthread_join(destroyer, NULL) == 0); + check_true("race.worker-results", fixture.worker_failures == 0); + + for (i = 0; i < CONTEXT_THREADS; ++i) { + if (!fixture.results[i]) { + continue; + } + if (!published) { + published = fixture.results[i]; + } + check_true("race.single-publication", + fixture.results[i] == published); + } + check_true("race.closed", + kzt_guest_registry_context_get(&fixture.context, + &fixture.lock) == NULL); + kzt_guest_registry_context_destroy(&fixture.context, &fixture.lock); + destroy_fixture(&fixture); + } +} + +static void test_failed_init_is_not_retried_on_hot_path(void) +{ + registry_context_fixture_t fixture; + + init_fixture(&fixture, 1); + kzt_guest_registry_test_set_alloc_failure_after(0); + check_true("failure.first", + kzt_guest_registry_context_get(&fixture.context, + &fixture.lock) == NULL); + kzt_guest_registry_test_set_alloc_failure_after(-1); + check_true("failure.not-retried", + kzt_guest_registry_context_get(&fixture.context, + &fixture.lock) == NULL); + kzt_guest_registry_context_destroy(&fixture.context, &fixture.lock); + destroy_fixture(&fixture); +} + +static void test_main_namespace_head_is_context_owned_and_conflict_safe(void) +{ + registry_context_fixture_t fixture; + uintptr_t head = 0; + + init_fixture(&fixture, 1); + check_true("main-head.empty", + kzt_guest_registry_context_get_main_namespace_head( + &fixture.context, &head) != 0 && head == 0); + check_true("main-head.confirm", + kzt_guest_registry_context_confirm_main_namespace_head( + &fixture.context, &fixture.lock, 0x12340000) == 0); + check_true("main-head.get", + kzt_guest_registry_context_get_main_namespace_head( + &fixture.context, &head) == 0 && head == 0x12340000); + check_true("main-head.idempotent", + kzt_guest_registry_context_confirm_main_namespace_head( + &fixture.context, &fixture.lock, 0x12340000) == 0); + check_true("main-head.conflict", + kzt_guest_registry_context_confirm_main_namespace_head( + &fixture.context, &fixture.lock, 0x56780000) != 0); + + kzt_guest_registry_context_destroy(&fixture.context, &fixture.lock); + head = 1; + check_true("main-head.cleared-on-destroy", + kzt_guest_registry_context_get_main_namespace_head( + &fixture.context, &head) != 0 && head == 0); + check_true("main-head.closed-after-destroy", + kzt_guest_registry_context_confirm_main_namespace_head( + &fixture.context, &fixture.lock, 0x12340000) != 0); + destroy_fixture(&fixture); +} + +static void test_registry_evidence_cache_requires_exact_object_identity(void) +{ + registry_context_fixture_t fixture; + kzt_guest_registry_t *registry; + kzt_guest_object_observation_t observation = { + .link_map_addr = 0x70000000, + .load_bias = { 0x700000, KZT_GUEST_FIELD_OK }, + .dynamic_addr = { 0x701000, KZT_GUEST_FIELD_OK }, + .map_start = { 0, KZT_GUEST_FIELD_UNKNOWN }, + .map_end = { 0, KZT_GUEST_FIELD_UNKNOWN }, + .namespace_id = { 0, KZT_GUEST_FIELD_OK }, + .path = { NULL, KZT_GUEST_FIELD_UNKNOWN }, + .soname = { NULL, KZT_GUEST_FIELD_NOT_PARSED }, + .dynamic_view_status = KZT_GUEST_FIELD_NOT_PARSED, + }; + + init_fixture(&fixture, 1); + registry = kzt_guest_registry_context_get(&fixture.context, + &fixture.lock); + check_true("evidence.registry", registry != NULL); + check_true("evidence.observe", + kzt_guest_registry_observe(registry, &observation) == + KZT_GUEST_REGISTRY_ADDED); + check_true("evidence.no-head-no-reuse", + !kzt_guest_registry_context_has_main_namespace_evidence( + &fixture.context, registry, observation.link_map_addr, + observation.load_bias.value, + observation.dynamic_addr.value)); + check_true("evidence.confirm-head", + kzt_guest_registry_context_confirm_main_namespace_head( + &fixture.context, &fixture.lock, 0x60000000) == 0); + check_true("evidence.exact-reuse", + kzt_guest_registry_context_has_main_namespace_evidence( + &fixture.context, registry, observation.link_map_addr, + observation.load_bias.value, + observation.dynamic_addr.value)); + check_true("evidence.wrong-load-bias", + !kzt_guest_registry_context_has_main_namespace_evidence( + &fixture.context, registry, observation.link_map_addr, + observation.load_bias.value + 0x1000, + observation.dynamic_addr.value)); + check_true("evidence.wrong-dynamic", + !kzt_guest_registry_context_has_main_namespace_evidence( + &fixture.context, registry, observation.link_map_addr, + observation.load_bias.value, + observation.dynamic_addr.value + 0x1000)); + + kzt_guest_registry_context_destroy(&fixture.context, &fixture.lock); + destroy_fixture(&fixture); +} + +static void test_main_namespace_head_cannot_publish_after_destroy(void) +{ + unsigned int round; + + for (round = 0; round < CONTEXT_RACE_ROUNDS; ++round) { + registry_context_fixture_t fixture; + pthread_t confirmer; + pthread_t destroyer; + uintptr_t head = 1; + + init_fixture(&fixture, 2); + check_true("main-head-race.create-confirmer", + pthread_create(&confirmer, NULL, + context_confirm_main_head_worker, + &fixture) == 0); + check_true("main-head-race.create-destroyer", + pthread_create(&destroyer, NULL, + context_destroy_worker, &fixture) == 0); + check_true("main-head-race.join-confirmer", + pthread_join(confirmer, NULL) == 0); + check_true("main-head-race.join-destroyer", + pthread_join(destroyer, NULL) == 0); + check_true("main-head-race.closed", + kzt_guest_registry_context_get_main_namespace_head( + &fixture.context, &head) != 0 && head == 0); + kzt_guest_registry_context_destroy(&fixture.context, &fixture.lock); + destroy_fixture(&fixture); + } +} + +int main(void) +{ + test_concurrent_lazy_init_publishes_one_registry(); + test_init_and_destroy_race_closes_publication_gate(); + test_failed_init_is_not_retried_on_hot_path(); + test_main_namespace_head_is_context_owned_and_conflict_safe(); + test_registry_evidence_cache_requires_exact_object_identity(); + test_main_namespace_head_cannot_publish_after_destroy(); + + if (failures) { + fprintf(stderr, "kzt-guest-registry-context: %d failure(s)\n", + failures); + return 1; + } + puts("kzt-guest-registry-context: all contract tests passed"); + return 0; +} diff --git a/tests/unit/kzt/test_jump_slot_route.c b/tests/unit/kzt/test_jump_slot_route.c new file mode 100644 index 00000000000..7358b9b2160 --- /dev/null +++ b/tests/unit/kzt/test_jump_slot_route.c @@ -0,0 +1,558 @@ +#include +#include + +#include "elf.h" +#include "target/i386/latx/include/kzt_jump_slot_route.h" + +struct library_s { int live; }; + +typedef struct fixture { + uintptr_t slot; + uintptr_t raw_expected; + uintptr_t bridge; + struct library_s provider; + struct library_s other; + int owner_available; + int acquire_ok; + int exact_conflict; + int bridge_ok; + int bridge_changes_owner_link_map; + int bridge_changes_owner_generation; + int source_identity_valid; + kzt_jump_slot_route_writer_status_t writer_status; + int writer_mutates; + int writer_rollback_after_competitor; + int force_cas_mismatch; + int load_error; + int cas_error; + int acquired; + int released; + int enrich_calls; + int acquire_calls; + int bridge_calls; + int source_recheck_calls; + int provider_touched_after_release; + int writer_calls; + int cas_calls; +} fixture_t; + +static int failures; + +#define CHECK(name, condition) do { \ + if (!(condition)) { \ + fprintf(stderr, "%s failed at line %d\n", name, __LINE__); \ + ++failures; \ + } \ +} while (0) + +static int enrich_base(kzt_rela_immediate_candidate_request_t *request, + void *opaque) +{ + fixture_t *f = opaque; + ++f->enrich_calls; + request->owner_match = f->owner_available ? KZT_PATCH_OWNER_MATCH : + KZT_PATCH_OWNER_UNKNOWN; + request->current_owner.known = f->owner_available; + request->current_owner.link_map_addr = f->owner_available ? 0x1234 : 0; + request->current_owner.generation = f->owner_available ? 7 : 0; + return 0; +} + +static int acquire_exact(const kzt_patch_object_ref_t *owner, + library_t *resolved_provider, + kzt_guest_library_handle_t *handle, void *opaque) +{ + fixture_t *f = opaque; + if (f->released) + ++f->provider_touched_after_release; + ++f->acquire_calls; + CHECK("acquire.owner", owner->link_map_addr == 0x1234); + if (!f->acquire_ok) return -1; + ++f->acquired; + handle->entry = (void *)1; + handle->library = f->exact_conflict ? (library_t *)&f->other : + resolved_provider; + return 0; +} + +static void release_exact(kzt_guest_library_handle_t *handle, void *opaque) +{ + fixture_t *f = opaque; + CHECK("release.handle", handle->entry != NULL); + ++f->released; + f->provider.live = 0; + memset(handle, 0, sizeof(*handle)); +} + +static int enrich_bridge(kzt_rela_immediate_candidate_request_t *request, + library_t *held_provider, void *opaque) +{ + fixture_t *f = opaque; + if (f->released) + ++f->provider_touched_after_release; + ++f->bridge_calls; + CHECK("bridge.held", held_provider == (library_t *)&f->provider); + CHECK("bridge.live", f->provider.live == 1); + if (!f->bridge_ok) return -1; + request->native_bridge_target = f->bridge; + if (f->bridge_changes_owner_link_map) + request->current_owner.link_map_addr = 0x5678; + if (f->bridge_changes_owner_generation) + request->current_owner.generation = 8; + return 0; +} + +static int validate_source_identity( + const kzt_rela_immediate_candidate_request_t *request, void *opaque) +{ + fixture_t *f = opaque; + ++f->source_recheck_calls; + CHECK("source-recheck.source", request->source.known == 1); + return f->source_identity_valid; +} + +static int load_slot(uintptr_t slot_addr, uintptr_t *value, void *opaque) +{ + fixture_t *f = opaque; + if (f->load_error) return -1; + *value = *(uintptr_t *)slot_addr; + return 0; +} + +static int cas_slot(uintptr_t slot_addr, uintptr_t *expected, + uintptr_t replacement, void *opaque) +{ + fixture_t *f = opaque; + uintptr_t *slot = (uintptr_t *)slot_addr; + ++f->cas_calls; + if (f->cas_error) return -1; + if (f->force_cas_mismatch) { + *slot = 0xfeedface; + *expected = *slot; + f->force_cas_mismatch = 0; + return 0; + } + if (*slot != *expected) { + *expected = *slot; + return 0; + } + *slot = replacement; + return 1; +} + +static kzt_jump_slot_route_writer_status_t try_writer( + const kzt_rela_immediate_candidate_request_t *request, + const kzt_patch_spike_slot_ops_t *slot_ops, void *opaque) +{ + fixture_t *f = opaque; + uintptr_t observed = 0; + if (f->released) + ++f->provider_touched_after_release; + ++f->writer_calls; + CHECK("writer.raw-preserved", + request->expected_guest_target == f->raw_expected); + CHECK("writer.provider-live", f->provider.live == 1); + if (f->writer_mutates) { + if (slot_ops->read_slot(request->slot_addr, &observed, + slot_ops->opaque) != 0 || + slot_ops->write_slot(request->slot_addr, + request->native_bridge_target, + slot_ops->opaque) != 0) { + return KZT_JUMP_SLOT_ROUTE_WRITER_ERROR; + } + } + if (f->writer_rollback_after_competitor) { + if (slot_ops->read_slot(request->slot_addr, &observed, + slot_ops->opaque) != 0 || + slot_ops->write_slot(request->slot_addr, + request->native_bridge_target, + slot_ops->opaque) != 0) { + return KZT_JUMP_SLOT_ROUTE_WRITER_ERROR; + } + f->slot = 0xfeedface; + CHECK("rollback.must-not-overwrite-competitor", + slot_ops->write_slot(request->slot_addr, observed, + slot_ops->opaque) != 0); + return KZT_JUMP_SLOT_ROUTE_WRITER_ERROR; + } + return f->writer_status; +} + +static fixture_t fixture(void) +{ + return (fixture_t){ + .slot = 0x71000010, + .raw_expected = 0x71000020, + .bridge = 0x73000030, + .provider = { .live = 1 }, + .owner_available = 1, + .acquire_ok = 1, + .bridge_ok = 1, + .writer_status = KZT_JUMP_SLOT_ROUTE_WRITER_APPLIED, + .writer_mutates = 1, + .source_identity_valid = 1, + }; +} + +static kzt_jump_slot_route_ops_t ops_for(fixture_t *f); + +static kzt_jump_slot_route_input_t input_for(fixture_t *f) +{ + return (kzt_jump_slot_route_input_t){ + .enabled = 1, + .expected_guest_target_present = 1, + .resolved_target_matches_legacy = 1, + .resolved_provider = (library_t *)&f->provider, + .request = { + .relocation_type = R_X86_64_JUMP_SLOT, + .source = { + .known = 1, + .link_map_addr = 0x1111, + .generation = 7, + }, + .slot_addr = (uintptr_t)&f->slot, + .slot_current_value_present = 1, + .slot_current_value = f->slot, + .expected_guest_target = f->raw_expected, + .legacy_target = 0x72000040, + }, + }; +} + +static void test_caller_observation_change_is_never_authorized(void) +{ + for (int expected_present = 0; expected_present < 2; + ++expected_present) { + fixture_t f = fixture(); + kzt_jump_slot_route_input_t input = input_for(&f); + kzt_jump_slot_route_ops_t ops = ops_for(&f); + kzt_jump_slot_route_result_t result; + uintptr_t competing = expected_present ? 0x71000050 : 0x71000060; + + input.expected_guest_target_present = expected_present; + if (!expected_present) + input.request.expected_guest_target = 0; + f.slot = competing; + CHECK("caller-race.call", + kzt_jump_slot_route_apply(&input, &ops, &result) == 0); + CHECK("caller-race.status", + result.status == KZT_JUMP_SLOT_ROUTE_CAS_MISMATCH); + CHECK("caller-race.final", result.final_value == competing); + CHECK("caller-race.preserve", f.slot == competing); + CHECK("caller-race.no-enrich", f.enrich_calls == 0); + CHECK("caller-race.no-acquire", f.acquire_calls == 0); + CHECK("caller-race.no-bridge", f.bridge_calls == 0); + CHECK("caller-race.no-writer", f.writer_calls == 0); + CHECK("caller-race.no-cas", f.cas_calls == 0); + CHECK("caller-race.no-fallback", + result.legacy_fallback_attempted == 0); + } +} + +static void test_same_owner_competitor_cannot_authorize_native(void) +{ + fixture_t f = fixture(); + kzt_jump_slot_route_input_t input = input_for(&f); + kzt_jump_slot_route_ops_t ops = ops_for(&f); + kzt_jump_slot_route_result_t result; + uintptr_t competing = f.slot + 0x80; + + /* enrich_base would report the same exact owner/provider for this value. */ + f.slot = competing; + CHECK("same-owner-race.call", + kzt_jump_slot_route_apply(&input, &ops, &result) == 0); + CHECK("same-owner-race.status", + result.status == KZT_JUMP_SLOT_ROUTE_CAS_MISMATCH); + CHECK("same-owner-race.final", result.final_value == competing); + CHECK("same-owner-race.preserve", f.slot == competing); + CHECK("same-owner-race.no-enrich", f.enrich_calls == 0); + CHECK("same-owner-race.no-acquire", f.acquire_calls == 0); + CHECK("same-owner-race.no-writer", f.writer_calls == 0); + CHECK("same-owner-race.no-cas", f.cas_calls == 0); +} + +static void test_lazy_zero_competitor_is_preserved_without_write(void) +{ + fixture_t f = fixture(); + kzt_jump_slot_route_input_t input = input_for(&f); + kzt_jump_slot_route_ops_t ops = ops_for(&f); + kzt_jump_slot_route_result_t result; + + input.expected_guest_target_present = 0; + input.request.expected_guest_target = 0; + f.slot = 0; + CHECK("lazy-zero.call", + kzt_jump_slot_route_apply(&input, &ops, &result) == 0); + CHECK("lazy-zero.status", + result.status == KZT_JUMP_SLOT_ROUTE_CAS_MISMATCH); + CHECK("lazy-zero.final", result.final_value == 0); + CHECK("lazy-zero.preserve", f.slot == 0); + CHECK("lazy-zero.no-enrich", f.enrich_calls == 0); + CHECK("lazy-zero.no-acquire", f.acquire_calls == 0); + CHECK("lazy-zero.no-writer", f.writer_calls == 0); + CHECK("lazy-zero.no-cas", f.cas_calls == 0); +} + +static void test_resolved_target_mismatch_uses_legacy(void) +{ + fixture_t f = fixture(); + kzt_jump_slot_route_input_t input = input_for(&f); + kzt_jump_slot_route_ops_t ops = ops_for(&f); + kzt_jump_slot_route_result_t result; + + input.resolved_target_matches_legacy = 0; + CHECK("target-mismatch.call", + kzt_jump_slot_route_apply(&input, &ops, &result) == 0); + CHECK("target-mismatch.status", + result.status == KZT_JUMP_SLOT_ROUTE_LEGACY_APPLIED); + CHECK("target-mismatch.legacy", f.slot == input.request.legacy_target); + CHECK("target-mismatch.no-enrich", f.enrich_calls == 0); + CHECK("target-mismatch.no-acquire", f.acquire_calls == 0); + CHECK("target-mismatch.no-bridge", f.bridge_calls == 0); + CHECK("target-mismatch.no-writer", f.writer_calls == 0); +} + +static void test_owner_identity_change_during_bridge_falls_back(void) +{ + for (int change_generation = 0; change_generation < 2; + ++change_generation) { + fixture_t f = fixture(); + kzt_jump_slot_route_input_t input = input_for(&f); + kzt_jump_slot_route_ops_t ops = ops_for(&f); + kzt_jump_slot_route_result_t result; + + f.bridge_changes_owner_link_map = !change_generation; + f.bridge_changes_owner_generation = change_generation; + CHECK("owner-refresh.call", + kzt_jump_slot_route_apply(&input, &ops, &result) == 0); + CHECK("owner-refresh.status", + result.status == KZT_JUMP_SLOT_ROUTE_LEGACY_APPLIED); + CHECK("owner-refresh.legacy", + f.slot == input.request.legacy_target); + CHECK("owner-refresh.acquired", f.acquired == 1); + CHECK("owner-refresh.released", f.released == 1); + CHECK("owner-refresh.bridge", f.bridge_calls == 1); + CHECK("owner-refresh.no-writer", f.writer_calls == 0); + } +} + +static kzt_jump_slot_route_ops_t ops_for(fixture_t *f) +{ + return (kzt_jump_slot_route_ops_t){ + .enrich_base = enrich_base, + .acquire_exact_provider = acquire_exact, + .release_exact_provider = release_exact, + .enrich_bridge = enrich_bridge, + .validate_source_identity = validate_source_identity, + .try_native_writer = try_writer, + .load_slot = load_slot, + .compare_exchange_slot = cas_slot, + .opaque = f, + }; +} + +static void test_generation_change_before_writer_preserves_guest(void) +{ + fixture_t f = fixture(); + kzt_jump_slot_route_input_t input = input_for(&f); + kzt_jump_slot_route_ops_t ops = ops_for(&f); + kzt_jump_slot_route_result_t result; + + input.preserve_observed_on_failure = 1; + input.request.source.known = 1; + input.request.source.link_map_addr = 0x1111; + input.request.source.generation = 7; + f.source_identity_valid = 0; + CHECK("generation-race.call", + kzt_jump_slot_route_apply(&input, &ops, &result) == 0); + CHECK("generation-race.rechecked", f.source_recheck_calls == 1 && + result.source_identity_rechecked == 1); + CHECK("generation-race.preserved", + result.status == KZT_JUMP_SLOT_ROUTE_GUEST_PRESERVED); + CHECK("generation-race.slot", f.slot == input.request.slot_current_value); + CHECK("generation-race.no-writer", f.writer_calls == 0); + CHECK("generation-race.no-cas", f.cas_calls == 0); + CHECK("generation-race.no-legacy", + result.legacy_fallback_attempted == 0); +} + +static void test_exact_provider_bridge_success(void) +{ + fixture_t f = fixture(); + kzt_jump_slot_route_input_t input = input_for(&f); + kzt_jump_slot_route_ops_t ops = ops_for(&f); + kzt_jump_slot_route_result_t result; + CHECK("success.call", kzt_jump_slot_route_apply(&input, &ops, &result) == 0); + CHECK("success.status", result.status == KZT_JUMP_SLOT_ROUTE_NATIVE_APPLIED); + CHECK("success.slot", f.slot == f.bridge); + CHECK("success.raw", result.expected_guest_target == f.raw_expected); + CHECK("success.lease", f.acquired == 1 && f.released == 1); + CHECK("success.no-post-release-deref", f.provider_touched_after_release == 0); +} + +static void test_missing_or_stale_evidence_falls_back(void) +{ + fixture_t f = fixture(); + kzt_jump_slot_route_input_t input = input_for(&f); + kzt_jump_slot_route_ops_t ops = ops_for(&f); + kzt_jump_slot_route_result_t result; + f.acquire_ok = 0; + CHECK("stale.call", kzt_jump_slot_route_apply(&input, &ops, &result) == 0); + CHECK("stale.status", result.status == KZT_JUMP_SLOT_ROUTE_LEGACY_APPLIED); + CHECK("stale.legacy", f.slot == input.request.legacy_target); + CHECK("stale.no-writer", f.writer_calls == 0); +} + +static void test_lazy_missing_evidence_preserves_observed_without_legacy_cas(void) +{ + fixture_t f = fixture(); + kzt_jump_slot_route_input_t input = input_for(&f); + kzt_jump_slot_route_ops_t ops = ops_for(&f); + kzt_jump_slot_route_result_t result; + + input.preserve_observed_on_failure = 1; + f.acquire_ok = 0; + CHECK("preserve.call", + kzt_jump_slot_route_apply(&input, &ops, &result) == 0); + CHECK("preserve.status", + result.status == KZT_JUMP_SLOT_ROUTE_GUEST_PRESERVED); + CHECK("preserve.slot", f.slot == input.request.slot_current_value); + CHECK("preserve.no-legacy", result.legacy_fallback_attempted == 0); + CHECK("preserve.no-cas", f.cas_calls == 0); +} + +static void test_conflicting_exact_provider_falls_back(void) +{ + fixture_t f = fixture(); + kzt_jump_slot_route_input_t input = input_for(&f); + kzt_jump_slot_route_ops_t ops = ops_for(&f); + kzt_jump_slot_route_result_t result; + f.exact_conflict = 1; + CHECK("conflict.call", kzt_jump_slot_route_apply(&input, &ops, &result) == 0); + CHECK("conflict.status", result.status == KZT_JUMP_SLOT_ROUTE_LEGACY_APPLIED); + CHECK("conflict.lease", f.acquired == 1 && f.released == 1); + CHECK("conflict.no-writer", f.writer_calls == 0); +} + +static void test_lazy_missing_owner_explicitly_falls_back(void) +{ + fixture_t f = fixture(); + kzt_jump_slot_route_input_t input = input_for(&f); + kzt_jump_slot_route_ops_t ops = ops_for(&f); + kzt_jump_slot_route_result_t result; + input.expected_guest_target_present = 0; + input.request.expected_guest_target = 0; + f.owner_available = 0; + CHECK("lazy.call", kzt_jump_slot_route_apply(&input, &ops, &result) == 0); + CHECK("lazy.status", result.status == KZT_JUMP_SLOT_ROUTE_LEGACY_APPLIED); + CHECK("lazy.no-acquire", f.acquired == 0); + CHECK("lazy.no-writer", f.writer_calls == 0); +} + +static void test_cas_mismatch_preserves_competing_value(void) +{ + fixture_t f = fixture(); + kzt_jump_slot_route_input_t input = input_for(&f); + kzt_jump_slot_route_ops_t ops = ops_for(&f); + kzt_jump_slot_route_result_t result; + f.acquire_ok = 0; + f.force_cas_mismatch = 1; + CHECK("cas.call", kzt_jump_slot_route_apply(&input, &ops, &result) == 0); + CHECK("cas.status", result.status == KZT_JUMP_SLOT_ROUTE_CAS_MISMATCH); + CHECK("cas.competing", f.slot == 0xfeedface); +} + +static void test_native_cas_mismatch_does_not_overwrite_competitor(void) +{ + fixture_t f = fixture(); + kzt_jump_slot_route_input_t input = input_for(&f); + kzt_jump_slot_route_ops_t ops = ops_for(&f); + kzt_jump_slot_route_result_t result; + f.force_cas_mismatch = 1; + CHECK("native-cas.call", + kzt_jump_slot_route_apply(&input, &ops, &result) == 0); + CHECK("native-cas.status", + result.status == KZT_JUMP_SLOT_ROUTE_CAS_MISMATCH); + CHECK("native-cas.competing", f.slot == 0xfeedface); + CHECK("native-cas.release", f.released == 1); + CHECK("native-cas.once", f.cas_calls == 1); + CHECK("native-cas.report-competitor", + result.selected_target == 0xfeedface && + result.final_value == 0xfeedface); +} + +static void test_rollback_cas_does_not_overwrite_competitor(void) +{ + fixture_t f = fixture(); + kzt_jump_slot_route_input_t input = input_for(&f); + kzt_jump_slot_route_ops_t ops = ops_for(&f); + kzt_jump_slot_route_result_t result; + f.writer_mutates = 0; + f.writer_rollback_after_competitor = 1; + CHECK("rollback-race.call", + kzt_jump_slot_route_apply(&input, &ops, &result) == 0); + CHECK("rollback-race.status", + result.status == KZT_JUMP_SLOT_ROUTE_CAS_MISMATCH); + CHECK("rollback-race.competing", f.slot == 0xfeedface); +} + +static void test_writer_decline_and_error_use_legacy(void) +{ + for (int error = 0; error < 2; ++error) { + fixture_t f = fixture(); + kzt_jump_slot_route_input_t input = input_for(&f); + kzt_jump_slot_route_ops_t ops = ops_for(&f); + kzt_jump_slot_route_result_t result; + f.writer_mutates = 0; + f.writer_status = error ? KZT_JUMP_SLOT_ROUTE_WRITER_ERROR : + KZT_JUMP_SLOT_ROUTE_WRITER_DECLINED; + CHECK("writer-fallback.call", + kzt_jump_slot_route_apply(&input, &ops, &result) == 0); + CHECK("writer-fallback.status", + result.status == KZT_JUMP_SLOT_ROUTE_LEGACY_APPLIED); + CHECK("writer-fallback.slot", f.slot == input.request.legacy_target); + CHECK("writer-fallback.release", f.released == 1); + CHECK("writer-fallback.no-post-release-provider-callback", + f.provider_touched_after_release == 0); + } +} + +static void test_kzt_off_bypasses_route(void) +{ + fixture_t f = fixture(); + uintptr_t initial = f.slot; + kzt_jump_slot_route_input_t input = input_for(&f); + kzt_jump_slot_route_ops_t ops = ops_for(&f); + kzt_jump_slot_route_result_t result; + input.enabled = 0; + CHECK("off.call", kzt_jump_slot_route_apply(&input, &ops, &result) == 0); + CHECK("off.status", result.status == KZT_JUMP_SLOT_ROUTE_BYPASS); + CHECK("off.untouched", f.slot == initial); + CHECK("off.no-callbacks", f.acquired == 0 && f.cas_calls == 0); +} + +int main(void) +{ + test_caller_observation_change_is_never_authorized(); + test_same_owner_competitor_cannot_authorize_native(); + test_lazy_zero_competitor_is_preserved_without_write(); + test_resolved_target_mismatch_uses_legacy(); + test_owner_identity_change_during_bridge_falls_back(); + test_generation_change_before_writer_preserves_guest(); + test_exact_provider_bridge_success(); + test_missing_or_stale_evidence_falls_back(); + test_lazy_missing_evidence_preserves_observed_without_legacy_cas(); + test_conflicting_exact_provider_falls_back(); + test_lazy_missing_owner_explicitly_falls_back(); + test_cas_mismatch_preserves_competing_value(); + test_native_cas_mismatch_does_not_overwrite_competitor(); + test_rollback_cas_does_not_overwrite_competitor(); + test_writer_decline_and_error_use_legacy(); + test_kzt_off_bypasses_route(); + if (failures) { + fprintf(stderr, "%d jump-slot route checks failed\n", failures); + return 1; + } + puts("KZT shared jump-slot route: PASS"); + return 0; +} diff --git a/tests/unit/kzt/test_observation_adapter.c b/tests/unit/kzt/test_observation_adapter.c new file mode 100644 index 00000000000..c2e9973a790 --- /dev/null +++ b/tests/unit/kzt/test_observation_adapter.c @@ -0,0 +1,2326 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "target/i386/latx/include/box64context.h" +#include "target/i386/latx/include/kzt_observation_adapter.h" +#include "target/i386/latx/include/kzt_guest_library_binding.h" + +#define EVENT_READER_READ 1 +#define EVENT_LEGACY_FLOW 2 +#define EVENT_DIAGNOSTIC 3 + +#define TEST_ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0])) + +typedef struct fake_read_failure { + uintptr_t addr; + size_t size; +} fake_read_failure_t; + +typedef struct fake_reader_memory { + uintptr_t base; + size_t size; + const fake_read_failure_t *failures; + size_t failure_count; +} fake_reader_memory_t; + +typedef struct observation_trace { + int events[128]; + size_t event_count; + int reader_calls; + int dynamic_reader_calls; + int legacy_calls; + int diagnostic_calls; + int legacy_return; + uintptr_t legacy_link_map_addr; + kzt_observation_adapter_result_t diagnostic_result; + int diagnostic_emitted; + unsigned long diagnostic_result_observations; + int dynamic_attempted; + int dynamic_cache_hit; + int dynamic_parse_return; + uintptr_t dynamic_addr; + kzt_guest_dynamic_status_t dynamic_status; + kzt_guest_dynamic_error_t dynamic_error; + size_t dynamic_entry_count; + uintptr_t dynamic_read_error_addr; + int dynamic_commit_attempted; + kzt_guest_registry_result_t dynamic_commit_result; + int dynamic_registry_emitted; + int dynamic_comparison_attempted; + int dynamic_comparison_blocking; + int dynamic_comparison_matched; +} observation_trace_t; + +typedef struct fake_callback_event { + struct link_map_x64 link_map; + char guest_name[64]; + Elf64_Dyn dynamic[4]; + fake_reader_memory_t memory; + kzt_guest_link_map_reader_ops_t ops; + observation_trace_t trace; + struct callback_barrier *barrier; + const kzt_guest_library_loader_scope_t *loader_scope; + library_t *loader_library; + kzt_guest_library_binding_result_t pending_pair_result; + int namespace_id_present; + uintptr_t namespace_id; + int map_range_present; + uintptr_t map_start; + uintptr_t map_end; +} fake_callback_event_t; + +typedef enum callback_pause_point { + CALLBACK_PAUSE_NONE = 0, + CALLBACK_PAUSE_INITIAL_READER, + CALLBACK_PAUSE_DYNAMIC_READER, + CALLBACK_PAUSE_LEGACY, +} callback_pause_point_t; + +typedef struct callback_barrier { + pthread_mutex_t lock; + pthread_cond_t cond; + callback_pause_point_t pause_point; + int reached; + int released; +} callback_barrier_t; + +static int failures; + +static void check_true(const char *name, int condition) +{ + if (condition) { + return; + } + + fprintf(stderr, "%s: condition failed\n", name); + ++failures; +} + +static void check_int(const char *name, int got, int expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got %d expected %d\n", name, got, expected); + ++failures; +} + +static void check_ulong(const char *name, unsigned long got, + unsigned long expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got %lu expected %lu\n", name, got, expected); + ++failures; +} + +static void check_uintptr(const char *name, uintptr_t got, + uintptr_t expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got 0x%lx expected 0x%lx\n", name, + (unsigned long)got, (unsigned long)expected); + ++failures; +} + +static void record_event(observation_trace_t *trace, int event) +{ + if (trace->event_count < TEST_ARRAY_SIZE(trace->events)) { + trace->events[trace->event_count++] = event; + } +} + +static int ranges_overlap(uintptr_t left_addr, size_t left_size, + uintptr_t right_addr, size_t right_size) +{ + uintptr_t left_end = left_addr + left_size; + uintptr_t right_end = right_addr + right_size; + + return left_addr < right_end && right_addr < left_end; +} + +static int fake_read_memory(uintptr_t guest_addr, void *dst, size_t size, + void *opaque) +{ + fake_callback_event_t *event = opaque; + fake_reader_memory_t *memory = &event->memory; + size_t i; + + ++event->trace.reader_calls; + if (ranges_overlap(guest_addr, size, (uintptr_t)event->dynamic, + sizeof(event->dynamic))) { + ++event->trace.dynamic_reader_calls; + } + record_event(&event->trace, EVENT_READER_READ); + + if (event->barrier) { + callback_pause_point_t point = + ranges_overlap(guest_addr, size, (uintptr_t)event->dynamic, + sizeof(event->dynamic)) + ? CALLBACK_PAUSE_DYNAMIC_READER + : CALLBACK_PAUSE_INITIAL_READER; + pthread_mutex_lock(&event->barrier->lock); + if (event->barrier->pause_point == point && + !event->barrier->reached) { + event->barrier->reached = 1; + pthread_cond_broadcast(&event->barrier->cond); + while (!event->barrier->released) + pthread_cond_wait(&event->barrier->cond, + &event->barrier->lock); + } + pthread_mutex_unlock(&event->barrier->lock); + } + + for (i = 0; i < memory->failure_count; ++i) { + if (ranges_overlap(guest_addr, size, + memory->failures[i].addr, + memory->failures[i].size)) { + return -1; + } + } + + if (guest_addr < memory->base || + size > memory->size || + guest_addr - memory->base > memory->size - size) { + return -1; + } + + memcpy(dst, (const void *)guest_addr, size); + return 0; +} + +static int fake_legacy_flow(uintptr_t link_map_addr, void *opaque) +{ + fake_callback_event_t *event = opaque; + observation_trace_t *trace = &event->trace; + + ++trace->legacy_calls; + trace->legacy_link_map_addr = link_map_addr; + record_event(trace, EVENT_LEGACY_FLOW); + if (event->loader_scope && event->loader_library) { + event->pending_pair_result = + kzt_guest_library_loader_scope_note_pair( + event->loader_scope, link_map_addr, + event->loader_library, + KZT_GUEST_LIBRARY_OBJECT_WRAPPED); + } + if (event->barrier) { + pthread_mutex_lock(&event->barrier->lock); + if (event->barrier->pause_point == CALLBACK_PAUSE_LEGACY && + !event->barrier->reached) { + event->barrier->reached = 1; + pthread_cond_broadcast(&event->barrier->cond); + while (!event->barrier->released) + pthread_cond_wait(&event->barrier->cond, + &event->barrier->lock); + } + pthread_mutex_unlock(&event->barrier->lock); + } + return trace->legacy_return; +} + +static void fake_diagnostic( + const kzt_observation_adapter_diagnostic_t *diagnostic, + void *opaque) +{ + observation_trace_t *trace = opaque; + + ++trace->diagnostic_calls; + trace->diagnostic_result = diagnostic->result; + trace->diagnostic_emitted = diagnostic->emitted; + trace->diagnostic_result_observations = + diagnostic->registry.result_observations; + trace->dynamic_attempted = diagnostic->dynamic.attempted; + trace->dynamic_cache_hit = diagnostic->dynamic.cache_hit; + trace->dynamic_parse_return = diagnostic->dynamic.parse_return; + trace->dynamic_addr = diagnostic->dynamic.dynamic_addr; + trace->dynamic_status = diagnostic->dynamic.status; + trace->dynamic_error = diagnostic->dynamic.error; + trace->dynamic_entry_count = diagnostic->dynamic.entry_count; + trace->dynamic_read_error_addr = diagnostic->dynamic.read_error_addr; + trace->dynamic_commit_attempted = diagnostic->dynamic.commit_attempted; + trace->dynamic_commit_result = diagnostic->dynamic.commit_result; + trace->dynamic_registry_emitted = diagnostic->dynamic.registry.emitted; + trace->dynamic_comparison_attempted = + diagnostic->dynamic.comparison_attempted; + trace->dynamic_comparison_blocking = + diagnostic->dynamic.comparison.blocking; + trace->dynamic_comparison_matched = + diagnostic->dynamic.comparison.matched; + record_event(trace, EVENT_DIAGNOSTIC); +} + +static void init_fake_callback_event(fake_callback_event_t *event, + const char *path, + uintptr_t load_bias) +{ + memset(event, 0, sizeof(*event)); + strcpy(event->guest_name, path); + event->dynamic[0].d_tag = DT_SYMTAB; + event->dynamic[0].d_un.d_ptr = load_bias + 0x3000; + event->dynamic[1].d_tag = DT_STRTAB; + event->dynamic[1].d_un.d_ptr = load_bias + 0x4000; + event->dynamic[2].d_tag = DT_STRSZ; + event->dynamic[2].d_un.d_val = 0x180; + event->dynamic[3].d_tag = DT_NULL; + event->dynamic[3].d_un.d_val = 0; + event->link_map.l_addr = load_bias; + event->link_map.l_name = event->guest_name; + event->link_map.l_ld = event->dynamic; + event->namespace_id_present = 1; + event->namespace_id = 7; + event->map_range_present = 1; + event->map_start = load_bias; + event->map_end = load_bias + 0x20000; + event->memory.base = (uintptr_t)&event->link_map; + event->memory.size = sizeof(*event) - + offsetof(fake_callback_event_t, link_map); + event->ops.read_memory = fake_read_memory; + event->ops.opaque = event; + event->trace.legacy_return = 77; +} + +static kzt_guest_object_observation_t make_observation(uintptr_t link_map_addr, + uintptr_t load_bias, + const char *path) +{ + return (kzt_guest_object_observation_t) { + .link_map_addr = link_map_addr, + .load_bias = { load_bias, KZT_GUEST_FIELD_OK }, + .dynamic_addr = { load_bias + 0x1000, KZT_GUEST_FIELD_OK }, + .map_start = { load_bias, KZT_GUEST_FIELD_OK }, + .map_end = { load_bias + 0x20000, KZT_GUEST_FIELD_OK }, + .namespace_id = { 7, KZT_GUEST_FIELD_OK }, + .path = { path, KZT_GUEST_FIELD_OK }, + .soname = { NULL, KZT_GUEST_FIELD_NOT_PARSED }, + .dynamic_view_status = KZT_GUEST_FIELD_NOT_PARSED, + }; +} + +static void assert_old_flow_exactly_once(const char *name, + const fake_callback_event_t *event, + int expected_return) +{ + check_int(name, event->trace.legacy_calls, 1); + check_int("old-flow.return", event->trace.legacy_return, expected_return); + check_true("old-flow.link-map", + event->trace.legacy_link_map_addr == + (uintptr_t)&event->link_map); +} + +static void assert_reader_before_old_flow(const fake_callback_event_t *event) +{ + size_t i; + size_t first_reader = TEST_ARRAY_SIZE(event->trace.events); + size_t legacy = TEST_ARRAY_SIZE(event->trace.events); + + for (i = 0; i < event->trace.event_count; ++i) { + if (event->trace.events[i] == EVENT_READER_READ && + first_reader == TEST_ARRAY_SIZE(event->trace.events)) { + first_reader = i; + } + if (event->trace.events[i] == EVENT_LEGACY_FLOW && + legacy == TEST_ARRAY_SIZE(event->trace.events)) { + legacy = i; + } + } + + check_true("adapter.reader-ran", + first_reader != TEST_ARRAY_SIZE(event->trace.events)); + check_true("adapter.before-old-flow", first_reader < legacy); +} + +static int run_adapter(fake_callback_event_t *event, + kzt_guest_registry_t *registry, + int enabled, + kzt_observation_adapter_result_t *result) +{ + kzt_observation_adapter_request_t request = { + .enabled = enabled, + .link_map_addr = (uintptr_t)&event->link_map, + .registry = registry, + .reader_ops = &event->ops, + .namespace_id_present = event->namespace_id_present, + .namespace_id = event->namespace_id, + .map_range_present = event->map_range_present, + .map_start = event->map_start, + .map_end = event->map_end, + .legacy_flow = fake_legacy_flow, + .legacy_opaque = event, + }; + + return kzt_observe_guest_object_from_callback(&request, result); +} + +static int run_adapter_with_diagnostics( + fake_callback_event_t *event, + kzt_guest_registry_t *registry, + int enabled, + int force_dynamic_compare, + kzt_observation_adapter_result_t *result) +{ + kzt_observation_adapter_request_t request = { + .enabled = enabled, + .diagnostics_enabled = 1, + .dynamic_diagnostics_force_compare = force_dynamic_compare, + .link_map_addr = (uintptr_t)&event->link_map, + .registry = registry, + .reader_ops = &event->ops, + .namespace_id_present = event->namespace_id_present, + .namespace_id = event->namespace_id, + .map_range_present = event->map_range_present, + .map_start = event->map_start, + .map_end = event->map_end, + .legacy_flow = fake_legacy_flow, + .legacy_opaque = event, + .diagnostic = fake_diagnostic, + .diagnostic_opaque = &event->trace, + }; + + return kzt_observe_guest_object_from_callback(&request, result); +} + +static int run_adapter_with_bindings( + fake_callback_event_t *event, kzt_guest_registry_t *registry, + kzt_guest_library_bindings_t *bindings, + kzt_observation_adapter_result_t *result) +{ + kzt_observation_adapter_request_t request = { + .enabled = 1, + .link_map_addr = (uintptr_t)&event->link_map, + .registry = registry, + .library_bindings = bindings, + .reader_ops = &event->ops, + .namespace_id_present = event->namespace_id_present, + .namespace_id = event->namespace_id, + .map_range_present = event->map_range_present, + .map_start = event->map_start, + .map_end = event->map_end, + .legacy_flow = fake_legacy_flow, + .legacy_opaque = event, + }; + return kzt_observe_guest_object_from_callback(&request, result); +} + +static int run_adapter_with_loader_scope( + fake_callback_event_t *event, kzt_guest_registry_t *registry, + kzt_guest_library_bindings_t *bindings, + const kzt_guest_library_loader_scope_t *loader_scope, + kzt_observation_adapter_result_t *result) +{ + kzt_observation_adapter_request_t request = { + .enabled = 1, + .link_map_addr = (uintptr_t)&event->link_map, + .registry = registry, + .library_bindings = bindings, + .loader_scope = loader_scope, + .reader_ops = &event->ops, + .namespace_id_present = event->namespace_id_present, + .namespace_id = event->namespace_id, + .map_range_present = event->map_range_present, + .map_start = event->map_start, + .map_end = event->map_end, + .legacy_flow = fake_legacy_flow, + .legacy_opaque = event, + }; + return kzt_observe_guest_object_from_callback(&request, result); +} + +static unsigned long registry_object_count(kzt_guest_registry_t *registry) +{ + kzt_guest_registry_dump_t dump = { 0 }; + unsigned long count = 0; + + if (kzt_guest_registry_dump_snapshot(registry, &dump) == 0) { + count = dump.count; + } + kzt_guest_registry_dump_free(&dump); + return count; +} + +static int registry_object_state(kzt_guest_registry_t *registry, + uintptr_t link_map_addr, + unsigned long *generation, + kzt_guest_object_state_t *state) +{ + kzt_guest_registry_dump_t dump = { 0 }; + int found = 0; + + if (kzt_guest_registry_dump_snapshot(registry, &dump) == 0) { + for (size_t i = 0; i < dump.count; ++i) { + if (dump.objects[i].link_map_addr != link_map_addr) + continue; + if (generation) *generation = dump.objects[i].generation; + if (state) *state = dump.objects[i].state; + found = 1; + break; + } + } + kzt_guest_registry_dump_free(&dump); + return found ? 0 : -1; +} + +typedef struct adapter_unload_sync { + pthread_mutex_t lock; + pthread_cond_t cond; + kzt_guest_library_bindings_t *bindings; + kzt_guest_registry_t *registry; + fake_callback_event_t *event; + library_t *library; + kzt_guest_library_binding_key_t expected_key; + int phase2_entered; + int phase2_count; + int allow_phase2_retire; + kzt_guest_library_bindings_t *hook_bindings; + kzt_guest_library_binding_key_t hook_key; + library_t *hook_library; + int hook_from_observation; + int registry_waiters; + int unload_done; + int adapter_done; + int adapter_return; + kzt_observation_adapter_result_t adapter_result; +} adapter_unload_sync_t; + +static int wait_for_callback_barrier(callback_barrier_t *barrier); +static void release_callback_barrier(callback_barrier_t *barrier); +static int wait_for_unloading_lifecycle(adapter_unload_sync_t *sync); + +static int wait_for_value_at_least(pthread_cond_t *cond, + pthread_mutex_t *lock, + int *value, int expected) +{ + struct timespec deadline; + + clock_gettime(CLOCK_MONOTONIC, &deadline); + deadline.tv_sec += 30; + while (*value < expected) { + int result = pthread_cond_clockwait( + cond, lock, CLOCK_MONOTONIC, &deadline); + if (result != 0) { + fprintf(stderr, + "adapter timed wait failed: value=%d expected=%d error=%d\n", + *value, expected, result); + return -1; + } + } + return 0; +} + +static int wait_for_adapter_outcome(adapter_unload_sync_t *sync) +{ + struct timespec deadline; + + clock_gettime(CLOCK_MONOTONIC, &deadline); + deadline.tv_sec += 30; + while (!sync->adapter_done && !sync->registry_waiters) { + int result = pthread_cond_clockwait( + &sync->cond, &sync->lock, CLOCK_MONOTONIC, &deadline); + if (result != 0) { + fprintf(stderr, + "adapter outcome timeout: done=%d registry_waiters=%d error=%d\n", + sync->adapter_done, sync->registry_waiters, result); + return -1; + } + } + return 0; +} + +static void adapter_before_registry_retire( + kzt_guest_library_bindings_t *bindings, + const kzt_guest_library_binding_key_t *key, + library_t *library, int from_observation, void *opaque) +{ + adapter_unload_sync_t *sync = opaque; + + pthread_mutex_lock(&sync->lock); + sync->hook_bindings = bindings; + sync->hook_key = *key; + sync->hook_library = library; + sync->hook_from_observation = from_observation; + sync->phase2_entered = 1; + ++sync->phase2_count; + pthread_cond_broadcast(&sync->cond); + while (!sync->allow_phase2_retire) + pthread_cond_wait(&sync->cond, &sync->lock); + pthread_mutex_unlock(&sync->lock); +} + +static void adapter_registry_retire_waiting(void *opaque) +{ + adapter_unload_sync_t *sync = opaque; + + pthread_mutex_lock(&sync->lock); + ++sync->registry_waiters; + pthread_cond_broadcast(&sync->cond); + pthread_mutex_unlock(&sync->lock); +} + +static void *adapter_unload_worker(void *opaque) +{ + adapter_unload_sync_t *sync = opaque; + + kzt_guest_library_inactivate( + sync->bindings, sync->registry, sync->library, + sync->expected_key.link_map_addr); + pthread_mutex_lock(&sync->lock); + sync->unload_done = 1; + pthread_cond_broadcast(&sync->cond); + pthread_mutex_unlock(&sync->lock); + return NULL; +} + +static void *adapter_callback_worker(void *opaque) +{ + adapter_unload_sync_t *sync = opaque; + + sync->adapter_return = run_adapter_with_bindings( + sync->event, sync->registry, sync->bindings, + &sync->adapter_result); + pthread_mutex_lock(&sync->lock); + sync->adapter_done = 1; + pthread_cond_broadcast(&sync->cond); + pthread_mutex_unlock(&sync->lock); + return NULL; +} + +typedef struct source_lease_participant { + pthread_mutex_t lock; + pthread_cond_t cond; + kzt_guest_registry_t *registry; + kzt_guest_library_binding_key_t key; + kzt_guest_registry_source_lease_t lease; + int acquired; + int release; +} source_lease_participant_t; + +static void *source_lease_participant_worker(void *opaque) +{ + source_lease_participant_t *participant = opaque; + int acquired = kzt_guest_registry_source_lease_acquire( + participant->registry, participant->key.link_map_addr, + participant->key.generation, participant->key.namespace_id, + &participant->lease) == 0; + + pthread_mutex_lock(&participant->lock); + participant->acquired = acquired ? 1 : -1; + pthread_cond_broadcast(&participant->cond); + while (acquired && !participant->release) + pthread_cond_wait(&participant->cond, &participant->lock); + pthread_mutex_unlock(&participant->lock); + if (acquired) + kzt_guest_registry_source_lease_release(&participant->lease); + return NULL; +} + +static void test_three_participant_adapter_does_not_duplicate_retire(void) +{ + struct fake_library { int value; } library = { 9 }; + fake_callback_event_t event; + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_library_bindings_t *bindings = + kzt_guest_library_bindings_init(); + kzt_observation_adapter_result_t result; + unsigned long generation = 0; + adapter_unload_sync_t sync = { + .lock = PTHREAD_MUTEX_INITIALIZER, + .cond = PTHREAD_COND_INITIALIZER, + .bindings = bindings, + .registry = registry, + .event = &event, + .library = (library_t *)&library, + }; + source_lease_participant_t lease_participant = { + .lock = PTHREAD_MUTEX_INITIALIZER, + .cond = PTHREAD_COND_INITIALIZER, + .registry = registry, + }; + pthread_t lease_thread, unload_thread; + + init_fake_callback_event(&event, "/guest/libthree-participant.so", + 0x280000); + event.namespace_id = 0; + check_int("three.track", kzt_guest_library_track( + bindings, (library_t *)&library), 0); + check_int("three.loader-pair", kzt_guest_library_note_exact_pair( + bindings, (uintptr_t)&event.link_map, + (library_t *)&library, KZT_GUEST_LIBRARY_OBJECT_WRAPPED), + KZT_GUEST_LIBRARY_BINDING_PENDING); + check_int("three.initial-adapter", run_adapter_with_bindings( + &event, registry, bindings, &result), 77); + check_int("three.generation", registry_object_state( + registry, (uintptr_t)&event.link_map, &generation, NULL), 0); + sync.expected_key = (kzt_guest_library_binding_key_t) { + .link_map_addr = (uintptr_t)&event.link_map, + .generation = generation, + .namespace_id = 0, + .namespace_kind = KZT_GUEST_LIBRARY_NAMESPACE_MAIN, + }; + lease_participant.key = sync.expected_key; + + check_int("three.lease-thread", pthread_create( + &lease_thread, NULL, source_lease_participant_worker, + &lease_participant), 0); + pthread_mutex_lock(&lease_participant.lock); + check_int("three.lease-acquired", wait_for_value_at_least( + &lease_participant.cond, &lease_participant.lock, + &lease_participant.acquired, 1), 0); + pthread_mutex_unlock(&lease_participant.lock); + + kzt_guest_library_binding_test_set_before_registry_retire( + adapter_before_registry_retire, &sync); + kzt_guest_registry_test_set_before_retire_wait( + adapter_registry_retire_waiting, &sync); + check_int("three.unload-thread", pthread_create( + &unload_thread, NULL, adapter_unload_worker, &sync), 0); + pthread_mutex_lock(&sync.lock); + check_int("three.phase2", wait_for_value_at_least( + &sync.cond, &sync.lock, &sync.phase2_entered, 1), 0); + sync.allow_phase2_retire = 1; + pthread_cond_broadcast(&sync.cond); + check_int("three.registry-wait", wait_for_value_at_least( + &sync.cond, &sync.lock, &sync.registry_waiters, 1), 0); + check_true("three.binding-blocked-by-source", !sync.unload_done); + pthread_mutex_unlock(&sync.lock); + + memset(&event.trace, 0, sizeof(event.trace)); + event.trace.legacy_return = 77; + check_int("three.third-adapter", run_adapter_with_bindings( + &event, registry, bindings, &result), 0); + check_int("three.third-disabled", result, + KZT_OBSERVATION_ADAPTER_DISABLED); + check_true("three.third-no-invalid-read", + event.trace.reader_calls == 0 && + event.trace.legacy_calls == 0); + pthread_mutex_lock(&sync.lock); + check_true("three.single-retire-owner", + sync.phase2_count == 1 && sync.registry_waiters == 1 && + !sync.unload_done); + pthread_mutex_unlock(&sync.lock); + + pthread_mutex_lock(&lease_participant.lock); + lease_participant.release = 1; + pthread_cond_broadcast(&lease_participant.cond); + pthread_mutex_unlock(&lease_participant.lock); + check_int("three.lease-join", pthread_join(lease_thread, NULL), 0); + check_int("three.unload-join", pthread_join(unload_thread, NULL), 0); + + kzt_guest_registry_test_set_before_retire_wait(NULL, NULL); + kzt_guest_library_binding_test_set_before_registry_retire(NULL, NULL); + pthread_cond_destroy(&lease_participant.cond); + pthread_mutex_destroy(&lease_participant.lock); + pthread_cond_destroy(&sync.cond); + pthread_mutex_destroy(&sync.lock); + kzt_guest_library_bindings_destroy(&bindings); + kzt_guest_registry_destroy(®istry); +} + +static void test_binding_owned_retire_excludes_adapter_retire(void) +{ + struct fake_library { int value; } library = { 2 }; + fake_callback_event_t event; + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_library_bindings_t *bindings = + kzt_guest_library_bindings_init(); + kzt_guest_registry_source_lease_t lease = { 0 }; + kzt_observation_adapter_result_t initial_result; + unsigned long generation = 0; + kzt_guest_object_state_t state = KZT_GUEST_OBJECT_DISCOVERED; + kzt_guest_library_binding_state_t lifecycle_state; + adapter_unload_sync_t sync = { + .lock = PTHREAD_MUTEX_INITIALIZER, + .cond = PTHREAD_COND_INITIALIZER, + .bindings = bindings, + .registry = registry, + .event = &event, + .library = (library_t *)&library, + }; + pthread_t unload_thread, adapter_thread; + int adapter_started = 0; + + init_fake_callback_event(&event, "/guest/libretire-owned.so", 0x180000); + event.namespace_id = 0; + check_int("retire-owned.track", kzt_guest_library_track( + bindings, (library_t *)&library), 0); + check_int("retire-owned.observation-first", + run_adapter_with_bindings( + &event, registry, bindings, &initial_result), 77); + check_int("retire-owned.initial-result", initial_result, + KZT_OBSERVATION_ADAPTER_ADDED); + check_int("retire-owned.snapshot", registry_object_state( + registry, (uintptr_t)&event.link_map, &generation, &state), + 0); + sync.expected_key = (kzt_guest_library_binding_key_t) { + .link_map_addr = (uintptr_t)&event.link_map, + .generation = generation, + .namespace_id = 0, + .namespace_kind = KZT_GUEST_LIBRARY_NAMESPACE_MAIN, + }; + check_int("retire-owned.lease", kzt_guest_registry_source_lease_acquire( + registry, sync.expected_key.link_map_addr, generation, 0, + &lease), 0); + + kzt_guest_library_binding_test_set_before_registry_retire( + adapter_before_registry_retire, &sync); + kzt_guest_registry_test_set_before_retire_wait( + adapter_registry_retire_waiting, &sync); + check_int("retire-owned.unload-thread", pthread_create( + &unload_thread, NULL, adapter_unload_worker, &sync), 0); + pthread_mutex_lock(&sync.lock); + check_int("retire-owned.phase2-barrier", wait_for_value_at_least( + &sync.cond, &sync.lock, &sync.phase2_entered, 1), 0); + check_true("retire-owned.hook-identity", + sync.hook_bindings == bindings && + sync.hook_library == (library_t *)&library && + sync.hook_from_observation && + sync.hook_key.link_map_addr == sync.expected_key.link_map_addr && + sync.hook_key.generation == sync.expected_key.generation); + pthread_mutex_unlock(&sync.lock); + + adapter_started = pthread_create( + &adapter_thread, NULL, adapter_callback_worker, &sync) == 0; + check_true("retire-owned.adapter-thread", adapter_started); + if (adapter_started) { + pthread_mutex_lock(&sync.lock); + check_int("retire-owned.adapter-outcome", + wait_for_adapter_outcome(&sync), 0); + check_true("retire-owned.adapter-did-not-retire", + sync.adapter_done && sync.registry_waiters == 0); + check_true("retire-owned.unload-still-paused", !sync.unload_done); + pthread_mutex_unlock(&sync.lock); + } + + pthread_mutex_lock(&sync.lock); + sync.allow_phase2_retire = 1; + pthread_cond_broadcast(&sync.cond); + check_int("retire-owned.binding-waits-on-lease", wait_for_value_at_least( + &sync.cond, &sync.lock, &sync.registry_waiters, 1), 0); + check_true("retire-owned.unload-not-returned", !sync.unload_done); + pthread_mutex_unlock(&sync.lock); + kzt_guest_registry_source_lease_release(&lease); + + pthread_mutex_lock(&sync.lock); + check_int("retire-owned.unload-completes", wait_for_value_at_least( + &sync.cond, &sync.lock, &sync.unload_done, 1), 0); + pthread_mutex_unlock(&sync.lock); + if (adapter_started) { + check_int("retire-owned.adapter-join", + pthread_join(adapter_thread, NULL), 0); + check_int("retire-owned.adapter-result", sync.adapter_result, + KZT_OBSERVATION_ADAPTER_DISABLED); + check_int("retire-owned.adapter-return", sync.adapter_return, 0); + } + check_int("retire-owned.unload-join", pthread_join(unload_thread, NULL), + 0); + check_int("retire-owned.final-snapshot", registry_object_state( + registry, sync.expected_key.link_map_addr, NULL, &state), 0); + check_int("retire-owned.final-dead", state, KZT_GUEST_OBJECT_DEAD); + check_int("retire-owned.lifecycle-snapshot", + kzt_guest_library_binding_test_snapshot( + bindings, (library_t *)&library, &lifecycle_state, + NULL, NULL), 0); + check_int("retire-owned.lifecycle-dead", lifecycle_state, + KZT_GUEST_LIBRARY_BINDING_DEAD); + + kzt_guest_registry_test_set_before_retire_wait(NULL, NULL); + kzt_guest_library_binding_test_set_before_registry_retire(NULL, NULL); + pthread_cond_destroy(&sync.cond); + pthread_mutex_destroy(&sync.lock); + kzt_guest_library_bindings_destroy(&bindings); + kzt_guest_registry_destroy(®istry); +} + +static void test_adapter_note_before_unload_leaves_single_retire_owner(void) +{ + struct fake_library { int value; } library = { 3 }; + fake_callback_event_t event; + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_library_bindings_t *bindings = + kzt_guest_library_bindings_init(); + kzt_guest_registry_source_lease_t lease = { 0 }; + kzt_observation_adapter_result_t result; + unsigned long generation = 0; + kzt_guest_object_state_t state = KZT_GUEST_OBJECT_DISCOVERED; + kzt_guest_library_binding_state_t lifecycle_state; + adapter_unload_sync_t sync = { + .lock = PTHREAD_MUTEX_INITIALIZER, + .cond = PTHREAD_COND_INITIALIZER, + .bindings = bindings, + .registry = registry, + .event = &event, + .library = (library_t *)&library, + }; + pthread_t unload_thread; + + init_fake_callback_event(&event, "/guest/libadapter-first.so", 0x1a0000); + event.namespace_id = 0; + check_int("adapter-first.track", kzt_guest_library_track( + bindings, (library_t *)&library), 0); + check_int("adapter-first.callback", run_adapter_with_bindings( + &event, registry, bindings, &result), 77); + check_int("adapter-first.result", result, KZT_OBSERVATION_ADAPTER_ADDED); + check_int("adapter-first.live", registry_object_state( + registry, (uintptr_t)&event.link_map, &generation, &state), + 0); + check_true("adapter-first.note-did-not-retire", + state != KZT_GUEST_OBJECT_UNLOADING && + state != KZT_GUEST_OBJECT_DEAD); + sync.expected_key = (kzt_guest_library_binding_key_t) { + .link_map_addr = (uintptr_t)&event.link_map, + .generation = generation, + .namespace_id = 0, + .namespace_kind = KZT_GUEST_LIBRARY_NAMESPACE_MAIN, + }; + check_int("adapter-first.lease", kzt_guest_registry_source_lease_acquire( + registry, sync.expected_key.link_map_addr, generation, 0, + &lease), 0); + kzt_guest_library_binding_test_set_before_registry_retire( + adapter_before_registry_retire, &sync); + kzt_guest_registry_test_set_before_retire_wait( + adapter_registry_retire_waiting, &sync); + check_int("adapter-first.unload-thread", pthread_create( + &unload_thread, NULL, adapter_unload_worker, &sync), 0); + pthread_mutex_lock(&sync.lock); + check_int("adapter-first.phase2", wait_for_value_at_least( + &sync.cond, &sync.lock, &sync.phase2_entered, 1), 0); + check_true("adapter-first.single-binding-owner", + sync.hook_library == (library_t *)&library && + sync.hook_from_observation && + sync.hook_key.generation == generation); + sync.allow_phase2_retire = 1; + pthread_cond_broadcast(&sync.cond); + check_int("adapter-first.retire-wait", wait_for_value_at_least( + &sync.cond, &sync.lock, &sync.registry_waiters, 1), 0); + check_true("adapter-first.unload-blocked", !sync.unload_done); + pthread_mutex_unlock(&sync.lock); + kzt_guest_registry_source_lease_release(&lease); + check_int("adapter-first.join", pthread_join(unload_thread, NULL), 0); + check_int("adapter-first.final", registry_object_state( + registry, sync.expected_key.link_map_addr, NULL, &state), 0); + check_int("adapter-first.dead", state, KZT_GUEST_OBJECT_DEAD); + check_int("adapter-first.lifecycle", kzt_guest_library_binding_test_snapshot( + bindings, (library_t *)&library, &lifecycle_state, + NULL, NULL), 0); + check_int("adapter-first.lifecycle-dead", lifecycle_state, + KZT_GUEST_LIBRARY_BINDING_DEAD); + + kzt_guest_registry_test_set_before_retire_wait(NULL, NULL); + kzt_guest_library_binding_test_set_before_registry_retire(NULL, NULL); + pthread_cond_destroy(&sync.cond); + pthread_mutex_destroy(&sync.lock); + kzt_guest_library_bindings_destroy(&bindings); + kzt_guest_registry_destroy(®istry); +} + +static void test_cancelled_pending_without_owner_is_adapter_retired(void) +{ + struct fake_library { int value; } library = { 4 }; + fake_callback_event_t event; + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_library_bindings_t *bindings = + kzt_guest_library_bindings_init(); + kzt_observation_adapter_result_t result; + kzt_guest_object_state_t state = KZT_GUEST_OBJECT_DISCOVERED; + kzt_guest_library_binding_state_t lifecycle_state; + size_t active_pending = 1; + callback_barrier_t barrier = { + .lock = PTHREAD_MUTEX_INITIALIZER, + .cond = PTHREAD_COND_INITIALIZER, + .pause_point = CALLBACK_PAUSE_INITIAL_READER, + }; + adapter_unload_sync_t sync = { + .lock = PTHREAD_MUTEX_INITIALIZER, + .cond = PTHREAD_COND_INITIALIZER, + .bindings = bindings, + .registry = registry, + .event = &event, + .library = (library_t *)&library, + }; + pthread_t callback_thread, unload_thread; + + init_fake_callback_event(&event, "/guest/libcancelled-pending.so", + 0x1c0000); + event.namespace_id = 0; + check_int("cancelled-pending.track", kzt_guest_library_track( + bindings, (library_t *)&library), 0); + check_int("cancelled-pending.pair", kzt_guest_library_note_exact_pair( + bindings, (uintptr_t)&event.link_map, + (library_t *)&library, + KZT_GUEST_LIBRARY_OBJECT_WRAPPED), + KZT_GUEST_LIBRARY_BINDING_PENDING); + sync.expected_key.link_map_addr = (uintptr_t)&event.link_map; + event.barrier = &barrier; + check_int("cancelled-pending.callback-thread", pthread_create( + &callback_thread, NULL, adapter_callback_worker, &sync), 0); + check_int("cancelled-pending.reader-barrier", + wait_for_callback_barrier(&barrier), 0); + check_int("cancelled-pending.unload-thread", pthread_create( + &unload_thread, NULL, adapter_unload_worker, &sync), 0); + check_int("cancelled-pending.unloading", + wait_for_unloading_lifecycle(&sync), 0); + release_callback_barrier(&barrier); + check_int("cancelled-pending.callback-join", + pthread_join(callback_thread, NULL), 0); + check_int("cancelled-pending.unload-join", + pthread_join(unload_thread, NULL), 0); + result = sync.adapter_result; + check_int("cancelled-pending.callback", sync.adapter_return, 77); + check_int("cancelled-pending.result", result, + KZT_OBSERVATION_ADAPTER_ADDED); + check_int("cancelled-pending.snapshot", registry_object_state( + registry, (uintptr_t)&event.link_map, NULL, &state), 0); + check_int("cancelled-pending.adapter-retired", state, + KZT_GUEST_OBJECT_DEAD); + check_int("cancelled-pending.binding-state", + kzt_guest_library_binding_test_snapshot( + bindings, (library_t *)&library, &lifecycle_state, + &active_pending, NULL), 0); + check_true("cancelled-pending.no-owner-left", + lifecycle_state == KZT_GUEST_LIBRARY_BINDING_DEAD && + active_pending == 0); + + kzt_guest_library_bindings_destroy(&bindings); + kzt_guest_registry_destroy(®istry); + pthread_cond_destroy(&sync.cond); + pthread_mutex_destroy(&sync.lock); + pthread_cond_destroy(&barrier.cond); + pthread_mutex_destroy(&barrier.lock); +} + +static int wait_for_callback_barrier(callback_barrier_t *barrier) +{ + struct timespec deadline; + + clock_gettime(CLOCK_MONOTONIC, &deadline); + deadline.tv_sec += 30; + pthread_mutex_lock(&barrier->lock); + while (!barrier->reached) { + int result = pthread_cond_clockwait( + &barrier->cond, &barrier->lock, CLOCK_MONOTONIC, &deadline); + if (result != 0) { + fprintf(stderr, "callback barrier timeout: point=%d error=%d\n", + barrier->pause_point, result); + pthread_mutex_unlock(&barrier->lock); + return -1; + } + } + pthread_mutex_unlock(&barrier->lock); + return 0; +} + +static void release_callback_barrier(callback_barrier_t *barrier) +{ + pthread_mutex_lock(&barrier->lock); + barrier->released = 1; + pthread_cond_broadcast(&barrier->cond); + pthread_mutex_unlock(&barrier->lock); +} + +static int wait_for_unloading_lifecycle(adapter_unload_sync_t *sync) +{ + struct timespec deadline; + + clock_gettime(CLOCK_MONOTONIC, &deadline); + deadline.tv_sec += 30; + for (;;) { + kzt_guest_library_binding_state_t state; + if (kzt_guest_library_binding_test_snapshot( + sync->bindings, sync->library, &state, NULL, NULL) == 0 && + state != KZT_GUEST_LIBRARY_BINDING_LIVE) + return state == KZT_GUEST_LIBRARY_BINDING_UNLOADING ? 0 : 1; + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + if (now.tv_sec > deadline.tv_sec || + (now.tv_sec == deadline.tv_sec && now.tv_nsec >= deadline.tv_nsec)) { + fprintf(stderr, "lifecycle did not leave LIVE before timeout\n"); + return -1; + } + sched_yield(); + } +} + +static int unload_stays_blocked(adapter_unload_sync_t *sync) +{ + struct timespec deadline; + int result = 0; + + clock_gettime(CLOCK_MONOTONIC, &deadline); + deadline.tv_nsec += 100 * 1000 * 1000; + if (deadline.tv_nsec >= 1000 * 1000 * 1000) { + ++deadline.tv_sec; + deadline.tv_nsec -= 1000 * 1000 * 1000; + } + pthread_mutex_lock(&sync->lock); + while (!sync->unload_done && result == 0) + result = pthread_cond_clockwait( + &sync->cond, &sync->lock, CLOCK_MONOTONIC, &deadline); + int blocked = !sync->unload_done; + pthread_mutex_unlock(&sync->lock); + return blocked; +} + +static void test_exact_retire_owner_uses_complete_key(void) +{ + struct fake_library { int value; } library = { 5 }; + fake_callback_event_t event; + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_library_bindings_t *bindings = + kzt_guest_library_bindings_init(); + kzt_observation_adapter_result_t result; + unsigned long generation = 0; + adapter_unload_sync_t sync = { + .lock = PTHREAD_MUTEX_INITIALIZER, + .cond = PTHREAD_COND_INITIALIZER, + .bindings = bindings, + .registry = registry, + .event = &event, + .library = (library_t *)&library, + }; + pthread_t unload_thread; + + init_fake_callback_event(&event, "/guest/libexact-owner.so", 0x1e0000); + event.namespace_id = 0; + check_int("exact-owner.track", kzt_guest_library_track( + bindings, (library_t *)&library), 0); + check_int("exact-owner.pair", kzt_guest_library_note_exact_pair( + bindings, (uintptr_t)&event.link_map, + (library_t *)&library, KZT_GUEST_LIBRARY_OBJECT_WRAPPED), + KZT_GUEST_LIBRARY_BINDING_PENDING); + check_int("exact-owner.initial", run_adapter_with_bindings( + &event, registry, bindings, &result), 77); + check_int("exact-owner.snapshot", registry_object_state( + registry, (uintptr_t)&event.link_map, &generation, NULL), 0); + sync.expected_key = (kzt_guest_library_binding_key_t) { + .link_map_addr = (uintptr_t)&event.link_map, + .generation = generation, + .namespace_id = 0, + .namespace_kind = KZT_GUEST_LIBRARY_NAMESPACE_MAIN, + }; + kzt_guest_library_binding_test_set_before_registry_retire( + adapter_before_registry_retire, &sync); + check_int("exact-owner.unload-thread", pthread_create( + &unload_thread, NULL, adapter_unload_worker, &sync), 0); + pthread_mutex_lock(&sync.lock); + check_int("exact-owner.phase2", wait_for_value_at_least( + &sync.cond, &sync.lock, &sync.phase2_entered, 1), 0); + pthread_mutex_unlock(&sync.lock); + + check_int("exact-owner.same-generation", + kzt_guest_library_note_observation(bindings, + &sync.expected_key), + KZT_GUEST_LIBRARY_BINDING_RETIRE_OWNED); + kzt_guest_library_binding_key_t other = sync.expected_key; + ++other.generation; + check_true("exact-owner.other-generation-not-owned", + kzt_guest_library_note_observation(bindings, &other) != + KZT_GUEST_LIBRARY_BINDING_RETIRE_OWNED); + + pthread_mutex_lock(&sync.lock); + sync.allow_phase2_retire = 1; + pthread_cond_broadcast(&sync.cond); + pthread_mutex_unlock(&sync.lock); + check_int("exact-owner.join", pthread_join(unload_thread, NULL), 0); + kzt_guest_library_binding_test_set_before_registry_retire(NULL, NULL); + pthread_cond_destroy(&sync.cond); + pthread_mutex_destroy(&sync.lock); + kzt_guest_library_bindings_destroy(&bindings); + kzt_guest_registry_destroy(®istry); +} + +static void run_callback_lifetime_wait_test(callback_pause_point_t point, + const char *name) +{ + struct fake_library { int value; } library = { 6 }; + fake_callback_event_t event; + callback_barrier_t barrier = { + .lock = PTHREAD_MUTEX_INITIALIZER, + .cond = PTHREAD_COND_INITIALIZER, + .pause_point = point, + }; + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_library_bindings_t *bindings = + kzt_guest_library_bindings_init(); + kzt_observation_adapter_result_t result; + adapter_unload_sync_t sync = { + .lock = PTHREAD_MUTEX_INITIALIZER, + .cond = PTHREAD_COND_INITIALIZER, + .bindings = bindings, + .registry = registry, + .event = &event, + .library = (library_t *)&library, + }; + pthread_t callback_thread, unload_thread; + + init_fake_callback_event(&event, "/guest/libcallback-lifetime.so", + 0x200000 + (uintptr_t)point * 0x10000); + event.namespace_id = 0; + check_int(name, kzt_guest_library_track(bindings, + (library_t *)&library), 0); + check_int("callback-lifetime.pair", kzt_guest_library_note_exact_pair( + bindings, (uintptr_t)&event.link_map, + (library_t *)&library, KZT_GUEST_LIBRARY_OBJECT_WRAPPED), + KZT_GUEST_LIBRARY_BINDING_PENDING); + check_int("callback-lifetime.initial", run_adapter_with_bindings( + &event, registry, bindings, &result), 77); + memset(&event.trace, 0, sizeof(event.trace)); + event.trace.legacy_return = 77; + event.barrier = &barrier; + check_int("callback-lifetime.callback-thread", pthread_create( + &callback_thread, NULL, adapter_callback_worker, &sync), 0); + check_int("callback-lifetime.barrier", wait_for_callback_barrier(&barrier), + 0); + check_int("callback-lifetime.unload-thread", pthread_create( + &unload_thread, NULL, adapter_unload_worker, &sync), 0); + check_int("callback-lifetime.unloading", + wait_for_unloading_lifecycle(&sync), 0); + check_true("callback-lifetime.unload-waits", + unload_stays_blocked(&sync)); + release_callback_barrier(&barrier); + check_int("callback-lifetime.callback-join", + pthread_join(callback_thread, NULL), 0); + check_int("callback-lifetime.unload-join", + pthread_join(unload_thread, NULL), 0); + check_int("callback-lifetime.legacy-once", event.trace.legacy_calls, 1); + + pthread_cond_destroy(&sync.cond); + pthread_mutex_destroy(&sync.lock); + pthread_cond_destroy(&barrier.cond); + pthread_mutex_destroy(&barrier.lock); + kzt_guest_library_bindings_destroy(&bindings); + kzt_guest_registry_destroy(®istry); +} + +static void test_callback_lifetime_covers_all_guest_work(void) +{ + run_callback_lifetime_wait_test(CALLBACK_PAUSE_INITIAL_READER, + "callback-lifetime.initial-reader"); + run_callback_lifetime_wait_test(CALLBACK_PAUSE_DYNAMIC_READER, + "callback-lifetime.dynamic-reader"); + run_callback_lifetime_wait_test(CALLBACK_PAUSE_LEGACY, + "callback-lifetime.legacy"); +} + +static void test_unload_winner_rejects_late_callback_before_any_work(void) +{ + struct fake_library { int value; } library = { 7 }; + fake_callback_event_t event; + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_library_bindings_t *bindings = + kzt_guest_library_bindings_init(); + kzt_observation_adapter_result_t result; + + init_fake_callback_event(&event, "/guest/libunload-wins.so", 0x240000); + event.namespace_id = 0; + check_int("unload-wins.track", kzt_guest_library_track( + bindings, (library_t *)&library), 0); + check_int("unload-wins.pair", kzt_guest_library_note_exact_pair( + bindings, (uintptr_t)&event.link_map, + (library_t *)&library, KZT_GUEST_LIBRARY_OBJECT_WRAPPED), + KZT_GUEST_LIBRARY_BINDING_PENDING); + check_int("unload-wins.initial", run_adapter_with_bindings( + &event, registry, bindings, &result), 77); + kzt_guest_library_inactivate(bindings, registry, + (library_t *)&library, + (uintptr_t)&event.link_map); + memset(&event.trace, 0, sizeof(event.trace)); + event.trace.legacy_return = 77; + check_int("unload-wins.callback", run_adapter_with_bindings( + &event, registry, bindings, &result), 0); + check_int("unload-wins.result", result, + KZT_OBSERVATION_ADAPTER_DISABLED); + check_true("unload-wins.no-work", + event.trace.reader_calls == 0 && + event.trace.legacy_calls == 0 && + event.trace.diagnostic_calls == 0); + + kzt_guest_library_bindings_destroy(&bindings); + kzt_guest_registry_destroy(®istry); +} + +static void test_callback_gate_allocation_failure_is_safe_fail_open(void) +{ + struct fake_library { int value; } library = { 8 }; + fake_callback_event_t event; + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_library_bindings_t *bindings = + kzt_guest_library_bindings_init(); + kzt_observation_adapter_result_t result; + + init_fake_callback_event(&event, "/guest/libgate-alloc-fail.so", 0x260000); + event.namespace_id = 0; + check_int("gate-alloc-fail.track", kzt_guest_library_track( + bindings, (library_t *)&library), 0); + check_int("gate-alloc-fail.pair", kzt_guest_library_note_exact_pair( + bindings, (uintptr_t)&event.link_map, + (library_t *)&library, KZT_GUEST_LIBRARY_OBJECT_WRAPPED), + KZT_GUEST_LIBRARY_BINDING_PENDING); + kzt_guest_library_binding_test_set_alloc_failure_after(0); + check_int("gate-alloc-fail.callback", run_adapter_with_bindings( + &event, registry, bindings, &result), 77); + check_int("gate-alloc-fail.legacy", event.trace.legacy_calls, 1); + kzt_guest_library_inactivate(bindings, registry, + (library_t *)&library, + (uintptr_t)&event.link_map); + memset(&event.trace, 0, sizeof(event.trace)); + event.trace.legacy_return = 77; + check_int("gate-alloc-fail.late-callback", run_adapter_with_bindings( + &event, registry, bindings, &result), 0); + check_true("gate-alloc-fail.late-no-work", + event.trace.reader_calls == 0 && + event.trace.legacy_calls == 0); + kzt_guest_library_binding_test_set_alloc_failure_after(-1); + + kzt_guest_library_bindings_destroy(&bindings); + kzt_guest_registry_destroy(®istry); +} + +static void run_adapter_address_reuse_test(int force_gate_alloc_failure, + const char *name) +{ + struct fake_library { int value; } old_library = { 10 }, + new_library = { 11 }; + fake_callback_event_t event; + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_library_bindings_t *bindings = + kzt_guest_library_bindings_init(); + kzt_guest_library_loader_scope_t scope = { 0 }; + kzt_observation_adapter_result_t result; + + init_fake_callback_event(&event, "/guest/libadapter-reuse.so", 0x2a0000); + event.namespace_id = 0; + check_int(name, kzt_guest_library_track( + bindings, (library_t *)&old_library), 0); + check_int("adapter-reuse old pair", + kzt_guest_library_publish_loader_pair( + bindings, (uintptr_t)&event.link_map, + (library_t *)&old_library, + KZT_GUEST_LIBRARY_OBJECT_WRAPPED), + KZT_GUEST_LIBRARY_BINDING_PENDING); + if (!force_gate_alloc_failure) { + check_int("adapter-reuse seed gate", run_adapter_with_bindings( + &event, registry, bindings, &result), 77); + } else { + kzt_guest_library_binding_test_set_alloc_failure_after(0); + } + kzt_guest_library_inactivate(bindings, registry, + (library_t *)&old_library, + (uintptr_t)&event.link_map); + kzt_guest_library_binding_test_set_alloc_failure_after(-1); + + memset(&event.trace, 0, sizeof(event.trace)); + event.trace.legacy_return = 77; + check_int("adapter-reuse stale return", run_adapter_with_bindings( + &event, registry, bindings, &result), 0); + check_int("adapter-reuse stale disabled", result, + KZT_OBSERVATION_ADAPTER_DISABLED); + check_true("adapter-reuse stale no read", + event.trace.reader_calls == 0 && + event.trace.legacy_calls == 0); + + check_int("adapter-reuse new track", kzt_guest_library_track( + bindings, (library_t *)&new_library), 0); + check_int("adapter-reuse scope", kzt_guest_library_loader_scope_begin( + bindings, &scope), 0); + check_int("adapter-reuse current return", run_adapter_with_loader_scope( + &event, registry, bindings, &scope, &result), 77); + check_true("adapter-reuse current read", + event.trace.reader_calls > 0 && + event.trace.legacy_calls == 1); + check_true("adapter-reuse pending pair", + kzt_guest_library_loader_scope_note_pair( + &scope, (uintptr_t)&event.link_map, + (library_t *)&new_library, + KZT_GUEST_LIBRARY_OBJECT_WRAPPED) != + KZT_GUEST_LIBRARY_BINDING_ERROR); + check_true("adapter-reuse new pair", + kzt_guest_library_loader_scope_publish_pair( + &scope, (uintptr_t)&event.link_map, + (library_t *)&new_library, + KZT_GUEST_LIBRARY_OBJECT_WRAPPED) != + KZT_GUEST_LIBRARY_BINDING_ERROR); + kzt_guest_library_loader_scope_end(&scope); + + kzt_guest_library_bindings_destroy(&bindings); + kzt_guest_registry_destroy(®istry); +} + +static void test_adapter_address_reuse_requires_loader_causality(void) +{ + run_adapter_address_reuse_test(0, "adapter-reuse normal"); + run_adapter_address_reuse_test(1, "adapter-reuse fallback"); +} + +static void run_adapter_pending_then_wrapper_result(int wrapper_success) +{ + struct fake_library { int value; } old_library = { 12 }, + new_library = { 13 }; + fake_callback_event_t event; + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_library_bindings_t *bindings = + kzt_guest_library_bindings_init(); + kzt_guest_library_loader_scope_t scope = { 0 }; + kzt_guest_library_callback_access_t probe = { 0 }; + kzt_observation_adapter_result_t result; + uintptr_t map; + + init_fake_callback_event(&event, "/guest/libpending-wrapper.so", 0x2b0000); + event.namespace_id = 0; + map = (uintptr_t)&event.link_map; + check_int("pending-wrapper old track", kzt_guest_library_track( + bindings, (library_t *)&old_library), 0); + check_int("pending-wrapper old pair", kzt_guest_library_publish_loader_pair( + bindings, map, (library_t *)&old_library, + KZT_GUEST_LIBRARY_OBJECT_WRAPPED), + KZT_GUEST_LIBRARY_BINDING_PENDING); + kzt_guest_library_inactivate(bindings, registry, + (library_t *)&old_library, map); + check_int("pending-wrapper new track", kzt_guest_library_track( + bindings, (library_t *)&new_library), 0); + check_int("pending-wrapper scope", kzt_guest_library_loader_scope_begin( + bindings, &scope), 0); + event.loader_scope = &scope; + event.loader_library = (library_t *)&new_library; + check_int("pending-wrapper adapter", run_adapter_with_loader_scope( + &event, registry, bindings, &scope, &result), 77); + check_true("pending-wrapper callback observed", + event.pending_pair_result == KZT_GUEST_LIBRARY_BINDING_PENDING || + event.pending_pair_result == KZT_GUEST_LIBRARY_BINDING_ADDED || + event.pending_pair_result == KZT_GUEST_LIBRARY_BINDING_UNCHANGED); + check_true("pending-wrapper not yet reopened", + kzt_guest_library_callback_access_begin( + bindings, map, &probe) != 0); + + if (wrapper_success) { + check_true("pending-wrapper final publish", + kzt_guest_library_loader_scope_publish_pair( + &scope, map, (library_t *)&new_library, + KZT_GUEST_LIBRARY_OBJECT_WRAPPED) != + KZT_GUEST_LIBRARY_BINDING_ERROR); + } + kzt_guest_library_loader_scope_end(&scope); + if (wrapper_success) { + check_int("pending-wrapper success reopened", + kzt_guest_library_callback_access_begin( + bindings, map, &probe), 0); + kzt_guest_library_callback_access_end(&probe); + } else { + check_true("pending-wrapper failure closed", + kzt_guest_library_callback_access_begin( + bindings, map, &probe) != 0); + } + kzt_guest_library_bindings_destroy(&bindings); + kzt_guest_registry_destroy(®istry); +} + +static void test_callback_pending_observation_waits_for_wrapper_success(void) +{ + run_adapter_pending_then_wrapper_result(0); + run_adapter_pending_then_wrapper_result(1); +} + +static void assert_dynamic_view_complete(const char *name, + kzt_guest_registry_t *registry, + const fake_callback_event_t *event, + unsigned long expected_generation) +{ + kzt_guest_dynamic_view_t view = { 0 }; + kzt_guest_field_status_t status = KZT_GUEST_FIELD_UNKNOWN; + unsigned long generation = 0; + + check_int(name, kzt_guest_registry_find_dynamic_view( + registry, (uintptr_t)&event->link_map, &view, &status, + &generation), 0); + check_int("dynamic.status", status, KZT_GUEST_FIELD_OK); + check_ulong("dynamic.generation", generation, expected_generation); + check_uintptr("dynamic.addr", view.dynamic_addr, + (uintptr_t)event->dynamic); + check_uintptr("dynamic.load-bias", view.load_bias, + event->link_map.l_addr); + check_int("dynamic.view-status", view.status, + KZT_GUEST_DYNAMIC_COMPLETE); + check_ulong("dynamic.entry-count", view.entry_count, 3); + check_int("dynamic.has-null", view.has_null, 1); + check_true("dynamic.symtab.present", view.symtab.present); + check_uintptr("dynamic.symtab", view.symtab.value, + event->link_map.l_addr + 0x3000); + check_int("dynamic.symtab.semantics", view.symtab.address_semantics, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS); + check_true("dynamic.strtab.present", view.strtab.present); + check_uintptr("dynamic.strtab", view.strtab.value, + event->link_map.l_addr + 0x4000); + check_int("dynamic.strtab.semantics", view.strtab.address_semantics, + KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS); + check_true("dynamic.strsz.present", view.strsz.present); + check_ulong("dynamic.strsz", view.strsz.value, 0x180); + check_int("dynamic.strsz.semantics", view.strsz.address_semantics, + KZT_GUEST_DYNAMIC_SCALAR); +} + +static void assert_dynamic_view_read_error(const char *name, + kzt_guest_registry_t *registry, + const fake_callback_event_t *event) +{ + kzt_guest_dynamic_view_t view = { 0 }; + kzt_guest_field_status_t status = KZT_GUEST_FIELD_UNKNOWN; + unsigned long generation = 0; + + check_int(name, kzt_guest_registry_find_dynamic_view( + registry, (uintptr_t)&event->link_map, &view, &status, + &generation), 0); + check_int("dynamic.read-error.status", status, + KZT_GUEST_FIELD_READ_ERROR); + check_ulong("dynamic.read-error.generation", generation, 1); + check_uintptr("dynamic.read-error.addr", view.dynamic_addr, + (uintptr_t)event->dynamic); + check_int("dynamic.read-error.view-status", view.status, + KZT_GUEST_DYNAMIC_READ_ERROR); + check_ulong("dynamic.read-error.entry-count", view.entry_count, 1); + check_int("dynamic.read-error.no-null", view.has_null, 0); +} + +static void assert_dynamic_view_not_parsed(const char *name, + kzt_guest_registry_t *registry, + const fake_callback_event_t *event) +{ + kzt_guest_dynamic_view_t view = { 0 }; + kzt_guest_field_status_t status = KZT_GUEST_FIELD_UNKNOWN; + unsigned long generation = 0; + + check_int(name, kzt_guest_registry_find_dynamic_view( + registry, (uintptr_t)&event->link_map, &view, &status, + &generation), 0); + check_int("dynamic.not-parsed.status", status, + KZT_GUEST_FIELD_NOT_PARSED); + check_ulong("dynamic.not-parsed.generation", generation, 1); + check_uintptr("dynamic.not-parsed.addr", view.dynamic_addr, 0); +} + +static void test_active_observation_adds_object_and_preserves_old_flow(void) +{ + fake_callback_event_t event; + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_observation_adapter_result_t result = KZT_OBSERVATION_ADAPTER_DISABLED; + int ret; + + check_true("registry.init", registry != NULL); + if (!registry) { + return; + } + + init_fake_callback_event(&event, "/guest/libactive.so", 0x100000); + event.trace.legacy_return = 23; + + ret = run_adapter(&event, registry, 1, &result); + + check_int("active.return", ret, 23); + check_int("active.result", result, KZT_OBSERVATION_ADAPTER_ADDED); + assert_old_flow_exactly_once("active.old-flow", &event, 23); + assert_reader_before_old_flow(&event); + check_ulong("active.registry-count", registry_object_count(registry), 1); + + kzt_guest_registry_destroy(®istry); +} + +static void test_exact_pair_retries_after_transient_observation_failure(void) +{ + struct fake_library { int value; } library = { 1 }; + fake_callback_event_t event; + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_library_bindings_t *bindings = + kzt_guest_library_bindings_init(); + kzt_observation_adapter_result_t result; + kzt_guest_library_binding_key_t key; + kzt_guest_library_handle_t handle; + + init_fake_callback_event(&event, "/guest/libexact.so", 0x160000); + event.namespace_id = 0; + check_int("exact.track", kzt_guest_library_track( + bindings, (library_t *)&library), 0); + check_int("exact.pending", kzt_guest_library_note_exact_pair( + bindings, (uintptr_t)&event.link_map, + (library_t *)&library, + KZT_GUEST_LIBRARY_OBJECT_WRAPPED), + KZT_GUEST_LIBRARY_BINDING_PENDING); + kzt_guest_registry_test_set_alloc_failure_after(0); + (void)run_adapter_with_bindings(&event, registry, bindings, &result); + check_int("exact.transient-result", result, + KZT_OBSERVATION_ADAPTER_REGISTRY_FAILED); + kzt_guest_registry_test_set_alloc_failure_after(-1); + (void)run_adapter_with_bindings(&event, registry, bindings, &result); + check_int("exact.retry-result", result, KZT_OBSERVATION_ADAPTER_ADDED); + key = (kzt_guest_library_binding_key_t){ + .link_map_addr = (uintptr_t)&event.link_map, + .generation = 1, + .namespace_id = 0, + .namespace_kind = KZT_GUEST_LIBRARY_NAMESPACE_MAIN, + }; + check_int("exact.lookup", kzt_guest_library_lookup( + bindings, &key, &handle), 0); + check_true("exact.library", handle.library == (library_t *)&library); + kzt_guest_library_handle_release(&handle); + kzt_guest_library_unbind(bindings, registry, (library_t *)&library, + (uintptr_t)&event.link_map); + kzt_guest_library_bindings_destroy(&bindings); + kzt_guest_registry_destroy(®istry); +} + +static void test_dynamic_parser_success_commits_snapshot(void) +{ + fake_callback_event_t event; + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_observation_adapter_result_t result = KZT_OBSERVATION_ADAPTER_DISABLED; + kzt_guest_registry_diagnostic_config_t config = { + .enabled = 1, + .throttle_limit = 4, + }; + int ret; + + check_true("registry.init", registry != NULL); + if (!registry) { + return; + } + + init_fake_callback_event(&event, "/guest/libdynamic.so", 0x180000); + event.trace.legacy_return = 41; + check_int("dynamic-success.configure", + kzt_guest_registry_configure_diagnostics(registry, &config), + 0); + + ret = run_adapter_with_diagnostics(&event, registry, 1, 0, &result); + + check_int("dynamic-success.return", ret, 41); + check_int("dynamic-success.result", result, + KZT_OBSERVATION_ADAPTER_ADDED); + assert_old_flow_exactly_once("dynamic-success.old-flow", &event, 41); + assert_dynamic_view_complete("dynamic-success.view", registry, &event, 1); + check_int("dynamic-success.diagnostic-attempted", + event.trace.dynamic_attempted, 1); + check_int("dynamic-success.diagnostic-parse-return", + event.trace.dynamic_parse_return, 0); + check_int("dynamic-success.diagnostic-status", + event.trace.dynamic_status, KZT_GUEST_DYNAMIC_COMPLETE); + check_int("dynamic-success.diagnostic-commit", + event.trace.dynamic_commit_attempted, 1); + check_int("dynamic-success.diagnostic-commit-result", + event.trace.dynamic_commit_result, KZT_GUEST_REGISTRY_UPDATED); + + kzt_guest_registry_destroy(®istry); +} + +static void test_dynamic_parser_reuses_same_generation_complete_view(void) +{ + fake_callback_event_t event; + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_observation_adapter_result_t result = KZT_OBSERVATION_ADAPTER_DISABLED; + kzt_guest_registry_diagnostic_config_t config = { + .enabled = 1, + .throttle_limit = 4, + }; + int ret; + + check_true("registry.init", registry != NULL); + if (!registry) { + return; + } + + init_fake_callback_event(&event, "/guest/libdynamic-cache.so", 0x1a0000); + event.trace.legacy_return = 44; + check_int("dynamic-cache.configure", + kzt_guest_registry_configure_diagnostics(registry, &config), + 0); + + ret = run_adapter_with_diagnostics(&event, registry, 1, 0, &result); + check_int("dynamic-cache.first-return", ret, 44); + check_int("dynamic-cache.first-result", result, + KZT_OBSERVATION_ADAPTER_ADDED); + check_int("dynamic-cache.first-attempted", event.trace.dynamic_attempted, 1); + check_true("dynamic-cache.first-read", + event.trace.dynamic_reader_calls > 0); + assert_dynamic_view_complete("dynamic-cache.first-view", registry, &event, + 1); + + memset(&event.trace, 0, sizeof(event.trace)); + event.trace.legacy_return = 45; + result = KZT_OBSERVATION_ADAPTER_DISABLED; + ret = run_adapter_with_diagnostics(&event, registry, 1, 0, &result); + + check_int("dynamic-cache.second-return", ret, 45); + check_int("dynamic-cache.second-result", result, + KZT_OBSERVATION_ADAPTER_UNCHANGED); + assert_old_flow_exactly_once("dynamic-cache.second-old-flow", &event, 45); + check_int("dynamic-cache.second-cache-hit", + event.trace.dynamic_cache_hit, 1); + check_int("dynamic-cache.second-attempted", + event.trace.dynamic_attempted, 0); + check_int("dynamic-cache.second-dynamic-reads", + event.trace.dynamic_reader_calls, 0); + assert_dynamic_view_complete("dynamic-cache.second-view", registry, &event, + 1); + + kzt_guest_registry_destroy(®istry); +} + +static void test_dynamic_parser_read_failure_is_fail_open(void) +{ + fake_callback_event_t event; + fake_read_failure_t failure; + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_observation_adapter_result_t result = KZT_OBSERVATION_ADAPTER_DISABLED; + kzt_guest_registry_diagnostic_config_t config = { + .enabled = 1, + .throttle_limit = 4, + }; + int ret; + + check_true("registry.init", registry != NULL); + if (!registry) { + return; + } + + init_fake_callback_event(&event, "/guest/libdynamic-readfail.so", + 0x190000); + event.trace.legacy_return = 42; + failure.addr = (uintptr_t)&event.dynamic[1]; + failure.size = sizeof(event.dynamic[1]); + event.memory.failures = &failure; + event.memory.failure_count = 1; + check_int("dynamic-readfail.configure", + kzt_guest_registry_configure_diagnostics(registry, &config), + 0); + + ret = run_adapter_with_diagnostics(&event, registry, 1, 0, &result); + + check_int("dynamic-readfail.return", ret, 42); + check_int("dynamic-readfail.result", result, + KZT_OBSERVATION_ADAPTER_ADDED); + assert_old_flow_exactly_once("dynamic-readfail.old-flow", &event, 42); + assert_dynamic_view_read_error("dynamic-readfail.view", registry, &event); + check_int("dynamic-readfail.diagnostic-attempted", + event.trace.dynamic_attempted, 1); + check_int("dynamic-readfail.diagnostic-status", + event.trace.dynamic_status, KZT_GUEST_DYNAMIC_READ_ERROR); + check_uintptr("dynamic-readfail.diagnostic-read-addr", + event.trace.dynamic_read_error_addr, + (uintptr_t)&event.dynamic[1]); + check_int("dynamic-readfail.diagnostic-commit-result", + event.trace.dynamic_commit_result, KZT_GUEST_REGISTRY_UPDATED); + + kzt_guest_registry_destroy(®istry); +} + +static void test_dynamic_diagnostics_preserve_complete_view(void) +{ + fake_callback_event_t event; + fake_read_failure_t failure; + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_observation_adapter_result_t result = KZT_OBSERVATION_ADAPTER_DISABLED; + kzt_guest_registry_diagnostic_config_t config = { + .enabled = 1, + .throttle_limit = 4, + }; + + check_true("dynamic-compare.registry", registry != NULL); + if (!registry) { + return; + } + + init_fake_callback_event(&event, "/guest/libdynamic-compare.so", + 0x1d0000); + check_int("dynamic-compare.configure", + kzt_guest_registry_configure_diagnostics(registry, &config), 0); + check_int("dynamic-compare.seed", run_adapter_with_diagnostics( + &event, registry, 1, 0, &result), 77); + assert_dynamic_view_complete("dynamic-compare.seed-view", registry, + &event, 1); + + memset(&event.trace, 0, sizeof(event.trace)); + event.trace.legacy_return = 77; + failure.addr = (uintptr_t)&event.dynamic[1]; + failure.size = sizeof(event.dynamic[1]); + event.memory.failures = &failure; + event.memory.failure_count = 1; + check_int("dynamic-compare.read-failure", run_adapter_with_diagnostics( + &event, registry, 1, 1, &result), 77); + check_int("dynamic-compare.result", result, + KZT_OBSERVATION_ADAPTER_UNCHANGED); + assert_dynamic_view_complete("dynamic-compare.preserved", registry, + &event, 1); + check_int("dynamic-compare.attempted", + event.trace.dynamic_comparison_attempted, 1); + check_int("dynamic-compare.blocking", + event.trace.dynamic_comparison_blocking, 1); + check_int("dynamic-compare.matched", + event.trace.dynamic_comparison_matched, 0); + check_int("dynamic-compare.commit", event.trace.dynamic_commit_result, + KZT_GUEST_REGISTRY_UNCHANGED); + assert_old_flow_exactly_once("dynamic-compare.old-flow", &event, 77); + + kzt_guest_registry_destroy(®istry); +} + +static void test_dynamic_commit_failure_is_fail_open(void) +{ + fake_callback_event_t event; + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_observation_adapter_result_t result = KZT_OBSERVATION_ADAPTER_DISABLED; + kzt_guest_registry_diagnostic_config_t config = { + .enabled = 1, + .throttle_limit = 4, + }; + kzt_guest_registry_diagnostics_t diagnostics = { 0 }; + int ret; + + check_true("registry.init", registry != NULL); + if (!registry) { + return; + } + + init_fake_callback_event(&event, "/guest/libdynamic-commitfail.so", + 0x1a0000); + event.trace.legacy_return = 43; + check_int("dynamic-commitfail.configure", + kzt_guest_registry_configure_diagnostics(registry, &config), + 0); + + kzt_guest_registry_test_set_dynamic_commit_failure_after(0); + ret = run_adapter_with_diagnostics(&event, registry, 1, 0, &result); + kzt_guest_registry_test_set_dynamic_commit_failure_after(-1); + + check_int("dynamic-commitfail.return", ret, 43); + check_int("dynamic-commitfail.result", result, + KZT_OBSERVATION_ADAPTER_ADDED); + assert_old_flow_exactly_once("dynamic-commitfail.old-flow", &event, 43); + assert_dynamic_view_not_parsed("dynamic-commitfail.view", registry, + &event); + check_int("dynamic-commitfail.diagnostic-attempted", + event.trace.dynamic_attempted, 1); + check_int("dynamic-commitfail.diagnostic-status", + event.trace.dynamic_status, KZT_GUEST_DYNAMIC_COMPLETE); + check_int("dynamic-commitfail.diagnostic-commit-result", + event.trace.dynamic_commit_result, KZT_GUEST_REGISTRY_ERROR); + check_int("dynamic-commitfail.diagnostics", + kzt_guest_registry_get_diagnostics(registry, &diagnostics), 0); + check_ulong("dynamic-commitfail.error-count", diagnostics.errors, 1); + + kzt_guest_registry_destroy(®istry); +} + +static void test_disabled_adapter_skips_observation_but_preserves_old_flow(void) +{ + fake_callback_event_t event; + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_observation_adapter_result_t result = KZT_OBSERVATION_ADAPTER_ADDED; + int ret; + + check_true("registry.init", registry != NULL); + if (!registry) { + return; + } + + init_fake_callback_event(&event, "/guest/libdisabled.so", 0x200000); + event.trace.legacy_return = 24; + + ret = run_adapter(&event, registry, 0, &result); + + check_int("disabled.return", ret, 24); + check_int("disabled.result", result, KZT_OBSERVATION_ADAPTER_DISABLED); + assert_old_flow_exactly_once("disabled.old-flow", &event, 24); + check_int("disabled.reader-calls", event.trace.reader_calls, 0); + check_ulong("disabled.registry-count", registry_object_count(registry), 0); + + kzt_guest_registry_destroy(®istry); +} + +static void test_reader_failure_is_fail_open_for_old_flow(void) +{ + fake_callback_event_t event; + fake_read_failure_t failure; + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_observation_adapter_result_t result = KZT_OBSERVATION_ADAPTER_ADDED; + int ret; + + check_true("registry.init", registry != NULL); + if (!registry) { + return; + } + + init_fake_callback_event(&event, "/guest/libreaderfail.so", 0x300000); + event.trace.legacy_return = 25; + failure.addr = (uintptr_t)&event.link_map + + offsetof(struct link_map_x64, l_addr); + failure.size = sizeof(event.link_map.l_addr); + event.memory.failures = &failure; + event.memory.failure_count = 1; + + ret = run_adapter(&event, registry, 1, &result); + + check_int("reader-failure.return", ret, 25); + check_int("reader-failure.result", result, + KZT_OBSERVATION_ADAPTER_ADDED); + assert_old_flow_exactly_once("reader-failure.old-flow", &event, 25); + assert_reader_before_old_flow(&event); + check_ulong("reader-failure.registry-count", + registry_object_count(registry), 1); + + kzt_guest_registry_destroy(®istry); +} + +static void test_partial_link_map_fields_are_registered(void) +{ + static const struct { + const char *name; + size_t offset; + size_t size; + } cases[] = { + { "load-bias", offsetof(struct link_map_x64, l_addr), + sizeof(((struct link_map_x64 *)0)->l_addr) }, + { "dynamic", offsetof(struct link_map_x64, l_ld), + sizeof(((struct link_map_x64 *)0)->l_ld) }, + { "path", offsetof(struct link_map_x64, l_name), + sizeof(((struct link_map_x64 *)0)->l_name) }, + }; + size_t i; + + for (i = 0; i < TEST_ARRAY_SIZE(cases); ++i) { + fake_callback_event_t event; + fake_read_failure_t failure; + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_object_snapshot_t *snapshot = NULL; + kzt_observation_adapter_result_t result = + KZT_OBSERVATION_ADAPTER_DISABLED; + + check_true("partial-fields.registry", registry != NULL); + if (!registry) { + continue; + } + init_fake_callback_event(&event, "/guest/libpartial-fields.so", + 0x330000 + i * 0x10000); + failure.addr = (uintptr_t)&event.link_map + cases[i].offset; + failure.size = cases[i].size; + event.memory.failures = &failure; + event.memory.failure_count = 1; + + check_int(cases[i].name, run_adapter(&event, registry, 1, &result), + 77); + check_int("partial-fields.result", result, + KZT_OBSERVATION_ADAPTER_ADDED); + check_int("partial-fields.find", kzt_guest_registry_find_by_link_map( + registry, (uintptr_t)&event.link_map, &snapshot), 0); + check_true("partial-fields.snapshot", snapshot != NULL); + if (snapshot) { + check_uintptr("partial-fields.identity", snapshot->link_map_addr, + (uintptr_t)&event.link_map); + if (i == 0) { + check_int("partial-fields.load-bias", snapshot->load_bias.status, + KZT_GUEST_FIELD_READ_ERROR); + } else if (i == 1) { + check_int("partial-fields.dynamic", snapshot->dynamic_addr.status, + KZT_GUEST_FIELD_READ_ERROR); + } else { + check_int("partial-fields.path", snapshot->path.status, + KZT_GUEST_FIELD_READ_ERROR); + } + kzt_guest_object_snapshot_free(snapshot); + } + assert_old_flow_exactly_once("partial-fields.old-flow", &event, 77); + kzt_guest_registry_destroy(®istry); + } +} + +static void test_verified_hints_fill_private_link_map_evidence(void) +{ + fake_callback_event_t event; + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_object_snapshot_t *snapshot = NULL; + kzt_observation_adapter_result_t result = + KZT_OBSERVATION_ADAPTER_DISABLED; + + check_true("verified-hints.registry", registry != NULL); + if (!registry) { + return; + } + + init_fake_callback_event(&event, "/guest/libverified-hints.so", 0x360000); + event.link_map.l_ns = 91; + event.link_map.l_map_start = 0x111000; + event.link_map.l_map_end = 0x112000; + event.namespace_id = 0; + + check_int("verified-hints.return", + run_adapter(&event, registry, 1, &result), 77); + check_int("verified-hints.result", result, + KZT_OBSERVATION_ADAPTER_ADDED); + check_int("verified-hints.find", kzt_guest_registry_find_by_link_map( + registry, (uintptr_t)&event.link_map, &snapshot), 0); + check_true("verified-hints.snapshot", snapshot != NULL); + if (snapshot) { + check_int("verified-hints.namespace.status", + snapshot->namespace_id.status, KZT_GUEST_FIELD_OK); + check_uintptr("verified-hints.namespace.value", + snapshot->namespace_id.value, 0); + check_int("verified-hints.map-start.status", + snapshot->map_start.status, KZT_GUEST_FIELD_OK); + check_uintptr("verified-hints.map-start.value", + snapshot->map_start.value, event.map_start); + check_int("verified-hints.map-end.status", + snapshot->map_end.status, KZT_GUEST_FIELD_OK); + check_uintptr("verified-hints.map-end.value", + snapshot->map_end.value, event.map_end); + kzt_guest_object_snapshot_free(snapshot); + } + assert_old_flow_exactly_once("verified-hints.old-flow", &event, 77); + kzt_guest_registry_destroy(®istry); +} + +static void test_invalid_or_absent_hints_remain_unknown_and_fail_open(void) +{ + fake_callback_event_t event; + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_object_snapshot_t *snapshot = NULL; + kzt_observation_adapter_result_t result = + KZT_OBSERVATION_ADAPTER_DISABLED; + + check_true("invalid-hints.registry", registry != NULL); + if (!registry) { + return; + } + + init_fake_callback_event(&event, "/guest/libinvalid-hints.so", 0x370000); + event.namespace_id_present = 0; + event.namespace_id = 0; + event.map_range_present = 1; + event.map_start = 0x390000; + event.map_end = 0x380000; + event.link_map.l_ns = 0; + event.link_map.l_map_start = 0x370000; + event.link_map.l_map_end = 0x390000; + + check_int("invalid-hints.return", + run_adapter(&event, registry, 1, &result), 77); + check_int("invalid-hints.result", result, + KZT_OBSERVATION_ADAPTER_ADDED); + check_int("invalid-hints.find", kzt_guest_registry_find_by_link_map( + registry, (uintptr_t)&event.link_map, &snapshot), 0); + check_true("invalid-hints.snapshot", snapshot != NULL); + if (snapshot) { + check_int("invalid-hints.namespace", + snapshot->namespace_id.status, KZT_GUEST_FIELD_UNKNOWN); + check_int("invalid-hints.map-start", + snapshot->map_start.status, KZT_GUEST_FIELD_UNKNOWN); + check_int("invalid-hints.map-end", + snapshot->map_end.status, KZT_GUEST_FIELD_UNKNOWN); + kzt_guest_object_snapshot_free(snapshot); + } + assert_old_flow_exactly_once("invalid-hints.old-flow", &event, 77); + kzt_guest_registry_destroy(®istry); +} + +static void test_registry_failure_is_fail_open_for_old_flow(void) +{ + fake_callback_event_t event; + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_observation_adapter_result_t result = KZT_OBSERVATION_ADAPTER_ADDED; + int ret; + + check_true("registry.init", registry != NULL); + if (!registry) { + return; + } + + init_fake_callback_event(&event, "/guest/libregistryfail.so", 0x400000); + event.trace.legacy_return = 26; + + kzt_guest_registry_test_set_alloc_failure_after(0); + ret = run_adapter(&event, registry, 1, &result); + kzt_guest_registry_test_set_alloc_failure_after(-1); + + check_int("registry-failure.return", ret, 26); + check_int("registry-failure.result", result, + KZT_OBSERVATION_ADAPTER_REGISTRY_FAILED); + assert_old_flow_exactly_once("registry-failure.old-flow", &event, 26); + assert_reader_before_old_flow(&event); + check_ulong("registry-failure.registry-count", + registry_object_count(registry), 0); + + kzt_guest_registry_destroy(®istry); +} + +static void test_conflict_result_does_not_change_old_flow(void) +{ + fake_callback_event_t event; + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_object_observation_t original; + kzt_observation_adapter_result_t result = KZT_OBSERVATION_ADAPTER_ADDED; + kzt_guest_registry_diagnostic_config_t config = { + .enabled = 1, + .throttle_limit = 4, + }; + int ret; + + check_true("registry.init", registry != NULL); + if (!registry) { + return; + } + + init_fake_callback_event(&event, "/guest/libconflict-new.so", 0x500000); + event.trace.legacy_return = 27; + original = make_observation((uintptr_t)&event.link_map, + 0x510000, + "/guest/libconflict-old.so"); + check_int("conflict.configure", + kzt_guest_registry_configure_diagnostics(registry, &config), + 0); + check_int("conflict.prepopulate", + kzt_guest_registry_observe(registry, &original), + KZT_GUEST_REGISTRY_ADDED); + + ret = run_adapter_with_diagnostics(&event, registry, 1, 0, &result); + + check_int("conflict.return", ret, 27); + check_int("conflict.result", result, KZT_OBSERVATION_ADAPTER_CONFLICT); + assert_old_flow_exactly_once("conflict.old-flow", &event, 27); + assert_reader_before_old_flow(&event); + check_ulong("conflict.registry-count", + registry_object_count(registry), 1); + assert_dynamic_view_not_parsed("conflict.view", registry, &event); + check_int("conflict.diagnostic-calls", event.trace.diagnostic_calls, 1); + check_int("conflict.dynamic-not-attempted", + event.trace.dynamic_attempted, 0); + check_int("conflict.dynamic-no-commit", + event.trace.dynamic_commit_attempted, 0); + check_int("conflict.dynamic-commit-result", + event.trace.dynamic_commit_result, KZT_GUEST_REGISTRY_RESULT_COUNT); + + kzt_guest_registry_destroy(®istry); +} + +static void test_no_callback_event_does_not_create_registry_objects(void) +{ + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + + check_true("registry.init", registry != NULL); + if (!registry) { + return; + } + + check_ulong("no-callback.registry-count", + registry_object_count(registry), 0); + + kzt_guest_registry_destroy(®istry); +} + +static void test_enabled_diagnostics_are_throttled_and_fail_open(void) +{ + fake_callback_event_t event; + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_observation_adapter_result_t result = KZT_OBSERVATION_ADAPTER_DISABLED; + kzt_guest_registry_diagnostic_config_t config = { + .enabled = 1, + .throttle_limit = 1, + }; + + check_true("registry.init", registry != NULL); + if (!registry) { + return; + } + + init_fake_callback_event(&event, "/guest/libdiagnostic.so", 0x700000); + event.trace.legacy_return = 31; + check_int("diagnostic.configure", + kzt_guest_registry_configure_diagnostics(registry, &config), + 0); + + check_int("diagnostic.added.return", + run_adapter_with_diagnostics(&event, registry, 1, 0, &result), + 31); + check_int("diagnostic.added.result", result, + KZT_OBSERVATION_ADAPTER_ADDED); + check_int("diagnostic.added.calls", event.trace.diagnostic_calls, 1); + check_int("diagnostic.added.callback-result", + event.trace.diagnostic_result, KZT_OBSERVATION_ADAPTER_ADDED); + check_int("diagnostic.added.emitted", event.trace.diagnostic_emitted, 1); + check_int("diagnostic.added.legacy-calls", event.trace.legacy_calls, 1); + + check_int("diagnostic.unchanged.return", + run_adapter_with_diagnostics(&event, registry, 1, 0, &result), + 31); + check_int("diagnostic.unchanged.result", result, + KZT_OBSERVATION_ADAPTER_UNCHANGED); + check_int("diagnostic.unchanged.calls", event.trace.diagnostic_calls, 2); + check_int("diagnostic.unchanged.callback-result", + event.trace.diagnostic_result, + KZT_OBSERVATION_ADAPTER_UNCHANGED); + check_ulong("diagnostic.unchanged.observations", + event.trace.diagnostic_result_observations, 1); + check_int("diagnostic.unchanged.legacy-calls", + event.trace.legacy_calls, 2); + + check_int("diagnostic.suppressed.return", + run_adapter_with_diagnostics(&event, registry, 1, 0, &result), + 31); + check_int("diagnostic.suppressed.result", result, + KZT_OBSERVATION_ADAPTER_UNCHANGED); + check_int("diagnostic.suppressed.calls", event.trace.diagnostic_calls, 2); + check_int("diagnostic.suppressed.legacy-calls", + event.trace.legacy_calls, 3); + + kzt_guest_registry_destroy(®istry); +} + +static void test_reader_failures_are_throttled(void) +{ + fake_callback_event_t event; + fake_read_failure_t failure; + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_observation_adapter_result_t result = KZT_OBSERVATION_ADAPTER_DISABLED; + kzt_guest_registry_diagnostic_config_t config = { + .enabled = 1, + .throttle_limit = 1, + }; + + check_true("registry.init", registry != NULL); + if (!registry) { + return; + } + + init_fake_callback_event(&event, "/guest/libdiagnostic-fail.so", + 0x710000); + failure.addr = (uintptr_t)&event.link_map + + offsetof(struct link_map_x64, l_addr); + failure.size = sizeof(event.link_map.l_addr); + event.memory.failures = &failure; + event.memory.failure_count = 1; + event.trace.legacy_return = 37; + check_int("reader-diagnostic.configure", + kzt_guest_registry_configure_diagnostics(registry, &config), + 0); + + check_int("reader-diagnostic.first-return", + run_adapter_with_diagnostics(&event, registry, 1, 0, &result), + 37); + check_int("reader-diagnostic.first-result", result, + KZT_OBSERVATION_ADAPTER_ADDED); + check_int("reader-diagnostic.first-calls", + event.trace.diagnostic_calls, 1); + check_int("reader-diagnostic.first-emitted", + event.trace.diagnostic_emitted, 1); + check_int("reader-diagnostic.first-legacy", + event.trace.legacy_calls, 1); + + check_int("reader-diagnostic.second-return", + run_adapter_with_diagnostics(&event, registry, 1, 0, &result), + 37); + check_int("reader-diagnostic.second-result", result, + KZT_OBSERVATION_ADAPTER_UNCHANGED); + check_int("reader-diagnostic.second-calls", + event.trace.diagnostic_calls, 2); + check_int("reader-diagnostic.second-legacy", + event.trace.legacy_calls, 2); + + kzt_guest_registry_destroy(®istry); +} + +static void test_disabled_adapter_diagnostics_are_throttled(void) +{ + fake_callback_event_t event; + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_observation_adapter_result_t result = KZT_OBSERVATION_ADAPTER_ADDED; + kzt_guest_registry_diagnostic_config_t config = { + .enabled = 1, + .throttle_limit = 1, + }; + + check_true("registry.init", registry != NULL); + if (!registry) { + return; + } + + init_fake_callback_event(&event, "/guest/libdiagnostic-disabled.so", + 0x720000); + event.trace.legacy_return = 39; + check_int("disabled-diagnostic.configure", + kzt_guest_registry_configure_diagnostics(registry, &config), + 0); + + check_int("disabled-diagnostic.first-return", + run_adapter_with_diagnostics(&event, registry, 0, 0, &result), + 39); + check_int("disabled-diagnostic.first-result", result, + KZT_OBSERVATION_ADAPTER_DISABLED); + check_int("disabled-diagnostic.first-calls", + event.trace.diagnostic_calls, 1); + check_int("disabled-diagnostic.first-emitted", + event.trace.diagnostic_emitted, 1); + check_int("disabled-diagnostic.first-legacy", + event.trace.legacy_calls, 1); + check_int("disabled-diagnostic.reader-calls", + event.trace.reader_calls, 0); + + check_int("disabled-diagnostic.second-return", + run_adapter_with_diagnostics(&event, registry, 0, 0, &result), + 39); + check_int("disabled-diagnostic.second-result", result, + KZT_OBSERVATION_ADAPTER_DISABLED); + check_int("disabled-diagnostic.second-calls", + event.trace.diagnostic_calls, 1); + check_int("disabled-diagnostic.second-legacy", + event.trace.legacy_calls, 2); + check_int("disabled-diagnostic.second-reader-calls", + event.trace.reader_calls, 0); + + kzt_guest_registry_destroy(®istry); +} + +int main(void) +{ + test_active_observation_adds_object_and_preserves_old_flow(); + test_exact_pair_retries_after_transient_observation_failure(); + test_binding_owned_retire_excludes_adapter_retire(); + test_adapter_note_before_unload_leaves_single_retire_owner(); + test_cancelled_pending_without_owner_is_adapter_retired(); + test_exact_retire_owner_uses_complete_key(); + /* The direct test above proves exact-generation owner matching. This + * separate test proves the real adapter gate/registry path with a source + * lease participant, binding retire owner, and third callback participant. */ + test_three_participant_adapter_does_not_duplicate_retire(); + test_callback_lifetime_covers_all_guest_work(); + test_unload_winner_rejects_late_callback_before_any_work(); + test_callback_gate_allocation_failure_is_safe_fail_open(); + test_adapter_address_reuse_requires_loader_causality(); + test_callback_pending_observation_waits_for_wrapper_success(); + test_dynamic_parser_success_commits_snapshot(); + test_dynamic_parser_reuses_same_generation_complete_view(); + test_dynamic_parser_read_failure_is_fail_open(); + test_dynamic_diagnostics_preserve_complete_view(); + test_dynamic_commit_failure_is_fail_open(); + test_disabled_adapter_skips_observation_but_preserves_old_flow(); + test_reader_failure_is_fail_open_for_old_flow(); + test_partial_link_map_fields_are_registered(); + test_verified_hints_fill_private_link_map_evidence(); + test_invalid_or_absent_hints_remain_unknown_and_fail_open(); + test_registry_failure_is_fail_open_for_old_flow(); + test_conflict_result_does_not_change_old_flow(); + test_no_callback_event_does_not_create_registry_objects(); + test_enabled_diagnostics_are_throttled_and_fail_open(); + test_reader_failures_are_throttled(); + test_disabled_adapter_diagnostics_are_throttled(); + + if (failures) { + fprintf(stderr, "kzt-observation-adapter: %d failure(s)\n", failures); + return 1; + } + + puts("kzt-observation-adapter: all contract tests passed"); + return 0; +} diff --git a/tests/unit/kzt/test_owner_resolver.c b/tests/unit/kzt/test_owner_resolver.c new file mode 100644 index 00000000000..69eaef52b5f --- /dev/null +++ b/tests/unit/kzt/test_owner_resolver.c @@ -0,0 +1,338 @@ +#include +#include + +#include "target/i386/latx/include/kzt_owner_resolver.h" + +static int failures; + +static void check_int(const char *name, int got, int expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got %d expected %d\n", name, got, expected); + ++failures; +} + +static void check_ulong(const char *name, unsigned long got, + unsigned long expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got 0x%lx expected 0x%lx\n", name, got, + expected); + ++failures; +} + +static void check_str(const char *name, const char *got, + const char *expected) +{ + if ((!got && !expected) || (got && expected && !strcmp(got, expected))) { + return; + } + + fprintf(stderr, "%s: got '%s' expected '%s'\n", name, + got ? got : "(null)", expected ? expected : "(null)"); + ++failures; +} + +static kzt_guest_object_observation_t observation( + uintptr_t link_map_addr, + uintptr_t map_start, + uintptr_t map_end, + const char *soname) +{ + return (kzt_guest_object_observation_t) { + .link_map_addr = link_map_addr, + .load_bias = { map_start, KZT_GUEST_FIELD_OK }, + .dynamic_addr = { map_start + 0x1000, KZT_GUEST_FIELD_OK }, + .map_start = { map_start, KZT_GUEST_FIELD_OK }, + .map_end = { map_end, KZT_GUEST_FIELD_OK }, + .namespace_id = { 0, KZT_GUEST_FIELD_OK }, + .path = { soname, KZT_GUEST_FIELD_OK }, + .soname = { soname, KZT_GUEST_FIELD_OK }, + .dynamic_view_status = KZT_GUEST_FIELD_NOT_PARSED, + }; +} + +static kzt_patch_object_ref_t object_ref(uintptr_t link_map_addr, + unsigned long generation) +{ + return (kzt_patch_object_ref_t) { + .known = 1, + .link_map_addr = link_map_addr, + .map_start = 0x70000000, + .map_end = 0x70001000, + .generation = generation, + .soname = "libexpected.so", + .path = "/guest/libexpected.so", + }; +} + +static void test_same_soname_different_ranges_resolves_by_address(void) +{ + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_object_observation_t first = observation( + 0x1000, 0x70000000, 0x70001000, "libsame.so"); + kzt_guest_object_observation_t second = observation( + 0x2000, 0x71000000, 0x71001000, "libsame.so"); + kzt_owner_resolution_t resolution; + + check_int("same.observe.first", + kzt_guest_registry_observe(registry, &first), + KZT_GUEST_REGISTRY_ADDED); + check_int("same.observe.second", + kzt_guest_registry_observe(registry, &second), + KZT_GUEST_REGISTRY_ADDED); + + check_int("same.resolve", + kzt_owner_resolver_resolve_current( + registry, 0x71000080, 0x71000090, &resolution), 0); + check_int("same.status", resolution.status, + KZT_OWNER_RESOLVER_RESOLVED); + check_int("same.match", resolution.owner_match, + KZT_PATCH_OWNER_MATCH); + check_ulong("same.current.link_map", + resolution.current_owner.link_map_addr, 0x2000); + check_ulong("same.expected.link_map", + resolution.expected_owner.link_map_addr, 0x2000); + check_ulong("same.current.generation", + resolution.current_owner.generation, 2); + check_str("same.current.soname", resolution.current_owner.soname, + "libsame.so"); + check_int("same.current.matches", resolution.current_match_count, 1); + check_int("same.expected.matches", resolution.expected_match_count, 1); + + kzt_guest_registry_destroy(®istry); +} + +static void test_range_boundaries_are_start_inclusive_end_exclusive(void) +{ + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_object_observation_t object = observation( + 0x3000, 0x72000000, 0x72001000, "libbounds.so"); + kzt_owner_resolution_t resolution; + + check_int("bounds.observe", + kzt_guest_registry_observe(registry, &object), + KZT_GUEST_REGISTRY_ADDED); + + check_int("bounds.start.resolve", + kzt_owner_resolver_resolve_current( + registry, 0x72000000, 0x72000008, &resolution), 0); + check_int("bounds.start.status", resolution.status, + KZT_OWNER_RESOLVER_RESOLVED); + check_int("bounds.start.match", resolution.owner_match, + KZT_PATCH_OWNER_MATCH); + check_ulong("bounds.start.owner", + resolution.current_owner.link_map_addr, 0x3000); + + check_int("bounds.end.resolve", + kzt_owner_resolver_resolve_current( + registry, 0x72001000, 0x72000008, &resolution), 0); + check_int("bounds.end.status", resolution.status, + KZT_OWNER_RESOLVER_CURRENT_NOT_FOUND); + check_int("bounds.end.match", resolution.owner_match, + KZT_PATCH_OWNER_UNKNOWN); + check_int("bounds.end.current-known", resolution.current_owner.known, 0); + + kzt_guest_registry_destroy(®istry); +} + +static void test_unknown_range_keeps_owner_unknown(void) +{ + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_object_observation_t object = observation( + 0x4000, 0x73000000, 0x73001000, "libunknown-range.so"); + kzt_owner_resolution_t resolution; + + object.map_start.status = KZT_GUEST_FIELD_UNKNOWN; + object.map_end.status = KZT_GUEST_FIELD_UNKNOWN; + check_int("unknown-range.observe", + kzt_guest_registry_observe(registry, &object), + KZT_GUEST_REGISTRY_ADDED); + + check_int("unknown-range.resolve", + kzt_owner_resolver_resolve_current( + registry, 0x73000080, 0x73000090, &resolution), 0); + check_int("unknown-range.status", resolution.status, + KZT_OWNER_RESOLVER_CURRENT_NOT_FOUND); + check_int("unknown-range.match", resolution.owner_match, + KZT_PATCH_OWNER_UNKNOWN); + check_int("unknown-range.current-known", + resolution.current_owner.known, 0); + + kzt_guest_registry_destroy(®istry); +} + +static void test_missing_registry_keeps_owner_unknown(void) +{ + kzt_owner_resolution_t resolution; + + check_int("missing-registry.resolve", + kzt_owner_resolver_resolve_current( + NULL, 0x74000080, 0x74000090, &resolution), 0); + check_int("missing-registry.status", resolution.status, + KZT_OWNER_RESOLVER_REGISTRY_UNAVAILABLE); + check_int("missing-registry.match", resolution.owner_match, + KZT_PATCH_OWNER_UNKNOWN); + check_int("missing-registry.current-known", + resolution.current_owner.known, 0); +} + +static void test_owner_mismatch_is_reported_but_not_matched(void) +{ + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_object_observation_t current = observation( + 0x5000, 0x75000000, 0x75001000, "libpreload.so"); + kzt_guest_object_observation_t expected = observation( + 0x6000, 0x76000000, 0x76001000, "libgtk-3.so"); + kzt_owner_resolution_t resolution; + + check_int("mismatch.observe.current", + kzt_guest_registry_observe(registry, ¤t), + KZT_GUEST_REGISTRY_ADDED); + check_int("mismatch.observe.expected", + kzt_guest_registry_observe(registry, &expected), + KZT_GUEST_REGISTRY_ADDED); + + check_int("mismatch.resolve", + kzt_owner_resolver_resolve_current( + registry, 0x75000020, 0x76000020, &resolution), 0); + check_int("mismatch.status", resolution.status, + KZT_OWNER_RESOLVER_RESOLVED); + check_int("mismatch.match", resolution.owner_match, + KZT_PATCH_OWNER_MISMATCH); + check_ulong("mismatch.current.owner", + resolution.current_owner.link_map_addr, 0x5000); + check_ulong("mismatch.expected.owner", + resolution.expected_owner.link_map_addr, 0x6000); + check_str("mismatch.current.soname", resolution.current_owner.soname, + "libpreload.so"); + check_str("mismatch.expected.soname", resolution.expected_owner.soname, + "libgtk-3.so"); + + kzt_guest_registry_destroy(®istry); +} + +static void test_generation_unknown_does_not_match(void) +{ + kzt_patch_object_ref_t current = object_ref(0x7000, 0); + kzt_patch_object_ref_t expected = object_ref(0x7000, 1); + + check_int("generation-unknown.current", + kzt_owner_resolver_match_refs(¤t, &expected), + KZT_PATCH_OWNER_UNKNOWN); + + current.generation = 1; + expected.generation = 0; + check_int("generation-unknown.expected", + kzt_owner_resolver_match_refs(¤t, &expected), + KZT_PATCH_OWNER_UNKNOWN); +} + +static void test_native_bridge_address_is_not_expected_owner_input(void) +{ + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_object_observation_t guest = observation( + 0xa000, 0x79000000, 0x79001000, "libguest-target.so"); + kzt_guest_object_observation_t bridge_like = observation( + 0xb000, 0x7a000000, 0x7a001000, "libbridge-like.so"); + kzt_owner_resolution_t resolution; + + check_int("bridge-role.observe.guest", + kzt_guest_registry_observe(registry, &guest), + KZT_GUEST_REGISTRY_ADDED); + check_int("bridge-role.observe.bridge-like", + kzt_guest_registry_observe(registry, &bridge_like), + KZT_GUEST_REGISTRY_ADDED); + + check_int("bridge-role.guest-target.resolve", + kzt_owner_resolver_resolve_current( + registry, 0x79000020, 0x79000080, &resolution), 0); + check_int("bridge-role.guest-target.status", resolution.status, + KZT_OWNER_RESOLVER_RESOLVED); + check_int("bridge-role.guest-target.match", resolution.owner_match, + KZT_PATCH_OWNER_MATCH); + + check_int("bridge-role.native-bridge.resolve", + kzt_owner_resolver_resolve_current( + registry, 0x79000020, 0x7a000080, &resolution), 0); + check_int("bridge-role.native-bridge.status", resolution.status, + KZT_OWNER_RESOLVER_RESOLVED); + check_int("bridge-role.native-bridge.match", resolution.owner_match, + KZT_PATCH_OWNER_MISMATCH); + + kzt_guest_registry_destroy(®istry); +} + +static void test_ambiguous_range_keeps_owner_unknown(void) +{ + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_object_observation_t first = observation( + 0x8000, 0x78000000, 0x78002000, "liboverlap-a.so"); + kzt_guest_object_observation_t second = observation( + 0x9000, 0x78001000, 0x78003000, "liboverlap-b.so"); + kzt_owner_resolution_t resolution; + + check_int("ambiguous.observe.first", + kzt_guest_registry_observe(registry, &first), + KZT_GUEST_REGISTRY_ADDED); + check_int("ambiguous.observe.second", + kzt_guest_registry_observe(registry, &second), + KZT_GUEST_REGISTRY_ADDED); + + check_int("ambiguous.resolve", + kzt_owner_resolver_resolve_current( + registry, 0x78001800, 0x78000020, &resolution), 0); + check_int("ambiguous.status", resolution.status, + KZT_OWNER_RESOLVER_CURRENT_AMBIGUOUS); + check_int("ambiguous.match", resolution.owner_match, + KZT_PATCH_OWNER_UNKNOWN); + check_int("ambiguous.matches", resolution.current_match_count, 2); + + kzt_guest_registry_destroy(®istry); +} + +static void test_dead_objects_do_not_resolve_owner(void) +{ + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_object_observation_t object = observation( + 0xc000, 0x7b000000, 0x7b001000, "libdead.so"); + kzt_owner_resolution_t resolution; + + check_int("dead.observe", kzt_guest_registry_observe(registry, &object), + KZT_GUEST_REGISTRY_ADDED); + check_int("dead.retire", kzt_guest_registry_retire( + registry, object.link_map_addr, 1), 0); + check_int("dead.resolve", kzt_owner_resolver_resolve_current( + registry, 0x7b000010, 0x7b000020, &resolution), 0); + check_int("dead.status", resolution.status, + KZT_OWNER_RESOLVER_CURRENT_NOT_FOUND); + check_int("dead.owner-unknown", resolution.current_owner.known, 0); + kzt_guest_registry_destroy(®istry); +} + +int main(void) +{ + test_same_soname_different_ranges_resolves_by_address(); + test_range_boundaries_are_start_inclusive_end_exclusive(); + test_unknown_range_keeps_owner_unknown(); + test_missing_registry_keeps_owner_unknown(); + test_owner_mismatch_is_reported_but_not_matched(); + test_generation_unknown_does_not_match(); + test_native_bridge_address_is_not_expected_owner_input(); + test_ambiguous_range_keeps_owner_unknown(); + test_dead_objects_do_not_resolve_owner(); + + if (failures) { + fprintf(stderr, "kzt-owner-resolver: %d failure(s)\n", failures); + return 1; + } + + puts("kzt-owner-resolver: all resolver tests passed"); + return 0; +} diff --git a/tests/unit/kzt/test_patch_planner.c b/tests/unit/kzt/test_patch_planner.c new file mode 100644 index 00000000000..fe35473d632 --- /dev/null +++ b/tests/unit/kzt/test_patch_planner.c @@ -0,0 +1,509 @@ +#include +#include + +#include "target/i386/latx/include/kzt_patch_planner.h" + +static int failures; + +static void check_int(const char *name, int got, int expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got %d expected %d\n", name, got, expected); + ++failures; +} + +static void check_ulong(const char *name, unsigned long got, + unsigned long expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got 0x%lx expected 0x%lx\n", name, got, expected); + ++failures; +} + +static void check_str(const char *name, const char *got, const char *expected) +{ + if (got && expected && !strcmp(got, expected)) { + return; + } + + fprintf(stderr, "%s: got '%s' expected '%s'\n", name, + got ? got : "(null)", expected ? expected : "(null)"); + ++failures; +} + +static void check_str_contains(const char *name, const char *value, + const char *expected) +{ + if (value && expected && strstr(value, expected)) { + return; + } + + fprintf(stderr, "%s: '%s' does not contain '%s'\n", name, + value ? value : "(null)", expected ? expected : "(null)"); + ++failures; +} + +static kzt_patch_object_ref_t object_ref(uintptr_t link_map_addr, + unsigned long generation, + const char *soname) +{ + return (kzt_patch_object_ref_t) { + .known = 1, + .link_map_addr = link_map_addr, + .map_start = 0x7000000000 + generation * 0x100000, + .map_end = 0x7000008000 + generation * 0x100000, + .generation = generation, + .soname = soname, + .path = soname, + }; +} + +static kzt_patch_candidate_t base_candidate(void) +{ + return (kzt_patch_candidate_t) { + .source = object_ref(0x1000, 7, "librequester.so"), + .dynamic_addr = 0x7000100000, + .load_bias = 0x7000000000, + .dynamic_view_generation = 42, + .dynamic_view_available = 1, + .table_kind = KZT_PATCH_TABLE_PLT_RELA, + .entry_index = 3, + .entry_addr = 0x7000100180, + .reloc_type = KZT_PATCH_RELOCATION_JUMP_SLOT, + .slot_addr = 0x7100000018, + .slot_current_value_present = 1, + .slot_current_value = 0x7200001000, + .lazy_binding_deferred = 0, + .symbol_index = 77, + .symbol_name = "gtk_widget_show", + .version_evidence = KZT_SYMBOL_VERSION_VERSIONED, + .version = "GTK_3.0", + .current_owner = object_ref(0x2000, 12, "libgtk-3.so"), + .owner_match = KZT_PATCH_OWNER_MATCH, + .wrapper_match = KZT_PATCH_WRAPPER_VERSION_MATCH, + .wrapper_name = "wrappedgtk3", + .wrapper_version_evidence = KZT_SYMBOL_VERSION_VERSIONED, + .wrapper_symbol_version = "GTK_3.0", + .bridge_target = 0x7300002000, + }; +} + +static void assert_decision(const char *name, + const kzt_patch_decision_t *decision, + kzt_patch_decision_kind_t expected_kind, + kzt_patch_reason_t expected_reason, + int expected_allow) +{ + char field[128]; + + snprintf(field, sizeof(field), "%s.kind", name); + check_int(field, decision->kind, expected_kind); + snprintf(field, sizeof(field), "%s.reason", name); + check_int(field, decision->reason, expected_reason); + snprintf(field, sizeof(field), "%s.allow", name); + check_int(field, decision->allow_native_bridge, expected_allow); +} + +static void test_complete_evidence_approves_native_bridge(void) +{ + kzt_patch_candidate_t candidate = base_candidate(); + kzt_patch_decision_t decision; + char line[768]; + + check_int("approved.decide", + kzt_patch_planner_decide(&candidate, &decision), 0); + assert_decision("approved", &decision, + KZT_PATCH_DECISION_APPROVED, + KZT_PATCH_REASON_APPROVED_NATIVE_BRIDGE, 1); + check_ulong("approved.link-map", decision.source.link_map_addr, + candidate.source.link_map_addr); + check_ulong("approved.source-generation", decision.source.generation, + candidate.source.generation); + check_ulong("approved.dynamic-addr", decision.dynamic_addr, + candidate.dynamic_addr); + check_ulong("approved.load-bias", decision.load_bias, + candidate.load_bias); + check_ulong("approved.dynamic-generation", + decision.dynamic_view_generation, + candidate.dynamic_view_generation); + check_ulong("approved.entry-index", decision.entry_index, + candidate.entry_index); + check_ulong("approved.entry-addr", decision.entry_addr, + candidate.entry_addr); + check_ulong("approved.slot", decision.slot_addr, candidate.slot_addr); + check_ulong("approved.current-got", decision.slot_current_value, + candidate.slot_current_value); + check_ulong("approved.symbol-index", decision.symbol_index, + candidate.symbol_index); + check_ulong("approved.owner", decision.current_owner.link_map_addr, + candidate.current_owner.link_map_addr); + check_ulong("approved.bridge", decision.bridge_target, + candidate.bridge_target); + check_str("approved.kind-name", + kzt_patch_decision_kind_name(decision.kind), "APPROVED"); + check_str("approved.reason-name", + kzt_patch_reason_name(decision.reason), + "APPROVED_NATIVE_BRIDGE"); + + check_int("approved.summary", + kzt_patch_decision_format_summary(&decision, line, + sizeof(line)), 0); + check_str_contains("approved.summary-kind", line, "kind=APPROVED"); + check_str_contains("approved.summary-reason", line, + "reason=APPROVED_NATIVE_BRIDGE"); + check_str_contains("approved.summary-link-map", line, + "link_map=0x1000"); + check_str_contains("approved.summary-source-generation", line, + "source_generation=7"); + check_str_contains("approved.summary-dynamic-addr", line, + "dynamic_addr=0x7000100000"); + check_str_contains("approved.summary-load-bias", line, + "load_bias=0x7000000000"); + check_str_contains("approved.summary-dynamic-generation", line, + "dynamic_view_generation=42"); + check_str_contains("approved.summary-table", line, "table=PLT_RELA"); + check_str_contains("approved.summary-entry-index", line, + "entry_index=3"); + check_str_contains("approved.summary-entry-addr", line, + "entry_addr=0x7000100180"); + check_str_contains("approved.summary-reloc", line, "reloc=JUMP_SLOT"); + check_str_contains("approved.summary-slot", line, + "slot=0x7100000018"); + check_str_contains("approved.summary-symbol-index", line, + "symbol_index=77"); + check_str_contains("approved.summary-symbol", line, + "symbol=gtk_widget_show"); + check_str_contains("approved.summary-wrapper", line, + "wrapper=wrappedgtk3"); +} + +static void test_unsupported_relocation_is_not_guessed(void) +{ + kzt_patch_candidate_t candidate = base_candidate(); + kzt_patch_decision_t decision; + + candidate.reloc_type = KZT_PATCH_RELOCATION_RELATIVE; + + check_int("unsupported-relocation.decide", + kzt_patch_planner_decide(&candidate, &decision), 0); + assert_decision("unsupported-relocation", &decision, + KZT_PATCH_DECISION_UNSUPPORTED, + KZT_PATCH_REASON_INPUT_UNSUPPORTED_RELOCATION, 0); + check_str("unsupported-relocation.name", + kzt_patch_relocation_type_name(decision.reloc_type), + "RELATIVE"); +} + +static void test_dynamic_view_unavailable_is_unsupported(void) +{ + kzt_patch_candidate_t candidate = base_candidate(); + kzt_patch_decision_t decision; + char line[768]; + + candidate.dynamic_view_available = 0; + + check_int("dynamic-unavailable.decide", + kzt_patch_planner_decide(&candidate, &decision), 0); + assert_decision("dynamic-unavailable", &decision, + KZT_PATCH_DECISION_UNSUPPORTED, + KZT_PATCH_REASON_INPUT_UNAVAILABLE_DYNAMIC_VIEW, 0); + check_int("dynamic-unavailable.summary", + kzt_patch_decision_format_summary(&decision, line, + sizeof(line)), 0); + check_str_contains("dynamic-unavailable.summary", line, + "dynamic_view_available=0"); +} + +static void test_owner_unknown_is_unsupported(void) +{ + kzt_patch_candidate_t candidate = base_candidate(); + kzt_patch_decision_t decision; + + memset(&candidate.current_owner, 0, sizeof(candidate.current_owner)); + candidate.owner_match = KZT_PATCH_OWNER_UNKNOWN; + + check_int("owner-unknown.decide", + kzt_patch_planner_decide(&candidate, &decision), 0); + assert_decision("owner-unknown", &decision, + KZT_PATCH_DECISION_UNSUPPORTED, + KZT_PATCH_REASON_INPUT_UNAVAILABLE_OWNER, 0); +} + +static void test_owner_mismatch_is_a_stable_rejection(void) +{ + kzt_patch_candidate_t candidate = base_candidate(); + kzt_patch_decision_t decision; + char line[768]; + + candidate.current_owner = object_ref(0x3000, 13, "libpreload.so"); + candidate.owner_match = KZT_PATCH_OWNER_MISMATCH; + + check_int("owner-mismatch.decide", + kzt_patch_planner_decide(&candidate, &decision), 0); + assert_decision("owner-mismatch", &decision, + KZT_PATCH_DECISION_REJECTED, + KZT_PATCH_REASON_POLICY_OWNER_MISMATCH, 0); + check_str("owner-mismatch.kind-name", + kzt_patch_decision_kind_name(decision.kind), "REJECTED"); + check_str("owner-mismatch.owner-match-name", + kzt_patch_owner_match_name(decision.owner_match), + "MISMATCH"); + check_int("owner-mismatch.summary", + kzt_patch_decision_format_summary(&decision, line, + sizeof(line)), 0); + check_str_contains("owner-mismatch.summary-owner", line, + "current_owner=0x3000"); + check_str_contains("owner-mismatch.summary-match", line, + "owner_match=MISMATCH"); +} + +static void test_wrapper_version_mismatch_is_rejected(void) +{ + kzt_patch_candidate_t candidate = base_candidate(); + kzt_patch_decision_t decision; + + candidate.wrapper_match = KZT_PATCH_WRAPPER_VERSION_MISMATCH; + candidate.wrapper_symbol_version = "GTK_2.0"; + + check_int("version-mismatch.decide", + kzt_patch_planner_decide(&candidate, &decision), 0); + assert_decision("version-mismatch", &decision, + KZT_PATCH_DECISION_REJECTED, + KZT_PATCH_REASON_POLICY_VERSION_MISMATCH, 0); + check_str("version-mismatch.kind-name", + kzt_patch_decision_kind_name(decision.kind), "REJECTED"); +} + +static void test_no_wrapper_rejects_to_keep_guest_target(void) +{ + kzt_patch_candidate_t candidate = base_candidate(); + kzt_patch_decision_t decision; + + candidate.wrapper_match = KZT_PATCH_WRAPPER_NO_WRAPPER; + candidate.wrapper_name = NULL; + candidate.wrapper_symbol_version = NULL; + candidate.bridge_target = 0; + + check_int("no-wrapper.decide", + kzt_patch_planner_decide(&candidate, &decision), 0); + assert_decision("no-wrapper", &decision, + KZT_PATCH_DECISION_REJECTED, + KZT_PATCH_REASON_POLICY_NO_WRAPPER, 0); + check_str("no-wrapper.match-name", + kzt_patch_wrapper_match_name(decision.wrapper_match), + "NO_WRAPPER"); +} + +static void test_lazy_deferred_is_not_patched_yet(void) +{ + kzt_patch_candidate_t candidate = base_candidate(); + kzt_patch_decision_t decision; + char line[768]; + + candidate.lazy_binding_deferred = 1; + candidate.current_owner.known = 0; + candidate.owner_match = KZT_PATCH_OWNER_UNKNOWN; + + check_int("lazy-deferred.decide", + kzt_patch_planner_decide(&candidate, &decision), 0); + assert_decision("lazy-deferred", &decision, + KZT_PATCH_DECISION_DEFERRED, + KZT_PATCH_REASON_DEFERRED_LAZY_BINDING, 0); + check_str("lazy-deferred.kind-name", + kzt_patch_decision_kind_name(decision.kind), "DEFERRED"); + check_int("lazy-deferred.summary", + kzt_patch_decision_format_summary(&decision, line, + sizeof(line)), 0); + check_str_contains("lazy-deferred.summary", line, + "lazy_deferred=1"); +} + +static void test_symbol_only_wrapper_rejects_to_keep_guest_target(void) +{ + kzt_patch_candidate_t candidate = base_candidate(); + kzt_patch_decision_t decision; + + candidate.wrapper_match = KZT_PATCH_WRAPPER_SYMBOL_ONLY; + candidate.wrapper_symbol_version = NULL; + + check_int("symbol-only.decide", + kzt_patch_planner_decide(&candidate, &decision), 0); + assert_decision("symbol-only", &decision, + KZT_PATCH_DECISION_REJECTED, + KZT_PATCH_REASON_POLICY_WRAPPER_SYMBOL_ONLY, 0); + check_str("symbol-only.match-name", + kzt_patch_wrapper_match_name(decision.wrapper_match), + "SYMBOL_ONLY"); +} + +static void test_missing_symbol_version_is_malformed_input(void) +{ + kzt_patch_candidate_t candidate = base_candidate(); + kzt_patch_decision_t decision; + + candidate.version = NULL; + + check_int("missing-version.decide", + kzt_patch_planner_decide(&candidate, &decision), 0); + assert_decision("missing-version", &decision, + KZT_PATCH_DECISION_UNSUPPORTED, + KZT_PATCH_REASON_INPUT_MALFORMED_SYMBOL_VERSION, 0); +} + +static void test_confirmed_unversioned_evidence_is_approved(void) +{ + kzt_patch_candidate_t candidate = base_candidate(); + kzt_patch_decision_t decision; + + candidate.version_evidence = + KZT_SYMBOL_VERSION_CONFIRMED_UNVERSIONED; + candidate.version = NULL; + candidate.wrapper_match = KZT_PATCH_WRAPPER_UNVERSIONED_MATCH; + candidate.wrapper_version_evidence = + KZT_SYMBOL_VERSION_CONFIRMED_UNVERSIONED; + candidate.wrapper_symbol_version = NULL; + + check_int("unversioned.decide", + kzt_patch_planner_decide(&candidate, &decision), 0); + assert_decision("unversioned", &decision, + KZT_PATCH_DECISION_APPROVED, + KZT_PATCH_REASON_APPROVED_NATIVE_BRIDGE, 1); + check_int("unversioned.evidence", decision.version_evidence, + KZT_SYMBOL_VERSION_CONFIRMED_UNVERSIONED); +} + +static void test_unknown_and_error_version_evidence_are_rejected(void) +{ + kzt_patch_candidate_t candidate = base_candidate(); + kzt_patch_decision_t decision; + kzt_symbol_version_evidence_t evidence[] = { + KZT_SYMBOL_VERSION_UNKNOWN, + KZT_SYMBOL_VERSION_ERROR, + }; + size_t i; + + for (i = 0; i < sizeof(evidence) / sizeof(evidence[0]); ++i) { + candidate = base_candidate(); + candidate.version_evidence = evidence[i]; + check_int("untrusted-version.decide", + kzt_patch_planner_decide(&candidate, &decision), 0); + assert_decision("untrusted-version", &decision, + KZT_PATCH_DECISION_UNSUPPORTED, + KZT_PATCH_REASON_INPUT_MALFORMED_SYMBOL_VERSION, 0); + } +} + +static void test_no_manifest_is_unavailable_input(void) +{ + kzt_patch_candidate_t candidate = base_candidate(); + kzt_patch_decision_t decision; + + candidate.wrapper_match = KZT_PATCH_WRAPPER_NO_MANIFEST; + candidate.wrapper_name = NULL; + candidate.wrapper_symbol_version = NULL; + + check_int("no-manifest.decide", + kzt_patch_planner_decide(&candidate, &decision), 0); + assert_decision("no-manifest", &decision, + KZT_PATCH_DECISION_UNSUPPORTED, + KZT_PATCH_REASON_INPUT_UNAVAILABLE_WRAPPER_MANIFEST, 0); +} + +static void test_bridge_target_is_required_for_approval(void) +{ + kzt_patch_candidate_t candidate = base_candidate(); + kzt_patch_decision_t decision; + + candidate.bridge_target = 0; + + check_int("missing-bridge.decide", + kzt_patch_planner_decide(&candidate, &decision), 0); + assert_decision("missing-bridge", &decision, + KZT_PATCH_DECISION_UNSUPPORTED, + KZT_PATCH_REASON_INPUT_UNAVAILABLE_BRIDGE_TARGET, 0); +} + +static int test_matches_filter(const char *name, int argc, char **argv) +{ + int i; + + for (i = 1; i < argc; ++i) { + if (!strcmp(argv[i], "--filter") && i + 1 < argc) { + return strcmp(name, argv[i + 1]) == 0; + } + } + + return 1; +} + +int main(int argc, char **argv) +{ + if (test_matches_filter("complete_evidence_approves_native_bridge", + argc, argv)) { + test_complete_evidence_approves_native_bridge(); + } + if (test_matches_filter("unsupported_relocation_is_not_guessed", + argc, argv)) { + test_unsupported_relocation_is_not_guessed(); + } + if (test_matches_filter("dynamic_view_unavailable_is_unsupported", + argc, argv)) { + test_dynamic_view_unavailable_is_unsupported(); + } + if (test_matches_filter("owner_unknown_is_unsupported", argc, argv)) { + test_owner_unknown_is_unsupported(); + } + if (test_matches_filter("owner_mismatch_is_a_stable_rejection", + argc, argv)) { + test_owner_mismatch_is_a_stable_rejection(); + } + if (test_matches_filter("wrapper_version_mismatch_is_rejected", + argc, argv)) { + test_wrapper_version_mismatch_is_rejected(); + } + if (test_matches_filter("no_wrapper_rejects_to_keep_guest_target", + argc, argv)) { + test_no_wrapper_rejects_to_keep_guest_target(); + } + if (test_matches_filter("lazy_deferred_is_not_patched_yet", + argc, argv)) { + test_lazy_deferred_is_not_patched_yet(); + } + if (test_matches_filter("symbol_only_wrapper_rejects_to_keep_guest_target", + argc, argv)) { + test_symbol_only_wrapper_rejects_to_keep_guest_target(); + } + if (test_matches_filter("missing_symbol_version_is_malformed_input", + argc, argv)) { + test_missing_symbol_version_is_malformed_input(); + } + if (test_matches_filter("confirmed_unversioned_evidence_is_approved", + argc, argv)) { + test_confirmed_unversioned_evidence_is_approved(); + } + if (test_matches_filter( + "unknown_and_error_version_evidence_are_rejected", argc, argv)) { + test_unknown_and_error_version_evidence_are_rejected(); + } + if (test_matches_filter("no_manifest_is_unavailable_input", argc, argv)) { + test_no_manifest_is_unavailable_input(); + } + if (test_matches_filter("bridge_target_is_required_for_approval", + argc, argv)) { + test_bridge_target_is_required_for_approval(); + } + + if (failures) { + fprintf(stderr, "kzt-patch-planner: %d failure(s)\n", failures); + return 1; + } + + puts("kzt-patch-planner: selected contract tests passed"); + return 0; +} diff --git a/tests/unit/kzt/test_patch_spike_guard.c b/tests/unit/kzt/test_patch_spike_guard.c new file mode 100644 index 00000000000..c9d1f72941c --- /dev/null +++ b/tests/unit/kzt/test_patch_spike_guard.c @@ -0,0 +1,520 @@ +#include +#include +#include + +#include "kzt_test_options.h" +#include "target/i386/latx/include/kzt_patch_spike_guard.h" + +typedef struct fake_writer { + int read_calls; + int write_calls; + int verify_calls; + int rollback_calls; + int fail_read; + int fail_write; + int fail_verify; + int fail_rollback; + uintptr_t current_value; + uintptr_t last_written_value; +} fake_writer_t; + +static int failures; + +static void check_true(const char *name, int condition) +{ + if (condition) { + return; + } + + fprintf(stderr, "%s: condition failed\n", name); + ++failures; +} + +static void check_int(const char *name, int got, int expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got %d expected %d\n", name, got, expected); + ++failures; +} + +static void check_ulong(const char *name, unsigned long got, + unsigned long expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got %lu expected %lu\n", name, got, expected); + ++failures; +} + +static void check_uintptr(const char *name, uintptr_t got, + uintptr_t expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got 0x%lx expected 0x%lx\n", name, + (unsigned long)got, (unsigned long)expected); + ++failures; +} + +static kzt_patch_decision_t approved_decision(void) +{ + return (kzt_patch_decision_t) { + .kind = KZT_PATCH_DECISION_APPROVED, + .reason = KZT_PATCH_REASON_APPROVED_NATIVE_BRIDGE, + .allow_native_bridge = 1, + .slot_addr = 0x7100000018, + .slot_current_value_present = 1, + .slot_current_value = 0x7200001000, + .bridge_target = 0x7300002000, + }; +} + +static kzt_patch_spike_writer_status_t fake_write_slot( + const kzt_patch_decision_t *decision, + uintptr_t expected_value, + uintptr_t replacement_value, + uintptr_t *previous_value, + void *opaque) +{ + fake_writer_t *writer = opaque; + + (void)decision; + ++writer->read_calls; + if (writer->fail_read) { + return KZT_PATCH_SPIKE_WRITER_READ_FAILED; + } + + if (previous_value) { + *previous_value = writer->current_value; + } + if (writer->current_value != expected_value) { + return KZT_PATCH_SPIKE_WRITER_EXPECTED_MISMATCH; + } + + ++writer->write_calls; + writer->last_written_value = replacement_value; + if (writer->fail_write) { + return KZT_PATCH_SPIKE_WRITER_WRITE_FAILED; + } + + writer->current_value = replacement_value; + return KZT_PATCH_SPIKE_WRITER_OK; +} + +static int fake_verify_slot(const kzt_patch_decision_t *decision, + uintptr_t expected_value, + void *opaque) +{ + fake_writer_t *writer = opaque; + + (void)decision; + ++writer->verify_calls; + if (writer->fail_verify) { + return -1; + } + + return writer->current_value == expected_value ? 0 : -1; +} + +static int fake_rollback_slot(const kzt_patch_decision_t *decision, + uintptr_t previous_value, + void *opaque) +{ + fake_writer_t *writer = opaque; + + (void)decision; + ++writer->rollback_calls; + if (writer->fail_rollback) { + return -1; + } + + writer->current_value = previous_value; + return 0; +} + +static kzt_patch_spike_writer_ops_t writer_ops(fake_writer_t *writer) +{ + return (kzt_patch_spike_writer_ops_t) { + .write_slot = fake_write_slot, + .verify_slot = fake_verify_slot, + .rollback_slot = fake_rollback_slot, + .opaque = writer, + }; +} + +static kzt_patch_spike_guard_t init_guard(int enabled, int write_enabled, + unsigned long budget) +{ + kzt_patch_spike_config_t config = { + .enabled = enabled, + .write_enabled = write_enabled, + .budget = budget, + }; + kzt_patch_spike_guard_t guard; + + kzt_patch_spike_guard_init(&guard, &config); + return guard; +} + +static void assert_no_writer_calls(const char *name, + const fake_writer_t *writer) +{ + char field[128]; + + snprintf(field, sizeof(field), "%s.read", name); + check_int(field, writer->read_calls, 0); + snprintf(field, sizeof(field), "%s.write", name); + check_int(field, writer->write_calls, 0); + snprintf(field, sizeof(field), "%s.verify", name); + check_int(field, writer->verify_calls, 0); + snprintf(field, sizeof(field), "%s.rollback", name); + check_int(field, writer->rollback_calls, 0); +} + +static void test_default_config_is_closed_and_noop(void) +{ + kzt_patch_spike_config_t config = { 1, 1, 1 }; + kzt_patch_spike_guard_t guard; + kzt_patch_spike_outcome_t outcome; + fake_writer_t writer = { 0 }; + int fake_planner_calls = 0; + + option_kzt_patch_spike = 0; + option_kzt_patch_spike_write = 0; + option_kzt_patch_spike_budget = 0; + + kzt_patch_spike_config_from_options(&config); + check_int("default.enabled", config.enabled, 0); + check_int("default.write-enabled", config.write_enabled, 0); + check_ulong("default.budget", config.budget, 0); + + kzt_patch_spike_guard_init(&guard, &config); + if (kzt_patch_spike_guard_should_plan(&guard)) { + ++fake_planner_calls; + } + + check_int("default.should-plan", fake_planner_calls, 0); + check_int("default.try-write", + kzt_patch_spike_guard_try_write(&guard, NULL, NULL, + &outcome), 0); + check_int("default.result", outcome.result, + KZT_PATCH_SPIKE_RESULT_DISABLED); + check_int("default.action", outcome.action, + KZT_PATCH_SPIKE_ACTION_KEEP_LEGACY); + check_int("default.skip-legacy", outcome.skip_legacy_write, 0); + assert_no_writer_calls("default.writer", &writer); +} + +static void test_diagnostics_only_never_writes(void) +{ + kzt_patch_decision_t decision = approved_decision(); + kzt_patch_spike_guard_t guard = init_guard(1, 0, 4); + kzt_patch_spike_outcome_t outcome; + fake_writer_t writer = { + .current_value = decision.slot_current_value, + }; + kzt_patch_spike_writer_ops_t ops = writer_ops(&writer); + + check_int("diagnostics.should-plan", + kzt_patch_spike_guard_should_plan(&guard), 1); + check_int("diagnostics.try-write", + kzt_patch_spike_guard_try_write(&guard, &decision, &ops, + &outcome), 0); + check_int("diagnostics.result", outcome.result, + KZT_PATCH_SPIKE_RESULT_DIAGNOSTICS_ONLY); + check_int("diagnostics.failure", outcome.failure, + KZT_PATCH_SPIKE_FAILURE_WRITE_NOT_AUTHORIZED); + check_int("diagnostics.action", outcome.action, + KZT_PATCH_SPIKE_ACTION_KEEP_LEGACY); + check_int("diagnostics.skip-legacy", outcome.skip_legacy_write, 0); + assert_no_writer_calls("diagnostics.writer", &writer); +} + +static void test_spike_on_without_write_or_budget_is_noop(void) +{ + kzt_patch_decision_t decision = approved_decision(); + kzt_patch_spike_outcome_t outcome; + fake_writer_t writer = { + .current_value = decision.slot_current_value, + }; + kzt_patch_spike_writer_ops_t ops = writer_ops(&writer); + kzt_patch_spike_guard_t no_write = init_guard(1, 0, 1); + kzt_patch_spike_guard_t no_budget = init_guard(1, 1, 0); + + check_int("no-write.try-write", + kzt_patch_spike_guard_try_write(&no_write, &decision, &ops, + &outcome), 0); + check_int("no-write.result", outcome.result, + KZT_PATCH_SPIKE_RESULT_DIAGNOSTICS_ONLY); + check_int("no-write.skip-legacy", outcome.skip_legacy_write, 0); + assert_no_writer_calls("no-write.writer", &writer); + + check_int("no-budget.try-write", + kzt_patch_spike_guard_try_write(&no_budget, &decision, &ops, + &outcome), 0); + check_int("no-budget.result", outcome.result, + KZT_PATCH_SPIKE_RESULT_BUDGET_EXHAUSTED); + check_int("no-budget.failure", outcome.failure, + KZT_PATCH_SPIKE_FAILURE_BUDGET_EXHAUSTED); + check_int("no-budget.skip-legacy", outcome.skip_legacy_write, 0); + assert_no_writer_calls("no-budget.writer", &writer); +} + +static void test_approved_budget_one_allows_one_write(void) +{ + kzt_patch_decision_t decision = approved_decision(); + kzt_patch_spike_guard_t guard = init_guard(1, 1, 1); + kzt_patch_spike_outcome_t outcome; + fake_writer_t writer = { + .current_value = decision.slot_current_value, + }; + kzt_patch_spike_writer_ops_t ops = writer_ops(&writer); + + check_int("budget-one.first", + kzt_patch_spike_guard_try_write(&guard, &decision, &ops, + &outcome), 0); + check_int("budget-one.first-result", outcome.result, + KZT_PATCH_SPIKE_RESULT_APPLIED); + check_int("budget-one.first-action", outcome.action, + KZT_PATCH_SPIKE_ACTION_USE_NATIVE_BRIDGE); + check_int("budget-one.skip-legacy", outcome.skip_legacy_write, 1); + check_ulong("budget-one.remaining", outcome.writes_remaining, 0); + check_int("budget-one.reads", writer.read_calls, 1); + check_int("budget-one.writes", writer.write_calls, 1); + check_int("budget-one.verifies", writer.verify_calls, 1); + check_uintptr("budget-one.value", writer.current_value, + decision.bridge_target); + check_uintptr("budget-one.previous", outcome.previous_value, + decision.slot_current_value); + + writer.current_value = decision.slot_current_value; + check_int("budget-one.second", + kzt_patch_spike_guard_try_write(&guard, &decision, &ops, + &outcome), 0); + check_int("budget-one.second-result", outcome.result, + KZT_PATCH_SPIKE_RESULT_BUDGET_EXHAUSTED); + check_int("budget-one.second-skip-legacy", outcome.skip_legacy_write, 0); + check_int("budget-one.reads-after", writer.read_calls, 1); + check_int("budget-one.writes-after", writer.write_calls, 1); +} + +static void test_non_approved_decisions_fail_open_without_writes(void) +{ + kzt_patch_decision_kind_t kinds[] = { + KZT_PATCH_DECISION_REJECTED, + KZT_PATCH_DECISION_UNSUPPORTED, + KZT_PATCH_DECISION_DEFERRED, + KZT_PATCH_DECISION_ERROR, + }; + size_t i; + + for (i = 0; i < sizeof(kinds) / sizeof(kinds[0]); ++i) { + kzt_patch_decision_t decision = approved_decision(); + kzt_patch_spike_guard_t guard = init_guard(1, 1, 8); + kzt_patch_spike_outcome_t outcome; + fake_writer_t writer = { + .current_value = decision.slot_current_value, + }; + kzt_patch_spike_writer_ops_t ops = writer_ops(&writer); + + decision.kind = kinds[i]; + decision.allow_native_bridge = 0; + check_int("blocked.try-write", + kzt_patch_spike_guard_try_write(&guard, &decision, &ops, + &outcome), 0); + check_int("blocked.result", outcome.result, + KZT_PATCH_SPIKE_RESULT_FAIL_OPEN); + check_int("blocked.failure", outcome.failure, + KZT_PATCH_SPIKE_FAILURE_DECISION_NOT_APPROVED); + check_int("blocked.action", outcome.action, + KZT_PATCH_SPIKE_ACTION_KEEP_LEGACY); + check_int("blocked.skip-legacy", outcome.skip_legacy_write, 0); + assert_no_writer_calls("blocked.writer", &writer); + } +} + +static void run_fail_open_case(const char *name, + fake_writer_t writer, + kzt_patch_spike_failure_t expected_failure, + int expected_reads, + int expected_writes, + int expected_verifies, + int expected_rollbacks) +{ + kzt_patch_decision_t decision = approved_decision(); + kzt_patch_spike_guard_t guard = init_guard(1, 1, 8); + kzt_patch_spike_outcome_t outcome; + kzt_patch_spike_writer_ops_t ops = writer_ops(&writer); + char field[128]; + + check_int(name, + kzt_patch_spike_guard_try_write(&guard, &decision, &ops, + &outcome), 0); + snprintf(field, sizeof(field), "%s.result", name); + check_int(field, outcome.result, KZT_PATCH_SPIKE_RESULT_FAIL_OPEN); + snprintf(field, sizeof(field), "%s.failure", name); + check_int(field, outcome.failure, expected_failure); + snprintf(field, sizeof(field), "%s.action", name); + check_int(field, outcome.action, KZT_PATCH_SPIKE_ACTION_KEEP_LEGACY); + snprintf(field, sizeof(field), "%s.skip-legacy", name); + check_int(field, outcome.skip_legacy_write, 0); + snprintf(field, sizeof(field), "%s.reads", name); + check_int(field, writer.read_calls, expected_reads); + snprintf(field, sizeof(field), "%s.writes", name); + check_int(field, writer.write_calls, expected_writes); + snprintf(field, sizeof(field), "%s.verifies", name); + check_int(field, writer.verify_calls, expected_verifies); + snprintf(field, sizeof(field), "%s.rollbacks", name); + check_int(field, writer.rollback_calls, expected_rollbacks); + snprintf(field, sizeof(field), "%s.circuit", name); + check_int(field, kzt_patch_spike_guard_circuit_open(&guard), 0); +} + +static void test_writer_failures_fail_open(void) +{ + kzt_patch_decision_t decision = approved_decision(); + + run_fail_open_case("read-failure", (fake_writer_t) { + .current_value = decision.slot_current_value, + .fail_read = 1, + }, KZT_PATCH_SPIKE_FAILURE_READ_FAILED, 1, 0, 0, 0); + + run_fail_open_case("expected-mismatch", (fake_writer_t) { + .current_value = decision.slot_current_value + 8, + }, KZT_PATCH_SPIKE_FAILURE_EXPECTED_MISMATCH, 1, 0, 0, 0); + + run_fail_open_case("write-failure", (fake_writer_t) { + .current_value = decision.slot_current_value, + .fail_write = 1, + }, KZT_PATCH_SPIKE_FAILURE_WRITE_FAILED, 1, 1, 0, 0); + + run_fail_open_case("verify-failure", (fake_writer_t) { + .current_value = decision.slot_current_value, + .fail_verify = 1, + }, KZT_PATCH_SPIKE_FAILURE_VERIFY_FAILED, 1, 1, 1, 1); +} + +static void test_rollback_failure_opens_circuit_breaker(void) +{ + kzt_patch_decision_t decision = approved_decision(); + kzt_patch_spike_guard_t guard = init_guard(1, 1, 8); + kzt_patch_spike_outcome_t outcome; + fake_writer_t writer = { + .current_value = decision.slot_current_value, + .fail_verify = 1, + .fail_rollback = 1, + }; + kzt_patch_spike_writer_ops_t ops = writer_ops(&writer); + + check_int("rollback-failure.first", + kzt_patch_spike_guard_try_write(&guard, &decision, &ops, + &outcome), 0); + check_int("rollback-failure.result", outcome.result, + KZT_PATCH_SPIKE_RESULT_GUEST_PRESERVED); + check_int("rollback-failure.failure", outcome.failure, + KZT_PATCH_SPIKE_FAILURE_ROLLBACK_FAILED); + check_int("rollback-failure.rollback-called", outcome.rollback_called, 1); + check_int("rollback-failure.skip-legacy", outcome.skip_legacy_write, 1); + check_int("rollback-failure.circuit-open", + kzt_patch_spike_guard_circuit_open(&guard), 1); + + writer.fail_verify = 0; + writer.fail_rollback = 0; + writer.current_value = decision.slot_current_value; + check_int("rollback-failure.second", + kzt_patch_spike_guard_try_write(&guard, &decision, &ops, + &outcome), 0); + check_int("rollback-failure.second-result", outcome.result, + KZT_PATCH_SPIKE_RESULT_CIRCUIT_OPEN); + check_int("rollback-failure.second-failure", outcome.failure, + KZT_PATCH_SPIKE_FAILURE_CIRCUIT_BREAKER_OPEN); + check_int("rollback-failure.second-skip-legacy", + outcome.skip_legacy_write, 1); + check_int("rollback-failure.reads-after", writer.read_calls, 1); + check_int("rollback-failure.writes-after", writer.write_calls, 1); +} + +typedef struct concurrent_budget_case { + kzt_patch_spike_guard_t *guard; + kzt_patch_decision_t decision; + kzt_patch_spike_writer_ops_t ops; + kzt_patch_spike_outcome_t outcome; +} concurrent_budget_case_t; + +static void *run_concurrent_budget_case(void *opaque) +{ + concurrent_budget_case_t *test = opaque; + + kzt_patch_spike_guard_try_write(test->guard, &test->decision, + &test->ops, &test->outcome); + return NULL; +} + +static void test_concurrent_budget_reservation_allows_exactly_one_writer(void) +{ + enum { THREADS = 8 }; + kzt_patch_spike_guard_t guard = init_guard(1, 1, 1); + fake_writer_t writer = { + .current_value = 0x7200001000, + }; + kzt_patch_spike_writer_ops_t ops = writer_ops(&writer); + concurrent_budget_case_t cases[THREADS]; + pthread_t threads[THREADS]; + int i; + int applied = 0; + + for (i = 0; i < THREADS; ++i) { + cases[i] = (concurrent_budget_case_t) { + .guard = &guard, + .decision = approved_decision(), + .ops = ops, + }; + check_int("concurrent-budget.create", + pthread_create(&threads[i], NULL, + run_concurrent_budget_case, &cases[i]), 0); + } + for (i = 0; i < THREADS; ++i) { + check_int("concurrent-budget.join", pthread_join(threads[i], NULL), + 0); + applied += cases[i].outcome.result == KZT_PATCH_SPIKE_RESULT_APPLIED; + } + check_int("concurrent-budget.applied", applied, 1); + check_int("concurrent-budget.writer-calls", writer.write_calls, 1); + check_ulong("concurrent-budget.attempts", guard.write_attempts, 1); + check_ulong("concurrent-budget.successes", guard.write_successes, 1); + check_int("concurrent-budget.circuit", + kzt_patch_spike_guard_circuit_open(&guard), 0); +} + +int main(void) +{ + test_default_config_is_closed_and_noop(); + test_diagnostics_only_never_writes(); + test_spike_on_without_write_or_budget_is_noop(); + test_approved_budget_one_allows_one_write(); + test_non_approved_decisions_fail_open_without_writes(); + test_writer_failures_fail_open(); + test_rollback_failure_opens_circuit_breaker(); + test_concurrent_budget_reservation_allows_exactly_one_writer(); + + check_true("name.result", strcmp(kzt_patch_spike_result_name( + KZT_PATCH_SPIKE_RESULT_APPLIED), "APPLIED") == 0); + check_true("name.failure", strcmp(kzt_patch_spike_failure_name( + KZT_PATCH_SPIKE_FAILURE_ROLLBACK_FAILED), + "ROLLBACK_FAILED") == 0); + + if (failures) { + fprintf(stderr, "kzt-patch-spike-guard: %d failure(s)\n", failures); + return 1; + } + + puts("kzt-patch-spike-guard: all tests passed"); + return 0; +} diff --git a/tests/unit/kzt/test_patch_spike_writer.c b/tests/unit/kzt/test_patch_spike_writer.c new file mode 100644 index 00000000000..8ba12b0e5ae --- /dev/null +++ b/tests/unit/kzt/test_patch_spike_writer.c @@ -0,0 +1,907 @@ +#include +#include +#include + +#include "kzt_test_options.h" +#include "target/i386/latx/include/kzt_patch_spike_writer.h" + +static int failures; + +typedef struct fake_slot { + uintptr_t slot_addr; + uintptr_t value; + uintptr_t replacement_value; + uintptr_t previous_value; + int read_calls; + int write_calls; + int fail_replacement_write; + int fail_rollback_write; + int force_verify_mismatch; + int begin_calls; + int end_calls; + int fail_permission_begin; + int fail_permission_begin_after_enable; + int fail_permission_end; + int fail_generation; +} fake_slot_t; + +static void check_int(const char *name, int got, int expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got %d expected %d\n", name, got, expected); + ++failures; +} + +static void check_ulong(const char *name, unsigned long got, + unsigned long expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got %lu expected %lu\n", name, got, expected); + ++failures; +} + +static void check_uintptr(const char *name, uintptr_t got, uintptr_t expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got 0x%lx expected 0x%lx\n", name, + (unsigned long)got, (unsigned long)expected); + ++failures; +} + +static void check_ptr(const char *name, const void *got, const void *expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got %p expected %p\n", name, got, expected); + ++failures; +} + +static kzt_patch_decision_t approved_decision(uintptr_t slot_addr) +{ + return (kzt_patch_decision_t) { + .kind = KZT_PATCH_DECISION_APPROVED, + .reason = KZT_PATCH_REASON_APPROVED_NATIVE_BRIDGE, + .allow_native_bridge = 1, + .table_kind = KZT_PATCH_TABLE_PLT_RELA, + .entry_index = 3, + .entry_addr = 0x7100000040, + .reloc_type = KZT_PATCH_RELOCATION_JUMP_SLOT, + .source = { + .known = 1, + .link_map_addr = 0x7000001000, + .generation = 42, + }, + .dynamic_view_available = 1, + .dynamic_view_generation = 43, + .slot_addr = slot_addr, + .slot_current_value_present = 1, + .slot_current_value = 0x7200001000, + .current_owner = { + .known = 1, + .link_map_addr = 0x7200000000, + .generation = 44, + }, + .owner_match = KZT_PATCH_OWNER_MATCH, + .symbol_name = "puts", + .wrapper_name = "wrapped_puts", + .bridge_target = 0x7300002000, + }; +} + +static kzt_patch_candidate_t approved_candidate(uintptr_t slot_addr) +{ + return (kzt_patch_candidate_t) { + .source = { + .known = 1, + .link_map_addr = 0x7000001000, + .map_start = 0x7000000000, + .map_end = 0x7000008000, + .generation = 42, + .soname = "librequester.so", + .path = "/guest/lib/librequester.so", + }, + .dynamic_addr = 0x7000004000, + .load_bias = 0x7000000000, + .dynamic_view_generation = 43, + .dynamic_view_available = 1, + .table_kind = KZT_PATCH_TABLE_PLT_RELA, + .entry_index = 3, + .entry_addr = 0x7000004180, + .reloc_type = KZT_PATCH_RELOCATION_JUMP_SLOT, + .slot_addr = slot_addr, + .slot_current_value_present = 1, + .slot_current_value = 0x7200001000, + .symbol_index = 77, + .symbol_name = "gtk_widget_show", + .version = "GTK_3.0", + .current_owner = { + .known = 1, + .link_map_addr = 0x7200000000, + .map_start = 0x7200000000, + .map_end = 0x7200010000, + .generation = 44, + .soname = "libgtk-3.so", + .path = "/guest/lib/libgtk-3.so", + }, + .owner_match = KZT_PATCH_OWNER_MATCH, + .wrapper_match = KZT_PATCH_WRAPPER_VERSION_MATCH, + .wrapper_name = "wrappedgtk3", + .wrapper_symbol_version = "GTK_3.0", + .bridge_target = 0x7300002000, + }; +} + +static kzt_patch_spike_guard_t init_guard(int enabled, int write_enabled, + unsigned long budget) +{ + kzt_patch_spike_config_t config = { + .enabled = enabled, + .write_enabled = write_enabled, + .budget = budget, + }; + kzt_patch_spike_guard_t guard; + + kzt_patch_spike_guard_init(&guard, &config); + return guard; +} + +static int fake_read_slot(uintptr_t slot_addr, uintptr_t *value, void *opaque) +{ + fake_slot_t *slot = opaque; + + if (!slot || !value || slot_addr != slot->slot_addr) { + return -1; + } + + ++slot->read_calls; + if (slot->force_verify_mismatch && slot->read_calls == 2) { + *value = slot->replacement_value + 8; + return 0; + } + + *value = slot->value; + return 0; +} + +static int fake_write_slot(uintptr_t slot_addr, uintptr_t value, void *opaque) +{ + fake_slot_t *slot = opaque; + + if (!slot || slot_addr != slot->slot_addr) { + return -1; + } + ++slot->write_calls; + if (value == slot->replacement_value && slot->fail_replacement_write) { + return -1; + } + if (value == slot->previous_value && slot->fail_rollback_write) { + return -1; + } + + slot->value = value; + return 0; +} + +static int fake_begin_write(uintptr_t slot_addr, + kzt_patch_spike_permission_lease_t *lease, + void *opaque) +{ + fake_slot_t *slot = opaque; + + if (!slot || !lease || slot_addr != slot->slot_addr) { + return -1; + } + ++slot->begin_calls; + lease->checked = 1; + lease->guest_page = slot_addr & ~(uintptr_t)0xfff; + lease->guest_page_length = 0x1000; + lease->original_permissions = 5; + if (slot->fail_permission_begin) { + return -1; + } + lease->write_enabled = 1; + if (slot->fail_permission_begin_after_enable) { + return -1; + } + return 0; +} + +static int fake_end_write(kzt_patch_spike_permission_lease_t *lease, + void *opaque) +{ + fake_slot_t *slot = opaque; + + if (!slot || !lease) { + return -1; + } + ++slot->end_calls; + if (slot->fail_permission_end) { + return -1; + } + return 0; +} + +static int fake_validate_generation(const kzt_patch_decision_t *decision, + void *opaque) +{ + fake_slot_t *slot = opaque; + + return !decision || !slot || slot->fail_generation ? -1 : 0; +} + +static kzt_patch_spike_slot_ops_t fake_slot_ops(fake_slot_t *slot) +{ + return (kzt_patch_spike_slot_ops_t) { + .read_slot = fake_read_slot, + .write_slot = fake_write_slot, + .begin_write = fake_begin_write, + .end_write = fake_end_write, + .validate_generation = fake_validate_generation, + .opaque = slot, + }; +} + +static int trace_enabled(void) +{ + const char *value = getenv("KZT_PATCH_SPIKE_WRITER_TEST_TRACE"); + + return value && value[0] && strcmp(value, "0") != 0; +} + +static void trace_record(const char *tc, + const char *target, + const kzt_patch_decision_t *decision, + const kzt_patch_spike_record_t *record, + const fake_slot_t *slot) +{ + uintptr_t final_value = slot ? slot->value : 0; + + if (!trace_enabled() || !decision || !record) { + return; + } + + printf("KZT_SPIKE_TC tc=%s target=%s decision=%s reason=%s " + "table=%s reloc=%s symbol=%s wrapper=%s result=%s failure=%s " + "skip_legacy=%d writer_called=%d slot_addr=0x%lx " + "expected=0x%lx replacement=0x%lx observed=0x%lx " + "verified=0x%lx final=0x%lx rollback_called=%d " + "writes_remaining=%lu\n", + tc, target, + kzt_patch_decision_kind_name(decision->kind), + kzt_patch_reason_name(decision->reason), + kzt_patch_table_kind_name(decision->table_kind), + kzt_patch_relocation_type_name(decision->reloc_type), + decision->symbol_name ? decision->symbol_name : "(none)", + decision->wrapper_name ? decision->wrapper_name : "(none)", + kzt_patch_spike_result_name(record->result), + kzt_patch_spike_failure_name(record->failure), + record->skip_legacy_write, record->writer_called, + (unsigned long)decision->slot_addr, + (unsigned long)decision->slot_current_value, + (unsigned long)decision->bridge_target, + (unsigned long)record->observed_value, + (unsigned long)record->verified_value, + (unsigned long)final_value, + record->rollback_called, + record->writes_remaining); +} + +static void check_no_slot_calls(const char *name, const fake_slot_t *slot) +{ + char field[128]; + + snprintf(field, sizeof(field), "%s.read", name); + check_int(field, slot->read_calls, 0); + snprintf(field, sizeof(field), "%s.write", name); + check_int(field, slot->write_calls, 0); +} + +static void test_planner_approved_jump_slot_drives_writer(void) +{ + kzt_patch_candidate_t candidate = approved_candidate(0x7100000018); + kzt_patch_decision_t decision; + kzt_patch_spike_guard_t guard = init_guard(1, 1, 2); + kzt_patch_spike_record_t record; + fake_slot_t slot = { + .slot_addr = candidate.slot_addr, + .value = candidate.slot_current_value, + .replacement_value = candidate.bridge_target, + .previous_value = candidate.slot_current_value, + }; + kzt_patch_spike_slot_ops_t ops = fake_slot_ops(&slot); + + check_int("planner-writer.decide", + kzt_patch_planner_decide(&candidate, &decision), 0); + check_int("planner-writer.kind", decision.kind, + KZT_PATCH_DECISION_APPROVED); + check_int("planner-writer.reason", decision.reason, + KZT_PATCH_REASON_APPROVED_NATIVE_BRIDGE); + check_int("planner-writer.allow", decision.allow_native_bridge, 1); + check_int("planner-writer.reloc", decision.reloc_type, + KZT_PATCH_RELOCATION_JUMP_SLOT); + + check_int("planner-writer.apply", + kzt_patch_spike_writer_try_apply_with_slot_ops(&guard, + &decision, + &ops, + &record), 0); + check_int("planner-writer.result", record.result, + KZT_PATCH_SPIKE_RESULT_APPLIED); + check_int("planner-writer.failure", record.failure, + KZT_PATCH_SPIKE_FAILURE_NONE); + check_int("planner-writer.skip", record.skip_legacy_write, 1); + check_int("planner-writer.writer-called", record.writer_called, 1); + check_uintptr("planner-writer.final", slot.value, + candidate.bridge_target); + trace_record("TC1", "planner-approved-jump-slot", &decision, &record, + &slot); +} + +static void test_success_write_and_verify_with_direct_slot_ops(void) +{ + uintptr_t slot = 0x7200001000; + kzt_patch_decision_t decision = approved_decision((uintptr_t)&slot); + kzt_patch_spike_guard_t guard = init_guard(1, 1, 1); + kzt_patch_spike_record_t record; + + check_int("success.try-apply", + kzt_patch_spike_writer_try_apply(&guard, &decision, + &record), 0); + check_uintptr("success.slot", slot, decision.bridge_target); + check_int("success.result", record.result, KZT_PATCH_SPIKE_RESULT_APPLIED); + check_int("success.failure", record.failure, KZT_PATCH_SPIKE_FAILURE_NONE); + check_int("success.action", record.action, + KZT_PATCH_SPIKE_ACTION_USE_NATIVE_BRIDGE); + check_int("success.skip-legacy", record.skip_legacy_write, 1); + check_ulong("success.remaining", record.writes_remaining, 0); + check_int("success.writer-called", record.writer_called, 1); + check_int("success.read", record.read_attempted, 1); + check_int("success.expected-match", record.expected_current_matched, 1); + check_int("success.write-attempt", record.write_attempted, 1); + check_int("success.write-success", record.write_succeeded, 1); + check_int("success.verify-attempt", record.verify_attempted, 1); + check_int("success.verify-success", record.verify_succeeded, 1); + check_int("success.rollback", record.rollback_called, 0); + check_uintptr("success.previous", record.previous_value, + decision.slot_current_value); + check_uintptr("success.observed", record.observed_value, + decision.slot_current_value); + check_uintptr("success.verified", record.verified_value, + decision.bridge_target); +} + +static void test_expected_current_mismatch_does_not_write(void) +{ + kzt_patch_decision_t decision = approved_decision(0x7100000018); + kzt_patch_spike_guard_t guard = init_guard(1, 1, 8); + kzt_patch_spike_record_t record; + fake_slot_t slot = { + .slot_addr = decision.slot_addr, + .value = decision.slot_current_value + 4, + .replacement_value = decision.bridge_target, + .previous_value = decision.slot_current_value + 4, + }; + kzt_patch_spike_slot_ops_t ops = fake_slot_ops(&slot); + + check_int("mismatch.try-apply", + kzt_patch_spike_writer_try_apply_with_slot_ops(&guard, + &decision, + &ops, + &record), 0); + check_int("mismatch.result", record.result, + KZT_PATCH_SPIKE_RESULT_FAIL_OPEN); + check_int("mismatch.failure", record.failure, + KZT_PATCH_SPIKE_FAILURE_EXPECTED_MISMATCH); + check_int("mismatch.read-calls", slot.read_calls, 1); + check_int("mismatch.write-calls", slot.write_calls, 0); + check_int("mismatch.write-attempt", record.write_attempted, 0); + check_int("mismatch.expected-match", record.expected_current_matched, 0); + check_uintptr("mismatch.observed", record.observed_value, + decision.slot_current_value + 4); + trace_record("TC2", "expected-current-mismatch-fail-open", &decision, + &record, &slot); +} + +static void test_guard_diagnostics_only_does_not_call_writer(void) +{ + kzt_patch_decision_t decision = approved_decision(0x7100000018); + kzt_patch_spike_guard_t guard = init_guard(1, 0, 8); + kzt_patch_spike_record_t record; + fake_slot_t slot = { + .slot_addr = decision.slot_addr, + .value = decision.slot_current_value, + .replacement_value = decision.bridge_target, + .previous_value = decision.slot_current_value, + }; + kzt_patch_spike_slot_ops_t ops = fake_slot_ops(&slot); + + check_int("diagnostics.try-apply", + kzt_patch_spike_writer_try_apply_with_slot_ops(&guard, + &decision, + &ops, + &record), 0); + check_int("diagnostics.result", record.result, + KZT_PATCH_SPIKE_RESULT_DIAGNOSTICS_ONLY); + check_int("diagnostics.failure", record.failure, + KZT_PATCH_SPIKE_FAILURE_WRITE_NOT_AUTHORIZED); + check_int("diagnostics.writer-called", record.writer_called, 0); + check_no_slot_calls("diagnostics.slot", &slot); + trace_record("TC3", "diagnostics-only-keeps-legacy", &decision, &record, + &slot); +} + +static void test_write_failure_is_recorded(void) +{ + kzt_patch_decision_t decision = approved_decision(0x7100000018); + kzt_patch_spike_guard_t guard = init_guard(1, 1, 8); + kzt_patch_spike_record_t record; + fake_slot_t slot = { + .slot_addr = decision.slot_addr, + .value = decision.slot_current_value, + .replacement_value = decision.bridge_target, + .previous_value = decision.slot_current_value, + .fail_replacement_write = 1, + }; + kzt_patch_spike_slot_ops_t ops = fake_slot_ops(&slot); + + check_int("write-fail.try-apply", + kzt_patch_spike_writer_try_apply_with_slot_ops(&guard, + &decision, + &ops, + &record), 0); + check_int("write-fail.result", record.result, + KZT_PATCH_SPIKE_RESULT_FAIL_OPEN); + check_int("write-fail.failure", record.failure, + KZT_PATCH_SPIKE_FAILURE_WRITE_FAILED); + check_int("write-fail.read-calls", slot.read_calls, 1); + check_int("write-fail.write-calls", slot.write_calls, 1); + check_int("write-fail.write-attempt", record.write_attempted, 1); + check_int("write-fail.write-success", record.write_succeeded, 0); + check_uintptr("write-fail.slot", slot.value, decision.slot_current_value); + trace_record("TC4", "write-failure-fail-open", &decision, &record, + &slot); +} + +static void test_verify_failure_rolls_back_successfully(void) +{ + kzt_patch_decision_t decision = approved_decision(0x7100000018); + kzt_patch_spike_guard_t guard = init_guard(1, 1, 8); + kzt_patch_spike_record_t record; + fake_slot_t slot = { + .slot_addr = decision.slot_addr, + .value = decision.slot_current_value, + .replacement_value = decision.bridge_target, + .previous_value = decision.slot_current_value, + .force_verify_mismatch = 1, + }; + kzt_patch_spike_slot_ops_t ops = fake_slot_ops(&slot); + + check_int("verify-fail.try-apply", + kzt_patch_spike_writer_try_apply_with_slot_ops(&guard, + &decision, + &ops, + &record), 0); + check_int("verify-fail.result", record.result, + KZT_PATCH_SPIKE_RESULT_FAIL_OPEN); + check_int("verify-fail.failure", record.failure, + KZT_PATCH_SPIKE_FAILURE_VERIFY_FAILED); + check_int("verify-fail.read-calls", slot.read_calls, 3); + check_int("verify-fail.write-calls", slot.write_calls, 2); + check_int("verify-fail.verify-attempt", record.verify_attempted, 1); + check_int("verify-fail.verify-success", record.verify_succeeded, 0); + check_int("verify-fail.rollback-called", record.rollback_called, 1); + check_int("verify-fail.rollback-success", record.rollback_succeeded, 1); + check_int("verify-fail.rollback-verify", + record.rollback_verify_succeeded, 1); + check_uintptr("verify-fail.rollback-value", record.rollback_value, + decision.slot_current_value); + check_uintptr("verify-fail.slot", slot.value, decision.slot_current_value); + trace_record("TC5", "verify-failure-rolls-back", &decision, &record, + &slot); +} + +static void test_rollback_failure_is_observable(void) +{ + kzt_patch_decision_t decision = approved_decision(0x7100000018); + kzt_patch_spike_guard_t guard = init_guard(1, 1, 8); + kzt_patch_spike_record_t record; + fake_slot_t slot = { + .slot_addr = decision.slot_addr, + .value = decision.slot_current_value, + .replacement_value = decision.bridge_target, + .previous_value = decision.slot_current_value, + .force_verify_mismatch = 1, + .fail_rollback_write = 1, + }; + kzt_patch_spike_slot_ops_t ops = fake_slot_ops(&slot); + + check_int("rollback-fail.try-apply", + kzt_patch_spike_writer_try_apply_with_slot_ops(&guard, + &decision, + &ops, + &record), 0); + check_int("rollback-fail.result", record.result, + KZT_PATCH_SPIKE_RESULT_GUEST_PRESERVED); + check_int("rollback-fail.failure", record.failure, + KZT_PATCH_SPIKE_FAILURE_ROLLBACK_FAILED); + check_int("rollback-fail.rollback-called", record.rollback_called, 1); + check_int("rollback-fail.rollback-success", record.rollback_succeeded, 0); + check_int("rollback-fail.circuit", + kzt_patch_spike_guard_circuit_open(&guard), 1); + check_uintptr("rollback-fail.slot", slot.value, decision.bridge_target); + trace_record("TC6", "rollback-failure-opens-circuit", &decision, + &record, &slot); +} + +static void test_permission_enable_failure_does_not_write(void) +{ + kzt_patch_decision_t decision = approved_decision(0x7100000018); + kzt_patch_spike_guard_t guard = init_guard(1, 1, 8); + kzt_patch_spike_record_t record; + fake_slot_t slot = { + .slot_addr = decision.slot_addr, + .value = decision.slot_current_value, + .replacement_value = decision.bridge_target, + .previous_value = decision.slot_current_value, + .fail_permission_begin = 1, + }; + kzt_patch_spike_slot_ops_t ops = fake_slot_ops(&slot); + + check_int("permission-enable.apply", + kzt_patch_spike_writer_try_apply_with_slot_ops( + &guard, &decision, &ops, &record), 0); + check_int("permission-enable.failure", record.failure, + KZT_PATCH_SPIKE_FAILURE_PERMISSION_ENABLE_FAILED); + check_int("permission-enable.begin", slot.begin_calls, 1); + check_int("permission-enable.no-read", slot.read_calls, 0); + check_int("permission-enable.no-write", slot.write_calls, 0); + check_int("permission-enable.checked", record.permission_checked, 1); + check_int("permission-enable.opened", record.permission_write_enabled, 0); + check_int("permission-enable.preserve", record.skip_legacy_write, 1); +} + +static void test_permission_restore_failure_opens_circuit(void) +{ + kzt_patch_decision_t decision = approved_decision(0x7100000018); + kzt_patch_spike_guard_t guard = init_guard(1, 1, 8); + kzt_patch_spike_record_t record; + fake_slot_t slot = { + .slot_addr = decision.slot_addr, + .value = decision.slot_current_value, + .replacement_value = decision.bridge_target, + .previous_value = decision.slot_current_value, + .fail_permission_end = 1, + }; + kzt_patch_spike_slot_ops_t ops = fake_slot_ops(&slot); + + check_int("permission-restore.apply", + kzt_patch_spike_writer_try_apply_with_slot_ops( + &guard, &decision, &ops, &record), 0); + check_int("permission-restore.result", record.result, + KZT_PATCH_SPIKE_RESULT_GUEST_PRESERVED); + check_int("permission-restore.failure", record.failure, + KZT_PATCH_SPIKE_FAILURE_PERMISSION_RESTORE_FAILED); + check_int("permission-restore.end", slot.end_calls, 1); + check_int("permission-restore.write", record.write_succeeded, 1); + check_int("permission-restore.verify", record.verify_succeeded, 1); + check_int("permission-restore.attempted", + record.permission_restore_attempted, 1); + check_int("permission-restore.restored", record.permission_restored, 0); + check_int("permission-restore.circuit", + kzt_patch_spike_guard_circuit_open(&guard), 1); + check_int("permission-restore.preserve", record.skip_legacy_write, 1); +} + +static void test_partial_permission_enable_is_restored(void) +{ + kzt_patch_decision_t decision = approved_decision(0x7100000018); + kzt_patch_spike_guard_t guard = init_guard(1, 1, 8); + kzt_patch_spike_record_t record; + fake_slot_t slot = { + .slot_addr = decision.slot_addr, + .value = decision.slot_current_value, + .replacement_value = decision.bridge_target, + .previous_value = decision.slot_current_value, + .fail_permission_begin_after_enable = 1, + }; + kzt_patch_spike_slot_ops_t ops = fake_slot_ops(&slot); + + check_int("permission-partial.apply", + kzt_patch_spike_writer_try_apply_with_slot_ops( + &guard, &decision, &ops, &record), 0); + check_int("permission-partial.failure", record.failure, + KZT_PATCH_SPIKE_FAILURE_PERMISSION_ENABLE_FAILED); + check_int("permission-partial.begin", slot.begin_calls, 1); + check_int("permission-partial.end", slot.end_calls, 1); + check_int("permission-partial.restore", record.permission_restored, 1); + check_int("permission-partial.no-write", slot.write_calls, 0); +} + +static void test_generation_mismatch_does_not_touch_permissions_or_slot(void) +{ + kzt_patch_decision_t decision = approved_decision(0x7100000018); + kzt_patch_spike_guard_t guard = init_guard(1, 1, 8); + kzt_patch_spike_record_t record; + fake_slot_t slot = { + .slot_addr = decision.slot_addr, + .value = decision.slot_current_value, + .replacement_value = decision.bridge_target, + .previous_value = decision.slot_current_value, + .fail_generation = 1, + }; + kzt_patch_spike_slot_ops_t ops = fake_slot_ops(&slot); + + check_int("generation.apply", kzt_patch_spike_writer_try_apply_with_slot_ops( + &guard, &decision, &ops, &record), 0); + check_int("generation.failure", record.failure, + KZT_PATCH_SPIKE_FAILURE_GENERATION_MISMATCH); + check_int("generation.result", record.result, + KZT_PATCH_SPIKE_RESULT_GUEST_PRESERVED); + check_int("generation.checked", record.generation_checked, 1); + check_int("generation.match", record.generation_matched, 0); + check_int("generation.no-permission", slot.begin_calls, 0); + check_no_slot_calls("generation.slot", &slot); +} + +static void test_non_approved_decision_does_not_write(void) +{ + kzt_patch_decision_t decision = approved_decision(0x7100000018); + kzt_patch_spike_guard_t guard = init_guard(1, 1, 8); + kzt_patch_spike_record_t record; + fake_slot_t slot = { + .slot_addr = decision.slot_addr, + .value = decision.slot_current_value, + .replacement_value = decision.bridge_target, + .previous_value = decision.slot_current_value, + }; + kzt_patch_spike_slot_ops_t ops = fake_slot_ops(&slot); + + decision.kind = KZT_PATCH_DECISION_REJECTED; + decision.reason = KZT_PATCH_REASON_POLICY_KEEP_GUEST; + decision.allow_native_bridge = 0; + check_int("rejected.try-apply", + kzt_patch_spike_writer_try_apply_with_slot_ops(&guard, + &decision, + &ops, + &record), 0); + check_int("rejected.result", record.result, + KZT_PATCH_SPIKE_RESULT_FAIL_OPEN); + check_int("rejected.failure", record.failure, + KZT_PATCH_SPIKE_FAILURE_DECISION_NOT_APPROVED); + check_int("rejected.writer-called", record.writer_called, 0); + check_no_slot_calls("rejected.slot", &slot); +} + +static void test_approved_non_jump_slot_does_not_consume_guard_budget(void) +{ + kzt_patch_decision_t decision = approved_decision(0x7100000018); + kzt_patch_spike_guard_t guard = init_guard(1, 1, 8); + kzt_patch_spike_record_t record; + fake_slot_t slot = { + .slot_addr = decision.slot_addr, + .value = decision.slot_current_value, + .replacement_value = decision.bridge_target, + .previous_value = decision.slot_current_value, + }; + kzt_patch_spike_slot_ops_t ops = fake_slot_ops(&slot); + + decision.table_kind = KZT_PATCH_TABLE_RELA; + decision.reloc_type = KZT_PATCH_RELOCATION_GLOB_DAT; + check_int("glob-dat.try-apply", + kzt_patch_spike_writer_try_apply_with_slot_ops(&guard, + &decision, + &ops, + &record), 0); + check_int("glob-dat.result", record.result, + KZT_PATCH_SPIKE_RESULT_FAIL_OPEN); + check_int("glob-dat.failure", record.failure, + KZT_PATCH_SPIKE_FAILURE_INVALID_ARGUMENT); + check_int("glob-dat.writer-called", record.writer_called, 0); + check_ulong("glob-dat.remaining", record.writes_remaining, 8); + check_no_slot_calls("glob-dat.slot", &slot); +} + +static void test_persistent_guard_budget_blocks_second_write(void) +{ + kzt_patch_spike_guard_t guard = init_guard(1, 1, 1); + kzt_patch_decision_t first_decision = approved_decision(0x7100000018); + kzt_patch_decision_t second_decision = approved_decision(0x7100000020); + kzt_patch_spike_record_t first_record; + kzt_patch_spike_record_t second_record; + fake_slot_t first_slot = { + .slot_addr = first_decision.slot_addr, + .value = first_decision.slot_current_value, + .replacement_value = first_decision.bridge_target, + .previous_value = first_decision.slot_current_value, + }; + fake_slot_t second_slot = { + .slot_addr = second_decision.slot_addr, + .value = second_decision.slot_current_value, + .replacement_value = second_decision.bridge_target, + .previous_value = second_decision.slot_current_value, + }; + kzt_patch_spike_slot_ops_t first_ops = fake_slot_ops(&first_slot); + kzt_patch_spike_slot_ops_t second_ops = fake_slot_ops(&second_slot); + + check_int("persistent-budget.first-apply", + kzt_patch_spike_writer_try_apply_with_slot_ops(&guard, + &first_decision, + &first_ops, + &first_record), 0); + check_int("persistent-budget.first-result", first_record.result, + KZT_PATCH_SPIKE_RESULT_APPLIED); + check_int("persistent-budget.first-skip", first_record.skip_legacy_write, + 1); + check_uintptr("persistent-budget.first-slot", first_slot.value, + first_decision.bridge_target); + + check_int("persistent-budget.second-apply", + kzt_patch_spike_writer_try_apply_with_slot_ops(&guard, + &second_decision, + &second_ops, + &second_record), 0); + check_int("persistent-budget.second-result", second_record.result, + KZT_PATCH_SPIKE_RESULT_BUDGET_EXHAUSTED); + check_int("persistent-budget.second-failure", second_record.failure, + KZT_PATCH_SPIKE_FAILURE_BUDGET_EXHAUSTED); + check_int("persistent-budget.second-writer", second_record.writer_called, + 0); + check_int("persistent-budget.second-skip", second_record.skip_legacy_write, + 0); + check_ulong("persistent-budget.second-remaining", + second_record.writes_remaining, 0); + check_uintptr("persistent-budget.second-slot", second_slot.value, + second_decision.slot_current_value); + check_no_slot_calls("persistent-budget.second-slot-calls", &second_slot); + check_ulong("persistent-budget.attempts", guard.write_attempts, 1); + check_ulong("persistent-budget.successes", guard.write_successes, 1); + trace_record("TC7", "persistent-guard-budget-exhausted", + &second_decision, &second_record, &second_slot); +} + +static void test_persistent_guard_circuit_blocks_second_write(void) +{ + kzt_patch_spike_guard_t guard = init_guard(1, 1, 8); + kzt_patch_decision_t first_decision = approved_decision(0x7100000018); + kzt_patch_decision_t second_decision = approved_decision(0x7100000020); + kzt_patch_spike_record_t first_record; + kzt_patch_spike_record_t second_record; + fake_slot_t first_slot = { + .slot_addr = first_decision.slot_addr, + .value = first_decision.slot_current_value, + .replacement_value = first_decision.bridge_target, + .previous_value = first_decision.slot_current_value, + .force_verify_mismatch = 1, + .fail_rollback_write = 1, + }; + fake_slot_t second_slot = { + .slot_addr = second_decision.slot_addr, + .value = second_decision.slot_current_value, + .replacement_value = second_decision.bridge_target, + .previous_value = second_decision.slot_current_value, + }; + kzt_patch_spike_slot_ops_t first_ops = fake_slot_ops(&first_slot); + kzt_patch_spike_slot_ops_t second_ops = fake_slot_ops(&second_slot); + + check_int("persistent-circuit.first-apply", + kzt_patch_spike_writer_try_apply_with_slot_ops(&guard, + &first_decision, + &first_ops, + &first_record), 0); + check_int("persistent-circuit.first-result", first_record.result, + KZT_PATCH_SPIKE_RESULT_GUEST_PRESERVED); + check_int("persistent-circuit.first-failure", first_record.failure, + KZT_PATCH_SPIKE_FAILURE_ROLLBACK_FAILED); + check_int("persistent-circuit.first-rollback", first_record.rollback_called, + 1); + check_int("persistent-circuit.open", + kzt_patch_spike_guard_circuit_open(&guard), 1); + check_uintptr("persistent-circuit.first-slot", first_slot.value, + first_decision.bridge_target); + + check_int("persistent-circuit.second-apply", + kzt_patch_spike_writer_try_apply_with_slot_ops(&guard, + &second_decision, + &second_ops, + &second_record), 0); + check_int("persistent-circuit.second-result", second_record.result, + KZT_PATCH_SPIKE_RESULT_CIRCUIT_OPEN); + check_int("persistent-circuit.second-failure", second_record.failure, + KZT_PATCH_SPIKE_FAILURE_CIRCUIT_BREAKER_OPEN); + check_int("persistent-circuit.second-writer", second_record.writer_called, + 0); + check_int("persistent-circuit.second-skip", second_record.skip_legacy_write, + 1); + check_uintptr("persistent-circuit.second-slot", second_slot.value, + second_decision.slot_current_value); + check_no_slot_calls("persistent-circuit.second-slot-calls", &second_slot); + check_ulong("persistent-circuit.attempts", guard.write_attempts, 1); + check_ulong("persistent-circuit.successes", guard.write_successes, 0); +} + +static void test_record_fields_are_complete(void) +{ + uintptr_t slot = 0x7200001000; + kzt_patch_decision_t decision = approved_decision((uintptr_t)&slot); + kzt_patch_spike_guard_t guard = init_guard(1, 1, 2); + kzt_patch_spike_record_t record; + + decision.source.generation = 42; + decision.current_owner.generation = 43; + decision.dynamic_view_generation = 44; + + check_int("record.try-apply", + kzt_patch_spike_writer_try_apply(&guard, &decision, + &record), 0); + check_int("record.valid", record.valid, 1); + check_int("record.kind", record.decision_kind, decision.kind); + check_int("record.reason", record.decision_reason, decision.reason); + check_int("record.allow", record.allow_native_bridge, + decision.allow_native_bridge); + check_int("record.table", record.table_kind, decision.table_kind); + check_int("record.reloc", record.reloc_type, decision.reloc_type); + check_ulong("record.entry-index", record.entry_index, + decision.entry_index); + check_uintptr("record.entry", record.entry_addr, decision.entry_addr); + check_uintptr("record.slot", record.slot_addr, decision.slot_addr); + check_uintptr("record.source-link-map", record.source_link_map, + decision.source.link_map_addr); + check_uintptr("record.owner-link-map", record.current_owner_link_map, + decision.current_owner.link_map_addr); + check_ulong("record.source-generation", record.source_generation, 42); + check_ulong("record.owner-generation", record.current_owner_generation, + 43); + check_ulong("record.dynamic-generation", record.dynamic_view_generation, + 44); + check_int("record.expected-present", record.expected_value_present, 1); + check_uintptr("record.expected", record.expected_value, + decision.slot_current_value); + check_uintptr("record.replacement", record.replacement_value, + decision.bridge_target); + check_ptr("record.symbol", record.symbol_name, decision.symbol_name); + check_ptr("record.wrapper", record.wrapper_name, decision.wrapper_name); + check_int("record.result", record.result, KZT_PATCH_SPIKE_RESULT_APPLIED); + check_int("record.failure", record.failure, KZT_PATCH_SPIKE_FAILURE_NONE); + check_int("record.action", record.action, + KZT_PATCH_SPIKE_ACTION_USE_NATIVE_BRIDGE); + check_ulong("record.remaining", record.writes_remaining, 1); +} + +int main(void) +{ + test_planner_approved_jump_slot_drives_writer(); + test_success_write_and_verify_with_direct_slot_ops(); + test_expected_current_mismatch_does_not_write(); + test_guard_diagnostics_only_does_not_call_writer(); + test_write_failure_is_recorded(); + test_verify_failure_rolls_back_successfully(); + test_rollback_failure_is_observable(); + test_permission_enable_failure_does_not_write(); + test_permission_restore_failure_opens_circuit(); + test_partial_permission_enable_is_restored(); + test_generation_mismatch_does_not_touch_permissions_or_slot(); + test_non_approved_decision_does_not_write(); + test_approved_non_jump_slot_does_not_consume_guard_budget(); + test_persistent_guard_budget_blocks_second_write(); + test_persistent_guard_circuit_blocks_second_write(); + test_record_fields_are_complete(); + + if (failures) { + fprintf(stderr, "kzt-patch-spike-writer: %d failure(s)\n", failures); + return 1; + } + + puts("kzt-patch-spike-writer: all tests passed"); + return 0; +} diff --git a/tests/unit/kzt/test_real_guest_e2e.py b/tests/unit/kzt/test_real_guest_e2e.py new file mode 100755 index 00000000000..8e845bbd7f0 --- /dev/null +++ b/tests/unit/kzt/test_real_guest_e2e.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python3 +import argparse +import os +from pathlib import Path +import re +import subprocess +import sys + + +SUCCESS_PATTERN = re.compile( + r"KZT_GUEST_E2E_OK calls=2 slot=(0x[0-9a-f]+) " + r"before=(0x[0-9a-f]+) after_first=(0x[0-9a-f]+) " + r"after_second=(0x[0-9a-f]+) first_ns=(0x[0-9a-f]+) " + r"second_ns=(0x[0-9a-f]+)" +) +TARGET_SYMBOL = "uname" + + +def parse_record(line): + record = {} + for field in line.split(): + if "=" in field: + key, value = field.split("=", 1) + record[key] = value + return record + + +def symbol_records(output, prefix): + records = [] + for line in output.splitlines(): + marker = line.find(prefix) + if marker >= 0: + record = parse_record(line[marker:]) + if record.get("symbol") == TARGET_SYMBOL: + records.append((line, record)) + return records + + +def require(condition, message, output=None): + if condition: + return + if output: + message = f"{message}\n--- guest output ---\n{output.rstrip()}" + raise RuntimeError(message) + + +def verify_cross_arch_version(args): + guest_version = ( + args.fixture_dir / "guest-symbol-version.txt" + ).read_text(encoding="utf-8").strip() + completed = subprocess.run( + [args.readelf, "-Ws", str(args.host_libc)], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + timeout=args.timeout, + check=False, + ) + require(completed.returncode == 0, + "Could not inspect the host libc symbol versions.", + completed.stdout) + host_versions = set(re.findall( + rf"\b{TARGET_SYMBOL}@@?(GLIBC_[^\s]+)", completed.stdout + )) + require(len(host_versions) == 1, + "Host libc must expose one inspectable uname symbol version.", + completed.stdout) + host_version = next(iter(host_versions)) + require(host_version != guest_version, + "Cross-architecture fixture requires different guest and host " + "uname versions.") + print( + f"Cross-architecture versions: guest={guest_version} " + f"host={host_version}" + ) + + +def run_mode(args, mode, budget, expected_writer, expected_fallback): + executable = args.fixture_dir / "kzt_guest_main" + command = [ + str(args.latx), + "-L", + str(args.guest_root), + "-E", + f"LD_LIBRARY_PATH={args.fixture_dir}", + str(executable), + ] + environment = os.environ.copy() + environment.update( + { + "LATX_KZT": "1", + "LATX_KZT_REGISTRY_DIAGNOSTICS": "1", + "LATX_KZT_LAZY_DIAGNOSTICS": "1", + "LATX_KZT_PATCH_SPIKE": "1", + "LATX_KZT_PATCH_SPIKE_WRITE": "1", + "LATX_KZT_PATCH_SPIKE_BUDGET": str(budget), + } + ) + completed = subprocess.run( + command, + env=environment, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + timeout=args.timeout, + check=False, + ) + output = completed.stdout + log_path = args.log_dir / f"real-guest-{mode}.log" + log_path.write_text(output, encoding="utf-8") + + require(completed.returncode == 0, + f"{mode} mode exited with {completed.returncode}.", output) + success_matches = SUCCESS_PATTERN.findall(output) + require(len(success_matches) == 1, + f"{mode} mode did not complete both uname calls.", output) + slot, before, after_first, after_second, first_ns, second_ns = ( + int(value, 16) for value in success_matches[0] + ) + require(slot != 0 and before != 0, + f"{mode} mode reported an invalid uname GOT slot.", output) + require(after_first != before, + f"{mode} mode did not replace the unresolved uname slot.", output) + require(after_second == after_first, + f"{mode} mode changed the uname slot on the second call.", output) + require(first_ns > 0 and second_ns > 0, + f"{mode} mode did not report call timings.", output) + + rela_records = symbol_records(output, "kzt_rela_diagnostic ") + require(len(rela_records) == 1, + f"{mode} mode expected one uname relocation record, " + f"found {len(rela_records)}.", output) + _, rela = rela_records[0] + require(rela.get("decision") == "APPROVED", + f"{mode} mode did not approve the uname patch.", output) + require(rela.get("writer_result") == expected_writer, + f"{mode} mode writer result is not {expected_writer}.", output) + require(rela.get("legacy_fallback") == expected_fallback, + f"{mode} mode legacy fallback is not {expected_fallback}.", output) + if expected_writer == "APPLIED": + require(int(rela.get("bridge_target", "0"), 16) == after_first, + f"{mode} mode GOT value does not match the approved bridge.", + output) + + resolver_entries = symbol_records(output, "kzt_lazy_resolver_entry ") + require(len(resolver_entries) == 1, + f"{mode} mode entered the uname resolver " + f"{len(resolver_entries)} times instead of once.", output) + + lazy_records = symbol_records(output, "kzt_lazy_diagnostic ") + require(len(lazy_records) == 1, + f"{mode} mode resolved uname more than once or missed it; " + f"found {len(lazy_records)} lazy records.", output) + return log_path + + +def existing_directory(value): + path = Path(value).resolve() + if not path.is_dir(): + raise argparse.ArgumentTypeError(f"Directory does not exist: {path}") + return path + + +def existing_file(value): + path = Path(value).resolve() + if not path.is_file(): + raise argparse.ArgumentTypeError(f"File does not exist: {path}") + return path + + +def parse_args(): + parser = argparse.ArgumentParser( + description="Run the KZT real x86-64 guest lazy-binding gate." + ) + parser.add_argument("--latx", required=True, type=existing_file) + parser.add_argument("--guest-root", required=True, type=existing_directory) + parser.add_argument("--host-libc", required=True, type=existing_file) + parser.add_argument("--fixture-dir", required=True, type=existing_directory) + parser.add_argument("--readelf", default="readelf") + parser.add_argument("--log-dir", type=Path) + parser.add_argument("--timeout", type=float, default=30.0) + args = parser.parse_args() + if args.log_dir is None: + args.log_dir = args.fixture_dir / "logs" + else: + args.log_dir = args.log_dir.resolve() + args.log_dir.mkdir(parents=True, exist_ok=True) + require((args.fixture_dir / "kzt_guest_main").is_file(), + "Fixture directory is missing kzt_guest_main.") + require((args.fixture_dir / "libkzt_guest_probe.so").is_file(), + "Fixture directory is missing libkzt_guest_probe.so.") + require((args.fixture_dir / "guest-symbol-version.txt").is_file(), + "Fixture directory is missing guest-symbol-version.txt.") + return args + + +def main(): + args = parse_args() + verify_cross_arch_version(args) + normal_log = run_mode(args, "applied", 1, "APPLIED", "0") + fail_open_log = run_mode( + args, "fail-open", 0, "BUDGET_EXHAUSTED", "1" + ) + print("KZT real guest E2E: PASS") + print(f"Applied log: {normal_log}") + print(f"Fail-open log: {fail_open_log}") + + +if __name__ == "__main__": + try: + main() + except (RuntimeError, subprocess.TimeoutExpired) as error: + print(f"KZT real guest E2E: FAIL: {error}", file=sys.stderr) + sys.exit(1) diff --git a/tests/unit/kzt/test_real_guest_performance.py b/tests/unit/kzt/test_real_guest_performance.py new file mode 100644 index 00000000000..ffc8965bfd9 --- /dev/null +++ b/tests/unit/kzt/test_real_guest_performance.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +import argparse +import json +import os +from pathlib import Path +import random +import statistics +import subprocess +import sys + +from test_real_guest_e2e import SUCCESS_PATTERN, existing_directory, existing_file + + +MODES = { + "legacy": {"LATX_KZT": "0"}, + "fail_open": { + "LATX_KZT": "1", + "LATX_KZT_PATCH_SPIKE": "1", + "LATX_KZT_PATCH_SPIKE_WRITE": "1", + "LATX_KZT_PATCH_SPIKE_BUDGET": "0", + }, + "applied": { + "LATX_KZT": "1", + "LATX_KZT_PATCH_SPIKE": "1", + "LATX_KZT_PATCH_SPIKE_WRITE": "1", + "LATX_KZT_PATCH_SPIKE_BUDGET": "1", + }, +} + + +def run_once(args, mode): + environment = os.environ.copy() + for name in list(environment): + if name.startswith("LATX_KZT"): + environment.pop(name) + environment.update(MODES[mode]) + completed = subprocess.run( + [ + str(args.latx), "-L", str(args.guest_root), + "-E", f"LD_LIBRARY_PATH={args.fixture_dir}", + str(args.fixture_dir / "kzt_guest_main"), + ], + env=environment, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + timeout=args.timeout, + check=False, + ) + if completed.returncode != 0: + raise RuntimeError( + f"{mode} exited with {completed.returncode}:\n{completed.stdout}" + ) + matches = SUCCESS_PATTERN.findall(completed.stdout) + if len(matches) != 1: + raise RuntimeError(f"{mode} did not emit one timing record") + values = [int(value, 16) for value in matches[0]] + return {"first_ns": values[4], "second_ns": values[5]} + + +def regression_percent(value, baseline): + return (value / baseline - 1.0) * 100.0 + + +def main(): + parser = argparse.ArgumentParser( + description="Run the KZT real guest first-binding performance gate." + ) + parser.add_argument("--latx", required=True, type=existing_file) + parser.add_argument("--guest-root", required=True, type=existing_directory) + parser.add_argument("--fixture-dir", required=True, type=existing_directory) + parser.add_argument("--iterations", type=int, default=200) + parser.add_argument("--warmup", type=int, default=20) + parser.add_argument("--timeout", type=float, default=30.0) + parser.add_argument("--seed", type=int, default=20260720) + parser.add_argument("--max-first-regression-percent", type=float, + default=3.0) + parser.add_argument("--max-steady-regression-percent", type=float, + default=1.0) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + if args.iterations < 20 or args.warmup < 0: + parser.error("iterations must be >= 20 and warmup must be >= 0") + + samples = {mode: {"first_ns": [], "second_ns": []} for mode in MODES} + rng = random.Random(args.seed) + for round_index in range(args.warmup + args.iterations): + order = list(MODES) + rng.shuffle(order) + for mode in order: + sample = run_once(args, mode) + if round_index >= args.warmup: + for metric, value in sample.items(): + samples[mode][metric].append(value) + + medians = { + mode: { + metric: statistics.median(values) + for metric, values in metrics.items() + } + for mode, metrics in samples.items() + } + regressions = { + mode: { + metric: regression_percent( + medians[mode][metric], medians["legacy"][metric] + ) + for metric in ("first_ns", "second_ns") + } + for mode in ("fail_open", "applied") + } + report = { + "iterations": args.iterations, + "warmup": args.warmup, + "seed": args.seed, + "limits_percent": { + "first_ns": args.max_first_regression_percent, + "second_ns": args.max_steady_regression_percent, + }, + "medians_ns": medians, + "regressions_percent": regressions, + "samples_ns": samples, + } + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(report, indent=2) + "\n", + encoding="utf-8") + + failures = [] + for mode, metrics in regressions.items(): + if metrics["first_ns"] > args.max_first_regression_percent: + failures.append(f"{mode} first={metrics['first_ns']:.2f}%") + if metrics["second_ns"] > args.max_steady_regression_percent: + failures.append(f"{mode} second={metrics['second_ns']:.2f}%") + print(json.dumps({ + "medians_ns": medians, + "regressions_percent": regressions, + "result": "FAIL" if failures else "PASS", + }, indent=2)) + if failures: + raise RuntimeError("performance gate exceeded: " + ", ".join(failures)) + + +if __name__ == "__main__": + try: + main() + except (RuntimeError, subprocess.TimeoutExpired) as error: + print(f"KZT real guest performance: FAIL: {error}", file=sys.stderr) + sys.exit(1) diff --git a/tests/unit/kzt/test_registry_diagnostics_gate.c b/tests/unit/kzt/test_registry_diagnostics_gate.c new file mode 100644 index 00000000000..005f3f5e5f6 --- /dev/null +++ b/tests/unit/kzt/test_registry_diagnostics_gate.c @@ -0,0 +1,300 @@ +#include +#include +#include + +#include "target/i386/latx/include/box64context.h" +#include "target/i386/latx/include/debug.h" +#include "target/i386/latx/include/kzt_observation_adapter.h" + +int relocation_log; +int kzt_registry_diagnostics; +int option_kzt; +int wine_option_kzt; + +typedef struct gate_trace { + int reader_calls; + int legacy_calls; + int diagnostic_outputs; + int legacy_return; + uintptr_t legacy_link_map_addr; + kzt_observation_adapter_result_t result; + kzt_observation_adapter_result_t diagnostic_result; + int diagnostic_emitted; +} gate_trace_t; + +typedef struct gate_fixture { + struct link_map_x64 link_map; + char guest_name[64]; + kzt_guest_link_map_reader_ops_t reader_ops; + gate_trace_t trace; +} gate_fixture_t; + +static int failures; + +static void check_true(const char *name, int condition) +{ + if (condition) { + return; + } + + fprintf(stderr, "%s: condition failed\n", name); + ++failures; +} + +static void check_int(const char *name, int got, int expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got %d expected %d\n", name, got, expected); + ++failures; +} + +static int fake_read_memory(uintptr_t guest_addr, void *dst, size_t size, + void *opaque) +{ + gate_fixture_t *fixture = opaque; + uintptr_t base = (uintptr_t)&fixture->link_map; + size_t available = sizeof(*fixture) - + offsetof(gate_fixture_t, link_map); + + ++fixture->trace.reader_calls; + if (guest_addr < base || size > available || + guest_addr - base > available - size) { + return -1; + } + + memcpy(dst, (const void *)guest_addr, size); + return 0; +} + +static int fake_legacy_flow(uintptr_t link_map_addr, void *opaque) +{ + gate_trace_t *trace = opaque; + + ++trace->legacy_calls; + trace->legacy_link_map_addr = link_map_addr; + return trace->legacy_return; +} + +static void fake_registry_diagnostic_output( + const kzt_observation_adapter_diagnostic_t *diagnostic, + void *opaque) +{ + gate_trace_t *trace = opaque; + + ++trace->diagnostic_outputs; + trace->diagnostic_result = diagnostic->result; + trace->diagnostic_emitted = diagnostic->emitted; +} + +static void init_fixture(gate_fixture_t *fixture, const char *path, + uintptr_t load_bias) +{ + memset(fixture, 0, sizeof(*fixture)); + strcpy(fixture->guest_name, path); + fixture->link_map.l_addr = load_bias; + fixture->link_map.l_name = fixture->guest_name; + fixture->link_map.l_ld = (Elf64_Dyn *)(load_bias + 0x1000); + fixture->link_map.l_ns = 9; + fixture->link_map.l_map_start = load_bias; + fixture->link_map.l_map_end = load_bias + 0x20000; + fixture->reader_ops.read_memory = fake_read_memory; + fixture->reader_ops.opaque = fixture; + fixture->trace.legacy_return = 73; + fixture->trace.result = KZT_OBSERVATION_ADAPTER_DISABLED; +} + +static int kzt_active(void) +{ + return option_kzt || wine_option_kzt; +} + +static int run_gate_like_callback(gate_fixture_t *fixture, + kzt_guest_registry_t *registry) +{ + kzt_guest_registry_diagnostic_config_t diagnostic_config = { + .enabled = kzt_active(), + .throttle_limit = 1, + }; + kzt_observation_adapter_request_t request = { + .enabled = kzt_active(), + .diagnostics_enabled = kzt_registry_diagnostics_enabled(), + .link_map_addr = (uintptr_t)&fixture->link_map, + .registry = registry, + .reader_ops = &fixture->reader_ops, + .legacy_flow = fake_legacy_flow, + .legacy_opaque = &fixture->trace, + .diagnostic = fake_registry_diagnostic_output, + .diagnostic_opaque = &fixture->trace, + }; + + check_int("gate.configure", + kzt_guest_registry_configure_diagnostics(registry, + &diagnostic_config), + 0); + return kzt_observe_guest_object_from_callback(&request, + &fixture->trace.result); +} + +static void reset_options(void) +{ + relocation_log = LOG_NONE; + kzt_registry_diagnostics = 0; + option_kzt = 0; + wine_option_kzt = 0; +} + +static void assert_legacy_exactly_once(const char *name, + const gate_fixture_t *fixture) +{ + check_int(name, fixture->trace.legacy_calls, 1); + check_true("legacy.link-map", + fixture->trace.legacy_link_map_addr == + (uintptr_t)&fixture->link_map); +} + +static void test_gate_defaults_closed(void) +{ + reset_options(); + + check_int("default.diagnostics-enabled", + kzt_registry_diagnostics_enabled(), 0); +} + +static void test_diagnostics_option_alone_does_not_output(void) +{ + gate_fixture_t fixture; + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + int ret; + + check_true("registry.init", registry != NULL); + if (!registry) { + return; + } + + reset_options(); + kzt_registry_diagnostics = 1; + init_fixture(&fixture, "/guest/libkzt-off.so", 0x100000); + + ret = run_gate_like_callback(&fixture, registry); + + check_int("kzt-off.return", ret, 73); + check_int("kzt-off.result", fixture.trace.result, + KZT_OBSERVATION_ADAPTER_DISABLED); + check_int("kzt-off.diagnostic-outputs", + fixture.trace.diagnostic_outputs, 0); + check_int("kzt-off.reader-calls", fixture.trace.reader_calls, 0); + assert_legacy_exactly_once("kzt-off.legacy-calls", &fixture); + + kzt_guest_registry_destroy(®istry); +} + +static void test_active_kzt_without_diagnostics_does_not_output(void) +{ + gate_fixture_t fixture; + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + int ret; + + check_true("registry.init", registry != NULL); + if (!registry) { + return; + } + + reset_options(); + option_kzt = 2; + init_fixture(&fixture, "/guest/libdiagnostics-off.so", 0x200000); + + ret = run_gate_like_callback(&fixture, registry); + + check_int("diagnostics-off.return", ret, 73); + check_int("diagnostics-off.result", fixture.trace.result, + KZT_OBSERVATION_ADAPTER_ADDED); + check_int("diagnostics-off.diagnostic-outputs", + fixture.trace.diagnostic_outputs, 0); + check_true("diagnostics-off.reader-ran", fixture.trace.reader_calls > 0); + assert_legacy_exactly_once("diagnostics-off.legacy-calls", &fixture); + + kzt_guest_registry_destroy(®istry); +} + +static void test_active_kzt_with_diagnostics_outputs(void) +{ + gate_fixture_t fixture; + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + int ret; + + check_true("registry.init", registry != NULL); + if (!registry) { + return; + } + + reset_options(); + option_kzt = 2; + kzt_registry_diagnostics = 1; + init_fixture(&fixture, "/guest/libdiagnostics-on.so", 0x300000); + + ret = run_gate_like_callback(&fixture, registry); + + check_int("diagnostics-on.return", ret, 73); + check_int("diagnostics-on.result", fixture.trace.result, + KZT_OBSERVATION_ADAPTER_ADDED); + check_int("diagnostics-on.diagnostic-outputs", + fixture.trace.diagnostic_outputs, 1); + check_int("diagnostics-on.diagnostic-result", + fixture.trace.diagnostic_result, + KZT_OBSERVATION_ADAPTER_ADDED); + check_int("diagnostics-on.diagnostic-emitted", + fixture.trace.diagnostic_emitted, 1); + check_true("diagnostics-on.reader-ran", fixture.trace.reader_calls > 0); + assert_legacy_exactly_once("diagnostics-on.legacy-calls", &fixture); + + kzt_guest_registry_destroy(®istry); +} + +static void test_debug_log_level_is_equivalent_diagnostics_option(void) +{ + gate_fixture_t fixture; + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + int ret; + + check_true("registry.init", registry != NULL); + if (!registry) { + return; + } + + reset_options(); + option_kzt = 2; + relocation_log = LOG_DEBUG; + init_fixture(&fixture, "/guest/libdiagnostics-debug.so", 0x400000); + + ret = run_gate_like_callback(&fixture, registry); + + check_int("debug-log.return", ret, 73); + check_int("debug-log.result", fixture.trace.result, + KZT_OBSERVATION_ADAPTER_ADDED); + check_int("debug-log.diagnostic-outputs", + fixture.trace.diagnostic_outputs, 1); + assert_legacy_exactly_once("debug-log.legacy-calls", &fixture); + + kzt_guest_registry_destroy(®istry); +} + +int main(void) +{ + test_gate_defaults_closed(); + test_diagnostics_option_alone_does_not_output(); + test_active_kzt_without_diagnostics_does_not_output(); + test_active_kzt_with_diagnostics_outputs(); + test_debug_log_level_is_equivalent_diagnostics_option(); + + if (failures) { + fprintf(stderr, "kzt-registry-diagnostics-gate: %d failure(s)\n", + failures); + return 1; + } + + puts("kzt-registry-diagnostics-gate: all gate tests passed"); + return 0; +} diff --git a/tests/unit/kzt/test_rela_immediate_candidate.c b/tests/unit/kzt/test_rela_immediate_candidate.c new file mode 100644 index 00000000000..f9620cb5d85 --- /dev/null +++ b/tests/unit/kzt/test_rela_immediate_candidate.c @@ -0,0 +1,618 @@ +#include +#include + +#include "elf.h" +#include "target/i386/latx/include/kzt_patch_spike_writer.h" +#include "target/i386/latx/include/kzt_rela_immediate_candidate.h" + +static int failures; + +typedef struct wi231_fake_slot { + uintptr_t slot_addr; + uintptr_t value; + uintptr_t replacement_value; + int read_calls; + int writer_write_calls; + int legacy_write_calls; + int fail_replacement_write; + int fail_rollback_write; + int force_verify_mismatch; +} wi231_fake_slot_t; + +typedef struct wi231_writer_route { + int planner_called; + int writer_called; + int skip_legacy_write; + kzt_rela_immediate_candidate_result_t plan; + kzt_patch_spike_record_t record; +} wi231_writer_route_t; + +static void check_int(const char *name, int got, int expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got %d expected %d\n", name, got, expected); + ++failures; +} + +static void check_ulong(const char *name, unsigned long got, + unsigned long expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got 0x%lx expected 0x%lx\n", name, got, + expected); + ++failures; +} + +static void check_str(const char *name, const char *got, + const char *expected) +{ + if (got && expected && !strcmp(got, expected)) { + return; + } + + fprintf(stderr, "%s: got '%s' expected '%s'\n", name, + got ? got : "(null)", expected ? expected : "(null)"); + ++failures; +} + +static int wi231_fake_read_slot(uintptr_t slot_addr, uintptr_t *value, + void *opaque) +{ + wi231_fake_slot_t *slot = opaque; + + if (!slot || !value || slot_addr != slot->slot_addr) { + return -1; + } + + ++slot->read_calls; + if (slot->force_verify_mismatch && slot->read_calls == 2) { + *value = slot->replacement_value + 0x10; + return 0; + } + + *value = slot->value; + return 0; +} + +static int wi231_fake_write_slot(uintptr_t slot_addr, uintptr_t value, + void *opaque) +{ + wi231_fake_slot_t *slot = opaque; + + if (!slot || slot_addr != slot->slot_addr) { + return -1; + } + + ++slot->writer_write_calls; + if (value == slot->replacement_value && slot->fail_replacement_write) { + return -1; + } + if (value != slot->replacement_value && slot->fail_rollback_write) { + return -1; + } + + slot->value = value; + return 0; +} + +static kzt_patch_spike_slot_ops_t wi231_slot_ops(wi231_fake_slot_t *slot) +{ + return (kzt_patch_spike_slot_ops_t) { + .read_slot = wi231_fake_read_slot, + .write_slot = wi231_fake_write_slot, + .opaque = slot, + }; +} + +static kzt_patch_spike_guard_t wi231_enabled_guard(void) +{ + kzt_patch_spike_config_t config = { + .enabled = 1, + .write_enabled = 1, + .budget = 8, + }; + kzt_patch_spike_guard_t guard; + + kzt_patch_spike_guard_init(&guard, &config); + return guard; +} + +static void wi231_legacy_write(wi231_fake_slot_t *slot, + uintptr_t legacy_target) +{ + ++slot->legacy_write_calls; + slot->value = legacy_target; +} + +static void wi231_apply_step4_request_contract( + const kzt_rela_immediate_candidate_request_t *request, + wi231_fake_slot_t *slot, uintptr_t legacy_target, + wi231_writer_route_t *route) +{ + kzt_patch_spike_guard_t guard = wi231_enabled_guard(); + kzt_patch_spike_slot_ops_t ops = wi231_slot_ops(slot); + kzt_rela_immediate_writer_result_t writer_result; + + memset(route, 0, sizeof(*route)); + check_int("wi231.real.apply", + kzt_rela_immediate_jump_slot_try_write( + request, &guard, &ops, &writer_result), + 0); + route->planner_called = writer_result.planner_called; + route->writer_called = writer_result.writer_called; + route->skip_legacy_write = writer_result.skip_legacy_write; + route->plan = writer_result.plan; + route->record = writer_result.record; + if (!writer_result.skip_legacy_write) { + wi231_legacy_write(slot, legacy_target); + } +} + +static void wi231_trace(const char *tc, const wi231_fake_slot_t *slot, + const wi231_writer_route_t *route) +{ + printf("WI231_TC tc=%s plan_status=%d plan_reason=%d " + "decision=%s writer_called=%d legacy_writes=%d " + "skip_legacy=%d result=%s failure=%s reads=%d " + "writer_writes=%d final=0x%lx\n", + tc, route->plan.status, route->plan.reason, + route->plan.decision_present ? + kzt_patch_decision_kind_name(route->plan.decision.kind) : + "(none)", + route->writer_called, slot->legacy_write_calls, + route->record.skip_legacy_write, + kzt_patch_spike_result_name(route->record.result), + kzt_patch_spike_failure_name(route->record.failure), + slot->read_calls, slot->writer_write_calls, + (unsigned long)slot->value); +} + +static kzt_patch_object_ref_t object_ref(uintptr_t link_map_addr, + unsigned long generation, + const char *soname) +{ + return (kzt_patch_object_ref_t) { + .known = 1, + .link_map_addr = link_map_addr, + .map_start = 0x7000000000 + generation * 0x100000, + .map_end = 0x7000009000 + generation * 0x100000, + .generation = generation, + .soname = soname, + .path = soname, + }; +} + +static kzt_rela_immediate_candidate_request_t base_request(void) +{ + return (kzt_rela_immediate_candidate_request_t) { + .relocation_type = R_X86_64_JUMP_SLOT, + .table_kind = KZT_PATCH_TABLE_PLT_RELA, + .entry_index = 4, + .entry_addr = 0x7000100200, + .source = object_ref(0x1110, 5, "librequester.so"), + .dynamic_addr = 0x7000100000, + .load_bias = 0x7000000000, + .dynamic_view_generation = 18, + .dynamic_view_available = 1, + .slot_addr = 0x7000200088, + .slot_current_value_present = 1, + .slot_current_value = 0x7100001234, + .lazy_binding_deferred = 0, + .expected_guest_target = 0x7100005678, + .native_bridge_target = 0x7200004560, + .legacy_target = 0x7300004560, + .symbol_index = 33, + .symbol_name = "gtk_widget_show", + .version_evidence = KZT_SYMBOL_VERSION_VERSIONED, + .version = "GTK_3.0", + .current_owner = object_ref(0x2220, 9, "libgtk-3.so"), + .owner_match = KZT_PATCH_OWNER_MATCH, + .wrapper_match = KZT_PATCH_WRAPPER_VERSION_MATCH, + .wrapper_name = "wrappedgtk3", + .wrapper_version_evidence = KZT_SYMBOL_VERSION_VERSIONED, + .wrapper_symbol_version = "GTK_3.0", + }; +} + +static wi231_fake_slot_t wi231_slot_from_request( + const kzt_rela_immediate_candidate_request_t *request) +{ + return (wi231_fake_slot_t) { + .slot_addr = request->slot_addr, + .value = request->slot_current_value, + .replacement_value = request->native_bridge_target, + }; +} + +static void test_immediate_jump_slot_builds_candidate_fields(void) +{ + kzt_rela_immediate_candidate_request_t request = base_request(); + kzt_rela_immediate_candidate_result_t result; + const kzt_patch_candidate_t *candidate; + + check_int("jump_slot.call", + kzt_rela_immediate_jump_slot_plan(&request, &result), 0); + check_int("jump_slot.status", result.status, + KZT_RELA_IMMEDIATE_CANDIDATE_PLANNED); + check_int("jump_slot.reason", result.reason, + KZT_RELA_IMMEDIATE_CANDIDATE_REASON_NONE); + check_int("jump_slot.candidate_present", result.candidate_present, 1); + check_int("jump_slot.decision_present", result.decision_present, 1); + + candidate = &result.candidate; + check_int("jump_slot.table", candidate->table_kind, + KZT_PATCH_TABLE_PLT_RELA); + check_ulong("jump_slot.entry_index", candidate->entry_index, 4); + check_ulong("jump_slot.entry_addr", candidate->entry_addr, + request.entry_addr); + check_int("jump_slot.reloc", candidate->reloc_type, + KZT_PATCH_RELOCATION_JUMP_SLOT); + check_ulong("jump_slot.slot", candidate->slot_addr, request.slot_addr); + check_int("jump_slot.current_present", + candidate->slot_current_value_present, 1); + check_ulong("jump_slot.current", candidate->slot_current_value, + request.slot_current_value); + check_int("jump_slot.lazy", candidate->lazy_binding_deferred, 0); + check_ulong("jump_slot.symbol_index", candidate->symbol_index, 33); + check_str("jump_slot.symbol", candidate->symbol_name, + "gtk_widget_show"); + check_str("jump_slot.version", candidate->version, "GTK_3.0"); + check_int("jump_slot.owner", candidate->owner_match, + KZT_PATCH_OWNER_MATCH); + check_int("jump_slot.wrapper", candidate->wrapper_match, + KZT_PATCH_WRAPPER_VERSION_MATCH); + check_ulong("jump_slot.bridge", candidate->bridge_target, + request.native_bridge_target); + check_int("jump_slot.decision", result.decision.kind, + KZT_PATCH_DECISION_APPROVED); +} + +static void test_target_fields_keep_separate_meanings(void) +{ + kzt_rela_immediate_candidate_request_t request = base_request(); + kzt_rela_immediate_candidate_result_t result; + + request.expected_guest_target = 0x7100001111; + request.native_bridge_target = 0x7200002222; + request.legacy_target = 0x7300003333; + + check_int("target_roles.call", + kzt_rela_immediate_jump_slot_plan(&request, &result), 0); + check_int("target_roles.decision", result.decision.kind, + KZT_PATCH_DECISION_APPROVED); + check_ulong("target_roles.bridge", result.candidate.bridge_target, + request.native_bridge_target); + check_ulong("target_roles.decision_bridge", + result.decision.bridge_target, + request.native_bridge_target); + + request.native_bridge_target = 0; + check_int("target_roles.zero_bridge.call", + kzt_rela_immediate_jump_slot_plan(&request, &result), 0); + check_int("target_roles.zero_bridge.decision", result.decision.kind, + KZT_PATCH_DECISION_UNSUPPORTED); + check_int("target_roles.zero_bridge.reason", result.decision.reason, + KZT_PATCH_REASON_INPUT_UNAVAILABLE_BRIDGE_TARGET); + check_ulong("target_roles.zero_bridge.candidate", + result.candidate.bridge_target, 0); +} + +static void test_explicit_unversioned_and_unknown_evidence(void) +{ + kzt_rela_immediate_candidate_request_t request = base_request(); + kzt_rela_immediate_candidate_result_t result; + wi231_fake_slot_t slot; + wi231_writer_route_t route; + + request.version_evidence = KZT_SYMBOL_VERSION_CONFIRMED_UNVERSIONED; + request.version = NULL; + request.wrapper_match = KZT_PATCH_WRAPPER_UNVERSIONED_MATCH; + request.wrapper_version_evidence = + KZT_SYMBOL_VERSION_CONFIRMED_UNVERSIONED; + request.wrapper_symbol_version = NULL; + check_int("unversioned-plan.call", + kzt_rela_immediate_jump_slot_plan(&request, &result), 0); + check_int("unversioned-plan.status", result.status, + KZT_RELA_IMMEDIATE_CANDIDATE_PLANNED); + check_int("unversioned-plan.approved", result.decision.kind, + KZT_PATCH_DECISION_APPROVED); + slot = wi231_slot_from_request(&request); + wi231_apply_step4_request_contract( + &request, &slot, request.legacy_target, &route); + check_int("unversioned-write.applied", route.record.result, + KZT_PATCH_SPIKE_RESULT_APPLIED); + check_int("unversioned-write.skip-legacy", slot.legacy_write_calls, 0); + check_ulong("unversioned-write.final", slot.value, + request.native_bridge_target); + + request = base_request(); + request.version_evidence = KZT_SYMBOL_VERSION_UNKNOWN; + check_int("unknown-plan.call", + kzt_rela_immediate_jump_slot_plan(&request, &result), 0); + check_int("unknown-plan.status", result.status, + KZT_RELA_IMMEDIATE_CANDIDATE_FAIL_OPEN); + check_int("unknown-plan.reason", result.reason, + KZT_RELA_IMMEDIATE_CANDIDATE_REASON_MISSING_SYMBOL_VERSION); +} + +static void test_non_target_relocation_does_not_build_candidate(void) +{ + kzt_rela_immediate_candidate_request_t request = base_request(); + kzt_rela_immediate_candidate_result_t result; + + request.relocation_type = R_X86_64_RELATIVE; + check_int("relative.call", + kzt_rela_immediate_jump_slot_plan(&request, &result), 0); + check_int("relative.status", result.status, + KZT_RELA_IMMEDIATE_CANDIDATE_SKIPPED); + check_int("relative.reason", result.reason, + KZT_RELA_IMMEDIATE_CANDIDATE_REASON_NON_TARGET_RELOCATION); + check_int("relative.candidate_present", result.candidate_present, 0); + check_int("relative.decision_present", result.decision_present, 0); +} + +static void test_glob_dat_keeps_legacy_path_without_candidate(void) +{ + kzt_rela_immediate_candidate_request_t request = base_request(); + kzt_rela_immediate_candidate_result_t result; + + request.relocation_type = R_X86_64_GLOB_DAT; + request.table_kind = KZT_PATCH_TABLE_RELA; + check_int("glob_dat.call", + kzt_rela_immediate_jump_slot_plan(&request, &result), 0); + check_int("glob_dat.status", result.status, + KZT_RELA_IMMEDIATE_CANDIDATE_SKIPPED); + check_int("glob_dat.reason", result.reason, + KZT_RELA_IMMEDIATE_CANDIDATE_REASON_NON_TARGET_RELOCATION); + check_int("glob_dat.candidate_present", result.candidate_present, 0); +} + +static void test_deferred_lazy_binding_does_not_build_writable_candidate(void) +{ + kzt_rela_immediate_candidate_request_t request = base_request(); + kzt_rela_immediate_candidate_result_t result; + + request.lazy_binding_deferred = 1; + check_int("lazy.call", + kzt_rela_immediate_jump_slot_plan(&request, &result), 0); + check_int("lazy.status", result.status, + KZT_RELA_IMMEDIATE_CANDIDATE_SKIPPED); + check_int("lazy.reason", result.reason, + KZT_RELA_IMMEDIATE_CANDIDATE_REASON_DEFERRED_LAZY_BINDING); + check_int("lazy.candidate_present", result.candidate_present, 0); +} + +static void test_missing_symbol_information_fails_open(void) +{ + kzt_rela_immediate_candidate_request_t request = base_request(); + kzt_rela_immediate_candidate_result_t result; + + request.symbol_name = NULL; + check_int("missing_symbol.call", + kzt_rela_immediate_jump_slot_plan(&request, &result), 0); + check_int("missing_symbol.status", result.status, + KZT_RELA_IMMEDIATE_CANDIDATE_FAIL_OPEN); + check_int("missing_symbol.reason", result.reason, + KZT_RELA_IMMEDIATE_CANDIDATE_REASON_MISSING_SYMBOL_NAME); + check_int("missing_symbol.candidate_present", result.candidate_present, 0); + check_int("missing_symbol.decision_present", result.decision_present, 0); +} + +static void test_missing_owner_still_plans_but_keeps_legacy_decision(void) +{ + kzt_rela_immediate_candidate_request_t request = base_request(); + kzt_rela_immediate_candidate_result_t result; + + memset(&request.current_owner, 0, sizeof(request.current_owner)); + request.owner_match = KZT_PATCH_OWNER_UNKNOWN; + request.wrapper_match = KZT_PATCH_WRAPPER_NO_MANIFEST; + request.native_bridge_target = 0; + + check_int("owner_unknown.call", + kzt_rela_immediate_jump_slot_plan(&request, &result), 0); + check_int("owner_unknown.status", result.status, + KZT_RELA_IMMEDIATE_CANDIDATE_PLANNED); + check_int("owner_unknown.candidate_present", result.candidate_present, 1); + check_int("owner_unknown.decision_present", result.decision_present, 1); + check_int("owner_unknown.decision", result.decision.kind, + KZT_PATCH_DECISION_UNSUPPORTED); + check_int("owner_unknown.reason", result.decision.reason, + KZT_PATCH_REASON_INPUT_UNAVAILABLE_OWNER); +} + +static void test_wi231_approved_writer_success_skips_legacy_duplicate_write( + void) +{ + kzt_rela_immediate_candidate_request_t request = base_request(); + uintptr_t legacy_target = 0x7300002222; + wi231_fake_slot_t slot = wi231_slot_from_request(&request); + wi231_writer_route_t route; + + wi231_apply_step4_request_contract(&request, &slot, legacy_target, + &route); + check_int("wi231.success.planner", route.planner_called, 1); + check_int("wi231.success.decision", route.plan.decision.kind, + KZT_PATCH_DECISION_APPROVED); + check_int("wi231.success.writer", route.writer_called, 1); + check_int("wi231.success.skip_legacy", + route.record.skip_legacy_write, 1); + check_int("wi231.success.legacy", slot.legacy_write_calls, 0); + check_ulong("wi231.success.final", slot.value, request.native_bridge_target); + wi231_trace("approved-writer-success-skips-legacy", &slot, &route); +} + +static void test_wi231_planner_non_approved_results_keep_legacy(void) +{ + kzt_rela_immediate_candidate_request_t request = base_request(); + uintptr_t legacy_target = 0x7300003333; + wi231_fake_slot_t slot; + wi231_writer_route_t route; + + memset(&request.current_owner, 0, sizeof(request.current_owner)); + request.owner_match = KZT_PATCH_OWNER_UNKNOWN; + request.wrapper_match = KZT_PATCH_WRAPPER_NO_MANIFEST; + request.native_bridge_target = 0; + slot = wi231_slot_from_request(&request); + slot.replacement_value = 0x7200004560; + wi231_apply_step4_request_contract(&request, &slot, legacy_target, + &route); + check_int("wi231.unsupported.decision", route.plan.decision.kind, + KZT_PATCH_DECISION_UNSUPPORTED); + check_int("wi231.unsupported.writer", route.writer_called, 0); + check_int("wi231.unsupported.legacy", slot.legacy_write_calls, 1); + check_ulong("wi231.unsupported.final", slot.value, legacy_target); + wi231_trace("planner-unsupported-keeps-legacy", &slot, &route); + + request = base_request(); + request.owner_match = KZT_PATCH_OWNER_MISMATCH; + slot = wi231_slot_from_request(&request); + wi231_apply_step4_request_contract(&request, &slot, legacy_target, + &route); + check_int("wi231.rejected.decision", route.plan.decision.kind, + KZT_PATCH_DECISION_REJECTED); + check_int("wi231.rejected.writer", route.writer_called, 0); + check_int("wi231.rejected.legacy", slot.legacy_write_calls, 1); + check_ulong("wi231.rejected.final", slot.value, legacy_target); + wi231_trace("planner-rejected-keeps-legacy", &slot, &route); + + request = base_request(); + request.lazy_binding_deferred = 1; + slot = wi231_slot_from_request(&request); + wi231_apply_step4_request_contract(&request, &slot, legacy_target, + &route); + check_int("wi231.deferred.writer", route.writer_called, 0); + check_int("wi231.deferred.legacy", slot.legacy_write_calls, 1); + check_ulong("wi231.deferred.final", slot.value, legacy_target); + wi231_trace("lazy-deferred-keeps-legacy", &slot, &route); + + request = base_request(); + request.symbol_name = NULL; + slot = wi231_slot_from_request(&request); + wi231_apply_step4_request_contract(&request, &slot, legacy_target, + &route); + check_int("wi231.error.writer", route.writer_called, 0); + check_int("wi231.error.legacy", slot.legacy_write_calls, 1); + check_ulong("wi231.error.final", slot.value, legacy_target); + wi231_trace("planner-fail-open-keeps-legacy", &slot, &route); +} + +static void test_wi231_writer_failures_fail_open_to_legacy(void) +{ + kzt_rela_immediate_candidate_request_t request = base_request(); + uintptr_t legacy_target = 0x7300004444; + wi231_fake_slot_t slot; + wi231_writer_route_t route; + + slot = wi231_slot_from_request(&request); + slot.value = request.slot_current_value + 4; + wi231_apply_step4_request_contract(&request, &slot, legacy_target, + &route); + check_int("wi231.mismatch.writer", route.writer_called, 1); + check_int("wi231.mismatch.failure", route.record.failure, + KZT_PATCH_SPIKE_FAILURE_EXPECTED_MISMATCH); + check_int("wi231.mismatch.legacy", slot.legacy_write_calls, 1); + check_ulong("wi231.mismatch.final", slot.value, legacy_target); + wi231_trace("writer-expected-mismatch-fail-open", &slot, &route); + + slot = wi231_slot_from_request(&request); + slot.fail_replacement_write = 1; + wi231_apply_step4_request_contract(&request, &slot, legacy_target, + &route); + check_int("wi231.write_fail.failure", route.record.failure, + KZT_PATCH_SPIKE_FAILURE_WRITE_FAILED); + check_int("wi231.write_fail.legacy", slot.legacy_write_calls, 1); + check_ulong("wi231.write_fail.final", slot.value, legacy_target); + wi231_trace("writer-write-fail-fail-open", &slot, &route); + + slot = wi231_slot_from_request(&request); + slot.force_verify_mismatch = 1; + wi231_apply_step4_request_contract(&request, &slot, legacy_target, + &route); + check_int("wi231.verify_fail.failure", route.record.failure, + KZT_PATCH_SPIKE_FAILURE_VERIFY_FAILED); + check_int("wi231.verify_fail.rollback", + route.record.rollback_called, 1); + check_int("wi231.verify_fail.legacy", slot.legacy_write_calls, 1); + check_ulong("wi231.verify_fail.final", slot.value, legacy_target); + wi231_trace("writer-verify-fail-fail-open", &slot, &route); + + slot = wi231_slot_from_request(&request); + slot.force_verify_mismatch = 1; + slot.fail_rollback_write = 1; + wi231_apply_step4_request_contract(&request, &slot, legacy_target, + &route); + check_int("wi231.rollback_fail.failure", route.record.failure, + KZT_PATCH_SPIKE_FAILURE_ROLLBACK_FAILED); + check_int("wi231.rollback_fail.rollback", + route.record.rollback_called, 1); + check_int("wi231.rollback_fail.legacy", slot.legacy_write_calls, 1); + check_ulong("wi231.rollback_fail.final", slot.value, legacy_target); + wi231_trace("writer-rollback-fail-fail-open", &slot, &route); +} + +static void test_wi231_non_target_glob_dat_and_lazy_skip_writer(void) +{ + kzt_rela_immediate_candidate_request_t request = base_request(); + uintptr_t legacy_target = 0x7300005555; + wi231_fake_slot_t slot; + wi231_writer_route_t route; + + request.relocation_type = R_X86_64_RELATIVE; + slot = wi231_slot_from_request(&request); + wi231_apply_step4_request_contract(&request, &slot, legacy_target, + &route); + check_int("wi231.relative.writer", route.writer_called, 0); + check_int("wi231.relative.legacy", slot.legacy_write_calls, 1); + check_ulong("wi231.relative.final", slot.value, legacy_target); + wi231_trace("non-target-relocation-skips-writer", &slot, &route); + + request = base_request(); + request.relocation_type = R_X86_64_GLOB_DAT; + request.table_kind = KZT_PATCH_TABLE_RELA; + slot = wi231_slot_from_request(&request); + wi231_apply_step4_request_contract(&request, &slot, legacy_target, + &route); + check_int("wi231.glob_dat.writer", route.writer_called, 0); + check_int("wi231.glob_dat.legacy", slot.legacy_write_calls, 1); + check_ulong("wi231.glob_dat.final", slot.value, legacy_target); + wi231_trace("glob-dat-skips-writer", &slot, &route); + + request = base_request(); + request.lazy_binding_deferred = 1; + slot = wi231_slot_from_request(&request); + wi231_apply_step4_request_contract(&request, &slot, legacy_target, + &route); + check_int("wi231.lazy.writer", route.writer_called, 0); + check_int("wi231.lazy.legacy", slot.legacy_write_calls, 1); + check_ulong("wi231.lazy.final", slot.value, legacy_target); + wi231_trace("lazy-deferred-skips-writer", &slot, &route); +} + +int main(void) +{ + test_immediate_jump_slot_builds_candidate_fields(); + test_non_target_relocation_does_not_build_candidate(); + test_glob_dat_keeps_legacy_path_without_candidate(); + test_deferred_lazy_binding_does_not_build_writable_candidate(); + test_missing_symbol_information_fails_open(); + test_target_fields_keep_separate_meanings(); + test_explicit_unversioned_and_unknown_evidence(); + test_missing_owner_still_plans_but_keeps_legacy_decision(); + test_wi231_approved_writer_success_skips_legacy_duplicate_write(); + test_wi231_planner_non_approved_results_keep_legacy(); + test_wi231_writer_failures_fail_open_to_legacy(); + test_wi231_non_target_glob_dat_and_lazy_skip_writer(); + + if (failures) { + fprintf(stderr, "%d test failure(s)\n", failures); + return 1; + } + + return 0; +} diff --git a/tests/unit/kzt/test_rela_request_enricher.c b/tests/unit/kzt/test_rela_request_enricher.c new file mode 100644 index 00000000000..9c778e148ac --- /dev/null +++ b/tests/unit/kzt/test_rela_request_enricher.c @@ -0,0 +1,765 @@ +#include +#include + +#include "elf.h" +#include "target/i386/latx/include/kzt_rela_request_enricher.h" +#include "target/i386/latx/include/kzt_rela_stub_detector.h" + +static int failures; + +typedef struct fake_bridge_state { + uintptr_t next_bridge_target; + int check_calls; + int add_calls; +} fake_bridge_state_t; + +typedef struct fake_slot_entry { + uintptr_t *slot; + int read_calls; + int writer_write_calls; + int legacy_write_calls; +} fake_slot_entry_t; + +typedef struct fake_slot_bank { + uintptr_t canary_before; + uintptr_t values[2]; + uintptr_t canary_after; + fake_slot_entry_t entries[2]; + int invalid_accesses; +} fake_slot_bank_t; + +static void check_int(const char *name, int got, int expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got %d expected %d\n", name, got, expected); + ++failures; +} + +static void check_ulong(const char *name, unsigned long got, + unsigned long expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got 0x%lx expected 0x%lx\n", name, got, + expected); + ++failures; +} + +static uintptr_t fake_check_bridge(uintptr_t native_symbol, void *opaque) +{ + fake_bridge_state_t *state = opaque; + + (void)native_symbol; + ++state->check_calls; + return 0; +} + +static uintptr_t fake_add_bridge( + const kzt_wrapper_probe_bridge_request_t *request, void *opaque) +{ + fake_bridge_state_t *state = opaque; + + if (!request || !request->native_symbol) { + return 0; + } + + ++state->add_calls; + return state->next_bridge_target; +} + +static kzt_wrapper_probe_bridge_ops_t bridge_ops(fake_bridge_state_t *state) +{ + return (kzt_wrapper_probe_bridge_ops_t) { + .check_bridge = fake_check_bridge, + .add_bridge = fake_add_bridge, + .opaque = state, + }; +} + +static fake_slot_entry_t *fake_slot_find(fake_slot_bank_t *bank, + uintptr_t slot_addr) +{ + size_t i; + + if (!bank) { + return NULL; + } + for (i = 0; i < sizeof(bank->entries) / sizeof(bank->entries[0]); ++i) { + if ((uintptr_t)bank->entries[i].slot == slot_addr) { + return &bank->entries[i]; + } + } + + ++bank->invalid_accesses; + return NULL; +} + +static int fake_slot_read(uintptr_t slot_addr, uintptr_t *value, void *opaque) +{ + fake_slot_entry_t *slot = fake_slot_find(opaque, slot_addr); + + if (!slot || !value) { + return -1; + } + + ++slot->read_calls; + *value = *slot->slot; + return 0; +} + +static int fake_slot_write(uintptr_t slot_addr, uintptr_t value, void *opaque) +{ + fake_slot_entry_t *slot = fake_slot_find(opaque, slot_addr); + + if (!slot) { + return -1; + } + + ++slot->writer_write_calls; + *slot->slot = value; + return 0; +} + +static kzt_patch_spike_slot_ops_t fake_slot_ops(fake_slot_bank_t *bank) +{ + return (kzt_patch_spike_slot_ops_t) { + .read_slot = fake_slot_read, + .write_slot = fake_slot_write, + .opaque = bank, + }; +} + +static kzt_patch_spike_guard_t enabled_guard(void) +{ + kzt_patch_spike_config_t config = { + .enabled = 1, + .write_enabled = 1, + .budget = 4, + }; + kzt_patch_spike_guard_t guard; + + kzt_patch_spike_guard_init(&guard, &config); + return guard; +} + +static void fake_legacy_write(fake_slot_bank_t *bank, uintptr_t slot_addr, + uintptr_t legacy_target) +{ + fake_slot_entry_t *slot = fake_slot_find(bank, slot_addr); + + if (!slot) { + return; + } + + ++slot->legacy_write_calls; + *(uintptr_t *)slot_addr = legacy_target; +} + +static void fake_slot_bank_init(fake_slot_bank_t *bank, + uintptr_t canary_before, + uintptr_t selected_value, + uintptr_t other_value, + uintptr_t canary_after) +{ + memset(bank, 0, sizeof(*bank)); + bank->canary_before = canary_before; + bank->values[0] = selected_value; + bank->values[1] = other_value; + bank->canary_after = canary_after; + bank->entries[0].slot = &bank->values[0]; + bank->entries[1].slot = &bank->values[1]; +} + +static kzt_guest_object_observation_t observation( + uintptr_t link_map_addr, uintptr_t map_start, uintptr_t map_end, + const char *soname) +{ + return (kzt_guest_object_observation_t) { + .link_map_addr = link_map_addr, + .load_bias = { map_start, KZT_GUEST_FIELD_OK }, + .dynamic_addr = { map_start + 0x2000, KZT_GUEST_FIELD_OK }, + .map_start = { map_start, KZT_GUEST_FIELD_OK }, + .map_end = { map_end, KZT_GUEST_FIELD_OK }, + .namespace_id = { 0, KZT_GUEST_FIELD_OK }, + .path = { soname, KZT_GUEST_FIELD_OK }, + .soname = { soname, KZT_GUEST_FIELD_OK }, + .dynamic_view_status = KZT_GUEST_FIELD_NOT_PARSED, + }; +} + +static kzt_guest_dynamic_view_t dynamic_view(uintptr_t dynamic_addr, + uintptr_t load_bias) +{ + return (kzt_guest_dynamic_view_t) { + .dynamic_addr = dynamic_addr, + .load_bias = load_bias, + .status = KZT_GUEST_DYNAMIC_COMPLETE, + .entry_count = 1, + .has_null = 1, + }; +} + +static kzt_guest_registry_t *registry_with_source_and_owner( + int commit_dynamic, int include_owner) +{ + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_object_observation_t source = observation( + 0x1000, 0x70000000, 0x70010000, "librequester.so"); + kzt_guest_object_observation_t owner = observation( + 0x2000, 0x71000000, 0x71010000, "libgtk-3.so"); + kzt_guest_dynamic_view_t view = dynamic_view(0x70002000, 0x70000000); + + check_int("registry.observe.source", + kzt_guest_registry_observe(registry, &source), + KZT_GUEST_REGISTRY_ADDED); + if (include_owner) { + check_int("registry.observe.owner", + kzt_guest_registry_observe(registry, &owner), + KZT_GUEST_REGISTRY_ADDED); + } + if (commit_dynamic) { + check_int("registry.dynamic", + kzt_guest_registry_commit_dynamic_view( + registry, source.link_map_addr, 1, &view), + KZT_GUEST_REGISTRY_UPDATED); + } + + return registry; +} + +static const kzt_wrapper_probe_entry_t wrapper_entries[] = { + { + .symbol_name = "gtk_widget_show", + .symbol_version = "GTK_3.0", + .wrapper_name = "wrappedgtk3", + .wrapper_symbol_version = "GTK_3.0", + .native_symbol = 0x60000000, + }, +}; + +static kzt_wrapper_probe_manifest_t wrapper_manifest(void) +{ + return (kzt_wrapper_probe_manifest_t) { + .available = 1, + .manifest_name = "wrappedgtk3", + .entries = wrapper_entries, + .entry_count = sizeof(wrapper_entries) / sizeof(wrapper_entries[0]), + }; +} + +static kzt_rela_immediate_candidate_request_t base_request(void) +{ + return (kzt_rela_immediate_candidate_request_t) { + .relocation_type = R_X86_64_JUMP_SLOT, + .table_kind = KZT_PATCH_TABLE_PLT_RELA, + .entry_index = 7, + .entry_addr = 0x70003000, + .source = { + .known = 1, + .map_start = 0x70000000, + .map_end = 0x70010000, + .soname = "fallback-requester", + .path = "/fallback/requester", + }, + .dynamic_addr = 0x70002000, + .load_bias = 0x70000000, + .slot_addr = 0x70004000, + .slot_current_value_present = 1, + .slot_current_value = 0x71000010, + .expected_guest_target = 0x71000020, + .native_bridge_target = 0x7fffffff, + .legacy_target = 0x71000020, + .symbol_index = 9, + .symbol_name = "gtk_widget_show", + .version = "GTK_3.0", + }; +} + +static void enrich_and_plan( + kzt_rela_immediate_candidate_request_t *request, + const kzt_rela_request_enricher_input_t *input, + kzt_rela_request_enricher_result_t *enrich_result, + kzt_rela_immediate_candidate_result_t *plan_result) +{ + check_int("enrich.call", + kzt_rela_immediate_request_enrich( + request, input, enrich_result), 0); + check_int("plan.call", + kzt_rela_immediate_jump_slot_plan(request, plan_result), 0); +} + +static void test_all_evidence_allows_approved_plan(void) +{ + kzt_guest_registry_t *registry = registry_with_source_and_owner(1, 1); + kzt_wrapper_probe_manifest_t manifest = wrapper_manifest(); + fake_bridge_state_t bridge = { .next_bridge_target = 0x72000000 }; + kzt_wrapper_probe_bridge_ops_t ops = bridge_ops(&bridge); + kzt_rela_request_enricher_input_t input = { + .registry = registry, + .wrapper_manifest = &manifest, + .bridge_ops = &ops, + }; + kzt_rela_request_enricher_result_t enrich_result; + kzt_rela_immediate_candidate_result_t plan_result; + kzt_rela_immediate_candidate_request_t request = base_request(); + + enrich_and_plan(&request, &input, &enrich_result, &plan_result); + check_int("approved.dynamic", request.dynamic_view_available, 1); + check_ulong("approved.source.link_map", request.source.link_map_addr, + 0x1000); + check_ulong("approved.source.generation", request.source.generation, 1); + check_int("approved.owner", request.owner_match, + KZT_PATCH_OWNER_MATCH); + check_int("approved.wrapper", request.wrapper_match, + KZT_PATCH_WRAPPER_VERSION_MATCH); + check_ulong("approved.bridge", request.native_bridge_target, + 0x72000000); + check_int("approved.decision", plan_result.decision.kind, + KZT_PATCH_DECISION_APPROVED); + check_int("approved.reason", plan_result.decision.reason, + KZT_PATCH_REASON_APPROVED_NATIVE_BRIDGE); + check_int("approved.add_bridge", bridge.add_calls, 1); + + kzt_guest_registry_destroy(®istry); +} + +static void test_unresolved_stub_detector_coordinates_and_bounds(void) +{ + const uintptr_t plt_start = 0x2000; + const uintptr_t plt_end = 0x2040; + const uintptr_t gotplt_start = 0x3000; + const uintptr_t gotplt_end = 0x3040; + const intptr_t load_bias = 0x71000000; + const intptr_t negative_bias = -0x1000; + + check_int("detector.delta0.plt.start", + kzt_rela_slot_current_is_unresolved_stub( + plt_start, KZT_RELA_STUB_COORDINATE_LINK_TIME_RAW, + 0, plt_start, plt_end, + gotplt_start, gotplt_end), 1); + check_int("detector.delta0.gotplt.last", + kzt_rela_slot_current_is_unresolved_stub( + gotplt_end - 1, + KZT_RELA_STUB_COORDINATE_RUNTIME_REBASED, + 0, plt_start, plt_end, + gotplt_start, gotplt_end), 1); + check_int("detector.delta0.plt.end", + kzt_rela_slot_current_is_unresolved_stub( + plt_end, KZT_RELA_STUB_COORDINATE_LINK_TIME_RAW, + 0, plt_start, plt_end, + gotplt_start, gotplt_end), 0); + check_int("detector.delta0.gotplt.before", + kzt_rela_slot_current_is_unresolved_stub( + gotplt_start - 1, + KZT_RELA_STUB_COORDINATE_RUNTIME_REBASED, + 0, plt_start, plt_end, + gotplt_start, gotplt_end), 0); + check_int("detector.positive_delta.plt", + kzt_rela_slot_current_is_unresolved_stub( + plt_start + load_bias + 0x10, + KZT_RELA_STUB_COORDINATE_RUNTIME_REBASED, load_bias, + plt_start, plt_end, gotplt_start, gotplt_end), 1); + check_int("detector.positive_delta.gotplt", + kzt_rela_slot_current_is_unresolved_stub( + gotplt_start + load_bias + 0x18, + KZT_RELA_STUB_COORDINATE_RUNTIME_REBASED, load_bias, + plt_start, plt_end, gotplt_start, gotplt_end), 1); + check_int("detector.delta.link_time_before_lazy_adjust", + kzt_rela_slot_current_is_unresolved_stub( + plt_start + 0x10, + KZT_RELA_STUB_COORDINATE_LINK_TIME_RAW, load_bias, + plt_start, plt_end, gotplt_start, gotplt_end), 1); + check_int("detector.delta.resolved_same_dso", + kzt_rela_slot_current_is_unresolved_stub( + load_bias + 0x5000, + KZT_RELA_STUB_COORDINATE_RUNTIME_REBASED, load_bias, + plt_start, plt_end, gotplt_start, gotplt_end), 0); + check_int("detector.delta.runtime_end", + kzt_rela_slot_current_is_unresolved_stub( + load_bias + gotplt_end, + KZT_RELA_STUB_COORDINATE_RUNTIME_REBASED, load_bias, + plt_start, plt_end, gotplt_start, gotplt_end), 0); + check_int("detector.negative_delta.plt", + kzt_rela_slot_current_is_unresolved_stub( + 0x3010, KZT_RELA_STUB_COORDINATE_RUNTIME_REBASED, + negative_bias, 0x4000, 0x4040, 0x5000, 0x5040), 1); + check_int("detector.negative_delta.gotplt", + kzt_rela_slot_current_is_unresolved_stub( + 0x4018, KZT_RELA_STUB_COORDINATE_RUNTIME_REBASED, + negative_bias, 0x4000, 0x4040, 0x5000, 0x5040), 1); + check_int("detector.negative_delta.raw_collision_is_text", + kzt_rela_slot_current_is_unresolved_stub( + 0x2010, KZT_RELA_STUB_COORDINATE_RUNTIME_REBASED, + negative_bias, 0x2000, 0x2040, 0x5000, 0x5040), 0); + check_int("detector.unknown.raw_looking_value", + kzt_rela_slot_current_is_unresolved_stub( + plt_start + 0x10, KZT_RELA_STUB_COORDINATE_UNKNOWN, + load_bias, plt_start, plt_end, + gotplt_start, gotplt_end), 0); + check_int("detector.overflow.no_wrap", + kzt_rela_slot_current_is_unresolved_stub( + 0x10, KZT_RELA_STUB_COORDINATE_RUNTIME_REBASED, + 0x20, UINTPTR_MAX - 0x10, UINTPTR_MAX - 0x8, + 0, 0), 0); + check_int("detector.underflow.no_wrap", + kzt_rela_slot_current_is_unresolved_stub( + 0x10, KZT_RELA_STUB_COORDINATE_RUNTIME_REBASED, + -0x20, 0x10, 0x18, 0, 0), 0); +} + +static void test_distinct_targets_write_only_selected_slot(void) +{ + const uintptr_t current_target = 0x71000010; + const uintptr_t expected_guest_target = 0x71000020; + const uintptr_t native_bridge_target = 0x72000030; + const uintptr_t legacy_target = 0x73000040; + const uintptr_t other_value = 0x75000060; + const uintptr_t canary_before = 0x1111222233334444; + const uintptr_t canary_after = 0xaaaabbbbccccdddd; + kzt_guest_registry_t *registry = registry_with_source_and_owner(1, 1); + kzt_wrapper_probe_manifest_t manifest = wrapper_manifest(); + fake_bridge_state_t bridge = { + .next_bridge_target = native_bridge_target, + }; + kzt_wrapper_probe_bridge_ops_t bridge_provider = bridge_ops(&bridge); + kzt_rela_request_enricher_input_t input = { + .registry = registry, + .wrapper_manifest = &manifest, + .bridge_ops = &bridge_provider, + }; + fake_slot_bank_t bank; + kzt_patch_spike_slot_ops_t slot_ops = fake_slot_ops(&bank); + kzt_patch_spike_guard_t guard = enabled_guard(); + kzt_rela_request_enricher_result_t enrich_result; + kzt_rela_immediate_writer_result_t writer_result; + kzt_rela_immediate_candidate_request_t request = base_request(); + uintptr_t selected_slot; + + fake_slot_bank_init(&bank, canary_before, current_target, other_value, + canary_after); + selected_slot = (uintptr_t)&bank.values[0]; + + request.slot_addr = selected_slot; + request.slot_current_value = current_target; + request.expected_guest_target = expected_guest_target; + request.native_bridge_target = 0; + request.legacy_target = legacy_target; + + check_int("targets.enrich", + kzt_rela_immediate_request_enrich( + &request, &input, &enrich_result), 0); + check_ulong("targets.expected", request.expected_guest_target, + expected_guest_target); + check_ulong("targets.bridge", request.native_bridge_target, + native_bridge_target); + check_ulong("targets.legacy", request.legacy_target, legacy_target); + check_ulong("targets.slot", request.slot_addr, selected_slot); + check_int("targets.owner", request.owner_match, KZT_PATCH_OWNER_MATCH); + + check_int("targets.write", + kzt_rela_immediate_jump_slot_try_write( + &request, &guard, &slot_ops, &writer_result), 0); + check_int("targets.decision", writer_result.plan.decision.kind, + KZT_PATCH_DECISION_APPROVED); + check_int("targets.writer", writer_result.writer_called, 1); + check_int("targets.skip_legacy", writer_result.skip_legacy_write, 1); + check_ulong("targets.record.slot", writer_result.record.slot_addr, + selected_slot); + check_ulong("targets.record.expected", writer_result.record.expected_value, + current_target); + check_ulong("targets.record.replacement", + writer_result.record.replacement_value, + native_bridge_target); + check_ulong("targets.selected.value", bank.values[0], + native_bridge_target); + check_int("targets.selected.writes", + bank.entries[0].writer_write_calls, 1); + check_ulong("targets.other.value", bank.values[1], other_value); + check_int("targets.other.writes", bank.entries[1].writer_write_calls, 0); + check_ulong("targets.canary.before", bank.canary_before, canary_before); + check_ulong("targets.canary.after", bank.canary_after, canary_after); + check_int("targets.invalid_accesses", bank.invalid_accesses, 0); + + kzt_guest_registry_destroy(®istry); +} + +static void test_unresolved_stub_skips_owner_and_falls_back_to_legacy(void) +{ + const uintptr_t plt_start = 0x1000; + const uintptr_t plt_end = 0x1040; + const uintptr_t gotplt_start = 0x2000; + const uintptr_t gotplt_end = 0x2040; + const intptr_t load_bias = 0x70fff000; + const uintptr_t stub_target = 0x71000010; + const uintptr_t expected_guest_target = 0x71000020; + const uintptr_t native_bridge_target = 0x72000030; + const uintptr_t legacy_target = 0x73000040; + const uintptr_t other_value = 0x75000060; + const uintptr_t canary_before = 0x1111222233334444; + const uintptr_t canary_after = 0xaaaabbbbccccdddd; + kzt_guest_registry_t *registry = registry_with_source_and_owner(1, 1); + kzt_wrapper_probe_manifest_t manifest = wrapper_manifest(); + fake_bridge_state_t bridge = { + .next_bridge_target = native_bridge_target, + }; + kzt_wrapper_probe_bridge_ops_t bridge_provider = bridge_ops(&bridge); + kzt_rela_request_enricher_input_t input = { + .registry = registry, + .wrapper_manifest = &manifest, + .bridge_ops = &bridge_provider, + }; + fake_slot_bank_t bank; + kzt_patch_spike_slot_ops_t slot_ops = fake_slot_ops(&bank); + kzt_patch_spike_guard_t guard = enabled_guard(); + kzt_owner_resolution_t unsafe_resolution; + kzt_rela_request_enricher_result_t enrich_result; + kzt_rela_immediate_writer_result_t writer_result; + kzt_rela_immediate_candidate_request_t request = base_request(); + uintptr_t selected_slot; + + fake_slot_bank_init(&bank, canary_before, stub_target, other_value, + canary_after); + selected_slot = (uintptr_t)&bank.values[0]; + input.slot_current_value_is_unresolved_stub = + kzt_rela_slot_current_is_unresolved_stub( + stub_target, KZT_RELA_STUB_COORDINATE_RUNTIME_REBASED, + load_bias, plt_start, plt_end, + gotplt_start, gotplt_end); + check_int("stub.detector", input.slot_current_value_is_unresolved_stub, + 1); + + request.slot_addr = selected_slot; + request.slot_current_value = stub_target; + request.expected_guest_target = expected_guest_target; + request.native_bridge_target = 0; + request.legacy_target = legacy_target; + + kzt_owner_resolver_init(&unsafe_resolution); + check_int("stub.control.resolve", + kzt_owner_resolver_resolve_current( + registry, stub_target, expected_guest_target, + &unsafe_resolution), 0); + check_int("stub.control.would_match", unsafe_resolution.owner_match, + KZT_PATCH_OWNER_MATCH); + + check_int("stub.enrich", + kzt_rela_immediate_request_enrich( + &request, &input, &enrich_result), 0); + check_int("stub.owner", request.owner_match, KZT_PATCH_OWNER_UNKNOWN); + check_int("stub.owner.known", request.current_owner.known, 0); + check_int("stub.lazy_deferred", request.lazy_binding_deferred, 1); + check_int("stub.owner_present", enrich_result.owner_present, 0); + + check_int("stub.write", + kzt_rela_immediate_jump_slot_try_write( + &request, &guard, &slot_ops, &writer_result), 0); + check_int("stub.plan.status", writer_result.plan.status, + KZT_RELA_IMMEDIATE_CANDIDATE_SKIPPED); + check_int("stub.plan.reason", writer_result.plan.reason, + KZT_RELA_IMMEDIATE_CANDIDATE_REASON_DEFERRED_LAZY_BINDING); + check_int("stub.plan.candidate", writer_result.plan.candidate_present, 0); + check_int("stub.writer", writer_result.writer_called, 0); + check_int("stub.skip_legacy", writer_result.skip_legacy_write, 0); + + if (!writer_result.skip_legacy_write) { + fake_legacy_write(&bank, request.slot_addr, request.legacy_target); + } + check_ulong("stub.selected.value", bank.values[0], legacy_target); + check_int("stub.selected.writer_writes", + bank.entries[0].writer_write_calls, 0); + check_int("stub.selected.legacy_writes", + bank.entries[0].legacy_write_calls, 1); + check_ulong("stub.other.value", bank.values[1], other_value); + check_int("stub.other.writer_writes", + bank.entries[1].writer_write_calls, 0); + check_int("stub.other.legacy_writes", + bank.entries[1].legacy_write_calls, 0); + check_ulong("stub.canary.before", bank.canary_before, canary_before); + check_ulong("stub.canary.after", bank.canary_after, canary_after); + check_int("stub.invalid_accesses", bank.invalid_accesses, 0); + + kzt_guest_registry_destroy(®istry); +} + +static void test_no_manifest_keeps_default_fail_open(void) +{ + kzt_guest_registry_t *registry = registry_with_source_and_owner(1, 1); + fake_bridge_state_t bridge = { .next_bridge_target = 0x72000000 }; + kzt_wrapper_probe_bridge_ops_t ops = bridge_ops(&bridge); + kzt_rela_request_enricher_input_t input = { + .registry = registry, + .wrapper_manifest = NULL, + .bridge_ops = &ops, + }; + kzt_rela_request_enricher_result_t enrich_result; + kzt_rela_immediate_candidate_result_t plan_result; + kzt_rela_immediate_candidate_request_t request = base_request(); + + enrich_and_plan(&request, &input, &enrich_result, &plan_result); + check_int("no_manifest.wrapper", request.wrapper_match, + KZT_PATCH_WRAPPER_NO_MANIFEST); + check_ulong("no_manifest.bridge", request.native_bridge_target, 0); + check_int("no_manifest.decision", plan_result.decision.kind, + KZT_PATCH_DECISION_UNSUPPORTED); + check_int("no_manifest.reason", plan_result.decision.reason, + KZT_PATCH_REASON_INPUT_UNAVAILABLE_WRAPPER_MANIFEST); + check_int("no_manifest.add_bridge", bridge.add_calls, 0); + + kzt_guest_registry_destroy(®istry); +} + +static void test_missing_owner_blocks_approved_plan(void) +{ + kzt_guest_registry_t *registry = registry_with_source_and_owner(1, 0); + kzt_wrapper_probe_manifest_t manifest = wrapper_manifest(); + fake_bridge_state_t bridge = { .next_bridge_target = 0x72000000 }; + kzt_wrapper_probe_bridge_ops_t ops = bridge_ops(&bridge); + kzt_rela_request_enricher_input_t input = { + .registry = registry, + .wrapper_manifest = &manifest, + .bridge_ops = &ops, + }; + kzt_rela_request_enricher_result_t enrich_result; + kzt_rela_immediate_candidate_result_t plan_result; + kzt_rela_immediate_candidate_request_t request = base_request(); + + enrich_and_plan(&request, &input, &enrich_result, &plan_result); + check_int("missing_owner.owner", request.owner_match, + KZT_PATCH_OWNER_UNKNOWN); + check_int("missing_owner.decision", plan_result.decision.kind, + KZT_PATCH_DECISION_UNSUPPORTED); + check_int("missing_owner.reason", plan_result.decision.reason, + KZT_PATCH_REASON_INPUT_UNAVAILABLE_OWNER); + check_int("missing_owner.no_add_bridge", bridge.add_calls, 0); + + kzt_guest_registry_destroy(®istry); +} + +static void test_missing_dynamic_view_blocks_approved_plan(void) +{ + kzt_guest_registry_t *registry = registry_with_source_and_owner(0, 1); + kzt_wrapper_probe_manifest_t manifest = wrapper_manifest(); + fake_bridge_state_t bridge = { .next_bridge_target = 0x72000000 }; + kzt_wrapper_probe_bridge_ops_t ops = bridge_ops(&bridge); + kzt_rela_request_enricher_input_t input = { + .registry = registry, + .wrapper_manifest = &manifest, + .bridge_ops = &ops, + }; + kzt_rela_request_enricher_result_t enrich_result; + kzt_rela_immediate_candidate_result_t plan_result; + kzt_rela_immediate_candidate_request_t request = base_request(); + + enrich_and_plan(&request, &input, &enrich_result, &plan_result); + check_int("missing_dynamic.available", request.dynamic_view_available, 0); + check_int("missing_dynamic.decision", plan_result.decision.kind, + KZT_PATCH_DECISION_UNSUPPORTED); + check_int("missing_dynamic.reason", plan_result.decision.reason, + KZT_PATCH_REASON_INPUT_UNAVAILABLE_DYNAMIC_VIEW); + check_int("missing_dynamic.no_add_bridge", bridge.add_calls, 0); + + kzt_guest_registry_destroy(®istry); +} + +static void test_bridge_zero_blocks_approved_plan(void) +{ + kzt_guest_registry_t *registry = registry_with_source_and_owner(1, 1); + kzt_wrapper_probe_manifest_t manifest = wrapper_manifest(); + fake_bridge_state_t bridge = { .next_bridge_target = 0 }; + kzt_wrapper_probe_bridge_ops_t ops = bridge_ops(&bridge); + kzt_rela_request_enricher_input_t input = { + .registry = registry, + .wrapper_manifest = &manifest, + .bridge_ops = &ops, + }; + kzt_rela_request_enricher_result_t enrich_result; + kzt_rela_immediate_candidate_result_t plan_result; + kzt_rela_immediate_candidate_request_t request = base_request(); + + enrich_and_plan(&request, &input, &enrich_result, &plan_result); + check_int("bridge_zero.wrapper", request.wrapper_match, + KZT_PATCH_WRAPPER_VERSION_MATCH); + check_ulong("bridge_zero.bridge", request.native_bridge_target, 0); + check_int("bridge_zero.decision", plan_result.decision.kind, + KZT_PATCH_DECISION_UNSUPPORTED); + check_int("bridge_zero.reason", plan_result.decision.reason, + KZT_PATCH_REASON_INPUT_UNAVAILABLE_BRIDGE_TARGET); + + kzt_guest_registry_destroy(®istry); +} + +static void test_native_bridge_is_not_used_as_expected_owner(void) +{ + kzt_guest_registry_t *registry = registry_with_source_and_owner(1, 1); + kzt_guest_object_observation_t bridge_like = observation( + 0x3000, 0x72000000, 0x72010000, "libbridge-like.so"); + kzt_wrapper_probe_manifest_t manifest = wrapper_manifest(); + fake_bridge_state_t bridge = { .next_bridge_target = 0x72000080 }; + kzt_wrapper_probe_bridge_ops_t ops = bridge_ops(&bridge); + kzt_rela_request_enricher_input_t input = { + .registry = registry, + .wrapper_manifest = &manifest, + .bridge_ops = &ops, + }; + kzt_rela_request_enricher_result_t enrich_result; + kzt_rela_immediate_candidate_result_t plan_result; + kzt_rela_immediate_candidate_request_t request = base_request(); + + check_int("bridge_like.observe", + kzt_guest_registry_observe(registry, &bridge_like), + KZT_GUEST_REGISTRY_ADDED); + enrich_and_plan(&request, &input, &enrich_result, &plan_result); + check_int("bridge_like.owner", request.owner_match, + KZT_PATCH_OWNER_MATCH); + check_ulong("bridge_like.bridge", request.native_bridge_target, + 0x72000080); + check_int("bridge_like.decision", plan_result.decision.kind, + KZT_PATCH_DECISION_APPROVED); + + kzt_guest_registry_destroy(®istry); +} + +static void test_dead_object_is_not_relocation_source(void) +{ + kzt_guest_registry_t *registry = registry_with_source_and_owner(0, 0); + kzt_rela_request_enricher_input_t input = { .registry = registry }; + kzt_rela_request_enricher_result_t result; + kzt_rela_immediate_candidate_request_t request = base_request(); + + check_int("dead_source.retire", kzt_guest_registry_retire( + registry, 0x1000, 1), 0); + check_int("dead_source.enrich", kzt_rela_immediate_request_enrich( + &request, &input, &result), 0); + check_int("dead_source.absent", result.source_present, 0); + check_ulong("dead_source.no-link-map", request.source.link_map_addr, 0); + check_int("dead_source.no-dynamic", request.dynamic_view_available, 0); + kzt_guest_registry_destroy(®istry); +} + +int main(void) +{ + test_all_evidence_allows_approved_plan(); + test_unresolved_stub_detector_coordinates_and_bounds(); + test_distinct_targets_write_only_selected_slot(); + test_unresolved_stub_skips_owner_and_falls_back_to_legacy(); + test_no_manifest_keeps_default_fail_open(); + test_missing_owner_blocks_approved_plan(); + test_missing_dynamic_view_blocks_approved_plan(); + test_bridge_zero_blocks_approved_plan(); + test_native_bridge_is_not_used_as_expected_owner(); + test_dead_object_is_not_relocation_source(); + + if (failures) { + fprintf(stderr, "kzt-rela-request-enricher: %d failure(s)\n", + failures); + return 1; + } + + printf("kzt-rela-request-enricher: ok\n"); + return 0; +} diff --git a/tests/unit/kzt/test_rela_runtime_helper_chain.c b/tests/unit/kzt/test_rela_runtime_helper_chain.c new file mode 100644 index 00000000000..df564854665 --- /dev/null +++ b/tests/unit/kzt/test_rela_runtime_helper_chain.c @@ -0,0 +1,479 @@ +#include +#include +#include +#include + +#include "elf.h" +#include "target/i386/latx/include/box64context.h" +#include "target/i386/latx/include/bridge_private.h" +#include "target/i386/latx/include/khash.h" +#include "target/i386/latx/include/kzt_bridge_exact.h" +#include "target/i386/latx/include/kzt_guest_registry.h" +#include "target/i386/latx/include/kzt_rela_immediate_candidate.h" +#include "target/i386/latx/include/kzt_rela_request_enricher.h" +#include "target/i386/latx/include/kzt_rela_runtime_bridge.h" +#include "target/i386/latx/include/librarian_private.h" +#include "target/i386/latx/include/library.h" +#include "target/i386/latx/include/library_private.h" + +#define FIXTURE_SYMBOL "uname" +#define FIXTURE_VERSION "GLIBC_2.36" + +/* This test calls the runtime adapter, enricher, and writer helpers directly. + It is a helper-chain test, not a full RelocateElfRELA E2E test, and it does + not model or claim coverage of the production legacy-store path. */ + +static int failures; +static int fixture_not_applicable; + +KHASH_MAP_IMPL_STR(symbolmap, wrapper_t) +KHASH_MAP_IMPL_STR(symbol2map, symbol2_t) + +static void fixture_iFp(uintptr_t fnc) +{ + (void)fnc; +} + +typedef struct fixture_bridge_map { + void *native_symbol; + uintptr_t target; + int check_calls; +} fixture_bridge_map_t; + +typedef struct runtime_fixture { + box64context_t context; + lib_t scope; + library_t library; + fixture_bridge_map_t bridge_map; + onebridge_t bridge_entry; + uintptr_t native_symbol; +} runtime_fixture_t; + +enum fixture_setup_status { + FIXTURE_SETUP_ERROR = -1, + FIXTURE_SETUP_OK = 0, + FIXTURE_SETUP_SKIP = 1, +}; + +uintptr_t CheckBridged(bridge_t *bridge, void *fnc); + +uintptr_t CheckBridged(bridge_t *bridge, void *fnc) +{ + fixture_bridge_map_t *map = (fixture_bridge_map_t *)bridge; + + if (!map) { + return 0; + } + ++map->check_calls; + if (!fnc || fnc != map->native_symbol) { + return 0; + } + return map->target; +} + +typedef struct guarded_slot { + uintptr_t value; + uintptr_t canary; +} guarded_slot_t; + +static void check_int(const char *name, int got, int expected) +{ + if (got == expected) { + return; + } + fprintf(stderr, "%s: got %d expected %d\n", name, got, expected); + ++failures; +} + +static void check_ulong(const char *name, uintptr_t got, uintptr_t expected) +{ + if (got == expected) { + return; + } + fprintf(stderr, "%s: got 0x%lx expected 0x%lx\n", name, + (unsigned long)got, (unsigned long)expected); + ++failures; +} + +static kzt_guest_object_observation_t observation( + uintptr_t link_map_addr, uintptr_t map_start, uintptr_t map_end, + const char *name) +{ + return (kzt_guest_object_observation_t) { + .link_map_addr = link_map_addr, + .load_bias = { map_start, KZT_GUEST_FIELD_OK }, + .dynamic_addr = { map_start + 0x1000, KZT_GUEST_FIELD_OK }, + .map_start = { map_start, KZT_GUEST_FIELD_OK }, + .map_end = { map_end, KZT_GUEST_FIELD_OK }, + .namespace_id = { 0, KZT_GUEST_FIELD_OK }, + .path = { name, KZT_GUEST_FIELD_OK }, + .soname = { name, KZT_GUEST_FIELD_OK }, + .dynamic_view_status = KZT_GUEST_FIELD_NOT_PARSED, + }; +} + +static kzt_guest_registry_t *helper_chain_registry(void) +{ + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_object_observation_t source = observation( + 0x1000, 0x70000000, 0x70010000, "librequester.so"); + kzt_guest_object_observation_t owner = observation( + 0x2000, 0x71000000, 0x71010000, "libowner.so"); + kzt_guest_dynamic_view_t view = { + .dynamic_addr = 0x70001000, + .load_bias = 0x70000000, + .status = KZT_GUEST_DYNAMIC_COMPLETE, + .entry_count = 1, + .has_null = 1, + }; + + check_int("registry.source", + kzt_guest_registry_observe(registry, &source), + KZT_GUEST_REGISTRY_ADDED); + check_int("registry.owner", + kzt_guest_registry_observe(registry, &owner), + KZT_GUEST_REGISTRY_ADDED); + check_int("registry.dynamic", + kzt_guest_registry_commit_dynamic_view( + registry, source.link_map_addr, 1, &view), + KZT_GUEST_REGISTRY_UPDATED); + return registry; +} + +static kzt_rela_immediate_candidate_request_t request_for( + guarded_slot_t *slot, uintptr_t legacy_target, const char *version) +{ + return (kzt_rela_immediate_candidate_request_t) { + .relocation_type = R_X86_64_JUMP_SLOT, + .table_kind = KZT_PATCH_TABLE_PLT_RELA, + .entry_index = 3, + .entry_addr = 0x70002000, + .source = { + .known = 1, + .map_start = 0x70000000, + .map_end = 0x70010000, + .soname = "librequester.so", + .path = "/guest/librequester.so", + }, + .dynamic_addr = 0x70001000, + .load_bias = 0x70000000, + .slot_addr = (uintptr_t)&slot->value, + .slot_current_value_present = 1, + .slot_current_value = slot->value, + .expected_guest_target = 0x71000020, + .legacy_target = legacy_target, + .symbol_index = 7, + .symbol_name = FIXTURE_SYMBOL, + .version = version, + }; +} + +static void runtime_fixture_destroy(runtime_fixture_t *fixture) +{ + if (!fixture) { + return; + } + if (fixture->library.symbolmap) { + kh_destroy(symbolmap, fixture->library.symbolmap); + fixture->library.symbolmap = NULL; + } + if (fixture->library.priv.w.lib) { + dlclose(fixture->library.priv.w.lib); + fixture->library.priv.w.lib = NULL; + } + free(fixture->scope.libraries); + fixture->scope.libraries = NULL; + fixture->scope.libsz = 0; +} + +static int runtime_fixture_setup(runtime_fixture_t *fixture) +{ + static char libc_name[] = "libc.so.6"; + library_t **libraries; + const char *version_error; + khint_t key; + int inserted; + + if (fixture_not_applicable) { + return FIXTURE_SETUP_SKIP; + } + memset(fixture, 0, sizeof(*fixture)); + libraries = malloc(sizeof(*libraries)); + if (!libraries) { + return FIXTURE_SETUP_ERROR; + } + libraries[0] = &fixture->library; + fixture->scope.libraries = libraries; + fixture->scope.libsz = 1; + fixture->scope.context = &fixture->context; + fixture->context.maplib = &fixture->scope; + fixture->library.name = libc_name; + fixture->library.path = libc_name; + fixture->library.type = LIB_WRAPPED; + fixture->library.active = 1; + fixture->library.context = &fixture->context; + fixture->library.priv.w.lib = + dlopen("libc.so.6", RTLD_LAZY | RTLD_LOCAL); + if (!fixture->library.priv.w.lib) { + fprintf(stderr, "dlopen(libc.so.6): %s\n", dlerror()); + runtime_fixture_destroy(fixture); + return FIXTURE_SETUP_ERROR; + } + dlerror(); + fixture->native_symbol = (uintptr_t)dlvsym( + fixture->library.priv.w.lib, FIXTURE_SYMBOL, FIXTURE_VERSION); + version_error = dlerror(); + if (!fixture->native_symbol || version_error) { + printf("SKIP: %s@%s is not provided by this host libc; " + "runtime helper-chain fixture is not applicable\n", + FIXTURE_SYMBOL, FIXTURE_VERSION); + fixture_not_applicable = 1; + runtime_fixture_destroy(fixture); + return FIXTURE_SETUP_SKIP; + } + + fixture->library.priv.w.bridge = (bridge_t *)&fixture->bridge_map; + fixture->library.symbolmap = kh_init(symbolmap); + if (!fixture->library.symbolmap) { + runtime_fixture_destroy(fixture); + return FIXTURE_SETUP_ERROR; + } + key = kh_put(symbolmap, fixture->library.symbolmap, FIXTURE_SYMBOL, + &inserted); + if (inserted == -1 || key == kh_end(fixture->library.symbolmap)) { + runtime_fixture_destroy(fixture); + return FIXTURE_SETUP_ERROR; + } + kh_value(fixture->library.symbolmap, key) = fixture_iFp; + fixture->bridge_entry.CC = 0xCC; + fixture->bridge_entry.S = 'S'; + fixture->bridge_entry.C = 'C'; + fixture->bridge_entry.w = fixture_iFp; + fixture->bridge_entry.f = fixture->native_symbol; + fixture->bridge_entry.C3 = 0xC3; + fixture->bridge_map.native_symbol = (void *)fixture->native_symbol; + fixture->bridge_map.target = (uintptr_t)&fixture->bridge_entry.CC; + return FIXTURE_SETUP_OK; +} + +static void test_runtime_adapter_helper_chain(void) +{ + const uintptr_t canary = 0xcafebabedeadbeefULL; + const uintptr_t legacy_target = 0x7bad0000; + runtime_fixture_t fixture; + int setup_status; + uintptr_t bridge_target; + guarded_slot_t slot = { 0x71000010, canary }; + kzt_guest_registry_t *registry; + kzt_wrapper_bridge_provider_t runtime_provider; + kzt_rela_request_enricher_input_t enrich_input = { 0 }; + kzt_rela_request_enricher_result_t enrich_result; + kzt_rela_immediate_candidate_request_t request; + kzt_rela_immediate_writer_result_t writer_result; + kzt_patch_spike_guard_t guard; + kzt_patch_spike_config_t config = { 1, 1, 1 }; + + setup_status = runtime_fixture_setup(&fixture); + if (setup_status == FIXTURE_SETUP_SKIP) { + return; + } + if (setup_status != FIXTURE_SETUP_OK) { + ++failures; + return; + } + bridge_target = fixture.bridge_map.target; + check_int("runtime-provider.guest-native-distinct", + bridge_target != fixture.native_symbol, 1); + check_int("runtime-provider.prepare", + kzt_rela_runtime_wrapper_provider_prepare( + &fixture.context, &fixture.library, bridge_target, + FIXTURE_SYMBOL, + FIXTURE_VERSION, &runtime_provider), + 1); + check_int("runtime-provider.map-checked", + fixture.bridge_map.check_calls > 0, 1); + check_int("runtime-provider.no-add", + runtime_provider.bridge_ops.add_bridge == NULL, 1); + + registry = helper_chain_registry(); + request = request_for(&slot, legacy_target, FIXTURE_VERSION); + enrich_input.registry = registry; + enrich_input.slot_current_value_is_unresolved_stub = 0; + enrich_input.wrapper_manifest = &runtime_provider.manifest; + enrich_input.bridge_ops = &runtime_provider.bridge_ops; + check_int("enrich.call", + kzt_rela_immediate_request_enrich( + &request, &enrich_input, &enrich_result), + 0); + check_ulong("enrich.bridge", request.native_bridge_target, + bridge_target); + + kzt_patch_spike_guard_init(&guard, &config); + check_int("writer.call", + kzt_rela_immediate_jump_slot_try_write( + &request, &guard, NULL, &writer_result), + 0); + check_int("writer.called", writer_result.writer_called, 1); + check_int("writer.skip-legacy", writer_result.skip_legacy_write, 1); + check_ulong("writer.slot-is-bridge", slot.value, bridge_target); + check_int("writer.slot-not-legacy", slot.value != legacy_target, 1); + check_ulong("writer.canary", slot.canary, canary); + kzt_guest_registry_destroy(®istry); + runtime_fixture_destroy(&fixture); +} + +static void test_version_mismatch_leaves_helper_fallback_eligible(void) +{ + const uintptr_t canary = 0x1122334455667788ULL; + const uintptr_t legacy_target = 0x7bad1000; + const uintptr_t initial_slot = 0x71000010; + runtime_fixture_t fixture; + int setup_status; + uintptr_t bridge_target; + guarded_slot_t slot = { initial_slot, canary }; + kzt_wrapper_bridge_provider_t runtime_provider; + kzt_guest_registry_t *registry; + kzt_rela_request_enricher_input_t enrich_input = { 0 }; + kzt_rela_request_enricher_result_t enrich_result; + kzt_rela_immediate_candidate_request_t request; + kzt_rela_immediate_writer_result_t writer_result; + kzt_patch_spike_guard_t guard; + kzt_patch_spike_config_t config = { 1, 1, 1 }; + + setup_status = runtime_fixture_setup(&fixture); + if (setup_status == FIXTURE_SETUP_SKIP) { + return; + } + if (setup_status != FIXTURE_SETUP_OK) { + ++failures; + return; + } + bridge_target = fixture.bridge_map.target; + check_int("mismatch.provider", + kzt_rela_runtime_wrapper_provider_prepare( + &fixture.context, &fixture.library, bridge_target, + FIXTURE_SYMBOL, + "GLIBC_NOT_REAL", &runtime_provider), + 0); + registry = helper_chain_registry(); + request = request_for(&slot, legacy_target, "GLIBC_NOT_REAL"); + enrich_input.registry = registry; + enrich_input.slot_current_value_is_unresolved_stub = 0; + enrich_input.wrapper_manifest = &runtime_provider.manifest; + enrich_input.bridge_ops = &runtime_provider.bridge_ops; + check_int("mismatch.enrich", + kzt_rela_immediate_request_enrich( + &request, &enrich_input, &enrich_result), + 0); + kzt_patch_spike_guard_init(&guard, &config); + check_int("mismatch.writer", + kzt_rela_immediate_jump_slot_try_write( + &request, &guard, NULL, &writer_result), + 0); + check_int("mismatch.writer-not-called", writer_result.writer_called, 0); + check_int("mismatch.fallback-eligible", + writer_result.skip_legacy_write, 0); + check_ulong("mismatch.helper-left-slot-untouched", slot.value, + initial_slot); + check_ulong("mismatch.canary", slot.canary, canary); + kzt_guest_registry_destroy(®istry); + runtime_fixture_destroy(&fixture); +} + +static void test_bridge_owner_mismatch_fails_closed(void) +{ + runtime_fixture_t fixture; + kzt_wrapper_bridge_provider_t runtime_provider; + uintptr_t resolved_target; + int setup_status; + + setup_status = runtime_fixture_setup(&fixture); + if (setup_status == FIXTURE_SETUP_SKIP) { + return; + } + if (setup_status != FIXTURE_SETUP_OK) { + ++failures; + return; + } + resolved_target = fixture.bridge_map.target; + fixture.bridge_map.target = resolved_target + sizeof(onebridge_t); + check_int("owner-mismatch.prepare", + kzt_rela_runtime_wrapper_provider_prepare( + &fixture.context, &fixture.library, resolved_target, + FIXTURE_SYMBOL, FIXTURE_VERSION, &runtime_provider), + 0); + check_int("owner-mismatch.map-checked", + fixture.bridge_map.check_calls > 0, 1); + check_int("owner-mismatch.unavailable", + runtime_provider.manifest.available, 0); + runtime_fixture_destroy(&fixture); +} + +static void test_null_inputs_fail_without_bridge_inspection(void) +{ + runtime_fixture_t fixture; + kzt_wrapper_bridge_provider_t runtime_provider; + uintptr_t resolved_target; + int setup_status; + + check_int("null-provider.error", + kzt_rela_runtime_wrapper_provider_prepare( + NULL, NULL, 0, NULL, NULL, NULL), + -1); + + setup_status = runtime_fixture_setup(&fixture); + if (setup_status == FIXTURE_SETUP_SKIP) { + return; + } + if (setup_status != FIXTURE_SETUP_OK) { + ++failures; + return; + } + resolved_target = fixture.bridge_map.target; + check_int("null-target.prepare", + kzt_rela_runtime_wrapper_provider_prepare( + &fixture.context, &fixture.library, 0, FIXTURE_SYMBOL, + FIXTURE_VERSION, &runtime_provider), + 0); + fixture.bridge_map.target = 0; + check_int("null-map-result.prepare", + kzt_rela_runtime_wrapper_provider_prepare( + &fixture.context, &fixture.library, resolved_target, + FIXTURE_SYMBOL, FIXTURE_VERSION, &runtime_provider), + 0); + check_int("null-map-result.checked", + fixture.bridge_map.check_calls > 0, 1); + fixture.bridge_map.target = resolved_target; + fixture.library.priv.w.bridge = NULL; + check_int("null-bridge-map.prepare", + kzt_rela_runtime_wrapper_provider_prepare( + &fixture.context, &fixture.library, + fixture.bridge_map.target, FIXTURE_SYMBOL, + FIXTURE_VERSION, &runtime_provider), + 0); + check_int("null-exact.target", + kzt_bridge_is_exact(0, fixture_iFp, + (void *)fixture.native_symbol), + 0); + runtime_fixture_destroy(&fixture); +} + +int main(void) +{ + test_runtime_adapter_helper_chain(); + test_version_mismatch_leaves_helper_fallback_eligible(); + test_bridge_owner_mismatch_fails_closed(); + test_null_inputs_fail_without_bridge_inspection(); + + if (failures) { + fprintf(stderr, "kzt-rela-runtime-helper-chain: %d failure(s)\n", + failures); + return 1; + } + if (fixture_not_applicable) { + puts("kzt-rela-runtime-helper-chain: skipped"); + return 77; + } + puts("kzt-rela-runtime-helper-chain: ok"); + return 0; +} diff --git a/tests/unit/kzt/test_runtime_candidate_enrichment_shadow.c b/tests/unit/kzt/test_runtime_candidate_enrichment_shadow.c new file mode 100644 index 00000000000..112c545b6b1 --- /dev/null +++ b/tests/unit/kzt/test_runtime_candidate_enrichment_shadow.c @@ -0,0 +1,1093 @@ +#include +#include + +#include "elf.h" +#include "target/i386/latx/include/kzt_runtime_candidate_shadow.h" + +#define ARRAY_SIZE(array) (sizeof(array) / sizeof((array)[0])) +#define TEST_ELF64_R_INFO(symbol, type) \ + (((uint64_t)(symbol) << 32) | (uint32_t)(type)) + +#define TEST_SOURCE_LINK_MAP 0x1000ULL +#define TEST_OWNER_A_LINK_MAP 0x2000ULL +#define TEST_OWNER_B_LINK_MAP 0x3000ULL +#define TEST_SOURCE_BASE 0x60000000ULL +#define TEST_OWNER_A_BASE 0x71000000ULL +#define TEST_OWNER_B_BASE 0x72000000ULL +#define TEST_LOAD_BIAS TEST_SOURCE_BASE +#define TEST_PLT_RELA_ADDR 0x68001000ULL +#define TEST_RELA_ADDR 0x68002000ULL +#define TEST_SYMTAB_ADDR 0x68003000ULL +#define TEST_STRTAB_ADDR 0x68004000ULL +#define TEST_VERSYM_ADDR 0x68005000ULL +#define TEST_VERNEED_ADDR 0x68006000ULL +#define TEST_PLT_SLOT_ADDR (TEST_LOAD_BIAS + 0x3010) +#define TEST_GOT_SLOT_ADDR (TEST_LOAD_BIAS + 0x4020) +#define TEST_NATIVE_PUTS 0x90001000ULL +#define TEST_NATIVE_ERRNO 0x90002000ULL +#define TEST_LEGACY_TARGET 0xa0003000ULL + +enum { + TEST_SYMBOL_PUTS = 1, + TEST_SYMBOL_ERRNO = 2, + TEST_STR_PUTS = 1, + TEST_STR_ERRNO = 6, + TEST_STR_VERSION = 12, +}; + +static const char test_dynstr[] = + "\0puts\0errno\0GLIBC_2.2.5\0"; + +static int failures; +static size_t tests_run; +static size_t slot_write_checks; + +typedef struct fake_region { + uintptr_t guest_base; + const void *host_base; + size_t size; +} fake_region_t; + +typedef struct fake_memory { + fake_region_t regions[16]; + size_t region_count; + uintptr_t fail_addr; + size_t read_calls; +} fake_memory_t; + +typedef struct guarded_slot { + uint64_t before; + uint64_t value; + uint64_t after; +} guarded_slot_t; + +typedef struct version_fixture { + Elf64_Verneed need; + Elf64_Vernaux aux; +} version_fixture_t; + +typedef struct expected_target_state { + uintptr_t target; + size_t calls; + const char *last_symbol; +} expected_target_state_t; + +typedef struct bridge_state { + uintptr_t cache_target; + uintptr_t add_target; + uintptr_t last_native_symbol; + size_t check_calls; + size_t mutation_calls; +} bridge_state_t; + +typedef struct stub_classifier_state { + uintptr_t plt_start; + uintptr_t plt_end; + uintptr_t gotplt_start; + uintptr_t gotplt_end; + size_t calls; +} stub_classifier_state_t; + +typedef struct generation_query_state { + unsigned long generations[4]; + int results[4]; + size_t response_count; + size_t calls; +} generation_query_state_t; + +typedef struct shadow_fixture { + fake_memory_t memory; + kzt_guest_link_map_reader_ops_t reader_ops; + kzt_guest_registry_t *registry; + kzt_guest_dynamic_view_t view; + kzt_patch_object_ref_t source; + + Elf64_Rela plt_rela; + Elf64_Rela rela; + Elf64_Sym symbols[3]; + Elf64_Half versions[3]; + version_fixture_t version; + guarded_slot_t plt_slot; + guarded_slot_t got_slot; + + kzt_patch_candidate_t candidates[2]; + char string_storage[256]; + kzt_runtime_candidate_shadow_record_t records[2]; + + kzt_wrapper_probe_entry_t manifest_entries[2]; + kzt_wrapper_probe_manifest_t manifest; + expected_target_state_t expected; + bridge_state_t bridge; + kzt_wrapper_probe_bridge_ops_t bridge_ops; + stub_classifier_state_t stub_classifier; + generation_query_state_t generation_query; + + kzt_runtime_got_plt_candidate_request_t collector_request; + kzt_runtime_candidate_shadow_input_t input; + kzt_runtime_candidate_shadow_result_t result; +} shadow_fixture_t; + +static void check_int(const char *name, int got, int expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got %d expected %d\n", name, got, expected); + ++failures; +} + +static void check_size(const char *name, size_t got, size_t expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got %lu expected %lu\n", name, + (unsigned long)got, (unsigned long)expected); + ++failures; +} + +static void check_ulong(const char *name, + unsigned long got, + unsigned long expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got 0x%lx expected 0x%lx\n", + name, got, expected); + ++failures; +} + +static void check_memory(const char *name, + const void *got, + const void *expected, + size_t size) +{ + if (memcmp(got, expected, size) == 0) { + return; + } + + fprintf(stderr, "%s: memory changed\n", name); + ++failures; +} + +static void add_region(fake_memory_t *memory, + uintptr_t guest_base, + const void *host_base, + size_t size) +{ + fake_region_t *region; + + if (memory->region_count >= ARRAY_SIZE(memory->regions)) { + ++failures; + return; + } + + region = &memory->regions[memory->region_count++]; + region->guest_base = guest_base; + region->host_base = host_base; + region->size = size; +} + +static int fake_read_memory(uintptr_t guest_addr, + void *dst, + size_t size, + void *opaque) +{ + fake_memory_t *memory = opaque; + size_t i; + + ++memory->read_calls; + if (memory->fail_addr && guest_addr == memory->fail_addr) { + return -1; + } + + for (i = 0; i < memory->region_count; ++i) { + const fake_region_t *region = &memory->regions[i]; + uintptr_t offset; + + if (guest_addr < region->guest_base) { + continue; + } + + offset = guest_addr - region->guest_base; + if (offset > region->size || size > region->size - offset) { + continue; + } + + memcpy(dst, (const char *)region->host_base + offset, size); + return 0; + } + + return -1; +} + +static kzt_guest_dynamic_field_t runtime_field(uint64_t value) +{ + return (kzt_guest_dynamic_field_t) { + .present = 1, + .value = value, + .address_semantics = KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS, + }; +} + +static kzt_guest_dynamic_field_t scalar_field(uint64_t value) +{ + return (kzt_guest_dynamic_field_t) { + .present = 1, + .value = value, + .address_semantics = KZT_GUEST_DYNAMIC_SCALAR, + }; +} + +static kzt_guest_object_observation_t object_observation( + uintptr_t link_map_addr, + uintptr_t map_start, + uintptr_t map_end, + const char *soname, + const char *path) +{ + return (kzt_guest_object_observation_t) { + .link_map_addr = link_map_addr, + .load_bias = { map_start, KZT_GUEST_FIELD_OK }, + .dynamic_addr = { map_start + 0x1000, KZT_GUEST_FIELD_OK }, + .map_start = { map_start, KZT_GUEST_FIELD_OK }, + .map_end = { map_end, KZT_GUEST_FIELD_OK }, + .namespace_id = { 0, KZT_GUEST_FIELD_OK }, + .path = { path, KZT_GUEST_FIELD_OK }, + .soname = { soname, KZT_GUEST_FIELD_OK }, + .dynamic_view_status = KZT_GUEST_FIELD_NOT_PARSED, + }; +} + +static kzt_patch_object_ref_t source_ref_from_registry( + kzt_guest_registry_t *registry) +{ + kzt_guest_registry_dump_t dump = { 0 }; + kzt_patch_object_ref_t source = { + .known = 1, + .link_map_addr = TEST_SOURCE_LINK_MAP, + .map_start = TEST_SOURCE_BASE, + .map_end = TEST_SOURCE_BASE + 0x10000, + .soname = "libsource.so", + .path = "/guest/libsource.so", + }; + size_t i; + + if (kzt_guest_registry_dump_snapshot(registry, &dump) != 0) { + ++failures; + return source; + } + + for (i = 0; i < dump.count; ++i) { + if (dump.objects[i].link_map_addr == TEST_SOURCE_LINK_MAP) { + source.generation = dump.objects[i].generation; + break; + } + } + + kzt_guest_registry_dump_free(&dump); + return source; +} + +static int resolve_expected_guest_target( + const kzt_patch_candidate_t *candidate, + uintptr_t *expected_guest_target, + void *opaque) +{ + expected_target_state_t *state = opaque; + + ++state->calls; + state->last_symbol = candidate->symbol_name; + if (!state->target) { + return -1; + } + + *expected_guest_target = state->target; + return 0; +} + +static uintptr_t check_bridge(uintptr_t native_symbol, void *opaque) +{ + bridge_state_t *state = opaque; + + ++state->check_calls; + state->last_native_symbol = native_symbol; + return state->cache_target; +} + +static uintptr_t unexpected_mutating_bridge( + const kzt_wrapper_probe_bridge_request_t *request, + void *opaque) +{ + bridge_state_t *state = opaque; + + (void)request; + ++state->mutation_calls; + return state->add_target; +} + +static kzt_runtime_candidate_shadow_stub_classification_t +classify_precise_stub(const kzt_patch_candidate_t *candidate, void *opaque) +{ + stub_classifier_state_t *state = opaque; + uintptr_t start = 0; + uintptr_t end = 0; + + ++state->calls; + if (candidate->reloc_type == KZT_PATCH_RELOCATION_JUMP_SLOT) { + start = state->plt_start; + end = state->plt_end; + } else if (candidate->reloc_type == KZT_PATCH_RELOCATION_GLOB_DAT) { + start = state->gotplt_start; + end = state->gotplt_end; + } else { + return KZT_RUNTIME_CANDIDATE_SHADOW_STUB_UNKNOWN; + } + + if (start == 0 || start >= end) { + return KZT_RUNTIME_CANDIDATE_SHADOW_STUB_UNKNOWN; + } + + return candidate->slot_current_value >= start && + candidate->slot_current_value < end ? + KZT_RUNTIME_CANDIDATE_SHADOW_STUB_MATCH : + KZT_RUNTIME_CANDIDATE_SHADOW_STUB_NO_MATCH; +} + +static int query_test_generation(uintptr_t link_map_addr, + unsigned long *generation, + void *opaque) +{ + generation_query_state_t *state = opaque; + size_t index; + + (void)link_map_addr; + if (state->response_count == 0) { + return -1; + } + + index = state->calls < state->response_count ? + state->calls : state->response_count - 1; + ++state->calls; + if (state->results[index] != 0) { + return state->results[index]; + } + + *generation = state->generations[index]; + return 0; +} + +static void fixture_init(shadow_fixture_t *fixture, int include_glob_dat) +{ + kzt_guest_object_observation_t source; + kzt_guest_object_observation_t owner_a; + kzt_guest_object_observation_t owner_b; + + memset(fixture, 0, sizeof(*fixture)); + fixture->registry = kzt_guest_registry_init(); + source = object_observation( + TEST_SOURCE_LINK_MAP, TEST_SOURCE_BASE, + TEST_SOURCE_BASE + 0x10000, "libsource.so", + "/guest/libsource.so"); + owner_a = object_observation( + TEST_OWNER_A_LINK_MAP, TEST_OWNER_A_BASE, + TEST_OWNER_A_BASE + 0x10000, "libowner-a.so", + "/guest/libowner-a.so"); + owner_b = object_observation( + TEST_OWNER_B_LINK_MAP, TEST_OWNER_B_BASE, + TEST_OWNER_B_BASE + 0x10000, "libowner-b.so", + "/guest/libowner-b.so"); + + check_int("fixture.observe.source", + kzt_guest_registry_observe(fixture->registry, &source), + KZT_GUEST_REGISTRY_ADDED); + check_int("fixture.observe.owner-a", + kzt_guest_registry_observe(fixture->registry, &owner_a), + KZT_GUEST_REGISTRY_ADDED); + check_int("fixture.observe.owner-b", + kzt_guest_registry_observe(fixture->registry, &owner_b), + KZT_GUEST_REGISTRY_ADDED); + fixture->source = source_ref_from_registry(fixture->registry); + + fixture->reader_ops.read_memory = fake_read_memory; + fixture->reader_ops.opaque = &fixture->memory; + fixture->view.dynamic_addr = TEST_SOURCE_BASE + 0x1000; + fixture->view.load_bias = TEST_LOAD_BIAS; + fixture->view.status = KZT_GUEST_DYNAMIC_COMPLETE; + fixture->view.has_null = 1; + fixture->view.jmprel = runtime_field(TEST_PLT_RELA_ADDR); + fixture->view.pltrelsz = scalar_field(sizeof(fixture->plt_rela)); + fixture->view.pltrel = scalar_field(DT_RELA); + if (include_glob_dat) { + fixture->view.rela = runtime_field(TEST_RELA_ADDR); + fixture->view.relasz = scalar_field(sizeof(fixture->rela)); + fixture->view.relaent = scalar_field(sizeof(Elf64_Rela)); + } + + fixture->view.symtab = runtime_field(TEST_SYMTAB_ADDR); + fixture->view.syment = scalar_field(sizeof(Elf64_Sym)); + fixture->view.strtab = runtime_field(TEST_STRTAB_ADDR); + fixture->view.strsz = scalar_field(sizeof(test_dynstr)); + fixture->view.versym = runtime_field(TEST_VERSYM_ADDR); + fixture->view.verneed = runtime_field(TEST_VERNEED_ADDR); + fixture->view.verneednum = scalar_field(1); + + fixture->plt_rela.r_offset = TEST_PLT_SLOT_ADDR - TEST_LOAD_BIAS; + fixture->plt_rela.r_info = + TEST_ELF64_R_INFO(TEST_SYMBOL_PUTS, R_X86_64_JUMP_SLOT); + fixture->rela.r_offset = TEST_GOT_SLOT_ADDR - TEST_LOAD_BIAS; + fixture->rela.r_info = + TEST_ELF64_R_INFO(TEST_SYMBOL_ERRNO, R_X86_64_GLOB_DAT); + fixture->symbols[TEST_SYMBOL_PUTS].st_name = TEST_STR_PUTS; + fixture->symbols[TEST_SYMBOL_ERRNO].st_name = TEST_STR_ERRNO; + fixture->versions[TEST_SYMBOL_PUTS] = 2; + fixture->versions[TEST_SYMBOL_ERRNO] = 2; + fixture->version.need.vn_version = 1; + fixture->version.need.vn_cnt = 1; + fixture->version.need.vn_aux = sizeof(Elf64_Verneed); + fixture->version.aux.vna_other = 2; + fixture->version.aux.vna_name = TEST_STR_VERSION; + + fixture->plt_slot.before = 0x1111111111111111ULL; + fixture->plt_slot.value = TEST_OWNER_A_BASE + 0x40; + fixture->plt_slot.after = 0x2222222222222222ULL; + fixture->got_slot.before = 0x3333333333333333ULL; + fixture->got_slot.value = TEST_OWNER_A_BASE + 0x50; + fixture->got_slot.after = 0x4444444444444444ULL; + + add_region(&fixture->memory, TEST_PLT_RELA_ADDR, + &fixture->plt_rela, sizeof(fixture->plt_rela)); + if (include_glob_dat) { + add_region(&fixture->memory, TEST_RELA_ADDR, + &fixture->rela, sizeof(fixture->rela)); + } + add_region(&fixture->memory, TEST_SYMTAB_ADDR, + fixture->symbols, sizeof(fixture->symbols)); + add_region(&fixture->memory, TEST_STRTAB_ADDR, + test_dynstr, sizeof(test_dynstr)); + add_region(&fixture->memory, TEST_VERSYM_ADDR, + fixture->versions, sizeof(fixture->versions)); + add_region(&fixture->memory, TEST_VERNEED_ADDR, + &fixture->version, sizeof(fixture->version)); + add_region(&fixture->memory, TEST_PLT_SLOT_ADDR, + &fixture->plt_slot.value, sizeof(fixture->plt_slot.value)); + if (include_glob_dat) { + add_region(&fixture->memory, TEST_GOT_SLOT_ADDR, + &fixture->got_slot.value, + sizeof(fixture->got_slot.value)); + } + + fixture->manifest_entries[0] = (kzt_wrapper_probe_entry_t) { + .symbol_name = "puts", + .symbol_version = "GLIBC_2.2.5", + .wrapper_name = "wrapped_puts", + .wrapper_symbol_version = "GLIBC_2.2.5", + .native_symbol = TEST_NATIVE_PUTS, + }; + fixture->manifest_entries[1] = (kzt_wrapper_probe_entry_t) { + .symbol_name = "errno", + .symbol_version = "GLIBC_2.2.5", + .wrapper_name = "wrapped_errno", + .wrapper_symbol_version = "GLIBC_2.2.5", + .native_symbol = TEST_NATIVE_ERRNO, + }; + fixture->manifest = (kzt_wrapper_probe_manifest_t) { + .available = 1, + .manifest_name = "shadow-test", + .entries = fixture->manifest_entries, + .entry_count = ARRAY_SIZE(fixture->manifest_entries), + }; + fixture->expected.target = TEST_OWNER_A_BASE + 0x80; + fixture->bridge.cache_target = TEST_OWNER_B_BASE + 0x88; + fixture->bridge.add_target = 0xdeadbeefULL; + fixture->bridge_ops = (kzt_wrapper_probe_bridge_ops_t) { + .check_bridge = check_bridge, + .add_bridge = unexpected_mutating_bridge, + .opaque = &fixture->bridge, + }; + fixture->stub_classifier = (stub_classifier_state_t) { + .plt_start = TEST_SOURCE_BASE + 0x400, + .plt_end = TEST_SOURCE_BASE + 0x580, + .gotplt_start = TEST_SOURCE_BASE + 0x580, + .gotplt_end = TEST_SOURCE_BASE + 0x700, + }; + + fixture->collector_request = + (kzt_runtime_got_plt_candidate_request_t) { + .view = &fixture->view, + .reader_ops = &fixture->reader_ops, + .source = &fixture->source, + .dynamic_view_generation = fixture->source.generation, + .candidates = fixture->candidates, + .candidate_capacity = ARRAY_SIZE(fixture->candidates), + .string_storage = fixture->string_storage, + .string_storage_size = sizeof(fixture->string_storage), + }; + fixture->input = (kzt_runtime_candidate_shadow_input_t) { + .collector_request = &fixture->collector_request, + .registry = fixture->registry, + .wrapper_manifest = &fixture->manifest, + .bridge_ops = &fixture->bridge_ops, + .resolve_expected_guest_target = + resolve_expected_guest_target, + .expected_target_opaque = &fixture->expected, + .classify_stub = classify_precise_stub, + .stub_classifier_opaque = &fixture->stub_classifier, + .records = fixture->records, + .record_capacity = ARRAY_SIZE(fixture->records), + }; +} + +static int fixture_run(shadow_fixture_t *fixture) +{ + guarded_slot_t plt_before = fixture->plt_slot; + guarded_slot_t got_before = fixture->got_slot; + size_t decision_total = 0; + size_t reason_total = 0; + size_t i; + int status; + + status = kzt_runtime_candidate_shadow_run( + &fixture->input, &fixture->result); + if (fixture->result.status == + KZT_RUNTIME_CANDIDATE_SHADOW_FAIL_OPEN) { + check_size("fail-open.collector-consumable-count", + fixture->result.collector_result.candidate_count, 0); + } + for (i = 0; + i < KZT_RUNTIME_CANDIDATE_SHADOW_DECISION_BUCKETS; ++i) { + decision_total += fixture->result.decision_histogram[i]; + } + for (i = 0; + i < KZT_RUNTIME_CANDIDATE_SHADOW_REASON_BUCKETS; ++i) { + reason_total += fixture->result.reason_histogram[i]; + } + check_size("histogram.decision-conservation", decision_total, + fixture->result.record_count); + check_size("histogram.reason-conservation", reason_total, + fixture->result.record_count); + check_memory("slot.plt", &fixture->plt_slot, &plt_before, + sizeof(plt_before)); + check_memory("slot.got", &fixture->got_slot, &got_before, + sizeof(got_before)); + slot_write_checks += 2; + return status; +} + +static void fixture_destroy(shadow_fixture_t *fixture) +{ + kzt_guest_registry_destroy(&fixture->registry); +} + +static kzt_runtime_candidate_shadow_record_t *record_for_reloc( + shadow_fixture_t *fixture, + kzt_patch_relocation_type_t reloc_type) +{ + size_t i; + + for (i = 0; i < fixture->result.record_count; ++i) { + if (fixture->records[i].decision.reloc_type == reloc_type) { + return &fixture->records[i]; + } + } + + return NULL; +} + +static void test_w01_owner_match(void) +{ + shadow_fixture_t fixture; + + ++tests_run; + fixture_init(&fixture, 0); + check_int("W01.run", fixture_run(&fixture), 0); + check_int("W01.status", fixture.result.status, + KZT_RUNTIME_CANDIDATE_SHADOW_OK); + check_size("W01.records", fixture.result.record_count, 1); + check_int("W01.owner", fixture.records[0].decision.owner_match, + KZT_PATCH_OWNER_MATCH); + check_int("W01.decision", fixture.records[0].decision.kind, + KZT_PATCH_DECISION_APPROVED); + check_int("W01.reason", fixture.records[0].decision.reason, + KZT_PATCH_REASON_APPROVED_NATIVE_BRIDGE); + check_size("W01.mutation-calls", fixture.bridge.mutation_calls, 0); + fixture_destroy(&fixture); +} + +static void test_w02_owner_mismatch(void) +{ + shadow_fixture_t fixture; + + ++tests_run; + fixture_init(&fixture, 0); + fixture.plt_slot.value = TEST_OWNER_B_BASE + 0x40; + check_int("W02.run", fixture_run(&fixture), 0); + check_int("W02.owner", fixture.records[0].decision.owner_match, + KZT_PATCH_OWNER_MISMATCH); + check_int("W02.decision", fixture.records[0].decision.kind, + KZT_PATCH_DECISION_REJECTED); + check_int("W02.reason", fixture.records[0].decision.reason, + KZT_PATCH_REASON_POLICY_OWNER_MISMATCH); + fixture_destroy(&fixture); +} + +static void test_w03_owner_unknown(void) +{ + shadow_fixture_t fixture; + + ++tests_run; + fixture_init(&fixture, 0); + fixture.plt_slot.value = 0x7f000040ULL; + check_int("W03.run", fixture_run(&fixture), 0); + check_int("W03.owner", fixture.records[0].decision.owner_match, + KZT_PATCH_OWNER_UNKNOWN); + check_int("W03.decision", fixture.records[0].decision.kind, + KZT_PATCH_DECISION_UNSUPPORTED); + check_int("W03.reason", fixture.records[0].decision.reason, + KZT_PATCH_REASON_INPUT_UNAVAILABLE_OWNER); + fixture_destroy(&fixture); +} + +static void test_w04_no_manifest(void) +{ + shadow_fixture_t fixture; + + ++tests_run; + fixture_init(&fixture, 0); + fixture.input.wrapper_manifest = NULL; + check_int("W04.run", fixture_run(&fixture), 0); + check_int("W04.wrapper", fixture.records[0].decision.wrapper_match, + KZT_PATCH_WRAPPER_NO_MANIFEST); + check_int("W04.decision", fixture.records[0].decision.kind, + KZT_PATCH_DECISION_UNSUPPORTED); + check_int("W04.reason", fixture.records[0].decision.reason, + KZT_PATCH_REASON_INPUT_UNAVAILABLE_WRAPPER_MANIFEST); + check_size("W04.cache-calls", fixture.bridge.check_calls, 0); + check_size("W04.mutation-calls", fixture.bridge.mutation_calls, 0); + fixture_destroy(&fixture); +} + +static void test_w05_bridge_zero_never_adds(void) +{ + shadow_fixture_t fixture; + + ++tests_run; + fixture_init(&fixture, 0); + fixture.bridge.cache_target = 0; + check_int("W05.run", fixture_run(&fixture), 0); + check_size("W05.cache-calls", fixture.bridge.check_calls, 1); + check_size("W05.mutation-calls", fixture.bridge.mutation_calls, 0); + check_ulong("W05.bridge", fixture.records[0].decision.bridge_target, 0); + check_int("W05.decision", fixture.records[0].decision.kind, + KZT_PATCH_DECISION_UNSUPPORTED); + check_int("W05.reason", fixture.records[0].decision.reason, + KZT_PATCH_REASON_INPUT_UNAVAILABLE_BRIDGE_TARGET); + fixture_destroy(&fixture); +} + +static void test_w06_plt_and_got_stubs_are_deferred(void) +{ + shadow_fixture_t fixture; + kzt_runtime_candidate_shadow_record_t *plt; + kzt_runtime_candidate_shadow_record_t *got; + + ++tests_run; + fixture_init(&fixture, 1); + fixture.plt_slot.value = TEST_SOURCE_BASE + 0x500; + fixture.got_slot.value = TEST_SOURCE_BASE + 0x600; + check_int("W06.run", fixture_run(&fixture), 0); + check_size("W06.records", fixture.result.record_count, 2); + plt = record_for_reloc(&fixture, KZT_PATCH_RELOCATION_JUMP_SLOT); + got = record_for_reloc(&fixture, KZT_PATCH_RELOCATION_GLOB_DAT); + check_int("W06.plt-present", plt != NULL, 1); + check_int("W06.got-present", got != NULL, 1); + if (plt) { + check_int("W06.plt.decision", plt->decision.kind, + KZT_PATCH_DECISION_DEFERRED); + check_int("W06.plt.reason", plt->decision.reason, + KZT_PATCH_REASON_DEFERRED_LAZY_BINDING); + } + if (got) { + check_int("W06.got.decision", got->decision.kind, + KZT_PATCH_DECISION_DEFERRED); + check_int("W06.got.reason", got->decision.reason, + KZT_PATCH_REASON_DEFERRED_LAZY_BINDING); + check_int("W06.got.observe-only", got->observe_only, 1); + } + check_size("W06.owner-lookups", fixture.expected.calls, 0); + check_size("W06.mutation-calls", fixture.bridge.mutation_calls, 0); + fixture_destroy(&fixture); + + fixture_init(&fixture, 1); + fixture.plt_slot.value = TEST_SOURCE_BASE + 0x8000; + fixture.got_slot.value = TEST_SOURCE_BASE + 0x8100; + check_int("W06.same-dso-nonstub.run", fixture_run(&fixture), 0); + check_size("W06.same-dso-nonstub.records", + fixture.result.record_count, 2); + plt = record_for_reloc(&fixture, KZT_PATCH_RELOCATION_JUMP_SLOT); + got = record_for_reloc(&fixture, KZT_PATCH_RELOCATION_GLOB_DAT); + if (plt) { + check_int("W06.same-dso-nonstub.plt-not-deferred", + plt->decision.kind == KZT_PATCH_DECISION_DEFERRED, 0); + } + if (got) { + check_int("W06.same-dso-nonstub.got-not-deferred", + got->decision.kind == KZT_PATCH_DECISION_DEFERRED, 0); + } + check_size("W06.same-dso-nonstub.classifier-calls", + fixture.stub_classifier.calls, 2); + check_size("W06.same-dso-nonstub.owner-lookups", + fixture.expected.calls, 2); + fixture_destroy(&fixture); + + fixture_init(&fixture, 0); + fixture.plt_slot.value = TEST_SOURCE_BASE + 0x500; + fixture.input.classify_stub = NULL; + check_int("W06.no-evidence.run", fixture_run(&fixture), 0); + check_int("W06.no-evidence.not-deferred", + fixture.records[0].decision.kind == + KZT_PATCH_DECISION_DEFERRED, + 0); + check_size("W06.no-evidence.owner-lookups", fixture.expected.calls, 1); + fixture_destroy(&fixture); +} + +static void test_w07_expected_bridge_slot_exclude_legacy(void) +{ + shadow_fixture_t fixture; + uintptr_t legacy_target = TEST_LEGACY_TARGET; + + ++tests_run; + fixture_init(&fixture, 0); + fixture.plt_slot.value = TEST_OWNER_A_BASE + 0x40; + fixture.expected.target = TEST_OWNER_A_BASE + 0x80; + fixture.bridge.cache_target = TEST_OWNER_B_BASE + 0x88; + check_int("W07.run", fixture_run(&fixture), 0); + check_ulong("W07.slot-current", + fixture.records[0].decision.slot_current_value, + TEST_OWNER_A_BASE + 0x40); + check_ulong("W07.expected-owner", + fixture.records[0].owner_resolution.expected_owner + .link_map_addr, + TEST_OWNER_A_LINK_MAP); + check_ulong("W07.current-owner", + fixture.records[0].owner_resolution.current_owner + .link_map_addr, + TEST_OWNER_A_LINK_MAP); + check_ulong("W07.bridge", + fixture.records[0].decision.bridge_target, + TEST_OWNER_B_BASE + 0x88); + check_ulong("W07.cache-key", fixture.bridge.last_native_symbol, + TEST_NATIVE_PUTS); + check_int("W07.audit-only", fixture.records[0].audit_only, 1); + check_int("W07.legacy-not-consumed", + fixture.records[0].legacy_target_consumed, 0); + check_int("W07.legacy-not-slot", + legacy_target == + fixture.records[0].decision.slot_current_value, + 0); + check_int("W07.legacy-not-bridge", + legacy_target == + fixture.records[0].decision.bridge_target, + 0); + check_int("W07.legacy-not-expected", + legacy_target == fixture.expected.target, 0); + check_int("W07.decision", fixture.records[0].decision.kind, + KZT_PATCH_DECISION_APPROVED); + check_size("W07.mutation-calls", fixture.bridge.mutation_calls, 0); + fixture_destroy(&fixture); +} + +static void test_w08_bridge_ops_missing(void) +{ + shadow_fixture_t fixture; + + ++tests_run; + fixture_init(&fixture, 0); + fixture.input.bridge_ops = NULL; + check_int("W08.run", fixture_run(&fixture), 0); + check_int("W08.wrapper", fixture.records[0].decision.wrapper_match, + KZT_PATCH_WRAPPER_VERSION_MATCH); + check_ulong("W08.bridge", fixture.records[0].decision.bridge_target, 0); + check_int("W08.decision", fixture.records[0].decision.kind, + KZT_PATCH_DECISION_UNSUPPORTED); + check_int("W08.reason", fixture.records[0].decision.reason, + KZT_PATCH_REASON_INPUT_UNAVAILABLE_BRIDGE_TARGET); + check_size("W08.cache-calls", fixture.bridge.check_calls, 0); + check_size("W08.mutation-calls", fixture.bridge.mutation_calls, 0); + fixture_destroy(&fixture); +} + +static void test_w09_generation_change_fails_open(void) +{ + shadow_fixture_t fixture; + unsigned long generation; + + ++tests_run; + fixture_init(&fixture, 0); + fixture.collector_request.dynamic_view_generation = + fixture.source.generation + 1; + check_int("W09.run", fixture_run(&fixture), 0); + check_int("W09.status", fixture.result.status, + KZT_RUNTIME_CANDIDATE_SHADOW_FAIL_OPEN); + check_int("W09.reason", fixture.result.reason, + KZT_RUNTIME_CANDIDATE_SHADOW_REASON_OBJECT_GENERATION_CHANGED); + check_size("W09.candidates", fixture.result.candidate_count, 0); + check_size("W09.records", fixture.result.record_count, 0); + check_size("W09.owner-lookups", fixture.expected.calls, 0); + check_size("W09.cache-calls", fixture.bridge.check_calls, 0); + check_size("W09.mutation-calls", fixture.bridge.mutation_calls, 0); + fixture_destroy(&fixture); + + fixture_init(&fixture, 0); + fixture.source.generation = 0; + check_int("W09.zero.run", fixture_run(&fixture), 0); + check_int("W09.zero.status", fixture.result.status, + KZT_RUNTIME_CANDIDATE_SHADOW_FAIL_OPEN); + check_size("W09.zero.owner-lookups", fixture.expected.calls, 0); + fixture_destroy(&fixture); + + fixture_init(&fixture, 0); + fixture.source.link_map_addr = TEST_SOURCE_LINK_MAP + 1; + check_int("W09.disappeared.run", fixture_run(&fixture), 0); + check_int("W09.disappeared.status", fixture.result.status, + KZT_RUNTIME_CANDIDATE_SHADOW_FAIL_OPEN); + check_size("W09.disappeared.owner-lookups", fixture.expected.calls, 0); + fixture_destroy(&fixture); + + fixture_init(&fixture, 0); + fixture.generation_query.response_count = 1; + fixture.generation_query.results[0] = -1; + fixture.input.query_generation = query_test_generation; + fixture.input.generation_query_opaque = &fixture.generation_query; + check_int("W09.lookup-failed.run", fixture_run(&fixture), 0); + check_int("W09.lookup-failed.status", fixture.result.status, + KZT_RUNTIME_CANDIDATE_SHADOW_FAIL_OPEN); + fixture_destroy(&fixture); + + fixture_init(&fixture, 0); + fixture.generation_query.response_count = 1; + fixture.generation_query.results[0] = -2; + fixture.input.query_generation = query_test_generation; + fixture.input.generation_query_opaque = &fixture.generation_query; + check_int("W09.nonunique.run", fixture_run(&fixture), 0); + check_int("W09.nonunique.status", fixture.result.status, + KZT_RUNTIME_CANDIDATE_SHADOW_FAIL_OPEN); + fixture_destroy(&fixture); + + fixture_init(&fixture, 0); + generation = fixture.source.generation; + fixture.generation_query.response_count = 2; + fixture.generation_query.generations[0] = generation; + fixture.generation_query.generations[1] = generation + 1; + fixture.input.query_generation = query_test_generation; + fixture.input.generation_query_opaque = &fixture.generation_query; + check_int("W09.cross-generation.run", fixture_run(&fixture), 0); + check_int("W09.cross-generation.status", fixture.result.status, + KZT_RUNTIME_CANDIDATE_SHADOW_FAIL_OPEN); + check_int("W09.cross-generation.reason", fixture.result.reason, + KZT_RUNTIME_CANDIDATE_SHADOW_REASON_OBJECT_GENERATION_CHANGED); + check_size("W09.cross-generation.queries", + fixture.generation_query.calls, 2); + check_size("W09.cross-generation.owner-lookups", + fixture.expected.calls, 1); + fixture_destroy(&fixture); +} + +static void test_w10_rel_fails_open(void) +{ + shadow_fixture_t fixture; + + ++tests_run; + fixture_init(&fixture, 0); + fixture.view.pltrel = scalar_field(DT_REL); + check_int("W10.run", fixture_run(&fixture), 0); + check_int("W10.status", fixture.result.status, + KZT_RUNTIME_CANDIDATE_SHADOW_FAIL_OPEN); + check_int("W10.reason", fixture.result.reason, + KZT_RUNTIME_CANDIDATE_SHADOW_REASON_COLLECTOR_FAIL_OPEN); + check_int("W10.collector-reason", fixture.result.collector_result.reason, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_DT_REL_UNSUPPORTED); + check_int("W10.table", fixture.result.collector_result.table_kind, + KZT_PATCH_TABLE_PLT_REL); + check_size("W10.records", fixture.result.record_count, 0); + check_size("W10.mutation-calls", fixture.bridge.mutation_calls, 0); + fixture_destroy(&fixture); +} + +static void test_w10b_dynamic_rel_fails_open(void) +{ + shadow_fixture_t fixture; + + ++tests_run; + fixture_init(&fixture, 0); + fixture.view.rel = runtime_field(TEST_RELA_ADDR + 0x1000); + fixture.view.relsz = scalar_field(sizeof(Elf64_Rel)); + fixture.view.relent = scalar_field(sizeof(Elf64_Rel)); + check_int("W10b.run", fixture_run(&fixture), 0); + check_int("W10b.status", fixture.result.status, + KZT_RUNTIME_CANDIDATE_SHADOW_FAIL_OPEN); + check_int("W10b.reason", fixture.result.reason, + KZT_RUNTIME_CANDIDATE_SHADOW_REASON_COLLECTOR_FAIL_OPEN); + check_int("W10b.collector-reason", fixture.result.collector_result.reason, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_DT_REL_UNSUPPORTED); + check_int("W10b.table", fixture.result.collector_result.table_kind, + KZT_PATCH_TABLE_REL); + check_size("W10b.records", fixture.result.record_count, 0); + check_size("W10b.mutation-calls", fixture.bridge.mutation_calls, 0); + fixture_destroy(&fixture); +} + +static void test_w11_glob_dat_is_observe_only(void) +{ + shadow_fixture_t fixture; + kzt_runtime_candidate_shadow_record_t *plt; + kzt_runtime_candidate_shadow_record_t *got; + + ++tests_run; + fixture_init(&fixture, 1); + check_int("W11.run", fixture_run(&fixture), 0); + check_int("W11.status", fixture.result.status, + KZT_RUNTIME_CANDIDATE_SHADOW_OK); + check_size("W11.records", fixture.result.record_count, 2); + check_size("W11.eligible", fixture.result.eligible_count, 1); + check_size("W11.observe-only", fixture.result.observe_only_count, 1); + check_size("W11.approved-histogram", + fixture.result.decision_histogram[ + KZT_PATCH_DECISION_APPROVED], 2); + plt = record_for_reloc(&fixture, KZT_PATCH_RELOCATION_JUMP_SLOT); + got = record_for_reloc(&fixture, KZT_PATCH_RELOCATION_GLOB_DAT); + check_int("W11.plt-present", plt != NULL, 1); + check_int("W11.got-present", got != NULL, 1); + if (plt) { + check_int("W11.plt.eligible", plt->eligible, 1); + check_int("W11.plt.observe-only", plt->observe_only, 0); + } + if (got) { + check_int("W11.got.decision", got->decision.kind, + KZT_PATCH_DECISION_APPROVED); + check_int("W11.got.eligible", got->eligible, 0); + check_int("W11.got.observe-only", got->observe_only, 1); + } + check_size("W11.mutation-calls", fixture.bridge.mutation_calls, 0); + fixture_destroy(&fixture); +} + +static void test_w12_candidate_capacity_fails_open(void) +{ + shadow_fixture_t fixture; + kzt_patch_candidate_t zero_candidates[2] = { 0 }; + + ++tests_run; + fixture_init(&fixture, 1); + fixture.collector_request.candidate_capacity = 1; + check_int("W12.run", fixture_run(&fixture), 0); + check_int("W12.status", fixture.result.status, + KZT_RUNTIME_CANDIDATE_SHADOW_FAIL_OPEN); + check_int("W12.collector-reason", fixture.result.collector_result.reason, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_CAPACITY_EXCEEDED); + check_memory("W12.candidates-cleared", fixture.candidates, + zero_candidates, sizeof(zero_candidates)); + check_size("W12.records", fixture.result.record_count, 0); + check_size("W12.mutation-calls", fixture.bridge.mutation_calls, 0); + fixture_destroy(&fixture); +} + +static void test_w13_record_capacity_fails_open(void) +{ + shadow_fixture_t fixture; + kzt_patch_candidate_t zero_candidates[2] = { 0 }; + kzt_runtime_candidate_shadow_record_t zero_records[2] = { 0 }; + + ++tests_run; + fixture_init(&fixture, 1); + fixture.input.record_capacity = 1; + check_int("W13.run", fixture_run(&fixture), 0); + check_int("W13.status", fixture.result.status, + KZT_RUNTIME_CANDIDATE_SHADOW_FAIL_OPEN); + check_int("W13.reason", fixture.result.reason, + KZT_RUNTIME_CANDIDATE_SHADOW_REASON_RECORD_CAPACITY_EXCEEDED); + check_memory("W13.candidates-cleared", fixture.candidates, + zero_candidates, sizeof(zero_candidates)); + check_memory("W13.records-cleared", fixture.records, + zero_records, sizeof(zero_records)); + check_size("W13.mutation-calls", fixture.bridge.mutation_calls, 0); + fixture_destroy(&fixture); +} + +static void test_w14_relocation_read_failure(void) +{ + shadow_fixture_t fixture; + + ++tests_run; + fixture_init(&fixture, 0); + fixture.memory.fail_addr = TEST_PLT_RELA_ADDR; + check_int("W14.run", fixture_run(&fixture), 0); + check_int("W14.status", fixture.result.status, + KZT_RUNTIME_CANDIDATE_SHADOW_FAIL_OPEN); + check_int("W14.collector-reason", fixture.result.collector_result.reason, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_RELOCATION_READ_FAILED); + check_ulong("W14.read-error", + fixture.result.collector_result.read_error_addr, + TEST_PLT_RELA_ADDR); + check_size("W14.records", fixture.result.record_count, 0); + check_size("W14.mutation-calls", fixture.bridge.mutation_calls, 0); + fixture_destroy(&fixture); +} + +static void test_w15_slot_read_failure(void) +{ + shadow_fixture_t fixture; + + ++tests_run; + fixture_init(&fixture, 0); + fixture.memory.fail_addr = TEST_PLT_SLOT_ADDR; + check_int("W15.run", fixture_run(&fixture), 0); + check_int("W15.status", fixture.result.status, + KZT_RUNTIME_CANDIDATE_SHADOW_FAIL_OPEN); + check_int("W15.collector-reason", fixture.result.collector_result.reason, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_SLOT_READ_FAILED); + check_ulong("W15.read-error", + fixture.result.collector_result.read_error_addr, + TEST_PLT_SLOT_ADDR); + check_size("W15.records", fixture.result.record_count, 0); + check_size("W15.mutation-calls", fixture.bridge.mutation_calls, 0); + fixture_destroy(&fixture); +} + +int main(void) +{ + test_w01_owner_match(); + test_w02_owner_mismatch(); + test_w03_owner_unknown(); + test_w04_no_manifest(); + test_w05_bridge_zero_never_adds(); + test_w06_plt_and_got_stubs_are_deferred(); + test_w07_expected_bridge_slot_exclude_legacy(); + test_w08_bridge_ops_missing(); + test_w09_generation_change_fails_open(); + test_w10_rel_fails_open(); + test_w10b_dynamic_rel_fails_open(); + test_w11_glob_dat_is_observe_only(); + test_w12_candidate_capacity_fails_open(); + test_w13_record_capacity_fails_open(); + test_w14_relocation_read_failure(); + test_w15_slot_read_failure(); + + if (failures) { + fprintf(stderr, + "kzt-runtime-candidate-enrichment-shadow: " + "%d failure(s), %lu scenario(s)\n", + failures, (unsigned long)tests_run); + return 1; + } + + printf("kzt-runtime-candidate-enrichment-shadow: " + "%lu scenario(s) passed, %lu guarded slot checks\n", + (unsigned long)tests_run, (unsigned long)slot_write_checks); + return 0; +} diff --git a/tests/unit/kzt/test_runtime_got_plt_candidate.c b/tests/unit/kzt/test_runtime_got_plt_candidate.c new file mode 100644 index 00000000000..ac057ec4eaf --- /dev/null +++ b/tests/unit/kzt/test_runtime_got_plt_candidate.c @@ -0,0 +1,933 @@ +#include +#include + +#include "elf.h" +#include "target/i386/latx/include/kzt_runtime_got_plt_candidate.h" + +#define TEST_ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0])) +#define TEST_R_INFO(sym, type) ((((uint64_t)(sym)) << 32) | (type)) + +enum { + TEST_PLT_RELA_ADDR = 0x7000100000ULL, + TEST_RELA_ADDR = 0x7000200000ULL, + TEST_STRTAB_ADDR = 0x7000300000ULL, + TEST_SYMTAB_ADDR = 0x7000400000ULL, + TEST_VERSYM_ADDR = 0x7000500000ULL, + TEST_VERNEED_ADDR = 0x7000600000ULL, + TEST_LOAD_BIAS = 0x7000000000ULL, + TEST_STR_PUTS = 1, + TEST_STR_ERRNO = 6, + TEST_STR_GLIBC = 12, + TEST_STR_LIBFOO = 24, + TEST_JUMP_SLOT_SYMBOL = 11, + TEST_GLOB_DAT_SYMBOL = 22, +}; + +static const char test_dynstr[] = + "\0puts\0errno\0GLIBC_2.2.5\0LIBFOO_1.0\0"; + +typedef struct version_need_image { + Elf64_Verneed need; + Elf64_Vernaux aux[2]; +} version_need_image_t; + +typedef struct symbol_version_fixture { + Elf64_Sym dynsym[23]; + Elf64_Half versym[23]; + version_need_image_t verneed; +} symbol_version_fixture_t; + +typedef struct fake_region { + uintptr_t guest_base; + const void *host_base; + size_t size; +} fake_region_t; + +typedef struct fake_memory { + fake_region_t regions[16]; + size_t region_count; + uintptr_t fail_addr; + int read_calls; +} fake_memory_t; + +static int failures; + +static void check_int(const char *name, int got, int expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got %d expected %d\n", name, got, expected); + ++failures; +} + +static void check_size(const char *name, size_t got, size_t expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got %lu expected %lu\n", name, + (unsigned long)got, (unsigned long)expected); + ++failures; +} + +static void check_ulong(const char *name, + unsigned long got, + unsigned long expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got 0x%lx expected 0x%lx\n", name, got, expected); + ++failures; +} + +static void check_str(const char *name, const char *got, const char *expected) +{ + if ((!got && !expected) || + (got && expected && !strcmp(got, expected))) { + return; + } + + fprintf(stderr, "%s: got '%s' expected '%s'\n", name, + got ? got : "(null)", expected ? expected : "(null)"); + ++failures; +} + +static void add_region(fake_memory_t *memory, + uintptr_t guest_base, + const void *host_base, + size_t size) +{ + fake_region_t *region = &memory->regions[memory->region_count++]; + + region->guest_base = guest_base; + region->host_base = host_base; + region->size = size; +} + +static int fake_read_memory(uintptr_t guest_addr, + void *dst, + size_t size, + void *opaque) +{ + fake_memory_t *memory = opaque; + size_t i; + + ++memory->read_calls; + if (memory->fail_addr && guest_addr == memory->fail_addr) { + return -1; + } + + for (i = 0; i < memory->region_count; ++i) { + const fake_region_t *region = &memory->regions[i]; + uintptr_t offset; + + if (guest_addr < region->guest_base) { + continue; + } + + offset = guest_addr - region->guest_base; + if (offset > region->size || size > region->size - offset) { + continue; + } + + memcpy(dst, (const char *)region->host_base + offset, size); + return 0; + } + + return -1; +} + +static kzt_guest_link_map_reader_ops_t fake_ops(fake_memory_t *memory) +{ + kzt_guest_link_map_reader_ops_t ops = { + .read_memory = fake_read_memory, + .opaque = memory, + }; + + return ops; +} + +static kzt_guest_dynamic_field_t runtime_field(uint64_t value) +{ + return (kzt_guest_dynamic_field_t) { + .present = 1, + .value = value, + .address_semantics = KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS, + }; +} + +static kzt_guest_dynamic_field_t scalar_field(uint64_t value) +{ + return (kzt_guest_dynamic_field_t) { + .present = 1, + .value = value, + .address_semantics = KZT_GUEST_DYNAMIC_SCALAR, + }; +} + +static kzt_guest_dynamic_view_t base_view(void) +{ + return (kzt_guest_dynamic_view_t) { + .dynamic_addr = 0x7000002000ULL, + .load_bias = TEST_LOAD_BIAS, + .status = KZT_GUEST_DYNAMIC_COMPLETE, + .has_null = 1, + }; +} + +static kzt_patch_object_ref_t source_ref(void) +{ + return (kzt_patch_object_ref_t) { + .known = 1, + .link_map_addr = 0x7000001000ULL, + .map_start = TEST_LOAD_BIAS, + .map_end = TEST_LOAD_BIAS + 0x100000, + .generation = 42, + .soname = "libcandidate.so", + .path = "/guest/libcandidate.so", + }; +} + +static kzt_runtime_got_plt_candidate_request_t make_request( + const kzt_guest_dynamic_view_t *view, + const kzt_guest_link_map_reader_ops_t *ops, + const kzt_patch_object_ref_t *source, + kzt_patch_candidate_t *candidates, + size_t capacity) +{ + static char string_storage[512]; + + memset(string_storage, 0, sizeof(string_storage)); + return (kzt_runtime_got_plt_candidate_request_t) { + .view = view, + .reader_ops = ops, + .source = source, + .dynamic_view_generation = 88, + .candidates = candidates, + .candidate_capacity = capacity, + .string_storage = string_storage, + .string_storage_size = sizeof(string_storage), + }; +} + +static void add_symbol_version_fixture(fake_memory_t *memory, + kzt_guest_dynamic_view_t *view, + symbol_version_fixture_t *fixture) +{ + memset(fixture, 0, sizeof(*fixture)); + fixture->dynsym[TEST_JUMP_SLOT_SYMBOL].st_name = TEST_STR_PUTS; + fixture->dynsym[TEST_GLOB_DAT_SYMBOL].st_name = TEST_STR_ERRNO; + fixture->versym[TEST_JUMP_SLOT_SYMBOL] = 2; + fixture->versym[TEST_GLOB_DAT_SYMBOL] = 3; + fixture->verneed.need.vn_version = 1; + fixture->verneed.need.vn_cnt = 2; + fixture->verneed.need.vn_aux = sizeof(Elf64_Verneed); + fixture->verneed.need.vn_next = 0; + fixture->verneed.aux[0].vna_other = 2; + fixture->verneed.aux[0].vna_name = TEST_STR_GLIBC; + fixture->verneed.aux[0].vna_next = sizeof(Elf64_Vernaux); + fixture->verneed.aux[1].vna_other = 3; + fixture->verneed.aux[1].vna_name = TEST_STR_LIBFOO; + fixture->verneed.aux[1].vna_next = 0; + + view->symtab = runtime_field(TEST_SYMTAB_ADDR); + view->syment = scalar_field(sizeof(Elf64_Sym)); + view->strtab = runtime_field(TEST_STRTAB_ADDR); + view->strsz = scalar_field(sizeof(test_dynstr)); + view->versym = runtime_field(TEST_VERSYM_ADDR); + view->verneed = runtime_field(TEST_VERNEED_ADDR); + view->verneednum = scalar_field(1); + + add_region(memory, TEST_SYMTAB_ADDR, fixture->dynsym, + sizeof(fixture->dynsym)); + add_region(memory, TEST_STRTAB_ADDR, test_dynstr, sizeof(test_dynstr)); + add_region(memory, TEST_VERSYM_ADDR, fixture->versym, + sizeof(fixture->versym)); + add_region(memory, TEST_VERNEED_ADDR, &fixture->verneed, + sizeof(fixture->verneed)); +} + +static void check_fail_open( + const char *name, + const kzt_runtime_got_plt_candidate_result_t *result, + kzt_runtime_got_plt_candidate_reason_t reason, + kzt_patch_reason_t patch_reason) +{ + char field[128]; + + snprintf(field, sizeof(field), "%s.status", name); + check_int(field, result->status, + KZT_RUNTIME_GOT_PLT_CANDIDATE_FAIL_OPEN); + snprintf(field, sizeof(field), "%s.reason", name); + check_int(field, result->reason, reason); + snprintf(field, sizeof(field), "%s.patch-present", name); + check_int(field, result->patch_reason_present, 1); + snprintf(field, sizeof(field), "%s.patch-reason", name); + check_int(field, result->patch_reason, patch_reason); + snprintf(field, sizeof(field), "%s.count", name); + check_size(field, result->candidate_count, 0); +} + +static void test_happy_path_enumerates_jump_slot_and_glob_dat(void) +{ + Elf64_Rela plt_relas[] = { + { + .r_offset = 0x3010, + .r_info = TEST_R_INFO(TEST_JUMP_SLOT_SYMBOL, + R_X86_64_JUMP_SLOT), + .r_addend = 0, + }, + }; + Elf64_Rela relas[] = { + { + .r_offset = 0x4020, + .r_info = TEST_R_INFO(TEST_GLOB_DAT_SYMBOL, + R_X86_64_GLOB_DAT), + .r_addend = 0, + }, + }; + uint64_t plt_slot = 0x7100001000ULL; + uint64_t rela_slot = 0x7200002000ULL; + fake_memory_t memory = { 0 }; + symbol_version_fixture_t symbols; + kzt_guest_link_map_reader_ops_t ops = fake_ops(&memory); + kzt_patch_object_ref_t source = source_ref(); + kzt_guest_dynamic_view_t view = base_view(); + kzt_patch_candidate_t candidates[4]; + kzt_runtime_got_plt_candidate_request_t request; + kzt_runtime_got_plt_candidate_result_t result; + + view.jmprel = runtime_field(TEST_PLT_RELA_ADDR); + view.pltrelsz = scalar_field(sizeof(plt_relas)); + view.pltrel = scalar_field(DT_RELA); + view.rela = runtime_field(TEST_RELA_ADDR); + view.relasz = scalar_field(sizeof(relas)); + view.relaent = scalar_field(sizeof(Elf64_Rela)); + add_symbol_version_fixture(&memory, &view, &symbols); + + add_region(&memory, TEST_PLT_RELA_ADDR, plt_relas, sizeof(plt_relas)); + add_region(&memory, TEST_RELA_ADDR, relas, sizeof(relas)); + add_region(&memory, TEST_LOAD_BIAS + plt_relas[0].r_offset, + &plt_slot, sizeof(plt_slot)); + add_region(&memory, TEST_LOAD_BIAS + relas[0].r_offset, + &rela_slot, sizeof(rela_slot)); + + request = make_request(&view, &ops, &source, candidates, + TEST_ARRAY_SIZE(candidates)); + check_int("happy.collect", + kzt_runtime_got_plt_candidates_collect(&request, &result), 0); + check_int("happy.status", result.status, + KZT_RUNTIME_GOT_PLT_CANDIDATE_OK); + check_int("happy.reason", result.reason, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_NONE); + check_int("happy.patch-present", result.patch_reason_present, 0); + check_size("happy.count", result.candidate_count, 2); + + check_int("happy.plt.table", candidates[0].table_kind, + KZT_PATCH_TABLE_PLT_RELA); + check_size("happy.plt.index", candidates[0].entry_index, 0); + check_ulong("happy.plt.entry", candidates[0].entry_addr, + TEST_PLT_RELA_ADDR); + check_int("happy.plt.reloc", candidates[0].reloc_type, + KZT_PATCH_RELOCATION_JUMP_SLOT); + check_ulong("happy.plt.slot", candidates[0].slot_addr, + TEST_LOAD_BIAS + plt_relas[0].r_offset); + check_int("happy.plt.current-present", + candidates[0].slot_current_value_present, 1); + check_ulong("happy.plt.current", candidates[0].slot_current_value, + plt_slot); + check_ulong("happy.plt.symbol", candidates[0].symbol_index, + TEST_JUMP_SLOT_SYMBOL); + check_str("happy.plt.symbol-name", candidates[0].symbol_name, + "puts"); + check_int("happy.plt.version-evidence", + candidates[0].version_evidence, + KZT_SYMBOL_VERSION_VERSIONED); + check_str("happy.plt.version", candidates[0].version, + "GLIBC_2.2.5"); + check_ulong("happy.plt.source", candidates[0].source.link_map_addr, + source.link_map_addr); + check_ulong("happy.plt.source-generation", + candidates[0].source.generation, source.generation); + check_ulong("happy.plt.dynamic", candidates[0].dynamic_addr, + view.dynamic_addr); + check_ulong("happy.plt.load-bias", candidates[0].load_bias, + view.load_bias); + check_ulong("happy.plt.generation", + candidates[0].dynamic_view_generation, 88); + check_int("happy.plt.available", + candidates[0].dynamic_view_available, 1); + + check_int("happy.rela.table", candidates[1].table_kind, + KZT_PATCH_TABLE_RELA); + check_size("happy.rela.index", candidates[1].entry_index, 0); + check_ulong("happy.rela.entry", candidates[1].entry_addr, + TEST_RELA_ADDR); + check_int("happy.rela.reloc", candidates[1].reloc_type, + KZT_PATCH_RELOCATION_GLOB_DAT); + check_ulong("happy.rela.slot", candidates[1].slot_addr, + TEST_LOAD_BIAS + relas[0].r_offset); + check_ulong("happy.rela.current", candidates[1].slot_current_value, + rela_slot); + check_ulong("happy.rela.symbol", candidates[1].symbol_index, + TEST_GLOB_DAT_SYMBOL); + check_str("happy.rela.symbol-name", candidates[1].symbol_name, + "errno"); + check_str("happy.rela.version", candidates[1].version, + "LIBFOO_1.0"); + check_str("happy.status-name", + kzt_runtime_got_plt_candidate_status_name(result.status), + "OK"); + check_str("happy.reason-name", + kzt_runtime_got_plt_candidate_reason_name(result.reason), + "NONE"); + + /* The production shadow route audits one selected relocation with bounded + * stack storage, even when the object has further candidate tables. */ + { + kzt_patch_candidate_t one_candidate[1]; + + request = make_request(&view, &ops, &source, one_candidate, 1); + request.only_entry = 1; + request.only_table_kind = KZT_PATCH_TABLE_PLT_RELA; + request.only_entry_index = 0; + check_int("single.collect", + kzt_runtime_got_plt_candidates_collect(&request, &result), + 0); + check_int("single.status", result.status, + KZT_RUNTIME_GOT_PLT_CANDIDATE_OK); + check_size("single.count", result.candidate_count, 1); + check_int("single.table", one_candidate[0].table_kind, + KZT_PATCH_TABLE_PLT_RELA); + check_size("single.index", one_candidate[0].entry_index, 0); + } +} + +static void test_single_entry_reads_only_selected_relocation(void) +{ + Elf64_Rela plt_relas[3] = { + { .r_offset = 0x3010, + .r_info = TEST_R_INFO(TEST_JUMP_SLOT_SYMBOL, + R_X86_64_JUMP_SLOT) }, + { .r_offset = 0x3020, + .r_info = TEST_R_INFO(TEST_JUMP_SLOT_SYMBOL, + R_X86_64_JUMP_SLOT) }, + { .r_offset = 0x3030, + .r_info = TEST_R_INFO(TEST_JUMP_SLOT_SYMBOL, + R_X86_64_JUMP_SLOT) }, + }; + uint64_t selected_slot = 0x7100003000ULL; + fake_memory_t memory = { 0 }; + symbol_version_fixture_t symbols; + kzt_guest_link_map_reader_ops_t ops = fake_ops(&memory); + kzt_patch_object_ref_t source = source_ref(); + kzt_guest_dynamic_view_t view = base_view(); + kzt_patch_candidate_t candidate; + kzt_runtime_got_plt_candidate_request_t request; + kzt_runtime_got_plt_candidate_result_t result; + + view.jmprel = runtime_field(TEST_PLT_RELA_ADDR); + view.pltrelsz = scalar_field(sizeof(plt_relas)); + view.pltrel = scalar_field(DT_RELA); + add_symbol_version_fixture(&memory, &view, &symbols); + add_region(&memory, TEST_PLT_RELA_ADDR, plt_relas, sizeof(plt_relas)); + add_region(&memory, TEST_LOAD_BIAS + plt_relas[2].r_offset, + &selected_slot, sizeof(selected_slot)); + memory.fail_addr = TEST_PLT_RELA_ADDR; + + request = make_request(&view, &ops, &source, &candidate, 1); + request.only_entry = 1; + request.only_table_kind = KZT_PATCH_TABLE_PLT_RELA; + request.only_entry_index = 2; + check_int("single-direct.collect", + kzt_runtime_got_plt_candidates_collect(&request, &result), 0); + check_int("single-direct.status", result.status, + KZT_RUNTIME_GOT_PLT_CANDIDATE_OK); + check_size("single-direct.count", result.candidate_count, 1); + check_size("single-direct.index", candidate.entry_index, 2); + check_ulong("single-direct.entry", candidate.entry_addr, + TEST_PLT_RELA_ADDR + 2 * sizeof(Elf64_Rela)); +} + +static void test_missing_dynamic_field_fails_open(void) +{ + fake_memory_t memory = { 0 }; + kzt_guest_link_map_reader_ops_t ops = fake_ops(&memory); + kzt_guest_dynamic_view_t view = base_view(); + kzt_patch_candidate_t candidates[2]; + kzt_runtime_got_plt_candidate_request_t request = + make_request(&view, &ops, NULL, candidates, TEST_ARRAY_SIZE(candidates)); + kzt_runtime_got_plt_candidate_result_t result; + + view.jmprel = runtime_field(TEST_PLT_RELA_ADDR); + view.pltrel = scalar_field(DT_RELA); + + check_int("missing.collect", + kzt_runtime_got_plt_candidates_collect(&request, &result), 0); + check_fail_open( + "missing", &result, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_MISSING_DYNAMIC_FIELD, + KZT_PATCH_REASON_INPUT_MALFORMED_TABLE); + check_int("missing.table", result.table_kind, + KZT_PATCH_TABLE_PLT_RELA); +} + +static void test_confirmed_unversioned_evidence_is_collected(void) +{ + Elf64_Rela plt_relas[] = { + { + .r_offset = 0x3010, + .r_info = TEST_R_INFO(TEST_JUMP_SLOT_SYMBOL, + R_X86_64_JUMP_SLOT), + .r_addend = 0, + }, + }; + uint64_t plt_slot = 0x7100001000ULL; + fake_memory_t memory = { 0 }; + symbol_version_fixture_t symbols; + kzt_guest_link_map_reader_ops_t ops = fake_ops(&memory); + kzt_guest_dynamic_view_t view = base_view(); + kzt_patch_candidate_t candidates[2]; + kzt_runtime_got_plt_candidate_request_t request = + make_request(&view, &ops, NULL, candidates, TEST_ARRAY_SIZE(candidates)); + kzt_runtime_got_plt_candidate_result_t result; + + view.jmprel = runtime_field(TEST_PLT_RELA_ADDR); + view.pltrelsz = scalar_field(sizeof(plt_relas)); + view.pltrel = scalar_field(DT_RELA); + add_symbol_version_fixture(&memory, &view, &symbols); + add_region(&memory, TEST_PLT_RELA_ADDR, plt_relas, sizeof(plt_relas)); + add_region(&memory, TEST_LOAD_BIAS + plt_relas[0].r_offset, + &plt_slot, sizeof(plt_slot)); + + view.versym.present = 0; + check_int("no-versym.collect", + kzt_runtime_got_plt_candidates_collect(&request, &result), 0); + check_int("no-versym.status", result.status, + KZT_RUNTIME_GOT_PLT_CANDIDATE_OK); + check_size("no-versym.count", result.candidate_count, 1); + check_int("no-versym.evidence", candidates[0].version_evidence, + KZT_SYMBOL_VERSION_CONFIRMED_UNVERSIONED); + check_int("no-versym.result-evidence", result.version_evidence, + KZT_SYMBOL_VERSION_CONFIRMED_UNVERSIONED); + check_str("no-versym.version", candidates[0].version, NULL); + + view.versym.present = 1; + symbols.versym[TEST_JUMP_SLOT_SYMBOL] = 0; + check_int("versym-zero.collect", + kzt_runtime_got_plt_candidates_collect(&request, &result), 0); + check_int("versym-zero.evidence", candidates[0].version_evidence, + KZT_SYMBOL_VERSION_CONFIRMED_UNVERSIONED); + check_str("versym-zero.version", candidates[0].version, NULL); + + symbols.versym[TEST_JUMP_SLOT_SYMBOL] = 1; + check_int("versym-one.collect", + kzt_runtime_got_plt_candidates_collect(&request, &result), 0); + check_int("versym-one.evidence", candidates[0].version_evidence, + KZT_SYMBOL_VERSION_CONFIRMED_UNVERSIONED); + check_str("versym-one.version", candidates[0].version, NULL); +} + +static void test_version_read_failure_is_error_and_fails_open(void) +{ + Elf64_Rela plt_rela = { + .r_offset = 0x3010, + .r_info = TEST_R_INFO(TEST_JUMP_SLOT_SYMBOL, R_X86_64_JUMP_SLOT), + }; + uint64_t plt_slot = 0x7100001000ULL; + fake_memory_t memory = { 0 }; + symbol_version_fixture_t symbols; + kzt_guest_link_map_reader_ops_t ops = fake_ops(&memory); + kzt_guest_dynamic_view_t view = base_view(); + kzt_patch_candidate_t candidate; + kzt_runtime_got_plt_candidate_request_t request; + kzt_runtime_got_plt_candidate_result_t result; + + view.jmprel = runtime_field(TEST_PLT_RELA_ADDR); + view.pltrelsz = scalar_field(sizeof(plt_rela)); + view.pltrel = scalar_field(DT_RELA); + add_symbol_version_fixture(&memory, &view, &symbols); + add_region(&memory, TEST_PLT_RELA_ADDR, &plt_rela, sizeof(plt_rela)); + add_region(&memory, TEST_LOAD_BIAS + plt_rela.r_offset, + &plt_slot, sizeof(plt_slot)); + memory.fail_addr = TEST_VERSYM_ADDR + + TEST_JUMP_SLOT_SYMBOL * sizeof(Elf64_Half); + request = make_request(&view, &ops, NULL, &candidate, 1); + + check_int("version-read.collect", + kzt_runtime_got_plt_candidates_collect(&request, &result), 0); + check_fail_open( + "version-read", &result, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_VERSION_READ_FAILED, + KZT_PATCH_REASON_INPUT_MALFORMED_SYMBOL_VERSION); + check_int("version-read.evidence", result.version_evidence, + KZT_SYMBOL_VERSION_ERROR); +} + +static void test_missing_version_definition_is_error_and_fails_open(void) +{ + Elf64_Rela plt_rela = { + .r_offset = 0x3010, + .r_info = TEST_R_INFO(TEST_JUMP_SLOT_SYMBOL, R_X86_64_JUMP_SLOT), + }; + uint64_t plt_slot = 0x7100001000ULL; + fake_memory_t memory = { 0 }; + symbol_version_fixture_t symbols; + kzt_guest_link_map_reader_ops_t ops = fake_ops(&memory); + kzt_guest_dynamic_view_t view = base_view(); + kzt_patch_candidate_t candidate; + kzt_runtime_got_plt_candidate_request_t request; + kzt_runtime_got_plt_candidate_result_t result; + + view.jmprel = runtime_field(TEST_PLT_RELA_ADDR); + view.pltrelsz = scalar_field(sizeof(plt_rela)); + view.pltrel = scalar_field(DT_RELA); + add_symbol_version_fixture(&memory, &view, &symbols); + view.verneed.present = 0; + view.verneednum.present = 0; + add_region(&memory, TEST_PLT_RELA_ADDR, &plt_rela, sizeof(plt_rela)); + add_region(&memory, TEST_LOAD_BIAS + plt_rela.r_offset, + &plt_slot, sizeof(plt_slot)); + request = make_request(&view, &ops, NULL, &candidate, 1); + + check_int("missing-version-definition.collect", + kzt_runtime_got_plt_candidates_collect(&request, &result), 0); + check_fail_open( + "missing-version-definition", &result, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_MALFORMED_SYMBOL_VERSION, + KZT_PATCH_REASON_INPUT_MALFORMED_SYMBOL_VERSION); + check_int("missing-version-definition.evidence", + result.version_evidence, KZT_SYMBOL_VERSION_ERROR); +} + +static void test_dt_rel_is_unsupported_and_fails_open(void) +{ + fake_memory_t memory = { 0 }; + kzt_guest_link_map_reader_ops_t ops = fake_ops(&memory); + kzt_guest_dynamic_view_t view = base_view(); + kzt_patch_candidate_t candidates[2]; + kzt_runtime_got_plt_candidate_request_t request = + make_request(&view, &ops, NULL, candidates, TEST_ARRAY_SIZE(candidates)); + kzt_runtime_got_plt_candidate_result_t result; + + view.rel = runtime_field(0x7000300000ULL); + view.relsz = scalar_field(sizeof(Elf64_Rel)); + view.relent = scalar_field(sizeof(Elf64_Rel)); + + check_int("dt-rel.collect", + kzt_runtime_got_plt_candidates_collect(&request, &result), 0); + check_fail_open( + "dt-rel", &result, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_DT_REL_UNSUPPORTED, + KZT_PATCH_REASON_INPUT_UNSUPPORTED_RELOCATION); + check_int("dt-rel.table", result.table_kind, KZT_PATCH_TABLE_REL); + check_str("dt-rel.reason-name", + kzt_runtime_got_plt_candidate_reason_name(result.reason), + "DT_REL_UNSUPPORTED"); +} + +static void test_non_divisible_size_fails_open(void) +{ + fake_memory_t memory = { 0 }; + kzt_guest_link_map_reader_ops_t ops = fake_ops(&memory); + kzt_guest_dynamic_view_t view = base_view(); + kzt_patch_candidate_t candidates[2]; + kzt_runtime_got_plt_candidate_request_t request = + make_request(&view, &ops, NULL, candidates, TEST_ARRAY_SIZE(candidates)); + kzt_runtime_got_plt_candidate_result_t result; + + view.rela = runtime_field(TEST_RELA_ADDR); + view.relasz = scalar_field(sizeof(Elf64_Rela) + 1); + view.relaent = scalar_field(sizeof(Elf64_Rela)); + + check_int("non-divisible.collect", + kzt_runtime_got_plt_candidates_collect(&request, &result), 0); + check_fail_open( + "non-divisible", &result, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_MALFORMED_TABLE, + KZT_PATCH_REASON_INPUT_MALFORMED_TABLE); + check_int("non-divisible.table", result.table_kind, + KZT_PATCH_TABLE_RELA); +} + +static void test_relocation_reader_failure_fails_open(void) +{ + Elf64_Rela plt_relas[] = { + { + .r_offset = 0x3010, + .r_info = TEST_R_INFO(TEST_JUMP_SLOT_SYMBOL, + R_X86_64_JUMP_SLOT), + .r_addend = 0, + }, + }; + fake_memory_t memory = { 0 }; + kzt_guest_link_map_reader_ops_t ops = fake_ops(&memory); + kzt_guest_dynamic_view_t view = base_view(); + kzt_patch_candidate_t candidates[2]; + kzt_runtime_got_plt_candidate_request_t request = + make_request(&view, &ops, NULL, candidates, TEST_ARRAY_SIZE(candidates)); + kzt_runtime_got_plt_candidate_result_t result; + + view.jmprel = runtime_field(TEST_PLT_RELA_ADDR); + view.pltrelsz = scalar_field(sizeof(plt_relas)); + view.pltrel = scalar_field(DT_RELA); + memory.fail_addr = TEST_PLT_RELA_ADDR; + add_region(&memory, TEST_PLT_RELA_ADDR, plt_relas, sizeof(plt_relas)); + + check_int("reader-fail.collect", + kzt_runtime_got_plt_candidates_collect(&request, &result), 0); + check_fail_open( + "reader-fail", &result, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_RELOCATION_READ_FAILED, + KZT_PATCH_REASON_INPUT_MALFORMED_TABLE); + check_ulong("reader-fail.error-addr", result.read_error_addr, + TEST_PLT_RELA_ADDR); + check_str("reader-fail.reason-name", + kzt_runtime_got_plt_candidate_reason_name(result.reason), + "RELOCATION_READ_FAILED"); +} + +static void test_slot_reader_failure_fails_open(void) +{ + Elf64_Rela plt_relas[] = { + { + .r_offset = 0x3010, + .r_info = TEST_R_INFO(TEST_JUMP_SLOT_SYMBOL, + R_X86_64_JUMP_SLOT), + .r_addend = 0, + }, + }; + fake_memory_t memory = { 0 }; + kzt_guest_link_map_reader_ops_t ops = fake_ops(&memory); + kzt_guest_dynamic_view_t view = base_view(); + kzt_patch_candidate_t candidates[2]; + kzt_runtime_got_plt_candidate_request_t request = + make_request(&view, &ops, NULL, candidates, TEST_ARRAY_SIZE(candidates)); + kzt_runtime_got_plt_candidate_result_t result; + + view.jmprel = runtime_field(TEST_PLT_RELA_ADDR); + view.pltrelsz = scalar_field(sizeof(plt_relas)); + view.pltrel = scalar_field(DT_RELA); + add_region(&memory, TEST_PLT_RELA_ADDR, plt_relas, sizeof(plt_relas)); + + check_int("slot-read-fail.collect", + kzt_runtime_got_plt_candidates_collect(&request, &result), 0); + check_fail_open( + "slot-read-fail", &result, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_SLOT_READ_FAILED, + KZT_PATCH_REASON_INPUT_UNAVAILABLE_CURRENT_GOT); + check_ulong("slot-read-fail.slot", result.slot_addr, + TEST_LOAD_BIAS + plt_relas[0].r_offset); +} + +static void test_slot_overflow_fails_open(void) +{ + Elf64_Rela plt_relas[] = { + { + .r_offset = 8, + .r_info = TEST_R_INFO(TEST_JUMP_SLOT_SYMBOL, + R_X86_64_JUMP_SLOT), + .r_addend = 0, + }, + }; + fake_memory_t memory = { 0 }; + kzt_guest_link_map_reader_ops_t ops = fake_ops(&memory); + kzt_guest_dynamic_view_t view = base_view(); + kzt_patch_candidate_t candidates[2]; + kzt_runtime_got_plt_candidate_request_t request = + make_request(&view, &ops, NULL, candidates, TEST_ARRAY_SIZE(candidates)); + kzt_runtime_got_plt_candidate_result_t result; + + view.load_bias = UINTPTR_MAX - 7; + view.jmprel = runtime_field(TEST_PLT_RELA_ADDR); + view.pltrelsz = scalar_field(sizeof(plt_relas)); + view.pltrel = scalar_field(DT_RELA); + add_region(&memory, TEST_PLT_RELA_ADDR, plt_relas, sizeof(plt_relas)); + + check_int("slot-overflow.collect", + kzt_runtime_got_plt_candidates_collect(&request, &result), 0); + check_fail_open( + "slot-overflow", &result, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_SLOT_OVERFLOW, + KZT_PATCH_REASON_INPUT_MALFORMED_SLOT); + check_int("slot-overflow.table", result.table_kind, + KZT_PATCH_TABLE_PLT_RELA); +} + +static void test_non_target_relocations_are_skipped(void) +{ + Elf64_Rela plt_relas[] = { + { + .r_offset = 0x3010, + .r_info = TEST_R_INFO(TEST_JUMP_SLOT_SYMBOL, + R_X86_64_GLOB_DAT), + .r_addend = 0, + }, + }; + Elf64_Rela relas[] = { + { + .r_offset = 0x4020, + .r_info = TEST_R_INFO(TEST_GLOB_DAT_SYMBOL, + R_X86_64_RELATIVE), + .r_addend = 0, + }, + }; + fake_memory_t memory = { 0 }; + kzt_guest_link_map_reader_ops_t ops = fake_ops(&memory); + kzt_guest_dynamic_view_t view = base_view(); + kzt_patch_candidate_t candidates[2]; + kzt_runtime_got_plt_candidate_request_t request; + kzt_runtime_got_plt_candidate_result_t result; + + view.jmprel = runtime_field(TEST_PLT_RELA_ADDR); + view.pltrelsz = scalar_field(sizeof(plt_relas)); + view.pltrel = scalar_field(DT_RELA); + view.rela = runtime_field(TEST_RELA_ADDR); + view.relasz = scalar_field(sizeof(relas)); + view.relaent = scalar_field(sizeof(Elf64_Rela)); + add_region(&memory, TEST_PLT_RELA_ADDR, plt_relas, sizeof(plt_relas)); + add_region(&memory, TEST_RELA_ADDR, relas, sizeof(relas)); + + request = make_request(&view, &ops, NULL, candidates, + TEST_ARRAY_SIZE(candidates)); + check_int("skip.collect", + kzt_runtime_got_plt_candidates_collect(&request, &result), 0); + check_int("skip.status", result.status, + KZT_RUNTIME_GOT_PLT_CANDIDATE_OK); + check_size("skip.count", result.candidate_count, 0); +} + +static void test_capacity_exceeded_fails_open(void) +{ + Elf64_Rela plt_relas[] = { + { + .r_offset = 0x3010, + .r_info = TEST_R_INFO(TEST_JUMP_SLOT_SYMBOL, + R_X86_64_JUMP_SLOT), + .r_addend = 0, + }, + }; + Elf64_Rela relas[] = { + { + .r_offset = 0x4020, + .r_info = TEST_R_INFO(TEST_GLOB_DAT_SYMBOL, + R_X86_64_GLOB_DAT), + .r_addend = 0, + }, + }; + uint64_t plt_slot = 0x7100001000ULL; + uint64_t rela_slot = 0x7200002000ULL; + fake_memory_t memory = { 0 }; + symbol_version_fixture_t symbols; + kzt_guest_link_map_reader_ops_t ops = fake_ops(&memory); + kzt_guest_dynamic_view_t view = base_view(); + kzt_patch_candidate_t candidates[1]; + kzt_runtime_got_plt_candidate_request_t request; + kzt_runtime_got_plt_candidate_result_t result; + + view.jmprel = runtime_field(TEST_PLT_RELA_ADDR); + view.pltrelsz = scalar_field(sizeof(plt_relas)); + view.pltrel = scalar_field(DT_RELA); + view.rela = runtime_field(TEST_RELA_ADDR); + view.relasz = scalar_field(sizeof(relas)); + view.relaent = scalar_field(sizeof(Elf64_Rela)); + add_symbol_version_fixture(&memory, &view, &symbols); + add_region(&memory, TEST_PLT_RELA_ADDR, plt_relas, sizeof(plt_relas)); + add_region(&memory, TEST_RELA_ADDR, relas, sizeof(relas)); + add_region(&memory, TEST_LOAD_BIAS + plt_relas[0].r_offset, + &plt_slot, sizeof(plt_slot)); + add_region(&memory, TEST_LOAD_BIAS + relas[0].r_offset, + &rela_slot, sizeof(rela_slot)); + + request = make_request(&view, &ops, NULL, candidates, + TEST_ARRAY_SIZE(candidates)); + check_int("capacity.collect", + kzt_runtime_got_plt_candidates_collect(&request, &result), 0); + check_fail_open( + "capacity", &result, + KZT_RUNTIME_GOT_PLT_CANDIDATE_REASON_CAPACITY_EXCEEDED, + KZT_PATCH_REASON_INPUT_MALFORMED_TABLE); + check_int("capacity.table", result.table_kind, KZT_PATCH_TABLE_RELA); + check_size("capacity.index", result.entry_index, 0); +} + +static int test_matches_filter(const char *name, int argc, char **argv) +{ + int i; + + for (i = 1; i < argc; ++i) { + if (!strcmp(argv[i], "--filter") && i + 1 < argc) { + return strcmp(name, argv[i + 1]) == 0; + } + } + + return 1; +} + +int main(int argc, char **argv) +{ + if (test_matches_filter("happy_path_enumerates_jump_slot_and_glob_dat", + argc, argv)) { + test_happy_path_enumerates_jump_slot_and_glob_dat(); + } + if (test_matches_filter("single_entry_reads_only_selected_relocation", + argc, argv)) { + test_single_entry_reads_only_selected_relocation(); + } + if (test_matches_filter("missing_dynamic_field_fails_open", + argc, argv)) { + test_missing_dynamic_field_fails_open(); + } + if (test_matches_filter("confirmed_unversioned_evidence_is_collected", + argc, argv)) { + test_confirmed_unversioned_evidence_is_collected(); + } + if (test_matches_filter("version_read_failure_is_error_and_fails_open", + argc, argv)) { + test_version_read_failure_is_error_and_fails_open(); + } + if (test_matches_filter( + "missing_version_definition_is_error_and_fails_open", + argc, argv)) { + test_missing_version_definition_is_error_and_fails_open(); + } + if (test_matches_filter("dt_rel_is_unsupported_and_fails_open", + argc, argv)) { + test_dt_rel_is_unsupported_and_fails_open(); + } + if (test_matches_filter("non_divisible_size_fails_open", argc, argv)) { + test_non_divisible_size_fails_open(); + } + if (test_matches_filter("relocation_reader_failure_fails_open", + argc, argv)) { + test_relocation_reader_failure_fails_open(); + } + if (test_matches_filter("slot_reader_failure_fails_open", argc, argv)) { + test_slot_reader_failure_fails_open(); + } + if (test_matches_filter("slot_overflow_fails_open", argc, argv)) { + test_slot_overflow_fails_open(); + } + if (test_matches_filter("non_target_relocations_are_skipped", + argc, argv)) { + test_non_target_relocations_are_skipped(); + } + if (test_matches_filter("capacity_exceeded_fails_open", argc, argv)) { + test_capacity_exceeded_fails_open(); + } + + if (failures) { + fprintf(stderr, + "kzt-runtime-got-plt-candidate: %d failure(s)\n", + failures); + return 1; + } + + puts("kzt-runtime-got-plt-candidate: selected tests passed"); + return 0; +} diff --git a/tests/unit/kzt/test_wi236_enrichment_matrix.c b/tests/unit/kzt/test_wi236_enrichment_matrix.c new file mode 100644 index 00000000000..e5ef7e5d679 --- /dev/null +++ b/tests/unit/kzt/test_wi236_enrichment_matrix.c @@ -0,0 +1,325 @@ +#include +#include + +#include "elf.h" +#include "target/i386/latx/include/kzt_guest_registry.h" +#include "target/i386/latx/include/kzt_owner_resolver.h" +#include "target/i386/latx/include/kzt_patch_planner.h" +#include "target/i386/latx/include/kzt_rela_immediate_candidate.h" + +static int failures; + +typedef struct wi236_contract_input { + kzt_guest_registry_t *registry; + uintptr_t slot_current_value; + uintptr_t expected_guest_target; + uintptr_t native_bridge_target; + kzt_patch_wrapper_match_t wrapper_match; +} wi236_contract_input_t; + +typedef struct wi236_contract_result { + kzt_rela_immediate_candidate_request_t request; + kzt_owner_resolution_t owner_resolution; + kzt_rela_immediate_candidate_result_t plan; +} wi236_contract_result_t; + +static void check_int(const char *name, int got, int expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got %d expected %d\n", name, got, expected); + ++failures; +} + +static void check_ulong(const char *name, unsigned long got, + unsigned long expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got 0x%lx expected 0x%lx\n", name, got, expected); + ++failures; +} + +static kzt_guest_object_observation_t observation( + uintptr_t link_map_addr, + uintptr_t map_start, + uintptr_t map_end, + const char *soname) +{ + return (kzt_guest_object_observation_t) { + .link_map_addr = link_map_addr, + .load_bias = { map_start, KZT_GUEST_FIELD_OK }, + .dynamic_addr = { map_start + 0x1000, KZT_GUEST_FIELD_OK }, + .map_start = { map_start, KZT_GUEST_FIELD_OK }, + .map_end = { map_end, KZT_GUEST_FIELD_OK }, + .namespace_id = { 0, KZT_GUEST_FIELD_OK }, + .path = { soname, KZT_GUEST_FIELD_OK }, + .soname = { soname, KZT_GUEST_FIELD_OK }, + .dynamic_view_status = KZT_GUEST_FIELD_NOT_PARSED, + }; +} + +static kzt_patch_object_ref_t source_ref(void) +{ + return (kzt_patch_object_ref_t) { + .known = 1, + .link_map_addr = 0x1110, + .map_start = 0x69000000, + .map_end = 0x69010000, + .generation = 1, + .soname = "librequester.so", + .path = "/guest/librequester.so", + }; +} + +static void request_base(kzt_rela_immediate_candidate_request_t *request) +{ + memset(request, 0, sizeof(*request)); + request->relocation_type = R_X86_64_JUMP_SLOT; + request->table_kind = KZT_PATCH_TABLE_PLT_RELA; + request->entry_index = 7; + request->entry_addr = 0x69002000; + request->source = source_ref(); + request->dynamic_addr = 0x69001000; + request->load_bias = 0x69000000; + request->dynamic_view_generation = 3; + request->dynamic_view_available = 1; + request->slot_addr = 0x69003000; + request->slot_current_value_present = 1; + request->symbol_index = 44; + request->symbol_name = "gtk_widget_show"; + request->version = "GTK_3.0"; + request->wrapper_name = "wrappedgtk3"; + request->wrapper_symbol_version = "GTK_3.0"; +} + +static void apply_wi236_contract(const wi236_contract_input_t *input, + wi236_contract_result_t *result) +{ + memset(result, 0, sizeof(*result)); + request_base(&result->request); + + result->request.slot_current_value = input->slot_current_value; + result->request.expected_guest_target = input->expected_guest_target; + result->request.native_bridge_target = input->native_bridge_target; + result->request.legacy_target = input->expected_guest_target; + result->request.wrapper_match = input->wrapper_match; + + kzt_owner_resolver_init(&result->owner_resolution); + check_int("contract.owner.resolve", + kzt_owner_resolver_resolve_current( + input->registry, + input->slot_current_value, + input->expected_guest_target, + &result->owner_resolution), + 0); + result->request.current_owner = result->owner_resolution.current_owner; + result->request.owner_match = result->owner_resolution.owner_match; + + check_int("contract.plan", + kzt_rela_immediate_jump_slot_plan(&result->request, + &result->plan), + 0); +} + +static kzt_guest_registry_t *registry_with_two_objects(void) +{ + kzt_guest_registry_t *registry = kzt_guest_registry_init(); + kzt_guest_object_observation_t lib_a = + observation(0x2000, 0x72000000, 0x72001000, "libcollision.so"); + kzt_guest_object_observation_t lib_b = + observation(0x3000, 0x76000000, 0x76001000, "libgtk-3.so"); + + check_int("registry.observe.a", + kzt_guest_registry_observe(registry, &lib_a), + KZT_GUEST_REGISTRY_ADDED); + check_int("registry.observe.b", + kzt_guest_registry_observe(registry, &lib_b), + KZT_GUEST_REGISTRY_ADDED); + return registry; +} + +static void test_bridge_address_is_not_expected_guest_target(void) +{ + kzt_guest_registry_t *registry = registry_with_two_objects(); + wi236_contract_result_t result; + kzt_owner_resolution_t unsafe_resolution; + wi236_contract_input_t input = { + .registry = registry, + .slot_current_value = 0x72000040, + .expected_guest_target = 0x76000080, + .native_bridge_target = 0x72000088, + .wrapper_match = KZT_PATCH_WRAPPER_VERSION_MATCH, + }; + + apply_wi236_contract(&input, &result); + check_int("bridge-not-expected.owner", + result.request.owner_match, KZT_PATCH_OWNER_MISMATCH); + check_int("bridge-not-expected.decision", + result.plan.decision.kind, KZT_PATCH_DECISION_REJECTED); + check_int("bridge-not-expected.reason", + result.plan.decision.reason, + KZT_PATCH_REASON_POLICY_OWNER_MISMATCH); + + check_int("unsafe.resolve", + kzt_owner_resolver_resolve_current( + registry, input.slot_current_value, + input.native_bridge_target, &unsafe_resolution), + 0); + check_int("unsafe.bridge-as-expected-would-match", + unsafe_resolution.owner_match, KZT_PATCH_OWNER_MATCH); + + kzt_guest_registry_destroy(®istry); +} + +static void test_only_full_evidence_can_be_approved(void) +{ + kzt_guest_registry_t *registry = registry_with_two_objects(); + wi236_contract_result_t result; + wi236_contract_input_t input = { + .registry = registry, + .slot_current_value = 0x76000040, + .expected_guest_target = 0x76000080, + .native_bridge_target = 0x73000080, + .wrapper_match = KZT_PATCH_WRAPPER_VERSION_MATCH, + }; + + apply_wi236_contract(&input, &result); + check_int("approved.owner", + result.request.owner_match, KZT_PATCH_OWNER_MATCH); + check_int("approved.wrapper", + result.request.wrapper_match, + KZT_PATCH_WRAPPER_VERSION_MATCH); + check_ulong("approved.candidate.bridge", + result.plan.candidate.bridge_target, 0x73000080); + check_ulong("approved.decision.bridge", + result.plan.decision.bridge_target, 0x73000080); + check_int("approved.decision", + result.plan.decision.kind, KZT_PATCH_DECISION_APPROVED); + check_int("approved.reason", + result.plan.decision.reason, + KZT_PATCH_REASON_APPROVED_NATIVE_BRIDGE); + + kzt_guest_registry_destroy(®istry); +} + +static void test_owner_unknown_and_mismatch_fail_open_or_reject(void) +{ + kzt_guest_registry_t *registry = registry_with_two_objects(); + wi236_contract_result_t result; + wi236_contract_input_t input = { + .registry = NULL, + .slot_current_value = 0x76000040, + .expected_guest_target = 0x76000080, + .native_bridge_target = 0x73000080, + .wrapper_match = KZT_PATCH_WRAPPER_VERSION_MATCH, + }; + + apply_wi236_contract(&input, &result); + check_int("owner-unknown.decision", + result.plan.decision.kind, KZT_PATCH_DECISION_UNSUPPORTED); + check_int("owner-unknown.reason", + result.plan.decision.reason, + KZT_PATCH_REASON_INPUT_UNAVAILABLE_OWNER); + + input.registry = registry; + input.slot_current_value = 0x72000040; + input.expected_guest_target = 0x76000080; + apply_wi236_contract(&input, &result); + check_int("owner-mismatch.decision", + result.plan.decision.kind, KZT_PATCH_DECISION_REJECTED); + check_int("owner-mismatch.reason", + result.plan.decision.reason, + KZT_PATCH_REASON_POLICY_OWNER_MISMATCH); + + kzt_guest_registry_destroy(®istry); +} + +static void test_wrapper_and_bridge_fail_open_matrix(void) +{ + static const struct { + kzt_patch_wrapper_match_t wrapper_match; + uintptr_t bridge_target; + kzt_patch_decision_kind_t decision; + kzt_patch_reason_t reason; + const char *name; + } cases[] = { + { + KZT_PATCH_WRAPPER_NO_MANIFEST, + 0x73000080, + KZT_PATCH_DECISION_UNSUPPORTED, + KZT_PATCH_REASON_INPUT_UNAVAILABLE_WRAPPER_MANIFEST, + "no-manifest", + }, + { + KZT_PATCH_WRAPPER_NO_WRAPPER, + 0x73000080, + KZT_PATCH_DECISION_REJECTED, + KZT_PATCH_REASON_POLICY_NO_WRAPPER, + "no-wrapper", + }, + { + KZT_PATCH_WRAPPER_SYMBOL_ONLY, + 0x73000080, + KZT_PATCH_DECISION_REJECTED, + KZT_PATCH_REASON_POLICY_WRAPPER_SYMBOL_ONLY, + "symbol-only", + }, + { + KZT_PATCH_WRAPPER_VERSION_MISMATCH, + 0x73000080, + KZT_PATCH_DECISION_REJECTED, + KZT_PATCH_REASON_POLICY_VERSION_MISMATCH, + "version-mismatch", + }, + { + KZT_PATCH_WRAPPER_VERSION_MATCH, + 0, + KZT_PATCH_DECISION_UNSUPPORTED, + KZT_PATCH_REASON_INPUT_UNAVAILABLE_BRIDGE_TARGET, + "bridge-zero", + }, + }; + kzt_guest_registry_t *registry = registry_with_two_objects(); + size_t i; + + for (i = 0; i < sizeof(cases) / sizeof(cases[0]); ++i) { + wi236_contract_result_t result; + wi236_contract_input_t input = { + .registry = registry, + .slot_current_value = 0x76000040, + .expected_guest_target = 0x76000080, + .native_bridge_target = cases[i].bridge_target, + .wrapper_match = cases[i].wrapper_match, + }; + + apply_wi236_contract(&input, &result); + check_int(cases[i].name, + result.plan.decision.kind, cases[i].decision); + check_int(cases[i].name, + result.plan.decision.reason, cases[i].reason); + } + + kzt_guest_registry_destroy(®istry); +} + +int main(void) +{ + test_bridge_address_is_not_expected_guest_target(); + test_only_full_evidence_can_be_approved(); + test_owner_unknown_and_mismatch_fail_open_or_reject(); + test_wrapper_and_bridge_fail_open_matrix(); + + if (failures) { + fprintf(stderr, "kzt-wi236-enrichment-matrix: %d failure(s)\n", + failures); + return 1; + } + + puts("kzt-wi236-enrichment-matrix: ok"); + return 0; +} diff --git a/tests/unit/kzt/test_wi237_jump_slot_route_contract.py b/tests/unit/kzt/test_wi237_jump_slot_route_contract.py new file mode 100644 index 00000000000..f3f62dc1ce5 --- /dev/null +++ b/tests/unit/kzt/test_wi237_jump_slot_route_contract.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +import pathlib +import re +import sys + + +root = pathlib.Path(sys.argv[1]) +elfloader = (root / "target/i386/latx/context/elfloader.c").read_text() +route = (root / "target/i386/latx/context/kzt_jump_slot_route.c").read_text() +production = (root / "target/i386/latx/context/kzt_jump_slot_production.c").read_text() + + +def body(text, signature): + start = text.index(signature) + brace = text.index("{", start) + depth = 0 + for pos in range(brace, len(text)): + if text[pos] == "{": + depth += 1 + elif text[pos] == "}": + depth -= 1 + if depth == 0: + return text[brace + 1:pos] + raise AssertionError(signature) + + +eager = body(elfloader, "int RelocateElfRELA(") +lazy = body(elfloader, "void PltResolver(void)") +shared = "kzt_production_jump_slot_route(" + +assert shared in eager +assert shared in lazy +assert eager.count("uintptr_t slot_observation = (uintptr_t)(*p);") == 1 +assert re.search( + r"kzt_rela_slot_current_is_unresolved_stub\(\s*slot_observation\s*,", + eager, +) +assert "uintptr_t expected_guest_target = slot_observation;" in eager +assert "(void *)slot_observation, (void*)legacy_target" in eager +assert "p,\n slot_observation," in eager +assert "expected_guest_target = legacy_target" not in eager + +assert lazy.count("uintptr_t slot_observation = (uintptr_t)(*p);") == 1 +assert "(void *)slot_observation, (void*)legacy_target" in lazy +assert "rel, p,\n slot_observation, 1," in lazy + +# Runtime-disabled KZT retains the historical direct stores. Runtime-enabled +# eager and lazy branches do not add an unchecked store after entering Step5. +assert "if (option_kzt || wine_option_kzt)" in eager +assert "if (option_kzt || wine_option_kzt)" in lazy +assert "__atomic_compare_exchange_n" in production +assert "snapshot->namespace_id.status != KZT_GUEST_FIELD_OK" in production +assert "snapshot->namespace_id.value != 0" in production +assert "input.resolved_target_matches_legacy =\n resolved_target == legacy_target;" in production +assert "production_request_is_main_namespace" in production +assert "production_shadow_runtime_candidate" in production +assert "kzt_runtime_candidate_shadow_run" in production +assert ".only_entry = 1" in production +assert "production_emit_diagnostic(&state, route_result);" in production + +# Enrichment can leave request string pointers referring into its result text +# buffers. Both callback results therefore belong to the route-wide state; +# neither callback may create result storage on its own stack. +state = body(production, "typedef struct kzt_production_jump_slot_state") +enrich = body(production, "static int production_enrich(") +base_enrich = body(production, "static int production_enrich_base(") +bridge_enrich = body(production, "static int production_enrich_bridge(") +assert "kzt_rela_request_enricher_result_t base_enrich_result;" in state +assert "kzt_rela_request_enricher_result_t bridge_enrich_result;" in state +assert "kzt_rela_request_enricher_result_t" not in enrich +assert "&state->base_enrich_result" in base_enrich +assert "&state->bridge_enrich_result" in bridge_enrich + +# The shared helper retains the exact handle across bridge enrichment/writing, +# releases it before fallback, and never performs a direct slot store. +acquire = route.index("ops->acquire_exact_provider") +bridge = route.index("ops->enrich_bridge", acquire) +writer = route.index("ops->try_native_writer", bridge) +release = route.index("ops->release_exact_provider", writer) +fallback = route.index("route_legacy_fallback", release) +assert acquire < bridge < writer < release < fallback +assert "*(uintptr_t *)" not in route +assert "compare_exchange_slot" in route + +# Lazy currently has no raw expected guest target/owner proof and explicitly +# enters the route with expected_guest_target_present == 0. +assert "vername, 0, 0, legacy_target, &route_result" in lazy + +print("WI-237 eager/lazy shared jump-slot route contract: PASS") diff --git a/tests/unit/kzt/test_wi238_structured_diagnostics.c b/tests/unit/kzt/test_wi238_structured_diagnostics.c new file mode 100644 index 00000000000..e1e712dc0b1 --- /dev/null +++ b/tests/unit/kzt/test_wi238_structured_diagnostics.c @@ -0,0 +1,695 @@ +#include +#include +#include +#include + +#include "target/i386/latx/include/kzt_rela_diagnostics.h" + +static int failures; +static int tests_run; + +static void check_true(const char *name, int condition) +{ + if (condition) { + return; + } + + fprintf(stderr, "%s: condition failed\n", name); + ++failures; +} + +static void check_int(const char *name, int got, int expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got %d expected %d\n", name, got, expected); + ++failures; +} + +static void check_ulong(const char *name, unsigned long got, + unsigned long expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got %lu expected %lu\n", name, got, expected); + ++failures; +} + +static void check_string(const char *name, const char *got, + const char *expected) +{ + if (got && expected && strcmp(got, expected) == 0) { + return; + } + + fprintf(stderr, "%s: got \"%s\" expected \"%s\"\n", name, + got ? got : "(null)", expected ? expected : "(null)"); + ++failures; +} + +typedef struct capture_sink { + int calls; + int fail; + char line[KZT_RELA_DIAGNOSTIC_LINE_LIMIT]; +} capture_sink_t; + +#define THROTTLE_THREADS 12 +#define THROTTLE_ITERATIONS 200 +#define SNAPSHOT_READERS 4 +#define ADAPTER_THREADS 10 +#define ADAPTER_ITERATIONS 50 + +static int wait_for_barrier(pthread_barrier_t *barrier) +{ + int ret = pthread_barrier_wait(barrier); + + return ret == 0 || ret == PTHREAD_BARRIER_SERIAL_THREAD ? 0 : ret; +} + +static int capture_line(const char *line, size_t line_length, void *opaque) +{ + capture_sink_t *capture = opaque; + size_t copy_length; + + ++capture->calls; + copy_length = line_length; + if (copy_length >= sizeof(capture->line)) { + copy_length = sizeof(capture->line) - 1; + } + memcpy(capture->line, line, copy_length); + capture->line[copy_length] = '\0'; + return capture->fail ? -1 : 0; +} + +typedef struct throttle_worker { + kzt_rela_diagnostic_throttle_t *throttle; + pthread_barrier_t *barrier; + unsigned long admitted; + unsigned long suppressed; + unsigned long errors; +} throttle_worker_t; + +static void *throttle_worker_main(void *opaque) +{ + throttle_worker_t *worker = opaque; + int i; + + if (wait_for_barrier(worker->barrier) != 0) { + ++worker->errors; + return NULL; + } + + for (i = 0; i < THROTTLE_ITERATIONS; ++i) { + int result = + kzt_rela_diagnostic_throttle_try_admit(worker->throttle); + + if (result > 0) { + ++worker->admitted; + } else if (result == 0) { + ++worker->suppressed; + } else { + ++worker->errors; + } + } + return NULL; +} + +typedef struct snapshot_reader { + kzt_rela_diagnostic_throttle_t *throttle; + unsigned long capacity; + unsigned int *done; + unsigned long snapshots; + unsigned long errors; +} snapshot_reader_t; + +static void *snapshot_reader_main(void *opaque) +{ + snapshot_reader_t *reader = opaque; + + do { + kzt_rela_diagnostic_throttle_snapshot_t snapshot; + + if (kzt_rela_diagnostic_throttle_snapshot( + reader->throttle, &snapshot) != 0) { + ++reader->errors; + continue; + } + ++reader->snapshots; + if (snapshot.capacity != reader->capacity || + snapshot.admitted > snapshot.capacity || + (snapshot.admitted < snapshot.capacity && + snapshot.suppressed != 0)) { + ++reader->errors; + } + } while (__atomic_load_n(reader->done, __ATOMIC_ACQUIRE) == 0); + + return NULL; +} + +typedef struct atomic_sink { + unsigned long calls; +} atomic_sink_t; + +static int count_line_atomically(const char *line, size_t line_length, + void *opaque) +{ + atomic_sink_t *sink = opaque; + + (void)line; + (void)line_length; + __atomic_fetch_add(&sink->calls, 1, __ATOMIC_RELAXED); + return 0; +} + +typedef struct adapter_worker { + const kzt_rela_immediate_candidate_request_t *request; + const kzt_rela_immediate_writer_result_t *writer_result; + kzt_rela_diagnostic_throttle_t *throttle; + pthread_barrier_t *barrier; + atomic_sink_t *sink; + unsigned long emitted; + unsigned long suppressed; + unsigned long errors; +} adapter_worker_t; + +static void *adapter_worker_main(void *opaque) +{ + adapter_worker_t *worker = opaque; + int i; + + if (wait_for_barrier(worker->barrier) != 0) { + ++worker->errors; + return NULL; + } + + for (i = 0; i < ADAPTER_ITERATIONS; ++i) { + kzt_rela_immediate_diagnostic_result_t result; + char buffer[KZT_RELA_DIAGNOSTIC_LINE_LIMIT]; + kzt_rela_immediate_diagnostic_input_t input = { + .mode = KZT_RELA_DIAGNOSTIC_MODE_DIAGNOSTICS_WRITE_ENABLED, + .request = worker->request, + .result = worker->writer_result, + .legacy_fallback = 0, + .throttle = worker->throttle, + .buffer = buffer, + .buffer_size = sizeof(buffer), + .sink = count_line_atomically, + .sink_opaque = worker->sink, + }; + + if (kzt_rela_immediate_diagnostic_emit(&input, &result) != 0) { + ++worker->errors; + } else if (result.status == KZT_RELA_DIAGNOSTIC_EMIT_EMITTED) { + ++worker->emitted; + } else if (result.status == KZT_RELA_DIAGNOSTIC_EMIT_SUPPRESSED) { + ++worker->suppressed; + } else { + ++worker->errors; + } + } + return NULL; +} + +static kzt_rela_immediate_candidate_request_t base_request(const char *path) +{ + kzt_rela_immediate_candidate_request_t request; + + memset(&request, 0, sizeof(request)); + request.source.known = 1; + request.source.link_map_addr = 0x110000; + request.source.map_start = 0x400000; + request.source.map_end = 0x410000; + request.source.generation = 17; + request.source.soname = "libdiag.so"; + request.source.path = path; + request.current_owner.known = 1; + request.current_owner.link_map_addr = 0x220000; + request.current_owner.generation = 23; + request.owner_match = KZT_PATCH_OWNER_MATCH; + request.wrapper_match = KZT_PATCH_WRAPPER_VERSION_MATCH; + request.native_bridge_target = 0x660000; + request.slot_addr = 0x550000; + request.slot_current_value_present = 1; + request.slot_current_value = 0x440000; + request.symbol_name = "diag_symbol"; + request.version = "DIAG_1.0"; + return request; +} + +static kzt_rela_immediate_writer_result_t base_result( + const kzt_rela_immediate_candidate_request_t *request) +{ + kzt_rela_immediate_writer_result_t result; + kzt_patch_decision_t *decision; + + memset(&result, 0, sizeof(result)); + result.planner_called = 1; + result.writer_called = 1; + result.skip_legacy_write = 1; + result.plan.status = KZT_RELA_IMMEDIATE_CANDIDATE_PLANNED; + result.plan.reason = KZT_RELA_IMMEDIATE_CANDIDATE_REASON_NONE; + result.plan.candidate_present = 1; + result.plan.decision_present = 1; + + decision = &result.plan.decision; + decision->kind = KZT_PATCH_DECISION_APPROVED; + decision->reason = KZT_PATCH_REASON_APPROVED_NATIVE_BRIDGE; + decision->allow_native_bridge = 1; + decision->source = request->source; + decision->current_owner = request->current_owner; + decision->owner_match = request->owner_match; + decision->wrapper_match = request->wrapper_match; + decision->bridge_target = request->native_bridge_target; + decision->slot_addr = request->slot_addr; + decision->slot_current_value_present = 1; + decision->slot_current_value = request->slot_current_value; + decision->symbol_name = request->symbol_name; + decision->version = request->version; + + result.record.valid = 1; + result.record.decision_kind = decision->kind; + result.record.decision_reason = decision->reason; + result.record.result = KZT_PATCH_SPIKE_RESULT_APPLIED; + result.record.failure = KZT_PATCH_SPIKE_FAILURE_NONE; + result.record.action = KZT_PATCH_SPIKE_ACTION_USE_NATIVE_BRIDGE; + result.record.skip_legacy_write = 1; + result.record.writer_called = 1; + return result; +} + +static kzt_rela_immediate_diagnostic_input_t base_input( + kzt_rela_diagnostic_mode_t mode, + const kzt_rela_immediate_candidate_request_t *request, + const kzt_rela_immediate_writer_result_t *writer_result, + kzt_rela_diagnostic_throttle_t *throttle, + char *buffer, + size_t buffer_size, + capture_sink_t *capture) +{ + return (kzt_rela_immediate_diagnostic_input_t) { + .mode = mode, + .request = request, + .result = writer_result, + .legacy_fallback = 1, + .throttle = throttle, + .buffer = buffer, + .buffer_size = buffer_size, + .sink = capture_line, + .sink_opaque = capture, + }; +} + +static void test_modes_gate_output_and_writer_fields(void) +{ + kzt_rela_immediate_candidate_request_t request = + base_request("/guest/libdiag.so"); + kzt_rela_immediate_writer_result_t writer_result = + base_result(&request); + kzt_rela_diagnostic_throttle_t throttle; + kzt_rela_diagnostic_throttle_snapshot_t snapshot; + kzt_rela_immediate_diagnostic_result_t result; + capture_sink_t capture = { 0 }; + char buffer[KZT_RELA_DIAGNOSTIC_LINE_LIMIT]; + kzt_rela_immediate_diagnostic_input_t input; + + ++tests_run; + check_int("mode.default.flags", + kzt_rela_diagnostic_mode_from_flags(0, 0), + KZT_RELA_DIAGNOSTIC_MODE_DEFAULT); + check_int("mode.write-only.flags", + kzt_rela_diagnostic_mode_from_flags(0, 1), + KZT_RELA_DIAGNOSTIC_MODE_WRITE_ENABLED_ONLY); + check_int("mode.diagnostics.flags", + kzt_rela_diagnostic_mode_from_flags(1, 0), + KZT_RELA_DIAGNOSTIC_MODE_DIAGNOSTICS); + check_int("mode.combined.flags", + kzt_rela_diagnostic_mode_from_flags(1, 1), + KZT_RELA_DIAGNOSTIC_MODE_DIAGNOSTICS_WRITE_ENABLED); + check_int("mode.throttle.init", + kzt_rela_diagnostic_throttle_init(&throttle, 8), 0); + + input = base_input(KZT_RELA_DIAGNOSTIC_MODE_DEFAULT, &request, + &writer_result, &throttle, buffer, sizeof(buffer), + &capture); + check_int("mode.default.emit", + kzt_rela_immediate_diagnostic_emit(&input, &result), 0); + check_int("mode.default.status", result.status, + KZT_RELA_DIAGNOSTIC_EMIT_DISABLED); + + input.mode = KZT_RELA_DIAGNOSTIC_MODE_WRITE_ENABLED_ONLY; + check_int("mode.write-only.emit", + kzt_rela_immediate_diagnostic_emit(&input, &result), 0); + check_int("mode.write-only.status", result.status, + KZT_RELA_DIAGNOSTIC_EMIT_DISABLED); + check_int("mode.closed.sink", capture.calls, 0); + + input.mode = KZT_RELA_DIAGNOSTIC_MODE_DIAGNOSTICS; + check_int("mode.diagnostics.emit", + kzt_rela_immediate_diagnostic_emit(&input, &result), 0); + check_int("mode.diagnostics.status", result.status, + KZT_RELA_DIAGNOSTIC_EMIT_EMITTED); + check_int("mode.diagnostics.domain", result.record.reason_domain, + KZT_RELA_DIAGNOSTIC_REASON_PLANNER); + check_string("mode.diagnostics.writer", result.record.writer_result, + "NOT_RECORDED"); + check_int("mode.diagnostics.fallback", result.record.legacy_fallback, 1); + + input.mode = KZT_RELA_DIAGNOSTIC_MODE_DIAGNOSTICS_WRITE_ENABLED; + input.legacy_fallback = 0; + check_int("mode.combined.emit", + kzt_rela_immediate_diagnostic_emit(&input, &result), 0); + check_int("mode.combined.status", result.status, + KZT_RELA_DIAGNOSTIC_EMIT_EMITTED); + check_int("mode.combined.domain", result.record.reason_domain, + KZT_RELA_DIAGNOSTIC_REASON_WRITER); + check_string("mode.combined.writer", result.record.writer_result, + "APPLIED"); + check_int("mode.combined.fallback", result.record.legacy_fallback, 0); + check_int("mode.open.sink", capture.calls, 2); + + check_int("mode.snapshot", + kzt_rela_diagnostic_throttle_snapshot(&throttle, &snapshot), + 0); + check_ulong("mode.snapshot.admitted", snapshot.admitted, 2); + check_ulong("mode.snapshot.suppressed", snapshot.suppressed, 0); +} + +static void test_record_copies_fields_before_formatting(void) +{ + char source_path[] = "/guest/libdiag-copy.so"; + kzt_rela_immediate_candidate_request_t request = + base_request(source_path); + kzt_rela_immediate_writer_result_t writer_result = + base_result(&request); + kzt_rela_diagnostic_record_t record; + char line[KZT_RELA_DIAGNOSTIC_LINE_LIMIT]; + + ++tests_run; + check_int("fields.record", + kzt_rela_immediate_diagnostic_record( + KZT_RELA_DIAGNOSTIC_MODE_DIAGNOSTICS_WRITE_ENABLED, + &request, &writer_result, 0, &record), + 0); + source_path[7] = 'X'; + check_int("fields.format", + kzt_rela_diagnostic_format(&record, line, sizeof(line)), + KZT_RELA_DIAGNOSTIC_FORMAT_OK); + check_true("fields.source", + strstr(line, "source=/guest/libdiag-copy.so") != NULL); + check_true("fields.source-link-map", + strstr(line, "source_link_map=0x110000") != NULL); + check_true("fields.current-owner", + strstr(line, "current_owner=0x220000") != NULL); + check_true("fields.source-generation", + strstr(line, "source_generation=17") != NULL); + check_true("fields.current-owner-generation", + strstr(line, "current_owner_generation=23") != NULL); + check_true("fields.owner-match", + strstr(line, "owner_match=MATCH") != NULL); + check_true("fields.wrapper-match", + strstr(line, "wrapper_match=VERSION_MATCH") != NULL); + check_true("fields.bridge-target", + strstr(line, "bridge_target=0x660000") != NULL); + check_true("fields.symbol", + strstr(line, "symbol=diag_symbol") != NULL); + check_true("fields.version", + strstr(line, "version=DIAG_1.0") != NULL); + check_true("fields.reason-domain", + strstr(line, "reason_domain=writer") != NULL); + check_true("fields.reason", strstr(line, "reason=NONE") != NULL); + check_true("fields.decision", + strstr(line, "decision=APPROVED") != NULL); + check_true("fields.writer-result", + strstr(line, "writer_result=APPLIED") != NULL); + check_true("fields.legacy-fallback", + strstr(line, "legacy_fallback=0") != NULL); +} + +static void test_candidate_reason_domain_is_preserved(void) +{ + kzt_rela_immediate_candidate_request_t request = + base_request("/guest/libcandidate.so"); + kzt_rela_immediate_writer_result_t writer_result; + kzt_rela_diagnostic_record_t record; + + ++tests_run; + memset(&writer_result, 0, sizeof(writer_result)); + writer_result.planner_called = 1; + writer_result.plan.status = KZT_RELA_IMMEDIATE_CANDIDATE_FAIL_OPEN; + writer_result.plan.reason = + KZT_RELA_IMMEDIATE_CANDIDATE_REASON_MISSING_SLOT; + + check_int("candidate.record", + kzt_rela_immediate_diagnostic_record( + KZT_RELA_DIAGNOSTIC_MODE_DIAGNOSTICS, + &request, &writer_result, 1, &record), + 0); + check_int("candidate.domain", record.reason_domain, + KZT_RELA_DIAGNOSTIC_REASON_CANDIDATE); + check_string("candidate.reason", record.reason, "MISSING_SLOT"); + check_string("candidate.decision", record.decision, "FAIL_OPEN"); + check_string("candidate.writer", record.writer_result, "NOT_RECORDED"); + check_int("candidate.fallback", record.legacy_fallback, 1); +} + +/* + * This unit only proves that diagnostic failures preserve the snapshots and + * slot visible to this adapter. Production probe/writer isolation belongs in + * the later wiring test where those call paths can be observed. + */ +static void test_diagnostic_failures_preserve_caller_inputs(void) +{ + uintptr_t slot = 0x440000; + kzt_rela_immediate_candidate_request_t request = + base_request("/guest/libfail-open.so"); + kzt_rela_immediate_writer_result_t writer_result = + base_result(&request); + kzt_rela_immediate_candidate_request_t request_before; + kzt_rela_immediate_writer_result_t writer_before; + kzt_rela_diagnostic_record_t record; + kzt_rela_diagnostic_throttle_t throttle; + kzt_rela_immediate_diagnostic_result_t emit_result; + capture_sink_t capture = { 0 }; + char tiny[32]; + char buffer[KZT_RELA_DIAGNOSTIC_LINE_LIMIT]; + kzt_rela_immediate_diagnostic_input_t input; + + ++tests_run; + request.slot_addr = (uintptr_t)&slot; + writer_result.plan.decision.slot_addr = request.slot_addr; + request_before = request; + writer_before = writer_result; + + check_int("failure.record", + kzt_rela_immediate_diagnostic_record( + KZT_RELA_DIAGNOSTIC_MODE_DIAGNOSTICS_WRITE_ENABLED, + &request, &writer_result, 1, &record), + 0); + check_int("failure.format.truncated", + kzt_rela_diagnostic_format(&record, tiny, sizeof(tiny)), + KZT_RELA_DIAGNOSTIC_FORMAT_TRUNCATED); + check_int("failure.format.nul", tiny[sizeof(tiny) - 1], '\0'); + + check_int("failure.throttle.init", + kzt_rela_diagnostic_throttle_init(&throttle, 4), 0); + input = base_input( + KZT_RELA_DIAGNOSTIC_MODE_DIAGNOSTICS_WRITE_ENABLED, + &request, &writer_result, &throttle, tiny, sizeof(tiny), &capture); + check_int("failure.emit.truncated", + kzt_rela_immediate_diagnostic_emit(&input, &emit_result), 0); + check_int("failure.emit.truncated.status", emit_result.status, + KZT_RELA_DIAGNOSTIC_EMIT_FORMAT_TRUNCATED); + check_int("failure.emit.truncated.sink", capture.calls, 0); + + input.buffer = buffer; + input.buffer_size = sizeof(buffer); + capture.fail = 1; + check_int("failure.emit.sink", + kzt_rela_immediate_diagnostic_emit(&input, &emit_result), 0); + check_int("failure.emit.sink.status", emit_result.status, + KZT_RELA_DIAGNOSTIC_EMIT_SINK_FAILED); + check_int("failure.emit.sink.calls", capture.calls, 1); + + memset(&throttle, 0, sizeof(throttle)); + capture.fail = 0; + input.throttle = &throttle; + check_int("failure.emit.throttle", + kzt_rela_immediate_diagnostic_emit(&input, &emit_result), 0); + check_int("failure.emit.throttle.status", emit_result.status, + KZT_RELA_DIAGNOSTIC_EMIT_THROTTLE_FAILED); + check_int("failure.emit.throttle.sink", capture.calls, 1); + + check_true("failure.request.unchanged", + memcmp(&request, &request_before, sizeof(request)) == 0); + check_true("failure.writer.unchanged", + memcmp(&writer_result, &writer_before, + sizeof(writer_result)) == 0); + check_ulong("failure.slot.unchanged", (unsigned long)slot, 0x440000); +} + +static void test_fixed_capacity_throttle_is_concurrency_safe(void) +{ + static const unsigned long total = + THROTTLE_THREADS * THROTTLE_ITERATIONS; + static const unsigned long capacity = + THROTTLE_THREADS * THROTTLE_ITERATIONS / 2; + kzt_rela_diagnostic_throttle_t throttle; + kzt_rela_diagnostic_throttle_snapshot_t snapshot; + pthread_barrier_t barrier; + pthread_t threads[THROTTLE_THREADS]; + pthread_t readers[SNAPSHOT_READERS]; + throttle_worker_t workers[THROTTLE_THREADS]; + snapshot_reader_t snapshot_readers[SNAPSHOT_READERS]; + unsigned int readers_done = 0; + unsigned long admitted = 0; + unsigned long suppressed = 0; + unsigned long errors = 0; + int i; + + ++tests_run; + check_int("throttle.concurrent.init", + kzt_rela_diagnostic_throttle_init(&throttle, capacity), 0); + check_int("throttle.barrier.init", + pthread_barrier_init(&barrier, NULL, THROTTLE_THREADS), 0); + memset(workers, 0, sizeof(workers)); + memset(snapshot_readers, 0, sizeof(snapshot_readers)); + for (i = 0; i < SNAPSHOT_READERS; ++i) { + snapshot_readers[i].throttle = &throttle; + snapshot_readers[i].capacity = capacity; + snapshot_readers[i].done = &readers_done; + check_int("throttle.reader.create", + pthread_create(&readers[i], NULL, snapshot_reader_main, + &snapshot_readers[i]), + 0); + } + for (i = 0; i < THROTTLE_THREADS; ++i) { + workers[i].throttle = &throttle; + workers[i].barrier = &barrier; + check_int("throttle.thread.create", + pthread_create(&threads[i], NULL, throttle_worker_main, + &workers[i]), + 0); + } + for (i = 0; i < THROTTLE_THREADS; ++i) { + check_int("throttle.thread.join", pthread_join(threads[i], NULL), 0); + admitted += workers[i].admitted; + suppressed += workers[i].suppressed; + errors += workers[i].errors; + } + __atomic_store_n(&readers_done, 1U, __ATOMIC_RELEASE); + for (i = 0; i < SNAPSHOT_READERS; ++i) { + check_int("throttle.reader.join", pthread_join(readers[i], NULL), 0); + check_true("throttle.reader.snapshots", + snapshot_readers[i].snapshots != 0); + errors += snapshot_readers[i].errors; + } + check_int("throttle.barrier.destroy", + pthread_barrier_destroy(&barrier), 0); + + check_ulong("throttle.concurrent.admitted", admitted, capacity); + check_ulong("throttle.concurrent.suppressed", suppressed, + total - capacity); + check_ulong("throttle.concurrent.errors", errors, 0); + check_int("throttle.concurrent.snapshot", + kzt_rela_diagnostic_throttle_snapshot(&throttle, &snapshot), + 0); + check_ulong("throttle.snapshot.capacity", snapshot.capacity, capacity); + check_ulong("throttle.snapshot.admitted", snapshot.admitted, capacity); + check_ulong("throttle.snapshot.suppressed", snapshot.suppressed, + total - capacity); + + check_int("throttle.saturation.init", + kzt_rela_diagnostic_throttle_init(&throttle, 0), 0); + throttle.suppressed = ULONG_MAX; + check_int("throttle.saturation.admit", + kzt_rela_diagnostic_throttle_try_admit(&throttle), 0); + check_int("throttle.saturation.snapshot", + kzt_rela_diagnostic_throttle_snapshot(&throttle, &snapshot), + 0); + check_ulong("throttle.saturation.suppressed", + snapshot.suppressed, ULONG_MAX); +} + +static void test_concurrent_adapter_has_exact_suppression_count(void) +{ + static const unsigned long capacity = 29; + static const unsigned long total = ADAPTER_THREADS * ADAPTER_ITERATIONS; + kzt_rela_immediate_candidate_request_t request = + base_request("/guest/libadapter-concurrent.so"); + kzt_rela_immediate_writer_result_t writer_result = + base_result(&request); + kzt_rela_diagnostic_throttle_t throttle; + kzt_rela_diagnostic_throttle_snapshot_t snapshot; + pthread_barrier_t barrier; + pthread_t threads[ADAPTER_THREADS]; + adapter_worker_t workers[ADAPTER_THREADS]; + atomic_sink_t sink = { 0 }; + unsigned long emitted = 0; + unsigned long suppressed = 0; + unsigned long errors = 0; + int i; + + ++tests_run; + check_int("adapter.concurrent.init", + kzt_rela_diagnostic_throttle_init(&throttle, capacity), 0); + check_int("adapter.barrier.init", + pthread_barrier_init(&barrier, NULL, ADAPTER_THREADS), 0); + memset(workers, 0, sizeof(workers)); + for (i = 0; i < ADAPTER_THREADS; ++i) { + workers[i].request = &request; + workers[i].writer_result = &writer_result; + workers[i].throttle = &throttle; + workers[i].barrier = &barrier; + workers[i].sink = &sink; + check_int("adapter.thread.create", + pthread_create(&threads[i], NULL, adapter_worker_main, + &workers[i]), + 0); + } + for (i = 0; i < ADAPTER_THREADS; ++i) { + check_int("adapter.thread.join", pthread_join(threads[i], NULL), 0); + emitted += workers[i].emitted; + suppressed += workers[i].suppressed; + errors += workers[i].errors; + } + check_int("adapter.barrier.destroy", + pthread_barrier_destroy(&barrier), 0); + + check_ulong("adapter.concurrent.emitted", emitted, capacity); + check_ulong("adapter.concurrent.suppressed", suppressed, + total - capacity); + check_ulong("adapter.concurrent.errors", errors, 0); + check_ulong("adapter.concurrent.sink", + __atomic_load_n(&sink.calls, __ATOMIC_RELAXED), capacity); + check_int("adapter.concurrent.snapshot", + kzt_rela_diagnostic_throttle_snapshot(&throttle, &snapshot), + 0); + check_ulong("adapter.snapshot.admitted", snapshot.admitted, capacity); + check_ulong("adapter.snapshot.suppressed", snapshot.suppressed, + total - capacity); +} + +int main(void) +{ + test_modes_gate_output_and_writer_fields(); + test_record_copies_fields_before_formatting(); + test_candidate_reason_domain_is_preserved(); + test_diagnostic_failures_preserve_caller_inputs(); + test_fixed_capacity_throttle_is_concurrency_safe(); + test_concurrent_adapter_has_exact_suppression_count(); + + if (failures) { + fprintf(stderr, + "kzt-wi238-structured-diagnostics-gate: %d failure(s)\n", + failures); + return 1; + } + + printf("kzt-wi238-structured-diagnostics-gate: %d tests passed\n", + tests_run); + return 0; +} diff --git a/tests/unit/kzt/test_wi254_loader_contract.py b/tests/unit/kzt/test_wi254_loader_contract.py new file mode 100644 index 00000000000..61567a64da4 --- /dev/null +++ b/tests/unit/kzt/test_wi254_loader_contract.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +import pathlib +import re +import sys + + +root = pathlib.Path(sys.argv[1]) +context = root / "target/i386/latx/context" +include = root / "target/i386/latx/include" + + +def read(path): + return path.read_text(encoding="utf-8") + + +def function_body(text, signature): + start = text.index(signature) + brace = text.index("{", start) + depth = 0 + for pos in range(brace, len(text)): + if text[pos] == "{": + depth += 1 + elif text[pos] == "}": + depth -= 1 + if depth == 0: + return text[brace + 1:pos] + raise AssertionError(f"unterminated function: {signature}") + + +binding = read(context / "kzt_guest_library_binding.c") +box_header = read(include / "box64context.h") +box_context = read(context / "box64context.c") +library = read(context / "library.c") +librarian = read(context / "librarian.c") +elfloader = read(context / "elfloader.c") +wrapped_dl = read(context / "wrappedlibdl.c") +wrapped_libc = read(context / "wrappedlibc.c") + +# No process-lifetime registry/gate/singleton is permitted. +for forbidden in ("call_gate", "gate_next", "call_gate_bindings"): + assert forbidden not in binding, forbidden +assert "kzt_guest_library_access_t kzt_guest_library_access;" in box_header +assert "KztGuestLibraryLookupForContext" in box_context +assert "kzt_guest_library_access_begin_teardown" in box_context + +# Both real wrapped dlopen implementations consume the exact AddNeeded result +# directly and never perform a second name lookup for the selected instance. +for name, text in (("wrappedlibdl", wrapped_dl), ("wrappedlibc", wrapped_libc)): + body = function_body(text, "void* my_dlopen(void *filename, int flag){") + assert "AddNeededLibWithLibrary" in body, name + assert "lib = GetLibInternal(rfilename)" not in body, name + assert "&lib) || !lib" in body, name + assert "dlopen_recycle_transaction(" in body, name + assert "recycle_prepare" in body, name + assert "recycle_rollback" in body, name + reset = function_body( + text, + "static void recycle_reset_loader_state(dlopen_recycle_context_t *recycle)", + ) + for cleared in ( + "recycle->library->x86linkmap = NULL", + "recycle->header->delta = 0", + "lm->l_addr = 0", + "LatxResetElf(recycle->header)", + ): + assert cleared in reset, (name, cleared) + call = function_body(text, "static int callx86dlopen(") + assert "h->delta = ret->l_addr" in call, name + assert "lm->l_addr = ret->l_addr" in call, name + assert "h->latx_hasfix = 1" in call, name + fixed = call.index("h->latx_hasfix = 1") + finish = call.index("finish_guest_dlopen_scoped", fixed) + assert fixed < finish, name + +# Both redlopen success branches publish the exact returned link_map/library +# pair before the follow-up x86 dlsym. +for name, text in (("wrappedlibdl", wrapped_dl), ("wrappedlibc", wrapped_libc)): + finish_body = function_body(text, "static void finish_guest_dlopen_scoped(") + assert "kzt_guest_library_publish_loader_pair_scoped" in finish_body, name + assert "kzt_guest_library_publish_loader_observed_scoped" in finish_body, name + assert finish_body.index("publish_loader_pair_scoped") < \ + finish_body.index("loader_scope_end"), name + redlopen = text.index("//redlopen") + publish = text.index("finish_guest_dlopen_scoped(", redlopen) + dlsym = text.index("x86dlsym", publish) + assert redlopen < publish < dlsym, name + +# Reload cannot reactivate or publish until all failure-returning reload work +# has completed. +reload_body = function_body(library, "int ReloadLibrary(library_t* lib)") +reactivate = reload_body.index("kzt_guest_library_reactivate") +assert reload_body.rfind("return 1", 0, reactivate) >= 0 +assert reload_body.index("RelocateElfPlt") < reactivate +assert reload_body.index("kzt_guest_library_note_loader_pair", reactivate) > reactivate + +# The real PLT loader uses the tested status seam and returns before resolver +# injection when the underlying RELA worker fails. +plt_body = function_body( + elfloader, + "int RelocateElfPlt(lib_t *maplib, lib_t *local_maplib, int bindnow, elfheader_t* head)", +) +apply = plt_body.index("elf_plt_relocation_apply") +failure_return = plt_body.index("return -1", apply) +resolver = plt_body.index("if(need_resolver)") +assert apply < failure_return < resolver + +# The exact API preserves AddNeededLib's historical return behavior while +# clearing output on failure and publishing only the final successful choice. +exact_body = function_body(librarian, "int AddNeededLibWithLibrary(") +assert "if (exact_library) *exact_library = NULL;" in exact_body +assert "if (!add_result && exact_library)" in exact_body +assert re.search(r"return\s+0\s*;", exact_body) + +print("WI-254 loader/context white-box contract: PASS") diff --git a/tests/unit/kzt/test_wi256_lazy_binding.c b/tests/unit/kzt/test_wi256_lazy_binding.c new file mode 100644 index 00000000000..7ab0fad8715 --- /dev/null +++ b/tests/unit/kzt/test_wi256_lazy_binding.c @@ -0,0 +1,489 @@ +#include +#include + +#include "target/i386/latx/include/kzt_lazy_binding.h" + +typedef struct fixture { + uintptr_t slot; + uintptr_t guest_target; + uintptr_t native_bridge; + uintptr_t competing_target; + int post_bind_valid; + kzt_lazy_binding_route_status_t route_status; + int load_calls; + int validate_calls; + int route_calls; +} fixture_t; + +static int failures; + +#define CHECK(name, condition) do { \ + if (!(condition)) { \ + fprintf(stderr, "%s failed at line %d\n", name, __LINE__); \ + ++failures; \ + } \ +} while (0) + +static int load_slot(uintptr_t slot_addr, uintptr_t *value, void *opaque) +{ + fixture_t *fixture = opaque; + + ++fixture->load_calls; + *value = *(uintptr_t *)slot_addr; + return 0; +} + +static int validate_post_bind(const kzt_lazy_binding_pending_t *pending, + uintptr_t guest_target, void *opaque) +{ + fixture_t *fixture = opaque; + + ++fixture->validate_calls; + CHECK("validate.slot", pending->slot_addr == (uintptr_t)&fixture->slot); + CHECK("validate.guest-target", guest_target == fixture->guest_target); + return fixture->post_bind_valid; +} + +static int route_guest_target(const kzt_lazy_binding_pending_t *pending, + uintptr_t guest_target, + kzt_lazy_binding_route_result_t *result, + void *opaque) +{ + fixture_t *fixture = opaque; + + ++fixture->route_calls; + CHECK("route.main-namespace", pending->namespace_id == 0); + CHECK("route.main-namespace-kind", + pending->namespace_kind == KZT_GUEST_LIBRARY_NAMESPACE_MAIN); + CHECK("route.version-evidence", + kzt_symbol_version_evidence_valid(pending->version_evidence, + pending->version)); + CHECK("route.guest-target", guest_target == fixture->guest_target); + result->status = fixture->route_status; + switch (fixture->route_status) { + case KZT_LAZY_BINDING_ROUTE_NATIVE_APPLIED: + result->selected_target = fixture->native_bridge; + result->final_value = fixture->native_bridge; + fixture->slot = fixture->native_bridge; + break; + case KZT_LAZY_BINDING_ROUTE_CAS_MISMATCH: + result->selected_target = fixture->competing_target; + result->final_value = fixture->competing_target; + fixture->slot = fixture->competing_target; + break; + case KZT_LAZY_BINDING_ROUTE_GUEST_PRESERVED: + case KZT_LAZY_BINDING_ROUTE_ERROR: + result->selected_target = guest_target; + result->final_value = guest_target; + break; + } + return 0; +} + +static fixture_t fixture(void) +{ + return (fixture_t) { + .slot = 0x71000100, + .guest_target = 0x72000200, + .native_bridge = 0x73000300, + .competing_target = 0x74000400, + .post_bind_valid = 1, + .route_status = KZT_LAZY_BINDING_ROUTE_NATIVE_APPLIED, + }; +} + +static kzt_lazy_binding_begin_request_t begin_request_for(fixture_t *fixture) +{ + return (kzt_lazy_binding_begin_request_t) { + .enabled = 1, + .context_id = 0x1000, + .source_link_map = 0x2000, + .source_generation = 7, + .namespace_id = 0, + .namespace_kind = KZT_GUEST_LIBRARY_NAMESPACE_MAIN, + .relocation_index = 3, + .slot_addr = (uintptr_t)&fixture->slot, + .unresolved_stub = fixture->slot, + .symbol = "realloc", + .version_evidence = KZT_SYMBOL_VERSION_VERSIONED, + .version = "GLIBC_2.2.5", + .guest_resolver = 0x75000500, + }; +} + +static kzt_lazy_binding_ops_t ops_for(fixture_t *fixture) +{ + return (kzt_lazy_binding_ops_t) { + .load_slot = load_slot, + .validate_post_bind = validate_post_bind, + .route_guest_target = route_guest_target, + .opaque = fixture, + }; +} + +static void begin_binding(kzt_lazy_binding_begin_request_t *request, + kzt_lazy_binding_pending_t *pending, + kzt_lazy_binding_result_t *result) +{ + memset(pending, 0, sizeof(*pending)); + CHECK("begin.call", + kzt_lazy_binding_begin(request, pending, result) == 0); +} + +static void test_first_call_hands_off_to_guest(void) +{ + fixture_t f = fixture(); + kzt_lazy_binding_begin_request_t request = begin_request_for(&f); + kzt_lazy_binding_result_t result; + kzt_lazy_binding_pending_t pending; + + begin_binding(&request, &pending, &result); + + CHECK("first.status", + result.status == KZT_LAZY_BINDING_HANDOFF_GUEST); + CHECK("first.armed", pending.armed == 1); + CHECK("first.result-armed", result.pending_armed == 1); + CHECK("first.guest-resolver", + result.selected_target == request.guest_resolver); + CHECK("first.slot-still-stub", f.slot == request.unresolved_stub); + CHECK("first.no-route", f.route_calls == 0); +} + +static void test_post_bind_call_can_install_native_bridge(void) +{ + fixture_t f = fixture(); + kzt_lazy_binding_begin_request_t request = begin_request_for(&f); + kzt_lazy_binding_ops_t ops = ops_for(&f); + kzt_lazy_binding_result_t result; + kzt_lazy_binding_pending_t pending; + + begin_binding(&request, &pending, &result); + + /* This write represents the guest dynamic linker completing call one. */ + f.slot = f.guest_target; + CHECK("complete.call", + kzt_lazy_binding_complete(&pending, &ops, &result) == 0); + CHECK("complete.status", + result.status == KZT_LAZY_BINDING_NATIVE_APPLIED); + CHECK("complete.before", result.slot_before == f.guest_target); + CHECK("complete.after", result.slot_after == f.native_bridge); + CHECK("complete.slot", f.slot == f.native_bridge); + CHECK("complete.one-route", f.route_calls == 1); + CHECK("complete.consumed", result.pending_armed == 0); +} + +static void test_expected_target_mismatch_preserves_competitor_without_retry(void) +{ + fixture_t f = fixture(); + kzt_lazy_binding_begin_request_t request = begin_request_for(&f); + kzt_lazy_binding_ops_t ops = ops_for(&f); + kzt_lazy_binding_result_t result; + kzt_lazy_binding_pending_t pending; + + begin_binding(&request, &pending, &result); + + f.slot = f.guest_target; + f.route_status = KZT_LAZY_BINDING_ROUTE_CAS_MISMATCH; + CHECK("race.call", + kzt_lazy_binding_complete(&pending, &ops, &result) == 0); + CHECK("race.status", result.status == KZT_LAZY_BINDING_CAS_MISMATCH); + CHECK("race.preserve", f.slot == f.competing_target); + CHECK("race.report-final", result.slot_after == f.competing_target); + CHECK("race.one-route", f.route_calls == 1); + CHECK("race.consumed", result.pending_armed == 0); +} + +static void test_missing_symbol_version_fails_open(void) +{ + fixture_t f = fixture(); + kzt_lazy_binding_begin_request_t request = begin_request_for(&f); + kzt_lazy_binding_ops_t ops = ops_for(&f); + kzt_lazy_binding_result_t result; + kzt_lazy_binding_pending_t pending; + + request.version = NULL; + begin_binding(&request, &pending, &result); + f.slot = f.guest_target; + CHECK("version.complete", + kzt_lazy_binding_complete(&pending, &ops, &result) == 0); + CHECK("version.status", + result.status == KZT_LAZY_BINDING_GUEST_PRESERVED); + CHECK("version.reason", + result.reason == KZT_LAZY_BINDING_REASON_MISSING_VERSION); + CHECK("version.slot", f.slot == f.guest_target); + CHECK("version.no-route", f.route_calls == 0); + CHECK("version.consumed", result.pending_armed == 0); +} + +static void test_confirmed_unversioned_binding_can_apply(void) +{ + fixture_t f = fixture(); + kzt_lazy_binding_begin_request_t request = begin_request_for(&f); + kzt_lazy_binding_ops_t ops = ops_for(&f); + kzt_lazy_binding_result_t result; + kzt_lazy_binding_pending_t pending; + + request.version_evidence = KZT_SYMBOL_VERSION_CONFIRMED_UNVERSIONED; + request.version = NULL; + begin_binding(&request, &pending, &result); + CHECK("unversioned.begin", + result.status == KZT_LAZY_BINDING_HANDOFF_GUEST && + pending.version_evidence == + KZT_SYMBOL_VERSION_CONFIRMED_UNVERSIONED && + pending.version == NULL); + f.slot = f.guest_target; + CHECK("unversioned.complete", + kzt_lazy_binding_complete(&pending, &ops, &result) == 0); + CHECK("unversioned.applied", + result.status == KZT_LAZY_BINDING_NATIVE_APPLIED && + f.route_calls == 1 && f.slot == f.native_bridge); +} + +static void test_unknown_and_error_version_evidence_fail_open(void) +{ + kzt_symbol_version_evidence_t evidence[] = { + KZT_SYMBOL_VERSION_UNKNOWN, + KZT_SYMBOL_VERSION_ERROR, + }; + size_t i; + + for (i = 0; i < sizeof(evidence) / sizeof(evidence[0]); ++i) { + fixture_t f = fixture(); + kzt_lazy_binding_begin_request_t request = begin_request_for(&f); + kzt_lazy_binding_ops_t ops = ops_for(&f); + kzt_lazy_binding_result_t result; + kzt_lazy_binding_pending_t pending; + + request.version_evidence = evidence[i]; + begin_binding(&request, &pending, &result); + CHECK("untrusted.evidence-propagated", + pending.version_evidence == evidence[i]); + f.slot = f.guest_target; + CHECK("untrusted.complete", + kzt_lazy_binding_complete(&pending, &ops, &result) == 0); + CHECK("untrusted.preserved", + result.status == KZT_LAZY_BINDING_GUEST_PRESERVED && + result.reason == KZT_LAZY_BINDING_REASON_MISSING_VERSION && + f.route_calls == 0 && f.slot == f.guest_target); + } +} + +static void test_post_bind_validation_failure_keeps_guest_target(void) +{ + fixture_t f = fixture(); + kzt_lazy_binding_begin_request_t request = begin_request_for(&f); + kzt_lazy_binding_ops_t ops = ops_for(&f); + kzt_lazy_binding_result_t result; + kzt_lazy_binding_pending_t pending; + + begin_binding(&request, &pending, &result); + + f.slot = f.guest_target; + f.post_bind_valid = 0; + CHECK("validation.complete", + kzt_lazy_binding_complete(&pending, &ops, &result) == 0); + CHECK("validation.status", + result.status == KZT_LAZY_BINDING_GUEST_PRESERVED); + CHECK("validation.reason", + result.reason == KZT_LAZY_BINDING_REASON_POST_BIND_INVALID); + CHECK("validation.slot", f.slot == f.guest_target); + CHECK("validation.one-check", f.validate_calls == 1); + CHECK("validation.no-route", f.route_calls == 0); + CHECK("validation.consumed", result.pending_armed == 0); +} + +static void test_slot_unchanged_consumes_pending(void) +{ + fixture_t f = fixture(); + kzt_lazy_binding_begin_request_t request = begin_request_for(&f); + kzt_lazy_binding_ops_t ops = ops_for(&f); + kzt_lazy_binding_result_t result; + kzt_lazy_binding_pending_t pending; + + begin_binding(&request, &pending, &result); + + CHECK("unchanged.complete", + kzt_lazy_binding_complete(&pending, &ops, &result) == 0); + CHECK("unchanged.status", + result.status == KZT_LAZY_BINDING_GUEST_PRESERVED); + CHECK("unchanged.reason", + result.reason == KZT_LAZY_BINDING_REASON_SLOT_UNCHANGED); + CHECK("unchanged.no-route", f.route_calls == 0); + CHECK("unchanged.consumed", pending.armed == 0 && + result.pending_cleared == 1); +} + +static void test_overlong_symbol_and_version_fail_open(void) +{ + char overlong[KZT_LAZY_BINDING_SYMBOL_MAX + 1]; + fixture_t f = fixture(); + kzt_lazy_binding_begin_request_t request = begin_request_for(&f); + kzt_lazy_binding_result_t result; + kzt_lazy_binding_pending_t pending; + + memset(overlong, 'x', sizeof(overlong) - 1); + overlong[sizeof(overlong) - 1] = '\0'; + memset(&pending, 0, sizeof(pending)); + request.symbol = overlong; + CHECK("long-symbol.begin", + kzt_lazy_binding_begin(&request, &pending, &result) == 0); + CHECK("long-symbol.preserved", + result.status == KZT_LAZY_BINDING_GUEST_PRESERVED && + result.reason == KZT_LAZY_BINDING_REASON_INVALID_REQUEST && + pending.armed == 0); + + memset(&pending, 0, sizeof(pending)); + request = begin_request_for(&f); + request.version = overlong; + CHECK("long-version.begin", + kzt_lazy_binding_begin(&request, &pending, &result) == 0); + CHECK("long-version.preserved", + result.status == KZT_LAZY_BINDING_GUEST_PRESERVED && + result.reason == KZT_LAZY_BINDING_REASON_INVALID_REQUEST && + pending.armed == 0); +} + +static void test_nested_pending_busy_preserves_outer_state(void) +{ + fixture_t f = fixture(); + kzt_lazy_binding_begin_request_t outer = begin_request_for(&f); + kzt_lazy_binding_begin_request_t nested = begin_request_for(&f); + kzt_lazy_binding_result_t result; + kzt_lazy_binding_pending_t pending; + kzt_lazy_binding_pending_t saved; + + begin_binding(&outer, &pending, &result); + saved = pending; + saved.symbol = pending.symbol ? saved.symbol_storage : NULL; + saved.version = pending.version ? saved.version_storage : NULL; + + nested.relocation_index = outer.relocation_index + 1; + nested.slot_addr += sizeof(uintptr_t); + CHECK("nested.begin", + kzt_lazy_binding_begin(&nested, &pending, &result) == 0); + CHECK("nested.busy", + result.status == KZT_LAZY_BINDING_GUEST_PRESERVED && + result.reason == KZT_LAZY_BINDING_REASON_PENDING_BUSY); + CHECK("nested.outer-armed", pending.armed == 1); + CHECK("nested.outer-identity", + pending.context_id == saved.context_id && + pending.source_link_map == saved.source_link_map && + pending.source_generation == saved.source_generation && + pending.relocation_index == saved.relocation_index && + pending.slot_addr == saved.slot_addr && + !strcmp(pending.symbol, saved.symbol) && + !strcmp(pending.version, saved.version)); +} + +static void test_new_context_replaces_abandoned_pending(void) +{ + fixture_t f = fixture(); + kzt_lazy_binding_begin_request_t abandoned = begin_request_for(&f); + kzt_lazy_binding_begin_request_t next = begin_request_for(&f); + kzt_lazy_binding_result_t result; + kzt_lazy_binding_pending_t pending; + + begin_binding(&abandoned, &pending, &result); + + next.context_id = abandoned.context_id + 1; + next.source_link_map = abandoned.source_link_map + 0x1000; + next.source_generation = abandoned.source_generation + 1; + next.relocation_index = abandoned.relocation_index + 1; + next.slot_addr += sizeof(uintptr_t); + next.symbol = "next_context_symbol"; + + CHECK("new-context.begin", + kzt_lazy_binding_begin(&next, &pending, &result) == 0); + CHECK("new-context.handoff", + result.status == KZT_LAZY_BINDING_HANDOFF_GUEST && + result.reason == KZT_LAZY_BINDING_REASON_NONE && + result.pending_armed == 1); + CHECK("new-context.replaced", + pending.armed == 1 && + pending.context_id == next.context_id && + pending.source_link_map == next.source_link_map && + pending.source_generation == next.source_generation && + pending.relocation_index == next.relocation_index && + pending.slot_addr == next.slot_addr && + !strcmp(pending.symbol, next.symbol)); +} + +static void test_cancelled_pending_allows_same_context_retry(void) +{ + fixture_t f = fixture(); + kzt_lazy_binding_begin_request_t first = begin_request_for(&f); + kzt_lazy_binding_begin_request_t retry = begin_request_for(&f); + kzt_lazy_binding_result_t result; + kzt_lazy_binding_pending_t pending; + + begin_binding(&first, &pending, &result); + + kzt_lazy_binding_cancel(&pending); + CHECK("cancel.cleared", pending.armed == 0 && + pending.context_id == 0 && pending.source_link_map == 0 && + pending.slot_addr == 0 && pending.symbol == NULL && + pending.version == NULL); + + retry.relocation_index = first.relocation_index + 1; + retry.slot_addr += sizeof(uintptr_t); + retry.symbol = "retry_symbol"; + CHECK("cancel.retry", + kzt_lazy_binding_begin(&retry, &pending, &result) == 0); + CHECK("cancel.retry-armed", + result.status == KZT_LAZY_BINDING_HANDOFF_GUEST && + result.pending_armed == 1 && pending.armed == 1 && + pending.context_id == retry.context_id && + pending.relocation_index == retry.relocation_index && + pending.slot_addr == retry.slot_addr && + !strcmp(pending.symbol, retry.symbol)); +} + +static void test_only_main_namespace_is_armed(void) +{ + fixture_t f = fixture(); + kzt_lazy_binding_begin_request_t request = begin_request_for(&f); + kzt_lazy_binding_result_t result; + kzt_lazy_binding_pending_t pending; + + request.namespace_id = 1; + request.namespace_kind = KZT_GUEST_LIBRARY_NAMESPACE_EXPLICIT; + begin_binding(&request, &pending, &result); + CHECK("namespace.status", + result.status == KZT_LAZY_BINDING_GUEST_PRESERVED); + CHECK("namespace.reason", + result.reason == KZT_LAZY_BINDING_REASON_NON_MAIN_NAMESPACE); + CHECK("namespace.not-armed", pending.armed == 0); + CHECK("namespace.guest-resolver", + result.selected_target == request.guest_resolver); + CHECK("namespace.slot", f.slot == request.unresolved_stub); + + request.namespace_id = 0; + request.namespace_kind = KZT_GUEST_LIBRARY_NAMESPACE_MAIN; + begin_binding(&request, &pending, &result); + CHECK("main.status", + result.status == KZT_LAZY_BINDING_HANDOFF_GUEST); + CHECK("main.armed", pending.armed == 1); +} + +int main(void) +{ + test_first_call_hands_off_to_guest(); + test_post_bind_call_can_install_native_bridge(); + test_expected_target_mismatch_preserves_competitor_without_retry(); + test_missing_symbol_version_fails_open(); + test_confirmed_unversioned_binding_can_apply(); + test_unknown_and_error_version_evidence_fail_open(); + test_post_bind_validation_failure_keeps_guest_target(); + test_only_main_namespace_is_armed(); + test_slot_unchanged_consumes_pending(); + test_overlong_symbol_and_version_fail_open(); + test_nested_pending_busy_preserves_outer_state(); + test_new_context_replaces_abandoned_pending(); + test_cancelled_pending_allows_same_context_retry(); + if (failures) { + fprintf(stderr, "%d WI-256 lazy-binding checks failed\n", failures); + return 1; + } + puts("KZT WI-256 lazy binding: PASS"); + return 0; +} diff --git a/tests/unit/kzt/test_wi256_lazy_diagnostics.c b/tests/unit/kzt/test_wi256_lazy_diagnostics.c new file mode 100644 index 00000000000..b683f4adfec --- /dev/null +++ b/tests/unit/kzt/test_wi256_lazy_diagnostics.c @@ -0,0 +1,341 @@ +#include +#include +#include +#include + +#include "target/i386/latx/include/kzt_lazy_binding.h" +#include "target/i386/latx/include/kzt_lazy_diagnostics.h" + +static int failures; + +static void check_true(const char *name, int condition) +{ + if (condition) { + return; + } + + fprintf(stderr, "%s: condition failed\n", name); + ++failures; +} + +static void check_int(const char *name, int got, int expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got %d expected %d\n", name, got, expected); + ++failures; +} + +static void check_ulong(const char *name, unsigned long got, + unsigned long expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got %lu expected %lu\n", name, got, expected); + ++failures; +} + +static void check_string(const char *name, const char *got, + const char *expected) +{ + if (got && expected && strcmp(got, expected) == 0) { + return; + } + + fprintf(stderr, "%s: got \"%s\" expected \"%s\"\n", name, + got ? got : "(null)", expected ? expected : "(null)"); + ++failures; +} + +static void check_contains(const char *name, const char *text, + const char *needle) +{ + if (text && needle && strstr(text, needle)) { + return; + } + + fprintf(stderr, "%s: \"%s\" missing \"%s\"\n", name, + text ? text : "(null)", needle ? needle : "(null)"); + ++failures; +} + +typedef struct capture_sink { + int calls; + int fail; + char line[KZT_LAZY_DIAGNOSTIC_LINE_LIMIT]; +} capture_sink_t; + +static int capture_line(const char *line, size_t line_length, void *opaque) +{ + capture_sink_t *capture = opaque; + size_t copy_length = line_length; + + ++capture->calls; + if (copy_length >= sizeof(capture->line)) { + copy_length = sizeof(capture->line) - 1; + } + memcpy(capture->line, line, copy_length); + capture->line[copy_length] = '\0'; + return capture->fail ? -1 : 0; +} + +static kzt_lazy_binding_pending_t base_pending(void) +{ + kzt_lazy_binding_pending_t pending; + + memset(&pending, 0, sizeof(pending)); + pending.armed = 1; + pending.slot_addr = 0x71000018; + pending.unresolved_stub = 0x72000020; + strcpy(pending.symbol_storage, "realloc"); + pending.symbol = pending.symbol_storage; + strcpy(pending.version_storage, "GLIBC_2.2.5"); + pending.version = pending.version_storage; + return pending; +} + +static kzt_lazy_binding_result_t base_result(void) +{ + kzt_lazy_binding_result_t result; + + memset(&result, 0, sizeof(result)); + result.status = KZT_LAZY_BINDING_NATIVE_APPLIED; + result.reason = KZT_LAZY_BINDING_REASON_NATIVE_APPLIED; + result.slot_before = 0x73000030; + result.slot_after = 0x74000040; + result.selected_target = 0x74000040; + return result; +} + +static void test_emit_disabled_skips_format_and_sink(void) +{ + kzt_lazy_binding_pending_t pending = base_pending(); + kzt_lazy_binding_result_t binding_result = base_result(); + capture_sink_t capture = { 0 }; + char buffer[KZT_LAZY_DIAGNOSTIC_LINE_LIMIT]; + kzt_lazy_diagnostic_input_t input = { + .enabled = 0, + .pending = &pending, + .binding_result = &binding_result, + .buffer = buffer, + .buffer_size = sizeof(buffer), + .sink = capture_line, + .sink_opaque = &capture, + }; + kzt_lazy_diagnostic_emit_result_t emit_result; + + memset(buffer, 'X', sizeof(buffer)); + check_int("disabled.emit", + kzt_lazy_diagnostics_emit(&input, &emit_result), 0); + check_int("disabled.status", emit_result.status, + KZT_LAZY_DIAGNOSTIC_EMIT_DISABLED); + check_int("disabled.sink", capture.calls, 0); + check_int("disabled.record", emit_result.record_present, 0); + check_int("disabled.buffer-untouched", buffer[0], 'X'); +} + +static void test_native_applied_records_schema_one(void) +{ + kzt_lazy_binding_pending_t pending = base_pending(); + kzt_lazy_binding_result_t binding_result = base_result(); + capture_sink_t capture = { 0 }; + char buffer[KZT_LAZY_DIAGNOSTIC_LINE_LIMIT]; + kzt_lazy_diagnostic_input_t input = { + .enabled = 1, + .pending = &pending, + .binding_result = &binding_result, + .buffer = buffer, + .buffer_size = sizeof(buffer), + .sink = capture_line, + .sink_opaque = &capture, + }; + kzt_lazy_diagnostic_emit_result_t emit_result; + + check_int("native.emit", + kzt_lazy_diagnostics_emit(&input, &emit_result), 0); + check_int("native.status", emit_result.status, + KZT_LAZY_DIAGNOSTIC_EMIT_EMITTED); + check_int("native.record", emit_result.record_present, 1); + check_ulong("native.schema", emit_result.record.schema, 1); + check_string("native.symbol", emit_result.record.symbol, "realloc"); + check_string("native.first-route", + emit_result.record.first_execution_route, "guest"); + check_string("native.route-status", + emit_result.record.completion_route_status, + "NATIVE_APPLIED"); + check_ulong("native.slot-before", emit_result.record.slot_before, + pending.unresolved_stub); + check_ulong("native.slot-after-guest", + emit_result.record.slot_after_guest, + binding_result.slot_before); + check_ulong("native.second-target", + emit_result.record.selected_second_target, + binding_result.slot_after); + check_string("native.reason", emit_result.record.reason, + "NATIVE_APPLIED"); + check_int("native.sink", capture.calls, 1); + check_contains("native.line.schema", capture.line, "schema=1"); + check_contains("native.line.symbol", capture.line, "symbol=realloc"); + check_contains("native.line.route", capture.line, + "completion_route_status=NATIVE_APPLIED"); + check_contains("native.line.before", capture.line, + "slot_before=0x72000020"); + check_contains("native.line.after-guest", capture.line, + "slot_after_guest=0x73000030"); + check_contains("native.line.second-target", capture.line, + "selected_second_target=0x74000040"); +} + +static void test_slot_unchanged_uses_stable_status_name(void) +{ + kzt_lazy_binding_pending_t pending = base_pending(); + kzt_lazy_binding_result_t binding_result = base_result(); + capture_sink_t capture = { 0 }; + char buffer[KZT_LAZY_DIAGNOSTIC_LINE_LIMIT]; + kzt_lazy_diagnostic_input_t input = { + .enabled = 1, + .pending = &pending, + .binding_result = &binding_result, + .buffer = buffer, + .buffer_size = sizeof(buffer), + .sink = capture_line, + .sink_opaque = &capture, + }; + kzt_lazy_diagnostic_emit_result_t emit_result; + + binding_result.status = KZT_LAZY_BINDING_GUEST_PRESERVED; + binding_result.reason = KZT_LAZY_BINDING_REASON_SLOT_UNCHANGED; + binding_result.slot_before = pending.unresolved_stub; + binding_result.slot_after = pending.unresolved_stub; + check_int("slot-unchanged.emit", + kzt_lazy_diagnostics_emit(&input, &emit_result), 0); + check_string("slot-unchanged.status", + emit_result.record.completion_route_status, + "SLOT_UNCHANGED"); + check_string("slot-unchanged.reason", emit_result.record.reason, + "SLOT_UNCHANGED"); +} + +static void test_missing_version_uses_stable_status_name(void) +{ + kzt_lazy_binding_pending_t pending = base_pending(); + kzt_lazy_binding_result_t binding_result = base_result(); + capture_sink_t capture = { 0 }; + char buffer[KZT_LAZY_DIAGNOSTIC_LINE_LIMIT]; + kzt_lazy_diagnostic_input_t input = { + .enabled = 1, + .pending = &pending, + .binding_result = &binding_result, + .buffer = buffer, + .buffer_size = sizeof(buffer), + .sink = capture_line, + .sink_opaque = &capture, + }; + kzt_lazy_diagnostic_emit_result_t emit_result; + + pending.version = NULL; + pending.version_storage[0] = '\0'; + binding_result.status = KZT_LAZY_BINDING_GUEST_PRESERVED; + binding_result.reason = KZT_LAZY_BINDING_REASON_MISSING_VERSION; + binding_result.slot_after = binding_result.slot_before; + check_int("missing-version.emit", + kzt_lazy_diagnostics_emit(&input, &emit_result), 0); + check_string("missing-version.status", + emit_result.record.completion_route_status, + "MISSING_VERSION"); + check_string("missing-version.reason", emit_result.record.reason, + "MISSING_VERSION"); +} + +static void test_cas_mismatch_uses_final_competitor_target(void) +{ + kzt_lazy_binding_pending_t pending = base_pending(); + kzt_lazy_binding_result_t binding_result = base_result(); + capture_sink_t capture = { 0 }; + char buffer[KZT_LAZY_DIAGNOSTIC_LINE_LIMIT]; + kzt_lazy_diagnostic_input_t input = { + .enabled = 1, + .pending = &pending, + .binding_result = &binding_result, + .buffer = buffer, + .buffer_size = sizeof(buffer), + .sink = capture_line, + .sink_opaque = &capture, + }; + kzt_lazy_diagnostic_emit_result_t emit_result; + + binding_result.status = KZT_LAZY_BINDING_CAS_MISMATCH; + binding_result.reason = KZT_LAZY_BINDING_REASON_CAS_MISMATCH; + binding_result.slot_after = 0x75000050; + binding_result.selected_target = 0x76000060; + check_int("cas.emit", + kzt_lazy_diagnostics_emit(&input, &emit_result), 0); + check_string("cas.status", emit_result.record.completion_route_status, + "CAS_MISMATCH"); + check_ulong("cas.second-target", + emit_result.record.selected_second_target, + 0x75000050); + check_contains("cas.line.second-target", capture.line, + "selected_second_target=0x75000050"); +} + +static void test_production_emit_writes_stderr(void) +{ + kzt_lazy_binding_pending_t pending = base_pending(); + kzt_lazy_binding_result_t binding_result = base_result(); + kzt_lazy_diagnostic_emit_result_t emit_result; + int saved_stderr; + int pipefd[2]; + char output[KZT_LAZY_DIAGNOSTIC_LINE_LIMIT]; + ssize_t count; + + check_int("production.pipe", pipe(pipefd), 0); + saved_stderr = dup(STDERR_FILENO); + check_true("production.saved-stderr", saved_stderr >= 0); + check_true("production.redirect", + dup2(pipefd[1], STDERR_FILENO) >= 0); + close(pipefd[1]); + + check_int("production.enabled.emit", + kzt_lazy_diagnostics_emit_production( + &pending, &binding_result, &emit_result), + 0); + check_int("production.enabled.status", emit_result.status, + KZT_LAZY_DIAGNOSTIC_EMIT_EMITTED); + + fflush(stderr); + check_true("production.restore", dup2(saved_stderr, STDERR_FILENO) >= 0); + close(saved_stderr); + + count = read(pipefd[0], output, sizeof(output) - 1); + check_true("production.read", count > 0); + if (count > 0) { + output[count] = '\0'; + check_contains("production.output.schema", output, "schema=1"); + check_contains("production.output.symbol", output, "symbol=realloc"); + } + close(pipefd[0]); +} + +int main(void) +{ + test_emit_disabled_skips_format_and_sink(); + test_native_applied_records_schema_one(); + test_slot_unchanged_uses_stable_status_name(); + test_missing_version_uses_stable_status_name(); + test_cas_mismatch_uses_final_competitor_target(); + test_production_emit_writes_stderr(); + + if (failures) { + fprintf(stderr, "kzt-wi256-lazy-diagnostics: %d failure(s)\n", + failures); + return 1; + } + + puts("KZT WI-256 lazy diagnostics: PASS"); + return 0; +} diff --git a/tests/unit/kzt/test_wi256_lazy_diagnostics_source_contract.py b/tests/unit/kzt/test_wi256_lazy_diagnostics_source_contract.py new file mode 100644 index 00000000000..7aa48daab30 --- /dev/null +++ b/tests/unit/kzt/test_wi256_lazy_diagnostics_source_contract.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +import pathlib +import re +import sys + + +def function_body(text: str, signature: str) -> str: + start = -1 + search_from = 0 + while True: + start = text.index(signature, search_from) + after_signature = start + len(signature) + brace = text.index("{", after_signature) + semicolon = text.find(";", after_signature, brace) + if semicolon < 0: + break + search_from = after_signature + depth = 0 + for pos in range(brace, len(text)): + if text[pos] == "{": + depth += 1 + elif text[pos] == "}": + depth -= 1 + if depth == 0: + return text[brace + 1:pos] + raise AssertionError(f"unterminated function: {signature}") + + +def braced_block(text: str, brace: int) -> tuple[str, int]: + depth = 0 + for pos in range(brace, len(text)): + if text[pos] == "{": + depth += 1 + elif text[pos] == "}": + depth -= 1 + if depth == 0: + return text[brace + 1:pos], pos + 1 + raise AssertionError("unterminated block") + + +root = pathlib.Path(sys.argv[1]).resolve() +main = (root / "linux-user/main.c").read_text(encoding="utf-8") +options = (root / "target/i386/latx/include/latx-options.h").read_text( + encoding="utf-8" +) +options_c = (root / "target/i386/latx/latx-options.c").read_text( + encoding="utf-8" +) +elfloader = (root / "target/i386/latx/context/elfloader.c").read_text( + encoding="utf-8" +) +diagnostics = ( + root / "target/i386/latx/context/kzt_lazy_diagnostics.c" +).read_text(encoding="utf-8") + +# The lazy diagnostics flag is independent and closed by default. +assert re.search(r"\bint\s+option_kzt_lazy_diagnostics\s*=\s*0\s*;", options_c) +assert not re.search(r"\bint\s+option_kzt_lazy_diagnostics\s*=", main) +assert re.search( + r'"LATX_KZT_LAZY_DIAGNOSTICS"\s*,\s*true\s*,\s*' + r'handle_arg_latx_kzt_lazy_diagnostics', + main, +) +assert "ENVFUN(LATX_KZT_LAZY_DIAGNOSTICS" in options +handler = function_body( + main, "static void handle_arg_latx_kzt_lazy_diagnostics(const char *arg)" +) +assert "option_kzt_lazy_diagnostics" in handler +assert "kzt_registry_diagnostics" not in handler +assert "option_kzt_lazy_diagnostics" not in function_body( + diagnostics, "int kzt_lazy_diagnostics_emit_production(" +) + +resolver = function_body( + elfloader, "static void KztLazyBindingCompleteResolver(void)" +) +armed_start = resolver.index("if (pending->armed)") +armed_brace = resolver.index("{", armed_start) +armed, _ = braced_block(resolver, armed_brace) + +# The option conditional is the sole diagnostics gate on the completion path. +gate = re.search(r"if\s*\(\s*option_kzt_lazy_diagnostics\s*\)\s*\{", armed) +assert gate is not None +enabled_brace = armed.index("{", gate.start()) +enabled, enabled_end = braced_block(armed, enabled_brace) +else_match = re.match(r"\s*else\s*\{", armed[enabled_end:]) +assert else_match is not None +disabled_brace = armed.index("{", enabled_end + else_match.start()) +disabled, _ = braced_block(armed, disabled_brace) + +assert "pending_snapshot = *pending;" in enabled +assert re.search( + r"pending_snapshot\.symbol\s*=\s*pending->symbol\s*\?\s*" + r"pending_snapshot\.symbol_storage\s*:\s*NULL\s*;", + enabled, + re.S, +) +assert re.search( + r"pending_snapshot\.version\s*=\s*pending->version\s*\?\s*" + r"pending_snapshot\.version_storage\s*:\s*NULL\s*;", + enabled, + re.S, +) +assert enabled.index("pending_snapshot = *pending;") < enabled.index( + "kzt_production_lazy_complete(" +) +assert enabled.index("kzt_production_lazy_complete(") < enabled.index( + "kzt_lazy_diagnostics_emit_production(" +) +assert re.search( + r"kzt_lazy_diagnostics_emit_production\s*\(\s*" + r"&pending_snapshot\s*,\s*&result\s*,", + enabled, + re.S, +) +assert "kzt_production_lazy_complete(" in disabled +for forbidden in ("pending_snapshot", "diagnostic", "emit"): + assert forbidden not in disabled + +# Record construction consumes only the pre-clear pending snapshot and the +# existing completion result; it must not rediscover or reroute the binding. +record = function_body(diagnostics, "int kzt_lazy_diagnostic_record_build(") +assert "record->slot_before = pending->unresolved_stub;" in record +assert "record->slot_after_guest = binding_result->slot_before;" in record +assert "record->selected_second_target = binding_result->slot_after;" in record +for forbidden in ( + "owner", + "provider", + "wrapper", + "bridge", + "planner", + "route_guest_target", + "compare_exchange", +): + assert forbidden not in record + +print("WI-256 lazy diagnostics source contract: PASS") diff --git a/tests/unit/kzt/test_wi256_lazy_production_bridge.c b/tests/unit/kzt/test_wi256_lazy_production_bridge.c new file mode 100644 index 00000000000..165c8c52fc5 --- /dev/null +++ b/tests/unit/kzt/test_wi256_lazy_production_bridge.c @@ -0,0 +1,1434 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "elf.h" +#include "target/i386/latx/include/box64context.h" +#include "target/i386/latx/include/bridge_private.h" +#include "target/i386/latx/include/elfloader_private.h" +#include "target/i386/latx/include/khash.h" +#include "target/i386/latx/include/kzt_guest_registry.h" +#include "target/i386/latx/include/kzt_jump_slot_production.h" +#include "target/i386/latx/include/kzt_rela_runtime_bridge.h" +#include "target/i386/latx/include/librarian_private.h" +#include "target/i386/latx/include/library.h" +#include "target/i386/latx/include/library_private.h" + +#define FIXTURE_SYMBOL "uname" +#define FIXTURE_VERSION "GLIBC_2.2.5" +#define SOURCE_LINK_MAP 0x1000 +#define PROVIDER_LINK_MAP 0x2000 +#define SOURCE_START 0x70000000 +#define GUEST_TARGET 0x71000020 +#define STRESS_ITERATIONS 1000 + +static int failures; +static uintptr_t fixture_native_symbol; +static uintptr_t fixture_native_bridge; +int relocation_log; +int kzt_registry_diagnostics; + +KHASH_MAP_IMPL_STR(symbolmap, wrapper_t) +KHASH_MAP_IMPL_STR(symbol2map, symbol2_t) + +#define CHECK(name, condition) do { \ + if (!(condition)) { \ + fprintf(stderr, "%s failed at line %d\n", name, __LINE__); \ + ++failures; \ + } \ +} while (0) + +typedef struct fixture_bridge_map { + void *native_symbol; + uintptr_t target; + onebridge_t *entry; + int add_calls; + int force_inexact_add; + int check_calls; +} fixture_bridge_map_t; + +typedef struct fixture_version_need { + Elf64_Verneed need; + Elf64_Vernaux aux; +} fixture_version_need_t; + +typedef struct fixture { + box64context_t context; + library_t provider; + lib_t scope; + library_t *scope_libraries[1]; + fixture_bridge_map_t bridge_map; + onebridge_t bridge_entry; + elfheader_t head; + elfheader_t *elfs[1]; + Elf64_Rela rela; + Elf64_Sym sym; + Elf64_Half versym; + fixture_version_need_t version_need; + uintptr_t slot; + kzt_lazy_binding_pending_t pending; + kzt_guest_object_observation_t source_observation; + kzt_guest_dynamic_view_t dynamic_view; +} fixture_t; + +typedef enum hook_mode { + HOOK_NONE = 0, + HOOK_RECYCLE_BEFORE_ACQUIRE, + HOOK_RETIRE_AFTER_CAS, + HOOK_RETIRE_AFTER_CAS_ROLLBACK, +} hook_mode_t; + +typedef struct retire_sync { + pthread_mutex_t lock; + pthread_cond_t cond; + fixture_t *fixture; + unsigned long generation; + int go; + int retire_called; + int retire_done; + int retire_result; + int unloading_observed; + int completed_inside_hook; +} retire_sync_t; + +static fixture_t *hook_fixture; +static retire_sync_t *hook_retire; +static hook_mode_t hook_mode; +static int before_acquire_calls; +static int source_memory_access_calls; +static int slot_load_calls; +static int after_cas_calls; +static int lease_seen_at_first_slot_load; +static int force_verify_mismatch; +static int shadow_run_calls; +static unsigned long recycled_generation; +static int permission_begin_calls; +static int permission_end_calls; +static int fail_permission_begin; +static int fail_permission_end; + +uintptr_t CheckBridged(bridge_t *bridge, void *fnc); +uintptr_t AddCheckBridge(bridge_t *bridge, wrapper_t wrapper, void *fnc, + int stack_bytes, const char *name); +const char *SymName(elfheader_t *head, Elf64_Sym *sym); +void kzt_jump_slot_production_test_before_source_lease_acquire(void); +void kzt_jump_slot_production_test_before_source_memory_access(void); +void kzt_jump_slot_production_test_before_slot_load(void); +void kzt_jump_slot_production_test_after_slot_load(uintptr_t *value); +void kzt_jump_slot_production_test_after_slot_cas(int exchanged); +void kzt_jump_slot_production_test_shadow_run(void); +int kzt_jump_slot_production_test_begin_slot_write( + uintptr_t slot_addr, kzt_patch_spike_permission_lease_t *lease); +int kzt_jump_slot_production_test_end_slot_write( + kzt_patch_spike_permission_lease_t *lease); + +static void fixture_iFp(uintptr_t fnc) +{ + (void)fnc; +} + +uintptr_t CheckBridged(bridge_t *bridge, void *fnc) +{ + fixture_bridge_map_t *map = (fixture_bridge_map_t *)bridge; + + if (!map) { + return 0; + } + ++map->check_calls; + return fnc == map->native_symbol ? map->target : 0; +} + +uintptr_t AddCheckBridge(bridge_t *bridge, wrapper_t wrapper, void *fnc, + int stack_bytes, const char *name) +{ + fixture_bridge_map_t *map = (fixture_bridge_map_t *)bridge; + uintptr_t target; + + (void)stack_bytes; + (void)name; + target = CheckBridged(bridge, fnc); + if (target || !map || !map->entry || !wrapper || !fnc) { + return target; + } + ++map->add_calls; + map->entry->CC = 0xCC; + map->entry->S = 'S'; + map->entry->C = 'C'; + map->entry->w = map->force_inexact_add ? NULL : wrapper; + map->entry->f = (uintptr_t)fnc; + map->entry->C3 = 0xC3; + map->native_symbol = fnc; + map->target = (uintptr_t)&map->entry->CC; + return map->target; +} + +void *GetNativeSymbolUnversionned(void *lib, const char *name) +{ + return lib && name ? dlsym(lib, name) : NULL; +} + +kzt_guest_registry_t *KztGuestRegistryForContext(box64context_t *context) +{ + return context ? context->kzt_guest_registry_context.registry : NULL; +} + +int KztGuestLibraryLookupForContext( + box64context_t *context, const kzt_guest_library_binding_key_t *key, + kzt_guest_library_handle_t *handle) +{ + return context ? kzt_guest_library_access_lookup( + &context->kzt_guest_library_access, key, handle) : + -1; +} + +kzt_patch_spike_guard_t *KztPatchSpikeGuardForContext( + box64context_t *context) +{ + return context ? &context->kzt_patch_spike_guard : NULL; +} + +const char *SymName(elfheader_t *head, Elf64_Sym *sym) +{ + (void)head; + (void)sym; + return FIXTURE_SYMBOL; +} + +const char *GetSymbolVersion(elfheader_t *head, int version) +{ + (void)head; + return version == 2 ? FIXTURE_VERSION : NULL; +} + +static kzt_guest_object_observation_t observation( + uintptr_t link_map, uintptr_t start, uintptr_t end, const char *name) +{ + return (kzt_guest_object_observation_t) { + .link_map_addr = link_map, + .load_bias = { start, KZT_GUEST_FIELD_OK }, + .dynamic_addr = { start + 0x1000, KZT_GUEST_FIELD_OK }, + .map_start = { start, KZT_GUEST_FIELD_OK }, + .map_end = { end, KZT_GUEST_FIELD_OK }, + .namespace_id = { 0, KZT_GUEST_FIELD_OK }, + .path = { name, KZT_GUEST_FIELD_OK }, + .soname = { name, KZT_GUEST_FIELD_OK }, + .dynamic_view_status = KZT_GUEST_FIELD_NOT_PARSED, + }; +} + +static kzt_guest_dynamic_field_t runtime_field(uintptr_t value) +{ + return (kzt_guest_dynamic_field_t) { + .present = 1, + .value = value, + .address_semantics = KZT_GUEST_DYNAMIC_RUNTIME_ADDRESS, + }; +} + +static kzt_guest_dynamic_field_t scalar_field(uint64_t value) +{ + return (kzt_guest_dynamic_field_t) { + .present = 1, + .value = value, + .address_semantics = KZT_GUEST_DYNAMIC_SCALAR, + }; +} + +static unsigned long observe_object( + kzt_guest_registry_t *registry, + const kzt_guest_object_observation_t *object) +{ + kzt_guest_object_snapshot_t *snapshot = NULL; + unsigned long generation = 0; + + CHECK("observe.add", kzt_guest_registry_observe(registry, object) == + KZT_GUEST_REGISTRY_ADDED); + CHECK("observe.find", kzt_guest_registry_find_by_link_map( + registry, object->link_map_addr, + &snapshot) == 0 && snapshot != NULL); + if (snapshot) { + generation = snapshot->generation; + } + kzt_guest_object_snapshot_free(snapshot); + return generation; +} + +static unsigned long fixture_publish_source(fixture_t *fixture) +{ + unsigned long generation = observe_object( + fixture->context.kzt_guest_registry_context.registry, + &fixture->source_observation); + + CHECK("source.dynamic", kzt_guest_registry_commit_dynamic_view( + fixture->context.kzt_guest_registry_context.registry, + SOURCE_LINK_MAP, + generation, &fixture->dynamic_view) == + KZT_GUEST_REGISTRY_UPDATED); + return generation; +} + +static void fixture_publish_resolver(fixture_t *fixture) +{ + kzt_guest_lazy_resolver_t resolver = { + .link_map_slot = SOURCE_START + 0x2000, + .resolver_slot = SOURCE_START + 0x2008, + .guest_link_map = SOURCE_LINK_MAP, + .guest_resolver = SOURCE_START + 0x3000, + }; + + fixture->pending.guest_resolver = resolver.guest_resolver; + CHECK("source.resolver", kzt_guest_registry_publish_lazy_resolver( + fixture->context.kzt_guest_registry_context.registry, + SOURCE_LINK_MAP, + fixture->pending.source_generation, 0, &resolver) == 0); +} + +static void reset_guard(fixture_t *fixture, int enabled) +{ + kzt_patch_spike_config_t config = { enabled, 1, 1 }; + kzt_patch_spike_guard_init(&fixture->context.kzt_patch_spike_guard, + &config); +} + +static int fixture_init(fixture_t *fixture) +{ + static char source_name[] = "librequester.so"; + static char provider_name[] = "libc.so.6"; + static char dynstr[] = "\0uname\0" FIXTURE_VERSION "\0KZT_BAD_VERSION\0"; + kzt_guest_object_observation_t provider_observation; + kzt_guest_library_binding_key_t provider_key; + khint_t map_key; + int inserted; + int initial_failures = failures; + + memset(fixture, 0, sizeof(*fixture)); + fixture->versym = 2; + fixture->slot = GUEST_TARGET; + fixture->context.kzt_guest_registry_context.registry = + kzt_guest_registry_init(); + CHECK("registry.init", + fixture->context.kzt_guest_registry_context.registry != NULL); + CHECK("binding.init", kzt_guest_library_access_init( + &fixture->context.kzt_guest_library_access) == 0); + if (!fixture->context.kzt_guest_registry_context.registry || + !fixture->context.kzt_guest_library_access.initialized) { + return -1; + } + reset_guard(fixture, 1); + + fixture->scope_libraries[0] = &fixture->provider; + fixture->scope.libraries = fixture->scope_libraries; + fixture->scope.libsz = 1; + fixture->scope.context = &fixture->context; + fixture->context.maplib = &fixture->scope; + fixture->provider.name = provider_name; + fixture->provider.path = provider_name; + fixture->provider.type = LIB_WRAPPED; + fixture->provider.active = 1; + fixture->provider.context = &fixture->context; + fixture->provider.priv.w.lib = dlopen( + "libc.so.6", RTLD_LAZY | RTLD_LOCAL); + CHECK("provider.dlopen", fixture->provider.priv.w.lib != NULL); + if (!fixture->provider.priv.w.lib) { + return -1; + } + dlerror(); + fixture_native_symbol = (uintptr_t)dlsym( + fixture->provider.priv.w.lib, FIXTURE_SYMBOL); + CHECK("provider.native", fixture_native_symbol != 0 && dlerror() == NULL); + + fixture->provider.symbolmap = kh_init(symbolmap); + CHECK("provider.map", fixture->provider.symbolmap != NULL); + if (!fixture->provider.symbolmap) { + return -1; + } + map_key = kh_put(symbolmap, fixture->provider.symbolmap, + FIXTURE_SYMBOL, &inserted); + CHECK("provider.map-entry", inserted != -1 && + map_key != kh_end(fixture->provider.symbolmap)); + kh_value(fixture->provider.symbolmap, map_key) = fixture_iFp; + fixture->bridge_entry.CC = 0xCC; + fixture->bridge_entry.S = 'S'; + fixture->bridge_entry.C = 'C'; + fixture->bridge_entry.w = fixture_iFp; + fixture->bridge_entry.f = fixture_native_symbol; + fixture->bridge_entry.C3 = 0xC3; + fixture_native_bridge = (uintptr_t)&fixture->bridge_entry.CC; + fixture->bridge_map.native_symbol = (void *)fixture_native_symbol; + fixture->bridge_map.target = fixture_native_bridge; + fixture->bridge_map.entry = &fixture->bridge_entry; + fixture->provider.priv.w.bridge = (bridge_t *)&fixture->bridge_map; + + fixture->source_observation = observation( + SOURCE_LINK_MAP, SOURCE_START, SOURCE_START + 0x10000, source_name); + provider_observation = observation( + PROVIDER_LINK_MAP, 0x71000000, 0x71010000, provider_name); + fixture->dynamic_view = (kzt_guest_dynamic_view_t) { + .dynamic_addr = SOURCE_START + 0x1000, + .load_bias = 0, + .status = KZT_GUEST_DYNAMIC_COMPLETE, + .entry_count = 1, + .has_null = 1, + }; + fixture->dynamic_view.jmprel = runtime_field( + (uintptr_t)&fixture->rela); + fixture->dynamic_view.pltrelsz = scalar_field(sizeof(fixture->rela)); + fixture->dynamic_view.pltrel = scalar_field(DT_RELA); + fixture->dynamic_view.symtab = runtime_field((uintptr_t)&fixture->sym); + fixture->dynamic_view.syment = scalar_field(sizeof(fixture->sym)); + fixture->dynamic_view.strtab = runtime_field((uintptr_t)dynstr); + fixture->dynamic_view.strsz = scalar_field(sizeof(dynstr)); + fixture->dynamic_view.versym = runtime_field((uintptr_t)&fixture->versym); + fixture->dynamic_view.verneed = runtime_field( + (uintptr_t)&fixture->version_need); + fixture->dynamic_view.verneednum = scalar_field(1); + fixture->sym.st_name = 1; + fixture->version_need.need.vn_version = 1; + fixture->version_need.need.vn_cnt = 1; + fixture->version_need.need.vn_aux = sizeof(Elf64_Verneed); + fixture->version_need.aux.vna_other = 2; + fixture->version_need.aux.vna_name = 7; + fixture->pending.source_generation = fixture_publish_source(fixture); + fixture_publish_resolver(fixture); + provider_key = (kzt_guest_library_binding_key_t) { + .link_map_addr = PROVIDER_LINK_MAP, + .generation = observe_object( + fixture->context.kzt_guest_registry_context.registry, + &provider_observation), + .namespace_id = 0, + .namespace_kind = KZT_GUEST_LIBRARY_NAMESPACE_MAIN, + }; + CHECK("provider.track", kzt_guest_library_track( + fixture->context.kzt_guest_library_access.bindings, + &fixture->provider) == 0); + CHECK("provider.pair", kzt_guest_library_note_exact_pair( + fixture->context.kzt_guest_library_access.bindings, + PROVIDER_LINK_MAP, &fixture->provider, + KZT_GUEST_LIBRARY_OBJECT_WRAPPED) == + KZT_GUEST_LIBRARY_BINDING_PENDING); + CHECK("provider.publish", kzt_guest_library_note_observation( + fixture->context.kzt_guest_library_access.bindings, + &provider_key) == KZT_GUEST_LIBRARY_BINDING_ADDED); + + fixture->rela.r_info = R_X86_64_JUMP_SLOT; + fixture->rela.r_offset = (uintptr_t)&fixture->slot; + fixture->head.name = source_name; + fixture->head.path = source_name; + fixture->head.memory = (char *)SOURCE_START; + fixture->head.memsz = 0x10000; + fixture->head.Dynamic = (Elf64_Dyn *)(SOURCE_START + 0x1000); + fixture->head.jmprel = (uintptr_t)&fixture->rela; + fixture->head.pltsz = sizeof(fixture->rela); + fixture->head.pltent = sizeof(fixture->rela); + fixture->head.DynSym = &fixture->sym; + fixture->head.VerSym = &fixture->versym; + fixture->head.self_link_map = SOURCE_LINK_MAP; + fixture->elfs[0] = &fixture->head; + fixture->context.elfs = fixture->elfs; + fixture->context.elfsize = 1; + + fixture->pending.armed = 1; + fixture->pending.context_id = (uintptr_t)&fixture->context; + fixture->pending.source_link_map = SOURCE_LINK_MAP; + fixture->pending.namespace_kind = KZT_GUEST_LIBRARY_NAMESPACE_MAIN; + fixture->pending.namespace_id = 0; + fixture->pending.slot_addr = (uintptr_t)&fixture->slot; + fixture->pending.unresolved_stub = GUEST_TARGET - 0x10; + fixture->pending.symbol = FIXTURE_SYMBOL; + fixture->pending.version_evidence = KZT_SYMBOL_VERSION_VERSIONED; + fixture->pending.version = FIXTURE_VERSION; + return failures == initial_failures ? 0 : -1; +} + +static void fixture_destroy(fixture_t *fixture) +{ + if (fixture->provider.symbolmap) { + kh_destroy(symbolmap, fixture->provider.symbolmap); + } + if (fixture->provider.priv.w.lib) { + dlclose(fixture->provider.priv.w.lib); + } + kzt_guest_library_access_destroy(&fixture->context.kzt_guest_library_access); + kzt_guest_registry_destroy( + &fixture->context.kzt_guest_registry_context.registry); +} + +static int fixture_route(fixture_t *fixture, + kzt_lazy_binding_route_result_t *result) +{ + return kzt_production_lazy_route_guest_target( + &fixture->context, &fixture->pending, GUEST_TARGET, result); +} + +static int fixture_eager_route(fixture_t *fixture, + uintptr_t expected_guest_target, + const char *version, + kzt_jump_slot_route_result_t *result) +{ + return kzt_production_jump_slot_route( + &fixture->context, &fixture->provider, fixture_native_bridge, + &fixture->head, 1, 0, &fixture->rela, (uint64_t *)&fixture->slot, + fixture->slot, 0, 0, FIXTURE_SYMBOL, version, 1, + expected_guest_target, fixture_native_bridge, result); +} + +static int fixture_eager_route_with_evidence( + fixture_t *fixture, uintptr_t expected_guest_target, + kzt_symbol_version_evidence_t version_evidence, const char *version, + kzt_jump_slot_route_result_t *result) +{ + return kzt_production_jump_slot_route_with_version_evidence( + &fixture->context, &fixture->provider, fixture_native_bridge, + &fixture->head, 1, 0, &fixture->rela, (uint64_t *)&fixture->slot, + fixture->slot, 0, 0, FIXTURE_SYMBOL, version_evidence, version, 1, + expected_guest_target, fixture_native_bridge, result); +} + +static void hooks_reset(fixture_t *fixture, hook_mode_t mode) +{ + hook_fixture = fixture; + hook_retire = NULL; + hook_mode = mode; + before_acquire_calls = 0; + source_memory_access_calls = 0; + slot_load_calls = 0; + after_cas_calls = 0; + lease_seen_at_first_slot_load = 0; + force_verify_mismatch = mode == HOOK_RETIRE_AFTER_CAS_ROLLBACK; + shadow_run_calls = 0; + recycled_generation = 0; + permission_begin_calls = 0; + permission_end_calls = 0; + fail_permission_begin = 0; + fail_permission_end = 0; +} + +int kzt_jump_slot_production_test_begin_slot_write( + uintptr_t slot_addr, kzt_patch_spike_permission_lease_t *lease) +{ + if (!lease || !slot_addr) { + return -1; + } + ++permission_begin_calls; + lease->checked = 1; + lease->guest_page = slot_addr & ~(uintptr_t)0xfff; + lease->guest_page_length = 0x1000; + lease->original_permissions = 5; + if (fail_permission_begin) { + return -1; + } + lease->write_enabled = 1; + return 0; +} + +int kzt_jump_slot_production_test_end_slot_write( + kzt_patch_spike_permission_lease_t *lease) +{ + if (!lease) { + return -1; + } + ++permission_end_calls; + return fail_permission_end ? -1 : 0; +} + +void kzt_jump_slot_production_test_before_source_lease_acquire(void) +{ + ++before_acquire_calls; + if (hook_mode != HOOK_RECYCLE_BEFORE_ACQUIRE || !hook_fixture) { + return; + } + CHECK("before.retire", kzt_guest_registry_retire( + hook_fixture->context.kzt_guest_registry_context.registry, + SOURCE_LINK_MAP, + hook_fixture->pending.source_generation) == 0); + recycled_generation = fixture_publish_source(hook_fixture); + hook_mode = HOOK_NONE; +} + +void kzt_jump_slot_production_test_before_source_memory_access(void) +{ + ++source_memory_access_calls; +} + +void kzt_jump_slot_production_test_before_slot_load(void) +{ + if (slot_load_calls == 0 && hook_fixture) { + kzt_guest_object_snapshot_t *snapshot = NULL; + + if (kzt_guest_registry_find_by_link_map( + hook_fixture->context.kzt_guest_registry_context.registry, + SOURCE_LINK_MAP, + &snapshot) == 0 && snapshot && + snapshot->active_source_leases == 1) { + lease_seen_at_first_slot_load = 1; + } + kzt_guest_object_snapshot_free(snapshot); + } + ++slot_load_calls; +} + +void kzt_jump_slot_production_test_after_slot_load(uintptr_t *value) +{ + if (force_verify_mismatch && slot_load_calls == 3 && value) { + *value ^= 0x10; + force_verify_mismatch = 0; + } +} + +static void *retire_worker(void *opaque) +{ + retire_sync_t *sync = opaque; + int result; + + pthread_mutex_lock(&sync->lock); + while (!sync->go) { + pthread_cond_wait(&sync->cond, &sync->lock); + } + sync->retire_called = 1; + pthread_cond_broadcast(&sync->cond); + pthread_mutex_unlock(&sync->lock); + + result = kzt_guest_registry_retire( + sync->fixture->context.kzt_guest_registry_context.registry, + SOURCE_LINK_MAP, + sync->generation); + pthread_mutex_lock(&sync->lock); + sync->retire_result = result; + sync->retire_done = 1; + pthread_cond_broadcast(&sync->cond); + pthread_mutex_unlock(&sync->lock); + return NULL; +} + +void kzt_jump_slot_production_test_after_slot_cas(int exchanged) +{ + struct timespec start; + retire_sync_t *sync = hook_retire; + + ++after_cas_calls; + if (!exchanged || + (hook_mode != HOOK_RETIRE_AFTER_CAS && + hook_mode != HOOK_RETIRE_AFTER_CAS_ROLLBACK) || !sync) { + return; + } + hook_mode = HOOK_NONE; + pthread_mutex_lock(&sync->lock); + sync->go = 1; + pthread_cond_broadcast(&sync->cond); + while (!sync->retire_called) { + pthread_cond_wait(&sync->cond, &sync->lock); + } + pthread_mutex_unlock(&sync->lock); + + clock_gettime(CLOCK_MONOTONIC, &start); + for (;;) { + kzt_guest_object_snapshot_t *snapshot = NULL; + struct timespec now; + int done; + + if (kzt_guest_registry_find_by_link_map( + sync->fixture->context.kzt_guest_registry_context.registry, + SOURCE_LINK_MAP, + &snapshot) != 0) { + pthread_mutex_lock(&sync->lock); + done = sync->retire_done; + sync->unloading_observed = !done; + sync->completed_inside_hook = done; + pthread_mutex_unlock(&sync->lock); + kzt_guest_object_snapshot_free(snapshot); + break; + } + kzt_guest_object_snapshot_free(snapshot); + pthread_mutex_lock(&sync->lock); + done = sync->retire_done; + pthread_mutex_unlock(&sync->lock); + if (done) { + pthread_mutex_lock(&sync->lock); + sync->completed_inside_hook = 1; + pthread_mutex_unlock(&sync->lock); + break; + } + clock_gettime(CLOCK_MONOTONIC, &now); + if (now.tv_sec - start.tv_sec >= 2) { + break; + } + sched_yield(); + } +} + +void kzt_jump_slot_production_test_shadow_run(void) +{ + ++shadow_run_calls; +} + +static int retire_sync_init(retire_sync_t *sync, fixture_t *fixture) +{ + memset(sync, 0, sizeof(*sync)); + sync->fixture = fixture; + sync->generation = fixture->pending.source_generation; + if (pthread_mutex_init(&sync->lock, NULL) != 0) { + return -1; + } + if (pthread_cond_init(&sync->cond, NULL) != 0) { + pthread_mutex_destroy(&sync->lock); + return -1; + } + return 0; +} + +static void retire_sync_destroy(retire_sync_t *sync) +{ + pthread_cond_destroy(&sync->cond); + pthread_mutex_destroy(&sync->lock); +} + +static void test_successful_production_route(void) +{ + fixture_t fixture; + kzt_lazy_binding_route_result_t result; + + if (fixture_init(&fixture) != 0) { + fixture_destroy(&fixture); + return; + } + hooks_reset(&fixture, HOOK_NONE); + CHECK("success.route", fixture_route(&fixture, &result) == 0); + CHECK("success.applied", + result.status == KZT_LAZY_BINDING_ROUTE_NATIVE_APPLIED); + CHECK("success.slot", fixture.slot == fixture_native_bridge); + CHECK("success.lease", before_acquire_calls == 1); + fixture_destroy(&fixture); +} + +static void test_guest_version_is_not_used_for_host_lookup(void) +{ + fixture_t fixture; + kzt_wrapper_bridge_provider_t provider; + + if (fixture_init(&fixture) != 0) { + fixture_destroy(&fixture); + return; + } + CHECK("version-different.prepare", + kzt_rela_runtime_wrapper_provider_discover( + &fixture.context, &fixture.provider, FIXTURE_SYMBOL, + FIXTURE_VERSION, &provider) == 1); + CHECK("version-different.guest-version-kept", + provider.manifest.entry_count == 1 && + !strcmp(provider.entry.symbol_version, FIXTURE_VERSION)); + CHECK("version-different.native-provider-symbol", + provider.entry.native_symbol == fixture_native_symbol); + fixture_destroy(&fixture); +} + +static void test_native_symbol_missing_fails_open(void) +{ + fixture_t fixture; + kzt_wrapper_bridge_provider_t provider; + khint_t key; + int inserted; + + if (fixture_init(&fixture) != 0) { + fixture_destroy(&fixture); + return; + } + key = kh_put(symbolmap, fixture.provider.symbolmap, + "kzt_native_symbol_missing", &inserted); + CHECK("native-missing.map", inserted != -1 && + key != kh_end(fixture.provider.symbolmap)); + if (key != kh_end(fixture.provider.symbolmap)) { + kh_value(fixture.provider.symbolmap, key) = fixture_iFp; + } + CHECK("native-missing.prepare", + kzt_rela_runtime_wrapper_provider_discover( + &fixture.context, &fixture.provider, + "kzt_native_symbol_missing", FIXTURE_VERSION, + &provider) == 0); + CHECK("native-missing.no-manifest", provider.manifest.available == 0); + CHECK("native-missing.no-add", fixture.bridge_map.add_calls == 0); + fixture_destroy(&fixture); +} + +static void test_dependency_symbol_owner_mismatch_fails_open(void) +{ + fixture_t fixture; + kzt_wrapper_bridge_provider_t provider; + void *libc_handle; + void *libm_handle; + void *dependency_symbol; + struct link_map *handle_map = NULL; + struct link_map *symbol_map = NULL; + Dl_info symbol_info; + khint_t key; + int inserted; + + if (fixture_init(&fixture) != 0) { + fixture_destroy(&fixture); + return; + } + libm_handle = dlopen("libm.so.6", RTLD_LAZY | RTLD_LOCAL); + CHECK("owner-mismatch.libm", libm_handle != NULL); + if (!libm_handle) { + fixture_destroy(&fixture); + return; + } + dependency_symbol = dlsym(libm_handle, "malloc"); + CHECK("owner-mismatch.dependency-symbol", dependency_symbol != NULL); + CHECK("owner-mismatch.precondition", + dependency_symbol && + dlinfo(libm_handle, RTLD_DI_LINKMAP, &handle_map) == 0 && + dladdr1(dependency_symbol, &symbol_info, (void **)&symbol_map, + RTLD_DL_LINKMAP) != 0 && + handle_map && symbol_map && handle_map != symbol_map); + key = kh_put(symbolmap, fixture.provider.symbolmap, "malloc", &inserted); + CHECK("owner-mismatch.map", inserted != -1 && + key != kh_end(fixture.provider.symbolmap)); + if (key != kh_end(fixture.provider.symbolmap)) { + kh_value(fixture.provider.symbolmap, key) = fixture_iFp; + } + libc_handle = fixture.provider.priv.w.lib; + fixture.provider.priv.w.lib = libm_handle; + CHECK("owner-mismatch.prepare", + kzt_rela_runtime_wrapper_provider_discover( + &fixture.context, &fixture.provider, "malloc", + FIXTURE_VERSION, &provider) == 0); + CHECK("owner-mismatch.no-manifest", provider.manifest.available == 0); + CHECK("owner-mismatch.no-add", fixture.bridge_map.add_calls == 0); + fixture.provider.priv.w.lib = libc_handle; + dlclose(libm_handle); + fixture_destroy(&fixture); +} + +static void test_missing_bridge_is_created_from_exact_provider(void) +{ + fixture_t fixture; + kzt_lazy_binding_route_result_t result; + + if (fixture_init(&fixture) != 0) { + fixture_destroy(&fixture); + return; + } + fixture.bridge_map.target = 0; + hooks_reset(&fixture, HOOK_NONE); + CHECK("create-bridge.route", fixture_route(&fixture, &result) == 0); + CHECK("create-bridge.applied", + result.status == KZT_LAZY_BINDING_ROUTE_NATIVE_APPLIED); + CHECK("create-bridge.add-once", fixture.bridge_map.add_calls == 1); + CHECK("create-bridge.slot", fixture.slot == fixture_native_bridge); + CHECK("create-bridge.exact", + CheckBridged(fixture.provider.priv.w.bridge, + (void *)fixture_native_symbol) == + fixture_native_bridge); + fixture_destroy(&fixture); +} + +static void test_created_inexact_bridge_fails_open(void) +{ + fixture_t fixture; + kzt_wrapper_bridge_provider_t provider; + kzt_wrapper_probe_request_t request = { + .symbol_name = FIXTURE_SYMBOL, + .symbol_version = FIXTURE_VERSION, + }; + kzt_wrapper_probe_result_t result; + + if (fixture_init(&fixture) != 0) { + fixture_destroy(&fixture); + return; + } + fixture.bridge_map.target = 0; + fixture.bridge_map.force_inexact_add = 1; + CHECK("inexact-add.prepare", + kzt_rela_runtime_wrapper_provider_discover( + &fixture.context, &fixture.provider, FIXTURE_SYMBOL, + FIXTURE_VERSION, &provider) == 1); + CHECK("inexact-add.probe", + kzt_wrapper_probe_minimal_manifest( + &provider.manifest, &request, &provider.bridge_ops, + &result) == 0); + CHECK("inexact-add.no-bridge", result.bridge_target == 0); + CHECK("inexact-add.add-once", fixture.bridge_map.add_calls == 1); + fixture_destroy(&fixture); +} + +static void test_eager_production_request_evidence(void) +{ + fixture_t fixture; + kzt_jump_slot_route_result_t result; + + if (fixture_init(&fixture) != 0) { + fixture_destroy(&fixture); + return; + } + + hooks_reset(&fixture, HOOK_NONE); + kzt_registry_diagnostics = 0; + CHECK("eager.owner-match.call", fixture_eager_route( + &fixture, GUEST_TARGET, FIXTURE_VERSION, &result) == 0); + CHECK("eager.owner-match.native", + result.status == KZT_JUMP_SLOT_ROUTE_NATIVE_APPLIED && + fixture.slot == fixture_native_bridge && + result.legacy_fallback_attempted == 0); + CHECK("eager.diagnostics-off-fast", shadow_run_calls == 0); + + fixture.slot = GUEST_TARGET; + reset_guard(&fixture, 1); + hooks_reset(&fixture, HOOK_NONE); + kzt_registry_diagnostics = 1; + CHECK("eager.shadow.call", fixture_eager_route( + &fixture, GUEST_TARGET, FIXTURE_VERSION, &result) == 0); + CHECK("eager.shadow.real-route", + result.status == KZT_JUMP_SLOT_ROUTE_NATIVE_APPLIED && + shadow_run_calls == 1); + + fixture.slot = GUEST_TARGET; + reset_guard(&fixture, 1); + hooks_reset(&fixture, HOOK_NONE); + CHECK("eager.owner-mismatch.call", fixture_eager_route( + &fixture, SOURCE_START + 0x20, FIXTURE_VERSION, &result) == 0); + CHECK("eager.owner-mismatch.legacy", + result.status == KZT_JUMP_SLOT_ROUTE_LEGACY_APPLIED && + result.legacy_fallback_attempted == 1 && + fixture.slot == fixture_native_bridge); + + fixture.slot = 0xdeadbeef; + reset_guard(&fixture, 1); + hooks_reset(&fixture, HOOK_NONE); + CHECK("eager.owner-unknown.call", fixture_eager_route( + &fixture, 0xdeadbeef, FIXTURE_VERSION, &result) == 0); + CHECK("eager.owner-unknown.legacy", + result.status == KZT_JUMP_SLOT_ROUTE_LEGACY_APPLIED && + result.legacy_fallback_attempted == 1 && + fixture.slot == fixture_native_bridge); + + fixture.slot = GUEST_TARGET; + reset_guard(&fixture, 1); + hooks_reset(&fixture, HOOK_NONE); + CHECK("eager.wrapper-version.call", fixture_eager_route( + &fixture, GUEST_TARGET, "KZT_UNSUPPORTED_VERSION", &result) == 0); + CHECK("eager.wrapper-version.legacy", + result.status == KZT_JUMP_SLOT_ROUTE_LEGACY_APPLIED && + result.legacy_fallback_attempted == 1 && + fixture.slot == fixture_native_bridge); + + fixture.slot = GUEST_TARGET; + reset_guard(&fixture, 1); + hooks_reset(&fixture, HOOK_NONE); + fixture.version_need.aux.vna_name = 20; + CHECK("eager.runtime-version.call", fixture_eager_route( + &fixture, GUEST_TARGET, FIXTURE_VERSION, &result) == 0); + CHECK("eager.runtime-version.legacy-once", + result.status == KZT_JUMP_SLOT_ROUTE_LEGACY_APPLIED && + result.legacy_fallback_attempted == 1 && after_cas_calls == 1 && + fixture.slot == fixture_native_bridge); + fixture.version_need.aux.vna_name = 7; + + fixture.slot = GUEST_TARGET; + reset_guard(&fixture, 1); + hooks_reset(&fixture, HOOK_RECYCLE_BEFORE_ACQUIRE); + CHECK("eager.generation-race.call", fixture_eager_route( + &fixture, GUEST_TARGET, FIXTURE_VERSION, &result) == 0); + CHECK("eager.generation-race.legacy-once", + recycled_generation > fixture.pending.source_generation && + result.status == KZT_JUMP_SLOT_ROUTE_LEGACY_APPLIED && + result.legacy_fallback_attempted == 1 && after_cas_calls == 1 && + fixture.slot == fixture_native_bridge); + kzt_registry_diagnostics = 0; + fixture_destroy(&fixture); +} + +static void fixture_set_confirmed_unversioned(fixture_t *fixture) +{ + fixture->dynamic_view.versym.present = 0; + fixture->dynamic_view.verneed.present = 0; + fixture->dynamic_view.verneednum.present = 0; + fixture->head.VerSym = NULL; + fixture->pending.version_evidence = + KZT_SYMBOL_VERSION_CONFIRMED_UNVERSIONED; + fixture->pending.version = NULL; + CHECK("unversioned.dynamic-view", kzt_guest_registry_commit_dynamic_view( + fixture->context.kzt_guest_registry_context.registry, + SOURCE_LINK_MAP, fixture->pending.source_generation, + &fixture->dynamic_view) != KZT_GUEST_REGISTRY_ERROR); +} + +static void test_confirmed_unversioned_production_paths_apply(void) +{ + fixture_t fixture; + kzt_lazy_binding_route_result_t lazy_result; + kzt_jump_slot_route_result_t eager_result; + + if (fixture_init(&fixture) != 0) { + fixture_destroy(&fixture); + return; + } + fixture_set_confirmed_unversioned(&fixture); + hooks_reset(&fixture, HOOK_NONE); + CHECK("unversioned-lazy.route", + fixture_route(&fixture, &lazy_result) == 0); + CHECK("unversioned-lazy.applied", + lazy_result.status == KZT_LAZY_BINDING_ROUTE_NATIVE_APPLIED && + fixture.slot == fixture_native_bridge); + + fixture.slot = GUEST_TARGET; + reset_guard(&fixture, 1); + hooks_reset(&fixture, HOOK_NONE); + CHECK("unversioned-eager.route", + fixture_eager_route_with_evidence( + &fixture, GUEST_TARGET, + KZT_SYMBOL_VERSION_CONFIRMED_UNVERSIONED, NULL, + &eager_result) == 0); + CHECK("unversioned-eager.applied", + eager_result.status == KZT_JUMP_SLOT_ROUTE_NATIVE_APPLIED && + eager_result.legacy_fallback_attempted == 0 && + fixture.slot == fixture_native_bridge); + fixture_destroy(&fixture); +} + +static void test_unknown_version_evidence_preserves_guest_path(void) +{ + fixture_t fixture; + kzt_jump_slot_route_result_t result; + + if (fixture_init(&fixture) != 0) { + fixture_destroy(&fixture); + return; + } + fixture_set_confirmed_unversioned(&fixture); + hooks_reset(&fixture, HOOK_NONE); + CHECK("unknown-eager.route", + fixture_eager_route_with_evidence( + &fixture, GUEST_TARGET, KZT_SYMBOL_VERSION_UNKNOWN, NULL, + &result) == 0); + CHECK("unknown-eager.legacy", + result.status == KZT_JUMP_SLOT_ROUTE_LEGACY_APPLIED && + result.legacy_fallback_attempted == 1 && + fixture.slot == fixture_native_bridge); + fixture_destroy(&fixture); +} + +static void test_production_permission_transaction_preserves_on_restore_error(void) +{ + fixture_t fixture; + kzt_lazy_binding_route_result_t result; + + if (fixture_init(&fixture) != 0) { + fixture_destroy(&fixture); + return; + } + hooks_reset(&fixture, HOOK_NONE); + fail_permission_end = 1; + CHECK("permission.route", fixture_route(&fixture, &result) == 0); + CHECK("permission.begin", permission_begin_calls == 1); + CHECK("permission.end", permission_end_calls == 1); + CHECK("permission.no-legacy-mask", + result.status == KZT_LAZY_BINDING_ROUTE_GUEST_PRESERVED); + CHECK("permission.no-second-write", after_cas_calls == 1); + CHECK("permission.circuit", + kzt_patch_spike_guard_circuit_open( + &fixture.context.kzt_patch_spike_guard) == 1); + fixture_destroy(&fixture); +} + +static void test_recycle_before_acquire_preserves_guest(void) +{ + fixture_t fixture; + kzt_lazy_binding_route_result_t result; + unsigned long old_generation; + + if (fixture_init(&fixture) != 0) { + fixture_destroy(&fixture); + return; + } + old_generation = fixture.pending.source_generation; + hooks_reset(&fixture, HOOK_RECYCLE_BEFORE_ACQUIRE); + CHECK("before.route", fixture_route(&fixture, &result) == 0); + CHECK("before.recycled", recycled_generation > old_generation); + CHECK("before.preserved", + result.status == KZT_LAZY_BINDING_ROUTE_GUEST_PRESERVED); + CHECK("before.slot", fixture.slot == GUEST_TARGET); + CHECK("before.no-source-memory", source_memory_access_calls == 0); + CHECK("before.no-slot-read", slot_load_calls == 0); + fixture_destroy(&fixture); +} + +static void test_retire_before_first_slot_read_skips_guest_memory(void) +{ + fixture_t fixture; + kzt_guest_registry_source_lease_t source_lease = { 0 }; + uintptr_t value = 0; + unsigned long old_generation; + + if (fixture_init(&fixture) != 0) { + fixture_destroy(&fixture); + return; + } + old_generation = fixture.pending.source_generation; + hooks_reset(&fixture, HOOK_RECYCLE_BEFORE_ACQUIRE); + CHECK("first-read.load-rejected", + kzt_production_lazy_load_slot_with_lease( + &fixture.context, &fixture.pending, + (uintptr_t)&fixture.slot, &value, &source_lease) != 0); + CHECK("first-read.recycled", recycled_generation > old_generation); + CHECK("first-read.no-lease", source_lease.active == 0); + CHECK("first-read.no-slot-access", slot_load_calls == 0); + CHECK("first-read.slot-untouched", fixture.slot == GUEST_TARGET); + fixture_destroy(&fixture); +} + +static void test_namespace_mismatch_precedes_any_source_read(void) +{ + fixture_t fixture; + kzt_lazy_binding_result_t result; + + if (fixture_init(&fixture) != 0) { + fixture_destroy(&fixture); + return; + } + fixture.pending.namespace_id = 1; + fixture.pending.namespace_kind = KZT_GUEST_LIBRARY_NAMESPACE_EXPLICIT; + hooks_reset(&fixture, HOOK_NONE); + CHECK("namespace.complete", kzt_production_lazy_complete( + &fixture.context, &fixture.pending, &result) == 0); + CHECK("namespace.preserved", + result.status == KZT_LAZY_BINDING_GUEST_PRESERVED); + CHECK("namespace.reason", + result.reason == KZT_LAZY_BINDING_REASON_SLOT_READ_ERROR); + CHECK("namespace.real-result-empty", result.slot_before == 0 && + result.slot_after == 0 && result.selected_target == 0); + CHECK("namespace.pending-consumed", fixture.pending.armed == 0 && + result.pending_cleared == 1); + CHECK("namespace.no-lease-hook", before_acquire_calls == 0); + CHECK("namespace.no-source-memory", source_memory_access_calls == 0); + CHECK("namespace.no-slot-read", slot_load_calls == 0); + CHECK("namespace.no-memory-change", fixture.slot == GUEST_TARGET); + fixture_destroy(&fixture); +} + +typedef enum guard_fail_open_mode { + GUARD_FAIL_OPEN_DISABLED = 0, + GUARD_FAIL_OPEN_CIRCUIT, + GUARD_FAIL_OPEN_BUDGET_ZERO, +} guard_fail_open_mode_t; + +static void run_completion_guard_fail_open(guard_fail_open_mode_t mode, + const char *prefix) +{ + fixture_t fixture; + kzt_lazy_binding_result_t result; + kzt_patch_spike_config_t config = { 0 }; + + if (fixture_init(&fixture) != 0) { + fixture_destroy(&fixture); + return; + } + switch (mode) { + case GUARD_FAIL_OPEN_DISABLED: + /* Matches the real verifier default-options all-zero guard. */ + break; + case GUARD_FAIL_OPEN_CIRCUIT: + config = (kzt_patch_spike_config_t) { 1, 1, 1 }; + break; + case GUARD_FAIL_OPEN_BUDGET_ZERO: + config = (kzt_patch_spike_config_t) { 1, 1, 0 }; + break; + } + kzt_patch_spike_guard_init(&fixture.context.kzt_patch_spike_guard, + &config); + if (mode == GUARD_FAIL_OPEN_CIRCUIT) { + fixture.context.kzt_patch_spike_guard.circuit_open = 1; + } + hooks_reset(&fixture, HOOK_NONE); + + CHECK(prefix, kzt_production_lazy_complete( + &fixture.context, &fixture.pending, &result) == 0); + CHECK(prefix, result.status == KZT_LAZY_BINDING_GUEST_PRESERVED); + CHECK(prefix, result.reason == KZT_LAZY_BINDING_REASON_NATIVE_UNAVAILABLE); + CHECK(prefix, result.slot_before == GUEST_TARGET); + CHECK(prefix, result.slot_after == GUEST_TARGET); + CHECK(prefix, result.selected_target == GUEST_TARGET); + CHECK(prefix, fixture.slot == GUEST_TARGET); + CHECK(prefix, fixture.context.kzt_patch_spike_guard.write_attempts == 0 && + fixture.context.kzt_patch_spike_guard.write_successes == 0); + CHECK(prefix, before_acquire_calls == 1); + CHECK(prefix, lease_seen_at_first_slot_load == 1); + CHECK(prefix, slot_load_calls > 0 && source_memory_access_calls > 0); + CHECK(prefix, fixture.pending.armed == 0 && result.pending_cleared == 1); + fixture_destroy(&fixture); +} + +static void test_guard_fail_open_keeps_real_completion_evidence(void) +{ + run_completion_guard_fail_open(GUARD_FAIL_OPEN_DISABLED, + "guard-disabled-completion"); + run_completion_guard_fail_open(GUARD_FAIL_OPEN_CIRCUIT, + "guard-circuit-completion"); + run_completion_guard_fail_open(GUARD_FAIL_OPEN_BUDGET_ZERO, + "guard-budget-completion"); +} + +static void run_after_cas_retire(fixture_t *fixture, const char *prefix) +{ + retire_sync_t sync; + pthread_t thread; + kzt_lazy_binding_route_result_t result; + int created; + + if (retire_sync_init(&sync, fixture) != 0) { + CHECK(prefix, 0); + return; + } + hooks_reset(fixture, HOOK_RETIRE_AFTER_CAS); + hook_retire = &sync; + created = pthread_create(&thread, NULL, retire_worker, &sync); + CHECK(prefix, created == 0); + if (created != 0) { + retire_sync_destroy(&sync); + return; + } + CHECK(prefix, fixture_route(fixture, &result) == 0); + CHECK(prefix, pthread_join(thread, NULL) == 0); + CHECK(prefix, sync.unloading_observed == 1); + CHECK(prefix, sync.completed_inside_hook == 0); + CHECK(prefix, sync.retire_done == 1 && sync.retire_result == 0); + CHECK(prefix, result.status == KZT_LAZY_BINDING_ROUTE_NATIVE_APPLIED); + CHECK(prefix, fixture->slot == fixture_native_bridge); + retire_sync_destroy(&sync); +} + +static void test_retire_waits_for_writer_transaction(void) +{ + fixture_t fixture; + + if (fixture_init(&fixture) != 0) { + fixture_destroy(&fixture); + return; + } + run_after_cas_retire(&fixture, "after-cas"); + fixture_destroy(&fixture); +} + +static void test_completion_holds_one_lease_until_final_release(void) +{ + fixture_t fixture; + retire_sync_t sync; + pthread_t thread; + kzt_lazy_binding_result_t result; + int created; + + if (fixture_init(&fixture) != 0) { + fixture_destroy(&fixture); + return; + } + if (retire_sync_init(&sync, &fixture) != 0) { + CHECK("completion.sync", 0); + fixture_destroy(&fixture); + return; + } + hooks_reset(&fixture, HOOK_RETIRE_AFTER_CAS); + hook_retire = &sync; + created = pthread_create(&thread, NULL, retire_worker, &sync); + CHECK("completion.thread", created == 0); + if (created != 0) { + retire_sync_destroy(&sync); + fixture_destroy(&fixture); + return; + } + CHECK("completion.call", kzt_production_lazy_complete( + &fixture.context, &fixture.pending, &result) == 0); + CHECK("completion.join", pthread_join(thread, NULL) == 0); + CHECK("completion.first-read-leased", lease_seen_at_first_slot_load == 1); + CHECK("completion.single-acquire", before_acquire_calls == 1); + CHECK("completion.retire-blocked", sync.unloading_observed == 1 && + sync.completed_inside_hook == 0); + CHECK("completion.release-allows-retire", sync.retire_done == 1 && + sync.retire_result == 0); + CHECK("completion.applied", result.status == KZT_LAZY_BINDING_NATIVE_APPLIED); + CHECK("completion.pending-cleared", fixture.pending.armed == 0); + retire_sync_destroy(&sync); + fixture_destroy(&fixture); +} + +static void test_retire_waits_through_verify_failure_and_rollback(void) +{ + fixture_t fixture; + retire_sync_t sync; + pthread_t thread; + kzt_lazy_binding_route_result_t result; + int created; + + if (fixture_init(&fixture) != 0) { + fixture_destroy(&fixture); + return; + } + if (retire_sync_init(&sync, &fixture) != 0) { + CHECK("rollback.sync", 0); + fixture_destroy(&fixture); + return; + } + hooks_reset(&fixture, HOOK_RETIRE_AFTER_CAS_ROLLBACK); + hook_retire = &sync; + created = pthread_create(&thread, NULL, retire_worker, &sync); + CHECK("rollback.thread", created == 0); + if (created != 0) { + retire_sync_destroy(&sync); + fixture_destroy(&fixture); + return; + } + CHECK("rollback.route", fixture_route(&fixture, &result) == 0); + CHECK("rollback.join", pthread_join(thread, NULL) == 0); + CHECK("rollback.retire-blocked", sync.unloading_observed == 1 && + sync.completed_inside_hook == 0); + CHECK("rollback.two-cas", after_cas_calls == 2); + CHECK("rollback.slot-restored", fixture.slot == GUEST_TARGET); + CHECK("rollback.guest-preserved", + result.status == KZT_LAZY_BINDING_ROUTE_GUEST_PRESERVED); + CHECK("rollback.retire-after-release", sync.retire_done == 1 && + sync.retire_result == 0); + retire_sync_destroy(&sync); + fixture_destroy(&fixture); +} + +static void test_fail_open_and_non_writer_paths(void) +{ + fixture_t fixture; + kzt_lazy_binding_route_result_t result; + kzt_lazy_binding_result_t completion_result; + kzt_guest_registry_t *live_registry; + kzt_guest_registry_t *disabled_registry; + unsigned long live_generation; + + if (fixture_init(&fixture) != 0) { + fixture_destroy(&fixture); + return; + } + live_registry = fixture.context.kzt_guest_registry_context.registry; + live_generation = fixture.pending.source_generation; + + hooks_reset(&fixture, HOOK_NONE); + fixture.context.kzt_guest_registry_context.registry = NULL; + CHECK("null.route", fixture_route(&fixture, &result) == 0); + CHECK("null.preserved", result.status == + KZT_LAZY_BINDING_ROUTE_GUEST_PRESERVED && + fixture.slot == GUEST_TARGET); + CHECK("null.no-lease", before_acquire_calls == 0); + fixture.context.kzt_guest_registry_context.registry = live_registry; + + kzt_guest_registry_test_set_alloc_failure_after(1); + disabled_registry = kzt_guest_registry_init(); + kzt_guest_registry_test_set_alloc_failure_after(-1); + CHECK("disabled.registry", disabled_registry != NULL); + hooks_reset(&fixture, HOOK_NONE); + fixture.context.kzt_guest_registry_context.registry = disabled_registry; + CHECK("disabled.route", fixture_route(&fixture, &result) == 0); + CHECK("disabled.preserved", result.status == + KZT_LAZY_BINDING_ROUTE_GUEST_PRESERVED && + fixture.slot == GUEST_TARGET); + CHECK("disabled.lease-attempt", before_acquire_calls == 1); + CHECK("disabled.no-source-memory", source_memory_access_calls == 0); + CHECK("disabled.no-slot-read", slot_load_calls == 0); + fixture.context.kzt_guest_registry_context.registry = live_registry; + kzt_guest_registry_destroy(&disabled_registry); + + hooks_reset(&fixture, HOOK_NONE); + fixture.pending.source_generation = live_generation + 1; + CHECK("stale.route", fixture_route(&fixture, &result) == 0); + CHECK("stale.preserved", result.status == + KZT_LAZY_BINDING_ROUTE_GUEST_PRESERVED && + fixture.slot == GUEST_TARGET); + CHECK("stale.lease-attempt", before_acquire_calls == 1); + CHECK("stale.no-source-memory", source_memory_access_calls == 0); + CHECK("stale.no-slot-read", slot_load_calls == 0); + fixture.pending.source_generation = live_generation; + + hooks_reset(&fixture, HOOK_NONE); + reset_guard(&fixture, 0); + CHECK("disabled-guard.route", fixture_route(&fixture, &result) == 0); + CHECK("disabled-guard.exact-lease", before_acquire_calls == 1); + CHECK("disabled-guard.source-validated", source_memory_access_calls > 0); + CHECK("disabled-guard.slot", fixture.slot == GUEST_TARGET); + + reset_guard(&fixture, 1); + hooks_reset(&fixture, HOOK_NONE); + CHECK("bound.first", fixture_route(&fixture, &result) == 0); + CHECK("bound.first-applied", fixture.slot == fixture_native_bridge); + hooks_reset(&fixture, HOOK_NONE); + CHECK("bound.second", kzt_production_lazy_complete( + &fixture.context, &fixture.pending, &completion_result) == 0); + CHECK("bound.revalidation-lease", before_acquire_calls == 1); + CHECK("bound.second-result", + completion_result.status == KZT_LAZY_BINDING_GUEST_PRESERVED && + completion_result.reason == + KZT_LAZY_BINDING_REASON_NATIVE_UNAVAILABLE); + CHECK("bound.second-values", + completion_result.slot_before == fixture_native_bridge && + completion_result.slot_after == fixture_native_bridge && + completion_result.selected_target == fixture_native_bridge); + CHECK("bound.second-pending", + fixture.pending.armed == 0 && completion_result.pending_armed == 0 && + completion_result.pending_cleared == 1); + CHECK("bound.slot-preserved", fixture.slot == fixture_native_bridge); + fixture_destroy(&fixture); +} + +static void test_retire_writer_stress_1000(void) +{ + fixture_t fixture; + int baseline_failures; + int i; + + if (fixture_init(&fixture) != 0) { + fixture_destroy(&fixture); + return; + } + baseline_failures = failures; + for (i = 0; i < STRESS_ITERATIONS; ++i) { + fixture.slot = GUEST_TARGET; + reset_guard(&fixture, 1); + run_after_cas_retire(&fixture, "stress"); + if (failures != baseline_failures) { + fprintf(stderr, "stress failed at iteration %d\n", i); + break; + } + if (i + 1 < STRESS_ITERATIONS) { + fixture.pending.source_generation = fixture_publish_source(&fixture); + } + } + CHECK("stress.iterations", i == STRESS_ITERATIONS); + fixture_destroy(&fixture); +} + +int main(void) +{ + test_successful_production_route(); + test_guest_version_is_not_used_for_host_lookup(); + test_native_symbol_missing_fails_open(); + test_dependency_symbol_owner_mismatch_fails_open(); + test_missing_bridge_is_created_from_exact_provider(); + test_created_inexact_bridge_fails_open(); + test_eager_production_request_evidence(); + test_confirmed_unversioned_production_paths_apply(); + test_unknown_version_evidence_preserves_guest_path(); + test_production_permission_transaction_preserves_on_restore_error(); + test_recycle_before_acquire_preserves_guest(); + test_retire_before_first_slot_read_skips_guest_memory(); + test_namespace_mismatch_precedes_any_source_read(); + test_guard_fail_open_keeps_real_completion_evidence(); + test_retire_waits_for_writer_transaction(); + test_completion_holds_one_lease_until_final_release(); + test_retire_waits_through_verify_failure_and_rollback(); + test_fail_open_and_non_writer_paths(); + test_retire_writer_stress_1000(); + + if (failures) { + fprintf(stderr, "%d lazy production lease checks failed\n", failures); + return 1; + } + puts("KZT WI-256 lazy production source lease: PASS (1000 races)"); + return 0; +} diff --git a/tests/unit/kzt/test_wi256_plt_resolver_adapter.c b/tests/unit/kzt/test_wi256_plt_resolver_adapter.c new file mode 100644 index 00000000000..ad9ac688d2e --- /dev/null +++ b/tests/unit/kzt/test_wi256_plt_resolver_adapter.c @@ -0,0 +1,346 @@ +#include +#include + +#include "target/i386/latx/include/kzt_plt_resolver_adapter.h" + +typedef enum begin_outcome { + BEGIN_ARMED = 0, + BEGIN_PENDING_OCCUPIED, + BEGIN_GENERATION_CHANGED, + BEGIN_NON_MAIN_NAMESPACE, +} begin_outcome_t; + +typedef struct fixture { + CPUX86State cpu; + uint64_t stack[10]; + uintptr_t object_head; + uintptr_t relocation_slot; + uintptr_t return_address; + uintptr_t self_link_map; + uintptr_t object_guest_resolver; + uintptr_t forbidden_global_resolver; + unsigned long generation; + uintptr_t namespace_id; + kzt_guest_library_namespace_kind_t namespace_kind; + int source_present; + begin_outcome_t begin_outcome; + int lookup_calls; + int begin_calls; + int pending_creations; + kzt_lazy_binding_pending_t pending; + kzt_lazy_binding_begin_request_t captured_request; +} fixture_t; + +static int failures; + +#define CHECK(name, condition) do { \ + if (!(condition)) { \ + fprintf(stderr, "%s failed at line %d\n", name, __LINE__); \ + ++failures; \ + } \ +} while (0) + +static void reset_frame(fixture_t *fixture) +{ + memset(fixture->stack, 0, sizeof(fixture->stack)); + fixture->stack[3] = fixture->object_head; + fixture->stack[4] = fixture->relocation_slot; + fixture->stack[5] = fixture->return_address; + fixture->cpu.regs[R_ESP] = (uintptr_t)&fixture->stack[3]; +} + +static void fixture_init(fixture_t *fixture) +{ + memset(fixture, 0, sizeof(*fixture)); + fixture->object_head = 0x11000100; + fixture->relocation_slot = 5; + fixture->return_address = 0x12000200; + fixture->self_link_map = 0x13000300; + fixture->object_guest_resolver = 0x14000400; + fixture->forbidden_global_resolver = 0x15000500; + fixture->generation = 7; + fixture->namespace_kind = KZT_GUEST_LIBRARY_NAMESPACE_MAIN; + fixture->source_present = 1; + fixture->begin_outcome = BEGIN_ARMED; + reset_frame(fixture); +} + +static int lookup_source(uintptr_t object_head, + kzt_plt_resolver_source_t *source, void *opaque) +{ + fixture_t *fixture = opaque; + + ++fixture->lookup_calls; + CHECK("lookup.object-head", object_head == fixture->object_head); + if (!fixture->source_present) { + return -1; + } + *source = (kzt_plt_resolver_source_t) { + .context_id = 0x16000600, + .object_head = fixture->object_head, + .source_link_map = fixture->self_link_map, + .source_generation = fixture->generation, + .namespace_id = fixture->namespace_id, + .namespace_kind = fixture->namespace_kind, + .guest_resolver = fixture->object_guest_resolver, + }; + return 0; +} + +/* Contract 1 is mocked as a boundary. No owner/provider/native decision is + * reproduced in this adapter test. */ +static int begin_lazy_binding( + const kzt_lazy_binding_begin_request_t *request, + kzt_lazy_binding_pending_t *pending, + kzt_lazy_binding_result_t *result, void *opaque) +{ + fixture_t *fixture = opaque; + + ++fixture->begin_calls; + fixture->captured_request = *request; + memset(result, 0, sizeof(*result)); + result->selected_target = fixture->object_guest_resolver; + switch (fixture->begin_outcome) { + case BEGIN_ARMED: + if (!pending->armed) { + pending->armed = 1; + ++fixture->pending_creations; + } + result->status = KZT_LAZY_BINDING_HANDOFF_GUEST; + result->pending_armed = 1; + break; + case BEGIN_PENDING_OCCUPIED: + result->status = KZT_LAZY_BINDING_GUEST_PRESERVED; + result->reason = KZT_LAZY_BINDING_REASON_PENDING_OCCUPIED; + result->pending_armed = 0; + break; + case BEGIN_GENERATION_CHANGED: + result->status = KZT_LAZY_BINDING_GUEST_PRESERVED; + result->reason = KZT_LAZY_BINDING_REASON_GENERATION_CHANGED; + result->pending_armed = 0; + break; + case BEGIN_NON_MAIN_NAMESPACE: + result->status = KZT_LAZY_BINDING_GUEST_PRESERVED; + result->reason = KZT_LAZY_BINDING_REASON_NON_MAIN_NAMESPACE; + result->pending_armed = 0; + break; + } + return 0; +} + +static kzt_plt_resolver_runtime_ops_t ops_for(fixture_t *fixture) +{ + return (kzt_plt_resolver_runtime_ops_t) { + .lookup_source = lookup_source, + .begin_lazy_binding = begin_lazy_binding, + .pending = &fixture->pending, + .opaque = fixture, + }; +} + +static void check_guest_frame(const char *prefix, fixture_t *fixture) +{ + uint64_t *sp = (uint64_t *)fixture->cpu.regs[R_ESP]; + + CHECK(prefix, sp == &fixture->stack[2]); + CHECK("frame.object-resolver", sp[0] == fixture->object_guest_resolver); + CHECK("frame.self-link-map", sp[1] == fixture->self_link_map); + CHECK("frame.relocation-slot", sp[2] == fixture->relocation_slot); + CHECK("frame.return-address", sp[3] == fixture->return_address); + CHECK("frame.no-global-resolver", + sp[0] != fixture->forbidden_global_resolver); +} + +static void check_original_frame(const char *prefix, fixture_t *fixture) +{ + uint64_t *sp = (uint64_t *)fixture->cpu.regs[R_ESP]; + + CHECK(prefix, sp == &fixture->stack[3]); + CHECK("original.object-head", sp[0] == fixture->object_head); + CHECK("original.relocation-slot", sp[1] == fixture->relocation_slot); + CHECK("original.return-address", sp[2] == fixture->return_address); +} + +static void test_reads_real_frame_and_preserves_return_address(void) +{ + fixture_t f; + kzt_plt_resolver_runtime_ops_t ops; + kzt_plt_resolver_enter_result_t result; + + fixture_init(&f); + ops = ops_for(&f); + CHECK("frame.enter", kzt_plt_resolver_enter(&f.cpu, &ops, &result) == 0); + CHECK("frame.status", + result.status == KZT_PLT_RESOLVER_HANDOFF_GUEST); + CHECK("frame.object", result.object_head == f.object_head); + CHECK("frame.slot", result.relocation_slot == f.relocation_slot); + CHECK("frame.return", result.return_address == f.return_address); + CHECK("frame.lookup-once", f.lookup_calls == 1); + CHECK("frame.begin-once", f.begin_calls == 1); + check_guest_frame("frame.stack-pointer", &f); +} + +static void test_uses_per_object_resolver_and_calls_only_contract_one(void) +{ + fixture_t f; + kzt_plt_resolver_runtime_ops_t ops; + kzt_plt_resolver_enter_result_t result; + + fixture_init(&f); + ops = ops_for(&f); + CHECK("object.enter", kzt_plt_resolver_enter(&f.cpu, &ops, &result) == 0); + CHECK("object.selected", + result.selected_resolver == f.object_guest_resolver); + CHECK("object.not-global", + result.selected_resolver != f.forbidden_global_resolver); + CHECK("object.contract-one-once", f.begin_calls == 1); + CHECK("object.request-link-map", + f.captured_request.source_link_map == f.self_link_map); + CHECK("object.request-generation", + f.captured_request.source_generation == f.generation); + CHECK("object.request-namespace", + f.captured_request.namespace_id == f.namespace_id && + f.captured_request.namespace_kind == f.namespace_kind); + CHECK("object.request-relocation", + f.captured_request.relocation_index == f.relocation_slot); +} + +static void test_main_generation_match_creates_one_pending(void) +{ + fixture_t f; + kzt_plt_resolver_runtime_ops_t ops; + kzt_plt_resolver_enter_result_t result; + + fixture_init(&f); + ops = ops_for(&f); + CHECK("pending.first", kzt_plt_resolver_enter(&f.cpu, &ops, &result) == 0); + CHECK("pending.first-armed", result.pending_armed == 1); + CHECK("pending.created-once", f.pending_creations == 1); + + reset_frame(&f); + f.begin_outcome = BEGIN_PENDING_OCCUPIED; + CHECK("pending.second", kzt_plt_resolver_enter(&f.cpu, &ops, &result) == 0); + CHECK("pending.second-fail-open", + result.status == KZT_PLT_RESOLVER_GUEST_PRESERVED); + CHECK("pending.still-created-once", f.pending_creations == 1); + CHECK("pending.contract-called-per-entry", f.begin_calls == 2); + check_guest_frame("pending.second-stack", &f); +} + +static void test_nested_pending_busy_preserves_outer_cpu_pending(void) +{ + fixture_t f; + kzt_plt_resolver_runtime_ops_t ops; + kzt_plt_resolver_enter_result_t result; + kzt_lazy_binding_pending_t saved; + + fixture_init(&f); + f.pending = (kzt_lazy_binding_pending_t) { + .armed = 1, + .context_id = 0x9000, + .source_link_map = 0x9100, + .source_generation = 11, + .relocation_index = 4, + .slot_addr = 0x9200, + .guest_resolver = 0x9300, + }; + saved = f.pending; + f.begin_outcome = BEGIN_PENDING_OCCUPIED; + ops = ops_for(&f); + CHECK("nested.enter", kzt_plt_resolver_enter(&f.cpu, &ops, &result) == 0); + CHECK("nested.preserved", !memcmp(&f.pending, &saved, sizeof(saved))); + CHECK("nested.status", + result.status == KZT_PLT_RESOLVER_GUEST_PRESERVED); + check_guest_frame("nested.stack-pointer", &f); +} + +static void test_missing_object_restores_original_frame(void) +{ + fixture_t f; + kzt_plt_resolver_runtime_ops_t ops; + kzt_plt_resolver_enter_result_t result; + + fixture_init(&f); + ops = ops_for(&f); + f.source_present = 0; + CHECK("missing.enter", kzt_plt_resolver_enter(&f.cpu, &ops, &result) == 0); + CHECK("missing.status", + result.status == KZT_PLT_RESOLVER_LEGACY_FRAME_RESTORED); + CHECK("missing.no-contract-one", f.begin_calls == 0); + CHECK("missing.no-pending", f.pending_creations == 0); + check_original_frame("missing.stack-pointer", &f); +} + +static void test_second_object_lookup_failure_never_uses_first_resolver(void) +{ + fixture_t f; + kzt_plt_resolver_runtime_ops_t ops; + kzt_plt_resolver_enter_result_t result; + + fixture_init(&f); + f.object_head = 0x21000100; + f.self_link_map = 0x23000300; + f.object_guest_resolver = 0x24000400; + f.forbidden_global_resolver = 0x14000400; + f.source_present = 0; + reset_frame(&f); + ops = ops_for(&f); + CHECK("second-missing.enter", + kzt_plt_resolver_enter(&f.cpu, &ops, &result) == 0); + CHECK("second-missing.legacy-frame", + result.status == KZT_PLT_RESOLVER_LEGACY_FRAME_RESTORED); + CHECK("second-missing.no-selected-resolver", + result.selected_resolver == 0); + CHECK("second-missing.not-first-resolver", + result.selected_resolver != f.forbidden_global_resolver); + CHECK("second-missing.no-begin", f.begin_calls == 0); + check_original_frame("second-missing.stack-pointer", &f); +} + +static void test_contract_one_rejections_restore_guest_frame(void) +{ + const begin_outcome_t outcomes[] = { + BEGIN_PENDING_OCCUPIED, + BEGIN_GENERATION_CHANGED, + BEGIN_NON_MAIN_NAMESPACE, + }; + + for (size_t i = 0; i < sizeof(outcomes) / sizeof(outcomes[0]); ++i) { + fixture_t f; + kzt_plt_resolver_runtime_ops_t ops; + kzt_plt_resolver_enter_result_t result; + + fixture_init(&f); + ops = ops_for(&f); + f.begin_outcome = outcomes[i]; + if (outcomes[i] == BEGIN_NON_MAIN_NAMESPACE) { + f.namespace_id = 1; + f.namespace_kind = KZT_GUEST_LIBRARY_NAMESPACE_EXPLICIT; + } + CHECK("reject.enter", + kzt_plt_resolver_enter(&f.cpu, &ops, &result) == 0); + CHECK("reject.status", + result.status == KZT_PLT_RESOLVER_GUEST_PRESERVED); + CHECK("reject.contract-one-once", f.begin_calls == 1); + CHECK("reject.no-pending", f.pending_creations == 0); + check_guest_frame("reject.stack-pointer", &f); + } +} + +int main(void) +{ + test_reads_real_frame_and_preserves_return_address(); + test_uses_per_object_resolver_and_calls_only_contract_one(); + test_main_generation_match_creates_one_pending(); + test_nested_pending_busy_preserves_outer_cpu_pending(); + test_missing_object_restores_original_frame(); + test_second_object_lookup_failure_never_uses_first_resolver(); + test_contract_one_rejections_restore_guest_frame(); + if (failures) { + fprintf(stderr, "%d WI-256 resolver-adapter checks failed\n", + failures); + return 1; + } + puts("KZT WI-256 PLT resolver adapter: PASS"); + return 0; +} diff --git a/tests/unit/kzt/test_wi256_plt_resolver_source_contract.py b/tests/unit/kzt/test_wi256_plt_resolver_source_contract.py new file mode 100644 index 00000000000..1e6c6d09aee --- /dev/null +++ b/tests/unit/kzt/test_wi256_plt_resolver_source_contract.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +import pathlib +import sys + + +def fail(message: str) -> None: + raise AssertionError(message) + + +root = pathlib.Path(sys.argv[1]).resolve() +adapter = root / "target/i386/latx/context/kzt_plt_resolver_adapter.c" +elfloader = root / "target/i386/latx/context/elfloader.c" + +if not adapter.is_file(): + fail(f"missing production adapter: {adapter}") + +adapter_text = adapter.read_text(encoding="utf-8") +elfloader_text = elfloader.read_text(encoding="utf-8") +lazy_header = (root / "target/i386/latx/include/kzt_lazy_binding.h").read_text( + encoding="utf-8" +) +lazy_core = (root / "target/i386/latx/context/kzt_lazy_binding.c").read_text( + encoding="utf-8" +) + +if not any( + call in adapter_text + for call in ("kzt_lazy_binding_begin(", "->begin_lazy_binding(") +): + fail("resolver adapter must delegate pending decisions to Contract 1") + +for forbidden in ( + "getAlternate(", + "GetGlobalSymbolStartEnd", + "kzt_owner_resolver", + "kzt_wrapper_probe", + "kzt_bridge_exact", +): + if forbidden in adapter_text: + fail(f"resolver adapter duplicates native resolution: {forbidden}") + +if "dl_runtime_resolver" in adapter_text: + fail("resolver adapter must never substitute a process-global resolver") + +entry = "kzt_plt_resolver_enter(" +resolver_start = elfloader_text.find("void PltResolver(void)") +if resolver_start < 0: + fail("missing production PltResolver slow path") +body_start = elfloader_text.find("{", resolver_start) +depth = 0 +resolver_end = -1 +for offset, char in enumerate(elfloader_text[body_start:], start=body_start): + if char == "{": + depth += 1 + elif char == "}": + depth -= 1 + if depth == 0: + resolver_end = offset + 1 + break +if resolver_end < 0: + fail("could not delimit production PltResolver") +if entry not in elfloader_text[resolver_start:resolver_end]: + fail("PltResolver slow path does not call kzt_plt_resolver_enter") + +relocate_start = elfloader_text.find("int RelocateElfPlt(") +relocate_end = elfloader_text.find("\n#if 0", relocate_start) +if relocate_start < 0 or relocate_end < 0: + fail("could not delimit RelocateElfPlt cold path") +relocate_body = elfloader_text[relocate_start:relocate_end] +if "kzt_lazy_completion_bridge = AddBridge(" not in relocate_body: + fail("completion bridge must be created in RelocateElfPlt cold path") + +for forbidden_seam in ("resolve_native_bridge", "compare_exchange_slot"): + if forbidden_seam in lazy_header or forbidden_seam in lazy_core: + fail(f"lazy core retains test-only fallback seam: {forbidden_seam}") + +production_route = ( + root / "target/i386/latx/context/kzt_jump_slot_production.c" +).read_text(encoding="utf-8") +resolver_complete_start = elfloader_text.find( + "static void KztLazyBindingCompleteResolver(void)" +) +resolver_complete_end = elfloader_text.find( + "typedef struct kzt_plt_resolver_production_state", resolver_complete_start +) +if resolver_complete_start < 0 or resolver_complete_end < 0: + fail("could not delimit lazy completion resolver") +if "kzt_production_lazy_complete(" not in elfloader_text[ + resolver_complete_start:resolver_complete_end +]: + fail("completion resolver must call the testable production orchestration") +if "kzt_lazy_binding_complete(" in elfloader_text[ + resolver_complete_start:resolver_complete_end +]: + fail("completion resolver bypasses production lease orchestration") +lazy_production_start = production_route.find( + "int kzt_production_lazy_route_guest_target(" +) +if lazy_production_start < 0: + fail("missing lazy production route") +lazy_production = production_route[lazy_production_start:] +for forbidden_lookup in ( + "GetGlobalSymbolStartEndWithProvider", + "GetGlobalSymbolStartEnd", + "GetMaplib", +): + if forbidden_lookup in lazy_production: + fail(f"lazy completion repeats legacy symbol lookup: {forbidden_lookup}") +if "validate_source_identity" not in production_route: + fail("production route lacks write-adjacent source generation recheck") +for required in ( + "kzt_production_lazy_load_slot_with_lease(", + "kzt_lazy_binding_complete(", + "kzt_guest_registry_source_lease_release(", +): + if required not in production_route: + fail(f"production completion orchestration missing: {required}") + +production_hits = [] +for path in (root / "target/i386").rglob("*.[ch]"): + if path == adapter or path.name == "kzt_plt_resolver_adapter.h": + continue + if entry in path.read_text(encoding="utf-8", errors="ignore"): + production_hits.append(path.relative_to(root).as_posix()) + +if production_hits != ["target/i386/latx/context/elfloader.c"]: + fail(f"resolver enter escaped the one-shot PltResolver path: {production_hits}") + +for hot_root in ( + root / "target/i386/tcg", + root / "target/i386/latx/translator", +): + if not hot_root.exists(): + continue + for path in hot_root.rglob("*.[ch]"): + if entry in path.read_text(encoding="utf-8", errors="ignore"): + fail(f"per-TB resolver check introduced in {path.relative_to(root)}") + +print("WI-256 PLT resolver one-shot source contract: PASS") diff --git a/tests/unit/kzt/test_wi382_registry_observation_source_contract.py b/tests/unit/kzt/test_wi382_registry_observation_source_contract.py new file mode 100644 index 00000000000..360b190ef60 --- /dev/null +++ b/tests/unit/kzt/test_wi382_registry_observation_source_contract.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +import pathlib +import sys + + +def function_body(text: str, signature: str) -> str: + start = text.index(signature) + brace = text.index("{", start + len(signature)) + depth = 0 + for pos in range(brace, len(text)): + if text[pos] == "{": + depth += 1 + elif text[pos] == "}": + depth -= 1 + if depth == 0: + return text[brace + 1:pos] + raise AssertionError(f"unterminated function: {signature}") + + +root = pathlib.Path(sys.argv[1]).resolve() +myalign = (root / "target/i386/latx/context/myalign.c").read_text() +elfloader = (root / "target/i386/latx/context/elfloader.c").read_text() +adapter = ( + root / "target/i386/latx/context/kzt_observation_adapter.c" +).read_text() +registry_context = ( + root / "target/i386/latx/context/kzt_guest_registry_context.c" +).read_text() +context_header = ( + root / "target/i386/latx/include/box64context.h" +).read_text() +production_meson = ( + root / "target/i386/latx/context/meson.build" +).read_text() +production_route = ( + root / "target/i386/latx/context/kzt_jump_slot_production.c" +).read_text() + +# The old callback must reject NULL identity and path before dereferencing. +legacy = function_body(myalign, "static int kzt_tb_callback_legacy(") +identity_guard = legacy.index("if (!my_lm)") +first_dereference = legacy.index("my_lm->") +path_guard = legacy.index("if (!my_lm->l_name)") +assert identity_guard < first_dereference +assert path_guard == legacy.index("my_lm->l_name") - len("if (!") + +# Public link_map evidence plus ELF PT_LOAD ranges replace glibc-private +# l_ns/l_map_start/l_map_end offsets. Startup publishes the exact pair while +# the callback gate is still held; dlopen keeps its scoped publish path. +assert "l_map_start" not in legacy +assert "l_map_end" not in legacy +assert "->l_ns" not in legacy +assert "GetElfLoadRange(" in legacy +assert "if (lib && !loader_scope_active &&" in legacy +assert "kzt_guest_library_note_loader_pair(" in legacy + +# A malformed ELF or either relocation failure must leave the startup pair +# unpublished. The final publish is reached only through the shared success +# condition after both failure-returning calls. +header_load = legacy.index("h = LoadAndCheckElfHeader(") +header_guard = legacy.index("if (!h)", header_load) +header_use = legacy.index("ElfHeadReFix(h", header_load) +assert header_load < header_guard < header_use +relocate = legacy.index("RelocateElf(", header_use) +relocate_plt = legacy.index("RelocateElfPlt(", relocate) +publish = legacy.index("kzt_guest_library_note_loader_pair(", relocate_plt) +assert "relocate_result" in legacy[relocate:publish] +assert "relocate_plt_result" in legacy[relocate:publish] +success_condition = legacy.rfind("if (", relocate_plt, publish) +assert success_condition >= 0 +condition = legacy[success_condition:publish] +assert "relocate_result == 0" in condition +assert "relocate_plt_result == 0" in condition + +plt_observation = function_body(elfloader, "static void kzt_observe_plt_source(") +assert "kzt_elfloader_head_identity(elf_header" in plt_observation +assert "info1.pt_dynamic_addr" not in plt_observation +assert "kzt_guest_link_map_classify_namespace(" in plt_observation +assert "kzt_guest_registry_context_get_main_namespace_head(" in plt_observation +assert "kzt_guest_registry_context_has_main_namespace_evidence(" in plt_observation +assert "kzt_guest_link_map_read_predecessor(" in plt_observation +assert "GetElfLoadRange(" in plt_observation +assert ".namespace_id_present = main_namespace == 1" in plt_observation +assert ".map_range_present = range_available" in plt_observation +relocate_plt = function_body(elfloader, "int RelocateElfPlt(") +assert "uintptr_t kzt_evidence_got = head->pltgot" in relocate_plt +assert "kzt_evidence_got_runtime + 8" in relocate_plt +assert "kzt_guest_link_map_identity_matches(" in relocate_plt +assert "head->pltgot ? head->pltgot : head->got" in relocate_plt +assert relocate_plt.index("kzt_observe_plt_source(") < relocate_plt.index( + "head->had_RelocateElfPlt = 1" +) + +# Production diagnostics are linked, opt-in, and avoid a configure lock when off. +assert "'kzt_guest_dynamic_diagnostics.c'" in production_meson +callback = function_body(myalign, "static void kzt_tb_callback(") +assert "kzt_main_elf_identity(elf_header" in callback +assert "info1.pt_dynamic_addr" not in callback +assert "kzt_guest_link_map_read_predecessor(" in callback +assert "kzt_guest_registry_context_confirm_main_namespace_head(" in callback +assert "int diagnostics_enabled = kzt_registry_diagnostics_enabled();" in callback +assert ".enabled = diagnostics_enabled" in callback +assert ".diagnostics_enabled = diagnostics_enabled" in callback +assert "if (registry && diagnostics_enabled)" in callback + +compare = function_body(adapter, "static void kzt_adapter_compare_dynamic_views(") +assert compare.index("!request->diagnostics_enabled") < compare.index( + "kzt_guest_registry_find_dynamic_view(" +) +assert "existing_result.unknown_tag_count = existing_view.unknown_tag_count;" in compare + +# Context ownership is represented as one state object with an atomic hot path. +assert "kzt_guest_registry_context_t kzt_guest_registry_context;" in context_header +assert "__atomic_load_n(&context->state" in registry_context +assert "kzt_guest_registry_context_destroy(" in registry_context +assert "registry->" not in registry_context + +# Early fail-open remains observable when enrichment stops before a full +# planner/writer diagnostic can be built, but only under the diagnostics gate. +emit = function_body(production_route, "static void production_emit_diagnostic(") +assert "kzt_registry_diagnostics_enabled()" in emit +assert "kzt_rela_fail_open stage=%s" in emit +assert "ROUTE_PRECONDITIONS" in production_route +assert "BASE_EVIDENCE" in production_route +assert "EXACT_LIBRARY_BINDING" in production_route +assert "BRIDGE_EVIDENCE" in production_route +assert "SOURCE_IDENTITY" in production_route +assert "WRITER" in production_route + +print("WI-382 registry observation source contract: PASS") diff --git a/tests/unit/kzt/test_wrapper_bridge_provider.c b/tests/unit/kzt/test_wrapper_bridge_provider.c new file mode 100644 index 00000000000..f9f40398ee8 --- /dev/null +++ b/tests/unit/kzt/test_wrapper_bridge_provider.c @@ -0,0 +1,381 @@ +#include +#include + +#include "target/i386/latx/include/kzt_wrapper_bridge_provider.h" + +static int failures; + +typedef struct fake_library { + int inspect_status; + const char *name; + uintptr_t native_symbol; + uintptr_t bridge_target; + uintptr_t add_target; + uintptr_t post_add_target; + int bridge_exact; + int lifetime_bound; + int inspect_calls; + int check_calls; + int add_calls; +} fake_library_t; + +static void fake_wrapper(uintptr_t fnc) +{ + (void)fnc; +} + +static void check_int(const char *name, int got, int expected) +{ + if (got == expected) { + return; + } + fprintf(stderr, "%s: got %d expected %d\n", name, got, expected); + ++failures; +} + +static void check_ulong(const char *name, uintptr_t got, uintptr_t expected) +{ + if (got == expected) { + return; + } + fprintf(stderr, "%s: got 0x%lx expected 0x%lx\n", name, + (unsigned long)got, (unsigned long)expected); + ++failures; +} + +static int fake_inspect( + void *library, const char *symbol_name, const char *symbol_version, + kzt_wrapper_bridge_provider_match_t *match, void *opaque) +{ + fake_library_t *lib = library; + + (void)opaque; + ++lib->inspect_calls; + if (lib->inspect_status <= 0) { + return lib->inspect_status; + } + if (strcmp(symbol_name, "gtk_widget_show") != 0 || + (symbol_version && strcmp(symbol_version, "GTK_3.0") != 0)) { + return -1; + } + + match->wrapper_name = lib->name; + snprintf(match->native_name, sizeof(match->native_name), "%s", + symbol_name); + match->abi_wrapper = fake_wrapper; + match->native_symbol = lib->native_symbol; + match->resolved_bridge_target = lib->bridge_target; + match->context_owner = lib; + match->wrapper_provider = lib; + match->native_lookup_handle = lib; + match->native_owner = lib; + match->bridge_owner = lib; + match->bridge_storage = lib; + match->resolved_bridge_exact = lib->bridge_exact; + match->wrapper_provider_lifetime_bound = lib->lifetime_bound; + match->native_owner_lifetime_bound = lib->lifetime_bound; + match->bridge_owner_lifetime_bound = lib->lifetime_bound; + return 1; +} + +static uintptr_t fake_check( + const kzt_wrapper_bridge_provider_match_t *match, void *opaque) +{ + fake_library_t *lib = match->bridge_owner; + + (void)opaque; + ++lib->check_calls; + return lib->bridge_target; +} + +static uintptr_t fake_add( + const kzt_wrapper_bridge_provider_match_t *match, + const kzt_wrapper_probe_bridge_request_t *request, void *opaque) +{ + fake_library_t *lib = match->bridge_owner; + + (void)opaque; + if (request->native_symbol != lib->native_symbol) { + return 0; + } + ++lib->add_calls; + lib->bridge_target = lib->post_add_target ? lib->post_add_target : + lib->add_target; + return lib->add_target; +} + +static kzt_wrapper_bridge_provider_runtime_ops_t fake_ops(void) +{ + return (kzt_wrapper_bridge_provider_runtime_ops_t) { + .inspect_library = fake_inspect, + .check_bridge = fake_check, + .add_bridge = fake_add, + }; +} + +static void test_exact_unique_candidate_builds_temporary_provider(void) +{ + fake_library_t skipped = { .inspect_status = 0 }; + fake_library_t exact = { + .inspect_status = 1, + .name = "libgtk-3.so.0", + .native_symbol = 0x71000000, + .bridge_target = 0x72000000, + .bridge_exact = 1, + .lifetime_bound = 1, + }; + void *libraries[] = { &skipped, &exact, &exact }; + kzt_wrapper_bridge_provider_runtime_ops_t ops = fake_ops(); + kzt_wrapper_bridge_provider_t provider; + kzt_wrapper_probe_result_t result; + kzt_wrapper_probe_request_t request = { + .symbol_name = "gtk_widget_show", + .symbol_version = "GTK_3.0", + }; + + ops.add_bridge = NULL; + + check_int("exact.prepare", + kzt_wrapper_bridge_provider_prepare( + &provider, libraries, 3, request.symbol_name, + request.symbol_version, &ops), + 1); + check_int("exact.duplicate-inspected-once", exact.inspect_calls, 1); + check_int("exact.probe", + kzt_wrapper_probe_minimal_manifest( + &provider.manifest, &request, &provider.bridge_ops, + &result), + 0); + check_int("exact.match", result.wrapper_match, + KZT_PATCH_WRAPPER_VERSION_MATCH); + check_ulong("exact.native", result.native_symbol, 0x71000000); + check_ulong("exact.bridge", result.bridge_target, 0x72000000); + check_int("exact.check", exact.check_calls, 1); + check_int("exact.no-add", exact.add_calls, 0); + check_int("exact.add-disabled", + provider.bridge_ops.add_bridge == NULL, 1); + check_int("exact.bound-owner", + provider.match.bridge_owner == &exact, 1); +} + +static void test_unversioned_query_failure_and_ambiguity_fail_open(void) +{ + fake_library_t exact_a = { + .inspect_status = 1, + .name = "libgtk-3.so.0", + .native_symbol = 0x71000000, + .bridge_target = 0x72000000, + .bridge_exact = 1, + .lifetime_bound = 1, + }; + fake_library_t exact_b = { + .inspect_status = 1, + .name = "libgtk-shadow.so.0", + .native_symbol = 0x71001000, + .bridge_target = 0x72001000, + .bridge_exact = 1, + .lifetime_bound = 1, + }; + fake_library_t failed = { .inspect_status = -1 }; + kzt_wrapper_bridge_provider_runtime_ops_t ops = fake_ops(); + kzt_wrapper_bridge_provider_t provider; + void *one[] = { &exact_a }; + void *ambiguous[] = { &exact_a, &exact_b }; + void *query_failed[] = { &exact_a, &failed }; + + check_int("unversioned.prepare", + kzt_wrapper_bridge_provider_prepare( + &provider, one, 1, "gtk_widget_show", NULL, &ops), + 0); + check_int("unversioned.no-query", exact_a.inspect_calls, 0); + + check_int("ambiguous.prepare", + kzt_wrapper_bridge_provider_prepare( + &provider, ambiguous, 2, "gtk_widget_show", + "GTK_3.0", &ops), + 0); + check_int("ambiguous.unavailable", provider.manifest.available, 0); + + check_int("query-failed.prepare", + kzt_wrapper_bridge_provider_prepare( + &provider, query_failed, 2, "gtk_widget_show", + "GTK_3.0", &ops), + 0); + check_int("query-failed.unavailable", provider.manifest.available, 0); +} + +static void test_confirmed_unversioned_provider_is_probeable(void) +{ + fake_library_t exact = { + .inspect_status = 1, + .name = "libgtk-3.so.0", + .native_symbol = 0x71000000, + .bridge_target = 0x72000000, + .bridge_exact = 1, + .lifetime_bound = 1, + }; + void *libraries[] = { &exact }; + kzt_wrapper_bridge_provider_runtime_ops_t ops = fake_ops(); + kzt_wrapper_bridge_provider_t provider; + kzt_wrapper_probe_request_t request = { + .symbol_name = "gtk_widget_show", + .symbol_version_evidence = + KZT_SYMBOL_VERSION_CONFIRMED_UNVERSIONED, + .symbol_version = NULL, + }; + kzt_wrapper_probe_result_t result; + + ops.add_bridge = NULL; + check_int("confirmed-unversioned.prepare", + kzt_wrapper_bridge_provider_prepare_with_version_evidence( + &provider, libraries, 1, request.symbol_name, + request.symbol_version_evidence, request.symbol_version, + &ops), + 1); + check_int("confirmed-unversioned.inspected", exact.inspect_calls, 1); + check_int("confirmed-unversioned.probe", + kzt_wrapper_probe_minimal_manifest( + &provider.manifest, &request, &provider.bridge_ops, + &result), + 0); + check_int("confirmed-unversioned.match", result.wrapper_match, + KZT_PATCH_WRAPPER_UNVERSIONED_MATCH); + check_ulong("confirmed-unversioned.bridge", result.bridge_target, + exact.bridge_target); +} + +static void test_missing_lifetime_binding_fails_open(void) +{ + fake_library_t unbound = { + .inspect_status = 1, + .name = "libgtk-3.so.0", + .native_symbol = 0x71000000, + .bridge_target = 0x72000000, + .bridge_exact = 1, + .lifetime_bound = 0, + }; + void *libraries[] = { &unbound }; + kzt_wrapper_bridge_provider_runtime_ops_t ops = fake_ops(); + kzt_wrapper_bridge_provider_t provider; + + check_int("unbound.prepare", + kzt_wrapper_bridge_provider_prepare( + &provider, libraries, 1, "gtk_widget_show", + "GTK_3.0", &ops), + 0); + check_int("unbound.unavailable", provider.manifest.available, 0); +} + +static void test_bridge_abi_conflict_fails_open_without_add(void) +{ + fake_library_t conflict = { + .inspect_status = 1, + .name = "libgtk-3.so.0", + .native_symbol = 0x71000000, + .bridge_target = 0x72000000, + .bridge_exact = 0, + .lifetime_bound = 1, + }; + void *libraries[] = { &conflict }; + kzt_wrapper_bridge_provider_runtime_ops_t ops = fake_ops(); + kzt_wrapper_bridge_provider_t provider; + + check_int("conflict.prepare", + kzt_wrapper_bridge_provider_prepare( + &provider, libraries, 1, "gtk_widget_show", + "GTK_3.0", &ops), + 0); + check_int("conflict.unavailable", provider.manifest.available, 0); + check_int("conflict.no-add", conflict.add_calls, 0); +} + +static void test_missing_bridge_is_created_then_rechecked(void) +{ + fake_library_t missing = { + .inspect_status = 1, + .name = "libgtk-3.so.0", + .native_symbol = 0x71000000, + .bridge_target = 0, + .add_target = 0x72000000, + .bridge_exact = 0, + .lifetime_bound = 1, + }; + void *libraries[] = { &missing }; + kzt_wrapper_bridge_provider_runtime_ops_t ops = fake_ops(); + kzt_wrapper_bridge_provider_t provider; + kzt_wrapper_probe_result_t result; + kzt_wrapper_probe_request_t request = { + .symbol_name = "gtk_widget_show", + .symbol_version = "GTK_3.0", + }; + + check_int("missing.prepare", + kzt_wrapper_bridge_provider_prepare( + &provider, libraries, 1, request.symbol_name, + request.symbol_version, &ops), + 1); + check_int("missing.probe", + kzt_wrapper_probe_minimal_manifest( + &provider.manifest, &request, &provider.bridge_ops, + &result), + 0); + check_ulong("missing.bridge", result.bridge_target, 0x72000000); + check_int("missing.bridge-source", result.bridge_source, + KZT_WRAPPER_PROBE_BRIDGE_ADD_BRIDGE); + check_int("missing.check-twice", missing.check_calls, 2); + check_int("missing.add-once", missing.add_calls, 1); +} + +static void test_created_bridge_must_match_recheck(void) +{ + fake_library_t inexact = { + .inspect_status = 1, + .name = "libgtk-3.so.0", + .native_symbol = 0x71000000, + .bridge_target = 0, + .add_target = 0x72000000, + .post_add_target = 0x73000000, + .bridge_exact = 0, + .lifetime_bound = 1, + }; + void *libraries[] = { &inexact }; + kzt_wrapper_bridge_provider_runtime_ops_t ops = fake_ops(); + kzt_wrapper_bridge_provider_t provider; + kzt_wrapper_probe_result_t result; + kzt_wrapper_probe_request_t request = { + .symbol_name = "gtk_widget_show", + .symbol_version = "GTK_3.0", + }; + + check_int("inexact-created.prepare", + kzt_wrapper_bridge_provider_prepare( + &provider, libraries, 1, request.symbol_name, + request.symbol_version, &ops), + 1); + check_int("inexact-created.probe", + kzt_wrapper_probe_minimal_manifest( + &provider.manifest, &request, &provider.bridge_ops, + &result), + 0); + check_ulong("inexact-created.no-bridge", result.bridge_target, 0); + check_int("inexact-created.add-once", inexact.add_calls, 1); +} + +int main(void) +{ + test_exact_unique_candidate_builds_temporary_provider(); + test_unversioned_query_failure_and_ambiguity_fail_open(); + test_confirmed_unversioned_provider_is_probeable(); + test_missing_lifetime_binding_fails_open(); + test_bridge_abi_conflict_fails_open_without_add(); + test_missing_bridge_is_created_then_rechecked(); + test_created_bridge_must_match_recheck(); + + if (failures) { + fprintf(stderr, "kzt-wrapper-bridge-provider: %d failure(s)\n", + failures); + return 1; + } + puts("kzt-wrapper-bridge-provider: ok"); + return 0; +} diff --git a/tests/unit/kzt/test_wrapper_probe.c b/tests/unit/kzt/test_wrapper_probe.c new file mode 100644 index 00000000000..4ca136735c4 --- /dev/null +++ b/tests/unit/kzt/test_wrapper_probe.c @@ -0,0 +1,405 @@ +#include +#include + +#include "target/i386/latx/include/kzt_wrapper_probe.h" + +static int failures; + +typedef struct fake_bridge_state { + uintptr_t cached_native_symbol; + uintptr_t cached_bridge_target; + uintptr_t next_bridge_target; + int check_calls; + int add_calls; + kzt_wrapper_probe_bridge_request_t last_request; +} fake_bridge_state_t; + +static void check_int(const char *name, int got, int expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got %d expected %d\n", name, got, expected); + ++failures; +} + +static void check_ulong(const char *name, unsigned long got, + unsigned long expected) +{ + if (got == expected) { + return; + } + + fprintf(stderr, "%s: got 0x%lx expected 0x%lx\n", name, got, + expected); + ++failures; +} + +static void check_str(const char *name, const char *got, + const char *expected) +{ + if (got && expected && !strcmp(got, expected)) { + return; + } + if (!got && !expected) { + return; + } + + fprintf(stderr, "%s: got '%s' expected '%s'\n", name, + got ? got : "(null)", expected ? expected : "(null)"); + ++failures; +} + +static uintptr_t fake_check_bridge(uintptr_t native_symbol, void *opaque) +{ + fake_bridge_state_t *state = opaque; + + ++state->check_calls; + if (native_symbol == state->cached_native_symbol) { + return state->cached_bridge_target; + } + + return 0; +} + +static uintptr_t fake_add_bridge( + const kzt_wrapper_probe_bridge_request_t *request, void *opaque) +{ + fake_bridge_state_t *state = opaque; + + ++state->add_calls; + state->last_request = *request; + return state->next_bridge_target; +} + +static kzt_wrapper_probe_bridge_ops_t fake_bridge_ops( + fake_bridge_state_t *state) +{ + return (kzt_wrapper_probe_bridge_ops_t) { + .check_bridge = fake_check_bridge, + .add_bridge = fake_add_bridge, + .opaque = state, + }; +} + +static const kzt_wrapper_probe_entry_t base_entries[] = { + { + .symbol_name = "gtk_widget_show", + .symbol_version_evidence = KZT_SYMBOL_VERSION_VERSIONED, + .symbol_version = "GTK_3.0", + .wrapper_name = "wrappedgtk3", + .wrapper_version_evidence = KZT_SYMBOL_VERSION_VERSIONED, + .wrapper_symbol_version = "GTK_3.0", + .native_symbol = 0x7100001000, + }, + { + .symbol_name = "gtk_widget_hide", + .symbol_version = NULL, + .wrapper_name = "wrappedgtk3", + .wrapper_symbol_version = NULL, + .native_symbol = 0x7100002000, + }, + { + .symbol_name = "gtk_widget_destroy", + .symbol_version_evidence = KZT_SYMBOL_VERSION_VERSIONED, + .symbol_version = "GTK_3.0", + .wrapper_name = "wrappedgtk3", + .wrapper_version_evidence = KZT_SYMBOL_VERSION_VERSIONED, + .wrapper_symbol_version = "GTK_3.0", + .native_symbol = 0, + }, + { + .symbol_name = "gtk_widget_queue_draw", + .symbol_version_evidence = KZT_SYMBOL_VERSION_VERSIONED, + .symbol_version = "GTK_3.0", + .wrapper_name = "wrappedgtk3", + .wrapper_version_evidence = KZT_SYMBOL_VERSION_VERSIONED, + .wrapper_symbol_version = "GTK_3.0", + .native_symbol = 0x7100003000, + }, + { + .symbol_name = "gtk_widget_unversioned", + .symbol_version_evidence = + KZT_SYMBOL_VERSION_CONFIRMED_UNVERSIONED, + .symbol_version = NULL, + .wrapper_name = "wrappedgtk3", + .wrapper_version_evidence = + KZT_SYMBOL_VERSION_CONFIRMED_UNVERSIONED, + .wrapper_symbol_version = NULL, + .native_symbol = 0x7100004000, + }, +}; + +static kzt_wrapper_probe_manifest_t base_manifest(void) +{ + return (kzt_wrapper_probe_manifest_t) { + .available = 1, + .manifest_name = "wrappedgtk3", + .entries = base_entries, + .entry_count = sizeof(base_entries) / sizeof(base_entries[0]), + }; +} + +static kzt_wrapper_probe_request_t request_for(const char *name, + const char *version) +{ + return (kzt_wrapper_probe_request_t) { + .symbol_name = name, + .symbol_version_evidence = KZT_SYMBOL_VERSION_VERSIONED, + .symbol_version = version, + }; +} + +static void test_no_manifest_keeps_probe_unavailable(void) +{ + kzt_wrapper_probe_request_t request = + request_for("gtk_widget_show", "GTK_3.0"); + kzt_wrapper_probe_manifest_t manifest = base_manifest(); + kzt_wrapper_probe_result_t result; + fake_bridge_state_t state = {0}; + kzt_wrapper_probe_bridge_ops_t ops = fake_bridge_ops(&state); + + manifest.available = 0; + check_int("no_manifest.call", + kzt_wrapper_probe_minimal_manifest(&manifest, &request, + &ops, &result), 0); + check_int("no_manifest.match", result.wrapper_match, + KZT_PATCH_WRAPPER_NO_MANIFEST); + check_str("no_manifest.wrapper", result.wrapper_name, NULL); + check_ulong("no_manifest.bridge", result.bridge_target, 0); + check_int("no_manifest.check_calls", state.check_calls, 0); + check_int("no_manifest.add_calls", state.add_calls, 0); +} + +static void test_no_wrapper_distinguishes_missing_symbol(void) +{ + kzt_wrapper_probe_manifest_t manifest = base_manifest(); + kzt_wrapper_probe_request_t request = + request_for("gtk_widget_missing", "GTK_3.0"); + kzt_wrapper_probe_result_t result; + fake_bridge_state_t state = {0}; + kzt_wrapper_probe_bridge_ops_t ops = fake_bridge_ops(&state); + + check_int("no_wrapper.call", + kzt_wrapper_probe_minimal_manifest(&manifest, &request, + &ops, &result), 0); + check_int("no_wrapper.match", result.wrapper_match, + KZT_PATCH_WRAPPER_NO_WRAPPER); + check_ulong("no_wrapper.native", result.native_symbol, 0); + check_ulong("no_wrapper.bridge", result.bridge_target, 0); + check_int("no_wrapper.add_calls", state.add_calls, 0); +} + +static void test_symbol_only_does_not_create_safe_bridge(void) +{ + kzt_wrapper_probe_manifest_t manifest = base_manifest(); + kzt_wrapper_probe_request_t request = + request_for("gtk_widget_hide", "GTK_3.0"); + kzt_wrapper_probe_result_t result; + fake_bridge_state_t state = { + .next_bridge_target = 0x7200001000, + }; + kzt_wrapper_probe_bridge_ops_t ops = fake_bridge_ops(&state); + + check_int("symbol_only.call", + kzt_wrapper_probe_minimal_manifest(&manifest, &request, + &ops, &result), 0); + check_int("symbol_only.match", result.wrapper_match, + KZT_PATCH_WRAPPER_SYMBOL_ONLY); + check_str("symbol_only.wrapper", result.wrapper_name, "wrappedgtk3"); + check_str("symbol_only.version", result.wrapper_symbol_version, NULL); + check_ulong("symbol_only.native", result.native_symbol, 0x7100002000); + check_ulong("symbol_only.bridge", result.bridge_target, 0); + check_int("symbol_only.check_calls", state.check_calls, 0); + check_int("symbol_only.add_calls", state.add_calls, 0); +} + +static void test_version_mismatch_keeps_bridge_empty(void) +{ + kzt_wrapper_probe_manifest_t manifest = base_manifest(); + kzt_wrapper_probe_request_t request = + request_for("gtk_widget_show", "GTK_4.0"); + kzt_wrapper_probe_result_t result; + fake_bridge_state_t state = { + .next_bridge_target = 0x7200001000, + }; + kzt_wrapper_probe_bridge_ops_t ops = fake_bridge_ops(&state); + + check_int("version_mismatch.call", + kzt_wrapper_probe_minimal_manifest(&manifest, &request, + &ops, &result), 0); + check_int("version_mismatch.match", result.wrapper_match, + KZT_PATCH_WRAPPER_VERSION_MISMATCH); + check_str("version_mismatch.wrapper", result.wrapper_name, + "wrappedgtk3"); + check_str("version_mismatch.version", result.wrapper_symbol_version, + "GTK_3.0"); + check_ulong("version_mismatch.bridge", result.bridge_target, 0); + check_int("version_mismatch.add_calls", state.add_calls, 0); +} + +static void test_version_match_creates_bridge_from_explicit_callback(void) +{ + kzt_wrapper_probe_manifest_t manifest = base_manifest(); + kzt_wrapper_probe_request_t request = + request_for("gtk_widget_show", "GTK_3.0"); + kzt_wrapper_probe_result_t result; + fake_bridge_state_t state = { + .next_bridge_target = 0x7200001000, + }; + kzt_wrapper_probe_bridge_ops_t ops = fake_bridge_ops(&state); + + check_int("version_match.call", + kzt_wrapper_probe_minimal_manifest(&manifest, &request, + &ops, &result), 0); + check_int("version_match.match", result.wrapper_match, + KZT_PATCH_WRAPPER_VERSION_MATCH); + check_str("version_match.wrapper", result.wrapper_name, + "wrappedgtk3"); + check_str("version_match.version", result.wrapper_symbol_version, + "GTK_3.0"); + check_ulong("version_match.native", result.native_symbol, + 0x7100001000); + check_ulong("version_match.bridge", result.bridge_target, + 0x7200001000); + check_int("version_match.bridge_source", result.bridge_source, + KZT_WRAPPER_PROBE_BRIDGE_ADD_BRIDGE); + check_int("version_match.check_calls", state.check_calls, 1); + check_int("version_match.add_calls", state.add_calls, 1); + check_str("version_match.bridge_request.name", + state.last_request.symbol_name, "gtk_widget_show"); + check_str("version_match.bridge_request.version", + state.last_request.symbol_version, "GTK_3.0"); + check_ulong("version_match.bridge_request.native", + state.last_request.native_symbol, 0x7100001000); +} + +static void test_confirmed_unversioned_match_creates_bridge(void) +{ + kzt_wrapper_probe_manifest_t manifest = base_manifest(); + kzt_wrapper_probe_request_t request = + request_for("gtk_widget_unversioned", NULL); + kzt_wrapper_probe_result_t result; + fake_bridge_state_t state = { + .next_bridge_target = 0x7200004000, + }; + kzt_wrapper_probe_bridge_ops_t ops = fake_bridge_ops(&state); + + request.symbol_version_evidence = + KZT_SYMBOL_VERSION_CONFIRMED_UNVERSIONED; + check_int("unversioned.call", + kzt_wrapper_probe_minimal_manifest(&manifest, &request, + &ops, &result), 0); + check_int("unversioned.match", result.wrapper_match, + KZT_PATCH_WRAPPER_UNVERSIONED_MATCH); + check_int("unversioned.wrapper-evidence", + result.wrapper_version_evidence, + KZT_SYMBOL_VERSION_CONFIRMED_UNVERSIONED); + check_str("unversioned.version", result.wrapper_symbol_version, NULL); + check_ulong("unversioned.bridge", result.bridge_target, 0x7200004000); + check_int("unversioned.add-calls", state.add_calls, 1); + check_int("unversioned.bridge-request-evidence", + state.last_request.symbol_version_evidence, + KZT_SYMBOL_VERSION_CONFIRMED_UNVERSIONED); +} + +static void test_unknown_and_error_evidence_do_not_probe_bridge(void) +{ + kzt_wrapper_probe_manifest_t manifest = base_manifest(); + kzt_wrapper_probe_result_t result; + kzt_symbol_version_evidence_t evidence[] = { + KZT_SYMBOL_VERSION_UNKNOWN, + KZT_SYMBOL_VERSION_ERROR, + }; + size_t i; + + for (i = 0; i < sizeof(evidence) / sizeof(evidence[0]); ++i) { + kzt_wrapper_probe_request_t request = + request_for("gtk_widget_show", "GTK_3.0"); + fake_bridge_state_t state = { + .next_bridge_target = 0x7200001000, + }; + kzt_wrapper_probe_bridge_ops_t ops = fake_bridge_ops(&state); + + request.symbol_version_evidence = evidence[i]; + check_int("untrusted.call", + kzt_wrapper_probe_minimal_manifest( + &manifest, &request, &ops, &result), 0); + check_int("untrusted.match", result.wrapper_match, + KZT_PATCH_WRAPPER_VERSION_MISMATCH); + check_int("untrusted.no-check", state.check_calls, 0); + check_int("untrusted.no-add", state.add_calls, 0); + } +} + +static void test_bridge_zero_preserves_fail_open_input(void) +{ + kzt_wrapper_probe_manifest_t manifest = base_manifest(); + kzt_wrapper_probe_request_t request = + request_for("gtk_widget_destroy", "GTK_3.0"); + kzt_wrapper_probe_result_t result; + fake_bridge_state_t state = { + .next_bridge_target = 0x7200001000, + }; + kzt_wrapper_probe_bridge_ops_t ops = fake_bridge_ops(&state); + + check_int("bridge_zero.call", + kzt_wrapper_probe_minimal_manifest(&manifest, &request, + &ops, &result), 0); + check_int("bridge_zero.match", result.wrapper_match, + KZT_PATCH_WRAPPER_VERSION_MATCH); + check_ulong("bridge_zero.native", result.native_symbol, 0); + check_ulong("bridge_zero.bridge", result.bridge_target, 0); + check_int("bridge_zero.source", result.bridge_source, + KZT_WRAPPER_PROBE_BRIDGE_NONE); + check_int("bridge_zero.check_calls", state.check_calls, 0); + check_int("bridge_zero.add_calls", state.add_calls, 0); +} + +static void test_bridge_cache_reuse_does_not_add_duplicate_bridge(void) +{ + kzt_wrapper_probe_manifest_t manifest = base_manifest(); + kzt_wrapper_probe_request_t request = + request_for("gtk_widget_queue_draw", "GTK_3.0"); + kzt_wrapper_probe_result_t result; + fake_bridge_state_t state = { + .cached_native_symbol = 0x7100003000, + .cached_bridge_target = 0x7200003000, + .next_bridge_target = 0x7200004000, + }; + kzt_wrapper_probe_bridge_ops_t ops = fake_bridge_ops(&state); + + check_int("bridge_cache.call", + kzt_wrapper_probe_minimal_manifest(&manifest, &request, + &ops, &result), 0); + check_int("bridge_cache.match", result.wrapper_match, + KZT_PATCH_WRAPPER_VERSION_MATCH); + check_ulong("bridge_cache.bridge", result.bridge_target, + 0x7200003000); + check_int("bridge_cache.source", result.bridge_source, + KZT_WRAPPER_PROBE_BRIDGE_CACHE); + check_int("bridge_cache.check_calls", state.check_calls, 1); + check_int("bridge_cache.add_calls", state.add_calls, 0); +} + +int main(void) +{ + test_no_manifest_keeps_probe_unavailable(); + test_no_wrapper_distinguishes_missing_symbol(); + test_symbol_only_does_not_create_safe_bridge(); + test_version_mismatch_keeps_bridge_empty(); + test_version_match_creates_bridge_from_explicit_callback(); + test_confirmed_unversioned_match_creates_bridge(); + test_unknown_and_error_evidence_do_not_probe_bridge(); + test_bridge_zero_preserves_fail_open_input(); + test_bridge_cache_reuse_does_not_add_duplicate_bridge(); + + if (failures) { + fprintf(stderr, "kzt-wrapper-probe failures: %d\n", failures); + return 1; + } + + puts("kzt-wrapper-probe: ok"); + return 0; +} diff --git a/tests/unit/kzt/wi231_step4_writer_test_report.md b/tests/unit/kzt/wi231_step4_writer_test_report.md new file mode 100644 index 00000000000..6d2d53dcc4c --- /dev/null +++ b/tests/unit/kzt/wi231_step4_writer_test_report.md @@ -0,0 +1,92 @@ +# WI-231 Step4 writer 接线白盒测试报告 + +## Summary + +- worktree: `/home/loongson/work/code/latu-worktrees/kzt-step4-wi230-wi231-integrated-20260709094322` +- branch: `lauren/kzt-step4-wi230-wi231-integrated-20260709094322` +- 代码验证范围: `20dec00bac069e77a07d30a6d43a24ce34882f17..d7401373939651a8c51b841d32457eafda5744a1` +- 报告修正说明: 本文件随后随报告修正 commit 更新,因此不把报告自身 commit 写作固定 HEAD。 +- 测试目标: `kzt-rela-immediate-candidate` 与 13 个 KZT 单测目标 +- 结论: `git diff --check`、debug build、13 个 KZT 单测、`latxbuild/build-release.sh` 均通过。 +- 集成说明: 当前测试已经调用真实生产 helper `kzt_rela_immediate_jump_slot_try_write()`,不是平行模拟 Step4 contract。该 helper 再调用 Step3 writer/guard,覆盖 approved writer success、planner 非批准、writer fail-open、`GLOB_DAT`/非目标/lazy skip writer。 + +## Checklist + +- [x] 测试证据包含 command。 +- [x] 测试证据包含 summary。 +- [x] 测试证据包含 stdout/raw output。 +- [x] 测试证据包含 exit_code。 +- [x] 测试点直接覆盖 WI-230/WI-231 writer 接线验收,不只复用 Step3 writer 通用单测。 +- [x] 测试调用真实生产 helper `kzt_rela_immediate_jump_slot_try_write()`。 +- [x] 生产接线仍通过 Step3 guard/writer,不直接写 slot。 +- [x] 未 push,未创建远端 PR。 + +## Commands + +```sh +cd /home/loongson/work/code/latu-worktrees/kzt-step4-wi230-wi231-integrated-20260709094322 +git diff --check +ninja -C build64-dbg +meson test -C build64-dbg --print-errorlogs -v \ + kzt-guest-registry \ + kzt-guest-registry-concurrency \ + kzt-guest-link-map-reader \ + kzt-guest-dynamic-parser \ + kzt-guest-dynamic-snapshot \ + kzt-guest-dynamic-diagnostics \ + kzt-observation-adapter \ + kzt-registry-diagnostics-gate \ + kzt-patch-planner \ + kzt-runtime-got-plt-candidate \ + kzt-rela-immediate-candidate \ + kzt-patch-spike-guard \ + kzt-patch-spike-writer +latxbuild/build-release.sh +``` + +## Exit Codes + +```text +git diff --check: 0 +ninja -C build64-dbg: 0 +meson test -C build64-dbg 13 KZT tests: 0 +latxbuild/build-release.sh: 0 +``` + +## Raw Output Highlights + +```text +[113/132] Compiling C object libqemu-x86_64-linux-user.fa.p/target_i386_latx_context_box64context.c.o +[119/132] Compiling C object libqemu-x86_64-linux-user.fa.p/target_i386_latx_context_elfloader.c.o +[132/132] Linking target latx-x86_64 +``` + +```text +WI231_TC tc=approved-writer-success-skips-legacy plan_status=1 plan_reason=0 decision=APPROVED writer_called=1 legacy_writes=0 skip_legacy=1 result=APPLIED failure=NONE reads=2 writer_writes=1 final=0x7200004560 +WI231_TC tc=planner-unsupported-keeps-legacy plan_status=1 plan_reason=0 decision=UNSUPPORTED writer_called=0 legacy_writes=1 skip_legacy=0 result=DISABLED failure=NONE reads=0 writer_writes=0 final=0x7300003333 +WI231_TC tc=planner-rejected-keeps-legacy plan_status=1 plan_reason=0 decision=REJECTED writer_called=0 legacy_writes=1 skip_legacy=0 result=DISABLED failure=NONE reads=0 writer_writes=0 final=0x7300003333 +WI231_TC tc=lazy-deferred-keeps-legacy plan_status=0 plan_reason=3 decision=(none) writer_called=0 legacy_writes=1 skip_legacy=0 result=DISABLED failure=NONE reads=0 writer_writes=0 final=0x7300003333 +WI231_TC tc=planner-fail-open-keeps-legacy plan_status=2 plan_reason=6 decision=(none) writer_called=0 legacy_writes=1 skip_legacy=0 result=DISABLED failure=NONE reads=0 writer_writes=0 final=0x7300003333 +WI231_TC tc=writer-expected-mismatch-fail-open plan_status=1 plan_reason=0 decision=APPROVED writer_called=1 legacy_writes=1 skip_legacy=0 result=FAIL_OPEN failure=EXPECTED_MISMATCH reads=1 writer_writes=0 final=0x7300004444 +WI231_TC tc=writer-write-fail-fail-open plan_status=1 plan_reason=0 decision=APPROVED writer_called=1 legacy_writes=1 skip_legacy=0 result=FAIL_OPEN failure=WRITE_FAILED reads=1 writer_writes=1 final=0x7300004444 +WI231_TC tc=writer-verify-fail-fail-open plan_status=1 plan_reason=0 decision=APPROVED writer_called=1 legacy_writes=1 skip_legacy=0 result=FAIL_OPEN failure=VERIFY_FAILED reads=2 writer_writes=2 final=0x7300004444 +WI231_TC tc=writer-rollback-fail-fail-open plan_status=1 plan_reason=0 decision=APPROVED writer_called=1 legacy_writes=1 skip_legacy=0 result=FAIL_OPEN failure=ROLLBACK_FAILED reads=2 writer_writes=2 final=0x7300004444 +WI231_TC tc=non-target-relocation-skips-writer plan_status=0 plan_reason=2 decision=(none) writer_called=0 legacy_writes=1 skip_legacy=0 result=DISABLED failure=NONE reads=0 writer_writes=0 final=0x7300005555 +WI231_TC tc=glob-dat-skips-writer plan_status=0 plan_reason=2 decision=(none) writer_called=0 legacy_writes=1 skip_legacy=0 result=DISABLED failure=NONE reads=0 writer_writes=0 final=0x7300005555 +WI231_TC tc=lazy-deferred-skips-writer plan_status=0 plan_reason=3 decision=(none) writer_called=0 legacy_writes=1 skip_legacy=0 result=DISABLED failure=NONE reads=0 writer_writes=0 final=0x7300005555 +``` + +```text +Ok: 13 +Fail: 0 +[67/67] Linking target latx-i386 +[132/132] Linking target latx-x86_64 +``` + +## Coverage Mapping + +- approved writer success: `approved-writer-success-skips-legacy` 验证 writer 调用、legacy 跳过、最终值为 bridge。 +- planner 非批准或 fail-open: `planner-unsupported-keeps-legacy`、`planner-rejected-keeps-legacy`、`planner-fail-open-keeps-legacy` 验证 writer 不调用且 legacy 保持。 +- writer fail-open: `writer-expected-mismatch-fail-open`、`writer-write-fail-fail-open`、`writer-verify-fail-fail-open`、`writer-rollback-fail-fail-open` 验证失败时继续 legacy。 +- 非目标范围: `non-target-relocation-skips-writer`、`glob-dat-skips-writer`、`lazy-deferred-skips-writer` 验证本阶段不接管 `GLOB_DAT`、非目标 relocation 和 lazy deferred。 +- 生产生命周期: `ninja -C build64-dbg` 和 `latxbuild/build-release.sh` 编译 `box64context.c`、`elfloader.c` 和 KZT helper,确认 guard 挂到 `box64context_t` 的生产接线可编译。 diff --git a/tests/unit/meson.build b/tests/unit/meson.build index 3bbb0e5647c..d0b0e20625a 100644 --- a/tests/unit/meson.build +++ b/tests/unit/meson.build @@ -6,3 +6,519 @@ test_elfload_pagesize = executable( ) test('test-elfload-pagesize', test_elfload_pagesize) + +kzt_guest_registry_test = executable( + 'kzt-guest-registry', + files( + 'kzt/test_guest_registry.c', + '../../target/i386/latx/context/kzt_guest_registry.c', + ), + c_args: ['-DKZT_GUEST_REGISTRY_TEST'], + include_directories: include_directories('../../target/i386/latx/include'), + dependencies: dependency('threads'), +) + +test('kzt-guest-registry', kzt_guest_registry_test) + +kzt_guest_registry_concurrency_test = executable( + 'kzt-guest-registry-concurrency', + files( + 'kzt/test_guest_registry_concurrency.c', + '../../target/i386/latx/context/kzt_guest_registry.c', + ), + c_args: ['-DKZT_GUEST_REGISTRY_TEST'], + include_directories: include_directories('../../target/i386/latx/include'), + dependencies: dependency('threads'), +) + +test('kzt-guest-registry-concurrency', kzt_guest_registry_concurrency_test) + +kzt_guest_registry_context_test = executable( + 'kzt-guest-registry-context', + files( + 'kzt/test_guest_registry_context.c', + '../../target/i386/latx/context/kzt_guest_registry.c', + '../../target/i386/latx/context/kzt_guest_registry_context.c', + ), + c_args: ['-DKZT_GUEST_REGISTRY_TEST'], + include_directories: include_directories('../..', '../../target/i386/latx/include'), + dependencies: dependency('threads'), +) + +test('kzt-guest-registry-context', kzt_guest_registry_context_test) + +kzt_guest_library_binding_test = executable( + 'kzt-guest-library-binding', + files( + 'kzt/test_guest_library_binding.c', + '../../target/i386/latx/context/kzt_guest_library_binding.c', + '../../target/i386/latx/context/kzt_guest_registry.c', + ), + c_args: ['-DKZT_GUEST_LIBRARY_BINDING_TEST', '-DKZT_GUEST_REGISTRY_TEST'], + include_directories: include_directories('../../target/i386/latx/include'), + dependencies: dependency('threads'), +) + +test('kzt-guest-library-binding', kzt_guest_library_binding_test) + +kzt_guest_library_binding_teardown_test = executable( + 'kzt-guest-library-binding-teardown', + files( + 'kzt/test_guest_library_binding_teardown.c', + '../../target/i386/latx/context/kzt_guest_library_binding.c', + '../../target/i386/latx/context/kzt_guest_registry.c', + ), + c_args: ['-DKZT_GUEST_LIBRARY_BINDING_TEST', '-DKZT_GUEST_REGISTRY_TEST'], + include_directories: include_directories('../../target/i386/latx/include'), + dependencies: dependency('threads'), +) + +test('kzt-guest-library-binding-teardown', + kzt_guest_library_binding_teardown_test) + +kzt_dlopen_recycle_transaction_test = executable( + 'kzt-dlopen-recycle-transaction', + files( + 'kzt/test_dlopen_recycle_transaction.c', + '../../target/i386/latx/context/dlopen_recycle_transaction.c', + '../../target/i386/latx/context/elf_plt_relocation.c', + '../../target/i386/latx/context/kzt_guest_library_binding.c', + '../../target/i386/latx/context/kzt_guest_registry.c', + ), + c_args: ['-DKZT_GUEST_LIBRARY_BINDING_TEST'], + include_directories: include_directories('../../target/i386/latx/include'), + dependencies: dependency('threads'), +) + +test('kzt-dlopen-recycle-transaction', + kzt_dlopen_recycle_transaction_test) + +python3 = find_program('python3') +test( + 'kzt-wi382-registry-observation-source-contract', + python3, + args: [ + files('kzt/test_wi382_registry_observation_source_contract.py'), + meson.source_root(), + ], +) + +test( + 'kzt-wi254-loader-contract', + python3, + args: [ + files('kzt/test_wi254_loader_contract.py'), + meson.source_root(), + ], +) + +kzt_guest_link_map_reader_test = executable( + 'kzt-guest-link-map-reader', + files( + 'kzt/test_guest_link_map_reader.c', + '../../target/i386/latx/context/kzt_guest_link_map_reader.c', + ), + c_args: ['-DKZT_GUEST_LINK_MAP_READER_TEST'], + include_directories: include_directories('../..', '../../target/i386/latx/include'), +) + +test('kzt-guest-link-map-reader', kzt_guest_link_map_reader_test) + +kzt_elf_map_range_test = executable( + 'kzt-elf-map-range', + files( + 'kzt/test_elf_map_range.c', + '../../target/i386/latx/context/elfmap.c', + ), + include_directories: include_directories('../..', '../../target/i386/latx/include'), +) + +test('kzt-elf-map-range', kzt_elf_map_range_test) + +kzt_guest_dynamic_parser_test = executable( + 'kzt-guest-dynamic-parser', + files( + 'kzt/test_guest_dynamic_parser.c', + '../../target/i386/latx/context/kzt_guest_dynamic.c', + ), + include_directories: include_directories('../..', '../../target/i386/latx/include'), +) + +test('kzt-guest-dynamic-parser', kzt_guest_dynamic_parser_test) + +kzt_guest_dynamic_snapshot_test = executable( + 'kzt-guest-dynamic-snapshot', + files( + 'kzt/test_guest_dynamic_snapshot.c', + '../../target/i386/latx/context/kzt_guest_registry.c', + ), + c_args: ['-DKZT_GUEST_REGISTRY_TEST'], + include_directories: include_directories('../..', '../../target/i386/latx/include'), + dependencies: dependency('threads'), +) + +test('kzt-guest-dynamic-snapshot', kzt_guest_dynamic_snapshot_test) + +kzt_guest_dynamic_diagnostics_test = executable( + 'kzt-guest-dynamic-diagnostics', + files( + 'kzt/test_guest_dynamic_diagnostics.c', + '../../target/i386/latx/context/kzt_guest_dynamic_diagnostics.c', + ), + include_directories: include_directories('../..', '../../target/i386/latx/include'), +) + +test('kzt-guest-dynamic-diagnostics', kzt_guest_dynamic_diagnostics_test) + +kzt_observation_adapter_test = executable( + 'kzt-observation-adapter', + files( + 'kzt/test_observation_adapter.c', + '../../target/i386/latx/context/kzt_guest_registry.c', + '../../target/i386/latx/context/kzt_guest_link_map_reader.c', + '../../target/i386/latx/context/kzt_observation_adapter.c', + '../../target/i386/latx/context/kzt_guest_dynamic.c', + '../../target/i386/latx/context/kzt_guest_dynamic_diagnostics.c', + '../../target/i386/latx/context/kzt_guest_library_binding.c', + ), + c_args: [ + '-DKZT_GUEST_REGISTRY_TEST', + '-DKZT_GUEST_LINK_MAP_READER_TEST', + '-DKZT_GUEST_LIBRARY_BINDING_TEST', + ], + include_directories: include_directories('../..', '../../target/i386/latx/include'), + dependencies: dependency('threads'), +) + +test('kzt-observation-adapter', kzt_observation_adapter_test) + +kzt_registry_diagnostics_gate_test = executable( + 'kzt-registry-diagnostics-gate', + files( + 'kzt/test_registry_diagnostics_gate.c', + '../../target/i386/latx/context/kzt_guest_registry.c', + '../../target/i386/latx/context/kzt_guest_link_map_reader.c', + '../../target/i386/latx/context/kzt_observation_adapter.c', + '../../target/i386/latx/context/kzt_guest_dynamic.c', + '../../target/i386/latx/context/kzt_guest_dynamic_diagnostics.c', + '../../target/i386/latx/context/kzt_guest_library_binding.c', + ), + c_args: [ + '-DCONFIG_LATX_KZT', + '-DKZT_GUEST_REGISTRY_TEST', + '-DKZT_GUEST_LINK_MAP_READER_TEST', + ], + include_directories: include_directories('../..', '../../target/i386/latx/include'), + dependencies: dependency('threads'), +) + +test('kzt-registry-diagnostics-gate', kzt_registry_diagnostics_gate_test) + +kzt_patch_planner_test = executable( + 'kzt-patch-planner', + files( + 'kzt/test_patch_planner.c', + '../../target/i386/latx/context/kzt_patch_planner.c', + ), + include_directories: include_directories('../..', '../../target/i386/latx/include'), +) + +test('kzt-patch-planner', kzt_patch_planner_test) + +kzt_runtime_got_plt_candidate_test = executable( + 'kzt-runtime-got-plt-candidate', + files( + 'kzt/test_runtime_got_plt_candidate.c', + '../../target/i386/latx/context/kzt_runtime_got_plt_candidate.c', + ), + include_directories: include_directories('../..', '../../target/i386/latx/include'), +) + +test('kzt-runtime-got-plt-candidate', kzt_runtime_got_plt_candidate_test) + +kzt_runtime_candidate_enrichment_shadow_test = executable( + 'kzt-runtime-candidate-enrichment-shadow', + files( + 'kzt/test_runtime_candidate_enrichment_shadow.c', + '../../target/i386/latx/context/kzt_runtime_candidate_shadow.c', + '../../target/i386/latx/context/kzt_runtime_got_plt_candidate.c', + '../../target/i386/latx/context/kzt_owner_resolver.c', + '../../target/i386/latx/context/kzt_wrapper_probe.c', + '../../target/i386/latx/context/kzt_patch_planner.c', + '../../target/i386/latx/context/kzt_guest_registry.c', + ), + c_args: ['-DKZT_GUEST_REGISTRY_TEST'], + include_directories: include_directories('../..', '../../target/i386/latx/include'), + dependencies: dependency('threads'), +) + +test( + 'kzt-runtime-candidate-enrichment-shadow', + kzt_runtime_candidate_enrichment_shadow_test, +) + +kzt_rela_immediate_candidate_test = executable( + 'kzt-rela-immediate-candidate', + files( + 'kzt/test_rela_immediate_candidate.c', + '../../target/i386/latx/context/kzt_rela_immediate_candidate.c', + '../../target/i386/latx/context/kzt_patch_planner.c', + '../../target/i386/latx/context/kzt_patch_spike_guard.c', + '../../target/i386/latx/context/kzt_patch_spike_writer.c', + ), + include_directories: include_directories('../..', '../../target/i386/latx/include'), +) + +test('kzt-rela-immediate-candidate', kzt_rela_immediate_candidate_test) + +kzt_rela_request_enricher_test = executable( + 'kzt-rela-request-enricher', + files( + 'kzt/test_rela_request_enricher.c', + '../../target/i386/latx/context/kzt_rela_request_enricher.c', + '../../target/i386/latx/context/kzt_rela_immediate_candidate.c', + '../../target/i386/latx/context/kzt_patch_planner.c', + '../../target/i386/latx/context/kzt_patch_spike_guard.c', + '../../target/i386/latx/context/kzt_patch_spike_writer.c', + '../../target/i386/latx/context/kzt_owner_resolver.c', + '../../target/i386/latx/context/kzt_wrapper_probe.c', + '../../target/i386/latx/context/kzt_guest_registry.c', + ), + c_args: ['-DKZT_GUEST_REGISTRY_TEST'], + include_directories: include_directories('../..', '../../target/i386/latx/include'), + dependencies: dependency('threads'), +) + +test('kzt-rela-request-enricher', kzt_rela_request_enricher_test) + +kzt_wi238_structured_diagnostics_gate_test = executable( + 'kzt-wi238-structured-diagnostics-gate', + files( + 'kzt/test_wi238_structured_diagnostics.c', + '../../target/i386/latx/context/kzt_rela_diagnostics.c', + '../../target/i386/latx/context/kzt_patch_planner.c', + '../../target/i386/latx/context/kzt_patch_spike_guard.c', + ), + include_directories: include_directories('../..', '../../target/i386/latx/include'), + dependencies: dependency('threads'), +) + +test('kzt-wi238-structured-diagnostics-gate', + kzt_wi238_structured_diagnostics_gate_test) + +kzt_owner_resolver_test = executable( + 'kzt-owner-resolver', + files( + 'kzt/test_owner_resolver.c', + '../../target/i386/latx/context/kzt_owner_resolver.c', + '../../target/i386/latx/context/kzt_guest_registry.c', + ), + c_args: ['-DKZT_GUEST_REGISTRY_TEST'], + include_directories: include_directories('../..', '../../target/i386/latx/include'), + dependencies: dependency('threads'), +) + +test('kzt-owner-resolver', kzt_owner_resolver_test) + + +kzt_patch_spike_guard_test = executable( + 'kzt-patch-spike-guard', + files( + 'kzt/test_patch_spike_guard.c', + 'kzt/kzt_test_options.c', + '../../target/i386/latx/context/kzt_patch_spike_guard.c', + ), + c_args: ['-DCONFIG_LATX_KZT'], + include_directories: include_directories('../..', '../../target/i386/latx/include'), + dependencies: dependency('threads'), +) + +test('kzt-patch-spike-guard', kzt_patch_spike_guard_test) + +kzt_patch_spike_writer_test = executable( + 'kzt-patch-spike-writer', + files( + 'kzt/test_patch_spike_writer.c', + 'kzt/kzt_test_options.c', + '../../target/i386/latx/context/kzt_patch_planner.c', + '../../target/i386/latx/context/kzt_patch_spike_guard.c', + '../../target/i386/latx/context/kzt_patch_spike_writer.c', + ), + c_args: ['-DCONFIG_LATX_KZT'], + include_directories: include_directories('../..', '../../target/i386/latx/include'), +) + +test('kzt-patch-spike-writer', kzt_patch_spike_writer_test) + +kzt_wrapper_probe_test = executable( + 'kzt-wrapper-probe', + files( + 'kzt/test_wrapper_probe.c', + '../../target/i386/latx/context/kzt_wrapper_probe.c', + ), + include_directories: include_directories('../..', '../../target/i386/latx/include'), +) + +test('kzt-wrapper-probe', kzt_wrapper_probe_test) + +kzt_wrapper_bridge_provider_test = executable( + 'kzt-wrapper-bridge-provider', + files( + 'kzt/test_wrapper_bridge_provider.c', + '../../target/i386/latx/context/kzt_wrapper_bridge_provider.c', + '../../target/i386/latx/context/kzt_wrapper_probe.c', + ), + include_directories: include_directories('../..', '../../target/i386/latx/include'), +) + +test('kzt-wrapper-bridge-provider', kzt_wrapper_bridge_provider_test) + +kzt_rela_runtime_helper_chain_test = executable( + 'kzt-rela-runtime-helper-chain', + files( + 'kzt/test_rela_runtime_helper_chain.c', + '../../target/i386/latx/context/kzt_rela_runtime_bridge.c', + '../../target/i386/latx/context/kzt_wrapper_bridge_provider.c', + '../../target/i386/latx/context/kzt_wrapper_probe.c', + '../../target/i386/latx/context/kzt_rela_request_enricher.c', + '../../target/i386/latx/context/kzt_rela_immediate_candidate.c', + '../../target/i386/latx/context/kzt_patch_planner.c', + '../../target/i386/latx/context/kzt_patch_spike_guard.c', + '../../target/i386/latx/context/kzt_patch_spike_writer.c', + '../../target/i386/latx/context/kzt_owner_resolver.c', + '../../target/i386/latx/context/kzt_guest_registry.c', + '../../target/i386/latx/context/kzt_bridge_exact.c', + ), + c_args: ['-DKZT_GUEST_REGISTRY_TEST'], + include_directories: include_directories('../..', '../../target/i386/latx/include'), + dependencies: dependency('threads'), +) + +test('kzt-rela-runtime-helper-chain', kzt_rela_runtime_helper_chain_test) + +kzt_jump_slot_route_test = executable( + 'kzt-jump-slot-route', + files( + 'kzt/test_jump_slot_route.c', + '../../target/i386/latx/context/kzt_jump_slot_route.c', + ), + include_directories: include_directories('../..', '../../target/i386/latx/include'), +) + +test('kzt-jump-slot-route', kzt_jump_slot_route_test) + +kzt_wi256_lazy_binding_test = executable( + 'kzt-wi256-lazy-binding', + files( + 'kzt/test_wi256_lazy_binding.c', + '../../target/i386/latx/context/kzt_lazy_binding.c', + ), + include_directories: include_directories('../..', '../../target/i386/latx/include'), +) + +test('kzt-wi256-lazy-binding', kzt_wi256_lazy_binding_test) + +kzt_wi256_lazy_diagnostics_test = executable( + 'kzt-wi256-lazy-diagnostics', + files( + 'kzt/test_wi256_lazy_diagnostics.c', + '../../target/i386/latx/context/kzt_lazy_diagnostics.c', + ), + c_args: ['-DCONFIG_LATX_KZT'], + include_directories: include_directories('../..', '../../target/i386/latx/include'), +) + +test('kzt-wi256-lazy-diagnostics', kzt_wi256_lazy_diagnostics_test) + +test( + 'kzt-wi256-lazy-diagnostics-source-contract', + python3, + args: [ + files('kzt/test_wi256_lazy_diagnostics_source_contract.py'), + meson.source_root(), + ], +) + +kzt_wi256_plt_resolver_adapter_test = executable( + 'kzt-wi256-plt-resolver-adapter', + files( + 'kzt/test_wi256_plt_resolver_adapter.c', + '../../target/i386/latx/context/kzt_plt_resolver_adapter.c', + ), + c_args: ['-DKZT_PLT_RESOLVER_ADAPTER_TEST'], + include_directories: include_directories('../..', '../../target/i386/latx/include'), +) + +test('kzt-wi256-plt-resolver-adapter', + kzt_wi256_plt_resolver_adapter_test) + +kzt_wi256_lazy_production_bridge_test = executable( + 'kzt-wi256-lazy-production-bridge', + files( + 'kzt/test_wi256_lazy_production_bridge.c', + 'kzt/kzt_test_options.c', + '../../target/i386/latx/context/kzt_lazy_binding.c', + '../../target/i386/latx/context/kzt_jump_slot_production.c', + '../../target/i386/latx/context/kzt_jump_slot_route.c', + '../../target/i386/latx/context/kzt_rela_runtime_bridge.c', + '../../target/i386/latx/context/kzt_wrapper_bridge_provider.c', + '../../target/i386/latx/context/kzt_wrapper_probe.c', + '../../target/i386/latx/context/kzt_rela_request_enricher.c', + '../../target/i386/latx/context/kzt_rela_diagnostics.c', + '../../target/i386/latx/context/kzt_rela_immediate_candidate.c', + '../../target/i386/latx/context/kzt_patch_planner.c', + '../../target/i386/latx/context/kzt_patch_spike_guard.c', + '../../target/i386/latx/context/kzt_patch_spike_writer.c', + '../../target/i386/latx/context/kzt_owner_resolver.c', + '../../target/i386/latx/context/kzt_runtime_got_plt_candidate.c', + '../../target/i386/latx/context/kzt_runtime_candidate_shadow.c', + '../../target/i386/latx/context/kzt_guest_registry.c', + '../../target/i386/latx/context/kzt_guest_library_binding.c', + '../../target/i386/latx/context/kzt_bridge_exact.c', + ), + c_args: [ + '-DCONFIG_LATX_KZT', + '-DKZT_GUEST_REGISTRY_TEST', + '-DKZT_JUMP_SLOT_PRODUCTION_TEST', + ], + include_directories: include_directories('../..', '../../target/i386/latx/include'), + dependencies: dependency('threads'), +) + +test('kzt-wi256-lazy-production-bridge', + kzt_wi256_lazy_production_bridge_test) + +test( + 'kzt-wi256-plt-resolver-source-contract', + python3, + args: [ + files('kzt/test_wi256_plt_resolver_source_contract.py'), + meson.source_root(), + ], +) + +test( + 'kzt-wi237-jump-slot-route-contract', + python3, + args: [ + files('kzt/test_wi237_jump_slot_route_contract.py'), + meson.source_root(), + ], +) + +kzt_wi236_enrichment_matrix_test = executable( + 'kzt-wi236-enrichment-matrix', + files( + 'kzt/test_wi236_enrichment_matrix.c', + '../../target/i386/latx/context/kzt_guest_registry.c', + '../../target/i386/latx/context/kzt_owner_resolver.c', + '../../target/i386/latx/context/kzt_patch_planner.c', + '../../target/i386/latx/context/kzt_patch_spike_guard.c', + '../../target/i386/latx/context/kzt_patch_spike_writer.c', + '../../target/i386/latx/context/kzt_rela_immediate_candidate.c', + ), + include_directories: include_directories('../..', '../../target/i386/latx/include'), + dependencies: dependency('threads'), +) + +test('kzt-wi236-enrichment-matrix', kzt_wi236_enrichment_matrix_test)