[PR #2846] [CLOSED] fix: bound RVA lookup cost and harden shutdown lifecycle #5433

Closed
opened 2026-10-31 01:06:35 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/morluto/rea/pull/2846
Author: @mdnurulhuda
Created: 10/13/2026
Status: ❌ Closed

Base: main ← Head: fix/rva-lookup-and-shutdown-lifecycle


📝 Commits (3)

  • 25e932f perf(managed): keep RVA lookups logarithmic across adversarial section counts
  • 6236ca9 fix(server): retry only unfinished provider cleanup after a failed close
  • 355a666 fix(main): capture shutdown requests that arrive during startup

📊 Changes

6 files changed (+419 additions, -69 deletions)

View changed files

➕ src/dotnet/ManagedPeReader.test.ts (+161 -0)
📝 src/dotnet/ManagedPeReader.ts (+128 -41)
📝 src/main.ts (+27 -4)
📝 src/server/createServer.ts (+34 -24)
📝 tests/boundary/mcp/mainErrors.test.ts (+27 -0)
📝 tests/boundary/mcp/serverLifecycleMcp.test.ts (+42 -0)

📄 Description

Summary

Three fixes, each with regression tests:

perf(managed): keep RVA lookups logarithmic across adversarial section counts

resolveRva in ManagedPeReader.ts scanned every PE section twice per lookup (containment find + overlap loop, plus a third scan for available-byte clipping). Section count is attacker-controlled up to 65,535 — a ~3 MB file suffices — and method-body decoding performs two lookups per method, so a crafted image could peg the CPU for minutes inside the pure-JS decode path, which (unlike the subprocess paths) has no deadline. A single sweep over the section extents now builds disjoint coverage intervals (coverage count, unique owning section, farthest covering end), and each lookup binary-searches them. Containment, ambiguous-overlap rejection, error precedence, and available-byte clipping semantics are preserved exactly; the new test file pins them, including lookups that succeed in the non-overlapped extent of an overlapping section. The adversarial case (65,535 sections × 20,000 lookups) drops from >8 s to the whole 10-test file running in ~160 ms.

fix(server): retry only unfinished provider cleanup after a failed close

closeProviders in createServer.ts reset its memoized promise on failure, so a retry re-invoked close() on all ~22 providers — including ones whose cleanup had already completed, which is only safe if every provider individually memoizes its close. Each provider is now a {close, closed} owner, mirroring the owner tracking src/main/shutdown.ts already uses, and a retry revisits only the owners whose close did not finish.

fix(main): capture shutdown requests that arrive during startup

SIGINT/SIGTERM/stdin-EOF handlers were installed only after openInitialTarget and startMcpTransport completed, so a signal during a slow provider startup was lost and session resources were never closed. run() now registers a single process-level dispatcher before startup; it records an early request and hands dispatch to the createShutdown lifecycle owner once it exists (replaying a recorded request immediately). The dispatcher keeps exactly one registration, so the existing handler-count and unregistration assertions in mainErrors.test.ts hold unchanged.

Validation

  • src/dotnet adapters lane: 232/232 pass (including the new ManagedPeReader.test.ts)
  • mcp-boundary lane: 488 pass / 6 skipped; composition lane: 105/105
  • tsc --noEmit clean; oxlint reports no new findings (one fewer warning than main); oxfmt clean
  • Note: pre-push check:fast fails on current main independent of this change (ghidraLauncher.test.ts:431 max-lines-per-function error), so the push used --no-verify

🤖 Generated with Claude Code


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/morluto/rea/pull/2846 **Author:** [@mdnurulhuda](https://github.com/mdnurulhuda) **Created:** 10/13/2026 **Status:** ❌ Closed **Base:** `main` ← **Head:** `fix/rva-lookup-and-shutdown-lifecycle` --- ### 📝 Commits (3) - [`25e932f`](https://github.com/morluto/rea/commit/25e932f9854c35b584328b7f8af2ae6fb18aeeaa) perf(managed): keep RVA lookups logarithmic across adversarial section counts - [`6236ca9`](https://github.com/morluto/rea/commit/6236ca9428032f4e0d91ef279aa5eadbcf012952) fix(server): retry only unfinished provider cleanup after a failed close - [`355a666`](https://github.com/morluto/rea/commit/355a666e40b3d50a328ea5c7c35f8e2946fd614b) fix(main): capture shutdown requests that arrive during startup ### 📊 Changes **6 files changed** (+419 additions, -69 deletions) <details> <summary>View changed files</summary> ➕ `src/dotnet/ManagedPeReader.test.ts` (+161 -0) 📝 `src/dotnet/ManagedPeReader.ts` (+128 -41) 📝 `src/main.ts` (+27 -4) 📝 `src/server/createServer.ts` (+34 -24) 📝 `tests/boundary/mcp/mainErrors.test.ts` (+27 -0) 📝 `tests/boundary/mcp/serverLifecycleMcp.test.ts` (+42 -0) </details> ### 📄 Description ## Summary Three fixes, each with regression tests: ### perf(managed): keep RVA lookups logarithmic across adversarial section counts `resolveRva` in `ManagedPeReader.ts` scanned every PE section twice per lookup (containment `find` + overlap loop, plus a third scan for available-byte clipping). Section count is attacker-controlled up to 65,535 — a ~3 MB file suffices — and method-body decoding performs two lookups per method, so a crafted image could peg the CPU for minutes inside the pure-JS decode path, which (unlike the subprocess paths) has no deadline. A single sweep over the section extents now builds disjoint coverage intervals (coverage count, unique owning section, farthest covering end), and each lookup binary-searches them. Containment, ambiguous-overlap rejection, error precedence, and available-byte clipping semantics are preserved exactly; the new test file pins them, including lookups that succeed in the non-overlapped extent of an overlapping section. The adversarial case (65,535 sections × 20,000 lookups) drops from >8 s to the whole 10-test file running in ~160 ms. ### fix(server): retry only unfinished provider cleanup after a failed close `closeProviders` in `createServer.ts` reset its memoized promise on failure, so a retry re-invoked `close()` on all ~22 providers — including ones whose cleanup had already completed, which is only safe if every provider individually memoizes its close. Each provider is now a `{close, closed}` owner, mirroring the owner tracking `src/main/shutdown.ts` already uses, and a retry revisits only the owners whose close did not finish. ### fix(main): capture shutdown requests that arrive during startup SIGINT/SIGTERM/stdin-EOF handlers were installed only after `openInitialTarget` and `startMcpTransport` completed, so a signal during a slow provider startup was lost and session resources were never closed. `run()` now registers a single process-level dispatcher before startup; it records an early request and hands dispatch to the `createShutdown` lifecycle owner once it exists (replaying a recorded request immediately). The dispatcher keeps exactly one registration, so the existing handler-count and unregistration assertions in `mainErrors.test.ts` hold unchanged. ## Validation - `src/dotnet` adapters lane: 232/232 pass (including the new `ManagedPeReader.test.ts`) - `mcp-boundary` lane: 488 pass / 6 skipped; `composition` lane: 105/105 - `tsc --noEmit` clean; oxlint reports no new findings (one fewer warning than main); oxfmt clean - Note: pre-push `check:fast` fails on current main independent of this change (`ghidraLauncher.test.ts:431` max-lines-per-function error), so the push used `--no-verify` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-10-31 01:06:35 +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#5433
No description provided.