Description
Description
Hello,
I'm struggling to read out a resource that was added to an Assembly with PersistedAssemblyBuilder. I'm not sure if I'm doing it wrong, the documentation is wrong, or there's actually a bug.
Reproduction Steps
Given the following code (which is just the example code with 4 lines tacked on the end):
SetResource();
static void SetResource()
{
PersistedAssemblyBuilder ab = new PersistedAssemblyBuilder(new AssemblyName("MyAssembly"), typeof(object).Assembly);
ab.DefineDynamicModule("MyModule");
MetadataBuilder metadata = ab.GenerateMetadata(out BlobBuilder ilStream, out _);
using MemoryStream stream = new MemoryStream();
ResourceWriter myResourceWriter = new ResourceWriter(stream);
myResourceWriter.AddResource("AddResource 1", "First added resource");
myResourceWriter.AddResource("AddResource 2", "Second added resource");
myResourceWriter.AddResource("AddResource 3", "Third added resource");
myResourceWriter.Close();
BlobBuilder resourceBlob = new BlobBuilder();
resourceBlob.WriteBytes(stream.ToArray());
metadata.AddManifestResource(ManifestResourceAttributes.Public, metadata.GetOrAddString("MyResource"), default, (uint)resourceBlob.Count);
ManagedPEBuilder peBuilder = new ManagedPEBuilder(
header: new PEHeaderBuilder(imageCharacteristics: Characteristics.Dll | Characteristics.ExecutableImage),
metadataRootBuilder: new MetadataRootBuilder(metadata),
ilStream: ilStream,
managedResources: resourceBlob);
BlobBuilder blob = new BlobBuilder();
peBuilder.Serialize(blob);
using var fileStream = new FileStream("MyAssemblyWithResource.dll", FileMode.Create, FileAccess.Write);
blob.WriteContentTo(fileStream);
fileStream.Close();
var readAssembly = Assembly.LoadFile(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "MyAssemblyWithResource.dll"));
string[] names = readAssembly.GetManifestResourceNames();
using var readStream = readAssembly.GetManifestResourceStream(names[0]);
ResourceReader reader = new ResourceReader(readStream);
}
Expected behavior
I expect to be able to read the resource back out using ResourceReader.
Actual behavior
BadImageFormatException
Regression?
I tried the HEAD and the 9.0 tag, but nothing other than that.
Known Workarounds
No response
Configuration
.NET 9.0, Windows 10 x64
Other information
The example here shows how you can add a byte array as a resource. Having added that array, I'd now like to read it back out later on.
If, after writing the assembly to disk, I try and load the resource with
var readAssembly = Assembly.LoadFile(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "MyAssemblyWithResource.dll"));
string[] names = readAssembly.GetManifestResourceNames();
using var readStream = readAssembly.GetManifestResourceStream(names[0]);
ResourceReader reader = new ResourceReader(readStream);
I receive the error
An attempt was made to load a program with an incorrect format. (0x8007000B)
at
using var readStream = readAssembly.GetManifestResourceStream(names[0]);
As far as I can tell (using dnSpy, looking at the assembly in a hex editor) the assembly contains the resource. If I modify the serialisation code to spit out a huge byte array, the size of the assembly grows accordingly, and I'm able to see the bytes in my hex editor.
When I debug the runtime I see the error is thrown here peassembly.cpp:463
void PEAssembly::GetEmbeddedResource(DWORD dwOffset, DWORD *cbResource, PBYTE *pbInMemoryResource)
{
CONTRACTL
{
INSTANCE_CHECK;
THROWS;
GC_TRIGGERS;
MODE_ANY;
INJECT_FAULT(ThrowOutOfMemory(););
}
CONTRACTL_END;
PEImage* image = GetPEImage();
PEImageLayout* theImage = image->GetOrCreateLayout(PEImageLayout::LAYOUT_ANY);
if (!theImage->CheckResource(dwOffset))
ThrowHR(COR_E_BADIMAGEFORMAT);
COUNT_T size;
const void *resource = theImage->GetResource(dwOffset, &size);
*cbResource = size;
*pbInMemoryResource = (PBYTE) resource;
}
specifically, CHECK(CheckOverflow(VAL32(pDir->VirtualAddress), offset));
in pedecoder.cpp:1243
CHECK PEDecoder::CheckResource(COUNT_T offset) const
{
CONTRACT_CHECK
{
INSTANCE_CHECK;
NOTHROW;
GC_NOTRIGGER;
PRECONDITION(CheckCorHeader());
}
CONTRACT_CHECK_END;
IMAGE_DATA_DIRECTORY *pDir = &GetCorHeader()->Resources;
CHECK(CheckOverflow(VAL32(pDir->VirtualAddress), offset));
RVA rva = VAL32(pDir->VirtualAddress) + offset;
// Make sure we have at least enough data for a length
CHECK(CheckRva(rva, sizeof(DWORD)));
// Make sure resource is within resource section
CHECK(CheckBounds(VAL32(pDir->VirtualAddress), VAL32(pDir->Size),
rva + sizeof(DWORD), GET_UNALIGNED_VAL32((LPVOID)GetRvaData(rva))));
CHECK_OK;
}
CheckOverflow is passed the VirtualAddress and the offset given with
metadata.GetOrAddString("MyResource"), default, (uint)resourceBlob.Count);
in the example code, which is over the end of the data. Looking at that, it doesn't seem that I didn't need an offset, but just passing in 0 didn't help much. I've started reading around about the PE Header - it's really interesting and great to come down from the Typescript clouds - but I think I'm out of my depth for the allotted time I have to learn to swim :-)