From da3981c124ee165d8e5eba286cc65459273776e1 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 10 Jul 2026 10:53:43 +0200 Subject: [PATCH 01/15] [CoreCLR] Experimental: dlopen()-based assembly store loading Prototype (additive, CoreCLR-only; Mono untouched) that lets the runtime locate the assembly store via dlopen()+dlsym() instead of parsing the APK ZIP central directory, mmapping the wrapper .so and walking its ELF section headers by hand. Build side (opt-in via -p:_AndroidDlopenAssemblyStore=true): * New DlopenAssemblyStoreGenerator wraps the store blob into a shared library whose payload lives in a *loadable* section (SHF_ALLOC, covered by PT_LOAD) pointed at by an exported dynamic symbol `_assembly_store_start`. It emits a tiny `.incbin` stub assembled with the shipped `llvm-mc` and linked with `ld` (no clang needed). This differs from DSOWrapperGenerator, which uses `llvm-objcopy --add-section` to inject a *non-loadable* section. * WrapAssembliesAsSharedLibraries gains a UseDlopenAssemblyStore switch, plumbed from the new _AndroidDlopenAssemblyStore MSBuild property. The existing DSOWrapperGenerator path is left intact. Runtime side (self-detecting, no runtime flag): * AssemblyStore::map is refactored to extract configure_from_payload(); a new map_from_pointer() configures the store from an in-memory payload pointer. * Host::gather_assemblies_and_libraries first tries dlopen(libassembly-store.so) + dlsym(_assembly_store_start). On success it skips the ZIP scan entirely; if the symbol is absent it falls back to the existing scan, so a prebuilt host handles both old and new APKs. Feasibility of the loadable-symbol wrapper was validated on an arm64 emulator (dlopen+dlsym resolves the payload; objcopy --add-section does not). Startup A/B benchmarking is still pending. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 527f04b6-c95a-4a66-b90f-4c52ba5496f3 --- .../Tasks/WrapAssembliesAsSharedLibraries.cs | 11 +- .../Utilities/DlopenAssemblyStoreGenerator.cs | 110 ++++++++++++++++++ .../Xamarin.Android.Common.targets | 5 + src/native/clr/host/assembly-store.cc | 40 ++++--- src/native/clr/host/host.cc | 33 ++++++ src/native/clr/include/host/assembly-store.hh | 7 ++ src/native/clr/include/host/host.hh | 4 + 7 files changed, 192 insertions(+), 18 deletions(-) create mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/DlopenAssemblyStoreGenerator.cs diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/WrapAssembliesAsSharedLibraries.cs b/src/Xamarin.Android.Build.Tasks/Tasks/WrapAssembliesAsSharedLibraries.cs index f2d3a7b385d..c9e9a8d163b 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/WrapAssembliesAsSharedLibraries.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/WrapAssembliesAsSharedLibraries.cs @@ -33,6 +33,13 @@ public class WrapAssembliesAsSharedLibraries : AndroidTask public bool UseAssemblyStore { get; set; } + /// + /// EXPERIMENTAL (CoreCLR only): when true, the assembly store is wrapped into a shared library + /// whose payload is exported via the _assembly_store_start dynamic symbol, so the runtime + /// can locate it with dlopen+dlsym instead of parsing the APK ZIP directory. + /// + public bool UseDlopenAssemblyStore { get; set; } + [Required] public ITaskItem [] ResolvedAssemblies { get; set; } = []; @@ -69,7 +76,9 @@ void WrapAssemblyStores (DSOWrapperGenerator.Config dsoWrapperConfig, PackageFil var arch = MonoAndroidHelper.AbiToTargetArch (abi); var archive_path = MakeArchiveLibPath (abi, "lib" + Path.GetFileName (store_path)); - var wrapped_source_path = DSOWrapperGenerator.WrapIt (Log, dsoWrapperConfig, arch, store_path, Path.GetFileName (archive_path)); + var wrapped_source_path = UseDlopenAssemblyStore + ? DlopenAssemblyStoreGenerator.WrapIt (Log, dsoWrapperConfig, arch, store_path, Path.GetFileName (archive_path)) + : DSOWrapperGenerator.WrapIt (Log, dsoWrapperConfig, arch, store_path, Path.GetFileName (archive_path)); files.AddItem (wrapped_source_path, archive_path); } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/DlopenAssemblyStoreGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/DlopenAssemblyStoreGenerator.cs new file mode 100644 index 00000000000..6b9d9d4f3be --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/DlopenAssemblyStoreGenerator.cs @@ -0,0 +1,110 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.IO; + +using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Utilities; +using Xamarin.Android.Tools; + +namespace Xamarin.Android.Tasks; + +/// +/// EXPERIMENTAL / ADDITIVE (CoreCLR only). +/// +/// Produces an assembly-store wrapper shared library whose payload lives in a +/// *loadable* ELF section (SHF_ALLOC, covered by a PT_LOAD segment) and is +/// pointed at by an exported dynamic symbol (_assembly_store_start). +/// +/// This differs from , which injects the +/// payload with llvm-objcopy --add-section into a *non-loadable* section +/// and requires the runtime to locate the store inside the APK (ZIP central +/// directory parsing), mmap it and walk the ELF section headers by hand. +/// +/// With this layout the CoreCLR runtime can instead simply +/// dlopen("libassembly-store.so") + dlsym("_assembly_store_start") +/// and let the dynamic linker locate + map the payload. +/// +/// The wrapper is built with the shipped binutils (llvm-mc to assemble a +/// tiny .incbin stub, ld to link a shared object). No clang is +/// required. +/// +static class DlopenAssemblyStoreGenerator +{ + public const string PayloadStartSymbol = "_assembly_store_start"; + + // 16k so the payload is page-aligned on both 4k and 16k page-size devices. + const int PayloadAlignment = 16384; + + static (string Triple, string ElfArch, int MaxPageSize) GetArchToolInfo (AndroidTargetArch arch) => arch switch { + AndroidTargetArch.Arm64 => ("aarch64-linux-android", "aarch64linux", 16384), + AndroidTargetArch.Arm => ("armv7-linux-androideabi", "armelf_linux_eabi", 4096), + AndroidTargetArch.X86 => ("i686-linux-android", "elf_i386", 4096), + AndroidTargetArch.X86_64 => ("x86_64-linux-android", "elf_x86_64", 16384), + _ => throw new NotSupportedException ($"Unsupported Android target architecture: {arch}"), + }; + + /// + /// Wraps (the raw assembly-store blob) into a loadable-symbol + /// shared library and returns the path to the produced .so. + /// + public static string WrapIt (TaskLoggingHelper log, DSOWrapperGenerator.Config config, AndroidTargetArch targetArch, string payloadFilePath, string outputFileName) + { + var toolInfo = GetArchToolInfo (targetArch); + + string outputDir = Path.Combine (config.BaseOutputDirectory, MonoAndroidHelper.ArchToRid (targetArch), "wrapped-dlopen"); + Directory.CreateDirectory (outputDir); + + string outputFile = Path.Combine (outputDir, outputFileName); + string objFile = Path.Combine (outputDir, outputFileName + ".o"); + string asmFile = Path.Combine (outputDir, outputFileName + ".S"); + + log.LogDebugMessage ($"[{targetArch}] Wrapping '{payloadFilePath}' into loadable-symbol shared library '{outputFile}'"); + + // The `.incbin` uses an absolute path so we don't depend on the assembler's working directory. + string incbinPath = payloadFilePath.Replace ("\\", "\\\\").Replace ("\"", "\\\""); + string asm = $""" + .section .payload, "a" + .balign {PayloadAlignment} + .globl {PayloadStartSymbol} + {PayloadStartSymbol}: + .incbin "{incbinPath}" + + """; + File.WriteAllText (asmFile, asm); + + string llvmMc = Path.Combine (config.AndroidBinUtilsDirectory, MonoAndroidHelper.GetExecutablePath (config.AndroidBinUtilsDirectory, "llvm-mc")); + var mcArgs = new List { + "--filetype=obj", + $"-triple={toolInfo.Triple}", + $"-o {MonoAndroidHelper.QuoteFileNameArgument (objFile)}", + MonoAndroidHelper.QuoteFileNameArgument (asmFile), + }; + + int ret = MonoAndroidHelper.RunProcess (llvmMc, String.Join (" ", mcArgs), log); + if (ret != 0) { + log.LogError ($"Failed to assemble assembly-store wrapper for '{targetArch}' (llvm-mc exit code {ret})"); + return outputFile; + } + + string ld = Path.Combine (config.AndroidBinUtilsDirectory, MonoAndroidHelper.GetExecutablePath (config.AndroidBinUtilsDirectory, "ld")); + var ldArgs = new List { + "--shared", + $"-soname {MonoAndroidHelper.QuoteFileNameArgument (outputFileName)}", + $"-m {toolInfo.ElfArch}", + $"-z max-page-size={toolInfo.MaxPageSize}", + "--build-id=sha1", + $"--export-dynamic-symbol={PayloadStartSymbol}", + $"-o {MonoAndroidHelper.QuoteFileNameArgument (outputFile)}", + MonoAndroidHelper.QuoteFileNameArgument (objFile), + }; + + ret = MonoAndroidHelper.RunProcess (ld, String.Join (" ", ldArgs), log); + if (ret != 0) { + log.LogError ($"Failed to link assembly-store wrapper for '{targetArch}' (ld exit code {ret})"); + return outputFile; + } + + return outputFile; + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 5cc9126552c..bc6a96edcb8 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -326,6 +326,10 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. false true <_AndroidUseAssemblyStore>$(AndroidUseAssemblyStore) + + <_AndroidDlopenAssemblyStore Condition=" '$(_AndroidDlopenAssemblyStore)' == '' ">false - <_AndroidDlopenAssemblyStore Condition=" '$(_AndroidDlopenAssemblyStore)' == '' ">false + + <_AndroidDlopenAssemblyStore>false + <_AndroidDlopenAssemblyStore>true diff --git a/src/native/CMakeLists.txt b/src/native/CMakeLists.txt index 26139682952..335021fefb3 100644 --- a/src/native/CMakeLists.txt +++ b/src/native/CMakeLists.txt @@ -679,7 +679,6 @@ else() if (IS_CLR_RUNTIME) add_subdirectory(clr/pinvoke-override) add_subdirectory(clr/host) - add_subdirectory(clr/startup) endif() if (IS_NAOT_RUNTIME) diff --git a/src/native/clr/host/CMakeLists.txt b/src/native/clr/host/CMakeLists.txt index e18d1a2531d..40818c6b7da 100644 --- a/src/native/clr/host/CMakeLists.txt +++ b/src/native/clr/host/CMakeLists.txt @@ -150,7 +150,6 @@ macro(lib_target_options TARGET_NAME) ${TARGET_NAME} xa::xamarin-app ${SHARED_LIB_NAME} - xa::xamarin-startup xa::runtime-base xa::java-interop xa::pinvoke-override-precompiled diff --git a/src/native/clr/host/assembly-store.cc b/src/native/clr/host/assembly-store.cc index 0792695e0c0..3960abd8669 100644 --- a/src/native/clr/host/assembly-store.cc +++ b/src/native/clr/host/assembly-store.cc @@ -256,32 +256,6 @@ auto AssemblyStore::open_assembly (std::string_view const& name, int64_t &size) return assembly_data; } -void AssemblyStore::map (int fd, std::string_view const& apk_path, std::string_view const& store_path, uint32_t offset, uint32_t size) noexcept -{ - detail::mmap_info assembly_store_map = Util::mmap_file (fd, offset, size, store_path); - - auto [payload_start, payload_size] = Util::get_wrapper_dso_payload_pointer_and_size (assembly_store_map, store_path); - log_debug (LOG_ASSEMBLY, "Adjusted assembly store pointer: {:p}; size: {}"sv, payload_start, payload_size); - - auto get_full_store_path = [&apk_path, &store_path]() -> std::string { - std::string full_store_path; - - if (!apk_path.empty ()) { - full_store_path.append (apk_path); - // store path will be relative, to the apk - full_store_path.append ("!/"sv); - full_store_path.append (store_path); - } else { - full_store_path.append (store_path); - } - - return full_store_path; - }; - - configure_from_payload (payload_start, get_full_store_path); - log_debug (LOG_ASSEMBLY, "Mapped assembly store {}"sv, get_full_store_path ()); -} - void AssemblyStore::configure_from_payload (void *payload_start, const std::function& get_full_store_path) noexcept { auto header = static_cast(payload_start); diff --git a/src/native/clr/host/host.cc b/src/native/clr/host/host.cc index b687faba791..08c5b973101 100644 --- a/src/native/clr/host/host.cc +++ b/src/native/clr/host/host.cc @@ -30,7 +30,6 @@ #include #include #include -#include using namespace xamarin::android; @@ -137,38 +136,6 @@ bool Host::clr_external_assembly_probe (const char *path, void **data_start, int return log_and_return (path, *data_start, *size); } -auto Host::zip_scan_callback (std::string_view const& apk_path, int apk_fd, dynamic_local_string const& entry_name, uint32_t offset, uint32_t size) -> bool -{ - log_debug (LOG_ASSEMBLY, "zip entry: {}"sv, entry_name.get ()); - if (!found_assembly_store) { - found_assembly_store = Zip::assembly_store_file_path.compare (0, entry_name.length (), entry_name.get ()) == 0; - if (found_assembly_store) { - log_debug (LOG_ASSEMBLY, "Found assembly store in '{}': {}"sv, apk_path, Zip::assembly_store_file_path); - AssemblyStore::map (apk_fd, apk_path, Zip::assembly_store_file_path, offset, size); - return false; // This will make the scanner keep the APK open - } - } - - if (!AndroidSystem::is_embedded_dso_mode_enabled () || !entry_name.starts_with (Zip::lib_prefix) || !entry_name.ends_with (Constants::dso_suffix)) { - return false; - } - - log_debug (LOG_ASSEMBLY, "Found shared library in '{}': {}"sv, apk_path, entry_name.get ()); - std::string_view lib_name { entry_name.get () + Zip::lib_prefix.length () }; - hash_t name_hash = xxhash::hash (lib_name.data (), lib_name.length ()); - log_debug (LOG_ASSEMBLY, "Library name is: {}; hash == 0x{:x}", lib_name, name_hash); - - DSOApkEntry *apk_entry = MonodroidDl::find_dso_apk_entry (name_hash); - if (apk_entry == nullptr) { - return false; - } - - log_debug (LOG_ASSEMBLY, "Found matching DSO APK entry"); - apk_entry->fd = apk_fd; - apk_entry->offset = offset; - return false; -} - [[gnu::always_inline]] void Host::scan_filesystem_for_assemblies_and_libraries () noexcept { @@ -222,113 +189,53 @@ void Host::scan_filesystem_for_assemblies_and_libraries () noexcept } log_debug (LOG_ASSEMBLY, "Found assembly store in '{}/{}'"sv, native_lib_dir, Constants::assembly_store_file_name); - int store_fd = openat (dir_fd, cur->d_name, O_RDONLY); - if (store_fd < 0) { - Helpers::abort_application ( - LOG_ASSEMBLY, - std::format ( - "Unable to open assembly store '{}/{}' for reading. {}"sv, - native_lib_dir, - Constants::assembly_store_file_name, - std::strerror (errno) - ) - ); - } - - auto file_size = Util::get_file_size_at (dir_fd, cur->d_name); - if (!file_size) { - // get_file_size_at logged errno for us - Helpers::abort_application ( - LOG_ASSEMBLY, - std::format ( - "Unable to map assembly store '{}/{}'"sv, - native_lib_dir, - Constants::assembly_store_file_name - ) - ); - } - AssemblyStore::map (store_fd, cur->d_name, 0, static_cast(file_size.value ())); - close (store_fd); + std::string store_path = native_lib_dir; + store_path.append ("/"sv); + store_path.append (cur->d_name); + map_assembly_store_via_dlopen (store_path.c_str ()); break; // we've found all we need } } while (true); closedir (lib_dir); } -void Host::gather_assemblies_and_libraries (jstring_array_wrapper& runtimeApks, bool have_split_apks) +void Host::gather_assemblies_and_libraries ([[maybe_unused]] jstring_array_wrapper& runtimeApks, [[maybe_unused]] bool have_split_apks) { if (!AndroidSystem::is_embedded_dso_mode_enabled ()) { scan_filesystem_for_assemblies_and_libraries (); return; } - // EXPERIMENTAL fast path (self-detecting): when the assembly store was wrapped so that its - // payload is exposed via the `_assembly_store_start` dynamic symbol, the dynamic linker has - // already located and mapped it out of the APK for us. In that case we skip parsing the APK - // ZIP central directory entirely and just resolve the payload with dlopen()+dlsym(). - if (try_map_assembly_store_via_dlopen ()) { - return; - } - - int64_t apk_count = static_cast(runtimeApks.get_length ()); - bool got_split_config_abi_apk = false; - std::string_view base_apk{}; - - for (int64_t i = 0; i < apk_count; i++) { - std::string_view apk_file = runtimeApks [static_cast(i)].get_string_view (); - - if (have_split_apks) { - bool scan_apk = false; - - // With split configs we need to scan only the abi apk, because both the assembly stores and the runtime - // configuration blob **should be** in `lib/{ARCH}`, which in turn lives in the split config APK - if (!got_split_config_abi_apk && apk_file.ends_with (Constants::split_config_abi_apk_name.data ())) { - got_split_config_abi_apk = scan_apk = true; - } else if (base_apk.empty () && apk_file.ends_with (Constants::base_apk_name)) { - base_apk = apk_file; - } - - if (!scan_apk) { - continue; - } - } - - Zip::scan_archive (apk_file, zip_scan_callback); - } - - // This apparently can happen now... It seems that sometimes (when and why? No idea) when AAB format is used, bundletool - // won't put the native libraries in a separate split config file, but it will instead put **all** of the ABIs - // in base.apk - if (have_split_apks && !got_split_config_abi_apk) { - abort_unless (!base_apk.empty (), "Split config APKs are used, but no ABI config was found and no base.apk was encountered."); - Zip::scan_archive (base_apk, zip_scan_callback); - } + // The assembly store is wrapped as a real shared library whose payload is exposed via the + // `_assembly_store_start` dynamic symbol, so the dynamic linker has already located and mapped + // it out of the APK for us. We resolve the payload with dlopen()+dlsym() instead of parsing the + // APK ZIP central directory ourselves. + map_assembly_store_via_dlopen (Constants::assembly_store_file_name.data ()); } -auto Host::try_map_assembly_store_via_dlopen () noexcept -> bool +void Host::map_assembly_store_via_dlopen (const char *store_path) noexcept { - const char *store_name = Constants::assembly_store_file_name.data (); - - void *handle = ::dlopen (store_name, RTLD_NOW | RTLD_GLOBAL); - if (handle == nullptr) { - log_debug (LOG_ASSEMBLY, "dlopen('{}') failed ({}); falling back to APK scan"sv, optional_string (store_name), optional_string (::dlerror ())); - return false; + void *handle = ::dlopen (store_path, RTLD_NOW | RTLD_GLOBAL); + if (handle == nullptr) [[unlikely]] { + Helpers::abort_application ( + LOG_ASSEMBLY, + std::format ("Unable to dlopen() assembly store '{}': {}"sv, optional_string (store_path), optional_string (::dlerror ())) + ); } + // NOTE: intentionally not calling dlclose() - we keep the store mapped for the lifetime of the app. void *payload = ::dlsym (handle, DLOPEN_ASSEMBLY_STORE_SYMBOL.data ()); - if (payload == nullptr) { - // Not a dlopen-style assembly store (no exported symbol); keep the legacy behavior. - log_debug (LOG_ASSEMBLY, "Symbol '{}' not found in '{}'; falling back to APK scan"sv, DLOPEN_ASSEMBLY_STORE_SYMBOL, optional_string (store_name)); - ::dlclose (handle); - return false; + if (payload == nullptr) [[unlikely]] { + Helpers::abort_application ( + LOG_ASSEMBLY, + std::format ("Assembly store '{}' does not export the '{}' symbol"sv, optional_string (store_path), DLOPEN_ASSEMBLY_STORE_SYMBOL) + ); } - // NOTE: intentionally not calling dlclose() on success - we keep the store mapped. - log_debug (LOG_ASSEMBLY, "Assembly store payload via dynamic symbol: {:p} ({})"sv, payload, optional_string (store_name)); - AssemblyStore::configure_from_payload (payload, [store_name]() -> std::string { return std::string { store_name }; }); + log_debug (LOG_ASSEMBLY, "Assembly store payload via dynamic symbol: {:p} ({})"sv, payload, optional_string (store_path)); + AssemblyStore::configure_from_payload (payload, [store_path]() -> std::string { return std::string { store_path }; }); found_assembly_store = true; - return true; } [[gnu::always_inline]] diff --git a/src/native/clr/include/host/assembly-store.hh b/src/native/clr/include/host/assembly-store.hh index 2bb2a39ee25..64da6bf673e 100644 --- a/src/native/clr/include/host/assembly-store.hh +++ b/src/native/clr/include/host/assembly-store.hh @@ -17,17 +17,9 @@ namespace xamarin::android { public: static auto open_assembly (std::string_view const& name, int64_t &size) noexcept -> void*; - static void map (int fd, std::string_view const& apk_path, std::string_view const& store_path, uint32_t offset, uint32_t size) noexcept; - - static void map (int fd, std::string_view const& file_path, uint32_t offset, uint32_t size) noexcept - { - map (fd, {}, file_path, offset, size); - } - - // Configure the store directly from an in-memory payload pointer (e.g. obtained via - // dlopen()+dlsym() of the `_assembly_store_start` dynamic symbol) instead of locating and - // mmapping it out of the APK. `get_full_store_path` is invoked only to build diagnostics - // if the payload turns out to be invalid. + // Configure the store directly from an in-memory payload pointer (obtained via + // dlopen()+dlsym() of the `_assembly_store_start` dynamic symbol). `get_full_store_path` + // is invoked only to build diagnostics if the payload turns out to be invalid. static void configure_from_payload (void *payload_start, const std::function& get_full_store_path) noexcept; private: diff --git a/src/native/clr/include/host/host.hh b/src/native/clr/include/host/host.hh index 4373ee1218a..629937146a5 100644 --- a/src/native/clr/include/host/host.hh +++ b/src/native/clr/include/host/host.hh @@ -37,9 +37,8 @@ namespace xamarin::android { // Must match `DlopenAssemblyStoreGenerator.PayloadStartSymbol` in the build tasks. static constexpr std::string_view DLOPEN_ASSEMBLY_STORE_SYMBOL { "_assembly_store_start" }; - static auto zip_scan_callback (std::string_view const& apk_path, int apk_fd, dynamic_local_string const& entry_name, uint32_t offset, uint32_t size) -> bool; static void gather_assemblies_and_libraries (jstring_array_wrapper& runtimeApks, bool have_split_apks); - static auto try_map_assembly_store_via_dlopen () noexcept -> bool; + static void map_assembly_store_via_dlopen (const char *store_path) noexcept; static void scan_filesystem_for_assemblies_and_libraries () noexcept; static size_t clr_get_runtime_property (const char *key, char *value_buffer, size_t value_buffer_size, void *contract_context) noexcept; diff --git a/src/native/clr/include/runtime-base/monodroid-dl.hh b/src/native/clr/include/runtime-base/monodroid-dl.hh index cc2973b4515..41d56e092c4 100644 --- a/src/native/clr/include/runtime-base/monodroid-dl.hh +++ b/src/native/clr/include/runtime-base/monodroid-dl.hh @@ -84,23 +84,6 @@ namespace xamarin::android return &dso_names_data[dso->name_index]; } - [[gnu::flatten]] - static auto find_dso_apk_entry (hash_t hash) -> DSOApkEntry* - { - auto equal = [](DSOApkEntry const& entry, hash_t key) -> bool { return entry.name_hash == key; }; - auto less_than = [](DSOApkEntry const& entry, hash_t key) -> bool { return entry.name_hash < key; }; - ssize_t idx = Search::binary_search ( - hash, - dso_apk_entries, application_config.number_of_shared_libraries - ); - - if (idx >= 0) [[likely]] { - return &dso_apk_entries[idx]; - } - - return nullptr; - } - [[gnu::flatten]] static auto monodroid_dlopen (DSOCacheEntry *dso, std::string_view const& name, int flags) noexcept -> void* { @@ -122,18 +105,6 @@ namespace xamarin::android std::string_view dso_name = get_dso_name (dso); StartupAwareLock lock (dso_handle_write_lock); -#if defined (RELEASE) - if (AndroidSystem::is_embedded_dso_mode_enabled ()) { - DSOApkEntry *apk_entry = find_dso_apk_entry (dso->real_name_hash); - if (apk_entry != nullptr && apk_entry->fd != -1) { - dso->handle = DsoLoader::load (apk_entry->fd, apk_entry->offset, dso_name, flags, dso->is_jni_library); - } - - if (dso->handle != nullptr) { - return dso->handle; - } - } -#endif dso->handle = AndroidSystem::load_dso_from_any_directories (dso_name, flags, dso->is_jni_library); if (dso->handle != nullptr) { diff --git a/src/native/clr/include/startup/zip.hh b/src/native/clr/include/startup/zip.hh deleted file mode 100644 index df8fc674a2c..00000000000 --- a/src/native/clr/include/startup/zip.hh +++ /dev/null @@ -1,106 +0,0 @@ -#pragma once - -#include - -#include -#include - -#include -#include -#include - -namespace xamarin::android { - namespace detail { - template - concept ByteArrayContainer = requires (T a) { - a.size (); - a.data (); - requires std::same_as; - }; - } - - class Zip - { - public: - // Returns `true` if the entry was something we need. - using ScanCallbackFn = bool(std::string_view const& apk_path, int apk_fd, dynamic_local_string const& entry_name, uint32_t offset, uint32_t size); - - struct zip_scan_state - { - int file_fd; - std::string_view const& file_name; - const char * const prefix; - uint32_t prefix_len; - size_t buf_offset; - uint16_t compression_method; - uint32_t local_header_offset; - uint32_t data_offset; - uint32_t file_size; - bool bundled_assemblies_slow_path; - uint32_t max_assembly_name_size; - uint32_t max_assembly_file_name_size; - }; - - private: - static inline constexpr off_t ZIP_EOCD_LEN = 22; - static inline constexpr off_t ZIP_CENTRAL_LEN = 46; - static inline constexpr off_t ZIP_LOCAL_LEN = 30; - - static inline constexpr std::string_view ZIP_CENTRAL_MAGIC { "PK\1\2" }; - static inline constexpr std::string_view ZIP_LOCAL_MAGIC { "PK\3\4" }; - static inline constexpr std::string_view ZIP_EOCD_MAGIC { "PK\5\6" }; - - static constexpr std::string_view zip_path_separator { "/" }; - static constexpr std::string_view apk_lib_dir_name { "lib" }; - - static constexpr size_t lib_prefix_size = calc_size(apk_lib_dir_name, zip_path_separator, Constants::android_lib_abi, zip_path_separator); - static constexpr auto lib_prefix_array = concat_string_views (apk_lib_dir_name, zip_path_separator, Constants::android_lib_abi, zip_path_separator); - - public: - // .data() must be used otherwise string_view length will include the trailing \0 in the array - static constexpr std::string_view lib_prefix { lib_prefix_array.data () }; - - private: - static constexpr size_t assembly_store_file_path_size = calc_size(lib_prefix, Constants::assembly_store_file_name); - static constexpr auto assembly_store_file_path_array = concat_string_views (lib_prefix, Constants::assembly_store_file_name); - - public: - // .data() must be used otherwise string_view length will include the trailing \0 in the array - static constexpr std::string_view assembly_store_file_path { assembly_store_file_path_array.data () }; - - public: - // Scans the ZIP archive for any entries matching the `lib/{ARCH}/` prefix and calls `entry_cb` - // for each of them. If the callback returns `false` for all of the entries (meaning none of them - // was interesting/useful), then the APK file descriptor is closed. Otherwise, the descriptor is - // kept open since we will need it later on. - static void scan_archive (std::string_view const& apk_path, ScanCallbackFn entry_cb) noexcept; - - private: - static std::tuple get_assemblies_prefix_and_length () noexcept; - - // Returns `true` if the APK fd needs to remain open. - static bool zip_scan_entries (int apk_fd, std::string_view const& apk_path, ScanCallbackFn entry_cb) noexcept; - static bool zip_read_cd_info (int apk_fd, uint32_t& cd_offset, uint32_t& cd_size, uint16_t& cd_entries) noexcept; - static bool zip_read_entry_info (std::vector const& buf, dynamic_local_string& file_name, zip_scan_state &state) noexcept; - static bool zip_load_entry_common (size_t entry_index, std::vector const& buf, dynamic_local_string &entry_name, zip_scan_state &state) noexcept; - static bool zip_adjust_data_offset (int fd, zip_scan_state &state) noexcept; - - template - static bool zip_extract_cd_info (std::array const& buf, uint32_t& cd_offset, uint32_t& cd_size, uint16_t& cd_entries) noexcept; - - template - static bool zip_ensure_valid_params (T const& buf, size_t index, size_t to_read) noexcept; - - template - static bool zip_read_field (T const& src, size_t source_index, uint16_t& dst) noexcept; - - template - static bool zip_read_field (T const& src, size_t source_index, uint32_t& dst) noexcept; - - template - static bool zip_read_field (T const& src, size_t source_index, std::array& dst_sig) noexcept; - - template - static bool zip_read_field (T const& buf, size_t index, size_t count, dynamic_local_string& characters) noexcept; - }; -} diff --git a/src/native/clr/startup/CMakeLists.txt b/src/native/clr/startup/CMakeLists.txt deleted file mode 100644 index 48600e115e8..00000000000 --- a/src/native/clr/startup/CMakeLists.txt +++ /dev/null @@ -1,53 +0,0 @@ -set(LIB_NAME xamarin-startup) -set(LIB_ALIAS xa::xamarin-startup) - -set(XAMARIN_STARTUP_SOURCES - zip.cc -) -add_clang_check_sources("${XAMARIN_STARTUP_SOURCES}") - -add_library( - ${LIB_NAME} - STATIC - ${XAMARIN_STARTUP_SOURCES} -) - -add_library(${LIB_ALIAS} ALIAS ${LIB_NAME}) -set_static_library_suffix(${LIB_NAME}) - -target_include_directories( - ${LIB_NAME} - SYSTEM PRIVATE - ${SYSROOT_CXX_INCLUDE_DIR} -) - -target_compile_options( - ${LIB_NAME} - PRIVATE - ${XA_COMMON_CXX_ARGS} - # Avoid the 'warning: dynamic exception specifications are deprecated' warning from libc++ headers - -Wno-deprecated-dynamic-exception-spec -) - -target_link_directories( - ${LIB_NAME} - PRIVATE - ${NET_RUNTIME_DIR}/native -) - -target_link_options( - ${LIB_NAME} - PRIVATE - ${XA_COMMON_CXX_LINKER_ARGS} - ${XA_CXX_DSO_LINKER_ARGS} -) - -target_link_libraries( - ${LIB_NAME} - PRIVATE - xa::shared - -llog -) - -xa_add_compile_definitions(${LIB_NAME}) -xa_add_include_directories(${LIB_NAME}) diff --git a/src/native/clr/startup/zip.cc b/src/native/clr/startup/zip.cc deleted file mode 100644 index 0c5413fb2d6..00000000000 --- a/src/native/clr/startup/zip.cc +++ /dev/null @@ -1,488 +0,0 @@ -#include - -#include -#include -#include -#include - -#include -#include -#include -#include - -using namespace xamarin::android; - -[[gnu::always_inline]] -std::tuple Zip::get_assemblies_prefix_and_length () noexcept -{ - return {lib_prefix.data (), lib_prefix.size () }; -} - -[[gnu::always_inline]] -bool Zip::zip_adjust_data_offset (int fd, zip_scan_state &state) noexcept -{ - static constexpr size_t LH_FILE_NAME_LENGTH_OFFSET = 26uz; - static constexpr size_t LH_EXTRA_LENGTH_OFFSET = 28uz; - - off_t result = ::lseek (fd, static_cast(state.local_header_offset), SEEK_SET); - if (result < 0) { - log_error ( - LOG_ASSEMBLY, - "Failed to seek to archive entry local header at offset {}. {} (result: {}; errno: {})", - state.local_header_offset, std::strerror (errno), result, errno - ); - return false; - } - - std::array local_header; - std::array signature; - - ssize_t nread = ::read (fd, local_header.data (), local_header.size ()); - if (nread < 0 || nread != ZIP_LOCAL_LEN) { - log_error (LOG_ASSEMBLY, "Failed to read local header at offset {}: {} (nread: {}; errno: {})", state.local_header_offset, std::strerror (errno), nread, errno); - return false; - } - - size_t index = 0; - if (!zip_read_field (local_header, index, signature)) { - log_error (LOG_ASSEMBLY, "Failed to read Local Header entry signature at offset {}", state.local_header_offset); - return false; - } - - if (memcmp (signature.data (), ZIP_LOCAL_MAGIC.data (), signature.size ()) != 0) { - log_error (LOG_ASSEMBLY, "Invalid Local Header entry signature at offset {}", state.local_header_offset); - return false; - } - - uint16_t file_name_length; - index = LH_FILE_NAME_LENGTH_OFFSET; - if (!zip_read_field (local_header, index, file_name_length)) { - log_error (LOG_ASSEMBLY, "Failed to read Local Header 'file name length' field at offset {}", (state.local_header_offset + index)); - return false; - } - - uint16_t extra_field_length; - index = LH_EXTRA_LENGTH_OFFSET; - if (!zip_read_field (local_header, index, extra_field_length)) { - log_error (LOG_ASSEMBLY, "Failed to read Local Header 'extra field length' field at offset {}", (state.local_header_offset + index)); - return false; - } - - state.data_offset = static_cast(state.local_header_offset) + file_name_length + extra_field_length + local_header.size (); - - return true; -} - -[[gnu::always_inline]] -bool Zip::zip_read_entry_info (std::vector const& buf, dynamic_local_string& file_name, zip_scan_state &state) noexcept -{ - constexpr size_t CD_COMPRESSION_METHOD_OFFSET = 10uz; - constexpr size_t CD_UNCOMPRESSED_SIZE_OFFSET = 24uz; - constexpr size_t CD_FILENAME_LENGTH_OFFSET = 28uz; - constexpr size_t CD_EXTRA_LENGTH_OFFSET = 30uz; - constexpr size_t CD_LOCAL_HEADER_POS_OFFSET = 42uz; - constexpr size_t CD_COMMENT_LENGTH_OFFSET = 32uz; - - size_t index = state.buf_offset; - zip_ensure_valid_params (buf, index, ZIP_CENTRAL_LEN); - - std::array signature; - if (!zip_read_field (buf, index, signature)) { - log_error (LOG_ASSEMBLY, "Failed to read Central Directory entry signature"sv); - return false; - } - - if (memcmp (signature.data (), ZIP_CENTRAL_MAGIC.data (), signature.size ()) != 0) { - log_error (LOG_ASSEMBLY, "Invalid Central Directory entry signature"sv); - return false; - } - - index = state.buf_offset + CD_COMPRESSION_METHOD_OFFSET; - if (!zip_read_field (buf, index, state.compression_method)) { - log_error (LOG_ASSEMBLY, "Failed to read Central Directory entry 'compression method' field"sv); - return false; - } - - index = state.buf_offset + CD_UNCOMPRESSED_SIZE_OFFSET;; - if (!zip_read_field (buf, index, state.file_size)) { - log_error (LOG_ASSEMBLY, "Failed to read Central Directory entry 'uncompressed size' field"sv); - return false; - } - - uint16_t file_name_length; - index = state.buf_offset + CD_FILENAME_LENGTH_OFFSET; - if (!zip_read_field (buf, index, file_name_length)) { - log_error (LOG_ASSEMBLY, "Failed to read Central Directory entry 'file name length' field"sv); - return false; - } - - uint16_t extra_field_length; - index = state.buf_offset + CD_EXTRA_LENGTH_OFFSET; - if (!zip_read_field (buf, index, extra_field_length)) { - log_error (LOG_ASSEMBLY, "Failed to read Central Directory entry 'extra field length' field"sv); - return false; - } - - uint16_t comment_length; - index = state.buf_offset + CD_COMMENT_LENGTH_OFFSET; - if (!zip_read_field (buf, index, comment_length)) { - log_error (LOG_ASSEMBLY, "Failed to read Central Directory entry 'file comment length' field"sv); - return false; - } - - index = state.buf_offset + CD_LOCAL_HEADER_POS_OFFSET; - if (!zip_read_field (buf, index, state.local_header_offset)) { - log_error (LOG_ASSEMBLY, "Failed to read Central Directory entry 'relative offset of local header' field"sv); - return false; - } - index += sizeof(state.local_header_offset); - - if (file_name_length == 0) { - file_name.clear (); - } else if (!zip_read_field (buf, index, file_name_length, file_name)) { - log_error (LOG_ASSEMBLY, "Failed to read Central Directory entry 'file name' field"sv); - return false; - } - - state.buf_offset += ZIP_CENTRAL_LEN + file_name_length + extra_field_length + comment_length; - return true; -} - -bool Zip::zip_load_entry_common (size_t entry_index, std::vector const& buf, dynamic_local_string &entry_name, zip_scan_state &state) noexcept -{ - entry_name.clear (); - - bool result = zip_read_entry_info (buf, entry_name, state); - - log_debug (LOG_ASSEMBLY, "{} entry: {}", state.file_name, optional_string (entry_name.get (), "unknown")); - if (!result || entry_name.empty ()) { - Helpers::abort_application ( - LOG_ASSEMBLY, - std::format ( - "Failed to read Central Directory info for entry {} in APK {}", - entry_index, - state.file_name - ) - ); - } - - if (!zip_adjust_data_offset (state.file_fd, state)) { - Helpers::abort_application ( - LOG_ASSEMBLY, - std::format ( - "Failed to adjust data start offset for entry {} in APK {}", - entry_index, - state.file_name - ) - ); - } - - log_debug (LOG_ASSEMBLY, " ZIP: local header offset: {}; data offset: {}; file size: {}", state.local_header_offset, state.data_offset, state.file_size); - if (state.compression_method != 0) { - return false; - } - - if (entry_name.get ()[0] != state.prefix[0] || entry_name.length () < state.prefix_len || memcmp (state.prefix, entry_name.get (), state.prefix_len) != 0) { - // state.prefix and lib_prefix can point to the same location, see get_assemblies_prefix_and_length() - // In such instance we short-circuit and avoid a couple of comparisons below. - if (state.prefix == lib_prefix.data ()) { - return false; - } - - if (entry_name.get ()[0] != lib_prefix[0] || memcmp (lib_prefix.data (), entry_name.get (), lib_prefix.size () - 1) != 0) { - return false; - } - } - - // assemblies must be 16-byte or 4-byte aligned, or Bad Things happen - if (((state.data_offset & 0xf) != 0) || ((state.data_offset & 0x3) != 0)) { - std::string_view::size_type pos = state.file_name.find_last_of ('/'); - if (pos == state.file_name.npos) { - pos = 0; - } else { - pos++; - } - std::string_view const& name_no_path = state.file_name.substr (pos); - - Helpers::abort_application ( - LOG_ASSEMBLY, - std::format ( - "Assembly '{}' is at bad offset {} in the APK (not aligned to 4 or 16 bytes). 'zipalign' MUST be used on {} to align it properly", - optional_string (entry_name.get ()), - state.data_offset, - name_no_path - ) - ); - } - - return true; -} - -template [[gnu::always_inline]] -bool Zip::zip_extract_cd_info (std::array const& buf, uint32_t& cd_offset, uint32_t& cd_size, uint16_t& cd_entries) noexcept -{ - constexpr size_t EOCD_TOTAL_ENTRIES_OFFSET = 10uz; - constexpr size_t EOCD_CD_SIZE_OFFSET = 12uz; - constexpr size_t EOCD_CD_START_OFFSET = 16uz; - - static_assert (BufSize >= ZIP_EOCD_LEN, "Buffer too short for EOCD"); - - if (!zip_read_field (buf, EOCD_TOTAL_ENTRIES_OFFSET, cd_entries)) { - log_error (LOG_ASSEMBLY, "Failed to read EOCD 'total number of entries' field"sv); - return false; - } - - if (!zip_read_field (buf, EOCD_CD_START_OFFSET, cd_offset)) { - log_error (LOG_ASSEMBLY, "Failed to read EOCD 'central directory size' field"sv); - return false; - } - - if (!zip_read_field (buf, EOCD_CD_SIZE_OFFSET, cd_size)) { - log_error (LOG_ASSEMBLY, "Failed to read EOCD 'central directory offset' field"sv); - return false; - } - - return true; -} - -template [[gnu::always_inline]] -bool Zip::zip_ensure_valid_params (T const& buf, size_t index, size_t to_read) noexcept -{ - if (index + to_read > buf.size ()) { - log_error (LOG_ASSEMBLY, "Buffer too short to read {} bytes of data", to_read); - return false; - } - - return true; -} - -template [[gnu::always_inline]] -bool Zip::zip_read_field (T const& src, size_t source_index, uint16_t& dst) noexcept -{ - if (!zip_ensure_valid_params (src, source_index, sizeof (dst))) { - return false; - } - - dst = static_cast((src [source_index + 1] << 8) | src [source_index]); - return true; -} - -template [[gnu::always_inline]] -bool Zip::zip_read_field (T const& src, size_t source_index, uint32_t& dst) noexcept -{ - if (!zip_ensure_valid_params (src, source_index, sizeof (dst))) { - return false; - } - - dst = - (static_cast (src [source_index + 3]) << 24) | - (static_cast (src [source_index + 2]) << 16) | - (static_cast (src [source_index + 1]) << 8) | - (static_cast (src [source_index + 0])); - - return true; -} - -template [[gnu::always_inline]] -bool Zip::zip_read_field (T const& src, size_t source_index, std::array& dst_sig) noexcept -{ - if (!zip_ensure_valid_params (src, source_index, dst_sig.size ())) { - return false; - } - - memcpy (dst_sig.data (), src.data () + source_index, dst_sig.size ()); - return true; -} - -template [[gnu::always_inline]] -bool Zip::zip_read_field (T const& buf, size_t index, size_t count, dynamic_local_string& characters) noexcept -{ - if (!zip_ensure_valid_params (buf, index, count)) { - return false; - } - - characters.assign (reinterpret_cast(buf.data () + index), count); - return true; -} - -[[gnu::always_inline]] -bool Zip::zip_read_cd_info (int apk_fd, uint32_t& cd_offset, uint32_t& cd_size, uint16_t& cd_entries) noexcept -{ - // The simplest case - no file comment - off_t ret = ::lseek (apk_fd, -ZIP_EOCD_LEN, SEEK_END); - if (ret < 0) { - log_error (LOG_ASSEMBLY, "Unable to seek into the APK to find ECOD: {} (ret: {}; errno: {})", std::strerror (errno), ret, errno); - return false; - } - - std::array eocd; - ssize_t nread = ::read (apk_fd, eocd.data (), eocd.size ()); - if (nread < 0 || nread != eocd.size ()) { - log_error (LOG_ASSEMBLY, "Failed to read EOCD from the APK: {} (nread: {}; errno: {})", std::strerror (errno), nread, errno); - return false; - } - - size_t index = 0uz; // signature - std::array signature; - - if (!zip_read_field (eocd, index, signature)) { - log_error (LOG_ASSEMBLY, "Failed to read EOCD signature"sv); - return false; - } - - if (memcmp (signature.data (), ZIP_EOCD_MAGIC.data (), signature.size ()) == 0) { - return zip_extract_cd_info (eocd, cd_offset, cd_size, cd_entries); - } - - // Most probably a ZIP with comment - constexpr size_t alloc_size = 65535uz + ZIP_EOCD_LEN; // 64k is the biggest comment size allowed - ret = ::lseek (apk_fd, static_cast(-alloc_size), SEEK_END); - if (ret < 0) { - log_error (LOG_ASSEMBLY, "Unable to seek into the file to find ECOD before APK comment: {} (ret: {}; errno: {})", std::strerror (errno), ret, errno); - return false; - } - - std::vector buf (alloc_size); - - nread = ::read (apk_fd, buf.data (), buf.size ()); - - if (nread < 0 || static_cast(nread) != alloc_size) { - log_error (LOG_ASSEMBLY, "Failed to read EOCD and comment from the APK: {} (nread: {}; errno: {})", std::strerror (errno), nread, errno); - return false; - } - - // We scan from the end to save time - bool found = false; - const uint8_t* data = buf.data (); - for (ssize_t i = static_cast(alloc_size - (ZIP_EOCD_LEN + 2)); i >= 0z; i--) { - if (memcmp (data + i, ZIP_EOCD_MAGIC.data (), sizeof(ZIP_EOCD_MAGIC)) != 0) - continue; - - found = true; - memcpy (eocd.data (), data + i, ZIP_EOCD_LEN); - break; - } - - if (!found) { - log_error (LOG_ASSEMBLY, "Unable to find EOCD in the APK (with comment)"sv); - return false; - } - - return zip_extract_cd_info (eocd, cd_offset, cd_size, cd_entries); -} - -[[gnu::always_inline]] -bool Zip::zip_scan_entries (int apk_fd, std::string_view const& apk_path, ScanCallbackFn entry_cb) noexcept -{ - uint32_t cd_offset; - uint32_t cd_size; - uint16_t cd_entries; - - if (!zip_read_cd_info (apk_fd, cd_offset, cd_size, cd_entries)) { - Helpers::abort_application ( - LOG_ASSEMBLY, - std::format ( - "Failed to read the EOCD record from APK file %s", - apk_path - ) - ); - } - - log_debug (LOG_ASSEMBLY, "Central directory offset: {}", cd_offset); - log_debug (LOG_ASSEMBLY, "Central directory size: {}", cd_size); - log_debug (LOG_ASSEMBLY, "Central directory entries: {}", cd_entries); - - off_t retval = ::lseek (apk_fd, static_cast(cd_offset), SEEK_SET); - if (retval < 0) { - Helpers::abort_application ( - LOG_ASSEMBLY, - std::format ( - "Failed to seek to central directory position in APK: {}. retval={} errno={}, File={}", - std::strerror (errno), - retval, - errno, - apk_path - ) - ); - } - - std::vector buf (cd_size); - const auto [prefix, prefix_len] = get_assemblies_prefix_and_length (); - zip_scan_state state { - .file_fd = apk_fd, - .file_name = apk_path, - .prefix = prefix, - .prefix_len = prefix_len, - .buf_offset = 0uz, - .compression_method = 0u, - .local_header_offset = 0u, - .data_offset = 0u, - .file_size = 0u, - .bundled_assemblies_slow_path = false, - .max_assembly_name_size = 0u, - .max_assembly_file_name_size = 0u, - }; - - ssize_t nread; - do { - nread = read (apk_fd, buf.data (), buf.size ()); - } while (nread < 0 && errno == EINTR); - - if (static_cast(nread) != cd_size) { - Helpers::abort_application ( - LOG_ASSEMBLY, - std::format ( - "Failed to read Central Directory from APK: {}. nread={} errno={} File={}", - std::strerror (errno), - nread, - errno, - apk_path - ) - ); - } - - dynamic_local_string entry_name; - bool keep_archive_open = false; - - for (size_t i = 0uz; i < cd_entries; i++) { - bool interesting_entry = zip_load_entry_common (i, buf, entry_name, state); - if (!interesting_entry) { - continue; - } - - keep_archive_open |= entry_cb (apk_path, apk_fd, entry_name, state.data_offset, state.file_size); - } - - return keep_archive_open; -} - -void Zip::scan_archive (std::string_view const& apk_path, ScanCallbackFn entry_cb) noexcept -{ - int fd; - do { - fd = open (apk_path.data (), O_RDONLY); - } while (fd < 0 && errno == EINTR); - - if (fd < 0) { - Helpers::abort_application ( - LOG_ASSEMBLY, - std::format ( - "ERROR: Unable to load application package {}. {}", - apk_path, strerror (errno) - ) - ); - } - log_debug (LOG_ASSEMBLY, "APK {} FD: {}", apk_path, fd); - if (!zip_scan_entries (fd, apk_path, entry_cb)) { - return; - } - - if (close (fd) < 0) { - log_warn ( - LOG_ASSEMBLY, - "Failed to close file descriptor for {}. {}", - apk_path, - strerror (errno) - ); - } -} From e76d85428ce90603a74743eb2e836bfec9d4ce1e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 10 Jul 2026 13:21:06 +0200 Subject: [PATCH 06/15] [CoreCLR] dlopen the assembly store with RTLD_LOCAL We only dlsym() our own handle, so there is no need to add the store's symbols to the global lookup scope. RTLD_LOCAL avoids the extra linker bookkeeping that RTLD_GLOBAL would incur. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 527f04b6-c95a-4a66-b90f-4c52ba5496f3 --- src/native/clr/host/host.cc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/native/clr/host/host.cc b/src/native/clr/host/host.cc index 08c5b973101..db72f3f7334 100644 --- a/src/native/clr/host/host.cc +++ b/src/native/clr/host/host.cc @@ -216,7 +216,9 @@ void Host::gather_assemblies_and_libraries ([[maybe_unused]] jstring_array_wrapp void Host::map_assembly_store_via_dlopen (const char *store_path) noexcept { - void *handle = ::dlopen (store_path, RTLD_NOW | RTLD_GLOBAL); + // RTLD_LOCAL: we only dlsym() our own handle, so there's no need to add the store's symbols to + // the global lookup scope (RTLD_GLOBAL would just add linker bookkeeping). + void *handle = ::dlopen (store_path, RTLD_NOW | RTLD_LOCAL); if (handle == nullptr) [[unlikely]] { Helpers::abort_application ( LOG_ASSEMBLY, From 4833d43dd0cf79f6fb55b2f9f63fbecf2ef97eda Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 10 Jul 2026 15:14:04 +0200 Subject: [PATCH 07/15] [CoreCLR] Fix assembly-store-reader compat, rename symbol, const-correctness Three refinements to the dlopen-based assembly store: - Fix CI: name the wrapper's payload section `payload` (not `.payload`) so tools/assembly-store-reader-mk2 (Utils.FindELFPayloadSectionOffsetAndSize looks up "payload") and the CoreClrTrimmableTypeMap build tests that read the APK via it recognize the store again. Matches the section name the MonoVM DSOWrapperGenerator already uses. The runtime is unaffected: it resolves the exported dynamic symbol, not the section name. - Rename the exported payload symbol `_assembly_store_start` -> `_assembly_store`. - Make the whole read-only assembly-store pointer chain const so there are no const_casts on the store path: configure_from_payload() takes `const void*`, and data_start, image_data, debug_info_data, config_data, assemblies, descriptor and assembly_store_hashes are now const pointers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 527f04b6-c95a-4a66-b90f-4c52ba5496f3 --- .../Tasks/WrapAssembliesAsSharedLibraries.cs | 2 +- .../Utilities/DlopenAssemblyStoreGenerator.cs | 14 +++++++++---- .../Xamarin.Android.Common.targets | 2 +- src/native/clr/host/assembly-store.cc | 20 +++++++++---------- src/native/clr/host/host.cc | 2 +- src/native/clr/include/host/assembly-store.hh | 6 +++--- src/native/clr/include/host/host.hh | 2 +- src/native/clr/include/xamarin-app.hh | 12 +++++------ 8 files changed, 33 insertions(+), 27 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/WrapAssembliesAsSharedLibraries.cs b/src/Xamarin.Android.Build.Tasks/Tasks/WrapAssembliesAsSharedLibraries.cs index 1e5c6bd720f..8d56f739650 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/WrapAssembliesAsSharedLibraries.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/WrapAssembliesAsSharedLibraries.cs @@ -35,7 +35,7 @@ public class WrapAssembliesAsSharedLibraries : AndroidTask /// /// When true (always the case for CoreCLR), the assembly store is wrapped into a shared library - /// whose payload is exported via the _assembly_store_start dynamic symbol, so the runtime + /// whose payload is exported via the _assembly_store dynamic symbol, so the runtime /// can locate it with dlopen+dlsym instead of parsing the APK ZIP directory. /// When false (MonoVM), the classic layout is used instead. /// diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/DlopenAssemblyStoreGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/DlopenAssemblyStoreGenerator.cs index ed3c9d70e49..8984949db64 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/DlopenAssemblyStoreGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/DlopenAssemblyStoreGenerator.cs @@ -12,7 +12,7 @@ namespace Xamarin.Android.Tasks; /// /// Produces the CoreCLR assembly-store wrapper shared library, whose payload lives in a /// *loadable* ELF section (SHF_ALLOC, covered by a PT_LOAD segment) and is -/// pointed at by an exported dynamic symbol (_assembly_store_start). +/// pointed at by an exported dynamic symbol (_assembly_store). /// /// This differs from (used by MonoVM), which injects the /// payload with llvm-objcopy --add-section into a *non-loadable* section @@ -20,7 +20,7 @@ namespace Xamarin.Android.Tasks; /// directory parsing), mmap it and walk the ELF section headers by hand. /// /// With this layout the CoreCLR runtime simply -/// dlopen("libassembly-store.so") + dlsym("_assembly_store_start") +/// dlopen("libassembly-store.so") + dlsym("_assembly_store") /// and lets the dynamic linker locate + map the payload. /// /// The wrapper is built with the shipped binutils (llvm-mc to assemble a @@ -29,7 +29,11 @@ namespace Xamarin.Android.Tasks; /// static class DlopenAssemblyStoreGenerator { - public const string PayloadStartSymbol = "_assembly_store_start"; + public const string PayloadStartSymbol = "_assembly_store"; + + // Section name that holds the payload. Must match the name `tools/assembly-store-reader-mk2` + // (Utils.FindELFPayloadSectionOffsetAndSize) looks for and the one `DSOWrapperGenerator` uses. + const string PayloadSectionName = "payload"; // 16k so the payload is page-aligned on both 4k and 16k page-size devices. const int PayloadAlignment = 16384; @@ -60,9 +64,11 @@ public static string WrapIt (TaskLoggingHelper log, DSOWrapperGenerator.Config c log.LogDebugMessage ($"[{targetArch}] Wrapping '{payloadFilePath}' into loadable-symbol shared library '{outputFile}'"); // The `.incbin` uses an absolute path so we don't depend on the assembler's working directory. + // The section is named `payload` (no leading dot) to match the name the MonoVM + // `DSOWrapperGenerator` uses and that `tools/assembly-store-reader-mk2` looks for. string incbinPath = payloadFilePath.Replace ("\\", "\\\\").Replace ("\"", "\\\""); string asm = $""" - .section .payload, "a" + .section {PayloadSectionName}, "a" .balign {PayloadAlignment} .globl {PayloadStartSymbol} {PayloadStartSymbol}: diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index f02ec940bec..cd07087d9a7 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -338,7 +338,7 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. <_AndroidUseAssemblyStore Condition="'$(EmbedAssembliesIntoApk)' == 'true' ">true <_AndroidUseAssemblyStore Condition="'$(EmbedAssembliesIntoApk)' != 'true' ">false <_AndroidDlopenAssemblyStore>true diff --git a/src/native/clr/host/assembly-store.cc b/src/native/clr/host/assembly-store.cc index 3960abd8669..3e0ffa0da98 100644 --- a/src/native/clr/host/assembly-store.cc +++ b/src/native/clr/host/assembly-store.cc @@ -225,7 +225,7 @@ auto AssemblyStore::open_assembly (std::string_view const& name, int64_t &size) ); } - AssemblyStoreEntryDescriptor &store_entry = assembly_store.assemblies[hash_entry->descriptor_index]; + const AssemblyStoreEntryDescriptor &store_entry = assembly_store.assemblies[hash_entry->descriptor_index]; AssemblyStoreSingleAssemblyRuntimeData &assembly_runtime_info = assembly_store_bundled_assemblies[store_entry.mapping_index]; if (assembly_runtime_info.image_data == nullptr) { @@ -240,10 +240,10 @@ auto AssemblyStore::open_assembly (std::string_view const& name, int64_t &size) log_debug ( LOG_ASSEMBLY, "Mapped: image_data == {:p}; debug_info_data == {:p}; config_data == {:p}; descriptor == {:p}; data size == {}; debug data size == {}; config data size == {}; name == '{}'"sv, - static_cast(assembly_runtime_info.image_data), - static_cast(assembly_runtime_info.debug_info_data), - static_cast(assembly_runtime_info.config_data), - static_cast(assembly_runtime_info.descriptor), + static_cast(assembly_runtime_info.image_data), + static_cast(assembly_runtime_info.debug_info_data), + static_cast(assembly_runtime_info.config_data), + static_cast(assembly_runtime_info.descriptor), assembly_runtime_info.descriptor->data_size, assembly_runtime_info.descriptor->debug_data_size, assembly_runtime_info.descriptor->config_data_size, @@ -256,9 +256,9 @@ auto AssemblyStore::open_assembly (std::string_view const& name, int64_t &size) return assembly_data; } -void AssemblyStore::configure_from_payload (void *payload_start, const std::function& get_full_store_path) noexcept +void AssemblyStore::configure_from_payload (const void *payload_start, const std::function& get_full_store_path) noexcept { - auto header = static_cast(payload_start); + auto header = static_cast(payload_start); if (header->magic != ASSEMBLY_STORE_MAGIC) { Helpers::abort_application ( @@ -284,9 +284,9 @@ void AssemblyStore::configure_from_payload (void *payload_start, const std::func constexpr size_t header_size = sizeof(AssemblyStoreHeader); - assembly_store.data_start = static_cast(payload_start); + assembly_store.data_start = static_cast(payload_start); assembly_store.assembly_count = header->entry_count; assembly_store.index_entry_count = header->index_entry_count; - assembly_store.assemblies = reinterpret_cast(assembly_store.data_start + header_size + header->index_size); - assembly_store_hashes = reinterpret_cast(assembly_store.data_start + header_size); + assembly_store.assemblies = reinterpret_cast(assembly_store.data_start + header_size + header->index_size); + assembly_store_hashes = reinterpret_cast(assembly_store.data_start + header_size); } diff --git a/src/native/clr/host/host.cc b/src/native/clr/host/host.cc index db72f3f7334..c28af90875c 100644 --- a/src/native/clr/host/host.cc +++ b/src/native/clr/host/host.cc @@ -208,7 +208,7 @@ void Host::gather_assemblies_and_libraries ([[maybe_unused]] jstring_array_wrapp } // The assembly store is wrapped as a real shared library whose payload is exposed via the - // `_assembly_store_start` dynamic symbol, so the dynamic linker has already located and mapped + // `_assembly_store` dynamic symbol, so the dynamic linker has already located and mapped // it out of the APK for us. We resolve the payload with dlopen()+dlsym() instead of parsing the // APK ZIP central directory ourselves. map_assembly_store_via_dlopen (Constants::assembly_store_file_name.data ()); diff --git a/src/native/clr/include/host/assembly-store.hh b/src/native/clr/include/host/assembly-store.hh index 64da6bf673e..847b81c157d 100644 --- a/src/native/clr/include/host/assembly-store.hh +++ b/src/native/clr/include/host/assembly-store.hh @@ -18,9 +18,9 @@ namespace xamarin::android { static auto open_assembly (std::string_view const& name, int64_t &size) noexcept -> void*; // Configure the store directly from an in-memory payload pointer (obtained via - // dlopen()+dlsym() of the `_assembly_store_start` dynamic symbol). `get_full_store_path` + // dlopen()+dlsym() of the `_assembly_store` dynamic symbol). `get_full_store_path` // is invoked only to build diagnostics if the payload turns out to be invalid. - static void configure_from_payload (void *payload_start, const std::function& get_full_store_path) noexcept; + static void configure_from_payload (const void *payload_start, const std::function& get_full_store_path) noexcept; private: static void set_assembly_data_and_size (uint8_t* source_assembly_data, uint32_t source_assembly_data_size, uint8_t*& dest_assembly_data, uint32_t& dest_assembly_data_size) noexcept; @@ -30,7 +30,7 @@ namespace xamarin::android { static auto find_assembly_store_entry (hash_t hash, const AssemblyStoreIndexEntry *entries, size_t entry_count) noexcept -> const AssemblyStoreIndexEntry*; private: - static inline AssemblyStoreIndexEntry *assembly_store_hashes = nullptr; + static inline const AssemblyStoreIndexEntry *assembly_store_hashes = nullptr; static inline std::mutex assembly_decompress_mutex {}; }; } diff --git a/src/native/clr/include/host/host.hh b/src/native/clr/include/host/host.hh index 629937146a5..e47a1f420cd 100644 --- a/src/native/clr/include/host/host.hh +++ b/src/native/clr/include/host/host.hh @@ -35,7 +35,7 @@ namespace xamarin::android { private: // Must match `DlopenAssemblyStoreGenerator.PayloadStartSymbol` in the build tasks. - static constexpr std::string_view DLOPEN_ASSEMBLY_STORE_SYMBOL { "_assembly_store_start" }; + static constexpr std::string_view DLOPEN_ASSEMBLY_STORE_SYMBOL { "_assembly_store" }; static void gather_assemblies_and_libraries (jstring_array_wrapper& runtimeApks, bool have_split_apks); static void map_assembly_store_via_dlopen (const char *store_path) noexcept; diff --git a/src/native/clr/include/xamarin-app.hh b/src/native/clr/include/xamarin-app.hh index 39fbb97f822..686b99dae7f 100644 --- a/src/native/clr/include/xamarin-app.hh +++ b/src/native/clr/include/xamarin-app.hh @@ -192,18 +192,18 @@ struct [[gnu::packed]] AssemblyStoreEntryDescriptor final struct AssemblyStoreRuntimeData final { - uint8_t *data_start; + const uint8_t *data_start; uint32_t assembly_count; uint32_t index_entry_count; - AssemblyStoreEntryDescriptor *assemblies; + const AssemblyStoreEntryDescriptor *assemblies; }; struct AssemblyStoreSingleAssemblyRuntimeData final { - uint8_t *image_data; - uint8_t *debug_info_data; - uint8_t *config_data; - AssemblyStoreEntryDescriptor *descriptor; + const uint8_t *image_data; + const uint8_t *debug_info_data; + const uint8_t *config_data; + const AssemblyStoreEntryDescriptor *descriptor; }; // Keep in strict sync with: From 5afb5fa852f7291e6d3dc54dc84c33f054e941be Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 10 Jul 2026 15:53:44 +0200 Subject: [PATCH 08/15] [CoreCLR] Remove now-dead DSOApkEntry / dso_apk_entries With the dlopen-based store the CoreCLR host no longer consumes the DSOApkEntry {name_hash,offset,fd} table (the ZIP-scan native-library fast path was removed earlier in this PR). Delete the now-orphaned CoreCLR-side dead code: - the `dso_apk_entries` LLVM IR emission + `DSOApkEntry` struct/data-provider and PopulateDsoApkEntries() in ApplicationConfigNativeAssemblyGeneratorCLR - the `DSOApkEntry` struct, `dso_apk_entries[]` extern declaration, and stub definition in the CoreCLR xamarin-app.hh / application_dso_stub.cc MonoVM still uses DSOApkEntry (its own ApplicationConfigNativeAssemblyGenerator + mono/xamarin-app-stub/xamarin-app.hh) and is left untouched. Verified: native builds + links clean (all ABIs), and a net11.0-android MAUI sample-content app builds, links, and cold-launches (Status: ok) with no dso_apk_entries present. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 527f04b6-c95a-4a66-b90f-4c52ba5496f3 --- ...icationConfigNativeAssemblyGeneratorCLR.cs | 65 ------------------- src/native/clr/include/xamarin-app.hh | 8 --- .../xamarin-app-stub/application_dso_stub.cc | 2 - 3 files changed, 75 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGeneratorCLR.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGeneratorCLR.cs index f4ef94d75b1..5c439dbf460 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGeneratorCLR.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGeneratorCLR.cs @@ -67,34 +67,6 @@ sealed class DSOCacheEntry public IntPtr handle = IntPtr.Zero; } - sealed class DSOApkEntryContextDataProvider : NativeAssemblerStructContextDataProvider - { - public override string GetComment (object data, string fieldName) - { - var dso_apk_entry = EnsureType (data); - if (MonoAndroidHelper.StringEquals ("name_hash", fieldName)) { - return $" from name: {dso_apk_entry.Name}"; - } - - return String.Empty; - } - } - - // Order of fields and their type must correspond *exactly* (with exception of the - // ignored managed members) to that in - // src/native/clr/include/xamarin-app.hh DSOApkEntry structure - [NativeAssemblerStructContextDataProvider (typeof (DSOApkEntryContextDataProvider))] - sealed class DSOApkEntry - { - [NativeAssembler (Ignore = true)] - public string Name = String.Empty; - - [NativeAssembler (UsesDataProvider = true, NumberFormat = LlvmIrVariableNumberFormat.Hexadecimal)] - public ulong name_hash; - public uint offset; // offset into the APK - public int fd; // apk file descriptor - }; - // Order of fields and their type must correspond *exactly* to that in // src/monodroid/jni/xamarin-app.hh AssemblyStoreAssemblyDescriptor structure sealed class AssemblyStoreAssemblyDescriptor @@ -256,7 +228,6 @@ sealed class DsoCacheState StructureInfo? applicationConfigStructureInfo; StructureInfo? dsoCacheEntryStructureInfo; - StructureInfo? dsoApkEntryStructureInfo; StructureInfo? xamarinAndroidBundledAssemblyStructureInfo; StructureInfo? assemblyStoreSingleAssemblyRuntimeDataStructureinfo; StructureInfo? assemblyStoreRuntimeDataStructureInfo; @@ -404,12 +375,6 @@ protected override void Construct (LlvmIrModule module) module.Add (aot_dso_cache); module.AddGlobalVariable ("dso_names_data", dsoState.NamesBlob, LlvmIrVariableOptions.GlobalConstant); - var dso_apk_entries = new LlvmIrGlobalVariable (new List> (NativeLibraries.Count), "dso_apk_entries") { - Options = LlvmIrVariableOptions.GlobalWritable, - BeforeWriteCallback = PopulateDsoApkEntries, - }; - module.Add (dso_apk_entries); - string bundledBuffersSize = xamarinAndroidBundledAssemblies == null ? "empty (unused when assembly stores are enabled)" : $"{BundledAssemblyNameWidth} bytes long"; var bundled_assemblies = new LlvmIrGlobalVariable (typeof(List>), "bundled_assemblies", LlvmIrVariableOptions.GlobalWritable) { Value = xamarinAndroidBundledAssemblies, @@ -573,35 +538,6 @@ void AddAssemblyStores (LlvmIrModule module) return $" {dsoState.JniPreloadNames[(int)index]}"; } - void PopulateDsoApkEntries (LlvmIrVariable variable, LlvmIrModuleTarget target, object? state) - { - var dso_apk_entries = variable.Value as List>; - if (dso_apk_entries == null) { - throw new InvalidOperationException ("Internal error: DSO apk entries list not present."); - } - - if (dso_apk_entries.Capacity != NativeLibraries.Count) { - throw new InvalidOperationException ($"Internal error: DSO apk entries count ({dso_apk_entries.Count}) is different to the native libraries count ({NativeLibraries.Count})."); - } - - bool is64Bit = target.Is64Bit; - foreach (ITaskItem item in NativeLibraries) { - string name = Path.GetFileName (item.ItemSpec); - var entry = new DSOApkEntry { - Name = name, - - name_hash = MonoAndroidHelper.GetXxHash (name, is64Bit), - offset = 0, - fd = -1, - }; - dso_apk_entries.Add (new StructureInstance (dsoApkEntryStructureInfo, entry)); - } - - dso_apk_entries.Sort ((StructureInstance a, StructureInstance b) => { - return a.Instance!.name_hash.CompareTo (b.Instance!.name_hash); - }); - } - void PopulatePreloadIndices (LlvmIrVariable variable, LlvmIrModuleTarget target, object? state) { var dsoState = state as DsoCacheState; @@ -774,7 +710,6 @@ void MapStructures (LlvmIrModule module) assemblyStoreRuntimeDataStructureInfo = module.MapStructure (); xamarinAndroidBundledAssemblyStructureInfo = module.MapStructure (); dsoCacheEntryStructureInfo = module.MapStructure (); - dsoApkEntryStructureInfo = module.MapStructure (); runtimePropertyStructureInfo = module.MapStructure (); runtimePropertyIndexEntryStructureInfo = module.MapStructure (); appEnvironmentVariableStructureInfo = module.MapStructure (); diff --git a/src/native/clr/include/xamarin-app.hh b/src/native/clr/include/xamarin-app.hh index 686b99dae7f..82d1de01026 100644 --- a/src/native/clr/include/xamarin-app.hh +++ b/src/native/clr/include/xamarin-app.hh @@ -246,13 +246,6 @@ struct RuntimePropertyIndexEntry uint32_t index; }; -struct DSOApkEntry -{ - uint64_t name_hash; - uint32_t offset; // offset into the APK - int32_t fd; // apk file descriptor -}; - struct DSOCacheEntry { const uint64_t hash; @@ -354,7 +347,6 @@ extern "C" { [[gnu::visibility("default")]] extern const uint dso_jni_preloads_idx[]; [[gnu::visibility("default")]] extern DSOCacheEntry aot_dso_cache[]; [[gnu::visibility("default")]] extern const char dso_names_data[]; - [[gnu::visibility("default")]] extern DSOApkEntry dso_apk_entries[]; [[gnu::visibility("default")]] extern const RuntimeProperty runtime_properties[]; [[gnu::visibility("default")]] extern const char runtime_properties_data[]; diff --git a/src/native/clr/xamarin-app-stub/application_dso_stub.cc b/src/native/clr/xamarin-app-stub/application_dso_stub.cc index 14f33be4c29..91d6ca6c9a6 100644 --- a/src/native/clr/xamarin-app-stub/application_dso_stub.cc +++ b/src/native/clr/xamarin-app-stub/application_dso_stub.cc @@ -156,8 +156,6 @@ DSOCacheEntry aot_dso_cache[] = { const char dso_names_data[] = {}; -DSOApkEntry dso_apk_entries[2] {}; - // // Support for marshal methods // From 2ded5a4af88f73836cba35b7ebc7b4a627fe2832 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 10 Jul 2026 16:08:04 +0200 Subject: [PATCH 09/15] [CoreCLR] Clarify configure_from_payload read-only comment Note in the comment that the assembly-store payload is mapped read-only and never modified, which is why it and every pointer derived from it are `const`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 527f04b6-c95a-4a66-b90f-4c52ba5496f3 --- src/native/clr/include/host/assembly-store.hh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/native/clr/include/host/assembly-store.hh b/src/native/clr/include/host/assembly-store.hh index 847b81c157d..57842df483c 100644 --- a/src/native/clr/include/host/assembly-store.hh +++ b/src/native/clr/include/host/assembly-store.hh @@ -18,8 +18,10 @@ namespace xamarin::android { static auto open_assembly (std::string_view const& name, int64_t &size) noexcept -> void*; // Configure the store directly from an in-memory payload pointer (obtained via - // dlopen()+dlsym() of the `_assembly_store` dynamic symbol). `get_full_store_path` - // is invoked only to build diagnostics if the payload turns out to be invalid. + // dlopen()+dlsym() of the `_assembly_store` dynamic symbol). The payload is mapped + // read-only and is never modified, so it (and every pointer derived from it) is `const`. + // `get_full_store_path` is invoked only to build diagnostics if the payload turns out + // to be invalid. static void configure_from_payload (const void *payload_start, const std::function& get_full_store_path) noexcept; private: From b05de7576d72ea7797572bace6d308aee81b4d0f Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 10 Jul 2026 20:26:26 +0200 Subject: [PATCH 10/15] [tests] Update CoreCLR APK size baseline Accept the size changes from dlopen-based assembly store loading. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7be7d34c-aff3-4338-b15b-6061bc7a3ba9 --- ...ldReleaseArm64SimpleDotNet.CoreCLR.apkdesc | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.apkdesc b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.apkdesc index 065272635fa..860584f22c8 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.apkdesc +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.apkdesc @@ -5,34 +5,34 @@ "Size": 3036 }, "classes.dex": { - "Size": 402352 + "Size": 402852 }, "lib/arm64-v8a/libassembly-store.so": { - "Size": 2717688 + "Size": 2725488 }, "lib/arm64-v8a/libclrjit.so": { - "Size": 2835128 + "Size": 2757816 }, "lib/arm64-v8a/libcoreclr.so": { - "Size": 4891056 + "Size": 4837240 }, "lib/arm64-v8a/libmonodroid.so": { - "Size": 1324320 + "Size": 1235528 }, "lib/arm64-v8a/libSystem.Globalization.Native.so": { "Size": 72112 }, "lib/arm64-v8a/libSystem.IO.Compression.Native.so": { - "Size": 1262888 + "Size": 1258776 }, "lib/arm64-v8a/libSystem.Native.so": { - "Size": 101888 + "Size": 99664 }, "lib/arm64-v8a/libSystem.Security.Cryptography.Native.Android.so": { - "Size": 168080 + "Size": 163936 }, "lib/arm64-v8a/libxamarin-app.so": { - "Size": 20968 + "Size": 21288 }, "res/drawable-hdpi-v4/icon.png": { "Size": 2178 @@ -59,5 +59,5 @@ "Size": 1904 } }, - "PackageSize": 7271867 + "PackageSize": 7235003 } \ No newline at end of file From 537a2a3162b73157e335d8631d732d4fea9b4774 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 11 Jul 2026 09:32:32 +0200 Subject: [PATCH 11/15] [CoreCLR] Preserve FastDev without assembly store Propagate UseAssemblyStore into the native application config and skip dlopen-based discovery when no store is packaged. This restores individual assembly loading from FastDev overrides. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7be7d34c-aff3-4338-b15b-6061bc7a3ba9 --- .../GenerateNativeApplicationConfigSources.cs | 1 + ...rateNativeApplicationConfigSourcesTests.cs | 45 +++++++++++++++++++ .../Utilities/EnvironmentHelper.cs | 9 +++- .../Utilities/ApplicationConfigCLR.cs | 1 + ...icationConfigNativeAssemblyGeneratorCLR.cs | 2 + src/native/clr/host/host.cc | 5 +++ src/native/clr/include/xamarin-app.hh | 1 + .../xamarin-app-stub/application_dso_stub.cc | 1 + 8 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateNativeApplicationConfigSourcesTests.cs diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs index 52404559199..dd2788f30a1 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs @@ -285,6 +285,7 @@ static bool ShouldSkipAssembly (ITaskItem assembly) MarshalMethodsEnabled = EnableMarshalMethods, ManagedMarshalMethodsLookupEnabled = EnableManagedMarshalMethodsLookup, IgnoreSplitConfigs = ShouldIgnoreSplitConfigs (), + HaveAssemblyStore = UseAssemblyStore, }; } else { appConfigAsmGen = new ApplicationConfigNativeAssemblyGenerator (envBuilder.EnvironmentVariables, envBuilder.SystemProperties, Log) { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateNativeApplicationConfigSourcesTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateNativeApplicationConfigSourcesTests.cs new file mode 100644 index 00000000000..803cc00bc32 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateNativeApplicationConfigSourcesTests.cs @@ -0,0 +1,45 @@ +#nullable enable +using System.IO; + +using Microsoft.Build.Utilities; +using NUnit.Framework; +using Xamarin.Android.Tasks; +using Xamarin.ProjectTools; + +namespace Xamarin.Android.Build.Tests.Tasks; + +[TestFixture] +public class GenerateNativeApplicationConfigSourcesTests : BaseTest +{ + [TestCase (false)] + [TestCase (true)] + public void HaveAssemblyStoreIsEmittedForCoreCLR (bool haveAssemblyStore) + { + string outputRoot = Path.Combine (Root, "temp", $"{nameof (HaveAssemblyStoreIsEmittedForCoreCLR)}-{haveAssemblyStore}"); + string monoAndroidPath = Path.Combine (TestEnvironment.MonoAndroidFrameworkDirectory, "Mono.Android.dll"); + FileAssert.Exists (monoAndroidPath); + + var task = new GenerateNativeApplicationConfigSources { + BuildEngine = new MockBuildEngine (TestContext.Out), + ResolvedAssemblies = [new TaskItem (monoAndroidPath)], + EnvironmentOutputDirectory = Path.Combine (outputRoot, "android"), + SupportedAbis = ["arm64-v8a"], + AndroidPackageName = "com.microsoft.android.assemblystoretest", + EnablePreloadAssembliesDefault = false, + TargetsCLR = true, + AndroidRuntime = "CoreCLR", + UseAssemblyStore = haveAssemblyStore, + }; + + Assert.IsTrue (task.Execute (), "GenerateNativeApplicationConfigSources should succeed."); + + var environmentFiles = EnvironmentHelper.GatherEnvironmentFiles ( + outputRoot, + "arm64-v8a", + required: true, + runtime: AndroidRuntime.CoreCLR + ); + var config = (EnvironmentHelper.ApplicationConfig_CoreCLR)EnvironmentHelper.ReadApplicationConfig (environmentFiles, AndroidRuntime.CoreCLR); + Assert.AreEqual (haveAssemblyStore, config.have_assembly_store); + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/EnvironmentHelper.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/EnvironmentHelper.cs index 927e4ccad70..c5ad091db24 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/EnvironmentHelper.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/EnvironmentHelper.cs @@ -62,9 +62,10 @@ public sealed class ApplicationConfig_CoreCLR : IApplicationConfig public uint jni_remapping_replacement_method_index_entry_count; public string android_package_name = String.Empty; public bool managed_marshal_methods_lookup_enabled; + public bool have_assembly_store; } - const uint ApplicationConfigFieldCount_CoreCLR = 20; + const uint ApplicationConfigFieldCount_CoreCLR = 21; // This must be identical to the ApplicationConfig structure in src/native/mono/xamarin-app-stub/xamarin-app.hh public sealed class ApplicationConfig_MonoVM : IApplicationConfig @@ -407,6 +408,11 @@ static IApplicationConfig ReadApplicationConfig_CoreCLR (EnvironmentFile envFile AssertFieldType (envFile.Path, parser.SourceFilePath, ".byte", field [0], item.LineNumber); ret.managed_marshal_methods_lookup_enabled = ConvertFieldToBool ("managed_marshal_methods_lookup_enabled", envFile.Path, parser.SourceFilePath, item.LineNumber, field [1]); break; + + case 20: // have_assembly_store: bool / .byte + AssertFieldType (envFile.Path, parser.SourceFilePath, ".byte", field [0], item.LineNumber); + ret.have_assembly_store = ConvertFieldToBool ("have_assembly_store", envFile.Path, parser.SourceFilePath, item.LineNumber, field [1]); + break; } fieldCount++; } @@ -771,6 +777,7 @@ static void AssertApplicationConfigIsIdentical (ApplicationConfig_CoreCLR firstA Assert.AreEqual (firstAppConfig.environment_variable_count, secondAppConfig.environment_variable_count, $"Field 'environment_variable_count' has different value in environment file '{secondEnvFile}' than in environment file '{firstEnvFile}'"); Assert.AreEqual (firstAppConfig.system_property_count, secondAppConfig.system_property_count, $"Field 'system_property_count' has different value in environment file '{secondEnvFile}' than in environment file '{firstEnvFile}'"); Assert.AreEqual (firstAppConfig.android_package_name, secondAppConfig.android_package_name, $"Field 'android_package_name' has different value in environment file '{secondEnvFile}' than in environment file '{firstEnvFile}'"); + Assert.AreEqual (firstAppConfig.have_assembly_store, secondAppConfig.have_assembly_store, $"Field 'have_assembly_store' has different value in environment file '{secondEnvFile}' than in environment file '{firstEnvFile}'"); } static void AssertApplicationConfigIsIdentical (ApplicationConfig_MonoVM firstAppConfig, string firstEnvFile, ApplicationConfig_MonoVM secondAppConfig, string secondEnvFile) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigCLR.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigCLR.cs index b48178218f1..5db6385d949 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigCLR.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigCLR.cs @@ -51,4 +51,5 @@ sealed class ApplicationConfigCLR public uint jni_remapping_replacement_method_index_entry_count; public string android_package_name = String.Empty; public bool managed_marshal_methods_lookup_enabled; + public bool have_assembly_store; } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGeneratorCLR.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGeneratorCLR.cs index 5c439dbf460..7524def0989 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGeneratorCLR.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGeneratorCLR.cs @@ -258,6 +258,7 @@ sealed class DsoCacheState public bool MarshalMethodsEnabled { get; set; } public bool ManagedMarshalMethodsLookupEnabled { get; set; } public bool IgnoreSplitConfigs { get; set; } + public bool HaveAssemblyStore { get; set; } public ApplicationConfigNativeAssemblyGeneratorCLR (IDictionary environmentVariables, IDictionary systemProperties, IDictionary? runtimeProperties, TaskLoggingHelper log) @@ -344,6 +345,7 @@ protected override void Construct (LlvmIrModule module) jni_remapping_replacement_type_count = (uint)JniRemappingReplacementTypeCount, jni_remapping_replacement_method_index_entry_count = (uint)JniRemappingReplacementMethodIndexEntryCount, android_package_name = AndroidPackageName, + have_assembly_store = HaveAssemblyStore, }; application_config = new StructureInstance (applicationConfigStructureInfo, app_cfg); module.AddGlobalVariable ("application_config", application_config); diff --git a/src/native/clr/host/host.cc b/src/native/clr/host/host.cc index c28af90875c..5d91e65af4c 100644 --- a/src/native/clr/host/host.cc +++ b/src/native/clr/host/host.cc @@ -202,6 +202,11 @@ void Host::scan_filesystem_for_assemblies_and_libraries () noexcept void Host::gather_assemblies_and_libraries ([[maybe_unused]] jstring_array_wrapper& runtimeApks, [[maybe_unused]] bool have_split_apks) { + if (!application_config.have_assembly_store) { + log_debug (LOG_ASSEMBLY, "No assembly store configured; skipping assembly store discovery"sv); + return; + } + if (!AndroidSystem::is_embedded_dso_mode_enabled ()) { scan_filesystem_for_assemblies_and_libraries (); return; diff --git a/src/native/clr/include/xamarin-app.hh b/src/native/clr/include/xamarin-app.hh index 82d1de01026..c4c1c124281 100644 --- a/src/native/clr/include/xamarin-app.hh +++ b/src/native/clr/include/xamarin-app.hh @@ -231,6 +231,7 @@ struct ApplicationConfig uint32_t jni_remapping_replacement_method_index_entry_count; const char *android_package_name; bool managed_marshal_methods_lookup_enabled; + bool have_assembly_store; }; struct RuntimeProperty diff --git a/src/native/clr/xamarin-app-stub/application_dso_stub.cc b/src/native/clr/xamarin-app-stub/application_dso_stub.cc index 91d6ca6c9a6..8731f85cadb 100644 --- a/src/native/clr/xamarin-app-stub/application_dso_stub.cc +++ b/src/native/clr/xamarin-app-stub/application_dso_stub.cc @@ -71,6 +71,7 @@ const ApplicationConfig application_config = { .jni_remapping_replacement_method_index_entry_count = 2, .android_package_name = android_package_name, .managed_marshal_methods_lookup_enabled = false, + .have_assembly_store = false, }; // TODO: migrate to std::string_view for these two From 4864037e96888342b38e30b9b29ecb4fa2fff150 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 11 Jul 2026 15:59:47 +0200 Subject: [PATCH 12/15] [docs] Document CoreCLR dlopen()-based assembly store loading Update the assembly store and shared-library payload design docs to describe the CoreCLR loading mechanism introduced in this PR and to disambiguate it from the existing MonoVM behavior. - AssemblyStores.md: add a per-runtime note explaining that MonoVM locates the store by scanning the APK ZIP and mmap-ing the wrapper, while CoreCLR resolves it via dlopen()+dlsym("_assembly_store"). - ApkSharedLibraries.md: mark the existing stub/mmap narrative and readelf sample as MonoVM-only, and add a detailed "Layout of the CoreCLR (dlopen) payload library" section with real llvm-readelf section-header, program-header and dynamic-symbol output showing the allocatable `payload` section, its PT_LOAD segment and the exported `_assembly_store` symbol. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b99a3be6-afab-4429-a40e-1a236ac0d98e --- .../project-docs/ApkSharedLibraries.md | 102 +++++++++++++++++- Documentation/project-docs/AssemblyStores.md | 14 +++ 2 files changed, 115 insertions(+), 1 deletion(-) diff --git a/Documentation/project-docs/ApkSharedLibraries.md b/Documentation/project-docs/ApkSharedLibraries.md index 5c68e070c87..93ce7eaccc4 100644 --- a/Documentation/project-docs/ApkSharedLibraries.md +++ b/Documentation/project-docs/ApkSharedLibraries.md @@ -72,6 +72,12 @@ those directories into actual ELF shared libraries. The way it is done is descr ## Data payload stub library +> **Note:** there are currently two payload-library layouts. The **MonoVM** layout described in this section +> (a stub with a non-loadable `payload` section, located by scanning the APK ZIP and `mmap`ed by hand) and the +> **CoreCLR** layout (a real shared library loaded via `dlopen()`+`dlsym()`), described in +> [CoreCLR: dlopen-based payload library](#coreclr-dlopen-based-payload-library). Everything below this note +> describes the MonoVM layout unless stated otherwise. + ELF binaries consist of a number of sections, which contain code, data (read-only and read-write), debug symbols etc. However, the ELF specification doesn't dictate names of any of those sections and, thus, developers are free to lay out ELF binaries any way they see fit, as long as the binary conforms to the ELF specification and the operating system @@ -94,7 +100,35 @@ One downside of this approach is that if one were to run the `llvm-strip` or `st shared libray, the `payload` section (as it uses a "non-standard" name) would be considered by the strip utility to be unnecessary and summarily removed. -### Layout of the payload library +The layout described above (a non-loadable `payload` section that the runtime finds by parsing the ELF section +headers itself) is used by the **MonoVM** runtime, which also locates the wrapper library by scanning the APK/AAB +ZIP central directory and then `mmap`s it. + +### CoreCLR: dlopen-based payload library + +The **CoreCLR** runtime uses a different, simpler mechanism. Its prebuilt native host (`libmonodroid.so`) is +shared by every application and build configuration, so it cannot rely on a baked-in `DT_NEEDED` dependency on the +assembly store (Debug/FastDev builds don't ship one). Instead, the assembly store wrapper library is produced so +that the store payload lives in a **loadable** ELF section (`SHF_ALLOC`, covered by a `PT_LOAD` segment) that is +pointed at by an exported dynamic symbol named `_assembly_store`. At runtime the host simply calls +`dlopen("libassembly-store.so", …)` followed by `dlsym(handle, "_assembly_store")` and lets the dynamic linker +locate and map the payload out of the APK — there is no ZIP scanning and no manual ELF section-header parsing. + +Because the section is allocatable and referenced by a dynamic symbol, this layout survives `strip`/`llvm-strip`, +unlike the MonoVM `payload` section described above. + +This wrapper is produced by +[`DlopenAssemblyStoreGenerator`](../../src/Xamarin.Android.Build.Tasks/Utilities/DlopenAssemblyStoreGenerator.cs) +(rather than `DSOWrapperGenerator`), which assembles a tiny `.incbin` stub with `llvm-mc` and links it into a +shared object with `ld` (no `llvm-objcopy` and no clang are involved). The section is still named `payload`, so +the extraction command (`llvm-objcopy --dump-section=payload=…`) shown below works for both layouts; the two +layouts are compared in detail in [Layout of the CoreCLR (dlopen) payload library](#layout-of-the-coreclr-dlopen-payload-library). + +### Layout of the MonoVM (stub) payload library + +> **Note:** the sample output in this section is the MonoVM stub layout, produced by `DSOWrapperGenerator`. +> Its `payload` section is **non-loadable** (no `A` flag, `Address` `0`) and there is no `_assembly_store` +> dynamic symbol. For the CoreCLR layout see the [next section](#layout-of-the-coreclr-dlopen-payload-library). In order to examine content of our "payload" ELF shared library, one can run the `llvm-readelf` utility which is shipped with the Android NDK (and also part of native developer tools on macOS and Linux distributions which have @@ -188,3 +222,69 @@ $ hexdump -c -n 4 payload.bin 0000000 X A B A 0000004 ``` + +### Layout of the CoreCLR (dlopen) payload library + +The CoreCLR wrapper differs from the MonoVM stub in two ways that matter to the runtime: + + 1. The `payload` section is **allocatable** (`SHF_ALLOC`, shown as the `A` flag) and is assigned a + virtual address, so it is covered by a `PT_LOAD` program header and mapped into memory by the + dynamic linker as part of `dlopen()`. + 2. An exported dynamic symbol, `_assembly_store`, points at the beginning of the payload, so the runtime + can retrieve the payload address with a single `dlsym()` call. + +The section header listing shows the `payload` section carrying the `A` flag and a non-zero address (compare +with the MonoVM listing above, where `payload` has no flags and address `0`): + +```shell +$ llvm-readelf --section-headers libassembly-store.so +Section Headers: + [Nr] Name Type Address Off Size ES Flg Lk Inf Al + ... + [ 5] .dynstr STRTAB 0000000000000290 000290 000026 00 A 0 0 1 + [ 6] payload PROGBITS 0000000000004000 004000 001004 00 A 0 0 16384 + [ 7] .text PROGBITS 0000000000009004 005004 000000 00 AX 0 0 4 + [ 8] .dynamic DYNAMIC 000000000000d008 005008 000080 10 WA 5 0 8 + ... +``` + +The program headers confirm that the `payload` section is part of a read-only `PT_LOAD` segment, i.e. the +dynamic linker maps it for us: + +```shell +$ llvm-readelf --program-headers libassembly-store.so +Program Headers: + Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align + ... + LOAD 0x000000 0x0000000000000000 0x0000000000000000 0x005004 0x005004 R 0x4000 + ... + + Section to Segment mapping: + Segment Sections... + 01 .note.gnu.build-id .dynsym .gnu.hash .hash .dynstr payload + ... +``` + +Finally, the dynamic symbol table exposes `_assembly_store`, whose value (`0x4000`) is the virtual address of +the `payload` section — this is exactly what `dlsym(handle, "_assembly_store")` returns at runtime: + +```shell +$ llvm-readelf --dyn-symbols libassembly-store.so +Symbol table '.dynsym' contains 2 entries: + Num: Value Size Type Bind Vis Ndx Name + 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND + 1: 0000000000004000 0 NOTYPE GLOBAL DEFAULT 6 _assembly_store +``` + +(The offsets and sizes above come from a tiny sample payload; a real assembly store's `payload` section will +be much larger, but the flags, `PT_LOAD` membership and the `_assembly_store` symbol are what identify a valid +CoreCLR wrapper.) + +Extraction works the same as for the MonoVM layout, since the section is still called `payload`: + +```shell +$ llvm-objcopy --dump-section=payload=payload.bin libassembly-store.so +$ hexdump -c -n 4 payload.bin +0000000 X A B A +0000004 +``` diff --git a/Documentation/project-docs/AssemblyStores.md b/Documentation/project-docs/AssemblyStores.md index f2eb29aec9d..a0b70c8e005 100644 --- a/Documentation/project-docs/AssemblyStores.md +++ b/Documentation/project-docs/AssemblyStores.md @@ -59,6 +59,20 @@ An assembly store, however, needs to be mapped only once and any further operations are merely pointer arithmetic, making the process not only faster but also reducing the algorithm complexity to O(1). +The way the store is located and mapped depends on the runtime: + + - **MonoVM** locates the store by scanning the APK/AAB ZIP central + directory, then `mmap(2)`s the wrapper shared library and walks its + ELF section headers by hand to find the `payload` section. + - **CoreCLR** relies on the dynamic linker instead: the store is + wrapped in a shared library whose payload lives in a *loadable* ELF + section pointed at by the exported `_assembly_store` dynamic symbol. + The runtime simply `dlopen()`s `libassembly-store.so` and resolves + the payload pointer with `dlsym("_assembly_store")`; there is no + ZIP scanning or manual section-header parsing. See + [ApkSharedLibraries.md](ApkSharedLibraries.md) for the two payload + layouts. + # Store locations There exists only one Assembly Store per architecture. Each application will contain From 8716783baeaf5de4214c8cba48c0c9cd13b59b84 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 13 Jul 2026 17:43:32 +0200 Subject: [PATCH 13/15] [CoreCLR] Clean up dlopen assembly-store wrappers between builds The CoreCLR dlopen()-based assembly-store wrapper is written to a per-RID `wrapped-dlopen` directory, but the packaging cleanup (`DSOWrapperGenerator.GetDirectoriesToCleanUp`, consumed via `CollectNativeFilesForArchive` -> `DSODirectoriesToDelete`) only knew about the MonoVM `wrapped` directory. As a result the dlopen wrappers were never pruned like their MonoVM counterparts, reintroducing the stale-wrapper problem the cleanup is meant to avoid. Centralize both subdirectory names (`wrapped`, `wrapped-dlopen`) in `DSOWrapperGenerator` and clean both in `CleanUp` / `GetDirectoriesToCleanUp`, so the two layouts are pruned consistently. `DlopenAssemblyStoreGenerator` now references the shared constant instead of a duplicated literal. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 99f309fa-142b-452d-b6c6-04a1dbaa52e3 --- .../Utilities/DSOWrapperGenerator.cs | 38 +++++++++++++------ .../Utilities/DlopenAssemblyStoreGenerator.cs | 2 +- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/DSOWrapperGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/DSOWrapperGenerator.cs index b0d450822fa..7a478ee48a4 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/DSOWrapperGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/DSOWrapperGenerator.cs @@ -79,9 +79,19 @@ public static Config GetConfig (TaskLoggingHelper log, string androidBinUtilsDir return new Config (stubPaths, androidBinUtilsDirectory, baseOutputDirectory); } - static string GetArchOutputPath (AndroidTargetArch targetArch, Config config) + // Per-RID subdirectory names that hold the generated wrapper shared libraries: `wrapped` for the + // MonoVM layout produced by this class, `wrapped-dlopen` for the CoreCLR layout produced by + // DlopenAssemblyStoreGenerator. Both live under `BaseOutputDirectory//` and MUST be cleaned up + // after packaging (see CleanUp/GetDirectoriesToCleanUp) so we never package a "stale" wrapper — they + // aren't part of any dependency chain, so their up-to-date state can't be checked reliably. + internal const string WrappedSubDirectory = "wrapped"; + internal const string WrappedDlopenSubDirectory = "wrapped-dlopen"; + + static readonly string[] WrapperSubDirectories = [ WrappedSubDirectory, WrappedDlopenSubDirectory ]; + + static string GetArchOutputPath (AndroidTargetArch targetArch, Config config, string subDirectory = WrappedSubDirectory) { - return Path.Combine (config.BaseOutputDirectory, MonoAndroidHelper.ArchToRid (targetArch), "wrapped"); + return Path.Combine (config.BaseOutputDirectory, MonoAndroidHelper.ArchToRid (targetArch), subDirectory); } /// @@ -134,12 +144,14 @@ public static string WrapIt (TaskLoggingHelper log, Config config, AndroidTarget public static void CleanUp (Config config) { foreach (var kvp in config.DSOStubPaths) { - string outputDir = GetArchOutputPath (kvp.Key, config); - if (!Directory.Exists (outputDir)) { - continue; - } + foreach (string subDirectory in WrapperSubDirectories) { + string outputDir = GetArchOutputPath (kvp.Key, config, subDirectory); + if (!Directory.Exists (outputDir)) { + continue; + } - Directory.Delete (outputDir, recursive: true); + Directory.Delete (outputDir, recursive: true); + } } } @@ -148,12 +160,14 @@ public static string [] GetDirectoriesToCleanUp (Config config) var dirs = new List (); foreach (var kvp in config.DSOStubPaths) { - string outputDir = GetArchOutputPath (kvp.Key, config); - if (!Directory.Exists (outputDir)) { - continue; - } + foreach (string subDirectory in WrapperSubDirectories) { + string outputDir = GetArchOutputPath (kvp.Key, config, subDirectory); + if (!Directory.Exists (outputDir)) { + continue; + } - dirs.Add (outputDir); + dirs.Add (outputDir); + } } return dirs.ToArray (); diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/DlopenAssemblyStoreGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/DlopenAssemblyStoreGenerator.cs index 8984949db64..c001e8015ae 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/DlopenAssemblyStoreGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/DlopenAssemblyStoreGenerator.cs @@ -54,7 +54,7 @@ public static string WrapIt (TaskLoggingHelper log, DSOWrapperGenerator.Config c { var toolInfo = GetArchToolInfo (targetArch); - string outputDir = Path.Combine (config.BaseOutputDirectory, MonoAndroidHelper.ArchToRid (targetArch), "wrapped-dlopen"); + string outputDir = Path.Combine (config.BaseOutputDirectory, MonoAndroidHelper.ArchToRid (targetArch), DSOWrapperGenerator.WrappedDlopenSubDirectory); Directory.CreateDirectory (outputDir); string outputFile = Path.Combine (outputDir, outputFileName); From 1ebd3eb07c492247d1d5f3e4480083394c03cb0d Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 13 Jul 2026 17:44:27 +0200 Subject: [PATCH 14/15] [CoreCLR] Fail fast if _AndroidDlopenAssemblyStore is overridden `_AndroidDlopenAssemblyStore` is documented as runtime-dictated and "MUST NOT be overridden" (true for CoreCLR, false otherwise), but MSBuild properties can still be overridden by a later import/target. If that happens, CoreCLR would dlopen()+dlsym() `_assembly_store` on a store wrapped with the MonoVM layout (or vice versa) and abort at app startup with an opaque failure. Add fail-fast validation right before the WrapAssembliesAsSharedLibraries call so an accidental override is caught at build time. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 99f309fa-142b-452d-b6c6-04a1dbaa52e3 --- .../Xamarin.Android.Common.targets | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index cd07087d9a7..0a294bd4171 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -2235,6 +2235,17 @@ because xbuild doesn't support framework reference assemblies. + + + + Date: Mon, 13 Jul 2026 17:48:47 +0200 Subject: [PATCH 15/15] [CoreCLR] Align dlopen wrapper payload to the arch max page size MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The assembly-store wrapper payload was `.balign`-ed to a hardcoded 16k for every ABI, but 32-bit ABIs (armeabi-v7a, x86) never run on 16k-page devices — Android's 16k-page support is 64-bit-only. `GetArchToolInfo` already carries a per-arch `MaxPageSize` (16k for the 64-bit arches, 4k for the 32-bit ones) that is used for `ld -z max-page-size`; align the payload to that value instead. This avoids up to ~12k of dead padding in each 32-bit wrapper while keeping the 64-bit wrappers 16k-aligned. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 99f309fa-142b-452d-b6c6-04a1dbaa52e3 --- .../Utilities/DlopenAssemblyStoreGenerator.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/DlopenAssemblyStoreGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/DlopenAssemblyStoreGenerator.cs index c001e8015ae..90bba8fb3c0 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/DlopenAssemblyStoreGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/DlopenAssemblyStoreGenerator.cs @@ -35,9 +35,11 @@ static class DlopenAssemblyStoreGenerator // (Utils.FindELFPayloadSectionOffsetAndSize) looks for and the one `DSOWrapperGenerator` uses. const string PayloadSectionName = "payload"; - // 16k so the payload is page-aligned on both 4k and 16k page-size devices. - const int PayloadAlignment = 16384; - + // Per-arch max page size, used both for `ld -z max-page-size` and to align the payload so it is + // page-aligned on the largest page size the ABI can run on: 16k for the 64-bit arches (which may + // run on 16k-page devices), 4k for the 32-bit arches (Android's 16k-page support is 64-bit-only). + // Using the per-arch value instead of a hardcoded 16k avoids ~12k of dead padding in each 32-bit + // wrapper. static (string Triple, string ElfArch, int MaxPageSize) GetArchToolInfo (AndroidTargetArch arch) => arch switch { AndroidTargetArch.Arm64 => ("aarch64-linux-android", "aarch64linux", 16384), AndroidTargetArch.Arm => ("armv7-linux-androideabi", "armelf_linux_eabi", 4096), @@ -69,7 +71,7 @@ public static string WrapIt (TaskLoggingHelper log, DSOWrapperGenerator.Config c string incbinPath = payloadFilePath.Replace ("\\", "\\\\").Replace ("\"", "\\\""); string asm = $""" .section {PayloadSectionName}, "a" - .balign {PayloadAlignment} + .balign {toolInfo.MaxPageSize} .globl {PayloadStartSymbol} {PayloadStartSymbol}: .incbin "{incbinPath}"