[GH-ISSUE #2172] fix(targets): linker-generated PE DLL with a short DOS header is classified as DOS #719

Closed
opened 2026-10-15 18:27:40 +00:00 by bsakaguchi · 0 comments
Owner

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

Summary

REA classifies a linker-generated x86-64 PE32+ DLL as dos-mz with architecture x86 when its custom DOS stub has a two-paragraph header. The public inspect command consequently reports that Hopper cannot support the target because it requires the DOS real-mode adapter. The DLL has a complete PE optional header and two file-backed sections, and independent format readers recognize it as PE32+. The unchanged artifact remains accessible through inspect-pe-resources.

Runtime or environment

Tested separately against source revision c22111eca2c4bc84a084f12711f405b3e7422a40 and published rea-agents 6.4.0. The source revision is 259 commits after the release tag, rather than the published release revision. Runtime was Node.js 24.21.0 on macOS 27.0.1 arm64, within the documented installed-runtime support range. The fixture was produced with Apple Clang 21.0.0 and LLD 23.1.1; LLVM 23.1.1 and file independently recognized it. No native analysis engine or Windows/DOS runtime executed the artifact.

Steps to reproduce

  1. Use either tested REA build and a Clang installation capable of generating Windows COFF objects, plus lld-link, Python 3, and file. In a disposable directory, generate a benign DOS stub and a C function:
python3 - <<'PY'
from pathlib import Path
import struct
stub = bytearray(96)
# DOS MZ header: 96-byte image, no relocations, 32-byte header,
# entry CS:IP 0:0 and stack SS:SP 0:64 within the 64-byte module.
struct.pack_into('<14H', stub, 0,
                 0x5a4d, 96, 1, 0, 2, 0, 0xffff, 0, 64, 0, 0, 0, 28, 0)
# The DOS program immediately exits with status 0.
stub[32:37] = bytes.fromhex('b8004ccd21')
Path('short-dos.exe').write_bytes(stub)
Path('fixture.c').write_text('__declspec(dllexport) int answer(void) { return 42; }\n')
PY
clang --target=x86_64-pc-windows-msvc -c fixture.c -o fixture.obj
lld-link /dll /noentry /nodefaultlib /machine:x64 /timestamp:0 /out:short.dll /stub:short-dos.exe fixture.obj
lld-link /dll /noentry /nodefaultlib /machine:x64 /timestamp:0 /out:default.dll fixture.obj
file short.dll default.dll
  1. Make a discriminating copy that changes only e_cparhdr, and a DOS-only control that retains the stub bytes without the PE records:
python3 - <<'PY'
from pathlib import Path
import struct
original = Path('short.dll').read_bytes()
control = bytearray(original)
struct.pack_into('<H', control, 8, 4)
Path('header-four.dll').write_bytes(control)
Path('dos-only.exe').write_bytes(original[:96])
PY
  1. Run the public inspection route with provider startup deliberately disabled. The isolated missing launcher allows observation of format admission without opening a provider or executing the fixture:
mkdir -p isolated-home
env -u REA_IDA_MCP_CONFIG HOME="$PWD/isolated-home" XDG_CONFIG_HOME="$PWD/isolated-home" XDG_CACHE_HOME="$PWD/isolated-home" HOPPER_LAUNCHER_PATH="$PWD/missing-hopper" GHIDRA_INSTALL_DIR="$PWD/missing-ghidra" rea inspect ./short.dll --provider hopper --format json
env -u REA_IDA_MCP_CONFIG HOME="$PWD/isolated-home" XDG_CONFIG_HOME="$PWD/isolated-home" XDG_CACHE_HOME="$PWD/isolated-home" HOPPER_LAUNCHER_PATH="$PWD/missing-hopper" GHIDRA_INSTALL_DIR="$PWD/missing-ghidra" rea inspect ./header-four.dll --provider hopper --format json
rea inspect-pe-resources ./short.dll --format json

The measured runs invoked each build's scripts/rea.mjs dispatcher with Node, which is the package's rea executable. The fixture-generation commands and both public CLI operations were executed. The DOS-only control was checked through the production filesystem resolver, not through a native provider.

Expected behavior

The supported PE header route should recover the DLL's PE format, x86-64 architecture and shared-library role from the new-header pointer, PE signature and COFF/optional headers. It should not turn the DLL into a DOS-only target because the legacy DOS program's declared header occupies 32 bytes. With the launcher deliberately absent, the PE control and affected DLL should both reach provider-availability handling without a DOS-specific format refusal.

Microsoft's PE format description places the PE signature pointer at file offset 0x3c. Its linker documentation permits any valid MS-DOS application as a custom stub. Here the DOS program has a complete 28-byte fixed header padded to 32 bytes, a bounded module and stack, no relocations, and an immediate exit instruction. LLD supplies the pointer at 0x3c and emits the PE records and sections. This establishes applicability to static PE classification; it does not assert successful native Windows loading.

Actual behavior

Both tested builds return CLI exit status 1 for the affected DLL with code: "capability_unavailable" and selection_reason: "target_unsupported". The relevant rejection is:

{
  "provider_id": "hopper",
  "code": "target_format_unsupported",
  "reason": "DOS MZ/COM analysis requires the Ghidra 16-bit real-mode adapter.",
  "diagnostics": {
    "target_kind": "executable",
    "target_format": "dos-mz",
    "architecture": "x86"
  }
}

The production filesystem resolver returns dos-mz / x86 for short.dll, but pe / x86_64 / shared-library for both the default-stub DLL and the copy whose header-size field is four paragraphs. The public inspection route for both PE controls instead returns provider_unavailable, with no DOS-specific rejection. These control commands also exit 1 because the launcher was deliberately absent; they do not establish successful provider analysis.

inspect-pe-resources exits 0 on the original DLL in both builds and reports format: "pe32-plus", machine: 34404 and complete inspection of an absent resource directory. Resource inspection therefore remains available and does not depend on changing the artifact. The DOS-only control remains dos-mz / x86.

Evidence

  • Expected source: src/domain/binaryTarget.ts:44 documents supported executable header parsing and :228 recovers PE machine, role and managed status; docs/binary-diagnostics.md:148 documents explicit PE32/PE32+ inspection. Microsoft's PE format, MS-DOS stub and signature and /STUB linker option establish the custom-stub and signature-pointer semantics. The executed LLD output, independent recognition and complete resource-parser admission establish this fixture's static PE classification scope.
  • Failure source: Executed public inspect output and production parseBinaryTarget results for the inline fixture in both tested builds. src/application/BinaryTargetResolver.ts:411 calls mzWindowsHeaderOffset; src/domain/dosMz.ts:25 suppresses the pointer for a nonzero DOS header shorter than 64 bytes; src/domain/binaryTarget.ts:204 then selects DOS MZ validation. src/hopper/HopperProvider.ts:136 rejects the resulting DOS classification.
  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: complete

The affected DLL is 2048 bytes, with e_cparhdr = 2, e_crlc = 0, e_lfanew = 96, machine 0x8664, optional-header magic 0x20b, optional-header size 240, 16 data directories and two sections. Its DOS image declares one 96-byte page fragment, a 32-byte header and a 64-byte initialized module. Every DOS fallback validation passes; the PE signature and optional header also pass when the short-header guard is bypassed by the discriminating control. LLVM reports COFF-x86-64, .text and .rdata; file reports a PE32+ x86-64 DLL. Native loader execution and deep-provider output were not tested.

Restoration check

For the generated short-stub DLL, production target resolution must recover pe, x86_64 and shared-library, and public inspection must stop producing the DOS-specific target_format_unsupported rejection. In the isolated missing-launcher environment it should reach the same provider-availability result as the PE controls. The default-stub and four-paragraph controls must retain their PE classifications, and the unchanged original must continue to succeed through PE resource inspection. Preserve valid DOS-only classification for dos-only.exe, whose short-header module contains a nonzero value at 0x3c but has no appended PE records, along with existing truncated-header and relocation-table safeguards.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: A bounded, independently generated class of PE input produces incorrect target format and architecture facts and a false public provider-format refusal. The exact proof establishes classification and admission consequences, without observing wrong decompilation, data loss, or an affected user's otherwise working deep-analysis workflow; P1 is not established by this evidence.
  • Workaround status: unverified
  • Workaround basis: PE resource inspection is a verified alternative on the unchanged artifact for that specific operation, but it does not restore shared target classification or Hopper admission. No complete workaround for the reported contract was verified. Editing the header creates a different artifact, and raw-binary selection does not restore PE semantics.
Originally created by @coygeek on GitHub (Oct 12, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/2172 ## Summary REA classifies a linker-generated x86-64 PE32+ DLL as `dos-mz` with architecture `x86` when its custom DOS stub has a two-paragraph header. The public `inspect` command consequently reports that Hopper cannot support the target because it requires the DOS real-mode adapter. The DLL has a complete PE optional header and two file-backed sections, and independent format readers recognize it as PE32+. The unchanged artifact remains accessible through `inspect-pe-resources`. ## Runtime or environment Tested separately against source revision `c22111eca2c4bc84a084f12711f405b3e7422a40` and published `rea-agents` 6.4.0. The source revision is 259 commits after the release tag, rather than the published release revision. Runtime was Node.js 24.21.0 on macOS 27.0.1 arm64, within the documented installed-runtime support range. The fixture was produced with Apple Clang 21.0.0 and LLD 23.1.1; LLVM 23.1.1 and `file` independently recognized it. No native analysis engine or Windows/DOS runtime executed the artifact. ## Steps to reproduce 1. Use either tested REA build and a Clang installation capable of generating Windows COFF objects, plus `lld-link`, Python 3, and `file`. In a disposable directory, generate a benign DOS stub and a C function: ```sh python3 - <<'PY' from pathlib import Path import struct stub = bytearray(96) # DOS MZ header: 96-byte image, no relocations, 32-byte header, # entry CS:IP 0:0 and stack SS:SP 0:64 within the 64-byte module. struct.pack_into('<14H', stub, 0, 0x5a4d, 96, 1, 0, 2, 0, 0xffff, 0, 64, 0, 0, 0, 28, 0) # The DOS program immediately exits with status 0. stub[32:37] = bytes.fromhex('b8004ccd21') Path('short-dos.exe').write_bytes(stub) Path('fixture.c').write_text('__declspec(dllexport) int answer(void) { return 42; }\n') PY clang --target=x86_64-pc-windows-msvc -c fixture.c -o fixture.obj lld-link /dll /noentry /nodefaultlib /machine:x64 /timestamp:0 /out:short.dll /stub:short-dos.exe fixture.obj lld-link /dll /noentry /nodefaultlib /machine:x64 /timestamp:0 /out:default.dll fixture.obj file short.dll default.dll ``` 2. Make a discriminating copy that changes only `e_cparhdr`, and a DOS-only control that retains the stub bytes without the PE records: ```sh python3 - <<'PY' from pathlib import Path import struct original = Path('short.dll').read_bytes() control = bytearray(original) struct.pack_into('<H', control, 8, 4) Path('header-four.dll').write_bytes(control) Path('dos-only.exe').write_bytes(original[:96]) PY ``` 3. Run the public inspection route with provider startup deliberately disabled. The isolated missing launcher allows observation of format admission without opening a provider or executing the fixture: ```sh mkdir -p isolated-home env -u REA_IDA_MCP_CONFIG HOME="$PWD/isolated-home" XDG_CONFIG_HOME="$PWD/isolated-home" XDG_CACHE_HOME="$PWD/isolated-home" HOPPER_LAUNCHER_PATH="$PWD/missing-hopper" GHIDRA_INSTALL_DIR="$PWD/missing-ghidra" rea inspect ./short.dll --provider hopper --format json env -u REA_IDA_MCP_CONFIG HOME="$PWD/isolated-home" XDG_CONFIG_HOME="$PWD/isolated-home" XDG_CACHE_HOME="$PWD/isolated-home" HOPPER_LAUNCHER_PATH="$PWD/missing-hopper" GHIDRA_INSTALL_DIR="$PWD/missing-ghidra" rea inspect ./header-four.dll --provider hopper --format json rea inspect-pe-resources ./short.dll --format json ``` The measured runs invoked each build's `scripts/rea.mjs` dispatcher with Node, which is the package's `rea` executable. The fixture-generation commands and both public CLI operations were executed. The DOS-only control was checked through the production filesystem resolver, not through a native provider. ## Expected behavior The supported PE header route should recover the DLL's PE format, x86-64 architecture and shared-library role from the new-header pointer, PE signature and COFF/optional headers. It should not turn the DLL into a DOS-only target because the legacy DOS program's declared header occupies 32 bytes. With the launcher deliberately absent, the PE control and affected DLL should both reach provider-availability handling without a DOS-specific format refusal. Microsoft's PE format description places the PE signature pointer at file offset `0x3c`. Its linker documentation permits any valid MS-DOS application as a custom stub. Here the DOS program has a complete 28-byte fixed header padded to 32 bytes, a bounded module and stack, no relocations, and an immediate exit instruction. LLD supplies the pointer at `0x3c` and emits the PE records and sections. This establishes applicability to static PE classification; it does not assert successful native Windows loading. ## Actual behavior Both tested builds return CLI exit status 1 for the affected DLL with `code: "capability_unavailable"` and `selection_reason: "target_unsupported"`. The relevant rejection is: ```json { "provider_id": "hopper", "code": "target_format_unsupported", "reason": "DOS MZ/COM analysis requires the Ghidra 16-bit real-mode adapter.", "diagnostics": { "target_kind": "executable", "target_format": "dos-mz", "architecture": "x86" } } ``` The production filesystem resolver returns `dos-mz` / `x86` for `short.dll`, but `pe` / `x86_64` / `shared-library` for both the default-stub DLL and the copy whose header-size field is four paragraphs. The public inspection route for both PE controls instead returns `provider_unavailable`, with no DOS-specific rejection. These control commands also exit 1 because the launcher was deliberately absent; they do not establish successful provider analysis. `inspect-pe-resources` exits 0 on the original DLL in both builds and reports `format: "pe32-plus"`, `machine: 34404` and complete inspection of an absent resource directory. Resource inspection therefore remains available and does not depend on changing the artifact. The DOS-only control remains `dos-mz` / `x86`. ## Evidence - Expected source: `src/domain/binaryTarget.ts:44` documents supported executable header parsing and `:228` recovers PE machine, role and managed status; `docs/binary-diagnostics.md:148` documents explicit PE32/PE32+ inspection. Microsoft's [PE format, MS-DOS stub and signature](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#ms-dos-stub-image-only) and [/STUB linker option](https://learn.microsoft.com/en-us/cpp/build/reference/stub-ms-dos-stub-file-name?view=msvc-170) establish the custom-stub and signature-pointer semantics. The executed LLD output, independent recognition and complete resource-parser admission establish this fixture's static PE classification scope. - Failure source: Executed public `inspect` output and production `parseBinaryTarget` results for the inline fixture in both tested builds. `src/application/BinaryTargetResolver.ts:411` calls `mzWindowsHeaderOffset`; `src/domain/dosMz.ts:25` suppresses the pointer for a nonzero DOS header shorter than 64 bytes; `src/domain/binaryTarget.ts:204` then selects DOS MZ validation. `src/hopper/HopperProvider.ts:136` rejects the resulting DOS classification. - Evidence provenance: observed - Local verification: reproduced - Reproduction completeness: complete The affected DLL is 2048 bytes, with `e_cparhdr = 2`, `e_crlc = 0`, `e_lfanew = 96`, machine `0x8664`, optional-header magic `0x20b`, optional-header size 240, 16 data directories and two sections. Its DOS image declares one 96-byte page fragment, a 32-byte header and a 64-byte initialized module. Every DOS fallback validation passes; the PE signature and optional header also pass when the short-header guard is bypassed by the discriminating control. LLVM reports `COFF-x86-64`, `.text` and `.rdata`; `file` reports a PE32+ x86-64 DLL. Native loader execution and deep-provider output were not tested. ## Restoration check For the generated short-stub DLL, production target resolution must recover `pe`, `x86_64` and `shared-library`, and public inspection must stop producing the DOS-specific `target_format_unsupported` rejection. In the isolated missing-launcher environment it should reach the same provider-availability result as the PE controls. The default-stub and four-paragraph controls must retain their PE classifications, and the unchanged original must continue to succeed through PE resource inspection. Preserve valid DOS-only classification for `dos-only.exe`, whose short-header module contains a nonzero value at `0x3c` but has no appended PE records, along with existing truncated-header and relocation-table safeguards. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: A bounded, independently generated class of PE input produces incorrect target format and architecture facts and a false public provider-format refusal. The exact proof establishes classification and admission consequences, without observing wrong decompilation, data loss, or an affected user's otherwise working deep-analysis workflow; P1 is not established by this evidence. - Workaround status: unverified - Workaround basis: PE resource inspection is a verified alternative on the unchanged artifact for that specific operation, but it does not restore shared target classification or Hopper admission. No complete workaround for the reported contract was verified. Editing the header creates a different artifact, and raw-binary selection does not restore PE semantics.
bsakaguchi 2026-10-15 18:27:40 +00:00
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#719
No description provided.