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
- Create a new MAUI Android project (either via Avalonia Template Studio, or explicitly MAUI)
- Set the target framework for the project to
net8.0-android
- Create a project directory, mark it and all contents within as
AndroidAsset
(<AndroidAsset Include="Assets\plugins\**\*" />
) - Create a new .NET 8 library, build it and copy into the target plugin directory in the Android project
- Add code to copy the plugin library from APK assets to /data/data/.../files/ folder (via
AssetManager
andStreamReader/Writer
) - 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:
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();
}