Skip to content

fix(managed): admit ReadyToRun images built for non-Windows hosts - #1985

Merged
morluto merged 1 commit into
morluto:mainfrom
JayOfTheKeyboard:fix/managed-readytorun-os-machine
Oct 12, 2026
Merged

morluto merged 1 commit into
morluto:mainfrom
JayOfTheKeyboard:fix/managed-readytorun-os-machine

Conversation

@JayOfTheKeyboard

Copy link
Copy Markdown
Contributor

Summary

REA rejects every .NET ReadyToRun assembly compiled for Linux, macOS or the BSDs with unsupported PE architecture. On a Linux .NET install that covers most of the shared framework, so inspect-artifact and all three managed inspection tools refuse files like System.Private.CoreLib.dll. No issue is linked: I found this by running REA over the assemblies on my machine, and the fix is small enough to explain here.

Problem and expected behavior

ReadyToRun images built for a non-Windows host do not store a plain COFF machine. The .NET toolchain XORs it with an OS constant, IMAGE_FILE_MACHINE_NATIVE_OS_OVERRIDE, defined in dotnet/runtime src/coreclr/inc/pedecoder.h:

Host Override x64 machine on disk
Linux 0x7B79 0xFD1D
Apple 0x4644 0xC020
FreeBSD 0xADC4 0x2BA0
OpenBSD 0xADC5 0x2BA1
NetBSD 0x1993 0x9FF7
SunOS 0x1992 0x9FF6

parsePeRecord in src/domain/binaryTarget.ts and architectureFor in src/dotnet/ManagedPeReader.ts each had their own switch over the five Windows machine values. Anything else was rejected before the CLI header was read.

Measured on Fedora 44 with the distribution's .NET 8.0.31 SDK, on main at 1d4c4eb0:

  • 424 of the 2,451 PE DLLs under /usr/lib64/dotnet have machine 0xFD1D. That is 89 of the 168 in shared/Microsoft.NETCore.App (including System.Private.CoreLib.dll), all 139 in shared/Microsoft.AspNetCore.App, and SDK assemblies such as Microsoft.Build.Framework.dll. The other 2,027 are AnyCPU IL (0x014C).
  • Out of a random sample of 40 PE files from the machine, inspect-artifact and inspect-managed-members each failed on 9 and inspect-managed-native-boundaries on 7. Every failure was this bug.
$ rea inspect-managed-artifact /usr/lib64/dotnet/shared/Microsoft.NETCore.App/8.0.31/System.Linq.Parallel.dll --format json
{"code":"target_unavailable", "message":"Cannot open artifact: unsupported PE architecture. ...",
 "details":{"reason":"unsupported PE architecture", ...}}

Expected: these are managed PE files, and REA should inspect their metadata the same way it does for a Windows build.

Change and scope

  • New src/domain/peMachine.ts owns COFF machine decoding. decodePeMachine tries the plain value first, then the six documented overrides, and returns { architecture, nativeOsOverride }. No override value decodes to a second valid machine, and none collides with a plain one.
  • parsePeRecord and ManagedPeReader both call it. Their duplicated switches are removed.
  • An override machine is accepted only when the image has a CLI directory, because only ReadyToRun output uses it. A native PE carrying 0xFD1D is still rejected, with the same error text as before.
  • ManagedPeLayout.machine already keeps the raw value, so pe.machine_hex still reports 0xfd1d.

Not changed, and yours to decide:

  • The target OS is not recorded as its own field. Adding one would change the Evidence contract. The raw machine stays in the managed inspection result, so the OS can be derived from it.
  • Each caller enforces the CLI-directory guard itself. parsePeRecord needs the architecture before it can find the data directories, so it cannot pass the managed flag into the decoder. Folding the guard into the decoder would mean reordering that function's header checks.
  • I did not try Ghidra or IDA on these images. Admission now reaches them as managed x86_64 PE files. On Windows the Ghidra provider already refuses managed images, and IDA uses its CLI loader.

Contract and boundary impact

  • Semantic owner and earliest changed stage: PE machine decoding at target admission (src/domain/peMachine.ts), used by parsePeRecord and readManagedPeLayout.
  • CLI and MCP/tool-catalog contract: none. No schemas change.
  • Provider, bridge, target-format, or platform compatibility: ReadyToRun PE files built for non-Windows hosts are now admitted as format: "pe", managed: true. Windows machine values behave exactly as before.
  • Evidence, artifact, provenance, or reconstruction contract: none. The raw machine is still reported.
  • Process execution, authorization, cleanup, or containment impact: none.
  • Generated metadata, package, or installation impact: none.

Evidence and regression coverage

  • Tests added or updated:

    • src/domain/binaryTarget.test.ts: x86_64 for all six hosts and arm64 for Linux and Apple, on a managed PE32+ header. Also checks that an override machine on a native image is still rejected.
    • tests/boundary/providers/managed/managedNativeOsMachine.test.ts: Linux x86, Linux ARM, Apple ARM and FreeBSD x86 ReadyToRun fixtures go through parseBinaryTarget and all three managed operations. Asserts pe.machine, pe.architecture and managed_architecture.
    • src/dotnet/ManagedArtifactFailures.test.ts: the managed reader rejects an override machine when the image has no CLI directory.
  • Base reproduction: on the parent commit, the new header and boundary cases fail with unsupported PE architecture. Mutations each turn at least one test red: removing either guard, skipping the override loop, or dropping any one of the six constants.

  • User-visible CLI/MCP output, with the fix, on System.Private.CoreLib.dll:

    subject.architecture                       x86_64
    normalized_result.pe.machine_hex           0xfd1d
    normalized_result.pe.architecture          x86_64
    normalized_result.classification.status    managed
    normalized_result.classification.managed_architecture  x86_64
    normalized_result.metadata.status          complete
    
  • Remaining proof gaps: no macOS, BSD or arm64 ReadyToRun binaries were run. Those cases are covered by fixtures only. Linux x64 was run against real files.

For evidence-bearing changes:

  • Observed, derived, and inferred claims remain distinguishable.
  • Artifact identity, source provenance, and failed attempts remain preserved.
  • Unsupported, incomplete, unavailable, or uncertain outcomes remain visible.

Validation performed

  • npm run test:focused -- over every src/domain and src/dotnet test, tests/boundary/providers/managed/*, tests/acceptance/managedCliOutput.test.ts and tests/boundary/mcp/managedMcp.test.ts: 147 files, 2,172 tests passed.
  • npm run check:fast: passed.
  • npm run verify:test-discovery: "Discovered all 882 test files exactly once."
  • Real files, Linux x64, built CLI: 30 randomly chosen 0xFD1D assemblies all succeed with inspect-managed-members. The 40-file random sample now passes inspect-artifact, inspect-managed-members and inspect-managed-native-boundaries with no failures from this cause.

Compatibility, safety, and release

  • Breaking changes or migration steps: none.
  • Real Hopper/Ghidra, browser, or OS coverage: Linux x64 (Fedora 44) only. No provider runs.
  • Package or release metadata impact: none.
  • Security, privacy, process, or containment review: parsing only. The new branch runs only after an existing plain-machine lookup fails, and it accepts nothing on native images.

Review checklist

  • This PR addresses a concrete problem or an agreed enhancement.
  • The PR has one focused outcome and the title follows type(scope): outcome.
  • Related issue is linked, or the reason for not linking one is stated above.
  • Tests cover changed observable behavior and meaningful failure paths.
  • Owning docs, contracts, and generated metadata are updated where needed.
  • User-visible CLI/MCP changes include representative output.
  • I checked the final diff for secrets, unrelated cleanup, and unsupported claims.

ReadyToRun images compiled for Linux, macOS or the BSDs store the COFF machine XORed with IMAGE_FILE_MACHINE_NATIVE_OS_OVERRIDE (dotnet/runtime src/coreclr/inc/pedecoder.h). Every framework assembly in a Linux .NET install has machine 0xFD1D, so target admission and the managed PE reader rejected them as an unsupported architecture.

Decode the PE machine once in src/domain/peMachine.ts, replacing the two duplicated switches, and accept an OS override only on images with a CLI directory.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-11T22:29:30.514526Z 60edd8e PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@morluto
morluto merged commit 0d11f71 into morluto:main Oct 12, 2026
2 checks passed
@github-actions github-actions Bot mentioned this pull request Oct 12, 2026
3 tasks done
@morluto

morluto commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

@JayOfTheKeyboard, thanks for bringing the ReadyToRun OS-override machine decoding into one shared PE interpretation path. Your measurements against real .NET installations show why this matters in practice, and the coverage across Linux, Apple, and BSD encodings is especially helpful. Welcome to REA!

@JayOfTheKeyboard

Copy link
Copy Markdown
Contributor Author

Thank you, happy to be here!

@morluto morluto added bug Something isn't working area: domain Domain logic, errors, and pure functions labels Oct 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: domain Domain logic, errors, and pure functions bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants