[GH-ISSUE #6872] [Bug] inspect-pe-resources drops the identity of an empty resource branch #4211

Closed
opened 2026-10-31 00:47:45 +00:00 by bsakaguchi · 0 comments
Owner

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

Before submitting

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

The initial open-issue snapshot and fresh open/closed issue searches were checked; no duplicate was found. This is distinct from #2812, which requests decoding opaque RT_VERSION and RT_STRING payloads.

Area

Native analysis or decompilation

Observed behavior

The built public inspect-pe-resources CLI accepts a resource-only PE32 image containing one type directory with zero children, examines its type entry, and reports complete tree coverage. The result drops that entry's numeric type ID: directories contains header metadata and ranges, while resources is empty. Changing the input's type ID from 10 to 11 produces the same parsed report apart from artifact path and digest, so a caller cannot recover which branch was inspected from the returned tree.

Reproduction

From the stated source checkout with its existing built runtime and Node.js v24.22.1, save the following as /tmp/rea-pe-repro.py. It creates two 1,024-byte inert PE32 files, invokes only static resource inspection, saves stdout/stderr, and compares the results. Each resource table is 40 bytes: a root with one numeric entry pointing to an empty directory. The files differ only at file offset 528, where their numeric type ID is stored.

import struct,pathlib,subprocess,json,sys
root=pathlib.Path(sys.argv[1]); root.mkdir(parents=True,exist_ok=True)
node=sys.argv[2]
cli=sys.argv[3]
def image(type_id):
 b=bytearray(1024)
 def h(a,v):struct.pack_into('<H',b,a,v)
 def d(a,v):struct.pack_into('<I',b,a,v)
 h(0,0x5a4d);d(60,128);d(128,0x4550);h(132,0x14c);h(134,1);h(148,224);h(150,0x2102)
 o=152;h(o,0x10b);d(o+8,512);d(o+28,0x10000000);d(o+32,4096);d(o+36,512);h(o+40,6);h(o+48,6);d(o+56,8192);d(o+60,512);h(o+68,2);d(o+72,0x100000);d(o+76,4096);d(o+80,0x100000);d(o+84,4096);d(o+92,16);d(o+112,4096);d(o+116,40)
 s=o+224;b[s:s+5]=b'.rsrc';d(s+8,40);d(s+12,4096);d(s+16,512);d(s+20,512);d(s+36,0x40000040)
 h(512+14,1);d(512+16,type_id);d(512+20,0x80000018)
 return bytes(b)
reports=[]
for type_id in (10,11):
 data=image(type_id);path=root/f'empty-type-{type_id}.dll';path.write_bytes(data)
 p=subprocess.run([node,cli,'inspect-pe-resources',str(path),'--json'],capture_output=True,text=True)
 (root/f'empty-type-{type_id}.stdout.json').write_text(p.stdout);(root/f'empty-type-{type_id}.stderr.txt').write_text(p.stderr)
 assert p.returncode==0,(p.stdout,p.stderr)
 report=json.loads(p.stdout)['normalized_result'];assert path.read_bytes()==data
 reports.append(report)
 print(json.dumps({'type_id':type_id,'exit':p.returncode,'directory':report['directory'],'directories':report['directories'],'resources':report['resources'],'coverage':report['coverage']}))
comparison=[{k:v for k,v in r.items() if k!='artifact'} for r in reports]
assert comparison[0]==comparison[1]
assert reports[0]['coverage']=={'status':'complete','examined_entries':1,'resources':0}
e={'candidate':'empty resource type branch loses identity','runs':2,'fixture_bytes':1024,'resource_table_bytes':40,'only_input_byte_difference':{'offset':528,'left':10,'right':11},'outputs_equal_except_artifact':True,'reports':reports}
assert [i for i,(a,b) in enumerate(zip(image(10),image(11))) if a!=b]==[528]
(root/'empty-branches.evidence.json').write_text(json.dumps(e,indent=2))

Run from the checkout with Node.js on PATH:

python3 /tmp/rea-pe-repro.py /tmp/rea-pe-repro node "$PWD/scripts/rea.mjs"

Both cases were observed through the public built CLI twice before drafting, first with the initial fixture harness and then with the portable script above. All four invocations exited 0. The portable script's assertions passed. The selected files remained byte-for-byte unchanged. No target code was executed.

Expected behavior

The advertised complete bounded type/name/language resource tree should retain the identity of every examined directory entry, including a branch with no payload leaves. These two inputs should expose type ID 10 and type ID 11 respectively; they should not collapse into the same parsed tree. src/contracts/native/binaryDiagnosticsToolContracts.ts:111-118 advertises complete-tree inspection and original identities, and AGENTS.md requires preserving identity and explicit omission across boundaries.

The Microsoft PE format specification describes per-directory name/ID entry counts and entries pointing to the next directory. The fixtures use those structures with a zero-entry child table. Windows loader behavior and incidence in compiler-produced binaries were not tested.

Impact and scope

Observed impact is bounded loss of branch identity in the static parsed tree, despite complete coverage. No payload is missing in these fixtures because their branches have no leaves. Original bytes, artifact hashes, directory counts and file ranges remain available. Real-world occurrence and empty name/language branch variants were not measured. An alternative complete-tree workflow was not tested.

REA version

Source revision 8d8d85ba22463b80715a1af1fdb51acc9b5d760b, using that checkout's existing built CLI. No rebuild or published-package comparison was performed.

Runtime and platform

Node.js v24.22.1; Linux x86_64; Python 3 fixture generator. No native analysis engine or Windows SDK was used.

Target and artifact context

Two generated resource-only PE32 DLL fixtures with one .rsrc section, one root numeric type entry and one empty child resource directory. Neither fixture contains executable code or a resource payload.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

  • Expected source: The public resource tool contract at src/contracts/native/binaryDiagnosticsToolContracts.ts:111-118 promises a complete resource tree and original identities.
  • Failure source: The executed built CLI returned the following parsed fields for both distinct numeric type IDs; the portable script also asserted equality of every report field except artifact.
  • Evidence provenance: observed; the public CLI output and unchanged input bytes were checked locally. The producing-boundary explanation below is source-derived.
  • Local verification: reproduced; two successful inspections per fixture before drafting.
  • Reproduction completeness: complete.
{
  "directories": [
    {"location":{"offset":512,"bytes":24},"characteristics":0,"timestamp":0,"major_version":0,"minor_version":0,"named_entries":0,"id_entries":1},
    {"location":{"offset":536,"bytes":16},"characteristics":0,"timestamp":0,"major_version":0,"minor_version":0,"named_entries":0,"id_entries":0}
  ],
  "resources": [],
  "icon_groups": [],
  "coverage": {"status":"complete","examined_entries":1,"resources":0}
}

Typed outcome or recovery expected

Restoration check: Inspect the unchanged pair and observe their respective branch type IDs in the parsed tree. Keep the empty payload inventory, complete coverage, artifact identity and original directory ranges accurate. A populated branch should continue to retain its existing leaf identities and payload hashes. The representation and implementation are left to the maintainer.

Analysis or suspected cause

Source-derived: src/native/pe/PeResourceParser.ts:134-144 retains directory headers without a path or incoming identity. Traversal reads the entry identity and passes it recursively, but only payload-leaf rows retain the accumulated path. src/domain/native/peResources.ts:66-84 likewise places identities only on resource leaves. PeResourcesService accepts the resulting complete report through the public workflow. A branch with no leaves therefore has no identity-bearing output row.

Additional context

The report does not concern missing payload decoders, language fallback, image decoding, malformed inputs or target execution. Encoded icon dimensions of zero were separately checked and rejected as a candidate because docs/binary-diagnostics.md explicitly preserves that encoding.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: The executed public inspection silently loses an examined resource branch's numeric identity while claiming a complete tree; the reduced input pair proves the bounded information loss. It contains no payload loss or mutation.
  • Workaround status: unverified
  • Workaround basis: Original bytes and branch-entry file ranges remain available, but an alternative workflow preserving the complete branch identities was not tested.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6872 ## 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. The initial open-issue snapshot and fresh open/closed issue searches were checked; no duplicate was found. This is distinct from #2812, which requests decoding opaque RT_VERSION and RT_STRING payloads. ## Area Native analysis or decompilation ## Observed behavior The built public `inspect-pe-resources` CLI accepts a resource-only PE32 image containing one type directory with zero children, examines its type entry, and reports complete tree coverage. The result drops that entry's numeric type ID: `directories` contains header metadata and ranges, while `resources` is empty. Changing the input's type ID from 10 to 11 produces the same parsed report apart from artifact path and digest, so a caller cannot recover which branch was inspected from the returned tree. ## Reproduction From the stated source checkout with its existing built runtime and Node.js v24.22.1, save the following as `/tmp/rea-pe-repro.py`. It creates two 1,024-byte inert PE32 files, invokes only static resource inspection, saves stdout/stderr, and compares the results. Each resource table is 40 bytes: a root with one numeric entry pointing to an empty directory. The files differ only at file offset 528, where their numeric type ID is stored. ```python import struct,pathlib,subprocess,json,sys root=pathlib.Path(sys.argv[1]); root.mkdir(parents=True,exist_ok=True) node=sys.argv[2] cli=sys.argv[3] def image(type_id): b=bytearray(1024) def h(a,v):struct.pack_into('<H',b,a,v) def d(a,v):struct.pack_into('<I',b,a,v) h(0,0x5a4d);d(60,128);d(128,0x4550);h(132,0x14c);h(134,1);h(148,224);h(150,0x2102) o=152;h(o,0x10b);d(o+8,512);d(o+28,0x10000000);d(o+32,4096);d(o+36,512);h(o+40,6);h(o+48,6);d(o+56,8192);d(o+60,512);h(o+68,2);d(o+72,0x100000);d(o+76,4096);d(o+80,0x100000);d(o+84,4096);d(o+92,16);d(o+112,4096);d(o+116,40) s=o+224;b[s:s+5]=b'.rsrc';d(s+8,40);d(s+12,4096);d(s+16,512);d(s+20,512);d(s+36,0x40000040) h(512+14,1);d(512+16,type_id);d(512+20,0x80000018) return bytes(b) reports=[] for type_id in (10,11): data=image(type_id);path=root/f'empty-type-{type_id}.dll';path.write_bytes(data) p=subprocess.run([node,cli,'inspect-pe-resources',str(path),'--json'],capture_output=True,text=True) (root/f'empty-type-{type_id}.stdout.json').write_text(p.stdout);(root/f'empty-type-{type_id}.stderr.txt').write_text(p.stderr) assert p.returncode==0,(p.stdout,p.stderr) report=json.loads(p.stdout)['normalized_result'];assert path.read_bytes()==data reports.append(report) print(json.dumps({'type_id':type_id,'exit':p.returncode,'directory':report['directory'],'directories':report['directories'],'resources':report['resources'],'coverage':report['coverage']})) comparison=[{k:v for k,v in r.items() if k!='artifact'} for r in reports] assert comparison[0]==comparison[1] assert reports[0]['coverage']=={'status':'complete','examined_entries':1,'resources':0} e={'candidate':'empty resource type branch loses identity','runs':2,'fixture_bytes':1024,'resource_table_bytes':40,'only_input_byte_difference':{'offset':528,'left':10,'right':11},'outputs_equal_except_artifact':True,'reports':reports} assert [i for i,(a,b) in enumerate(zip(image(10),image(11))) if a!=b]==[528] (root/'empty-branches.evidence.json').write_text(json.dumps(e,indent=2)) ``` Run from the checkout with Node.js on PATH: ```sh python3 /tmp/rea-pe-repro.py /tmp/rea-pe-repro node "$PWD/scripts/rea.mjs" ``` Both cases were observed through the public built CLI twice before drafting, first with the initial fixture harness and then with the portable script above. All four invocations exited 0. The portable script's assertions passed. The selected files remained byte-for-byte unchanged. No target code was executed. ## Expected behavior The advertised complete bounded type/name/language resource tree should retain the identity of every examined directory entry, including a branch with no payload leaves. These two inputs should expose type ID 10 and type ID 11 respectively; they should not collapse into the same parsed tree. `src/contracts/native/binaryDiagnosticsToolContracts.ts:111-118` advertises complete-tree inspection and original identities, and `AGENTS.md` requires preserving identity and explicit omission across boundaries. The [Microsoft PE format specification](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#resource-directory-table) describes per-directory name/ID entry counts and entries pointing to the next directory. The fixtures use those structures with a zero-entry child table. Windows loader behavior and incidence in compiler-produced binaries were not tested. ## Impact and scope Observed impact is bounded loss of branch identity in the static parsed tree, despite complete coverage. No payload is missing in these fixtures because their branches have no leaves. Original bytes, artifact hashes, directory counts and file ranges remain available. Real-world occurrence and empty name/language branch variants were not measured. An alternative complete-tree workflow was not tested. ## REA version Source revision `8d8d85ba22463b80715a1af1fdb51acc9b5d760b`, using that checkout's existing built CLI. No rebuild or published-package comparison was performed. ## Runtime and platform Node.js v24.22.1; Linux x86_64; Python 3 fixture generator. No native analysis engine or Windows SDK was used. ## Target and artifact context Two generated resource-only PE32 DLL fixtures with one `.rsrc` section, one root numeric type entry and one empty child resource directory. Neither fixture contains executable code or a resource payload. ## Interface CLI ## Earliest failure stage Native analysis or reconstruction ## Relevant output or evidence - Expected source: The public resource tool contract at `src/contracts/native/binaryDiagnosticsToolContracts.ts:111-118` promises a complete resource tree and original identities. - Failure source: The executed built CLI returned the following parsed fields for both distinct numeric type IDs; the portable script also asserted equality of every report field except `artifact`. - Evidence provenance: observed; the public CLI output and unchanged input bytes were checked locally. The producing-boundary explanation below is source-derived. - Local verification: reproduced; two successful inspections per fixture before drafting. - Reproduction completeness: complete. ```json { "directories": [ {"location":{"offset":512,"bytes":24},"characteristics":0,"timestamp":0,"major_version":0,"minor_version":0,"named_entries":0,"id_entries":1}, {"location":{"offset":536,"bytes":16},"characteristics":0,"timestamp":0,"major_version":0,"minor_version":0,"named_entries":0,"id_entries":0} ], "resources": [], "icon_groups": [], "coverage": {"status":"complete","examined_entries":1,"resources":0} } ``` ## Typed outcome or recovery expected Restoration check: Inspect the unchanged pair and observe their respective branch type IDs in the parsed tree. Keep the empty payload inventory, complete coverage, artifact identity and original directory ranges accurate. A populated branch should continue to retain its existing leaf identities and payload hashes. The representation and implementation are left to the maintainer. ## Analysis or suspected cause Source-derived: `src/native/pe/PeResourceParser.ts:134-144` retains directory headers without a path or incoming identity. Traversal reads the entry identity and passes it recursively, but only payload-leaf rows retain the accumulated path. `src/domain/native/peResources.ts:66-84` likewise places identities only on resource leaves. `PeResourcesService` accepts the resulting complete report through the public workflow. A branch with no leaves therefore has no identity-bearing output row. ## Additional context The report does not concern missing payload decoders, language fallback, image decoding, malformed inputs or target execution. Encoded icon dimensions of zero were separately checked and rejected as a candidate because `docs/binary-diagnostics.md` explicitly preserves that encoding. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: The executed public inspection silently loses an examined resource branch's numeric identity while claiming a complete tree; the reduced input pair proves the bounded information loss. It contains no payload loss or mutation. - Workaround status: unverified - Workaround basis: Original bytes and branch-entry file ranges remain available, but an alternative workflow preserving the complete branch identities was not tested.
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#4211
No description provided.