[GH-ISSUE #2926] [Bug] Delphi/Borland SEH handler blocks are cut from Ghidra function bodies without notice #1031

Closed
opened 2026-10-15 18:30:29 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @matbrgz on GitHub (Oct 13, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/2926

Before submitting

  • I searched open and closed issues (SEH, exception handler, try except, handler block, unwind). Closest: #2326 (Itanium C++ landing pads cut from Ghidra bodies, closed). This one is the Win32 Borland/Delphi SEH frame case, which has no exception table at all.
  • Contribution scope understood. No credentials or private data.

Area

Native analysis or decompilation

Current limitation

Delphi (and C++Builder) compile try … except / try … finally as inline SEH frames:

push   ebp
push   0x5355c3              ; handler address
push   dword ptr fs:[eax]
mov    dword ptr fs:[eax], esp
...                          ; protected block
jmp    0x404350              ; at the handler: _HandleAnyException / _HandleFinally

The handler code (0x5355c3…0x53566c below) is reached only through the SEH chain, so Ghidra auto-analysis does not include it in the function body and REA reports it as outside any procedure. In this target the handler holds the user-visible error branch ("Houve erro ao processar paciente! CEP não encontrado! …", "…Município não encontrado!…") and calls to the message routine. Those are missing from callees, referenced_strings and trace_feature's containing-procedure resolution, and nothing tells the reader:

Query (REA 6.4.0, Ghidra 12.1.4) Result
procedure_info 0x535300 body ranges 0x535300–0x5355c2, 0x53566d–0x5356be (non-contiguous; handler gap 0x5355c3–0x53566c); basicblock_count 7; no limitation about the gap
resolve_containing_procedure 0x5355eb (push of the error string) found: false, reason: not_in_procedure
resolve_containing_procedure 0x5353a4 (protected block, same function) FUN_00535300

The same shape appears throughout the binary: every Delphi event handler with a try block has one or more orphaned handler regions.

Environment: macOS 27.0.1 arm64, Node 24.13.0, rea-agents@6.4.0 (local, MCP over stdio), Ghidra 12.1.4 portable, JDK 21; REA_GHIDRA_SEED_FILE with 1,610 Delphi method names (#2811 context).

Proposed outcome

  • Recognize the Borland SEH prologue (push imm32; push fs:[reg]; mov fs:[reg], esp, also the xor eax,eax; push ebp; push imm32 variants) and treat imm32 as an exception-handler entry of the enclosing function. Either seed it as code and add it to the body (when its fall-through/jumps return into the function or end at the RTL handler stubs), or keep it separate but report it in procedure_info / analyze_function as an explicit handler region with its callees and strings.
  • Like the #2326 fix, never report a function's callees/strings as complete while a known handler region is excluded.
  • Optional: name the RTL targets (_HandleAnyException, _HandleFinally, _HandleOnException) when the caller supplies or REA recognizes them, so the handler kind (except/finally) is visible.

Minimal public repro

  • ftp://arpoador.datasus.gov.br/siasus/autorizador/AUTORIZADOR_VER6630.exe (sha256 0c79af43ce52a9317591e081ebfc9f29f70d5575033b2eb9e177d0828a634c44) → innoextract -e → app/AUTORIZADOR.exe (sha256 1398d55a71a9e4ac04f7a12f5f1bec9d0b4343fbd1c0f8635938d261f18fa363, PE32, Delphi 7).
  • open_binary (ghidra) → procedure_info 0x535300, resolve_containing_procedure 0x5355eb. The push 0x5355c3 that registers the handler is at 0x5353ac.

Suggested approach

A small Ghidra pre/post script in the bridge, or an REA-side pass over the decoded instructions of each function, looking for the SEH registration pattern and returning the handler addresses. The data needed is in the instruction stream; no exception directory is involved.

Originally created by @matbrgz on GitHub (Oct 13, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/2926 ### Before submitting - [x] I searched open and closed issues (`SEH`, `exception handler`, `try except`, `handler block`, `unwind`). Closest: #2326 (Itanium C++ landing pads cut from Ghidra bodies, closed). This one is the **Win32 Borland/Delphi SEH frame** case, which has no exception table at all. - [x] Contribution scope understood. No credentials or private data. ### Area Native analysis or decompilation ### Current limitation Delphi (and C++Builder) compile `try … except` / `try … finally` as inline SEH frames: ```asm push ebp push 0x5355c3 ; handler address push dword ptr fs:[eax] mov dword ptr fs:[eax], esp ... ; protected block jmp 0x404350 ; at the handler: _HandleAnyException / _HandleFinally ``` The handler code (`0x5355c3…0x53566c` below) is reached only through the SEH chain, so Ghidra auto-analysis does not include it in the function body and REA reports it as outside any procedure. In this target the handler holds the user-visible error branch (`"Houve erro ao processar paciente! CEP não encontrado! …"`, `"…Município não encontrado!…"`) and calls to the message routine. Those are missing from callees, `referenced_strings` and `trace_feature`'s containing-procedure resolution, and nothing tells the reader: | Query (REA 6.4.0, Ghidra 12.1.4) | Result | |---|---| | `procedure_info 0x535300` | body ranges `0x535300–0x5355c2`, `0x53566d–0x5356be` (non-contiguous; handler gap `0x5355c3–0x53566c`); `basicblock_count 7`; no limitation about the gap | | `resolve_containing_procedure 0x5355eb` (push of the error string) | `found: false`, `reason: not_in_procedure` | | `resolve_containing_procedure 0x5353a4` (protected block, same function) | `FUN_00535300` | The same shape appears throughout the binary: every Delphi event handler with a `try` block has one or more orphaned handler regions. Environment: macOS 27.0.1 arm64, Node 24.13.0, `rea-agents@6.4.0` (local, MCP over stdio), Ghidra 12.1.4 portable, JDK 21; `REA_GHIDRA_SEED_FILE` with 1,610 Delphi method names (#2811 context). ### Proposed outcome - Recognize the Borland SEH prologue (`push imm32; push fs:[reg]; mov fs:[reg], esp`, also the `xor eax,eax; push ebp; push imm32` variants) and treat `imm32` as an exception-handler entry of the enclosing function. Either seed it as code and add it to the body (when its fall-through/jumps return into the function or end at the RTL handler stubs), or keep it separate but **report it** in `procedure_info` / `analyze_function` as an explicit handler region with its callees and strings. - Like the #2326 fix, never report a function's callees/strings as complete while a known handler region is excluded. - Optional: name the RTL targets (`_HandleAnyException`, `_HandleFinally`, `_HandleOnException`) when the caller supplies or REA recognizes them, so the handler kind (except/finally) is visible. ### Minimal public repro - `ftp://arpoador.datasus.gov.br/siasus/autorizador/AUTORIZADOR_VER6630.exe` (sha256 `0c79af43ce52a9317591e081ebfc9f29f70d5575033b2eb9e177d0828a634c44`) → `innoextract -e` → `app/AUTORIZADOR.exe` (sha256 `1398d55a71a9e4ac04f7a12f5f1bec9d0b4343fbd1c0f8635938d261f18fa363`, PE32, Delphi 7). - `open_binary` (ghidra) → `procedure_info 0x535300`, `resolve_containing_procedure 0x5355eb`. The `push 0x5355c3` that registers the handler is at `0x5353ac`. ### Suggested approach A small Ghidra pre/post script in the bridge, or an REA-side pass over the decoded instructions of each function, looking for the SEH registration pattern and returning the handler addresses. The data needed is in the instruction stream; no exception directory is involved.
bsakaguchi 2026-10-15 18:30:29 +00:00
Author
Owner

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

Fixed the remaining reference-path warning loss in 9a44fea34: procedure_references now uses the existing function-observation envelope, so both directions preserve the provider's boundary caveats in Evidence. One shared scanner owns terminal-call and SEH detection. Native traces and composed workflows retain source attribution, and semantic profile changes prevent older snapshots from bypassing these corrections.

Actual local verification used Ghidra 12.1.4, Temurin 21, Node 24.18.0 and the source-generated x86 PE fixture, through public stdio MCP with SDK 2.3.1. Baseline carried the SEH warning in procedure info but lost it in both reference directions. Fixed code preserved it in info, references and the dossier; retained the pushed handler address as a data reference; and kept handler 0x4010c0 outside the observed registration procedure body. The ordinary control had no SEH warning. The final merged run passed 38 reference/function observations, preserved the input digest, and closed with empty owned runtime/process lineage.

Handler body ownership and runtime behavior remain unknown; this fix supplies the explicit static limitation allowed by the report rather than claiming recovered handler code. Complete exact-head qualification: https://github.com/morluto/rea/actions/runs/38652688611. Closing as resolved within that scope.

<!-- gh-comment-id:6180766256 --> @morluto commented on GitHub (Oct 14, 2026): Fixed the remaining reference-path warning loss in `9a44fea34`: `procedure_references` now uses the existing function-observation envelope, so both directions preserve the provider's boundary caveats in Evidence. One shared scanner owns terminal-call and SEH detection. Native traces and composed workflows retain source attribution, and semantic profile changes prevent older snapshots from bypassing these corrections. Actual local verification used Ghidra 12.1.4, Temurin 21, Node 24.18.0 and the source-generated x86 PE fixture, through public stdio MCP with SDK 2.3.1. Baseline carried the SEH warning in procedure info but lost it in both reference directions. Fixed code preserved it in info, references and the dossier; retained the pushed handler address as a data reference; and kept handler `0x4010c0` outside the observed registration procedure body. The ordinary control had no SEH warning. The final merged run passed 38 reference/function observations, preserved the input digest, and closed with empty owned runtime/process lineage. Handler body ownership and runtime behavior remain unknown; this fix supplies the explicit static limitation allowed by the report rather than claiming recovered handler code. Complete exact-head qualification: https://github.com/morluto/rea/actions/runs/38652688611. Closing as resolved within that scope.
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#1031
No description provided.