Skip to content

Commit 1a62af3

Browse files
authored
[native] When preloading shared JNI libraries, update alias entries too (#10444)
Context: cba39dc Context: #10376 Context: #10324 cba39dc implemented preloading of JNI-using native libraries but it missed to update alias entries for each preloaded library. During application build we generate native code that contains cache for each native library packaged with the managed application code. Each library follows the same naming pattern: `lib<NAME>.so`. However, the managed code can refer to those libraries (when e.g. declaring a p/invoke with the `[DllImporrt]` attribute) using different forms of names. The request may have a form of `lib<NAME>` or `<NAME>` etc. When the runtime tries to resolve the p/invoke symbol, it first needs to load the shared library. This is done (in our case) by using a callback into our runtime which then tries to find the library and load it. Should the attempt fail, the runtime will mutate the library name and ask as again until all the possible names are tried or the library is loaded successfully. This roundtrip is pretty expensive, so in our native library loader code we implemented (in c227042) a scheme where at build time we mutate library names ourselves and a separate entry for each name mutation in the shared library cache. This way, when the runtime request comes, we perform a single search and are able to find the library no matter what name the managed code requested. Each of the cache entries contains, among other things irrelevant to this PR, a field which stores the native library's handle, after it is loaded. cba39dc loaded the library and set that field in just a single cache entry, the one corresponding to the canonical library name (`lib<NAME>.so`) but it failed to set the field in all the aliases. This resulted in an attempt to load the library again, with the managed code requesting it by a different name, finding the corresponding cache entry and seeing that its handle is unset. However, since the request was sent from a different thread, we attempted to load the library on the main thread (described in detail in cba39dc commit message), which attempt always failed leading to an endless loop and application crash/hang while debugging. Fix the issue by setting native shared library handle in all the cache entries corresponding to various mutations of the library name. This makes sure that further requests to load the library will see the handle set in cache and use it, instead of attempting to load the it again.
1 parent 68f1238 commit 1a62af3

File tree

13 files changed

+226
-29
lines changed

13 files changed

+226
-29
lines changed

src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Forms/MainActivity.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,6 @@ protected override void OnCreate (Bundle savedInstanceState)
2323
//${AFTER_FORMS_INIT}
2424
LoadApplication (new App ());
2525
}
26+
//${AFTER_ONCREATE}
2627
}
2728
}

src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGenerator.cs

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,9 @@ sealed class DsoCacheState
161161
{
162162
public List<StructureInstance<DSOCacheEntry>> DsoCache = [];
163163
public List<DSOCacheEntry> JniPreloadDSOs = [];
164+
public List<string> JniPreloadNames = [];
164165
public List<StructureInstance<DSOCacheEntry>> AotDsoCache = [];
166+
public uint NameMutationsCount = 1;
165167
}
166168

167169
// Keep in sync with FORMAT_TAG in src/monodroid/jni/xamarin-app.hh
@@ -274,10 +276,14 @@ protected override void Construct (LlvmIrModule module)
274276
};
275277
module.Add (dso_cache);
276278

279+
module.AddGlobalVariable ("dso_jni_preloads_idx_stride", dsoState.NameMutationsCount);
280+
277281
// This variable MUST be written after `dso_cache` since it relies on sorting performed by HashAndSortDSOCache
278282
var dso_jni_preloads_idx = new LlvmIrGlobalVariable (new List<uint> (), "dso_jni_preloads_idx", LlvmIrVariableOptions.GlobalConstant) {
279283
Comment = " Indices into dso_cache[] of DSO libraries to preload because of JNI use",
280284
ArrayItemCount = (uint)dsoState.JniPreloadDSOs.Count,
285+
GetArrayItemCommentCallback = GetPreloadIndicesLibraryName,
286+
GetArrayItemCommentCallbackCallerState = dsoState,
281287
BeforeWriteCallback = PopulatePreloadIndices,
282288
BeforeWriteCallbackCallerState = dsoState,
283289
};
@@ -346,6 +352,21 @@ void AddAssemblyStores (LlvmIrModule module)
346352
module.Add (assembly_store);
347353
}
348354

355+
string? GetPreloadIndicesLibraryName (LlvmIrVariable v, LlvmIrModuleTarget target, ulong index, object? value, object? callerState)
356+
{
357+
// Instead of throwing for such a triviality like a comment, we will return error messages as comments instead
358+
var dsoState = callerState as DsoCacheState;
359+
if (dsoState == null) {
360+
return " Internal error: DSO state not present.";
361+
}
362+
363+
if (index >= (ulong)dsoState.JniPreloadNames.Count) {
364+
return $" Invalid index {index}";
365+
}
366+
367+
return $" {dsoState.JniPreloadNames[(int)index]}";
368+
}
369+
349370
void PopulatePreloadIndices (LlvmIrVariable variable, LlvmIrModuleTarget target, object? state)
350371
{
351372
var indices = variable.Value as List<uint>;
@@ -358,6 +379,9 @@ void PopulatePreloadIndices (LlvmIrVariable variable, LlvmIrModuleTarget target,
358379
throw new InvalidOperationException ($"Internal error: DSO state not present.");
359380
}
360381

382+
var dsoNames = new List<string> ();
383+
384+
// Indices array MUST NOT be sorted, since it groups alias entries together with the main entry
361385
foreach (DSOCacheEntry preload in dsoState.JniPreloadDSOs) {
362386
int dsoIdx = dsoState.DsoCache.FindIndex (entry => {
363387
if (entry.Instance == null) {
@@ -372,8 +396,9 @@ void PopulatePreloadIndices (LlvmIrVariable variable, LlvmIrModuleTarget target,
372396
}
373397

374398
indices.Add ((uint)dsoIdx);
399+
dsoNames.Add (preload.HashedName ?? String.Empty);
375400
}
376-
indices.Sort ();
401+
dsoState.JniPreloadNames = dsoNames;
377402
}
378403

379404
void HashAndSortDSOCache (LlvmIrVariable variable, LlvmIrModuleTarget target, object? state)
@@ -427,15 +452,20 @@ DsoCacheState InitDSOCache ()
427452
var jniPreloads = new List<DSOCacheEntry> ();
428453
var aotDsoCache = new List<StructureInstance<DSOCacheEntry>> ();
429454
var nameMutations = new List<string> ();
455+
int nameMutationsCount = -1;
430456

431457
for (int i = 0; i < dsos.Count; i++) {
432458
string name = dsos[i].name;
433459

434460
bool isJniLibrary = ELFHelper.IsJniLibrary (Log, dsos[i].item.ItemSpec);
435461
bool ignore = dsos[i].ignore;
462+
bool ignore_for_preload = !ApplicationConfigNativeAssemblyGeneratorCLR.DsoCacheJniPreloadIgnore.Contains (name);
436463

437464
nameMutations.Clear();
438465
AddNameMutations (name);
466+
if (nameMutationsCount == -1) {
467+
nameMutationsCount = nameMutations.Count;
468+
}
439469

440470
// All mutations point to the actual library name, but have hash of the mutated one
441471
foreach (string entryName in nameMutations) {
@@ -447,23 +477,27 @@ DsoCacheState InitDSOCache ()
447477
name = name,
448478
};
449479

450-
if (entry.is_jni_library && entry.HashedName == name && !ApplicationConfigNativeAssemblyGeneratorCLR.DsoCacheJniPreloadIgnore.Contains (name)) {
451-
jniPreloads.Add (entry);
452-
}
453-
454480
var item = new StructureInstance<DSOCacheEntry> (dsoCacheEntryStructureInfo, entry);
455481
if (name.StartsWith ("libaot-", StringComparison.OrdinalIgnoreCase)) {
456482
aotDsoCache.Add (item);
457-
} else {
458-
dsoCache.Add (item);
483+
continue;
459484
}
485+
486+
// We must add all aliases to the preloads indices array so that all of them have their handle
487+
// set when the library is preloaded.
488+
if (entry.is_jni_library && ignore_for_preload) {
489+
jniPreloads.Add (entry);
490+
}
491+
492+
dsoCache.Add (item);
460493
}
461494
}
462495

463496
return new DsoCacheState {
464497
DsoCache = dsoCache,
465498
AotDsoCache = aotDsoCache,
466499
JniPreloadDSOs = jniPreloads,
500+
NameMutationsCount = (uint)(nameMutationsCount <= 0 ? 1 : nameMutationsCount),
467501
};
468502

469503
void AddNameMutations (string name)

src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGeneratorCLR.cs

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -243,8 +243,10 @@ sealed class DsoCacheState
243243
{
244244
public List<StructureInstance<DSOCacheEntry>> DsoCache = [];
245245
public List<DSOCacheEntry> JniPreloadDSOs = [];
246+
public List<string> JniPreloadNames = [];
246247
public List<StructureInstance<DSOCacheEntry>> AotDsoCache = [];
247248
public LlvmIrStringBlob NamesBlob = null!;
249+
public uint NameMutationsCount = 1;
248250
}
249251

250252
// Keep in sync with FORMAT_TAG in src/monodroid/jni/xamarin-app.hh
@@ -394,10 +396,14 @@ protected override void Construct (LlvmIrModule module)
394396
};
395397
module.Add (dso_cache);
396398

399+
module.AddGlobalVariable ("dso_jni_preloads_idx_stride", dsoState.NameMutationsCount);
400+
397401
// This variable MUST be written after `dso_cache` since it relies on sorting performed by HashAndSortDSOCache
398402
var dso_jni_preloads_idx = new LlvmIrGlobalVariable (new List<uint> (), "dso_jni_preloads_idx", LlvmIrVariableOptions.GlobalConstant) {
399403
Comment = " Indices into dso_cache[] of DSO libraries to preload because of JNI use",
400404
ArrayItemCount = (uint)dsoState.JniPreloadDSOs.Count,
405+
GetArrayItemCommentCallback = GetPreloadIndicesLibraryName,
406+
GetArrayItemCommentCallbackCallerState = dsoState,
401407
BeforeWriteCallback = PopulatePreloadIndices,
402408
BeforeWriteCallbackCallerState = dsoState,
403409
};
@@ -566,6 +572,21 @@ void AddAssemblyStores (LlvmIrModule module)
566572
module.Add (assembly_store);
567573
}
568574

575+
string? GetPreloadIndicesLibraryName (LlvmIrVariable v, LlvmIrModuleTarget target, ulong index, object? value, object? callerState)
576+
{
577+
// Instead of throwing for such a triviality like a comment, we will return error messages as comments instead
578+
var dsoState = callerState as DsoCacheState;
579+
if (dsoState == null) {
580+
return " Internal error: DSO state not present.";
581+
}
582+
583+
if (index >= (ulong)dsoState.JniPreloadNames.Count) {
584+
return $" Invalid index {index}";
585+
}
586+
587+
return $" {dsoState.JniPreloadNames[(int)index]}";
588+
}
589+
569590
void PopulatePreloadIndices (LlvmIrVariable variable, LlvmIrModuleTarget target, object? state)
570591
{
571592
var indices = variable.Value as List<uint>;
@@ -578,6 +599,9 @@ void PopulatePreloadIndices (LlvmIrVariable variable, LlvmIrModuleTarget target,
578599
throw new InvalidOperationException ($"Internal error: DSO state not present.");
579600
}
580601

602+
var dsoNames = new List<string> ();
603+
604+
// Indices array MUST NOT be sorted, since it groups alias entries together with the main entry
581605
foreach (DSOCacheEntry preload in dsoState.JniPreloadDSOs) {
582606
int dsoIdx = dsoState.DsoCache.FindIndex (entry => {
583607
if (entry.Instance == null) {
@@ -592,8 +616,9 @@ void PopulatePreloadIndices (LlvmIrVariable variable, LlvmIrModuleTarget target,
592616
}
593617

594618
indices.Add ((uint)dsoIdx);
619+
dsoNames.Add (preload.HashedName ?? String.Empty);
595620
}
596-
indices.Sort ();
621+
dsoState.JniPreloadNames = dsoNames;
597622
}
598623

599624
void HashAndSortDSOCache (LlvmIrVariable variable, LlvmIrModuleTarget target, object? state)
@@ -648,16 +673,22 @@ DsoCacheState InitDSOCache ()
648673
var aotDsoCache = new List<StructureInstance<DSOCacheEntry>> ();
649674
var nameMutations = new List<string> ();
650675
var dsoNamesBlob = new LlvmIrStringBlob ();
676+
int nameMutationsCount = -1;
651677

652678
for (int i = 0; i < dsos.Count; i++) {
653679
string name = dsos[i].name;
654680
(int nameOffset, _) = dsoNamesBlob.Add (name);
655681

656682
bool isJniLibrary = ELFHelper.IsJniLibrary (Log, dsos[i].item.ItemSpec);
657683
bool ignore = dsos[i].ignore;
684+
bool ignore_for_preload = !DsoCacheJniPreloadIgnore.Contains (name);
658685

659686
nameMutations.Clear();
660687
AddNameMutations (name);
688+
if (nameMutationsCount == -1) {
689+
nameMutationsCount = nameMutations.Count;
690+
}
691+
661692
// All mutations point to the actual library name, but have hash of the mutated one
662693
foreach (string entryName in nameMutations) {
663694
var entry = new DSOCacheEntry {
@@ -670,16 +701,19 @@ DsoCacheState InitDSOCache ()
670701
name_index = (uint)nameOffset,
671702
};
672703

673-
if (entry.is_jni_library && entry.HashedName == name && !DsoCacheJniPreloadIgnore.Contains (name)) {
674-
jniPreloads.Add (entry);
675-
}
676-
677704
var item = new StructureInstance<DSOCacheEntry> (dsoCacheEntryStructureInfo, entry);
678705
if (name.StartsWith ("libaot-", StringComparison.OrdinalIgnoreCase)) {
679706
aotDsoCache.Add (item);
680-
} else {
681-
dsoCache.Add (item);
707+
continue;
682708
}
709+
710+
// We must add all aliases to the preloads indices array so that all of them have their handle
711+
// set when the library is preloaded.
712+
if (entry.is_jni_library && ignore_for_preload) {
713+
jniPreloads.Add (entry);
714+
}
715+
716+
dsoCache.Add (item);
683717
}
684718
}
685719

@@ -688,6 +722,7 @@ DsoCacheState InitDSOCache ()
688722
JniPreloadDSOs = jniPreloads,
689723
AotDsoCache = aotDsoCache,
690724
NamesBlob = dsoNamesBlob,
725+
NameMutationsCount = (uint)(nameMutationsCount <= 0 ? 1 : nameMutationsCount),
691726
};
692727

693728
void AddNameMutations (string name)

src/native/clr/host/host.cc

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,60 @@ auto Host::create_delegate (
334334
return delegate;
335335
}
336336

337+
[[gnu::flatten, gnu::always_inline]]
338+
void Host::preload_jni_libraries () noexcept
339+
{
340+
// NOTE: when fixing a bug here, fix also the MonoVM code in src/native/mono/monodroid-glue.cc@preload_jni_libraries
341+
if (application_config.number_of_shared_libraries == 0) [[unlikely]] {
342+
return;
343+
}
344+
345+
log_debug (LOG_ASSEMBLY, "DSO jni preloads index stride == {}", dso_jni_preloads_idx_stride);
346+
347+
if ((dso_jni_preloads_idx_count % dso_jni_preloads_idx_stride) != 0) [[unlikely]] {
348+
Helpers::abort_application (
349+
LOG_ASSEMBLY,
350+
std::format (
351+
"DSO preload index is invalid, size ({}) is not a multiple of {}"sv,
352+
dso_jni_preloads_idx_count,
353+
dso_jni_preloads_idx_stride
354+
)
355+
);
356+
}
357+
358+
for (size_t i = 0; i < dso_jni_preloads_idx_count; i += dso_jni_preloads_idx_stride) {
359+
const size_t entry_index = dso_jni_preloads_idx[i];
360+
DSOCacheEntry &entry = dso_cache[entry_index];
361+
const std::string_view dso_name = MonodroidDl::get_dso_name (&entry);
362+
363+
log_debug (
364+
LOG_ASSEMBLY,
365+
"Preloading JNI shared library: {} (entry's index: {}; real name hash: {:x}; name hash: {:x})",
366+
dso_name,
367+
entry_index,
368+
entry.real_name_hash,
369+
entry.hash
370+
);
371+
372+
void *handle = MonodroidDl::monodroid_dlopen (&entry, dso_name, RTLD_NOW);
373+
374+
// Set handle in all the alias entries
375+
for (size_t j = 1; j < dso_jni_preloads_idx_stride; j++) {
376+
const size_t entry_alias_index = dso_jni_preloads_idx[i + j];
377+
DSOCacheEntry &entry_alias = dso_cache[entry_alias_index];
378+
const std::string_view entry_alias_name = MonodroidDl::get_dso_name (&entry);
379+
380+
log_debug (
381+
LOG_ASSEMBLY,
382+
"Putting JNI library handle in alias entry at index {}: {}",
383+
entry_alias_index,
384+
entry_alias_name
385+
);
386+
entry_alias.handle = handle;
387+
}
388+
}
389+
}
390+
337391
void Host::Java_mono_android_Runtime_initInternal (
338392
JNIEnv *env, jclass runtimeClass, jstring lang, jobjectArray runtimeApksJava,
339393

@@ -433,12 +487,7 @@ void Host::Java_mono_android_Runtime_initInternal (
433487
gettid ()
434488
);
435489

436-
for (size_t i = 0; i < dso_jni_preloads_idx_count; i++) {
437-
DSOCacheEntry &entry = dso_cache[dso_jni_preloads_idx[i]];
438-
const std::string_view dso_name = MonodroidDl::get_dso_name (&entry);
439-
log_debug (LOG_ASSEMBLY, "Preloading JNI shared library: {}", dso_name);
440-
MonodroidDl::monodroid_dlopen (&entry, dso_name, RTLD_NOW);
441-
}
490+
preload_jni_libraries ();
442491

443492
struct JnienvInitializeArgs init = {};
444493
init.javaVm = jvm;

src/native/clr/include/host/host.hh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ namespace xamarin::android {
4949
std::string_view const& assembly_name, std::string_view const& type_name,
5050
std::string_view const& method_name) noexcept -> void*;
5151

52+
static void preload_jni_libraries () noexcept;
53+
5254
private:
5355
static inline void *clr_host = nullptr;
5456
static inline unsigned int domain_id = 0;

src/native/clr/include/xamarin-app.hh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ extern "C" {
348348
[[gnu::visibility("default")]] extern AssemblyStoreRuntimeData assembly_store;
349349

350350
[[gnu::visibility("default")]] extern DSOCacheEntry dso_cache[];
351+
[[gnu::visibility("default")]] extern const uint dso_jni_preloads_idx_stride;
351352
[[gnu::visibility("default")]] extern const uint dso_jni_preloads_idx_count;
352353
[[gnu::visibility("default")]] extern const uint dso_jni_preloads_idx[];
353354
[[gnu::visibility("default")]] extern DSOCacheEntry aot_dso_cache[];

src/native/clr/xamarin-app-stub/application_dso_stub.cc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ DSOCacheEntry dso_cache[] = {
127127
},
128128
};
129129

130+
const uint dso_jni_preloads_idx_stride = 1;
130131
const uint dso_jni_preloads_idx_count = 1;
131132
const uint dso_jni_preloads_idx[1] = {
132133
0

src/native/mono/monodroid/monodroid-glue-internal.hh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ namespace xamarin::android::internal
162162
static void monodroid_unhandled_exception (MonoObject *java_exception);
163163
static MonoClass* get_android_runtime_class () noexcept;
164164

165+
static void preload_jni_libraries () noexcept;
165166
static MonoDomain* create_domain (JNIEnv *env, jstring_array_wrapper &runtimeApks, bool is_root_domain, bool have_split_apks) noexcept;
166167
static MonoDomain* create_and_initialize_domain (JNIEnv* env, jclass runtimeClass, jstring_array_wrapper &runtimeApks,
167168
jstring_array_wrapper &assemblies, jobjectArray assembliesBytes, jstring_array_wrapper &assembliesPaths,

0 commit comments

Comments
 (0)