Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PersistedAssemblyBuilder: unable to retrieve resources added with AddManifestResource #110686

Open
mattjacobsen opened this issue Dec 13, 2024 · 4 comments
Assignees
Labels
area-System.Reflection.Emit bug documentation Documentation bug or enhancement, does not impact product or test code
Milestone

Comments

@mattjacobsen
Copy link

mattjacobsen commented Dec 13, 2024

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 :-)

Copy link
Contributor

Tagging subscribers to this area: @dotnet/area-system-resources
See info in area-owners.md if you want to be subscribed.

@dotnet-policy-service dotnet-policy-service bot added the untriaged New issue has not been triaged by the area owner label Dec 13, 2024
Copy link
Contributor

Tagging subscribers to this area: @dotnet/area-system-reflection-emit
See info in area-owners.md if you want to be subscribed.

@steveharter steveharter self-assigned this Dec 20, 2024
@steveharter steveharter removed the untriaged New issue has not been triaged by the area owner label Dec 20, 2024
@steveharter steveharter added this to the 10.0.0 milestone Dec 20, 2024
@steveharter steveharter added question Answer questions and provide assistance, not an issue with source code or documentation. bug and removed question Answer questions and provide assistance, not an issue with source code or documentation. labels Dec 22, 2024
@steveharter
Copy link
Member

steveharter commented Dec 22, 2024

I have this working locally after fixing some issues with the sample:

  • Offset to AddManifestResource should be 0, not the size of the blob.
  • The size of the serialized ResourceWriter needs to be added to the resource blob before the actual data.
    and fixing an issue with an alignment Assert which appears to be perf related and is not necessary for functionality.

I'll create a PR to fix the alignment and add a test, plus in the docs repo update the sample to something like this:

static void SetResource()
{
    PersistedAssemblyBuilder ab = new PersistedAssemblyBuilder(new AssemblyName("MyAssemblyWithResource"), typeof(object).Assembly);
    ab.DefineDynamicModule("MyModule");
    MetadataBuilder metadata = ab.GenerateMetadata(out BlobBuilder ilStream, out _);

    using MemoryStream memoryStream = new MemoryStream();
    ResourceWriter myResourceWriter = new ResourceWriter(memoryStream);
    myResourceWriter.AddResource("AddResource 1", "First added resource");
    myResourceWriter.AddResource("AddResource 2", "Second added resource");
    myResourceWriter.AddResource("AddResource 3", "Third added resource");
    myResourceWriter.Close();
    
    byte[] data = memoryStream.ToArray();
    BlobBuilder resourceBlob = new BlobBuilder();
    resourceBlob.WriteInt32(data.Length);
    resourceBlob.WriteBytes(data);

    metadata.AddManifestResource(
        ManifestResourceAttributes.Public,
        metadata.GetOrAddString("MyResource.resources"),
        implementation: default,
        offset: 0);

    ManagedPEBuilder peBuilder = new ManagedPEBuilder(
                    header: PEHeaderBuilder.CreateLibraryHeader(),
                    metadataRootBuilder: new MetadataRootBuilder(metadata),
                    ilStream: ilStream,
                    managedResources: resourceBlob);

    BlobBuilder blob = new BlobBuilder();
    peBuilder.Serialize(blob);
    using FileStream fileStream = new ("MyAssemblyWithResource.dll", FileMode.Create, FileAccess.Write);
    blob.WriteContentTo(fileStream);
    fileStream.Close();

    var readAssembly = Assembly.LoadFile(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "MyAssemblyWithResource.dll"));

    // Use ResourceReader to read the resources
    using Stream readStream = readAssembly.GetManifestResourceStream("MyResource.resources")!;
    using ResourceReader reader = new(readStream);
    foreach (DictionaryEntry entry in reader)
    {
        Console.WriteLine($"Key: {entry.Key}, Value: {entry.Value}");
    }

    // Use ResourceManager to read the resources
    ResourceManager rm = new ResourceManager("MyResource", readAssembly);
    ResourceSet resourceSet = rm.GetResourceSet(CultureInfo.InvariantCulture, createIfNotExists: true, tryParents: false);
    foreach (DictionaryEntry entry in resourceSet)
    {
        Console.WriteLine($"Key: {entry.Key}, Value: {entry.Value}");
    }
}

@mattjacobsen
Copy link
Author

thanks very much! Santa came early :-)

Kind regards,

Matt

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-System.Reflection.Emit bug documentation Documentation bug or enhancement, does not impact product or test code
Projects
None yet
Development

No branches or pull requests

3 participants