From a07be8e7c946b4661d921b72eb5a06ea8ee6febd Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Thu, 18 Jun 2026 15:09:01 -0500 Subject: [PATCH 1/8] [Mono.Android] CoreCLR: file/line in stack traces with FastDev When CoreCLR runs an Android app with FastDev, app assemblies live on disk in `files/.__override__//` with their portable PDBs alongside. But we resolved them through `host_runtime_contract.external_assembly_probe`, which reads the .dll into a heap buffer and hands the bytes to the runtime. The CLR never opens the file itself, so `Assembly.Location` is empty and `StackTraceSymbols` has no anchor for finding the sibling .pdb. The result: `Console.WriteLine(Environment.StackTrace)` and `Exception.StackTrace` print method names only, no `in File.cs:line N` info. Fix: in Debug startup, append `TRUSTED_PLATFORM_ASSEMBLIES` to the properties passed to `coreclr_initialize`, listing the full on-disk path of every assembly from the typemap that is also present in the override directory. The CLR then mmap's those files itself via `PEImage::OpenImage`, which records `m_path`, populates `Assembly.Location`, and lets `StackTraceSymbols.TryOpenAssociatedPortablePdb` find the matching .pdb via simple sibling-file lookup. No DebugType change required, no opt-in property, no impact on Release. The TPA list is bounded by `type_map_unique_assemblies` (the build-time set of assemblies contributing typemap entries), so we never pass arbitrary files. BCL assemblies still flow through the existing FastDev/AssemblyStore probe path unchanged. Test: adds `StackTraceContainsLineNumbers` to `InstallAndRunTests` (CoreCLR, Debug, FastDev) which runs an app emitting `Environment.StackTrace` and asserts logcat contains a frame like `at ...MainActivity.OnCreate ... in ...MainActivity.cs:line N`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/native/clr/host/fastdev-assemblies.cc | 51 +++++++++++++++++++ src/native/clr/host/host.cc | 37 ++++++++++++-- .../clr/include/host/fastdev-assemblies.hh | 7 +++ .../Tests/InstallAndRunTests.cs | 51 +++++++++++++++++++ 4 files changed, 143 insertions(+), 3 deletions(-) diff --git a/src/native/clr/host/fastdev-assemblies.cc b/src/native/clr/host/fastdev-assemblies.cc index dea16523aaf..e96d483833f 100644 --- a/src/native/clr/host/fastdev-assemblies.cc +++ b/src/native/clr/host/fastdev-assemblies.cc @@ -6,8 +6,10 @@ #include #include #include +#include #include +#include #include #include #include @@ -111,3 +113,52 @@ auto FastDevAssemblies::open_assembly (std::string_view const& name, int64_t &si return reinterpret_cast(buffer); } + +auto FastDevAssemblies::build_tpa_list (std::string &tpa_list) noexcept -> bool +{ + tpa_list.clear (); + + std::string const& override_dir_path = AndroidSystem::get_primary_override_dir (); + if (!Util::dir_exists (override_dir_path)) { + return false; + } + + DIR *dir = opendir (override_dir_path.c_str ()); + if (dir == nullptr) { + log_warn (LOG_ASSEMBLY, "FastDev: failed to open override dir '{}'. {}"sv, override_dir_path, std::strerror (errno)); + return false; + } + int dir_fd = dirfd (dir); + + size_t count = 0; + uint64_t expected_count = type_map.unique_assemblies_count; + for (uint64_t i = 0; i < expected_count; i++) { + TypeMapAssembly const &asm_entry = type_map_unique_assemblies[i]; + std::string_view name { + &type_map_assembly_names[asm_entry.name_offset], + static_cast(asm_entry.name_length) + }; + + // `Name` is the simple assembly name (e.g. "Mono.Android"), no extension. + std::string file_name; + file_name.reserve (name.size () + 4); + file_name.append (name); + file_name.append (".dll"); + + if (!Util::file_exists (dir_fd, file_name)) { + continue; + } + + if (!tpa_list.empty ()) { + tpa_list.append (":"); + } + tpa_list.append (override_dir_path); + tpa_list.append ("/"); + tpa_list.append (file_name); + count++; + } + closedir (dir); + + log_debug (LOG_ASSEMBLY, "FastDev: built TPA list with {} assemblies from '{}'"sv, count, override_dir_path); + return count > 0; +} diff --git a/src/native/clr/host/host.cc b/src/native/clr/host/host.cc index 3e84ef930d8..8193474eb96 100644 --- a/src/native/clr/host/host.cc +++ b/src/native/clr/host/host.cc @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include #include @@ -450,12 +452,41 @@ void Host::Java_mono_android_Runtime_initInternal ( // The first entry in the property arrays is for the host contract pointer. Application build makes sure // of that. init_runtime_property_values[0] = host_contract_ptr_buffer.data (); + + const char **prop_names = init_runtime_property_names; + const char **prop_values = const_cast(init_runtime_property_values); + int prop_count = static_cast(application_config.number_of_runtime_properties); + + // In Debug builds with FastDev, append `TRUSTED_PLATFORM_ASSEMBLIES` with full + // paths to the assemblies pushed into `.__override__//`. CoreCLR then + // opens those files from disk so `Assembly.Location` is populated and + // `StackTraceSymbols` can find sibling `.pdb` files for runtime-rendered + // managed stack traces (file/line). + if constexpr (Constants::is_debug_build) { + // Storage must outlive `coreclr_initialize`; function-local statics + // give us process lifetime without polluting global namespace. + static std::string fastdev_tpa_list; + static std::vector fastdev_prop_names; + static std::vector fastdev_prop_values; + + if (FastDevAssemblies::build_tpa_list (fastdev_tpa_list)) { + fastdev_prop_names.assign (prop_names, prop_names + prop_count); + fastdev_prop_values.assign (prop_values, prop_values + prop_count); + fastdev_prop_names.push_back (HOST_PROPERTY_TRUSTED_PLATFORM_ASSEMBLIES); + fastdev_prop_values.push_back (fastdev_tpa_list.c_str ()); + + prop_names = fastdev_prop_names.data (); + prop_values = fastdev_prop_values.data (); + prop_count = static_cast(fastdev_prop_names.size ()); + } + } + int hr = FastTiming::time_call ("coreclr_initialize"sv, coreclr_initialize, application_config.android_package_name, "Xamarin.Android", - (int)application_config.number_of_runtime_properties, - init_runtime_property_names, - const_cast(init_runtime_property_values), + prop_count, + prop_names, + prop_values, &clr_host, &domain_id ); diff --git a/src/native/clr/include/host/fastdev-assemblies.hh b/src/native/clr/include/host/fastdev-assemblies.hh index 51f1945fce3..fffcca1e650 100644 --- a/src/native/clr/include/host/fastdev-assemblies.hh +++ b/src/native/clr/include/host/fastdev-assemblies.hh @@ -4,6 +4,7 @@ #include #include +#include #include namespace xamarin::android { @@ -12,11 +13,17 @@ namespace xamarin::android { public: #if defined(DEBUG) static auto open_assembly (std::string_view const& name, int64_t &size) noexcept -> void*; + static auto build_tpa_list (std::string &tpa_list) noexcept -> bool; #else static auto open_assembly ([[maybe_unused]] std::string_view const& name, [[maybe_unused]] int64_t &size) noexcept -> void* { return nullptr; } + + static auto build_tpa_list ([[maybe_unused]] std::string &tpa_list) noexcept -> bool + { + return false; + } #endif private: diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index 322a8d5034b..cc4bd84b0a6 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using System.Threading; using System.Xml.Linq; using System.Xml.XPath; @@ -2227,6 +2228,56 @@ public void FastDeployEnvironmentFiles ([Values] bool isRelease, [Values] bool e } } + [Test] + public void StackTraceContainsLineNumbers () + { + // FastDev (Debug + assemblies on disk in .__override__) wires up + // portable PDB lookup for runtime-rendered stack traces on CoreCLR + // via the TPA list passed to coreclr_initialize. + AndroidRuntime runtime = AndroidRuntime.CoreCLR; + if (IgnoreUnsupportedConfiguration (runtime, release: false)) { + return; + } + + var proj = new XamarinAndroidApplicationProject (packageName: PackageUtils.MakePackageName (runtime)) { + ProjectName = nameof (StackTraceContainsLineNumbers), + RootNamespace = nameof (StackTraceContainsLineNumbers), + IsRelease = false, + EmbedAssembliesIntoApk = false, + EnableDefaultItems = true, + }; + proj.SetRuntime (runtime); + proj.MainActivity = proj.DefaultMainActivity.Replace ("//${AFTER_ONCREATE}", @" + Console.WriteLine (""#STACKTRACE-BEGIN#""); + Console.WriteLine (Environment.StackTrace); + Console.WriteLine (""#STACKTRACE-END#""); + "); + using var builder = CreateApkBuilder (); + Assert.IsTrue (builder.Install (proj), "App should have installed."); + RunProjectAndAssert (proj, builder); + + var appStartupLogcatFile = Path.Combine (Root, builder.ProjectDirectory, "logcat.log"); + Assert.IsTrue ( + WaitForActivityToStart (proj.PackageName, "MainActivity", appStartupLogcatFile, ActivityStartTimeoutInSeconds), + "MainActivity should have launched!" + ); + + var logcatOutput = File.ReadAllText (appStartupLogcatFile); + StringAssert.Contains ("#STACKTRACE-BEGIN#", logcatOutput, "Stack trace marker not found in logcat"); + + // Expect a frame in MainActivity.OnCreate to include + // "in MainActivity.cs:line ". + var match = Regex.Match ( + logcatOutput, + @"at\s+\S*MainActivity\.OnCreate.*\sin\s+\S+MainActivity\.cs:line\s+\d+", + RegexOptions.Singleline + ); + Assert.IsTrue ( + match.Success, + $"Expected MainActivity.OnCreate frame to include file/line info. Logcat:\n{logcatOutput}" + ); + } + [Test] public void DotNetRunEnvironmentVariables () { From f43e3de6ed1077ab11022a7292fd116aca6df7b0 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Thu, 18 Jun 2026 15:17:53 -0500 Subject: [PATCH 2/8] Address review feedback - Bail out cleanly if dirfd() fails - Assert #STACKTRACE-END# marker to catch truncated output Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/native/clr/host/fastdev-assemblies.cc | 5 +++++ tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/native/clr/host/fastdev-assemblies.cc b/src/native/clr/host/fastdev-assemblies.cc index e96d483833f..8cab1128c51 100644 --- a/src/native/clr/host/fastdev-assemblies.cc +++ b/src/native/clr/host/fastdev-assemblies.cc @@ -129,6 +129,11 @@ auto FastDevAssemblies::build_tpa_list (std::string &tpa_list) noexcept -> bool return false; } int dir_fd = dirfd (dir); + if (dir_fd < 0) { + log_warn (LOG_ASSEMBLY, "FastDev: failed to obtain fd for override dir '{}'. {}"sv, override_dir_path, std::strerror (errno)); + closedir (dir); + return false; + } size_t count = 0; uint64_t expected_count = type_map.unique_assemblies_count; diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index cc4bd84b0a6..942951c5934 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -2263,7 +2263,8 @@ public void StackTraceContainsLineNumbers () ); var logcatOutput = File.ReadAllText (appStartupLogcatFile); - StringAssert.Contains ("#STACKTRACE-BEGIN#", logcatOutput, "Stack trace marker not found in logcat"); + StringAssert.Contains ("#STACKTRACE-BEGIN#", logcatOutput, "Stack trace start marker not found in logcat"); + StringAssert.Contains ("#STACKTRACE-END#", logcatOutput, "Stack trace end marker not found in logcat (output may be truncated)"); // Expect a frame in MainActivity.OnCreate to include // "in MainActivity.cs:line ". From 4e0ee56016e4eeabe08c65569396c94ef7bcee5c Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Thu, 18 Jun 2026 15:39:20 -0500 Subject: [PATCH 3/8] Address review feedback: deterministic test capture - Use MonitorAdbLogcat to wait for #STACKTRACE-END# marker instead of WaitForActivityToStart, which races the async stdout->logcat flush and could finish capturing before the trace lands. - Drop RegexOptions.Singleline so the OnCreate frame and file:line must appear on the same line, ensuring the assertion is specific to the OnCreate frame. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Tests/InstallAndRunTests.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index 942951c5934..d68827b10b1 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -2256,22 +2256,20 @@ public void StackTraceContainsLineNumbers () Assert.IsTrue (builder.Install (proj), "App should have installed."); RunProjectAndAssert (proj, builder); - var appStartupLogcatFile = Path.Combine (Root, builder.ProjectDirectory, "logcat.log"); + var appStartupLogcatFile = Path.Combine (Root, builder.ProjectDirectory, "stacktrace-logcat.log"); Assert.IsTrue ( - WaitForActivityToStart (proj.PackageName, "MainActivity", appStartupLogcatFile, ActivityStartTimeoutInSeconds), - "MainActivity should have launched!" + MonitorAdbLogcat (line => line.Contains ("#STACKTRACE-END#"), appStartupLogcatFile, timeout: 60), + "Stack trace end marker not found in logcat (output may be missing or truncated)." ); var logcatOutput = File.ReadAllText (appStartupLogcatFile); StringAssert.Contains ("#STACKTRACE-BEGIN#", logcatOutput, "Stack trace start marker not found in logcat"); - StringAssert.Contains ("#STACKTRACE-END#", logcatOutput, "Stack trace end marker not found in logcat (output may be truncated)"); // Expect a frame in MainActivity.OnCreate to include - // "in MainActivity.cs:line ". + // "in MainActivity.cs:line " on a single line. var match = Regex.Match ( logcatOutput, - @"at\s+\S*MainActivity\.OnCreate.*\sin\s+\S+MainActivity\.cs:line\s+\d+", - RegexOptions.Singleline + @"at\s+\S*MainActivity\.OnCreate.*\sin\s+\S+MainActivity\.cs:line\s+\d+" ); Assert.IsTrue ( match.Success, From a439e1745d967663709d94e744be88a2d6f999d7 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Thu, 18 Jun 2026 16:40:58 -0500 Subject: [PATCH 4/8] Address review feedback: doc note and raw string literal Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/native/clr/host/fastdev-assemblies.cc | 5 +++++ .../MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/native/clr/host/fastdev-assemblies.cc b/src/native/clr/host/fastdev-assemblies.cc index 8cab1128c51..dbc2097a7d7 100644 --- a/src/native/clr/host/fastdev-assemblies.cc +++ b/src/native/clr/host/fastdev-assemblies.cc @@ -136,6 +136,11 @@ auto FastDevAssemblies::build_tpa_list (std::string &tpa_list) noexcept -> bool } size_t count = 0; + // NOTE: The TPA list is sourced from `type_map_unique_assemblies`, which is + // only populated when `_AndroidTypeMapImplementation=llvm-ir` (the Debug + // default). With `managed` or `trimmable` typemaps the native typemap is + // empty, so no TPA paths are added and stack frames won't carry file/line + // info even under FastDev. uint64_t expected_count = type_map.unique_assemblies_count; for (uint64_t i = 0; i < expected_count; i++) { TypeMapAssembly const &asm_entry = type_map_unique_assemblies[i]; diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index d68827b10b1..6cbb273088c 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -2247,11 +2247,11 @@ public void StackTraceContainsLineNumbers () EnableDefaultItems = true, }; proj.SetRuntime (runtime); - proj.MainActivity = proj.DefaultMainActivity.Replace ("//${AFTER_ONCREATE}", @" - Console.WriteLine (""#STACKTRACE-BEGIN#""); + proj.MainActivity = proj.DefaultMainActivity.Replace ("//${AFTER_ONCREATE}", """ + Console.WriteLine ("#STACKTRACE-BEGIN#"); Console.WriteLine (Environment.StackTrace); - Console.WriteLine (""#STACKTRACE-END#""); - "); + Console.WriteLine ("#STACKTRACE-END#"); + """); using var builder = CreateApkBuilder (); Assert.IsTrue (builder.Install (proj), "App should have installed."); RunProjectAndAssert (proj, builder); From 69f51db12fb128ec171a53299fa3b4ccb99cb9a6 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 24 Jun 2026 11:02:45 -0500 Subject: [PATCH 5/8] Scan override dir directly for TPA list Drops the dependency on type_map_unique_assemblies (only populated for _AndroidTypeMapImplementation=llvm-ir) and enumerates *.dll in the override directory instead. The feature now works for managed and trimmable typemaps too. The override dir is the app's private storage, only writable via adb push from the developer's build, and the existing clr_external_assembly_probe already loads anything CoreCLR asks for from this same directory unconditionally - so TPA-ing every *.dll adds no attack surface. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/native/clr/host/fastdev-assemblies.cc | 34 ++++------------------- 1 file changed, 6 insertions(+), 28 deletions(-) diff --git a/src/native/clr/host/fastdev-assemblies.cc b/src/native/clr/host/fastdev-assemblies.cc index dbc2097a7d7..b2b52874f6d 100644 --- a/src/native/clr/host/fastdev-assemblies.cc +++ b/src/native/clr/host/fastdev-assemblies.cc @@ -9,7 +9,6 @@ #include #include -#include #include #include #include @@ -128,34 +127,13 @@ auto FastDevAssemblies::build_tpa_list (std::string &tpa_list) noexcept -> bool log_warn (LOG_ASSEMBLY, "FastDev: failed to open override dir '{}'. {}"sv, override_dir_path, std::strerror (errno)); return false; } - int dir_fd = dirfd (dir); - if (dir_fd < 0) { - log_warn (LOG_ASSEMBLY, "FastDev: failed to obtain fd for override dir '{}'. {}"sv, override_dir_path, std::strerror (errno)); - closedir (dir); - return false; - } + constexpr std::string_view dll_ext { ".dll" }; size_t count = 0; - // NOTE: The TPA list is sourced from `type_map_unique_assemblies`, which is - // only populated when `_AndroidTypeMapImplementation=llvm-ir` (the Debug - // default). With `managed` or `trimmable` typemaps the native typemap is - // empty, so no TPA paths are added and stack frames won't carry file/line - // info even under FastDev. - uint64_t expected_count = type_map.unique_assemblies_count; - for (uint64_t i = 0; i < expected_count; i++) { - TypeMapAssembly const &asm_entry = type_map_unique_assemblies[i]; - std::string_view name { - &type_map_assembly_names[asm_entry.name_offset], - static_cast(asm_entry.name_length) - }; - - // `Name` is the simple assembly name (e.g. "Mono.Android"), no extension. - std::string file_name; - file_name.reserve (name.size () + 4); - file_name.append (name); - file_name.append (".dll"); - - if (!Util::file_exists (dir_fd, file_name)) { + dirent *e; + while ((e = readdir (dir)) != nullptr) { + std::string_view name { e->d_name }; + if (name.size () <= dll_ext.size () || !name.ends_with (dll_ext)) { continue; } @@ -164,7 +142,7 @@ auto FastDevAssemblies::build_tpa_list (std::string &tpa_list) noexcept -> bool } tpa_list.append (override_dir_path); tpa_list.append ("/"); - tpa_list.append (file_name); + tpa_list.append (name); count++; } closedir (dir); From 0ce17b3173ab69a5b54f9f1f8ede07d0190c3e4f Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Thu, 25 Jun 2026 10:58:38 -0500 Subject: [PATCH 6/8] Yield to TPA-based loading when FastDev TPA list is built CoreCLR's BindByTpaList in assemblybindercommon.cpp gives the external assembly probe precedence over TPA. When our clr_external_assembly_probe returns true with the FastDev assembly bytes, CoreCLR constructs the PEImage's m_path from just the bare simple name + .dll (since the app is not a bundle), so Assembly.Location ends up as 'Foo.dll' with no directory. StackTraceSymbols.TryOpenAssociatedPortablePdb then probes a non-existent 'Foo.pdb' and stack frames render without file/line info. Fix: when build_tpa_list successfully passes the override directory's DLLs to coreclr_initialize via TRUSTED_PLATFORM_ASSEMBLIES, set a flag and short-circuit the FastDev probe so CoreCLR falls through to the TPA path. The TPA path calls PEImage::OpenImage(fullPath, ...) and sets Assembly.Location to the full disk path, which makes sibling PDB discovery work. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/native/clr/host/fastdev-assemblies.cc | 16 +++++++++++++++- .../clr/include/host/fastdev-assemblies.hh | 7 +++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/native/clr/host/fastdev-assemblies.cc b/src/native/clr/host/fastdev-assemblies.cc index b2b52874f6d..9374f42798a 100644 --- a/src/native/clr/host/fastdev-assemblies.cc +++ b/src/native/clr/host/fastdev-assemblies.cc @@ -19,6 +19,16 @@ auto FastDevAssemblies::open_assembly (std::string_view const& name, int64_t &si { size = 0; + // When the override directory was used to build a `TRUSTED_PLATFORM_ASSEMBLIES` + // list (see `build_tpa_list`), the external probe must yield to TPA-based + // loading so that CoreCLR opens the assembly from disk via `PEImage::OpenImage` + // and `Assembly.Location` ends up populated. Otherwise sibling portable PDB + // lookup (used by `StackTraceSymbols`) returns an empty path and stack frames + // render without file/line info. + if (tpa_in_use) [[likely]] { + return nullptr; + } + std::string const& override_dir_path = AndroidSystem::get_primary_override_dir (); if (!Util::dir_exists (override_dir_path)) [[unlikely]] { log_debug (LOG_ASSEMBLY, "Override directory '{}' does not exist"sv, override_dir_path); @@ -148,5 +158,9 @@ auto FastDevAssemblies::build_tpa_list (std::string &tpa_list) noexcept -> bool closedir (dir); log_debug (LOG_ASSEMBLY, "FastDev: built TPA list with {} assemblies from '{}'"sv, count, override_dir_path); - return count > 0; + if (count > 0) { + tpa_in_use = true; + return true; + } + return false; } diff --git a/src/native/clr/include/host/fastdev-assemblies.hh b/src/native/clr/include/host/fastdev-assemblies.hh index fffcca1e650..d8d69bdabe0 100644 --- a/src/native/clr/include/host/fastdev-assemblies.hh +++ b/src/native/clr/include/host/fastdev-assemblies.hh @@ -31,6 +31,13 @@ namespace xamarin::android { static inline DIR *override_dir = nullptr; static inline int override_dir_fd = -1; static inline std::mutex override_dir_lock {}; + // Set by `build_tpa_list` when assemblies in the override directory are + // passed to CoreCLR via `TRUSTED_PLATFORM_ASSEMBLIES`. When true, the + // external assembly probe yields to TPA-based loading so that + // `Assembly.Location` is populated with the full disk path (needed for + // `StackTraceSymbols` to find sibling portable PDB files). + public: + static inline bool tpa_in_use = false; #endif }; } From 73cc65dc8f38a528cda158bde26b0409f3f371f1 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Thu, 25 Jun 2026 13:20:49 -0500 Subject: [PATCH 7/8] Only enable TPA when System.Private.CoreLib.dll is in the override dir Passing TRUSTED_PLATFORM_ASSEMBLIES to coreclr_initialize changes the CLR binder mode such that System.Private.CoreLib.dll is resolved via the TPA / external probe path instead of CoreCLR's built-in bootstrap. On partial FastDev deployments (e.g. MonoAndroidExportReferencedAppStarts, which only syncs ~29 user assemblies and has no assembly store and no CoreLib in .__override__/), the binder cannot find CoreLib and the process aborts with 'Failed to initialize CoreCLR. Error code: 80070002'. Gate TPA on the presence of System.Private.CoreLib.dll in the override directory. Full-deploy FastDev scenarios (such as the StackTrace file/line test) continue to get TPA and the resulting Assembly.Location fix; partial deployments retain the pre-PR behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/native/clr/host/fastdev-assemblies.cc | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/native/clr/host/fastdev-assemblies.cc b/src/native/clr/host/fastdev-assemblies.cc index 9374f42798a..9f661f2bb36 100644 --- a/src/native/clr/host/fastdev-assemblies.cc +++ b/src/native/clr/host/fastdev-assemblies.cc @@ -139,6 +139,8 @@ auto FastDevAssemblies::build_tpa_list (std::string &tpa_list) noexcept -> bool } constexpr std::string_view dll_ext { ".dll" }; + constexpr std::string_view corelib_name { "System.Private.CoreLib.dll" }; + bool found_corelib = false; size_t count = 0; dirent *e; while ((e = readdir (dir)) != nullptr) { @@ -153,14 +155,24 @@ auto FastDevAssemblies::build_tpa_list (std::string &tpa_list) noexcept -> bool tpa_list.append (override_dir_path); tpa_list.append ("/"); tpa_list.append (name); + if (name == corelib_name) { + found_corelib = true; + } count++; } closedir (dir); log_debug (LOG_ASSEMBLY, "FastDev: built TPA list with {} assemblies from '{}'"sv, count, override_dir_path); - if (count > 0) { + + // We can only safely hand a TPA list to CoreCLR when it contains + // `System.Private.CoreLib.dll`. Passing TPA without CoreLib changes the + // CLR binder mode such that CoreLib is searched via TPA/probe instead of + // the built-in bootstrap, which fails on incomplete FastDev deployments + // (e.g. tests that only sync a handful of user assemblies). + if (count > 0 && found_corelib) { tpa_in_use = true; return true; } + tpa_list.clear (); return false; } From 3f7e30d4de3879f947750c34daa2eb114b154f11 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Fri, 26 Jun 2026 14:58:45 -0500 Subject: [PATCH 8/8] Let CoreLib bootstrap through FastDev probe even when TPA is in use CoreCLR loads System.Private.CoreLib.dll via the external assembly probe during runtime startup (before the regular TPA-aware binder is online). When tpa_in_use was set we returned nullptr for every probe call, so the CoreLib bootstrap could not find CoreLib and coreclr_initialize aborted with 0x80070002 even on full FastDev deployments where CoreLib was sitting right there in .__override__//. Special-case 'System.Private.CoreLib.dll' so the probe returns its bytes even with TPA active. CoreLib has no user code to symbolicate, so its bare-filename Assembly.Location is irrelevant for the file/line work. All other assemblies still yield to TPA, get their full disk paths, and let StackTraceSymbols find sibling .pdb files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/native/clr/host/fastdev-assemblies.cc | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/native/clr/host/fastdev-assemblies.cc b/src/native/clr/host/fastdev-assemblies.cc index 9f661f2bb36..669605e55d7 100644 --- a/src/native/clr/host/fastdev-assemblies.cc +++ b/src/native/clr/host/fastdev-assemblies.cc @@ -20,12 +20,19 @@ auto FastDevAssemblies::open_assembly (std::string_view const& name, int64_t &si size = 0; // When the override directory was used to build a `TRUSTED_PLATFORM_ASSEMBLIES` - // list (see `build_tpa_list`), the external probe must yield to TPA-based + // list (see `build_tpa_list`), the external probe should yield to TPA-based // loading so that CoreCLR opens the assembly from disk via `PEImage::OpenImage` // and `Assembly.Location` ends up populated. Otherwise sibling portable PDB // lookup (used by `StackTraceSymbols`) returns an empty path and stack frames // render without file/line info. - if (tpa_in_use) [[likely]] { + // + // The CoreLib bootstrap is a special case: CoreCLR loads + // `System.Private.CoreLib.dll` via the external probe (not through the + // regular TPA-aware binder), so we must keep returning the bytes for it + // even when TPA is in use. CoreLib has no user code we'd symbolicate, so + // the resulting bare-filename `Assembly.Location` does not matter. + constexpr std::string_view corelib_name { "System.Private.CoreLib.dll" }; + if (tpa_in_use && name != corelib_name) { return nullptr; }