[GH-ISSUE #5750] [Bug] inspect-pe-resources rejects word-aligned resource data entries #3496

Closed
opened 2026-10-31 00:41:09 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Oct 21, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/5750

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Native analysis or decompilation

Observed behavior

inspect-pe-resources rejects the entire inventory when resource data-entry records start at word-aligned offsets 86 and 102 after a UTF-16 resource name. The CLI exits 1 with invalid_request / invalid_input, reason invalid_format, and Unaligned resource data entry. Adding two padding bytes before the records makes REA succeed with both resources and unchanged payload RVAs, bytes and hashes. The independent pefile reader recovers both resources from both variants.

These observations were independently reproduced by a verifier at the baseline below; the submitter inspected its retained CLI and reader output. This concerns static format inspection. Windows loading was not tested.

Reproduction

From a built source checkout with locked dependencies installed, run the following verifier-executed reproduction. It generates two PE32 resource-only DLLs and a truncated-entry control. Directory tables precede the UTF-16 name, the contiguous data-entry array follows the name, and payloads follow the array.

work_dir="$(mktemp -d)"
mkdir -p "$work_dir/home" "$work_dir/cfg" "$work_dir/data" "$work_dir/cache" "$work_dir/state" "$work_dir/tmp"
cat > "$work_dir/build_fixture.py" <<'PY_FIXTURE'
import struct, sys
from pathlib import Path
outdir = Path(sys.argv[1])
for label, data_at in [('unaligned', 86), ('aligned', 88)]:
    resource = bytearray(132)
    for offset, count in [(0, 1), (24, 1), (48, 2)]:
        struct.pack_into('<HH', resource, offset + 12, 0, count)
    # RCDATA, UTF-16 name AB, language IDs 0 and 1033.
    struct.pack_into('<II', resource, 16, 10, 0x80000018)
    struct.pack_into('<HH', resource, 36, 1, 0)
    struct.pack_into('<II', resource, 40, 0x80000050, 0x80000030)
    struct.pack_into('<II', resource, 64, 0, data_at)
    struct.pack_into('<II', resource, 72, 1033, data_at + 16)
    struct.pack_into('<H', resource, 80, 2)
    resource[82:86] = 'AB'.encode('utf-16le')
    for at, rva, size in [(data_at, 0x2078, 3), (data_at + 16, 0x207c, 5)]:
        struct.pack_into('<IIII', resource, at, rva, size, 0, 0)
    resource[120:123] = b'abc'
    resource[124:129] = b'hello'
    image = bytearray(0x600)
    image[:2] = b'MZ'
    struct.pack_into('<I', image, 60, 0x80)
    image[0x80:0x84] = b'PE\0\0'
    struct.pack_into('<HHIIIHH', image, 0x84, 0x14c, 1, 0, 0, 0, 224, 0x2102)
    opt = 0x98
    struct.pack_into('<H', image, opt, 0x10b)
    struct.pack_into('<I', image, opt + 8, 0x200)
    struct.pack_into('<I', image, opt + 28, 0x400000)
    struct.pack_into('<II', image, opt + 32, 0x1000, 0x200)
    struct.pack_into('<HH', image, opt + 40, 6, 0)
    struct.pack_into('<HH', image, opt + 48, 6, 0)
    struct.pack_into('<II', image, opt + 56, 0x3000, 0x400)
    struct.pack_into('<H', image, opt + 68, 3)
    struct.pack_into('<IIII', image, opt + 72, 0x100000, 0x1000, 0x100000, 0x1000)
    struct.pack_into('<I', image, opt + 92, 16)
    struct.pack_into('<II', image, opt + 112, 0x2000, len(resource))
    section = opt + 224
    image[section:section + 8] = b'.rsrc\0\0\0'
    struct.pack_into('<IIII', image, section + 8, len(resource), 0x2000, 0x200, 0x400)
    struct.pack_into('<I', image, section + 36, 0x40000040)
    image[0x400:0x484] = resource
    (outdir / (label + '.dll')).write_bytes(image)
    if label == 'aligned':
        struct.pack_into('<I', image, 0x400 + 76, len(resource) - 8)
        (outdir / 'truncated.dll').write_bytes(image)
    print(label, 'data entries', data_at, data_at + 16)
PY_FIXTURE
python3 -I "$work_dir/build_fixture.py" "$work_dir"
for variant in unaligned aligned truncated; do
  env -i PATH="$PATH" HOME="$work_dir/home" XDG_CONFIG_HOME="$work_dir/cfg" XDG_DATA_HOME="$work_dir/data" XDG_CACHE_HOME="$work_dir/cache" XDG_STATE_HOME="$work_dir/state" TMPDIR="$work_dir/tmp" REA_LOG_LEVEL=silent node scripts/rea.mjs inspect-pe-resources "$work_dir/$variant.dll" --json > "$work_dir/$variant.json"
  printf '%s exit %s\n' "$variant" "$?"
done
python3 -m pip install --disable-pip-version-check --target "$work_dir/py" pefile==2024.8.26
cat > "$work_dir/read_pefile.py" <<'PY_READER'
import hashlib, json, pathlib, sys
sys.path.insert(0, sys.argv[1])
import pefile
print('pefile', pefile.__version__)
for file in sys.argv[2:]:
    pe = pefile.PE(file)
    rows = []
    for typ in pe.DIRECTORY_ENTRY_RESOURCE.entries:
        for name in typ.directory.entries:
            for lang in name.directory.entries:
                d = lang.data.struct
                data = pe.get_data(d.OffsetToData, d.Size)
                rows.append(dict(type=typ.id, name=str(name.name), language=lang.id,
                    data_entry_offset=d.get_file_offset(), rva=d.OffsetToData,
                    size=d.Size, payload=data.hex(), sha256=hashlib.sha256(data).hexdigest()))
    print(pathlib.Path(file).name, json.dumps(rows))
    print('warnings', json.dumps(pe.get_warnings()))
PY_READER
python3 -I "$work_dir/read_pefile.py" "$work_dir/py" "$work_dir/unaligned.dll" "$work_dir/aligned.dll"

Observed: unaligned exit 1, aligned exit 0, truncated exit 1. The complete shell block was replayed successfully by the independent verifier. No selected target was executed.

Expected behavior

Return complete type/name/language inspection for both word-aligned and padded data-entry arrays, preserving original record offsets and opaque payload hashes. The portable PE resource contract promises those facts and explicitly lists malformed/truncated structures and ambiguous mappings as refusals; it does not document a four-byte data-entry exclusion.

Source-derived format reasoning: Microsoft's Resource Directory String and Resource Data Entry specification requires word alignment for strings and defines directory-relative offsets and four 32-bit fields for data entries, without specifying four-byte alignment of each record's start. Here the UTF-16 name ends at offset 86. All records and payloads fit within one file-backed section without overlapping ranges; both payloads are four-byte-aligned. Independent decoding supports the static inspection expectation, without establishing Windows loadability.

Impact and scope

P2, supported: a bounded resource-inspection feature refuses the entire inventory for this demonstrated layout. The executed pefile reader provides a workaround preserving identities, descriptor locations, payload bytes and SHA-256 hashes from the unchanged input; it requires Python/pefile and does not produce REA's Evidence envelope. No REA alignment override was found. Frequency in shipped binaries, Windows loader acceptance and MCP transport were not tested; no data mutation or security-boundary crossing was established.

REA version

rea-agents 6.7.0, source commit 9d8e7803326a382cbbb3ffdb900f5e93e5442d68. Fetched and checked current main d0067ba34a39ea47dd7e11bb2e6169bc0938cc3f: no intervening commits changed the cited parser, contract or documentation. The runtime reproduction is at the baseline; current main was source/history-checked, not rebuilt or rerun.

Runtime and platform

Node.js v26.12.0; macOS 27.0.1; arm64. Independent reader: Python 3.12.15 with pefile 2024.8.26. Hopper, Ghidra and Windows SDK are not involved.

Target and artifact context

Generated 1536-byte PE32 resource-only DLLs with one .rsrc section. The resource interval has 132 bytes at RVA 0x2000 / file offset 0x400. RCDATA type 10, UTF-16 name AB, language IDs 0 and 1033, payloads abc and hello. No third-party binary is required.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Relevant error projection from the word-aligned CLI result:

{"code":"invalid_request","category":"invalid_input","details":{"operation":"inspect_pe_resources","issues":[{"path":["path"],"reason":"invalid_format","message":"Unaligned resource data entry."}]}}

The padded control returns coverage: {"status":"complete","examined_entries":4,"resources":2}. The truncated control points its second data entry to relative offset 124, leaving only eight declared bytes for a 16-byte record; it fails with Resource directory offset leaves the declared resource interval. All three captured stderr streams are empty.

pefile reports both resources in each nontruncated variant: data-entry file offsets 1110/1126 versus 1112/1128, payload RVAs 8312/8316, sizes 3/5, and bytes 616263 / 68656c6c6f. Matching payload SHA-256 values are:

abc   ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
hello 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824

pefile warns about the high zero-byte proportion and zero/out-of-section entry point for both synthetic resource-only fixtures. Its successful resource decoding is not proof of full PE validity or Windows loadability. Evidence provenance: observed; local verification: independently reproduced; reproduction completeness: complete.

Typed outcome or recovery expected

Proposed restoration check: the word-aligned fixture should exit 0 with complete coverage, four examined directory entries and two resources, preserving data-entry file offsets 1110/1126, type 10, name AB, languages 0/1033, payload RVAs 8312/8316, sizes 3/5 and the hashes above. The padded control must continue succeeding and the truncated-entry control must continue failing without partial success. Keep the selected artifact bytes unchanged.

Analysis or suspected cause

Source-derived explanation: PeResourceParser.ts:179-180 first admits the full 16-byte range, then unconditionally rejects offset % 4 !== 0. This is the built-in parser guard, not a bundled dependency failure. The tool contract advertises complete portable resource inspection. A source/test search found the guard but no test asserting this exact error or documented four-byte-start restriction.

Additional context

Prepared with AI assistance using an independently verified fixture and reproduction. Related #3970, #4373 and #4455 concern zero-length/zero-extent RVA mapping; #1702 concerns entry budgets; #2812 and PR #2887 concern payload decoding. This report concerns descriptor-start alignment before opaque RCDATA payload handling. RT_GROUP_ICON layout is a separate payload format and is not a premise here. No earlier known-good version or successful fix/restoration has been established.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5750 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Native analysis or decompilation ### Observed behavior `inspect-pe-resources` rejects the entire inventory when resource data-entry records start at word-aligned offsets 86 and 102 after a UTF-16 resource name. The CLI exits 1 with `invalid_request` / `invalid_input`, reason `invalid_format`, and `Unaligned resource data entry.` Adding two padding bytes before the records makes REA succeed with both resources and unchanged payload RVAs, bytes and hashes. The independent pefile reader recovers both resources from both variants. These observations were independently reproduced by a verifier at the baseline below; the submitter inspected its retained CLI and reader output. This concerns static format inspection. Windows loading was not tested. ### Reproduction From a built source checkout with locked dependencies installed, run the following verifier-executed reproduction. It generates two PE32 resource-only DLLs and a truncated-entry control. Directory tables precede the UTF-16 name, the contiguous data-entry array follows the name, and payloads follow the array. ```sh work_dir="$(mktemp -d)" mkdir -p "$work_dir/home" "$work_dir/cfg" "$work_dir/data" "$work_dir/cache" "$work_dir/state" "$work_dir/tmp" cat > "$work_dir/build_fixture.py" <<'PY_FIXTURE' import struct, sys from pathlib import Path outdir = Path(sys.argv[1]) for label, data_at in [('unaligned', 86), ('aligned', 88)]: resource = bytearray(132) for offset, count in [(0, 1), (24, 1), (48, 2)]: struct.pack_into('<HH', resource, offset + 12, 0, count) # RCDATA, UTF-16 name AB, language IDs 0 and 1033. struct.pack_into('<II', resource, 16, 10, 0x80000018) struct.pack_into('<HH', resource, 36, 1, 0) struct.pack_into('<II', resource, 40, 0x80000050, 0x80000030) struct.pack_into('<II', resource, 64, 0, data_at) struct.pack_into('<II', resource, 72, 1033, data_at + 16) struct.pack_into('<H', resource, 80, 2) resource[82:86] = 'AB'.encode('utf-16le') for at, rva, size in [(data_at, 0x2078, 3), (data_at + 16, 0x207c, 5)]: struct.pack_into('<IIII', resource, at, rva, size, 0, 0) resource[120:123] = b'abc' resource[124:129] = b'hello' image = bytearray(0x600) image[:2] = b'MZ' struct.pack_into('<I', image, 60, 0x80) image[0x80:0x84] = b'PE\0\0' struct.pack_into('<HHIIIHH', image, 0x84, 0x14c, 1, 0, 0, 0, 224, 0x2102) opt = 0x98 struct.pack_into('<H', image, opt, 0x10b) struct.pack_into('<I', image, opt + 8, 0x200) struct.pack_into('<I', image, opt + 28, 0x400000) struct.pack_into('<II', image, opt + 32, 0x1000, 0x200) struct.pack_into('<HH', image, opt + 40, 6, 0) struct.pack_into('<HH', image, opt + 48, 6, 0) struct.pack_into('<II', image, opt + 56, 0x3000, 0x400) struct.pack_into('<H', image, opt + 68, 3) struct.pack_into('<IIII', image, opt + 72, 0x100000, 0x1000, 0x100000, 0x1000) struct.pack_into('<I', image, opt + 92, 16) struct.pack_into('<II', image, opt + 112, 0x2000, len(resource)) section = opt + 224 image[section:section + 8] = b'.rsrc\0\0\0' struct.pack_into('<IIII', image, section + 8, len(resource), 0x2000, 0x200, 0x400) struct.pack_into('<I', image, section + 36, 0x40000040) image[0x400:0x484] = resource (outdir / (label + '.dll')).write_bytes(image) if label == 'aligned': struct.pack_into('<I', image, 0x400 + 76, len(resource) - 8) (outdir / 'truncated.dll').write_bytes(image) print(label, 'data entries', data_at, data_at + 16) PY_FIXTURE python3 -I "$work_dir/build_fixture.py" "$work_dir" for variant in unaligned aligned truncated; do env -i PATH="$PATH" HOME="$work_dir/home" XDG_CONFIG_HOME="$work_dir/cfg" XDG_DATA_HOME="$work_dir/data" XDG_CACHE_HOME="$work_dir/cache" XDG_STATE_HOME="$work_dir/state" TMPDIR="$work_dir/tmp" REA_LOG_LEVEL=silent node scripts/rea.mjs inspect-pe-resources "$work_dir/$variant.dll" --json > "$work_dir/$variant.json" printf '%s exit %s\n' "$variant" "$?" done python3 -m pip install --disable-pip-version-check --target "$work_dir/py" pefile==2024.8.26 cat > "$work_dir/read_pefile.py" <<'PY_READER' import hashlib, json, pathlib, sys sys.path.insert(0, sys.argv[1]) import pefile print('pefile', pefile.__version__) for file in sys.argv[2:]: pe = pefile.PE(file) rows = [] for typ in pe.DIRECTORY_ENTRY_RESOURCE.entries: for name in typ.directory.entries: for lang in name.directory.entries: d = lang.data.struct data = pe.get_data(d.OffsetToData, d.Size) rows.append(dict(type=typ.id, name=str(name.name), language=lang.id, data_entry_offset=d.get_file_offset(), rva=d.OffsetToData, size=d.Size, payload=data.hex(), sha256=hashlib.sha256(data).hexdigest())) print(pathlib.Path(file).name, json.dumps(rows)) print('warnings', json.dumps(pe.get_warnings())) PY_READER python3 -I "$work_dir/read_pefile.py" "$work_dir/py" "$work_dir/unaligned.dll" "$work_dir/aligned.dll" ``` Observed: `unaligned exit 1`, `aligned exit 0`, `truncated exit 1`. The complete shell block was replayed successfully by the independent verifier. No selected target was executed. ### Expected behavior Return complete type/name/language inspection for both word-aligned and padded data-entry arrays, preserving original record offsets and opaque payload hashes. The [portable PE resource contract](https://github.com/morluto/rea/blob/9d8e7803326a382cbbb3ffdb900f5e93e5442d68/docs/binary-diagnostics.md#portable-pe-resources) promises those facts and explicitly lists malformed/truncated structures and ambiguous mappings as refusals; it does not document a four-byte data-entry exclusion. Source-derived format reasoning: Microsoft's [Resource Directory String and Resource Data Entry specification](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#resource-directory-string) requires word alignment for strings and defines directory-relative offsets and four 32-bit fields for data entries, without specifying four-byte alignment of each record's start. Here the UTF-16 name ends at offset 86. All records and payloads fit within one file-backed section without overlapping ranges; both payloads are four-byte-aligned. Independent decoding supports the static inspection expectation, without establishing Windows loadability. ### Impact and scope P2, supported: a bounded resource-inspection feature refuses the entire inventory for this demonstrated layout. The executed pefile reader provides a workaround preserving identities, descriptor locations, payload bytes and SHA-256 hashes from the unchanged input; it requires Python/pefile and does not produce REA's Evidence envelope. No REA alignment override was found. Frequency in shipped binaries, Windows loader acceptance and MCP transport were not tested; no data mutation or security-boundary crossing was established. ### REA version `rea-agents` 6.7.0, source commit `9d8e7803326a382cbbb3ffdb900f5e93e5442d68`. Fetched and checked current main `d0067ba34a39ea47dd7e11bb2e6169bc0938cc3f`: no intervening commits changed the cited parser, contract or documentation. The runtime reproduction is at the baseline; current main was source/history-checked, not rebuilt or rerun. ### Runtime and platform Node.js v26.12.0; macOS 27.0.1; arm64. Independent reader: Python 3.12.15 with pefile 2024.8.26. Hopper, Ghidra and Windows SDK are not involved. ### Target and artifact context Generated 1536-byte PE32 resource-only DLLs with one `.rsrc` section. The resource interval has 132 bytes at RVA 0x2000 / file offset 0x400. RCDATA type 10, UTF-16 name AB, language IDs 0 and 1033, payloads `abc` and `hello`. No third-party binary is required. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Relevant error projection from the word-aligned CLI result: ```json {"code":"invalid_request","category":"invalid_input","details":{"operation":"inspect_pe_resources","issues":[{"path":["path"],"reason":"invalid_format","message":"Unaligned resource data entry."}]}} ``` The padded control returns `coverage: {"status":"complete","examined_entries":4,"resources":2}`. The truncated control points its second data entry to relative offset 124, leaving only eight declared bytes for a 16-byte record; it fails with `Resource directory offset leaves the declared resource interval.` All three captured stderr streams are empty. pefile reports both resources in each nontruncated variant: data-entry file offsets 1110/1126 versus 1112/1128, payload RVAs 8312/8316, sizes 3/5, and bytes `616263` / `68656c6c6f`. Matching payload SHA-256 values are: ```text abc ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad hello 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 ``` pefile warns about the high zero-byte proportion and zero/out-of-section entry point for both synthetic resource-only fixtures. Its successful resource decoding is not proof of full PE validity or Windows loadability. Evidence provenance: observed; local verification: independently reproduced; reproduction completeness: complete. ### Typed outcome or recovery expected Proposed restoration check: the word-aligned fixture should exit 0 with complete coverage, four examined directory entries and two resources, preserving data-entry file offsets 1110/1126, type 10, name AB, languages 0/1033, payload RVAs 8312/8316, sizes 3/5 and the hashes above. The padded control must continue succeeding and the truncated-entry control must continue failing without partial success. Keep the selected artifact bytes unchanged. ### Analysis or suspected cause Source-derived explanation: [PeResourceParser.ts:179-180](https://github.com/morluto/rea/blob/9d8e7803326a382cbbb3ffdb900f5e93e5442d68/src/native/pe/PeResourceParser.ts#L179-L180) first admits the full 16-byte range, then unconditionally rejects `offset % 4 !== 0`. This is the built-in parser guard, not a bundled dependency failure. The [tool contract](https://github.com/morluto/rea/blob/9d8e7803326a382cbbb3ffdb900f5e93e5442d68/src/contracts/native/binaryDiagnosticsToolContracts.ts#L29-L33) advertises complete portable resource inspection. A source/test search found the guard but no test asserting this exact error or documented four-byte-start restriction. ### Additional context Prepared with AI assistance using an independently verified fixture and reproduction. Related #3970, #4373 and #4455 concern zero-length/zero-extent RVA mapping; #1702 concerns entry budgets; #2812 and PR #2887 concern payload decoding. This report concerns descriptor-start alignment before opaque RCDATA payload handling. RT_GROUP_ICON layout is a separate payload format and is not a premise here. No earlier known-good version or successful fix/restoration has been established.
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#3496
No description provided.