Repository navigation
Description
Description
Since the .NET 11 runtime picked up dotnet/runtime#130159 ("Use CodeVersioning for EnC"), Hot Reload crashes on the CoreCLR configurations that have no JIT (iOS and Mac Catalyst, i.e. the interpreter-only Apple mobile builds). Immediately after a delta is applied, the first call into an updated method throws:
dotnet watch ⌚ File updated: /tmp/hrtest/AdditionalFile.cs
Unhandled exception. System.BadImageFormatException: Bad IL range.
at HotReloadTestApp.Program.DoSomething(Int32 i)
at HotReloadTestApp.Program.RunTestLogic()
at HotReloadTestApp.Program.Main(String[] args)
dotnet watch ❌ [HotReloadTestApp (net11.0-maccatalyst)] Exited with error code 134
ApplyUpdate itself succeeds — dotnet watch --verbose reports Applying updates to module ..., Invoking metadata update handlers, Updates applied. with no error. The crash happens later, when the updated method is next invoked and its code is prepared.
This is 100% deterministic, both in CI and locally. It has been failing every dependency-flow PR in dotnet/macios for several days; we are tracking the CI side in dotnet/macios#26470 and have had to disable the affected test cases.
Analysis
Bad IL range (BFA_BAD_IL_RANGE) is only thrown from PEAssembly::GetIL (src/coreclr/vm/peassembly.cpp), reached via Module::GetIL(GetRVA()). So the runtime looked up the updated method's IL in the baseline PE image, using the RVA from the metadata delta. After a delta is applied, that MethodDef RVA is delta-relative (a small offset into the delta IL stream), so interpreting it against the baseline image is out of range — hence the error. In other words, the delta IL was never picked up when preparing the updated method.
That lines up exactly with what #130159 changed. EditAndContinueModule::ApplyEditAndContinue now stores the delta IL as an ILCodeVersion (src/coreclr/vm/encee.cpp):
ILCodeVersion ilCodeVersion;
CodeVersionManager *pCodeVersionManager = GetCodeVersionManager();
{
CodeVersionManager::LockHolder codeVersioningLockHolder;
if (FAILED(hr = pCodeVersionManager->AddILCodeVersion(this, token, &ilCodeVersion, FALSE, CodeVersionSource::kEnC, m_applyChangesCount)))
...
ilCodeVersion.SetIL((COR_ILMETHOD*)&pLocalILMemory[dwMethodRVA]);
ilCodeVersion.SetRejitState(RejitFlags::kStateActive);
}and that IL is reachable only through the versioned config (src/coreclr/vm/prestub.cpp):
COR_ILMETHOD* VersionedPrepareCodeConfig::GetILHeader()
{
STANDARD_VM_CONTRACT;
return m_ilCodeVersion.GetIL();
}whereas the default config falls back to the baseline RVA lookup that throws:
COR_ILMETHOD* PrepareCodeConfig::GetILHeader()
{
STANDARD_VM_CONTRACT;
return m_pMethodDesc->GetILHeader();
}Both go through the same GetAndVerifyMetadataILHeader → pConfig->GetILHeader() call, so the symptom means the updated method is being prepared with a non-versioned PrepareCodeConfig.
Why only the no-JIT configurations?
I want to explicitly rule out the obvious guess, because it's wrong: it is not that code versioning is unavailable on these targets.
MethodDesc::IsVersionable()isIsEligibleForTieredCompilation() || IsEligibleForReJIT() || IsEligibleForEnC(), andIsEligibleForEnC()only requiresInEnCEnabledModule() && IsIL() && !IsWrapperStub()— no dependency on tiering (src/coreclr/vm/method.hpp).FEATURE_CODE_VERSIONINGis enabled byFEATURE_TIERED_COMPILATION OR FEATURE_REJIT OR FEATURE_METADATA_UPDATER, andFEATURE_METADATA_UPDATERis on for arm64 — so code versioning is compiled in on these targets (src/coreclr/clrdefinitions.cmake).
The actual difference is that FEATURE_DYNAMIC_CODE_COMPILED is off for Apple mobile (src/coreclr/clrfeatures.cmake), which is also what turns FEATURE_TIERED_COMPILATION, FEATURE_REJIT and FEATURE_PGO off there:
if (FEATURE_DYNAMIC_CODE_COMPILED)
set(FEATURE_TIERED_COMPILATION 1)
set(FEATURE_REJIT 1)
set(FEATURE_PGO 1)
endif()So there is no JIT and these apps run the CoreCLR interpreter. My conclusion is that some interpreter-side code-preparation path doesn't consult the active ILCodeVersion and ends up with a default PrepareCodeConfig. I haven't tracked it down to the exact call site — that's where I'd appreciate help from someone who knows the interpreter entry points (MethodDesc::GetInterpreterCode / InterpreterPrecode / the InvokeManagedMethod path in prestub.cpp).
Corroborating evidence that the JIT path is fine: I built an equivalent plain console app with the same SDK and the same partial-method edit pattern and ran it on desktop osx-arm64 — Hot Reload works correctly there. Only the interpreter-only Apple targets fail.
Repro
Reproduces reliably in the dotnet/macios test suite:
git clone https://github.com/dotnet/macios
cd macios && ./configure --enable-maccatalyst && make world
cd tests/dotnet/UnitTests
dotnet test --filter "FullyQualifiedName~DotNetWatch"Minimal manual repro (what produced the log above), from tests/dotnet/HotReloadTestApp/MacCatalyst:
AdditionalFile=/tmp/hrtest/AdditionalFile.cs \
UseMonoRuntime=false RunWithOpen=false EnableSandbox=false \
HOTRELOAD_TEST_APP_LOGFILE=/tmp/hrtest/output.log \
dotnet watch --non-interactive --disable-build-serversthen edit /tmp/hrtest/AdditionalFile.cs so the partial method assigns a different value. The app prints the variable once a second, receives the edit, and crashes instead of printing the new value.
The app is built with UseInterpreter=true, MtouchLink=None, Registrar=partial-static (Hot Reload requires no trimming). Note MtouchLink=None means the assembly is not rewritten in any way — I verified the HotReloadTestApp.dll in the app bundle is byte-identical to the compiler output, so this is not a tooling/rewriting artifact.
Ruled out
- ReadyToRun — user assemblies are already excluded from the composite image, and building with
PublishReadyToRun=falsestill crashes. - Assembly rewriting —
obj/,linked/,bin/and the app bundle copies ofHotReloadTestApp.dllall have the same SHA1. DebuggableAttribute— identical blob (01 00 07 01 00 00 00 00) in the failing app and the working desktop console app.- Tiered compilation being merely disabled —
DOTNET_TieredCompilation=0on the desktop console app does not reproduce. The feature being compiled out is not the same as it being switched off.
Regression information
- Suspect: Use CodeVersioning for EnC #130159 ("Use CodeVersioning for EnC"), commit e614b71, merged 2026-07-28. I have not bisected, but the failure signature points directly at the mechanism that PR introduced, and the timing matches when it reached us via dependency flow.
- I could not find an existing issue for this — nothing references Use CodeVersioning for EnC #130159, and no commit to
codeversion.cpporprestub.cppsince it merged looks related. EnC: metadata rows added by a delta are invisible to lookups once the table's lookup hash is built (ApplyUpdate fails: 'The assembly update failed') #131752 is a different EnC bug (FieldRVA lookup hash) and is already fixed.
Configuration
- Runtime: dotnet/runtime
067d74c23c41161cedb93f672be3de0f2452a3d0(via VMRdotnet/dotnet@7cdb217445905f3342bbb0266a4497b9a014389a), packages11.0.0-rc.1.26411.119 - SDK: 11.0.100-rc.1.26411.119
- Targets:
net11.0-maccatalystandnet11.0-ios, arm64, CoreCLR + interpreter - Host: macOS, Apple Silicon
- Not affected: desktop macOS CoreCLR (JIT), and Mono on all platforms — the corresponding
useMonoRuntime: truetest cases still pass.
Activity
dotnet-policy-service commentedon Aug 26, 2026
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.
dotnet-policy-service commentedon Aug 26, 2026
Tagging subscribers to this area: @JulieLeeMSFT, @BrzVlad, @janvorli
See info in area-owners.md if you want to be subscribed.
[tests] Ignore the CoreCLR 'dotnet watch' tests on the mobile platforms.
rolfbjarne commentedon Aug 26, 2026
Root cause found (supersedes the speculation in the issue body)
I built an instrumented CoreCLR at the exact commit the app runs on (
067d74c23c41) and traced this to the end. The problem is not inPEAssembly::GetILor in the delta application; it's an IL code version lookup that is incorrectly gated onFEATURE_REJIT.The chain
On the platforms without a JIT (Apple mobile, WASM),
FEATURE_DYNAMIC_CODE_COMPILEDis off, sosrc/coreclr/clrfeatures.cmakenever setsFEATURE_REJIT:But
FEATURE_CODE_VERSIONINGis on, becauseFEATURE_METADATA_UPDATERis enabled (clrdefinitions.cmake:58). So EnC creates real, non-defaultILCodeVersionnodes on these platforms.CodeVersionManager::GetILCodeVersion(PTR_MethodDesc, ReJITID)(vm/codeversion.cpp) is guarded onFEATURE_REJIT, and in the#elsebranch it unconditionally returns the default IL code version:So after the update is applied:
EditAndContinueModule::ApplyEditAndContinuecorrectly adds the EnCILCodeVersionand makes it active.NativeCodeVersionNode::GetILCodeVersion()→GetILCodeVersion(pMD, GetILVersionId()).ILCodeVersion::GetIL()then takes the default-version path:GetMethodImplProps(methodDef, &rva)+pModule->GetIL(rva). After a delta, that RVA is delta-relative (0x4in the repro), so it is out of range in the baseline image →BFA_BAD_IL_RANGE.Instrumented evidence
Note the contradiction on the last three lines: the active IL version is correctly the EnC one (
activeIsDefault=0), but the version that code preparation actually ends up with is the default one (isDefaultVer=1 source=0—source=0iskUnknown, i.e. notkEnC=2). That's theNativeCodeVersion→ILCodeVersionmapping going through the broken lookup.Fix
Don't make the lookup conditional on
FEATURE_REJIT. ArejitIdof 0 always means the default version (ILCodeVersion::GetVersionId()returns 0 for the default version), so short-circuiting that case preserves existing behavior for both configurations while making non-default versions findable whenFEATURE_REJITis off:Verified: with this change built for
maccatalyst-arm64and swapped into the app bundle, the samedotnet watchrepro now applies the edit and the app exits cleanly:I'll put up a PR.
Side note
One thing I did not chase down, but which may be worth a look:
debugger-attached runs do not reproduce this — with lldb attached hot reload succeeds. That made this bug undiagnosable with a debugger and is presumably some other EnC path being taken when the debugger is present.Fix "Bad IL range" when applying a hot reload update without FEATURE_…