[GH-ISSUE #3970] [Bug] inspect-pe-resources rejects a Windows-readable zero-length resource at the section end #1572

Closed
opened 2026-10-16 21:07:17 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @Zhong-fan on GitHub (Oct 15, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/3970

Before submitting

  • I searched open and closed issues and PRs for related reports.
  • I removed credentials, private paths, and sensitive target data.

Area

Native analysis or decompilation

Observed behavior

inspect-pe-resources rejects a resource-only PE32+ DLL generated by GNU windres/ld when its only resource has a zero-byte payload at the end of .rsrc.

The CLI exits 1 with invalid_request / invalid_format:

Resource RVA has missing or overlapping section mappings.

The MCP tool inspect_pe_resources returns the same error with isError: true. An otherwise equivalent DLL containing one payload byte succeeds through both interfaces.

On the same Windows host, both DLLs can be opened with LoadLibraryExW(..., LOAD_LIBRARY_AS_DATAFILE | LOAD_LIBRARY_AS_IMAGE_RESOURCE). FindResourceExW finds type 42 / name 1 / language 1033, and LoadResource returns a non-null handle for both. SizeofResource reports 0 and 1 respectively, with the last-error value cleared before each call and remaining 0.

Reproduction

In an empty directory, create empty.bin with zero bytes and one.bin containing byte 0x2A. Create these resource scripts:

// empty.rc
1 42 "empty.bin"
// one.rc
1 42 "one.bin"

With MSYS2 UCRT64's bin directory on the process PATH, run:

windres.exe empty.rc -O coff -o empty.o
ld.exe --shared --entry 0 -o empty.dll empty.o
windres.exe one.rc -O coff -o one.o
ld.exe --shared --entry 0 -o one.dll one.o
node <REA>/scripts/rea.mjs inspect-pe-resources <absolute-path-to-empty.dll> --format json
node <REA>/scripts/rea.mjs inspect-pe-resources <absolute-path-to-one.dll> --format json

The angle-bracket paths stand for the tested source checkout or installed package and the generated DLLs. No PE headers or resource entries were patched after linking. Rebuilt and repeated locally: empty fails; one-byte control passes. Input SHA-256 values were unchanged after CLI, MCP, and native Windows inspection.

Expected behavior

Retain this Windows-readable resource identity with an empty payload, without rejecting the entire inventory. Nonempty payloads outside the allowed section extent should still be rejected.

Impact and scope

Observed on locally generated resource-only DLLs; no claim about prevalence in third-party applications. The whole inspection fails rather than reporting the resource. An executable linked through GCC can have extra resource-section padding and did not trigger this failure, so this is not a claim that every zero-length resource fails.

REA version

  • Official npm package rea-agents@6.5.0.
  • Compiled source checkout 1b036ecbffefca61d074ffd4ec65cc41c1b1636e.
  • Both CLI and stdio MCP reproduced on each build.

Runtime and platform

Windows x64, build 26200; Node.js 24.18.0. GNU windres/ld 2.47.20260726 (MSYS2 UCRT64); GCC 16.2.0 for the Windows API helper. This operation uses the built-in PE resource parser.

Target and artifact context

Linker-generated resource-only PE32+ DLL; resource type 42, name 1, language 1033.

Interface

CLI and MCP

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Check empty.dll one.dll
Windows LoadLibraryExW / FindResourceExW / LoadResource success success
Windows SizeofResource 0 bytes 1 byte
REA CLI, release and source invalid_format; exit 1 complete; one resource
REA MCP, release and source isError: true complete; one resource

The empty DLL has .rsrc.VirtualAddress = 0x4000, VirtualSize = 0x58, and a data entry with OffsetToData = 0x4058, Size = 0. The one-byte control has the same payload RVA and a larger virtual extent.

Native Windows helper used for the independent check (compile with gcc.exe -municode resource-oracle.c -o resource-oracle.exe, then pass the absolute DLL path):

#include <windows.h>
#include <stdio.h>
int wmain(int argc, wchar_t **argv) {
    if (argc != 2) return 2;
    HMODULE m = LoadLibraryExW(argv[1], NULL,
        LOAD_LIBRARY_AS_DATAFILE | LOAD_LIBRARY_AS_IMAGE_RESOURCE);
    if (!m) return 3;
    SetLastError(0);
    HRSRC r = FindResourceExW(m, MAKEINTRESOURCEW(42),
        MAKEINTRESOURCEW(1), 1033);
    DWORD find_error = GetLastError();
    SetLastError(0);
    DWORD size = r ? SizeofResource(m, r) : 0;
    DWORD size_error = GetLastError();
    SetLastError(0);
    HGLOBAL data = r ? LoadResource(m, r) : NULL;
    DWORD load_error = GetLastError();
    printf("found=%d find_error=%lu size=%lu size_error=%lu loaded=%d load_error=%lu\n",
        r != NULL, find_error, size, size_error, data != NULL, load_error);
    FreeLibrary(m);
    return r && data ? 0 : 4;
}

The helper above is a shortened presentation of the executed helper; the executed helper prints the same fields as JSON. The resource-loading flags follow LoadLibraryExW documentation.

Analysis or suspected cause

Source-derived: PeResourceLayout.ts selects a section using rva - section.rva < virtualExtent(section) without accounting for a zero-length interval. At this exact end boundary, no section is selected even though no payload bytes need to be read. PeResourceParser.ts calls this mapping for the zero-size leaf too.

Existing PeResourceParser.test.ts tests all pass (24). They cover an empty resource tree and reject four-byte payloads at/past the virtual boundary, but do not cover a zero-size leaf at that boundary.

Additional context

Related implementation history: #1678 correctly restricts nonempty resource reads to the section virtual extent. This report concerns a Windows-readable empty leaf at the end boundary; it does not ask to restore reads from raw alignment padding. #2812 requests decoding additional resource types, a separate capability.

GNU objdump from the same toolchain also prints Corrupt .rsrc section detected! for the empty DLL. The compatibility expectation here is grounded in the independently executed Windows resource API results, not in unanimous acceptance by other parsers. No earlier REA version was tested as a last-known-good version.

Originally created by @Zhong-fan on GitHub (Oct 15, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3970 ### Before submitting - [x] I searched open and closed issues and PRs for related reports. - [x] I removed credentials, private paths, and sensitive target data. ### Area Native analysis or decompilation ### Observed behavior `inspect-pe-resources` rejects a resource-only PE32+ DLL generated by GNU windres/ld when its only resource has a zero-byte payload at the end of `.rsrc`. The CLI exits 1 with `invalid_request` / `invalid_format`: ```text Resource RVA has missing or overlapping section mappings. ``` The MCP tool `inspect_pe_resources` returns the same error with `isError: true`. An otherwise equivalent DLL containing one payload byte succeeds through both interfaces. On the same Windows host, both DLLs can be opened with `LoadLibraryExW(..., LOAD_LIBRARY_AS_DATAFILE | LOAD_LIBRARY_AS_IMAGE_RESOURCE)`. `FindResourceExW` finds type 42 / name 1 / language 1033, and `LoadResource` returns a non-null handle for both. `SizeofResource` reports 0 and 1 respectively, with the last-error value cleared before each call and remaining 0. ### Reproduction In an empty directory, create `empty.bin` with zero bytes and `one.bin` containing byte 0x2A. Create these resource scripts: ```rc // empty.rc 1 42 "empty.bin" ``` ```rc // one.rc 1 42 "one.bin" ``` With MSYS2 UCRT64's bin directory on the process PATH, run: ```text windres.exe empty.rc -O coff -o empty.o ld.exe --shared --entry 0 -o empty.dll empty.o windres.exe one.rc -O coff -o one.o ld.exe --shared --entry 0 -o one.dll one.o node <REA>/scripts/rea.mjs inspect-pe-resources <absolute-path-to-empty.dll> --format json node <REA>/scripts/rea.mjs inspect-pe-resources <absolute-path-to-one.dll> --format json ``` The angle-bracket paths stand for the tested source checkout or installed package and the generated DLLs. No PE headers or resource entries were patched after linking. Rebuilt and repeated locally: empty fails; one-byte control passes. Input SHA-256 values were unchanged after CLI, MCP, and native Windows inspection. ### Expected behavior Retain this Windows-readable resource identity with an empty payload, without rejecting the entire inventory. Nonempty payloads outside the allowed section extent should still be rejected. ### Impact and scope Observed on locally generated resource-only DLLs; no claim about prevalence in third-party applications. The whole inspection fails rather than reporting the resource. An executable linked through GCC can have extra resource-section padding and did not trigger this failure, so this is not a claim that every zero-length resource fails. ### REA version - Official npm package `rea-agents@6.5.0`. - Compiled source checkout `1b036ecbffefca61d074ffd4ec65cc41c1b1636e`. - Both CLI and stdio MCP reproduced on each build. ### Runtime and platform Windows x64, build 26200; Node.js 24.18.0. GNU windres/ld 2.47.20260726 (MSYS2 UCRT64); GCC 16.2.0 for the Windows API helper. This operation uses the built-in PE resource parser. ### Target and artifact context Linker-generated resource-only PE32+ DLL; resource type 42, name 1, language 1033. ### Interface CLI and MCP ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence | Check | empty.dll | one.dll | | --- | --- | --- | | Windows LoadLibraryExW / FindResourceExW / LoadResource | success | success | | Windows SizeofResource | 0 bytes | 1 byte | | REA CLI, release and source | invalid_format; exit 1 | complete; one resource | | REA MCP, release and source | isError: true | complete; one resource | The empty DLL has `.rsrc.VirtualAddress = 0x4000`, `VirtualSize = 0x58`, and a data entry with `OffsetToData = 0x4058`, `Size = 0`. The one-byte control has the same payload RVA and a larger virtual extent. Native Windows helper used for the independent check (compile with `gcc.exe -municode resource-oracle.c -o resource-oracle.exe`, then pass the absolute DLL path): ```c #include <windows.h> #include <stdio.h> int wmain(int argc, wchar_t **argv) { if (argc != 2) return 2; HMODULE m = LoadLibraryExW(argv[1], NULL, LOAD_LIBRARY_AS_DATAFILE | LOAD_LIBRARY_AS_IMAGE_RESOURCE); if (!m) return 3; SetLastError(0); HRSRC r = FindResourceExW(m, MAKEINTRESOURCEW(42), MAKEINTRESOURCEW(1), 1033); DWORD find_error = GetLastError(); SetLastError(0); DWORD size = r ? SizeofResource(m, r) : 0; DWORD size_error = GetLastError(); SetLastError(0); HGLOBAL data = r ? LoadResource(m, r) : NULL; DWORD load_error = GetLastError(); printf("found=%d find_error=%lu size=%lu size_error=%lu loaded=%d load_error=%lu\n", r != NULL, find_error, size, size_error, data != NULL, load_error); FreeLibrary(m); return r && data ? 0 : 4; } ``` The helper above is a shortened presentation of the executed helper; the executed helper prints the same fields as JSON. The resource-loading flags follow [LoadLibraryExW documentation](https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibraryexw). ### Analysis or suspected cause Source-derived: [PeResourceLayout.ts](https://github.com/morluto/rea/blob/1b036ecbffefca61d074ffd4ec65cc41c1b1636e/src/native/pe/PeResourceLayout.ts#L94-L120) selects a section using `rva - section.rva < virtualExtent(section)` without accounting for a zero-length interval. At this exact end boundary, no section is selected even though no payload bytes need to be read. [PeResourceParser.ts](https://github.com/morluto/rea/blob/1b036ecbffefca61d074ffd4ec65cc41c1b1636e/src/native/pe/PeResourceParser.ts#L177) calls this mapping for the zero-size leaf too. Existing `PeResourceParser.test.ts` tests all pass (24). They cover an empty resource tree and reject four-byte payloads at/past the virtual boundary, but do not cover a zero-size leaf at that boundary. ### Additional context Related implementation history: #1678 correctly restricts nonempty resource reads to the section virtual extent. This report concerns a Windows-readable empty leaf at the end boundary; it does not ask to restore reads from raw alignment padding. #2812 requests decoding additional resource types, a separate capability. GNU objdump from the same toolchain also prints `Corrupt .rsrc section detected!` for the empty DLL. The compatibility expectation here is grounded in the independently executed Windows resource API results, not in unanimous acceptance by other parsers. No earlier REA version was tested as a last-known-good version.
Author
Owner

@morluto commented on GitHub (Oct 15, 2026):

Fixed on main in 045c84db7. The RVA mapper first chooses a section containing the address; only an otherwise unmapped zero-length range may use a positive virtual endpoint. Unique ownership and file backing remain required. This also addresses the adjacent-section review concern without admitting alignment padding.

Validation: source and compiled public regressions failed before the fix. Parser/service/filesystem/public workflow checks passed (37 cases); the PE32/PE32+ empty-resource cases run through both compiled CLI and real stdio MCP. Controls cover adjacent sections, one byte past the endpoint and missing raw backing. Main CI passed all 36 jobs: https://github.com/morluto/rea/actions/runs/38788727652. Independent Windows resource API/compiler comparison was not run locally. Closing as addressed.

<!-- gh-comment-id:6200007252 --> @morluto commented on GitHub (Oct 15, 2026): Fixed on main in 045c84db7. The RVA mapper first chooses a section containing the address; only an otherwise unmapped zero-length range may use a positive virtual endpoint. Unique ownership and file backing remain required. This also addresses the adjacent-section review concern without admitting alignment padding. Validation: source and compiled public regressions failed before the fix. Parser/service/filesystem/public workflow checks passed (37 cases); the PE32/PE32+ empty-resource cases run through both compiled CLI and real stdio MCP. Controls cover adjacent sections, one byte past the endpoint and missing raw backing. Main CI passed all 36 jobs: https://github.com/morluto/rea/actions/runs/38788727652. Independent Windows resource API/compiler comparison was not run locally. Closing as addressed.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#1572
No description provided.