dotnet/android

Dynamically loading an assembly results in BadImageFormatException

MasterMann opened this issue · 2 comments

Android framework version

net8.0-android

Affected platform version

VS 2022 (17.10.5), .NET 8.0.303, Android workload 34.0.113/8.0.100

Description

I have a plugin-style assembly located in /data/data/.../<apk name>/files/<assembly name>.dll, that is copied to an asset folder before build and marked as AndroidAsset (later copied to that directory on target device).
Loading it dynamically at runtime through Assembly.LoadFrom results in a BadImageFormatException.

My suspicion is that the related native code that produces the error is around here: https://github.com/dotnet/runtime/blob/c788546f9ad43ea17981d5dc9343b00b6f76d98f/src/coreclr/vm/assemblynative.cpp#L200-L208

Steps to Reproduce

  1. Create a new MAUI Android project (either via Avalonia Template Studio, or explicitly MAUI)
  2. Set the target framework for the project to net8.0-android
  3. Create a project directory, mark it and all contents within as AndroidAsset (<AndroidAsset Include="Assets\plugins\**\*" />)
  4. Create a new .NET 8 library, build it and copy into the target plugin directory in the Android project
  5. Add code to copy the plugin library from APK assets to /data/data/.../files/ folder (via AssetManager and StreamReader/Writer)
  6. Compile and run the Android project on the target device, observe the exception at Assembly.LoadFrom call

Did you find any workaround?

None at the moment. Tried exporting the library for linux-arm64 architecture, same problem.

Relevant log output

[ERROR] FATAL UNHANDLED EXCEPTION: System.BadImageFormatException: Invalid Image: /data/user/0/net.georgeb.my.life/files/plugins/MyLife.App.Plugins.Content.Todo/MyLife.App.Plugins.Content.Todo.dll
File name: '/data/user/0/net.georgeb.my.life/files/plugins/MyLife.App.Plugins.Content.Todo/MyLife.App.Plugins.Content.Todo.dll'
at System.Runtime.Loader.AssemblyLoadContext.InternalLoadFromPath(String assemblyPath, String nativeImagePath)
at System.Runtime.Loader.AssemblyLoadContext.LoadFromAssemblyPath(String assemblyPath)
at System.Reflection.Assembly.LoadFrom(String assemblyFile)
at MyLife.App.Plugins.Core.Utilities.Plugins.GenericPluginLoader`1[[MyLife.App.Plugins.Core.Services.Features.IFeaturePlugin, MyLife.App.Plugins.Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]].Load(String pluginPath, Object[] constructorArgs) in E:\Dev\MasterManDev\Personal\MyLife\Plugins\MyLife.App.Plugins.Core\Utilities\Plugins\GenericPluginLoader.cs:line 39
at MyLife.App.Shared.Services.Features.BaseFeaturePluginManager.Initialize(IEnumerable`1 pluginFilePaths) in E:\Dev\MasterManDev\Personal\MyLife\MyLife.App.Shared\Services\Features\BaseFeaturePluginManager.cs:line 53
at MyLife.App.Shared.Services.Features.BaseFeaturePluginManager..ctor() in E:\Dev\MasterManDev\Personal\MyLife\MyLife.App.Shared\Services\Features\BaseFeaturePluginManager.cs:line 44
at MyLife.App.Android.MyLifeAndroidApp.InitPlatform() in E:\Dev\MasterManDev\Personal\MyLife\Android\MyLife.App.Android\MyLifeAndroidApp.cs:line 59
at MyLife.App.Shared.MyLifeApp.OnFrameworkInitializationCompleted() in E:\Dev\MasterManDev\Personal\MyLife\MyLife.App.Shared.UI\MyLifeApp.axaml.cs:line 25
at Avalonia.AppBuilder.SetupUnsafe()
at Avalonia.AppBuilder.Setup()
at Avalonia.AppBuilder.SetupWithLifetime(IApplicationLifetime lifetime)
at Avalonia.Android.AvaloniaMainActivity.InitializeAvaloniaView(Object initialContent)
at Avalonia.Android.AvaloniaActivity.OnCreate(Bundle savedInstanceState)
at MyLife.App.Android.Activities.MobileMainActivity.OnCreate(Bundle bundle) in E:\Dev\MasterManDev\Personal\MyLife\Android\MyLife.App.Android\Activities\MainActivity.cs:line 34
at Android.App.Activity.n_OnCreate_Landroid_os_Bundle_(IntPtr jnienv, IntPtr native__this, IntPtr native_savedInstanceState) in /Users/runner/work/1/s/xamarin-android/src/Mono.Android/obj/Release/net8.0/android-34/mcw/Android.App.Activity.cs:line 3082
at Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PPL_V(_JniMarshal_PPL_V callback, IntPtr jnienv, IntPtr klazz, IntPtr p0) in /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Android.Runtime/JNINativeWrapper.g.cs:line 121

Update: I tried manually uploading the DLL into the target folder, instead of using the existing code that copies it from the package (APK), and tested with PEReader, which worked successfully.
Loading via Assembly.LoadFrom also worked.

Testing the original file with PEReader yieleded BadImageFormatException: invalid PE signature, so something probably went wrong during copying into the storage.

Original vs manual copy file sizes:
2024-08-14_14-14-19
2024-08-14_14-14-40

Actual file size, as intended:
зображення

Copying code in question, maybe someone can help find what causes the issue:

foreach (var pluginDirName in AssetManager.List("plugins") ?? [])
{
	var pluginDirPath = Path.Combine("plugins", pluginDirName);
	var pluginDirStoragePath = Path.Combine(pluginsStorageRootPath, pluginDirName);
	if (!Directory.Exists(pluginDirStoragePath))
	{
		Directory.CreateDirectory(pluginDirStoragePath);

		foreach (var pluginFile in AssetManager.List(pluginDirPath) ?? [])
		{
			var pluginFilePath = Path.Combine(pluginDirPath, pluginFile);
			var pluginFileStoragePath = Path.Combine(pluginDirStoragePath, pluginFile);

			if (!File.Exists(pluginFileStoragePath))
			{
				using var sr = new StreamReader(AssetManager.Open(pluginFilePath));
				using var sw = new StreamWriter(pluginFileStoragePath);
				sw.Write(sr.ReadToEnd());
			}
		}
	}
}

Seems like using StreamReader for binary files is a bad idea.
The issue has been resolved, going to close it.
This code works though, leaving it here for reference:

if (!File.Exists(pluginFileStoragePath))
{
    var file = AssetManager.Open(pluginFilePath);

    using var storageStream = File.OpenWrite(pluginFileStoragePath);
    file.CopyTo(storageStream);
    storageStream.Flush();
    storageStream.Close();
    file.Close();
}