Skip to content

[DRAFT] Load r2r binary#126662

Draft
davidwrighton wants to merge 17 commits intodotnet:mainfrom
davidwrighton:LoadR2RBinary
Draft

[DRAFT] Load r2r binary#126662
davidwrighton wants to merge 17 commits intodotnet:mainfrom
davidwrighton:LoadR2RBinary

Conversation

@davidwrighton
Copy link
Copy Markdown
Member

Initial change that actually makes it possible to load and execute an R2R method on WebAssembly.

Needed before merge

  • Review, and determining if some of the work should be split off as separate PRs
  • The logic around PortableEntryPoints and the PrefersInterpreter path needs additional thought
  • Write PR description
  • Needs to work better, currently only 1 minimal example works, but doing more is not yet working.

… the point of jumping into the r2r code, and then it fails with a function type mismatch since portable entrypoints are not quite correctly hooked up
…entrypoints

- This includes some initial work where there is a predefined set of new thunks generated into the coreclr codebase. I expect that we'll build some sort of hardcoded list here for invokes to/from R2R code, and then have R2R supplement the list with extra cases.
- Adjust PortableEntryPoint calling convention to match the WASM version
…into locations in the R2R file work

- Notably, a new level of indirection is needed
 - This is used to make loads/stores more efficient. loads/stores have an offset baked into them, so instead of
 global.get 1
 i32.const <reloc>
 add
 i32.load

 We can do
 global.get 1
 i32.load offset=<reloc>

 Overall this saves 3 bytes of space per load from an RVA relative address
…rator to match the ArgIterator behavior in the runtime

- Notably, the ArgIterator in the runtime follows interpreter semantics, so this logic adjusts the way we stash memory into the stack to match the interpreter behavior
- And the WasmImportThunk code now utilizes the ArgIterator to do its layout.
- We may need to reconcile behavior differences between the interpreter/RyuJIT abi, but they are fairly close, so this is probably reasonable to do for now. Alternatively, we can remove the whole GCRefMap infra in favor of a small bit of conservative reporting, and a completely different format.
@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @agocke
See info in area-owners.md if you want to be subscribed.


#elif defined(TARGET_WASM)

#define FCDECL0(rettype, funcname) rettype F_CALL_CONV funcname(uintptr_t stack_pointer, int32_t portableEntryPointContext)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will need #122988 (comment) to deal with the stack_pointer

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I've implemented this for the calls into the interpreter stubs now, but I was holding off on finishing this up for FCalls until my next PR, as its not really needed for initial bring up, and the PR is already way too large.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This draft PR introduces the initial plumbing needed to load Webcil R2R artifacts on WebAssembly and execute R2R methods via portable entrypoints, including new thunking paths and relocation support across the runtime, JIT, and AOT toolchain.

Changes:

  • Add PortableEntryPoint-aware thunk generation and call paths for wasm (including R2R→interpreter dispatch support).
  • Extend Webcil decoding / PE layout plumbing to surface ReadyToRun headers and apply relocations.
  • Introduce a new wasm relocation kind and update the wasm object writer and AOT thunk emission to use it.

Reviewed changes

Copilot reviewed 37 out of 37 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/tasks/WasmAppBuilder/coreclr/ManagedToNativeGenerator.cs Adds pregenerated signatures and adjusts cache file encoding.
src/tasks/WasmAppBuilder/coreclr/InterpToNativeGenerator.cs Generates PortableEntryPoint-aware call thunks (*_PE) and signature handling for p suffix.
src/coreclr/vm/wasm/helpers.cpp Adds wasm PortableEntryPoint→interpreter thunk table and lookup/cache; implements DelayLoad_MethodCall for wasm.
src/coreclr/vm/wasm/callhelpers-interp-to-managed.cpp Updates generated thunk table with *_PE entries and PortableEntryPoint-aware call helpers.
src/coreclr/vm/readytoruninfo.cpp Wires R2R entrypoint resolution through PortableEntryPoint temporary entrypoints.
src/coreclr/vm/prestub.cpp Adds PortableEntryPoint-based interpreted execution entry and lazy interpreter code init path.
src/coreclr/vm/precode_portable.hpp Adds “prefers interpreter” flag plumbing on PortableEntryPoint.
src/coreclr/vm/precode_portable.cpp Integrates “prefers interpreter” behavior into native-entrypoint checks and SetActualCode upgrade.
src/coreclr/vm/peimagelayout.inl Routes more queries through the active decoder; adds Webcil table-base offset accessor; enables Webcil R2R header queries.
src/coreclr/vm/peimagelayout.h Removes cached Webcil table-base offset storage and exposes decoder-driven accessor.
src/coreclr/vm/peimagelayout.cpp Applies relocations for Webcil and adds wasm-specific relocation parsing handling.
src/coreclr/vm/method.cpp Initializes PortableEntryPoint with optional R2R→interpreter stub and sets “prefer interpreter” behavior.
src/coreclr/vm/jithelpers.cpp Updates wasm helper wrappers for new FCALL signatures.
src/coreclr/vm/interpexec.cpp Adjusts helper call ABI for PortableEntryPoints (stack pointer + context) and prefers interpreter entrypoints where flagged.
src/coreclr/vm/ilstubcache.cpp Moves temporary entrypoint assignment to after method flags are set for IL stubs.
src/coreclr/vm/gccover.cpp Updates GCFrame usage to pass GC flags instead of a bool.
src/coreclr/vm/frames.h Changes GCFrame ctor signature and introduces conservative GC protect macro for interpreter builds.
src/coreclr/vm/frames.cpp Implements GCFrame flag-based scanning instead of the prior bool-based interior tracking.
src/coreclr/vm/fcall.h Defines wasm-specific FCALL/HCALL signatures including stack pointer + PortableEntryPoint context.
src/coreclr/vm/cgensys.h Aligns wasm DelayLoad_MethodCall declaration with C++ implementation signature.
src/coreclr/utilcode/webcildecoder.cpp Adds ReadyToRun header discovery and manifest metadata support for Webcil.
src/coreclr/tools/Common/JitInterface/CorInfoTypes.cs Adds CorInfoReloc value for relative wasm linear-memory loads/stores.
src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs Maps the new CorInfoReloc value to the compiler relocation type.
src/coreclr/tools/Common/Compiler/ObjectWriter/WasmObjectWriter.cs Implements relocation fixup handling for relative wasm linear-memory address relocations.
src/coreclr/tools/Common/Compiler/ObjectWriter/WasmInstructions.cs Adds relocation-capable memory-arg encoding and new LoadWithRVAOffset helper.
src/coreclr/tools/Common/Compiler/DependencyAnalysis/Relocation.cs Adds the new relocation kind and supports read/write/size operations for it.
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunk.cs Reworks delay-load helper thunk emission to use ArgIterator/TransitionBlock and new RVA-relative load encoding.
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/TransitionBlock.cs Updates wasm32 transition block sizing/alignment assumptions.
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/RuntimeFunctionsTableNode.cs Emits wasm-specific runtime function “BeginAddress” entries as table indices.
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/GCRefMapBuilder.cs Refactors ArgIterator construction into reusable helper used by wasm import thunk emission.
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/ArgIterator.cs Adjusts wasm32 argument sizing/alignment and stack offset computation.
src/coreclr/runtime/portable/AllocFast.cpp Updates an FCALL callsite to pass wasm stack/context parameters.
src/coreclr/jit/morph.cpp Adds wasm-specific extra indirection for R2R entrypoints treated as PortableEntryPoints.
src/coreclr/jit/lower.cpp Adds wasm-specific extra indirection when materializing entrypoints for calls.
src/coreclr/jit/codegenwasm.cpp Adjusts helper call emission to load through PortableEntryPoint indirections.
src/coreclr/inc/webcildecoder.h Exposes Webcil R2R header / metadata APIs and stores ReadyToRun header cache state.
src/coreclr/inc/corinfo.h Adds CorInfoReloc value for relative wasm linear-memory address relocations.
Comments suppressed due to low confidence (1)

src/coreclr/utilcode/webcildecoder.cpp:92

  • WebcilDecoder::Reset() does not reset the newly added ReadyToRun-related cache fields (m_hasNoReadyToRunHeader and m_pReadyToRunHeader). If a WebcilDecoder instance is reused, stale cached state could cause HasReadyToRunHeader()/GetReadyToRunHeader() to return incorrect results.

Reset these fields in Reset() (and consider also reinitializing them in Init()) to keep decoder state consistent across uses.

void WebcilDecoder::Reset()
{
    LIMITED_METHOD_CONTRACT;
    m_base = 0;
    m_size = 0;
    m_hasContents = false;
    m_pHeader = NULL;
    m_sections = NULL;
    m_pCorHeader = NULL;
    m_relocated = FALSE;
}

Comment on lines +2086 to +2092
if (targetIp == NULL)
{
GCPROTECT_BEGINCONSERVATIVE_ARRAY(args, (UINT)(argsSize/sizeof(OBJECTREF)));
GCX_PREEMP();
(void)pMethod->DoPrestub(NULL /* MethodTable */, CallerGCMode::Coop);
targetIp = pMethod->GetInterpreterCode();
GCPROTECT_END();
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GCPROTECT_BEGINCONSERVATIVE_ARRAY(args, ...) is protecting the local pointer variable args (i.e., &args) rather than the argument buffer it points to. This means the GC frame will conservatively scan unrelated stack memory adjacent to the pointer variable, not the actual argument slots, which can lead to incorrect root reporting and potential GC corruption/instability.

Instead, create a GCFrame over the actual buffer address (e.g., GCFrame gcFrame((OBJECTREF*)args, count, GC_CALL_INTERIOR | GC_CALL_PINNED)), or introduce a dedicated helper/macro for protecting a dynamic pointer+count buffer rather than using a macro that takes an array variable.

Copilot uses AI. Check for mistakes.
Comment on lines 279 to 287
COUNT_T dirPos = 0;
#ifdef TARGET_WASM
// WASM will padd out the reloc size to the next 16 byte boundary, so we need to validate we can safely read the IMAGE_BASE_RELOCATION struct before processing each entry.
while (dirPos < (dirSize - sizeof(IMAGE_BASE_RELOCATION)))
#else
while (dirPos < dirSize)
#endif
{
PIMAGE_BASE_RELOCATION r = (PIMAGE_BASE_RELOCATION)(dir + dirPos);
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On TARGET_WASM, the loop condition dirPos < (dirSize - sizeof(IMAGE_BASE_RELOCATION)) can underflow when dirSize < sizeof(IMAGE_BASE_RELOCATION) (since COUNT_T is unsigned), leading to an out-of-bounds read of IMAGE_BASE_RELOCATION.

Consider rewriting the condition as dirPos + sizeof(IMAGE_BASE_RELOCATION) <= dirSize (or guarding with if (dirSize < sizeof(...)) return;) to avoid unsigned underflow.

Copilot uses AI. Check for mistakes.
Comment on lines +91 to +103
void SetPrefersInterpreterEntryPoint()
{
LIMITED_METHOD_CONTRACT;
_ASSERTE(IsValid());
_flags = (_flags | kPrefersInterpreterEntryPoint); // TODO, use interlock operation here
}

void ClearPrefersInterpreterEntryPoint()
{
LIMITED_METHOD_CONTRACT;
_ASSERTE(IsValid());
_flags = (_flags & ~kPrefersInterpreterEntryPoint); // TODO, use interlock operation here
}
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SetPrefersInterpreterEntryPoint / ClearPrefersInterpreterEntryPoint update _flags via plain read/modify/write (_flags = _flags | ... / _flags = _flags & ...). Since _flags is also used for other state (e.g., UnmanagedCallersOnly flags) and can be accessed concurrently, this can drop updates or tear state.

Please switch these to interlocked bit operations (e.g., InterlockedOr / InterlockedAnd) similar to the existing SetFlags helper used elsewhere in this file.

Copilot uses AI. Check for mistakes.
WRAPPER_NO_CONTRACT;
return RhpNewArrayFast(pMT, size);
// Since we know the implementation of RhpNewArrayFast is native we don't need to actually
// pass a stack poitner or portable entry point context, so we can just specify 0 for those parameters.
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in comment: stack poitner should be stack pointer. (Also consider whether the comment should mention the portableEntryPointContext parameter name consistently.)

Suggested change
// pass a stack poitner or portable entry point context, so we can just specify 0 for those parameters.
// pass a stack pointer or portableEntryPointContext, so we can just specify 0 for those parameters.

Copilot uses AI. Check for mistakes.
Comment on lines +88 to +91
// All Signatures required for FCalls should be in this list, but we may also want to include pregenerated signatures for commonly used shapes used by
// R2R code to reduce duplication in generated R2R binaries. The signatures should be in the form of a string where the first character represents the
// return type and the following characters represent the argument types. The type characters should match those used by the SignatureMapper.CharToNativeType method.
string [] pregeneratedInterpreterToNativeSignatures =
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says the type characters in these pregenerated signatures should match SignatureMapper.CharToNativeType, but the added signatures include a 'p' suffix, which is not a valid type character for SignatureMapper and is handled specially elsewhere.

Please update the comment to document 'p' as a portable-entrypoint marker (hidden parameter) so future additions don't follow the comment literally and break generation.

Copilot uses AI. Check for mistakes.
Comment on lines +91 to +92
string [] pregeneratedInterpreterToNativeSignatures =
{
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor style inconsistency: this file otherwise uses string[] but this new declaration uses string []. Consider switching to string[] to match the surrounding conventions.

Copilot uses AI. Check for mistakes.

WASM_TABLE_INDEX_I32 = 0x207, // Wasm: a table index encoded as a 4-byte uint32, e.g. for storing the "address" of a function into linear memory
WASM_TABLE_INDEX_I64 = 0x208, // Wasm: a table index encoded as a 8-byte uint64, e.g. for storing the "address" of a function into linear memory
WASM_MEMORY_ADDR_REL_LEB = 0x209, // Wasm: a relative linear memory index encoded as a 5-byte varint32. Used as the immediate argument of a load or store instruction,
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The enum comment says WASM_MEMORY_ADDR_REL_LEB is a "varint32", but the relocation is encoded/decoded using ULEB128 (WritePaddedULEB128/ReadULEB128) and is described as varuint32 elsewhere.

Please fix the comment to avoid implying signed encoding.

Suggested change
WASM_MEMORY_ADDR_REL_LEB = 0x209, // Wasm: a relative linear memory index encoded as a 5-byte varint32. Used as the immediate argument of a load or store instruction,
WASM_MEMORY_ADDR_REL_LEB = 0x209, // Wasm: a relative linear memory index encoded as a 5-byte varuint32. Used as the immediate argument of a load or store instruction,

Copilot uses AI. Check for mistakes.
// e.g. in R2R scenarios as an offset from __image_base
WASM_TYPE_INDEX_LEB, // Wasm: a type index encoded as a 5-byte varuint32, e.g. the type immediate in a call_indirect.
WASM_GLOBAL_INDEX_LEB, // Wasm: a global index encoded as a 5-byte varuint32, e.g. the index immediate in a get_global.
WASM_MEMORY_ADDR_REL_LEB, // Wasm: a relative linear memory index encoded as a 5-byte varint32. Used as the immediate argument of a load or store instruction,
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The enum comment describes WASM_MEMORY_ADDR_REL_LEB as a "varint32", but the writer uses ULEB128 for this relocation type. Please update the comment to varuint32 (or clarify why it is signed) to avoid misleading JIT/objwriter implementers.

Suggested change
WASM_MEMORY_ADDR_REL_LEB, // Wasm: a relative linear memory index encoded as a 5-byte varint32. Used as the immediate argument of a load or store instruction,
WASM_MEMORY_ADDR_REL_LEB, // Wasm: a relative linear memory index encoded as a 5-byte varuint32. Used as the immediate argument of a load or store instruction,

Copilot uses AI. Check for mistakes.
InterpByteCodeStart* targetIp = pMethod->GetInterpreterCode();
if (targetIp == NULL)
{
GCPROTECT_BEGINCONSERVATIVE_ARRAY(args, (UINT)(argsSize/sizeof(OBJECTREF)));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not reliable. CoreCLR is not compatible with general conservative reporting - it will cause dynamic methods and collectible assemblies to crash intermittently.

We can only conservatively double-report (I believe it is what the interpreter does). We cannot conservatively report random pointers.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jkotas, I disagree. I'm pretty sure this IS safe, but it may not be sufficient. Notably, calls to LCG/collectible code may ALSO need to report a loaderallocator/LCG method. I'd prefer to add to that detail, than try to figure out how to report exactly here.

Copy link
Copy Markdown
Member

@jkotas jkotas Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ALSO need to report a loaderallocator/LCG method

That's not the only problem. Conservative reporting like this causes dead objects to become alive. Collectible code is not robust against that. Sure, it can be fixed with more work but I am not sure whether we want to do it.

Example of sequence of events that causes problem:

  1. The loader allocator is identified as dead and it is scheduled for cleanup
  2. Some conservatively reported root resurrects an object from the loader allocator that is scheduled for cleanup
  3. The loader allocator is cleaned up
  4. The resurrected object is still on the GC heap, but points to a MethodTable that does not exist anymore
  5. The GC crashes accessing the non-existing MethodTable when it tries to walk the heap

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, conservative reporting has been a source of unpredictable/flaky behavior in Mono, sometimes even leading to OOMs, for example dotnet/aspnetcore#23451. It would be nice to avoid that on CoreCLR.

than try to figure out how to report exactly here.

I think it should be cheap and straightforward to do the proper reporting once the transition thunks are produced by crossgen.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't agree. The issues with conservative gc resurrecting dead objects were fixed some years ago. (It was one of Chris Ahna's more subtle finds.) The only issues remaining are keeping things method frames live, and the general problems of having a conservative gc which can lead to ooms, etc. given the current expectation around around codegen, I don't see it as useful to avoid conservative gc here at this time.

Copy link
Copy Markdown
Member

@jkotas jkotas Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So what prevents the sequence of events in my comment above from crashing the GC?

It was one of Chris Ahna's more subtle finds.

That was in .NET Native that did not support collectible types (if you mean the same set of problems that I have in mind)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I am fine with using conservative GC as a bring up aid, as long as it is wrapped with TODOs that link to issue.)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jkotas That find, which Chris found on .NET Native, was also very interesting as my analysis at the time indicated that it actually addressed a number of issues that had been coming up in collectible assemblies for years but we had never managed to fix/explain to my satisfaction. Since then I haven't seen collectible assemblies fail in a way which would have been triggered by using conservative GC. Effectively, collectible assemblies had been relying on a property of garbage collection which should have always been true around marking dead objects as dead reliably, but had a very obscure bug in the GC, and when Chris fixed the issue in object liveness computation, this fundamentally fixed the problems that collectible assemblies had around walking over objects that had notionally been collected, but had not yet quite properly been considered free in the GC. The issue had been explained away for collectible assemblies as a detail that the GC sometimes walked over dead object memory that might not yet been marked as a Free region.

Effectively, Chris's fix was to the ability of the GC to determine if an object was live, and that is the fundamental
issue that collectible assemblies had had for years. It so happens that conservative GC has exactly the same requirements for correctness, but the failure mode with conservative GC was rather easier to understand (and frankly, Chris was never brought in to deal with the collectible types problems, and he is so intensely good at tracking these things down.)

Also, the current model for GC reporting on the interpreter in coreclr has a requirement on conservative GC, not just safe double-reporting. This is due to some rather awkward details around the CALL, and various call helpers instructions work. To fix this completely would require splitting the existing CALL/CALLVIRT/CALL_HELPER instructions into several separate instructions, and introducing a variety of different GC reporting rules at each. As noted when we put it together, it's a limited time with the conservative report on a given memory address, but it IS true conservative reporting where sometimes wild pointers are fed into the GC.

Copy link
Copy Markdown
Member Author

@davidwrighton davidwrighton Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regardless, we could use the PrestubMethodFrame here, which would be fairly reasonable. I'll look into it.

// All Signatures required for FCalls should be in this list, but we may also want to include pregenerated signatures for commonly used shapes used by
// R2R code to reduce duplication in generated R2R binaries. The signatures should be in the form of a string where the first character represents the
// return type and the following characters represent the argument types. The type characters should match those used by the SignatureMapper.CharToNativeType method.
string [] pregeneratedInterpreterToNativeSignatures =
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InterpreterToR2RSignatures

@jkotas jkotas requested a review from radekdoulik April 8, 2026 21:54
adamperlin added a commit that referenced this pull request Apr 10, 2026
This PR contains one of @davidwrighton's commits extracted from #126662
to reduce the PR size and complexity. It adds a relocation type that is
essentially `R_WASM_MEMORY_ADDR_REL_LEB`, so that a relocation
corresponding to a relative offset from r2r `imageBase` can be used
directly as the `offset` in a load, like:
```wasm
global.get $imageBase
i32.load align=... offset=<reloc>
```

The functionality isn't used yet, but will be in #126662.

---------

Co-authored-by: David Wrighton <davidwr@microsoft.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 10, 2026 21:02
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 36 out of 36 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

src/coreclr/utilcode/webcildecoder.cpp:92

  • WebcilDecoder::Reset (and Init) do not reset the new ReadyToRun header cache fields (m_hasNoReadyToRunHeader / m_pReadyToRunHeader). If a previous image had no R2R header, m_hasNoReadyToRunHeader stays true and subsequent HasReadyToRunHeader() calls will incorrectly return false even after reinitializing with an R2R-enabled image. Reset these fields in Reset() and/or Init().
void WebcilDecoder::Reset()
{
    LIMITED_METHOD_CONTRACT;
    m_base = 0;
    m_size = 0;
    m_hasContents = false;
    m_pHeader = NULL;
    m_sections = NULL;
    m_pCorHeader = NULL;
    m_relocated = FALSE;
}

Comment on lines +2096 to +2108
GCPROTECT_BEGINCONSERVATIVE_ARRAY(args, (UINT)(argsSize/sizeof(OBJECTREF)));
GCX_PREEMP();
(void)pMethod->DoPrestub(NULL /* MethodTable */, CallerGCMode::Coop);
targetIp = pMethod->GetInterpreterCode();

if (targetIp == NULL)
{
_ASSERTE(!PortableEntryPoint::PrefersInterpreterEntryPoint(portableEntrypoint));
ManagedMethodParam param = { pMethod, args, retBuff, (PCODE)targetIp, nullptr /* WASM-TODO, handle RuntimeAsync */};
return InvokeManagedMethod(&param);
}

GCPROTECT_END();
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GCPROTECT_BEGINCONSERVATIVE_ARRAY(args, ...) is passing a pointer variable (int8_t* args) into a macro that takes the address of its argument. This ends up protecting the local pointer (int8_t**) rather than scanning/protecting the argument buffer contents, so GC refs in the args buffer can move during DoPrestub. Consider passing an lvalue representing the first element of the buffer (e.g., ((OBJECTREF*)args)[0]) or constructing a GCFrame directly over (OBJECTREF*)args (and also guard for args == nullptr / argsSize == 0).

Suggested change
GCPROTECT_BEGINCONSERVATIVE_ARRAY(args, (UINT)(argsSize/sizeof(OBJECTREF)));
GCX_PREEMP();
(void)pMethod->DoPrestub(NULL /* MethodTable */, CallerGCMode::Coop);
targetIp = pMethod->GetInterpreterCode();
if (targetIp == NULL)
{
_ASSERTE(!PortableEntryPoint::PrefersInterpreterEntryPoint(portableEntrypoint));
ManagedMethodParam param = { pMethod, args, retBuff, (PCODE)targetIp, nullptr /* WASM-TODO, handle RuntimeAsync */};
return InvokeManagedMethod(&param);
}
GCPROTECT_END();
UINT gcRefCount = (UINT)(argsSize / sizeof(OBJECTREF));
if (args != NULL && gcRefCount != 0)
{
GCPROTECT_BEGINCONSERVATIVE_ARRAY(((OBJECTREF*)args)[0], gcRefCount);
GCX_PREEMP();
(void)pMethod->DoPrestub(NULL /* MethodTable */, CallerGCMode::Coop);
targetIp = pMethod->GetInterpreterCode();
if (targetIp == NULL)
{
_ASSERTE(!PortableEntryPoint::PrefersInterpreterEntryPoint(portableEntrypoint));
ManagedMethodParam param = { pMethod, args, retBuff, (PCODE)targetIp, nullptr /* WASM-TODO, handle RuntimeAsync */};
return InvokeManagedMethod(&param);
}
GCPROTECT_END();
}
else
{
GCX_PREEMP();
(void)pMethod->DoPrestub(NULL /* MethodTable */, CallerGCMode::Coop);
targetIp = pMethod->GetInterpreterCode();
if (targetIp == NULL)
{
_ASSERTE(!PortableEntryPoint::PrefersInterpreterEntryPoint(portableEntrypoint));
ManagedMethodParam param = { pMethod, args, retBuff, (PCODE)targetIp, nullptr /* WASM-TODO, handle RuntimeAsync */};
return InvokeManagedMethod(&param);
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +84 to +88
// Pregenerated signatures for commonly used shapes used by R2R code to reduce duplication in generated R2R binaries.
// The signatures should be in the form of a string where the first character represents the return type and the
// following characters represent the argument types. The type characters should match those used by the
// SignatureMapper.CharToNativeType method.
string[] pregeneratedInterpreterToNativeSignatures =
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment describing pregeneratedInterpreterToNativeSignatures says the type characters match SignatureMapper.CharToNativeType, but these signatures include the 'p' suffix which is handled as a portable-entrypoint marker (and is not recognized by CharToNativeType). Update the comment to reflect that 'p' is a special suffix, not a regular type code.

Copilot uses AI. Check for mistakes.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 10, 2026 21:23
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 36 out of 36 changed files in this pull request and generated 6 comments.

Comments suppressed due to low confidence (1)

src/coreclr/utilcode/webcildecoder.cpp:92

  • WebcilDecoder::Init/Reset initialize m_hasContents, m_pHeader, etc., but they don't reset the new ReadyToRun-related cache fields (m_hasNoReadyToRunHeader and m_pReadyToRunHeader). If the decoder instance is reused for another image, HasReadyToRunHeader()/GetReadyToRunHeader() could return stale results or pointers. These fields should be cleared in Init and Reset.
void WebcilDecoder::Init(void *flatBase, COUNT_T size)
{
    CONTRACTL
    {
        INSTANCE_CHECK;
        PRECONDITION((size == 0) || CheckPointer(flatBase));
        NOTHROW;
        GC_NOTRIGGER;
    }
    CONTRACTL_END;

    m_base = (TADDR)flatBase;
    m_size = size;
    m_hasContents = (size > 0);
    m_pHeader = (m_hasContents && size >= sizeof(WebcilHeader)) ? (const WebcilHeader *)flatBase : NULL;
    if (m_pHeader != NULL && m_pHeader->VersionMajor >= 1)
    {
        // For version 1 and above, the section headers start after the larger header
        if (size < sizeof(WebcilHeader_1))
        {
            m_pHeader = NULL; // Not enough data for even the header
        }
    }
    if (!m_pHeader)
    {
        m_sections = NULL;
    }
    else
    {
        m_sections = (const WebcilSectionHeader *)(((uint8_t*)flatBase) + (m_pHeader->VersionMajor >= 1 ? sizeof(WebcilHeader_1) : sizeof(WebcilHeader)));
    }
    m_pCorHeader = NULL;
    m_relocated = FALSE;
}

void WebcilDecoder::Reset()
{
    LIMITED_METHOD_CONTRACT;
    m_base = 0;
    m_size = 0;
    m_hasContents = false;
    m_pHeader = NULL;
    m_sections = NULL;
    m_pCorHeader = NULL;
    m_relocated = FALSE;
}

Comment on lines +144 to +145
// pass a stack pointer or portable entry point context, so we can just specify 0 for those parameters.
return RhpNewArrayFast(0 /* stack_pointer */, pMT, size, 0 /* portableEntryPointContext */);
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RhpNewArrayFast is declared via FCDECL2, which expands to a 2-arg function on non-WASM targets. This direct call with 4 arguments will not compile outside TARGET_WASM (and also makes the callsite inconsistent with the declaration on other platforms). Consider wrapping the extra stack_pointer / portableEntryPointContext arguments in #ifdef TARGET_WASM, or providing a small helper/macro that supplies the right argument list per target.

Suggested change
// pass a stack pointer or portable entry point context, so we can just specify 0 for those parameters.
return RhpNewArrayFast(0 /* stack_pointer */, pMT, size, 0 /* portableEntryPointContext */);
// pass a stack pointer or portable entry point context, so we can just specify 0 for those parameters.
#ifdef TARGET_WASM
return RhpNewArrayFast(0 /* stack_pointer */, pMT, size, 0 /* portableEntryPointContext */);
#else
return RhpNewArrayFast(pMT, size);
#endif

Copilot uses AI. Check for mistakes.
COUNT_T dirPos = 0;
#ifdef TARGET_WASM
// WASM will padd out the reloc size to the next 16 byte boundary, so we need to validate we can safely read the IMAGE_BASE_RELOCATION struct before processing each entry.
while (dirPos < (dirSize - sizeof(IMAGE_BASE_RELOCATION)))
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On WASM, the loop condition dirPos < (dirSize - sizeof(IMAGE_BASE_RELOCATION)) can underflow when dirSize < sizeof(IMAGE_BASE_RELOCATION) because dirSize is unsigned. That would turn the bound into a huge value and can lead to out-of-bounds reads. Use a form like dirPos + sizeof(IMAGE_BASE_RELOCATION) <= dirSize (and keep the existing padding handling) to avoid unsigned underflow.

Suggested change
while (dirPos < (dirSize - sizeof(IMAGE_BASE_RELOCATION)))
while (dirPos + sizeof(IMAGE_BASE_RELOCATION) <= dirSize)

Copilot uses AI. Check for mistakes.
Comment on lines +56 to +60
}

foreach (var nestedType in type.GetNestedTypes())
ScanType(nestedType);
}
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type.GetNestedTypes() without binding flags returns only public nested types. This will skip InternalCall methods on non-public nested types. Use type.GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic) (and consider BindingFlags.DeclaredOnly if desired) to ensure the scan is complete.

Copilot uses AI. Check for mistakes.
Comment on lines +84 to +96
// Pregenerated signatures for commonly used shapes used by R2R code to reduce duplication in generated R2R binaries.
// The signatures should be in the form of a string where the first character represents the return type and the
// following characters represent the argument types. The type characters should match those used by the
// SignatureMapper.CharToNativeType method.
string[] pregeneratedInterpreterToNativeSignatures =
{
"ip",
"iip",
"iiip",
"iiiip",
"vip",
"viip",
};
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment describing pregeneratedInterpreterToNativeSignatures says all characters match SignatureMapper.CharToNativeType, but these signatures now include the trailing 'p' portable-entrypoint marker (which is not a type char and isn't handled by CharToNativeType). Please update the comment to document the 'p' suffix convention (and ideally reference where it's interpreted) to avoid confusion.

Copilot uses AI. Check for mistakes.
Comment on lines 849 to 863
@@ -618,7 +859,7 @@ namespace
return NULL;
}

uint32_t keyBufferLen = sig.NumFixedArgs() + (sig.HasThis() ? 1 : 0) + 2;
uint32_t keyBufferLen = sig.NumFixedArgs() + (sig.HasThis() ? 1 : 0) + 2 + ((callConv == IMAGE_CEE_CS_CALLCONV_DEFAULT) ? 1 : 0);
char* keyBuffer = (char*)alloca(keyBufferLen);
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ComputeCalliSigThunk still allows non-default calling conventions (C/stdcall/fastcall/unmanaged), but GetSignatureKey only appends the 'p' marker for IMAGE_CEE_CS_CALLCONV_DEFAULT. With the thunk table updates, several floating-point shapes now only exist in the ...p form (e.g., ddp, ffp), so a non-default calli with double/float args/returns would compute keys like dd/ff and fail lookup. Either restrict the supported calling conventions here (return NULL for non-default until thunks exist), or ensure the thunk table/keying scheme continues to cover those non-default shapes.

Copilot uses AI. Check for mistakes.
Comment on lines 557 to +566
const StringToWasmSigThunk g_wasmThunks[] = {
{ "dd", (void*)&CallFunc_F64_RetF64 },
{ "ddd", (void*)&CallFunc_F64_F64_RetF64 },
{ "dddd", (void*)&CallFunc_F64_F64_F64_RetF64 },
{ "ddi", (void*)&CallFunc_F64_I32_RetF64 },
{ "ddddp", (void*)&CallFunc_F64_F64_F64_RetF64_PE },
{ "dddp", (void*)&CallFunc_F64_F64_RetF64_PE },
{ "ddip", (void*)&CallFunc_F64_I32_RetF64_PE },
{ "ddp", (void*)&CallFunc_F64_RetF64_PE },
{ "di", (void*)&CallFunc_I32_RetF64 },
{ "ff", (void*)&CallFunc_F32_RetF32 },
{ "fff", (void*)&CallFunc_F32_F32_RetF32 },
{ "ffff", (void*)&CallFunc_F32_F32_F32_RetF32 },
{ "ffi", (void*)&CallFunc_F32_I32_RetF32 },
{ "ffffp", (void*)&CallFunc_F32_F32_F32_RetF32_PE },
{ "fffp", (void*)&CallFunc_F32_F32_RetF32_PE },
{ "ffip", (void*)&CallFunc_F32_I32_RetF32_PE },
{ "ffp", (void*)&CallFunc_F32_RetF32_PE },
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thunk map no longer contains non-p floating-point signatures (e.g., dd, ddd, ff, etc.), but helpers.cpp can still compute and look up keys without the 'p' suffix for non-default calli calling conventions. If any such calli is exercised (C/stdcall/fastcall/unmanaged), it will now fail to find a thunk. Consider generating/keeping both the portable-entrypoint (...p) and non-portable variants for these float/double shapes, or tightening the runtime to reject non-default calling conventions until supported.

Copilot uses AI. Check for mistakes.
…rms the stack walker to stop walking

- Put the callers frame pointer into the TransitionBlock. The current implementation drops it into the m_ReturnAddress field, which is a bit dodgy, but we can fix this later when we actually work on the stack walker
- Replace use of conservative gc in the ExecuteInterpretedMethodWithArgs_PortableEntryPoint_Complex function in favor of reusing the PrestubMethodFrame which I believe is appropriate to the situation
return RhpNewArrayFast(pMT, size);
// Since we know the implementation of RhpNewArrayFast is native we don't need to actually
// pass a stack pointer or portable entry point context, so we can just specify 0 for those parameters.
return RhpNewArrayFast(0 /* stack_pointer */, pMT, size, 0 /* portableEntryPointContext */);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return RhpNewArrayFast(0 /* stack_pointer */, pMT, size, 0 /* portableEntryPointContext */);
return RhpNewArrayFast_IMPL(pMT, size);

Call the actual _IMPL method? I think this is going to overwrite the stack pointer with zero otherwise.

{
FCALL_CONTRACT;
IL_Throw_Impl(obj, NULL);
IL_Throw_Impl(0, obj, NULL, 0);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
IL_Throw_Impl(0, obj, NULL, 0);
IL_Throw_Impl_IMPL(obj, NULL);

{
FCALL_CONTRACT;
IL_Rethrow_Impl(NULL);
IL_Rethrow_Impl(0, NULL, 0);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
IL_Rethrow_Impl(0, NULL, 0);
IL_Rethrow_Impl_IMPL(NULL);

{
FCALL_CONTRACT;
IL_ThrowExact_Impl(obj, NULL);
IL_ThrowExact_Impl(0, obj, NULL, 0);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
IL_ThrowExact_Impl(0, obj, NULL, 0);
IL_ThrowExact_Impl_IMPL(obj, NULL);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants