From 862df6960bbde2cbbd312c543a6cd26961f99460 Mon Sep 17 00:00:00 2001 From: Jaroslav Ruzicka Date: Mon, 19 Jan 2026 11:16:33 +0100 Subject: [PATCH 01/32] add try/catch blocks to disposing loops --- .../ServiceProviderEngineScope.cs | 66 ++++++++-- .../ServiceProviderEngineScopeTests.cs | 123 ++++++++++++++++++ 2 files changed, 175 insertions(+), 14 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs index 7582ed8e79f025..13525f06ba9cf1 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Internal; @@ -120,40 +121,65 @@ public object GetRequiredKeyedService(Type serviceType, object? serviceKey) public void Dispose() { List? toDispose = BeginDispose(); + var exceptions = default(List); + var index = (toDispose?.Count ?? 0) - 1; - if (toDispose != null) + while (index >= 0) { - for (int i = toDispose.Count - 1; i >= 0; i--) + try { - if (toDispose[i] is IDisposable disposable) - { - disposable.Dispose(); - } - else + for (; index >= 0; index--) { - throw new InvalidOperationException(SR.Format(SR.AsyncDisposableServiceDispose, TypeNameHelper.GetTypeDisplayName(toDispose[i]))); + if (toDispose![index] is IDisposable disposable) + { + disposable.Dispose(); + } + else + { + throw new InvalidOperationException(SR.Format(SR.AsyncDisposableServiceDispose, TypeNameHelper.GetTypeDisplayName(toDispose[index]))); + } } } + catch (Exception ex) + { + exceptions ??= new List(); + exceptions.Add(ex); + index--; + } } + + if (exceptions is null) + { + return; + } + + if (exceptions.Count == 1) + { + throw exceptions[0]; + } + + throw new AggregateException(exceptions); } public ValueTask DisposeAsync() { List? toDispose = BeginDispose(); + var exceptions = default(List); + var index = toDispose is null ? -1 : toDispose.Count - 1; - if (toDispose != null) + while (index >= 0) { try { - for (int i = toDispose.Count - 1; i >= 0; i--) + for (; index >= 0; index--) { - object disposable = toDispose[i]; + object disposable = toDispose![index]; if (disposable is IAsyncDisposable asyncDisposable) { ValueTask vt = asyncDisposable.DisposeAsync(); if (!vt.IsCompletedSuccessfully) { - return Await(i, vt, toDispose); + return Await(index, vt, toDispose); } // If its a IValueTaskSource backed ValueTask, @@ -168,11 +194,23 @@ public ValueTask DisposeAsync() } catch (Exception ex) { - return new ValueTask(Task.FromException(ex)); + exceptions ??= new List(); + exceptions.Add(ex); + index--; } } - return default; + if (exceptions is null) + { + return default; + } + + if (exceptions.Count == 1) + { + return new ValueTask(Task.FromException(exceptions[0])); + } + + return new ValueTask(Task.FromException(new AggregateException(exceptions))); static async ValueTask Await(int i, ValueTask vt, List toDispose) { diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderEngineScopeTests.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderEngineScopeTests.cs index e25174cd011245..d8844ff3c137eb 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderEngineScopeTests.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderEngineScopeTests.cs @@ -3,9 +3,11 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection.Specification.Fakes; using Xunit; using Xunit.Abstractions; +using Xunit.Sdk; namespace Microsoft.Extensions.DependencyInjection.ServiceLookup { @@ -41,5 +43,126 @@ public void ServiceProviderEngineScope_ImplementsAllServiceProviderInterfaces() Assert.Contains(serviceProviderInterface, engineScopeInterfaces); } } + + [Fact] + public void Dispose_ServiceThrows_DisposesAllAndThrows() + { + var services = new ServiceCollection(); + services.AddKeyedTransient("throws", (_, _) => new TestDisposable(true)); + services.AddKeyedTransient("doesnotthrow", (_, _) => new TestDisposable(false)); + + + var scope = services.BuildServiceProvider().GetRequiredService().CreateScope().ServiceProvider; + + var disposables = new TestDisposable[] + { + scope.GetRequiredKeyedService("throws"), + scope.GetRequiredKeyedService("doesnotthrow") + }; + + var exception = Assert.Throws(() => ((IDisposable)scope).Dispose()); + Assert.Equal(TestDisposable.ErrorMessage, exception.Message); + Assert.All(disposables, disposable => Assert.True(disposable.IsDisposed)); + } + + [Fact] + public void Dispose_TwoServicesThrows_DisposesAllAndThrowsAgggregatedException() + { + var services = new ServiceCollection(); + services.AddKeyedTransient("throws", (_, _) => new TestDisposable(true)); + services.AddKeyedTransient("doesnotthrow", (_, _) => new TestDisposable(false)); + + + var scope = services.BuildServiceProvider().GetRequiredService().CreateScope().ServiceProvider; + + var disposables = new TestDisposable[] + { + scope.GetRequiredKeyedService("throws"), + scope.GetRequiredKeyedService("doesnotthrow"), + scope.GetRequiredKeyedService("throws"), + scope.GetRequiredKeyedService("doesnotthrow"), + }; + + var exception = Assert.Throws(() => ((IDisposable)scope).Dispose()); + Assert.Equal(2, exception.InnerExceptions.Count); + Assert.All(exception.InnerExceptions, ex => Assert.IsType(ex)); + Assert.All(disposables, disposable => Assert.True(disposable.IsDisposed)); + } + + [Fact] + public async Task DisposeAsync_ServiceThrows_DisposesAllAndThrows() + { + var services = new ServiceCollection(); + services.AddKeyedTransient("throws", (_, _) => new TestDisposable(true)); + services.AddKeyedTransient("doesnotthrow", (_, _) => new TestDisposable(false)); + + + var scope = services.BuildServiceProvider().GetRequiredService().CreateScope().ServiceProvider; + + var disposables = new TestDisposable[] + { + scope.GetRequiredKeyedService("throws"), + scope.GetRequiredKeyedService("doesnotthrow") + }; + + var exception = await Assert.ThrowsAsync(async() => await ((IAsyncDisposable)scope).DisposeAsync()); + Assert.Equal(TestDisposable.ErrorMessage, exception.Message); + Assert.All(disposables, disposable => Assert.True(disposable.IsDisposed)); + } + + [Fact] + public async Task DisposeAsync_TwoServicesThrows_DisposesAllAndThrowsAggregateException() + { + var services = new ServiceCollection(); + services.AddKeyedTransient("throws", (_, _) => new TestDisposable(true)); + services.AddKeyedTransient("doesnotthrow", (_, _) => new TestDisposable(false)); + + + var scope = services.BuildServiceProvider().GetRequiredService().CreateScope().ServiceProvider; + + var disposables = new TestDisposable[] + { + scope.GetRequiredKeyedService("throws"), + scope.GetRequiredKeyedService("doesnotthrow"), + scope.GetRequiredKeyedService("throws"), + scope.GetRequiredKeyedService("doesnotthrow"), + }; + + var exception = await Assert.ThrowsAsync(async () => await ((IAsyncDisposable)scope).DisposeAsync()); + Assert.Equal(2, exception.InnerExceptions.Count); + Assert.All(exception.InnerExceptions, ex => Assert.IsType(ex)); + Assert.All(disposables, disposable => Assert.True(disposable.IsDisposed)); + } + + private class TestDisposable : IDisposable, IAsyncDisposable + { + public const string ErrorMessage = "Dispose failed."; + + private readonly bool _throwsOnDispose; + public int DisposedCounter { get; private set; } + public bool IsDisposed => DisposedCounter > 0; + + public TestDisposable(bool throwsOnDispose) + { + _throwsOnDispose = throwsOnDispose; + } + + public void Dispose() + { + if (_throwsOnDispose) + { + DisposedCounter++; + throw new InvalidOperationException(ErrorMessage); + } + + DisposedCounter++; + } + + public ValueTask DisposeAsync() + { + Dispose(); + return default; + } + } } } From 99cdcf395575f4416a1bf8eb5a3f1cd06d5f1044 Mon Sep 17 00:00:00 2001 From: Jaroslav Ruzicka Date: Mon, 19 Jan 2026 12:28:55 +0100 Subject: [PATCH 02/32] remove unused using --- .../src/ServiceLookup/ServiceProviderEngineScope.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs index 13525f06ba9cf1..96a6b8154f76a5 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Internal; From 244edbe4ca76a95a6849fa5dcee40065f9c57c52 Mon Sep 17 00:00:00 2001 From: Adeel Mujahid <3840695+am11@users.noreply.github.com> Date: Fri, 16 Jan 2026 15:32:11 +0200 Subject: [PATCH 03/32] Bring throw helpers to PUSH_COOP_PINVOKE_FRAME plan (#123015) Fixes https://github.com/dotnet/runtime/issues/116375. --------- Co-authored-by: Jan Vorlicek --- src/coreclr/pal/inc/unixasmmacrosarm.inc | 37 ++ src/coreclr/pal/inc/unixasmmacrosarm64.inc | 49 ++- .../pal/inc/unixasmmacrosloongarch64.inc | 39 ++ src/coreclr/pal/inc/unixasmmacrosriscv64.inc | 45 ++ src/coreclr/vm/amd64/AsmHelpers.asm | 57 ++- src/coreclr/vm/amd64/AsmMacros.inc | 50 +++ src/coreclr/vm/amd64/asmhelpers.S | 45 ++ src/coreclr/vm/arm/asmhelpers.S | 45 ++ src/coreclr/vm/arm64/asmhelpers.S | 53 +++ src/coreclr/vm/arm64/asmhelpers.asm | 47 ++ src/coreclr/vm/arm64/asmmacros.h | 49 +++ src/coreclr/vm/excep.cpp | 401 ++++++++++++++++-- src/coreclr/vm/frames.h | 5 +- src/coreclr/vm/i386/asmhelpers.S | 6 +- src/coreclr/vm/i386/asmhelpers.asm | 12 +- src/coreclr/vm/jithelpers.cpp | 58 ++- src/coreclr/vm/loongarch64/asmhelpers.S | 45 ++ src/coreclr/vm/riscv64/asmhelpers.S | 44 ++ 18 files changed, 1014 insertions(+), 73 deletions(-) diff --git a/src/coreclr/pal/inc/unixasmmacrosarm.inc b/src/coreclr/pal/inc/unixasmmacrosarm.inc index 54a6f7d4dc3b19..4ea9aa35236eb8 100644 --- a/src/coreclr/pal/inc/unixasmmacrosarm.inc +++ b/src/coreclr/pal/inc/unixasmmacrosarm.inc @@ -288,6 +288,43 @@ C_FUNC(\Name): add \target, sp, 4 .endm +// Pushes a full TransitionBlock on the stack including float argument registers. +// On exit, \target contains the TransitionBlock pointer. +// +// Stack layout (from sp going up): +// sp+0: padding (4 bytes) - for 8-byte alignment +// sp+4: d8-d15 (64 bytes) - FP callee-saved +// sp+68: padding (4 bytes) - to make d0-d7 8-byte aligned at TransitionBlock-68 +// sp+72: d0-d7 (64 bytes) - float argument registers (at TransitionBlock - 68) +// sp+136: padding (4 bytes) - to keep total allocation 8-byte aligned +// sp+140: TransitionBlock starts here (CalleeSavedRegisters + ArgumentRegisters pushed above) +// +// GetNegSpaceSize() for ARM32 = 64 (FloatArgumentRegisters) + 4 (padding) = 68 +// GetOffsetOfFloatArgumentRegisters() = -68 +// +// Total stack alloc: 4 + 64 + 4 + 64 + 4 = 140 bytes +// Stack: Arguments(16) + callee-saved(36) + alloc(140) = 192 bytes +// 192 % 8 = 0, properly aligned for ARM32 (8-byte alignment required) +.macro PUSH_COOP_PINVOKE_FRAME_WITH_FLOATS target + // Push argument registers (r0-r3) - these will be at highest address in TransitionBlock + PUSH_ARGUMENT_REGISTERS + PUSH_CALLEE_SAVED_REGISTERS + PROLOG_STACK_SAVE_OFFSET r7, #12 + // let r7 point the saved r7 in the stack (clang FP style) + // Allocate space for: padding (4) + d8-d15 (64) + padding (4) + d0-d7 (64) + padding (4) = 140 bytes + alloc_stack 140 + // Save floating point argument registers (d0-d7) at sp+72 (TransitionBlock - 68) + add r12, sp, #72 + vstm r12, {d0-d7} + // Save FP callee-saved registers (d8-d15) at sp+4 + add r12, sp, #4 + vstm r12, {d8-d15} + CHECK_STACK_ALIGNMENT + END_PROLOGUE + // TransitionBlock is at sp + 140 + add \target, sp, #140 +.endm + .macro POP_COOP_PINVOKE_FRAME free_stack 4 POP_CALLEE_SAVED_REGISTERS diff --git a/src/coreclr/pal/inc/unixasmmacrosarm64.inc b/src/coreclr/pal/inc/unixasmmacrosarm64.inc index fa4265ab3fc9d2..2d27459372b561 100644 --- a/src/coreclr/pal/inc/unixasmmacrosarm64.inc +++ b/src/coreclr/pal/inc/unixasmmacrosarm64.inc @@ -419,7 +419,54 @@ C_FUNC(\Name\()_End): EPILOG_RESTORE_REG_PAIR x25, x26, 64 EPILOG_RESTORE_REG_PAIR x27, x28, 80 EPILOG_RESTORE_REG_PAIR_INDEXED fp, lr, 176 -.endm +.endm + +// Pushes a full TransitionBlock on the stack including argument registers and +// floating point argument registers. Used for exception throw helpers where we +// need to capture the complete register state including FP callee-saved registers. +// +// Stack layout (from low to high address): +// sp+0: FP callee-saved registers (d8-d15, 64 bytes) +// sp+64: FloatArgumentRegisters (q0-q7, 128 bytes) +// sp+192: TransitionBlock start (176 bytes) +// - CalleeSavedRegisters (fp, lr, x19-x28 - 96 bytes) +// - padding (8 bytes) +// - x8 (8 bytes) +// - ArgumentRegisters (x0-x7, 64 bytes) +// +// On exit, \target contains the TransitionBlock pointer (sp+192). +.macro PUSH_COOP_PINVOKE_FRAME_WITH_FLOATS target + PROLOG_SAVE_REG_PAIR_INDEXED fp, lr, -176 + + // Spill callee saved registers + PROLOG_SAVE_REG_PAIR x19, x20, 16 + PROLOG_SAVE_REG_PAIR x21, x22, 32 + PROLOG_SAVE_REG_PAIR x23, x24, 48 + PROLOG_SAVE_REG_PAIR x25, x26, 64 + PROLOG_SAVE_REG_PAIR x27, x28, 80 + + // Allocate space for FloatArgumentRegisters (128) + FP callee-saved (64) = 192 bytes + PROLOG_STACK_ALLOC 192 + + // Save argument registers (x8, x0-x7) at offset 296 from sp (192 + 104) + SAVE_ARGUMENT_REGISTERS sp, 296 + + // Save floating point argument registers (q0-q7) at sp+64 + SAVE_FLOAT_ARGUMENT_REGISTERS sp, 64 + + // Save FP callee-saved registers (d8-d15) at sp+0 + str d8, [sp, #0] + str d9, [sp, #8] + str d10, [sp, #16] + str d11, [sp, #24] + str d12, [sp, #32] + str d13, [sp, #40] + str d14, [sp, #48] + str d15, [sp, #56] + + // Set target to TransitionBlock pointer + add \target, sp, #192 +.endm // ------------------------------------------------------------------ // Macro to generate Redirection Stubs diff --git a/src/coreclr/pal/inc/unixasmmacrosloongarch64.inc b/src/coreclr/pal/inc/unixasmmacrosloongarch64.inc index 66ebdd147535e9..92d701598f933e 100644 --- a/src/coreclr/pal/inc/unixasmmacrosloongarch64.inc +++ b/src/coreclr/pal/inc/unixasmmacrosloongarch64.inc @@ -404,6 +404,45 @@ C_FUNC(\Name\()_End): EPILOG_STACK_FREE 160 .endm +// Pushes a full TransitionBlock on the stack including argument registers and +// floating point argument registers. Used for exception throw helpers where we +// need to capture the complete register state. +// +// Stack layout (from low to high address): +// sp+0: FloatArgumentRegisters (fa0-fa7, 64 bytes) +// sp+64: TransitionBlock start +// - CalleeSavedRegisters (fp, ra, s0-s8 - 96 bytes) +// - ArgumentRegisters (a0-a7, 64 bytes) +// +// On exit, \target contains the TransitionBlock pointer (sp+128). +.macro PUSH_COOP_PINVOKE_FRAME_WITH_FLOATS target + // Stack: FPCalleeSaved(64) + FloatArgs(64) + CalleeSaved(96) + Args(64) = 288 bytes + PROLOG_STACK_ALLOC 288 + PROLOG_SAVE_REG_PAIR 22, 1, 128, 1 + + // Save callee-saved registers at offset 128 (after FP callee-saved and FloatArgumentRegisters) + SAVE_CALLEESAVED_REGISTERS $sp, 128 + + // Save argument registers (a0-a7) at offset 224 + SAVE_ARGUMENT_REGISTERS $sp, 224 + + // Save floating-point argument registers (fa0-fa7) at offset 64 + SAVE_FLOAT_ARGUMENT_REGISTERS $sp, 64 + + // Save FP callee-saved registers (f24-f31) at offset 0 + fst.d $f24, $sp, 0 + fst.d $f25, $sp, 8 + fst.d $f26, $sp, 16 + fst.d $f27, $sp, 24 + fst.d $f28, $sp, 32 + fst.d $f29, $sp, 40 + fst.d $f30, $sp, 48 + fst.d $f31, $sp, 56 + + // Set target to TransitionBlock pointer + addi.d \target, $sp, 128 +.endm + // ------------------------------------------------------------------ // Macro to generate Redirection Stubs // diff --git a/src/coreclr/pal/inc/unixasmmacrosriscv64.inc b/src/coreclr/pal/inc/unixasmmacrosriscv64.inc index ead0d6b550d232..d244756c304eb9 100644 --- a/src/coreclr/pal/inc/unixasmmacrosriscv64.inc +++ b/src/coreclr/pal/inc/unixasmmacrosriscv64.inc @@ -349,6 +349,51 @@ C_FUNC(\Name): EPILOG_STACK_FREE 192 .endm +// Pushes a full TransitionBlock on the stack including argument registers and +// floating point argument registers. Used for exception throw helpers where we +// need to capture the complete register state. +// +// Stack layout (from low to high address): +// sp+0: FloatArgumentRegisters (fa0-fa7, 64 bytes) +// sp+64: TransitionBlock start +// - CalleeSavedRegisters (fp, ra, s1-s11, tp, gp - 120 bytes) +// - padding (8 bytes) +// - ArgumentRegisters (a0-a7, 64 bytes) +// +// On exit, \target contains the TransitionBlock pointer (sp+160). +.macro PUSH_COOP_PINVOKE_FRAME_WITH_FLOATS target + // Stack: FPCalleeSaved(96) + FloatArgs(64) + CalleeSaved(120) + pad(8) + Args(64) = 352 bytes + PROLOG_STACK_ALLOC 352 + PROLOG_SAVE_REG_PAIR fp, ra, 160, 1 + + // Save callee-saved registers at offset 160 (after FP callee-saved and FloatArgumentRegisters) + SAVE_CALLEESAVED_REGISTERS sp, 160 + + // Save argument registers (a0-a7) at offset 288 + SAVE_ARGUMENT_REGISTERS sp, 288 + + // Save floating-point argument registers (fa0-fa7) at offset 96 + SAVE_FLOAT_ARGUMENT_REGISTERS sp, 96 + + // Save FP callee-saved registers (fs0-fs11 = f8,f9,f18-f27) at offset 0 + // RISC-V FP callee-saved: fs0=f8, fs1=f9, fs2-fs11=f18-f27 + fsd fs0, 0(sp) // f8 + fsd fs1, 8(sp) // f9 + fsd fs2, 16(sp) // f18 + fsd fs3, 24(sp) // f19 + fsd fs4, 32(sp) // f20 + fsd fs5, 40(sp) // f21 + fsd fs6, 48(sp) // f22 + fsd fs7, 56(sp) // f23 + fsd fs8, 64(sp) // f24 + fsd fs9, 72(sp) // f25 + fsd fs10, 80(sp) // f26 + fsd fs11, 88(sp) // f27 + + // Set target to TransitionBlock pointer + addi \target, sp, 160 +.endm + // ------------------------------------------------------------------ // Macro to generate Redirection Stubs // diff --git a/src/coreclr/vm/amd64/AsmHelpers.asm b/src/coreclr/vm/amd64/AsmHelpers.asm index 125143377f0ecf..dc8c5eeec42f91 100644 --- a/src/coreclr/vm/amd64/AsmHelpers.asm +++ b/src/coreclr/vm/amd64/AsmHelpers.asm @@ -11,6 +11,9 @@ extern ProfileLeave:proc extern ProfileTailcall:proc extern OnHijackWorker:proc extern JIT_RareDisableHelperWorker:proc +extern IL_Throw_Impl:proc +extern IL_ThrowExact_Impl:proc +extern IL_Rethrow_Impl:proc ifdef FEATURE_INTERPRETER extern ExecuteInterpretedMethod:proc extern GetInterpThreadContextWithPossiblyMissingThreadOrCallStub:proc @@ -517,8 +520,9 @@ NESTED_ENTRY CallEHFunclet, _TEXT movdqa xmm14, [r8 + OFFSETOF__CONTEXT__Xmm14] movdqa xmm15, [r8 + OFFSETOF__CONTEXT__Xmm15] - ; Save the SP of this function. + ; Save the SP of this function. mov [r9], rsp + ; Invoke the funclet call rdx @@ -543,6 +547,7 @@ NESTED_ENTRY CallEHFilterFunclet, _TEXT mov [r9], rsp ; Restore RBP to match main function RBP mov rbp, rdx + ; Invoke the filter funclet call r8 @@ -1199,4 +1204,54 @@ NESTED_END CallJittedMethodRetI8, _TEXT endif ; FEATURE_INTERPRETER +;========================================================================== +; Capture a transition block with register values and call the IL_Throw_Impl +; implementation written in C. +; +; Input state: +; RCX = Pointer to exception object +;========================================================================== +NESTED_ENTRY IL_Throw, _TEXT + ; Shadow space for the call is included in PUSH_COOP_PINVOKE_FRAME_WITH_FLOATS + PUSH_COOP_PINVOKE_FRAME_WITH_FLOATS rdx + + ; RCX already contains exception object + ; RDX contains pointer to TransitionBlock + call IL_Throw_Impl + ; Should never return + int 3 +NESTED_END IL_Throw, _TEXT + +;========================================================================== +; Capture a transition block with register values and call the IL_ThrowExact_Impl +; implementation written in C. +; +; Input state: +; RCX = Pointer to exception object +;========================================================================== +NESTED_ENTRY IL_ThrowExact, _TEXT + ; Shadow space for the call is included in PUSH_COOP_PINVOKE_FRAME_WITH_FLOATS + PUSH_COOP_PINVOKE_FRAME_WITH_FLOATS rdx + + ; RCX already contains exception object + ; RDX contains pointer to TransitionBlock + call IL_ThrowExact_Impl + ; Should never return + int 3 +NESTED_END IL_ThrowExact, _TEXT + +;========================================================================== +; Capture a transition block with register values and call the IL_Rethrow_Impl +; implementation written in C. +;========================================================================== +NESTED_ENTRY IL_Rethrow, _TEXT + ; Shadow space for the call is included in PUSH_COOP_PINVOKE_FRAME_WITH_FLOATS + PUSH_COOP_PINVOKE_FRAME_WITH_FLOATS rcx + + ; RCX contains pointer to TransitionBlock + call IL_Rethrow_Impl + ; Should never return + int 3 +NESTED_END IL_Rethrow, _TEXT + end diff --git a/src/coreclr/vm/amd64/AsmMacros.inc b/src/coreclr/vm/amd64/AsmMacros.inc index c6966135fc7ef4..07531371d6627d 100644 --- a/src/coreclr/vm/amd64/AsmMacros.inc +++ b/src/coreclr/vm/amd64/AsmMacros.inc @@ -485,5 +485,55 @@ POP_COOP_PINVOKE_FRAME macro endm +; Pushes a full TransitionBlock on the stack including argument registers and +; floating point argument registers. Used for exception throw helpers where we +; need to capture the complete register state including FP callee-saved registers. +; +; Stack layout (from high to low address after prologue): +; Return address (m_ReturnAddress) +; CalleeSavedRegisters (r15, r14, r13, r12, rbp, rbx, rsi, rdi - 64 bytes) <- TransitionBlock starts here +; Outgoing argument homes (32 bytes) +; FloatArgumentRegisters (xmm0-xmm3, 64 bytes) +; FP Callee-saved registers (xmm6-xmm15, 160 bytes) +; Shadow space for call (32 bytes) +; sp points here +; +; On exit, target contains the TransitionBlock pointer (CalleeSavedRegisters). +PUSH_COOP_PINVOKE_FRAME_WITH_FLOATS macro target + + PUSH_CALLEE_SAVED_REGISTERS + + ; Allocate space for: shadow for call (32) + FP callee-saved (160) + float args (64) + arg regs (32) + padding (8) = 296 bytes + ; Shadow space at offset 0 is reserved for the call to IL_Throw_Impl etc. + ; This makes RSP 16-byte aligned (8 + 64 + 296 = 368, and original RSP - 368 is 16-byte aligned) + alloc_stack 296 + + ; Save argument registers at offset 256 (32 + 160 + 64) + SAVE_ARGUMENT_REGISTERS 256 + + ; Save float argument registers at offset 192 (32 + 160) + SAVE_FLOAT_ARGUMENT_REGISTERS 192 + + ; Save FP callee-saved registers (xmm6-xmm15) at offset 32 (after shadow space) + ; RSP is 16-byte aligned, so offset 32, 48, 64, ... are all 16-byte aligned + ; AND these offsets are multiples of 16 as required by unwind codes + save_xmm128_postrsp xmm6, 20h + save_xmm128_postrsp xmm7, 30h + save_xmm128_postrsp xmm8, 40h + save_xmm128_postrsp xmm9, 50h + save_xmm128_postrsp xmm10, 60h + save_xmm128_postrsp xmm11, 70h + save_xmm128_postrsp xmm12, 80h + save_xmm128_postrsp xmm13, 90h + save_xmm128_postrsp xmm14, 0A0h + save_xmm128_postrsp xmm15, 0B0h + + END_PROLOGUE + + ; TransitionBlock pointer points to CalleeSavedRegisters at rsp + 296 + lea target, [rsp + 296] + + endm + ;; GC type flags GC_ALLOC_FINALIZE equ 1 diff --git a/src/coreclr/vm/amd64/asmhelpers.S b/src/coreclr/vm/amd64/asmhelpers.S index 9f5fd30792dcf4..3141735397593b 100644 --- a/src/coreclr/vm/amd64/asmhelpers.S +++ b/src/coreclr/vm/amd64/asmhelpers.S @@ -1913,3 +1913,48 @@ END_PROLOGUE NESTED_END CallJittedMethodRetDoubleDouble, _TEXT #endif // FEATURE_INTERPRETER + +// ------------------------------------------------------------------ +// Capture a transition block with register values and call the IL_Throw_Impl +// implementation written in C. +// +// Input state: +// rdi = Pointer to exception object +// ------------------------------------------------------------------ +NESTED_ENTRY IL_Throw, _TEXT, NoHandler + PROLOG_WITH_TRANSITION_BLOCK + // rdi already contains exception object + lea rsi, [rsp + __PWTB_TransitionBlock] + call C_FUNC(IL_Throw_Impl) + // Should never return + int3 +NESTED_END IL_Throw, _TEXT + +// ------------------------------------------------------------------ +// Capture a transition block with register values and call the IL_ThrowExact_Impl +// implementation written in C. +// +// Input state: +// rdi = Pointer to exception object +// ------------------------------------------------------------------ +NESTED_ENTRY IL_ThrowExact, _TEXT, NoHandler + PROLOG_WITH_TRANSITION_BLOCK + // rdi already contains exception object + lea rsi, [rsp + __PWTB_TransitionBlock] + call C_FUNC(IL_ThrowExact_Impl) + // Should never return + int3 +NESTED_END IL_ThrowExact, _TEXT + +// ------------------------------------------------------------------ +// Capture a transition block with register values and call the IL_Rethrow_Impl +// implementation written in C. +// ------------------------------------------------------------------ +NESTED_ENTRY IL_Rethrow, _TEXT, NoHandler + PROLOG_WITH_TRANSITION_BLOCK + lea rdi, [rsp + __PWTB_TransitionBlock] + call C_FUNC(IL_Rethrow_Impl) + // Should never return + int3 +NESTED_END IL_Rethrow, _TEXT + diff --git a/src/coreclr/vm/arm/asmhelpers.S b/src/coreclr/vm/arm/asmhelpers.S index 74cf86fd5f31b1..5065a6d15c4cb4 100644 --- a/src/coreclr/vm/arm/asmhelpers.S +++ b/src/coreclr/vm/arm/asmhelpers.S @@ -897,3 +897,48 @@ LEAF_ENTRY ThisPtrRetBufPrecodeWorker, _TEXT eor r0, r0, r1 EPILOG_BRANCH_REG r12 LEAF_END ThisPtrRetBufPrecodeWorker, _TEXT + +// ------------------------------------------------------------------ +// Capture a transition block with register values and call the IL_Throw_Impl +// implementation written in C. +// +// Input state: +// r0 = Pointer to exception object +// ------------------------------------------------------------------ +NESTED_ENTRY IL_Throw, _TEXT, NoHandler + PUSH_COOP_PINVOKE_FRAME_WITH_FLOATS r1 + // r0 already contains exception object + // r1 contains pointer to TransitionBlock + bl C_FUNC(IL_Throw_Impl) + // Should never return + EMIT_BREAKPOINT +NESTED_END IL_Throw, _TEXT + +// ------------------------------------------------------------------ +// Capture a transition block with register values and call the IL_ThrowExact_Impl +// implementation written in C. +// +// Input state: +// r0 = Pointer to exception object +// ------------------------------------------------------------------ +NESTED_ENTRY IL_ThrowExact, _TEXT, NoHandler + PUSH_COOP_PINVOKE_FRAME_WITH_FLOATS r1 + // r0 already contains exception object + // r1 contains pointer to TransitionBlock + bl C_FUNC(IL_ThrowExact_Impl) + // Should never return + EMIT_BREAKPOINT +NESTED_END IL_ThrowExact, _TEXT + +// ------------------------------------------------------------------ +// Capture a transition block with register values and call the IL_Rethrow_Impl +// implementation written in C. +// ------------------------------------------------------------------ +NESTED_ENTRY IL_Rethrow, _TEXT, NoHandler + PUSH_COOP_PINVOKE_FRAME_WITH_FLOATS r0 + // r0 contains pointer to TransitionBlock + bl C_FUNC(IL_Rethrow_Impl) + // Should never return + EMIT_BREAKPOINT +NESTED_END IL_Rethrow, _TEXT + diff --git a/src/coreclr/vm/arm64/asmhelpers.S b/src/coreclr/vm/arm64/asmhelpers.S index 046482ea6e996c..127a5c0a118245 100644 --- a/src/coreclr/vm/arm64/asmhelpers.S +++ b/src/coreclr/vm/arm64/asmhelpers.S @@ -2752,3 +2752,56 @@ NESTED_END CallJittedMethodRet4Vector128, _TEXT #endif // FEATURE_INTERPRETER + +// ------------------------------------------------------------------ +// Capture a transition block with register values and call the IL_Throw_Impl +// implementation written in C. +// +// Stack layout (from low to high address): +// sp+0: FloatArgumentRegisters (q0-q7, 128 bytes) +// sp+128: TransitionBlock start (176 bytes) +// - CalleeSavedRegisters (fp, lr, x19-x28 - 96 bytes) +// - padding (8 bytes) +// - x8 (8 bytes) +// - ArgumentRegisters (x0-x7, 64 bytes) +// +// Input state: +// x0 = Pointer to exception object +// ------------------------------------------------------------------ +NESTED_ENTRY IL_Throw, _TEXT, NoHandler + PUSH_COOP_PINVOKE_FRAME_WITH_FLOATS x1 + // x0 already contains exception object + // x1 contains pointer to TransitionBlock + bl C_FUNC(IL_Throw_Impl) + // Should never return + brk #0 +NESTED_END IL_Throw, _TEXT + +// ------------------------------------------------------------------ +// Capture a transition block with register values and call the IL_ThrowExact_Impl +// implementation written in C. +// +// Input state: +// x0 = Pointer to exception object +// ------------------------------------------------------------------ +NESTED_ENTRY IL_ThrowExact, _TEXT, NoHandler + PUSH_COOP_PINVOKE_FRAME_WITH_FLOATS x1 + // x0 already contains exception object + // x1 contains pointer to TransitionBlock + bl C_FUNC(IL_ThrowExact_Impl) + // Should never return + brk #0 +NESTED_END IL_ThrowExact, _TEXT + +// ------------------------------------------------------------------ +// Capture a transition block with register values and call the IL_Rethrow_Impl +// implementation written in C. +// ------------------------------------------------------------------ +NESTED_ENTRY IL_Rethrow, _TEXT, NoHandler + PUSH_COOP_PINVOKE_FRAME_WITH_FLOATS x0 + // x0 contains pointer to TransitionBlock + bl C_FUNC(IL_Rethrow_Impl) + // Should never return + brk #0 +NESTED_END IL_Rethrow, _TEXT + diff --git a/src/coreclr/vm/arm64/asmhelpers.asm b/src/coreclr/vm/arm64/asmhelpers.asm index 437ab1a527016d..71448ca3948d27 100644 --- a/src/coreclr/vm/arm64/asmhelpers.asm +++ b/src/coreclr/vm/arm64/asmhelpers.asm @@ -24,6 +24,9 @@ #endif IMPORT HijackHandler IMPORT ThrowControlForThread + IMPORT IL_Throw_Impl + IMPORT IL_ThrowExact_Impl + IMPORT IL_Rethrow_Impl #ifdef FEATURE_INTERPRETER IMPORT GetInterpThreadContextWithPossiblyMissingThreadOrCallStub IMPORT ExecuteInterpretedMethod @@ -3011,5 +3014,49 @@ CopyLoop #endif // FEATURE_INTERPRETER +; ------------------------------------------------------------------ +; Capture a transition block with register values and call the IL_Throw_Impl +; implementation written in C. +; +; Input state: +; x0 = Pointer to exception object +; ------------------------------------------------------------------ + NESTED_ENTRY IL_Throw + PUSH_COOP_PINVOKE_FRAME_WITH_FLOATS x1 + ; x0 already contains exception object + ; x1 contains pointer to TransitionBlock + bl IL_Throw_Impl + ; Should never return + brk #0 + NESTED_END IL_Throw + +; ------------------------------------------------------------------ +; Capture a transition block with register values and call the IL_ThrowExact_Impl +; implementation written in C. +; +; Input state: +; x0 = Pointer to exception object +; ------------------------------------------------------------------ + NESTED_ENTRY IL_ThrowExact + PUSH_COOP_PINVOKE_FRAME_WITH_FLOATS x1 + ; x0 already contains exception object + ; x1 contains pointer to TransitionBlock + bl IL_ThrowExact_Impl + ; Should never return + brk #0 + NESTED_END IL_ThrowExact + +; ------------------------------------------------------------------ +; Capture a transition block with register values and call the IL_Rethrow_Impl +; implementation written in C. +; ------------------------------------------------------------------ + NESTED_ENTRY IL_Rethrow + PUSH_COOP_PINVOKE_FRAME_WITH_FLOATS x0 + ; x0 contains pointer to TransitionBlock + bl IL_Rethrow_Impl + ; Should never return + brk #0 + NESTED_END IL_Rethrow + ; Must be at very end of file END diff --git a/src/coreclr/vm/arm64/asmmacros.h b/src/coreclr/vm/arm64/asmmacros.h index a11067633ab82e..93778d775f87c9 100644 --- a/src/coreclr/vm/arm64/asmmacros.h +++ b/src/coreclr/vm/arm64/asmmacros.h @@ -204,6 +204,55 @@ OFFSETOF__ee_alloc_context EQU OFFSETOF__RuntimeThreadLocals__ee_alloc_context EPILOG_RESTORE_REG_PAIR fp, lr, #176! MEND +; Pushes a full TransitionBlock on the stack including argument registers and +; floating point argument registers. Used for exception throw helpers where we +; need to capture the complete register state including FP callee-saved registers. +; +; Stack layout (from low to high address): +; sp+0: FP callee-saved registers (d8-d15, 64 bytes) +; sp+64: FloatArgumentRegisters (q0-q7, 128 bytes) +; sp+192: TransitionBlock start (176 bytes) +; - CalleeSavedRegisters (fp, lr, x19-x28 - 96 bytes) +; - padding (8 bytes) +; - x8 (8 bytes) +; - ArgumentRegisters (x0-x7, 64 bytes) +; +; On exit, $Target contains the TransitionBlock pointer (sp+192). + MACRO + PUSH_COOP_PINVOKE_FRAME_WITH_FLOATS $Target + + PROLOG_SAVE_REG_PAIR fp, lr, #-176! + + ; Spill callee saved registers + PROLOG_SAVE_REG_PAIR x19, x20, #16 + PROLOG_SAVE_REG_PAIR x21, x22, #32 + PROLOG_SAVE_REG_PAIR x23, x24, #48 + PROLOG_SAVE_REG_PAIR x25, x26, #64 + PROLOG_SAVE_REG_PAIR x27, x28, #80 + + ; Allocate space for FloatArgumentRegisters (128) + FP callee-saved (64) = 192 bytes + PROLOG_STACK_ALLOC 192 + + ; Save argument registers (x8, x0-x7) at offset 296 from sp (192 + 104) + SAVE_ARGUMENT_REGISTERS sp, 296 + + ; Save floating point argument registers (q0-q7) at sp+64 + SAVE_FLOAT_ARGUMENT_REGISTERS sp, 64 + + ; Save FP callee-saved registers (d8-d15) at sp+0 + str d8, [sp, #0] + str d9, [sp, #8] + str d10, [sp, #16] + str d11, [sp, #24] + str d12, [sp, #32] + str d13, [sp, #40] + str d14, [sp, #48] + str d15, [sp, #56] + + ; Set target to TransitionBlock pointer + add $Target, sp, #192 + MEND + #define GC_ALLOC_FINALIZE 1 ;----------------------------------------------------------------------------- diff --git a/src/coreclr/vm/excep.cpp b/src/coreclr/vm/excep.cpp index 1df93c2061ca0c..8b670c6ea4911d 100644 --- a/src/coreclr/vm/excep.cpp +++ b/src/coreclr/vm/excep.cpp @@ -10746,43 +10746,390 @@ void SoftwareExceptionFrame::UpdateContextFromTransitionBlock(TransitionBlock *p m_ReturnAddress = pTransitionBlock->m_ReturnAddress; } -#endif // TARGET_X86 +#elif defined(TARGET_AMD64) -// -// Init a new frame -// -void SoftwareExceptionFrame::Init() +void SoftwareExceptionFrame::UpdateContextFromTransitionBlock(TransitionBlock *pTransitionBlock) { - WRAPPER_NO_CONTRACT; + LIMITED_METHOD_CONTRACT; - // On x86 we initialize the context state from transition block in - // UpdateContextFromTransitionBlock method. -#ifndef TARGET_X86 -#define CALLEE_SAVED_REGISTER(regname) m_ContextPointers.regname = NULL; - ENUM_CALLEE_SAVED_REGISTERS(); -#undef CALLEE_SAVED_REGISTER +#ifdef UNIX_AMD64_ABI + // On Unix AMD64, there are no non-volatile FP registers, so we only need + // control registers and integer callee-saved registers. We don't need to + // capture argument registers or FP state for exception handling. + m_Context.ContextFlags = CONTEXT_CONTROL | CONTEXT_INTEGER; + m_Context.SegCs = 0; + m_Context.SegSs = 0; + m_Context.EFlags = 0; + m_Context.Rax = 0; +#else + // On Windows AMD64, we need FP state because xmm6-xmm15 are non-volatile + m_Context.ContextFlags = CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_FLOATING_POINT; + m_Context.SegCs = 0; + m_Context.SegSs = 0; + m_Context.EFlags = 0; -#ifndef TARGET_UNIX - Thread::VirtualUnwindCallFrame(&m_Context, &m_ContextPointers); -#else // !TARGET_UNIX - BOOL success = PAL_VirtualUnwind(&m_Context, &m_ContextPointers); - if (!success) - { - _ASSERTE(!"SoftwareExceptionFrame::Init failed"); - EEPOLICY_HANDLE_FATAL_ERROR(COR_E_EXECUTIONENGINE); - } -#endif // !TARGET_UNIX + // Read FP callee-saved registers (xmm6-xmm15) from the stack + // They are stored at negative offsets from TransitionBlock: + // Layout: [shadow (32)] [xmm6-xmm15 (160)] [xmm0-xmm3 (64)] [arg regs (32)] [padding (8)] [CalleeSavedRegs] [RetAddr] + // xmm6 is at sp+32, TransitionBlock is at sp+296, so xmm6 is at TransitionBlock - 264 + M128A *pFpCalleeSaved = (M128A*)((BYTE*)pTransitionBlock - 264); + + m_Context.Xmm6 = pFpCalleeSaved[0]; + m_Context.Xmm7 = pFpCalleeSaved[1]; + m_Context.Xmm8 = pFpCalleeSaved[2]; + m_Context.Xmm9 = pFpCalleeSaved[3]; + m_Context.Xmm10 = pFpCalleeSaved[4]; + m_Context.Xmm11 = pFpCalleeSaved[5]; + m_Context.Xmm12 = pFpCalleeSaved[6]; + m_Context.Xmm13 = pFpCalleeSaved[7]; + m_Context.Xmm14 = pFpCalleeSaved[8]; + m_Context.Xmm15 = pFpCalleeSaved[9]; + + // Initialize FP control/status in FltSave - this is what fxrstor restores from + m_Context.FltSave.ControlWord = 0x27F; // Default x87 control word + m_Context.FltSave.MxCsr = 0x1F80; // Default MXCSR value (all exceptions masked) + m_Context.FltSave.MxCsr_Mask = 0x1FFF; // MXCSR mask + m_Context.MxCsr = 0x1F80; // Default MXCSR value (all exceptions masked) +#endif -#define CALLEE_SAVED_REGISTER(regname) if (m_ContextPointers.regname == NULL) m_ContextPointers.regname = &m_Context.regname; +#define CALLEE_SAVED_REGISTER(reg) \ + m_Context.reg = pTransitionBlock->m_calleeSavedRegisters.reg; \ + m_ContextPointers.reg = &m_Context.reg; ENUM_CALLEE_SAVED_REGISTERS(); #undef CALLEE_SAVED_REGISTER - _ASSERTE(ExecutionManager::IsManagedCode(::GetIP(&m_Context))); + m_Context.Rsp = (UINT_PTR)(pTransitionBlock + 1); + m_Context.Rip = pTransitionBlock->m_ReturnAddress; + m_ReturnAddress = pTransitionBlock->m_ReturnAddress; +} - m_ReturnAddress = ::GetIP(&m_Context); -#endif // !TARGET_X86 +#elif defined(TARGET_ARM) + +void SoftwareExceptionFrame::UpdateContextFromTransitionBlock(TransitionBlock *pTransitionBlock) +{ + LIMITED_METHOD_CONTRACT; + + m_Context.ContextFlags = CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_FLOATING_POINT; + + // Copy argument registers (R0-R3) + m_Context.R0 = pTransitionBlock->m_argumentRegisters.r[0]; + m_Context.R1 = pTransitionBlock->m_argumentRegisters.r[1]; + m_Context.R2 = pTransitionBlock->m_argumentRegisters.r[2]; + m_Context.R3 = pTransitionBlock->m_argumentRegisters.r[3]; + + // Copy callee-saved registers (R4-R11, Lr) + m_Context.R4 = pTransitionBlock->m_calleeSavedRegisters.r4; + m_Context.R5 = pTransitionBlock->m_calleeSavedRegisters.r5; + m_Context.R6 = pTransitionBlock->m_calleeSavedRegisters.r6; + m_Context.R7 = pTransitionBlock->m_calleeSavedRegisters.r7; + m_Context.R8 = pTransitionBlock->m_calleeSavedRegisters.r8; + m_Context.R9 = pTransitionBlock->m_calleeSavedRegisters.r9; + m_Context.R10 = pTransitionBlock->m_calleeSavedRegisters.r10; + m_Context.R11 = pTransitionBlock->m_calleeSavedRegisters.r11; + m_Context.Lr = pTransitionBlock->m_calleeSavedRegisters.r14; // r14 is link register + + // Copy floating point argument registers (d0-d7 / s0-s15) + FloatArgumentRegisters *pFloatArgs = (FloatArgumentRegisters*)((BYTE*)pTransitionBlock + TransitionBlock::GetOffsetOfFloatArgumentRegisters()); + for (int i = 0; i < 8; i++) + { + m_Context.D[i] = pFloatArgs->d[i]; + } + + // Read FP callee-saved registers (d8-d15) from the stack + // They are stored at negative offset from TransitionBlock: + // Layout: [d8-d15 (64 bytes)] [padding (4)] [d0-d7 (64 bytes)] [padding (4)] [TransitionBlock] + // FP callee-saved are at TransitionBlock - 136 (64 + 4 + 64 + 4) + UINT64 *pFpCalleeSaved = (UINT64*)((BYTE*)pTransitionBlock - 136); + for (int i = 0; i < 8; i++) + { + m_Context.D[8 + i] = pFpCalleeSaved[i]; + } + + // Initialize FP status/control register + m_Context.Fpscr = 0; + + // Set up context pointers for callee-saved registers + m_ContextPointers.R4 = &m_Context.R4; + m_ContextPointers.R5 = &m_Context.R5; + m_ContextPointers.R6 = &m_Context.R6; + m_ContextPointers.R7 = &m_Context.R7; + m_ContextPointers.R8 = &m_Context.R8; + m_ContextPointers.R9 = &m_Context.R9; + m_ContextPointers.R10 = &m_Context.R10; + m_ContextPointers.R11 = &m_Context.R11; + m_ContextPointers.Lr = &m_Context.Lr; + + m_Context.Sp = (UINT_PTR)(pTransitionBlock + 1); + m_Context.Pc = pTransitionBlock->m_ReturnAddress; + m_ReturnAddress = pTransitionBlock->m_ReturnAddress; } +#elif defined(TARGET_ARM64) + +void SoftwareExceptionFrame::UpdateContextFromTransitionBlock(TransitionBlock *pTransitionBlock) +{ + LIMITED_METHOD_CONTRACT; + + m_Context.ContextFlags = CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_FLOATING_POINT; + + // Copy argument registers (X0-X7) + for (int i = 0; i < 8; i++) + { + m_Context.X[i] = pTransitionBlock->m_argumentRegisters.x[i]; + } + + // Copy return buffer register (X8) + m_Context.X8 = pTransitionBlock->m_x8RetBuffReg; + + // Copy callee-saved registers (X19-X28) + m_Context.X19 = pTransitionBlock->m_calleeSavedRegisters.x19; + m_Context.X20 = pTransitionBlock->m_calleeSavedRegisters.x20; + m_Context.X21 = pTransitionBlock->m_calleeSavedRegisters.x21; + m_Context.X22 = pTransitionBlock->m_calleeSavedRegisters.x22; + m_Context.X23 = pTransitionBlock->m_calleeSavedRegisters.x23; + m_Context.X24 = pTransitionBlock->m_calleeSavedRegisters.x24; + m_Context.X25 = pTransitionBlock->m_calleeSavedRegisters.x25; + m_Context.X26 = pTransitionBlock->m_calleeSavedRegisters.x26; + m_Context.X27 = pTransitionBlock->m_calleeSavedRegisters.x27; + m_Context.X28 = pTransitionBlock->m_calleeSavedRegisters.x28; + + // Copy frame pointer and link register + m_Context.Fp = pTransitionBlock->m_calleeSavedRegisters.x29; + m_Context.Lr = pTransitionBlock->m_calleeSavedRegisters.x30; + + // Copy floating point argument registers (V0-V7) + FloatArgumentRegisters *pFloatArgs = (FloatArgumentRegisters*)((BYTE*)pTransitionBlock + TransitionBlock::GetOffsetOfFloatArgumentRegisters()); + for (int i = 0; i < 8; i++) + { + m_Context.V[i] = pFloatArgs->q[i]; + } + + // Read FP callee-saved registers (d8-d15) from the stack + // They are stored at negative offset from TransitionBlock: + // Layout: [d8-d15 (64 bytes)] [q0-q7 (128 bytes)] [TransitionBlock] + // FP callee-saved are at TransitionBlock - 192 (64 + 128) + UINT64 *pFpCalleeSaved = (UINT64*)((BYTE*)pTransitionBlock - 192); + m_Context.V[8].Low = pFpCalleeSaved[0]; + m_Context.V[8].High = 0; + m_Context.V[9].Low = pFpCalleeSaved[1]; + m_Context.V[9].High = 0; + m_Context.V[10].Low = pFpCalleeSaved[2]; + m_Context.V[10].High = 0; + m_Context.V[11].Low = pFpCalleeSaved[3]; + m_Context.V[11].High = 0; + m_Context.V[12].Low = pFpCalleeSaved[4]; + m_Context.V[12].High = 0; + m_Context.V[13].Low = pFpCalleeSaved[5]; + m_Context.V[13].High = 0; + m_Context.V[14].Low = pFpCalleeSaved[6]; + m_Context.V[14].High = 0; + m_Context.V[15].Low = pFpCalleeSaved[7]; + m_Context.V[15].High = 0; + + // Initialize remaining V registers (V16-V31) to zero - these are caller-saved + for (int i = 16; i < 32; i++) + { + m_Context.V[i].Low = 0; + m_Context.V[i].High = 0; + } + // Initialize FP control/status registers + m_Context.Fpcr = 0; + m_Context.Fpsr = 0; + + // Set up context pointers for callee-saved registers + m_ContextPointers.X19 = &m_Context.X19; + m_ContextPointers.X20 = &m_Context.X20; + m_ContextPointers.X21 = &m_Context.X21; + m_ContextPointers.X22 = &m_Context.X22; + m_ContextPointers.X23 = &m_Context.X23; + m_ContextPointers.X24 = &m_Context.X24; + m_ContextPointers.X25 = &m_Context.X25; + m_ContextPointers.X26 = &m_Context.X26; + m_ContextPointers.X27 = &m_Context.X27; + m_ContextPointers.X28 = &m_Context.X28; + m_ContextPointers.Fp = &m_Context.Fp; + m_ContextPointers.Lr = &m_Context.Lr; + + m_Context.Sp = (UINT_PTR)(pTransitionBlock + 1); + m_Context.Pc = pTransitionBlock->m_ReturnAddress; + m_ReturnAddress = pTransitionBlock->m_ReturnAddress; +} + +#elif defined(TARGET_LOONGARCH64) + +void SoftwareExceptionFrame::UpdateContextFromTransitionBlock(TransitionBlock *pTransitionBlock) +{ + LIMITED_METHOD_CONTRACT; + + m_Context.ContextFlags = CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_FLOATING_POINT; + + // Copy argument registers (A0-A7) + m_Context.A0 = pTransitionBlock->m_argumentRegisters.a[0]; + m_Context.A1 = pTransitionBlock->m_argumentRegisters.a[1]; + m_Context.A2 = pTransitionBlock->m_argumentRegisters.a[2]; + m_Context.A3 = pTransitionBlock->m_argumentRegisters.a[3]; + m_Context.A4 = pTransitionBlock->m_argumentRegisters.a[4]; + m_Context.A5 = pTransitionBlock->m_argumentRegisters.a[5]; + m_Context.A6 = pTransitionBlock->m_argumentRegisters.a[6]; + m_Context.A7 = pTransitionBlock->m_argumentRegisters.a[7]; + + // Copy callee-saved registers (Fp, Ra, S0-S8) + m_Context.Fp = pTransitionBlock->m_calleeSavedRegisters.fp; + m_Context.Ra = pTransitionBlock->m_calleeSavedRegisters.ra; + m_Context.S0 = pTransitionBlock->m_calleeSavedRegisters.s0; + m_Context.S1 = pTransitionBlock->m_calleeSavedRegisters.s1; + m_Context.S2 = pTransitionBlock->m_calleeSavedRegisters.s2; + m_Context.S3 = pTransitionBlock->m_calleeSavedRegisters.s3; + m_Context.S4 = pTransitionBlock->m_calleeSavedRegisters.s4; + m_Context.S5 = pTransitionBlock->m_calleeSavedRegisters.s5; + m_Context.S6 = pTransitionBlock->m_calleeSavedRegisters.s6; + m_Context.S7 = pTransitionBlock->m_calleeSavedRegisters.s7; + m_Context.S8 = pTransitionBlock->m_calleeSavedRegisters.s8; + + // Copy floating point argument registers (fa0-fa7) + // F[] array in CONTEXT is 4*32 elements for LSX/LASX support. + // Each FP register takes 4 slots (for 256-bit LASX vectors). + // For 64-bit doubles, we only use the first slot of each register. + FloatArgumentRegisters *pFloatArgs = (FloatArgumentRegisters*)((BYTE*)pTransitionBlock + TransitionBlock::GetOffsetOfFloatArgumentRegisters()); + for (int i = 0; i < 8; i++) + { + memcpy(&m_Context.F[i * 4], &pFloatArgs->f[i], sizeof(double)); + } + + // Read FP callee-saved registers (f24-f31) from the stack + // They are stored at negative offset from TransitionBlock: + // Layout: [f24-f31 (64 bytes)] [fa0-fa7 (64 bytes)] [TransitionBlock] + // FP callee-saved are at TransitionBlock - 128 (64 + 64) + UINT64 *pFpCalleeSaved = (UINT64*)((BYTE*)pTransitionBlock - 128); + for (int i = 0; i < 8; i++) + { + // f24-f31 map to indices 24-31 in the F array, each taking 4 slots + memcpy(&m_Context.F[(24 + i) * 4], &pFpCalleeSaved[i], sizeof(double)); + } + + // Initialize remaining F registers (f8-f23) to zero + for (int i = 8; i < 24; i++) + { + memset(&m_Context.F[i * 4], 0, sizeof(double) * 4); + } + // Initialize FP control/status register + m_Context.Fcsr = 0; + + // Set up context pointers for callee-saved registers + m_ContextPointers.S0 = &m_Context.S0; + m_ContextPointers.S1 = &m_Context.S1; + m_ContextPointers.S2 = &m_Context.S2; + m_ContextPointers.S3 = &m_Context.S3; + m_ContextPointers.S4 = &m_Context.S4; + m_ContextPointers.S5 = &m_Context.S5; + m_ContextPointers.S6 = &m_Context.S6; + m_ContextPointers.S7 = &m_Context.S7; + m_ContextPointers.S8 = &m_Context.S8; + m_ContextPointers.Fp = &m_Context.Fp; + m_ContextPointers.Ra = &m_Context.Ra; + + m_Context.Sp = (UINT_PTR)(pTransitionBlock + 1); + m_Context.Pc = pTransitionBlock->m_ReturnAddress; + m_ReturnAddress = pTransitionBlock->m_ReturnAddress; +} + +#elif defined(TARGET_RISCV64) + +void SoftwareExceptionFrame::UpdateContextFromTransitionBlock(TransitionBlock *pTransitionBlock) +{ + LIMITED_METHOD_CONTRACT; + + m_Context.ContextFlags = CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_FLOATING_POINT; + + // Copy argument registers (A0-A7) + m_Context.A0 = pTransitionBlock->m_argumentRegisters.a[0]; + m_Context.A1 = pTransitionBlock->m_argumentRegisters.a[1]; + m_Context.A2 = pTransitionBlock->m_argumentRegisters.a[2]; + m_Context.A3 = pTransitionBlock->m_argumentRegisters.a[3]; + m_Context.A4 = pTransitionBlock->m_argumentRegisters.a[4]; + m_Context.A5 = pTransitionBlock->m_argumentRegisters.a[5]; + m_Context.A6 = pTransitionBlock->m_argumentRegisters.a[6]; + m_Context.A7 = pTransitionBlock->m_argumentRegisters.a[7]; + + // Copy callee-saved registers (Fp, Ra, S1-S11, Tp, Gp) + m_Context.Fp = pTransitionBlock->m_calleeSavedRegisters.fp; + m_Context.Ra = pTransitionBlock->m_calleeSavedRegisters.ra; + m_Context.S1 = pTransitionBlock->m_calleeSavedRegisters.s1; + m_Context.S2 = pTransitionBlock->m_calleeSavedRegisters.s2; + m_Context.S3 = pTransitionBlock->m_calleeSavedRegisters.s3; + m_Context.S4 = pTransitionBlock->m_calleeSavedRegisters.s4; + m_Context.S5 = pTransitionBlock->m_calleeSavedRegisters.s5; + m_Context.S6 = pTransitionBlock->m_calleeSavedRegisters.s6; + m_Context.S7 = pTransitionBlock->m_calleeSavedRegisters.s7; + m_Context.S8 = pTransitionBlock->m_calleeSavedRegisters.s8; + m_Context.S9 = pTransitionBlock->m_calleeSavedRegisters.s9; + m_Context.S10 = pTransitionBlock->m_calleeSavedRegisters.s10; + m_Context.S11 = pTransitionBlock->m_calleeSavedRegisters.s11; + m_Context.Tp = pTransitionBlock->m_calleeSavedRegisters.tp; + m_Context.Gp = pTransitionBlock->m_calleeSavedRegisters.gp; + + // Initialize all F registers to zero first + memset(m_Context.F, 0, sizeof(m_Context.F)); + // Copy floating point argument registers (fa0-fa7) + FloatArgumentRegisters *pFloatArgs = (FloatArgumentRegisters*)((BYTE*)pTransitionBlock + TransitionBlock::GetOffsetOfFloatArgumentRegisters()); + for (int i = 0; i < 8; i++) + { + // F[10-17] are fa0-fa7 in RISC-V register naming + memcpy(&m_Context.F[10 + i], &pFloatArgs->f[i], sizeof(double)); + } + + // Read FP callee-saved registers (fs0-fs11) from the stack + // They are stored at negative offset from TransitionBlock: + // Layout: [fs0-fs11 (96 bytes)] [fa0-fa7 (64 bytes)] [TransitionBlock] + // FP callee-saved are at TransitionBlock - 160 (96 + 64) + // RISC-V FP callee-saved: fs0=f8, fs1=f9, fs2-fs11=f18-f27 + UINT64 *pFpCalleeSaved = (UINT64*)((BYTE*)pTransitionBlock - 160); + memcpy(&m_Context.F[8], &pFpCalleeSaved[0], sizeof(double)); // fs0 = f8 + memcpy(&m_Context.F[9], &pFpCalleeSaved[1], sizeof(double)); // fs1 = f9 + for (int i = 0; i < 10; i++) + { + memcpy(&m_Context.F[18 + i], &pFpCalleeSaved[2 + i], sizeof(double)); // fs2-fs11 = f18-f27 + } + + // Initialize FP control/status register + m_Context.Fcsr = 0; + + // Set up context pointers for callee-saved registers + m_ContextPointers.S1 = &m_Context.S1; + m_ContextPointers.S2 = &m_Context.S2; + m_ContextPointers.S3 = &m_Context.S3; + m_ContextPointers.S4 = &m_Context.S4; + m_ContextPointers.S5 = &m_Context.S5; + m_ContextPointers.S6 = &m_Context.S6; + m_ContextPointers.S7 = &m_Context.S7; + m_ContextPointers.S8 = &m_Context.S8; + m_ContextPointers.S9 = &m_Context.S9; + m_ContextPointers.S10 = &m_Context.S10; + m_ContextPointers.S11 = &m_Context.S11; + m_ContextPointers.Fp = &m_Context.Fp; + m_ContextPointers.Gp = &m_Context.Gp; + m_ContextPointers.Tp = &m_Context.Tp; + m_ContextPointers.Ra = &m_Context.Ra; + + m_Context.Sp = (UINT_PTR)(pTransitionBlock + 1); + m_Context.Pc = pTransitionBlock->m_ReturnAddress; + m_ReturnAddress = pTransitionBlock->m_ReturnAddress; +} + +#elif defined(TARGET_WASM) + +void SoftwareExceptionFrame::UpdateContextFromTransitionBlock(TransitionBlock *pTransitionBlock) +{ + LIMITED_METHOD_CONTRACT; + + // WASM cannot capture execution context, so just zero everything + memset(&m_Context, 0, sizeof(m_Context)); + memset(&m_ContextPointers, 0, sizeof(m_ContextPointers)); + m_ReturnAddress = 0; +} + +#endif // TARGET_X86 + // // Init and Link in a new frame // @@ -10790,8 +11137,6 @@ void SoftwareExceptionFrame::InitAndLink(Thread *pThread) { WRAPPER_NO_CONTRACT; - Init(); - Push(pThread); } diff --git a/src/coreclr/vm/frames.h b/src/coreclr/vm/frames.h index bbe17c2ef9e885..8ec28c47132c6e 100644 --- a/src/coreclr/vm/frames.h +++ b/src/coreclr/vm/frames.h @@ -1028,13 +1028,11 @@ class SoftwareExceptionFrame : public Frame public: #ifndef DACCESS_COMPILE - SoftwareExceptionFrame() : Frame(FrameIdentifier::SoftwareExceptionFrame) { + SoftwareExceptionFrame() : Frame(FrameIdentifier::SoftwareExceptionFrame), m_ReturnAddress(0) { LIMITED_METHOD_CONTRACT; } -#ifdef TARGET_X86 void UpdateContextFromTransitionBlock(TransitionBlock *pTransitionBlock); -#endif #endif TADDR GetReturnAddressPtr_Impl() @@ -1044,7 +1042,6 @@ class SoftwareExceptionFrame : public Frame } #ifndef DACCESS_COMPILE - void Init(); void InitAndLink(Thread *pThread); #endif diff --git a/src/coreclr/vm/i386/asmhelpers.S b/src/coreclr/vm/i386/asmhelpers.S index 62578e542c87c2..29b76b1f220ffb 100644 --- a/src/coreclr/vm/i386/asmhelpers.S +++ b/src/coreclr/vm/i386/asmhelpers.S @@ -1032,7 +1032,7 @@ LEAF_ENTRY IL_Throw, _TEXT CHECK_STACK_ALIGNMENT - call C_FUNC(IL_Throw_x86) + call C_FUNC(IL_Throw_Impl) add esp, STACK_ALIGN_PADDING #undef STACK_ALIGN_PADDING @@ -1058,7 +1058,7 @@ LEAF_ENTRY IL_ThrowExact, _TEXT CHECK_STACK_ALIGNMENT - call C_FUNC(IL_ThrowExact_x86) + call C_FUNC(IL_ThrowExact_Impl) add esp, STACK_ALIGN_PADDING #undef STACK_ALIGN_PADDING @@ -1081,7 +1081,7 @@ LEAF_ENTRY IL_Rethrow, _TEXT CHECK_STACK_ALIGNMENT - call C_FUNC(IL_Rethrow_x86) + call C_FUNC(IL_Rethrow_Impl) add esp, STACK_ALIGN_PADDING #undef STACK_ALIGN_PADDING diff --git a/src/coreclr/vm/i386/asmhelpers.asm b/src/coreclr/vm/i386/asmhelpers.asm index 2b74e0ac6d9b51..71e6edfeb3a443 100644 --- a/src/coreclr/vm/i386/asmhelpers.asm +++ b/src/coreclr/vm/i386/asmhelpers.asm @@ -82,9 +82,9 @@ EXTERN g_chained_lookup_miss_counter:DWORD EXTERN g_dispatch_cache_chain_success_counter:DWORD endif -EXTERN @IL_Throw_x86@8:PROC -EXTERN @IL_ThrowExact_x86@8:PROC -EXTERN @IL_Rethrow_x86@4:PROC +EXTERN @IL_Throw_Impl@8:PROC +EXTERN @IL_ThrowExact_Impl@8:PROC +EXTERN @IL_Rethrow_Impl@4:PROC UNREFERENCED macro arg local unref @@ -1635,7 +1635,7 @@ FASTCALL_FUNC IL_Throw, 4 STUB_PROLOG mov edx, esp - call @IL_Throw_x86@8 + call @IL_Throw_Impl@8 STUB_EPILOG ret 4 @@ -1652,7 +1652,7 @@ FASTCALL_FUNC IL_ThrowExact, 4 STUB_PROLOG mov edx, esp - call @IL_ThrowExact_x86@8 + call @IL_ThrowExact_Impl@8 STUB_EPILOG ret 4 @@ -1666,7 +1666,7 @@ FASTCALL_FUNC IL_Rethrow, 0 STUB_PROLOG mov ecx, esp - call @IL_Rethrow_x86@4 + call @IL_Rethrow_Impl@4 STUB_EPILOG ret 4 diff --git a/src/coreclr/vm/jithelpers.cpp b/src/coreclr/vm/jithelpers.cpp index fa2075019598ae..0520a652ff4ac1 100644 --- a/src/coreclr/vm/jithelpers.cpp +++ b/src/coreclr/vm/jithelpers.cpp @@ -745,9 +745,6 @@ HCIMPL1(EnregisteredTypeHandle, JIT_GetClassFromMethodParam, MethodDesc* pMD) HCIMPLEND #include - - - //======================================================================== // // EXCEPTION HELPERS @@ -765,12 +762,8 @@ HCIMPLEND /*************************************************************/ -#if defined(TARGET_X86) EXTERN_C FCDECL1(void, IL_Throw, Object* obj); -EXTERN_C HCIMPL2(void, IL_Throw_x86, Object* obj, TransitionBlock* transitionBlock) -#else -HCIMPL1(void, IL_Throw, Object* obj) -#endif +EXTERN_C HCIMPL2(void, IL_Throw_Impl, Object* obj, TransitionBlock* transitionBlock) { FCALL_CONTRACT; @@ -782,11 +775,7 @@ HCIMPL1(void, IL_Throw, Object* obj) Thread *pThread = GetThread(); SoftwareExceptionFrame exceptionFrame; -#ifdef TARGET_X86 exceptionFrame.UpdateContextFromTransitionBlock(transitionBlock); -#else - RtlCaptureContext(exceptionFrame.GetContext()); -#endif exceptionFrame.InitAndLink(pThread); FC_CAN_TRIGGER_GC(); @@ -804,23 +793,15 @@ HCIMPLEND /*************************************************************/ -#if defined(TARGET_X86) EXTERN_C FCDECL0(void, IL_Rethrow); -EXTERN_C HCIMPL1(void, IL_Rethrow_x86, TransitionBlock* transitionBlock) -#else -HCIMPL0(void, IL_Rethrow) -#endif +EXTERN_C HCIMPL1(void, IL_Rethrow_Impl, TransitionBlock* transitionBlock) { FCALL_CONTRACT; Thread *pThread = GetThread(); SoftwareExceptionFrame exceptionFrame; -#ifdef TARGET_X86 exceptionFrame.UpdateContextFromTransitionBlock(transitionBlock); -#else - RtlCaptureContext(exceptionFrame.GetContext()); -#endif exceptionFrame.InitAndLink(pThread); FC_CAN_TRIGGER_GC(); @@ -832,12 +813,8 @@ HCIMPL0(void, IL_Rethrow) } HCIMPLEND -#if defined(TARGET_X86) EXTERN_C FCDECL1(void, IL_ThrowExact, Object* obj); -EXTERN_C HCIMPL2(void, IL_ThrowExact_x86, Object* obj, TransitionBlock* transitionBlock) -#else -HCIMPL1(void, IL_ThrowExact, Object* obj) -#endif +EXTERN_C HCIMPL2(void, IL_ThrowExact_Impl, Object* obj, TransitionBlock* transitionBlock) { FCALL_CONTRACT; @@ -850,11 +827,7 @@ HCIMPL1(void, IL_ThrowExact, Object* obj) Thread *pThread = GetThread(); SoftwareExceptionFrame exceptionFrame; -#ifdef TARGET_X86 exceptionFrame.UpdateContextFromTransitionBlock(transitionBlock); -#else - RtlCaptureContext(exceptionFrame.GetContext()); -#endif exceptionFrame.InitAndLink(pThread); FC_CAN_TRIGGER_GC(); @@ -866,6 +839,31 @@ HCIMPL1(void, IL_ThrowExact, Object* obj) } HCIMPLEND +#ifdef TARGET_WASM +// WASM doesn't have assembly stubs, so provide thin wrapper entry points +// that call the _Impl functions with NULL (which zeros the context) +HCIMPL1(void, IL_Throw, Object* obj) +{ + FCALL_CONTRACT; + IL_Throw_Impl(obj, NULL); +} +HCIMPLEND + +HCIMPL0(void, IL_Rethrow) +{ + FCALL_CONTRACT; + IL_Rethrow_Impl(NULL); +} +HCIMPLEND + +HCIMPL1(void, IL_ThrowExact, Object* obj) +{ + FCALL_CONTRACT; + IL_ThrowExact_Impl(obj, NULL); +} +HCIMPLEND +#endif // TARGET_WASM + #ifndef STATUS_STACK_BUFFER_OVERRUN // Not defined yet in CESDK includes # define STATUS_STACK_BUFFER_OVERRUN ((NTSTATUS)0xC0000409L) #endif diff --git a/src/coreclr/vm/loongarch64/asmhelpers.S b/src/coreclr/vm/loongarch64/asmhelpers.S index 457f21b34ef022..9f424c39dd30f5 100644 --- a/src/coreclr/vm/loongarch64/asmhelpers.S +++ b/src/coreclr/vm/loongarch64/asmhelpers.S @@ -1020,3 +1020,48 @@ LEAF_ENTRY ThisPtrRetBufPrecodeWorker, _TEXT move $a1, $t0 // Move temp register to first arg register for static method with return buffer EPILOG_BRANCH_REG $METHODDESC_REGISTER LEAF_END ThisPtrRetBufPrecodeWorker, _TEXT + +// ------------------------------------------------------------------ +// Capture a transition block with register values and call the IL_Throw_Impl +// implementation written in C. +// +// Input state: +// $a0 = Pointer to exception object +// ------------------------------------------------------------------ +NESTED_ENTRY IL_Throw, _TEXT, NoHandler + PUSH_COOP_PINVOKE_FRAME_WITH_FLOATS $a1 + // $a0 already contains exception object + // $a1 contains pointer to TransitionBlock + bl C_FUNC(IL_Throw_Impl) + // Should never return + break 0 +NESTED_END IL_Throw, _TEXT + +// ------------------------------------------------------------------ +// Capture a transition block with register values and call the IL_ThrowExact_Impl +// implementation written in C. +// +// Input state: +// $a0 = Pointer to exception object +// ------------------------------------------------------------------ +NESTED_ENTRY IL_ThrowExact, _TEXT, NoHandler + PUSH_COOP_PINVOKE_FRAME_WITH_FLOATS $a1 + // $a0 already contains exception object + // $a1 contains pointer to TransitionBlock + bl C_FUNC(IL_ThrowExact_Impl) + // Should never return + break 0 +NESTED_END IL_ThrowExact, _TEXT + +// ------------------------------------------------------------------ +// Capture a transition block with register values and call the IL_Rethrow_Impl +// implementation written in C. +// ------------------------------------------------------------------ +NESTED_ENTRY IL_Rethrow, _TEXT, NoHandler + PUSH_COOP_PINVOKE_FRAME_WITH_FLOATS $a0 + // $a0 contains pointer to TransitionBlock + bl C_FUNC(IL_Rethrow_Impl) + // Should never return + break 0 +NESTED_END IL_Rethrow, _TEXT + diff --git a/src/coreclr/vm/riscv64/asmhelpers.S b/src/coreclr/vm/riscv64/asmhelpers.S index 64b24957211ec8..0d26ef514d4f58 100644 --- a/src/coreclr/vm/riscv64/asmhelpers.S +++ b/src/coreclr/vm/riscv64/asmhelpers.S @@ -878,6 +878,50 @@ LEAF_ENTRY ThisPtrRetBufPrecodeWorker, _TEXT EPILOG_BRANCH_REG t2 LEAF_END ThisPtrRetBufPrecodeWorker, _TEXT +// ------------------------------------------------------------------ +// Capture a transition block with register values and call the IL_Throw_Impl +// implementation written in C. +// +// Input state: +// a0 = Pointer to exception object +// ------------------------------------------------------------------ +NESTED_ENTRY IL_Throw, _TEXT, NoHandler + PUSH_COOP_PINVOKE_FRAME_WITH_FLOATS a1 + // a0 already contains exception object + // a1 contains pointer to TransitionBlock + call C_FUNC(IL_Throw_Impl) + // Should never return + ebreak +NESTED_END IL_Throw, _TEXT + +// ------------------------------------------------------------------ +// Capture a transition block with register values and call the IL_ThrowExact_Impl +// implementation written in C. +// +// Input state: +// a0 = Pointer to exception object +// ------------------------------------------------------------------ +NESTED_ENTRY IL_ThrowExact, _TEXT, NoHandler + PUSH_COOP_PINVOKE_FRAME_WITH_FLOATS a1 + // a0 already contains exception object + // a1 contains pointer to TransitionBlock + call C_FUNC(IL_ThrowExact_Impl) + // Should never return + ebreak +NESTED_END IL_ThrowExact, _TEXT + +// ------------------------------------------------------------------ +// Capture a transition block with register values and call the IL_Rethrow_Impl +// implementation written in C. +// ------------------------------------------------------------------ +NESTED_ENTRY IL_Rethrow, _TEXT, NoHandler + PUSH_COOP_PINVOKE_FRAME_WITH_FLOATS a0 + // a0 contains pointer to TransitionBlock + call C_FUNC(IL_Rethrow_Impl) + // Should never return + ebreak +NESTED_END IL_Rethrow, _TEXT + #ifdef FEATURE_INTERPRETER // Align interpreter stack by adjusting it by 8 bytes From 94e3e251c1c83c61606da9f86ba3c5b6111eef22 Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Fri, 16 Jan 2026 15:16:23 +0100 Subject: [PATCH 04/32] Fix assertions generated by optCreateJumpTableImpliedAssertions (#123240) Fixes https://github.com/dotnet/runtime/issues/122254 bug repro: ```c switch (X) { case 2: // ... break; case 3: // ... break; case 4: // ... break; default: // "X >= 5" assertion was created here (which is incorrect, X can be 0 or 1 as well) break; } ``` --- src/coreclr/jit/assertionprop.cpp | 3 +- .../JitBlue/Runtime_122254/Runtime_122254.cs | 47 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 src/tests/JIT/Regression/JitBlue/Runtime_122254/Runtime_122254.cs diff --git a/src/coreclr/jit/assertionprop.cpp b/src/coreclr/jit/assertionprop.cpp index 61cfb57382a612..b0744114f1c704 100644 --- a/src/coreclr/jit/assertionprop.cpp +++ b/src/coreclr/jit/assertionprop.cpp @@ -5786,7 +5786,8 @@ bool Compiler::optCreateJumpTableImpliedAssertions(BasicBlock* switchBb) // default: %name.Length is >= 8 here% // } // - if ((value > 0) && !vnStore->IsVNConstant(opVN)) + // NOTE: if offset != 0, we only know that "X + offset >= maxJumpIdx", which is not very useful. + if ((offset == 0) && (value > 0) && !vnStore->IsVNConstant(opVN)) { AssertionDsc dsc = {}; dsc.assertionKind = OAK_NOT_EQUAL; diff --git a/src/tests/JIT/Regression/JitBlue/Runtime_122254/Runtime_122254.cs b/src/tests/JIT/Regression/JitBlue/Runtime_122254/Runtime_122254.cs new file mode 100644 index 00000000000000..4ffc71b364bcb9 --- /dev/null +++ b/src/tests/JIT/Regression/JitBlue/Runtime_122254/Runtime_122254.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.CompilerServices; +using Xunit; + +public class Runtime_122254 +{ + [Fact] + public static void TestEntryPoint() + { + try + { + Test(new int[0]); + } + catch (InvalidOperationException) + { + return; + } + throw new InvalidOperationException(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void Test(int[] arr) + { + int block = arr.Length; + switch (block) + { + case 2: + Console.WriteLine("2"); + break; + case 3: + Console.WriteLine("3"); + break; + case 4: + Console.WriteLine("4"); + break; + default: + if (block == 0) + { + throw new InvalidOperationException(); + } + break; + } + } +} \ No newline at end of file From d6a65357a973700fb6f4b528b6720f02b82cd43c Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 10:46:57 -0600 Subject: [PATCH 05/32] [main] Source code updates from dotnet/dotnet (#123247) > [!NOTE] > This is a codeflow update. It may contain both source code changes from > [the VMR](https://github.com/dotnet/dotnet) > as well as dependency updates. Learn more [here](https://github.com/dotnet/dotnet/tree/main/docs/Codeflow-PRs.md). This pull request brings the following source code changes [marker]: <> (Begin:f7901f87-9f24-40d6-9bc1-564863937237) ## From https://github.com/dotnet/dotnet - **Subscription**: [f7901f87-9f24-40d6-9bc1-564863937237](https://maestro.dot.net/subscriptions?search=f7901f87-9f24-40d6-9bc1-564863937237) - **Build**: [20260115.1](https://dev.azure.com/dnceng/internal/_build/results?buildId=2879841) ([297491](https://maestro.dot.net/channel/8298/github:dotnet:dotnet/build/297491)) - **Date Produced**: January 15, 2026 12:49:22 PM UTC - **Commit**: [bac69806be267e5c3fbd02ea60d69f6bdc0356ae](https://github.com/dotnet/dotnet/commit/bac69806be267e5c3fbd02ea60d69f6bdc0356ae) - **Commit Diff**: [4d383e9...bac6980](https://github.com/dotnet/dotnet/compare/4d383e955786d8fa9b0880d00bdca12f9208c7f5...bac69806be267e5c3fbd02ea60d69f6bdc0356ae) - **Branch**: [main](https://github.com/dotnet/dotnet/tree/main) **Updated Dependencies** - From [5.4.0-2.26064.120 to 5.4.0-2.26065.101][1] - Microsoft.CodeAnalysis - Microsoft.CodeAnalysis.Analyzers - Microsoft.CodeAnalysis.CSharp - Microsoft.Net.Compilers.Toolset - From [11.0.100-alpha.1.26064.120 to 11.0.100-alpha.1.26065.101][1] - Microsoft.CodeAnalysis.NetAnalyzers - Microsoft.DotNet.ApiCompat.Task - Microsoft.NET.Workload.Emscripten.Current.Manifest-11.0.100.Transport - From [11.0.0-beta.26064.120 to 11.0.0-beta.26065.101][1] - Microsoft.DotNet.Arcade.Sdk - Microsoft.DotNet.Build.Tasks.Archives - Microsoft.DotNet.Build.Tasks.Feed - Microsoft.DotNet.Build.Tasks.Installers - Microsoft.DotNet.Build.Tasks.Packaging - Microsoft.DotNet.Build.Tasks.TargetFramework - Microsoft.DotNet.Build.Tasks.Templating - Microsoft.DotNet.Build.Tasks.Workloads - Microsoft.DotNet.CodeAnalysis - Microsoft.DotNet.GenAPI - Microsoft.DotNet.GenFacades - Microsoft.DotNet.Helix.Sdk - Microsoft.DotNet.PackageTesting - Microsoft.DotNet.RemoteExecutor - Microsoft.DotNet.SharedFramework.Sdk - Microsoft.DotNet.XliffTasks - Microsoft.DotNet.XUnitExtensions - From [0.11.5-alpha.26064.120 to 0.11.5-alpha.26065.101][1] - Microsoft.DotNet.Cecil - From [2.9.3-beta.26064.120 to 2.9.3-beta.26065.101][1] - Microsoft.DotNet.XUnitAssert - Microsoft.DotNet.XUnitConsoleRunner - From [11.0.0-alpha.1.26064.120 to 11.0.0-alpha.1.26065.101][1] - Microsoft.NET.Sdk.IL - Microsoft.NETCore.App.Ref - Microsoft.NETCore.ILAsm - runtime.native.System.IO.Ports - System.Reflection.Metadata - System.Reflection.MetadataLoadContext - System.Text.Json - From [7.3.0-preview.1.6520 to 7.3.0-preview.1.6601][1] - NuGet.Frameworks - NuGet.Packaging - NuGet.ProjectModel - NuGet.Versioning - From [3.0.0-alpha.1.26064.120 to 3.0.0-alpha.1.26065.101][1] - System.CommandLine [marker]: <> (End:f7901f87-9f24-40d6-9bc1-564863937237) [1]: https://github.com/dotnet/dotnet/compare/4d383e9557...bac69806be [marker]: <> (Start:Footer:CodeFlow PR) ## Associated changes in source repos - https://github.com/dotnet/razor/compare/53776e439d570e344caf5c9482e79abd8cc4cba0...3eb8321c9bb220a0263ee88979f11ec098ec20a2 - https://github.com/dotnet/roslyn/compare/52e811495adfce82f01a951e8ca92d21c7f015c2...e6f840bef17dfd3f3f808d30db6a0611b7d5228d - https://github.com/dotnet/runtime/compare/73294ebb8c1bf4724e8c785e599e5ac71e06bec3...d120108fc5d4fce1e0fcf2897e39e9ad18c466bb - https://github.com/dotnet/winforms/compare/01eefd447bf1de58c82bcd765bf85d1d7fb4e7ac...448e82a4481afb416d908d4011f58e67fe9c3cac
Diff the source with this PR branch ```bash darc vmr diff --name-only https://github.com/dotnet/dotnet:bac69806be267e5c3fbd02ea60d69f6bdc0356ae..https://github.com/dotnet/runtime:darc-main-f266d9fc-0def-4f1b-bd7a-692fc2b1696b ```
[marker]: <> (End:Footer:CodeFlow PR) --------- Co-authored-by: dotnet-maestro[bot] --- eng/Version.Details.props | 78 +++++++++---------- eng/Version.Details.xml | 158 +++++++++++++++++++------------------- global.json | 8 +- 3 files changed, 122 insertions(+), 122 deletions(-) diff --git a/eng/Version.Details.props b/eng/Version.Details.props index b7069e004ddf98..ef98dc9de034ed 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -40,45 +40,45 @@ This file should be imported by eng/Versions.props 19.1.0-alpha.1.25625.2 19.1.0-alpha.1.25625.2 - 5.4.0-2.26064.120 - 5.4.0-2.26064.120 - 5.4.0-2.26064.120 - 11.0.100-alpha.1.26064.120 - 11.0.100-alpha.1.26064.120 - 11.0.0-beta.26064.120 - 11.0.0-beta.26064.120 - 11.0.0-beta.26064.120 - 11.0.0-beta.26064.120 - 11.0.0-beta.26064.120 - 11.0.0-beta.26064.120 - 11.0.0-beta.26064.120 - 11.0.0-beta.26064.120 - 0.11.5-alpha.26064.120 - 11.0.0-beta.26064.120 - 11.0.0-beta.26064.120 - 11.0.0-beta.26064.120 - 11.0.0-beta.26064.120 - 11.0.0-beta.26064.120 - 11.0.0-beta.26064.120 - 11.0.0-beta.26064.120 - 11.0.0-beta.26064.120 - 2.9.3-beta.26064.120 - 2.9.3-beta.26064.120 - 11.0.0-beta.26064.120 - 5.4.0-2.26064.120 - 11.0.0-alpha.1.26064.120 - 11.0.100-alpha.1.26064.120 - 11.0.0-alpha.1.26064.120 - 11.0.0-alpha.1.26064.120 - 7.3.0-preview.1.6520 - 7.3.0-preview.1.6520 - 7.3.0-preview.1.6520 - 7.3.0-preview.1.6520 - 11.0.0-alpha.1.26064.120 - 3.0.0-alpha.1.26064.120 - 11.0.0-alpha.1.26064.120 - 11.0.0-alpha.1.26064.120 - 11.0.0-alpha.1.26064.120 + 5.4.0-2.26065.101 + 5.4.0-2.26065.101 + 5.4.0-2.26065.101 + 11.0.100-alpha.1.26065.101 + 11.0.100-alpha.1.26065.101 + 11.0.0-beta.26065.101 + 11.0.0-beta.26065.101 + 11.0.0-beta.26065.101 + 11.0.0-beta.26065.101 + 11.0.0-beta.26065.101 + 11.0.0-beta.26065.101 + 11.0.0-beta.26065.101 + 11.0.0-beta.26065.101 + 0.11.5-alpha.26065.101 + 11.0.0-beta.26065.101 + 11.0.0-beta.26065.101 + 11.0.0-beta.26065.101 + 11.0.0-beta.26065.101 + 11.0.0-beta.26065.101 + 11.0.0-beta.26065.101 + 11.0.0-beta.26065.101 + 11.0.0-beta.26065.101 + 2.9.3-beta.26065.101 + 2.9.3-beta.26065.101 + 11.0.0-beta.26065.101 + 5.4.0-2.26065.101 + 11.0.0-alpha.1.26065.101 + 11.0.100-alpha.1.26065.101 + 11.0.0-alpha.1.26065.101 + 11.0.0-alpha.1.26065.101 + 7.3.0-preview.1.6601 + 7.3.0-preview.1.6601 + 7.3.0-preview.1.6601 + 7.3.0-preview.1.6601 + 11.0.0-alpha.1.26065.101 + 3.0.0-alpha.1.26065.101 + 11.0.0-alpha.1.26065.101 + 11.0.0-alpha.1.26065.101 + 11.0.0-alpha.1.26065.101 11.0.0-beta.26059.1 11.0.0-beta.26059.1 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 1b12f253509c14..b573f85937d6f5 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,5 +1,5 @@ - + https://github.com/dotnet/icu @@ -41,91 +41,91 @@ https://github.com/dotnet/llvm-project 90a3b767fd10543cbaed7308406e0059dec0ffb7 - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae https://github.com/dotnet/runtime-assets @@ -263,33 +263,33 @@ https://github.com/dotnet/llvm-project 90a3b767fd10543cbaed7308406e0059dec0ffb7 - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae https://github.com/dotnet/xharness @@ -303,9 +303,9 @@ https://github.com/dotnet/xharness 799df8d4c86ff50c83b7a57df9e3691eeab813ec - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae https://dev.azure.com/dnceng/internal/_git/dotnet-optimization @@ -331,29 +331,29 @@ https://github.com/dotnet/runtime-assets fd342f922435b2725113b5226c7e25fcf830a149 - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae https://dev.azure.com/dnceng/internal/_git/dotnet-optimization @@ -365,21 +365,21 @@ - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae - + https://github.com/dotnet/dotnet - 4d383e955786d8fa9b0880d00bdca12f9208c7f5 + bac69806be267e5c3fbd02ea60d69f6bdc0356ae https://github.com/dotnet/node diff --git a/global.json b/global.json index 1ba3f948d3f723..81228fc7673725 100644 --- a/global.json +++ b/global.json @@ -8,11 +8,11 @@ "dotnet": "11.0.100-alpha.1.26060.102" }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "11.0.0-beta.26064.120", - "Microsoft.DotNet.Helix.Sdk": "11.0.0-beta.26064.120", - "Microsoft.DotNet.SharedFramework.Sdk": "11.0.0-beta.26064.120", + "Microsoft.DotNet.Arcade.Sdk": "11.0.0-beta.26065.101", + "Microsoft.DotNet.Helix.Sdk": "11.0.0-beta.26065.101", + "Microsoft.DotNet.SharedFramework.Sdk": "11.0.0-beta.26065.101", "Microsoft.Build.NoTargets": "3.7.0", "Microsoft.Build.Traversal": "3.4.0", - "Microsoft.NET.Sdk.IL": "11.0.0-alpha.1.26064.120" + "Microsoft.NET.Sdk.IL": "11.0.0-alpha.1.26065.101" } } From 0c3e4c22fdbc8d538548e775a217bd3fb0c1e184 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 16 Jan 2026 09:37:52 -0800 Subject: [PATCH 06/32] Update NativeAOT Docker samples to use .NET 10 images (#123241) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Jan Kotas --- .../nativeaot/docs/Dockerfile.cross-build-x64-arm64 | 5 ++--- src/coreclr/nativeaot/docs/Dockerfile.cross-build-x64-x64 | 5 ++--- src/coreclr/nativeaot/docs/containers.md | 8 ++++---- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/coreclr/nativeaot/docs/Dockerfile.cross-build-x64-arm64 b/src/coreclr/nativeaot/docs/Dockerfile.cross-build-x64-arm64 index 5196be167747c1..eb63be2503b447 100644 --- a/src/coreclr/nativeaot/docs/Dockerfile.cross-build-x64-arm64 +++ b/src/coreclr/nativeaot/docs/Dockerfile.cross-build-x64-arm64 @@ -1,4 +1,3 @@ -FROM mcr.microsoft.com/dotnet-buildtools/prereqs:cbl-mariner-2.0-cross-arm64 -COPY --from=mcr.microsoft.com/dotnet/sdk:8.0-cbl-mariner2.0-amd64 /usr/share/dotnet /usr/share/dotnet -RUN ln -s /usr/share/dotnet/dotnet /usr/bin/dotnet +FROM mcr.microsoft.com/dotnet-buildtools/prereqs:azurelinux-3.0-net10.0-cross-arm64 +COPY --from=mcr.microsoft.com/dotnet/sdk:10.0 /usr/share/dotnet /usr/share/dotnet ENV DOTNET_NOLOGO=true diff --git a/src/coreclr/nativeaot/docs/Dockerfile.cross-build-x64-x64 b/src/coreclr/nativeaot/docs/Dockerfile.cross-build-x64-x64 index 18b3c58b286e3b..c783b6a0d22308 100644 --- a/src/coreclr/nativeaot/docs/Dockerfile.cross-build-x64-x64 +++ b/src/coreclr/nativeaot/docs/Dockerfile.cross-build-x64-x64 @@ -1,4 +1,3 @@ -FROM mcr.microsoft.com/dotnet-buildtools/prereqs:cbl-mariner-2.0-cross-amd64 -COPY --from=mcr.microsoft.com/dotnet/sdk:8.0-cbl-mariner2.0-amd64 /usr/share/dotnet /usr/share/dotnet -RUN ln -s /usr/share/dotnet/dotnet /usr/bin/dotnet +FROM mcr.microsoft.com/dotnet-buildtools/prereqs:azurelinux-3.0-net10.0-cross-amd64 +COPY --from=mcr.microsoft.com/dotnet/sdk:10.0 /usr/share/dotnet /usr/share/dotnet ENV DOTNET_NOLOGO=true diff --git a/src/coreclr/nativeaot/docs/containers.md b/src/coreclr/nativeaot/docs/containers.md index 1a3e613a4506d9..c8ae8713fd0a25 100644 --- a/src/coreclr/nativeaot/docs/containers.md +++ b/src/coreclr/nativeaot/docs/containers.md @@ -16,14 +16,14 @@ For cloud native apps, build and runtime OS typically match, at least if you use The .NET build has this exact same need. We produce several [container images to enable cross-building](https://github.com/dotnet/runtime/blob/main/docs/workflow/using-docker.md#the-official-runtime-docker-images). -You can use these images to build native AOT apps which work on distros as old as Ubuntu 16.04. These build images are not supported, but are expected to work (since we use them to build .NET on daily basis). +You can use these images to build native AOT apps which work on distros as old as Ubuntu 18.04 in .NET 10, see [Linux compatibility in release notes](https://github.com/dotnet/core/blob/main/release-notes/10.0/supported-os.md#linux-compatibility) for details. These build images are not supported, but are expected to work (since we use them to build .NET on daily basis). ### Containerized build -The following Dockerfiles demonstrate how to construct a working build environment that can be used for volume-mounted docker builds. They can be modified to use the other image flavors we provide, like Alpine. +The following Dockerfiles demonstrate how to construct a working build environment that can be used for volume-mounted docker builds. They can be modified to use the other .NET runtime versions or other image flavors we provide, like Alpine. - [Dockerfile.cross-build-x64-arm64](Dockerfile.cross-build-x64-arm64) -- [Dockerfile.cross-build-x64-x64](Dockerfile.cross-build-x64-arm64) +- [Dockerfile.cross-build-x64-x64](Dockerfile.cross-build-x64-x64) ### x64 @@ -58,7 +58,7 @@ Hello, World! The app can be tested in an old Linux container, again through volume mounting. ```bash -$ docker run --rm -v $(pwd)/app:/app -w /app ubuntu:16.04 ./cross-build-test +$ docker run --rm -v $(pwd)/app:/app -w /app ubuntu:18.04 ./cross-build-test Hello, World! ``` From 15af27bc1c59fa28dc1d1f7a00b0953f07621f09 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 16 Jan 2026 18:51:33 +0100 Subject: [PATCH 07/32] Add multiple environment variable sets to aspnet2 SPMI collection (#123242) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description The `superpmi_aspnet2.py` script ran each benchmark scenario only once. Modified to run each scenario 6 times with different JIT configuration environment variables to collect comprehensive SPMI data across compilation modes. ## Changes - **Modified `run_crank_scenario` signature**: Added `env_vars: dict` parameter to accept environment variable configurations - **Added environment variable processing**: Converts dictionary to `--application.environmentVariables DOTNET_=` crank arguments - **Defined 6 environment variable sets**: 1. `Dummy=0` 2. `TieredCompilation=0` 3. `TieredPGO=0` 4. `TieredPGO=1, ReadyToRun=0` 5. `ReadyToRun=0, OSR_HitLimit=0, TC_OnStackReplacement_InitialCounter=10` 6. `TC_PartialCompilation=1` - **Updated main loop**: Nested iteration runs each of 4 scenarios with all 6 environment sets (24 total runs) ## Impact Before: 4 scenarios × 1 run = 4 total runs After: 4 scenarios × 6 env sets = 24 total runs Each run now displays the active configuration: ``` ### Running about-sqlite benchmark (set 1/6: Dummy=0)... ### ### Running about-sqlite benchmark (set 2/6: TieredCompilation=0)... ### ... ``` > [!WARNING] > >
> Firewall rules blocked me from connecting to one or more addresses (expand for details) > > #### I tried to connect to the following addresses, but was blocked by firewall rules: > > - `aka.ms` > - Triggering command: `/usr/bin/curl curl -I -sSL --retry 5 --retry-delay 2 --connect-timeout 15 REDACTED` (dns block) > > If you need me to access, download, or install something from one of these locations, you can either: > > - Configure [Actions setup steps](https://gh.io/copilot/actions-setup-steps) to set up my environment, which run before the firewall is enabled > - Add the appropriate URLs or hosts to the custom allowlist in this repository's [Copilot coding agent settings](https://github.com/dotnet/runtime/settings/copilot/coding_agent) (admins only) > >
Original prompt > # Add more scenarios to aspnet2 SPMI collection > > superpmi_aspnet2.py script currently runs all scenarios just once (e.g. "about-sqlite", "json", etc). I want you to runeach scenario multiple times with different sets of environment variables here are the sets: > > ``` > Dummy=0 > TieredCompilation=0 > TieredPGO=0 > TieredPGO=1, ReadyToRun=0 > ReadyToRun=0, OSR_HitLimit=0, TC_OnStackReplacement_InitialCounter=10 > TC_PartialCompilation=1 > ``` > 6 sets in total (some of them have multiple variables). > > each variable is basically extra `--application.environmentVariables DOTNET_=` argument passed to crank. > > You don't have to build the whole runtime or run the script (it requires a complex setup). Just modify the script to add the above functionality. Make sure the code is correct and compiles without errors.
--- 💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: EgorBo <523221+EgorBo@users.noreply.github.com> Co-authored-by: Egor Bogatov Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/coreclr/scripts/superpmi_aspnet2.py | 42 ++++++++++++++++++------- 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/src/coreclr/scripts/superpmi_aspnet2.py b/src/coreclr/scripts/superpmi_aspnet2.py index 9774bd04a4de42..3e2d4bcf6f92ea 100644 --- a/src/coreclr/scripts/superpmi_aspnet2.py +++ b/src/coreclr/scripts/superpmi_aspnet2.py @@ -223,7 +223,7 @@ def setup_and_run_crank_agent(workdir: Path): # Run crank scenario -def run_crank_scenario(crank_app: Path, scenario_name: str, framework: str, work_dir: Path, core_root_path: Path, config_path: Path, dryrun: bool, *extra_args: str): +def run_crank_scenario(crank_app: Path, scenario_name: str, framework: str, work_dir: Path, core_root_path: Path, config_path: Path, dryrun: bool, env_vars: dict, *extra_args: str): spmi_shim = native_dll("superpmi-shim-collector") clrjit = native_dll("clrjit") coreclr = native_dll("coreclr") @@ -270,6 +270,12 @@ def run_crank_scenario(crank_app: Path, scenario_name: str, framework: str, work "--application.options.outputFiles", str(core_root_path / spcorelib), ]) + # Add custom environment variables for this run + for var_name, var_value in env_vars.items(): + cmd.extend([ + "--application.environmentVariables", f"DOTNET_{var_name}={var_value}" + ]) + # Append any extra scenario-specific arguments if extra_args: cmd.extend(extra_args) @@ -352,19 +358,31 @@ def main(): "--config", "https://raw.githubusercontent.com/aspnet/Benchmarks/main/scenarios/platform.benchmarks.yml"), ] + # Define the environment variable sets to run for each scenario + env_var_sets = [ + {"Dummy": "0"}, + {"TieredPGO": "1", "ReadyToRun": "0"}, + {"TieredCompilation": "0", "ReadyToRun": "0"}, + {"TC_PartialCompilation": "1"}, + ] + for entry in scenarios: scenario_name, *extra = entry - print(f"### Running {scenario_name} benchmark... ###") - run_crank_scenario( - crank_app_path, - scenario_name, - args.tfm, - work_dir_base, - core_root_path, - config_path, - args.dryrun, - *extra, - ) + # Run each scenario with all environment variable sets + for idx, env_vars in enumerate(env_var_sets, 1): + env_vars_str = ", ".join(f"{k}={v}" for k, v in env_vars.items()) + print(f"### Running {scenario_name} benchmark (set {idx}/{len(env_var_sets)}: {env_vars_str})... ###") + run_crank_scenario( + crank_app_path, + scenario_name, + args.tfm, + work_dir_base, + core_root_path, + config_path, + args.dryrun, + env_vars, + *extra, + ) print("Finished running benchmarks.") From 01b4433b03c4992d634583c858f97ffcd85eb924 Mon Sep 17 00:00:00 2001 From: Jan Vorlicek Date: Fri, 16 Jan 2026 21:34:00 +0100 Subject: [PATCH 08/32] Fix interpreter threadabort in finally (#123231) This fixes the System.Runtime.Tests.ControlledExecutionTests.CancelItselfFromFinally libraries test that was failing with the interpreter. The issue was that finally block was being aborted due to the fact that instead of the complex COMPlusCheckForAbort, the interpreter was only checking if abort was requested. --- src/coreclr/vm/interpexec.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/coreclr/vm/interpexec.cpp b/src/coreclr/vm/interpexec.cpp index acfefa8eb55c8c..36150390b93d50 100644 --- a/src/coreclr/vm/interpexec.cpp +++ b/src/coreclr/vm/interpexec.cpp @@ -4220,8 +4220,8 @@ do \ pInterpreterFrame->SetIsFaulting(false); - Thread *pThread = GetThread(); - if (pThread->IsAbortRequested()) + void* abortAddress = COMPlusCheckForAbort(resumeIP); + if (abortAddress != NULL) { // Record the resume IP in the pFrame so that the exception handling unwinds from there pFrame->ip = ip; From 7797f55d19a4414ea43ae4e7e6ff253c2ef93e84 Mon Sep 17 00:00:00 2001 From: SingleAccretion <62474226+SingleAccretion@users.noreply.github.com> Date: Sat, 17 Jan 2026 00:03:50 +0300 Subject: [PATCH 09/32] [RyuJit/WASM] Some fixes related to local addresses and stores (#123261) Ref: https://github.com/dotnet/runtime/pull/122414/files#r2698997821. I also took the liberty of removing no-op `add`s from `LCL_ADDR` codegen. --- src/coreclr/jit/codegenwasm.cpp | 10 ++++++++-- src/coreclr/jit/regallocwasm.cpp | 19 +++++++++++-------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/coreclr/jit/codegenwasm.cpp b/src/coreclr/jit/codegenwasm.cpp index 534e494369be42..1083a96ad2a2f7 100644 --- a/src/coreclr/jit/codegenwasm.cpp +++ b/src/coreclr/jit/codegenwasm.cpp @@ -814,10 +814,16 @@ void CodeGen::genCodeForNegNot(GenTreeOp* tree) void CodeGen::genCodeForLclAddr(GenTreeLclFld* lclAddrNode) { assert(lclAddrNode->OperIs(GT_LCL_ADDR)); + bool FPBased; + unsigned lclNum = lclAddrNode->GetLclNum(); + unsigned lclOffset = lclAddrNode->GetLclOffs(); GetEmitter()->emitIns_I(INS_local_get, EA_PTRSIZE, WasmRegToIndex(GetFramePointerReg())); - GetEmitter()->emitIns_S(INS_I_const, EA_PTRSIZE, lclAddrNode->GetLclNum(), lclAddrNode->GetLclOffs()); - GetEmitter()->emitIns(INS_I_add); + if ((lclOffset != 0) || (compiler->lvaFrameAddress(lclNum, &FPBased) != 0)) + { + GetEmitter()->emitIns_S(INS_I_const, EA_PTRSIZE, lclNum, lclOffset); + GetEmitter()->emitIns(INS_I_add); + } genProduceReg(lclAddrNode); } diff --git a/src/coreclr/jit/regallocwasm.cpp b/src/coreclr/jit/regallocwasm.cpp index 4125c1e5031ee0..48bcd978abd25b 100644 --- a/src/coreclr/jit/regallocwasm.cpp +++ b/src/coreclr/jit/regallocwasm.cpp @@ -177,7 +177,8 @@ void WasmRegAlloc::RewriteLocalStackStore(GenTreeLclVarCommon* lclNode) { // At this point, the IR is already stackified, so we just need to find the first node in the dataflow. // TODO-WASM-TP: this is nice and simple, but can we do this more efficiently? - GenTree* op = lclNode->Data(); + GenTree* value = lclNode->Data(); + GenTree* op = value; GenTree::VisitResult visitResult; do { @@ -187,23 +188,25 @@ void WasmRegAlloc::RewriteLocalStackStore(GenTreeLclVarCommon* lclNode) }); } while (visitResult == GenTree::VisitResult::Abort); + // TODO-WASM-RA: figure out the address mode story here. Right now this will produce an address not folded + // into the store's address mode. We can utilize a contained LEA, but that will require some liveness work. + uint16_t offset = lclNode->GetLclOffs(); + lclNode->SetOper(GT_LCL_ADDR); + lclNode->ChangeType(TYP_I_IMPL); + lclNode->AsLclFld()->SetLclOffs(offset); + GenTree* store; GenTreeFlags indFlags = GTF_IND_NONFAULTING | GTF_IND_TGT_NOT_HEAP; if (lclNode->TypeIs(TYP_STRUCT)) { - store = m_compiler->gtNewStoreBlkNode(lclNode->GetLayout(m_compiler), lclNode, lclNode->Data(), indFlags); + store = m_compiler->gtNewStoreBlkNode(lclNode->GetLayout(m_compiler), lclNode, value, indFlags); } else { - store = m_compiler->gtNewStoreIndNode(lclNode->TypeGet(), lclNode, lclNode->Data(), indFlags); + store = m_compiler->gtNewStoreIndNode(lclNode->TypeGet(), lclNode, value, indFlags); } CurrentRange().InsertAfter(lclNode, store); CurrentRange().Remove(lclNode); - - // TODO-WASM-RA: figure out the address mode story here. Right now this will produce an address not folded - // into the store's address mode. We can utilize a contained LEA, but that will require some liveness work. - lclNode->SetOper(GT_LCL_ADDR); - lclNode->ChangeType(TYP_I_IMPL); CurrentRange().InsertBefore(op, lclNode); } From 95f82dd8f13ed6c38ac3250c1cdca366c9e14fcc Mon Sep 17 00:00:00 2001 From: Mitchell Hwang <16830051+mdh1418@users.noreply.github.com> Date: Fri, 16 Jan 2026 17:33:06 -0500 Subject: [PATCH 10/32] [Startup] Bump PerfMap ahead of DiagnosticServer (#123226) Fixes https://github.com/dotnet/runtime/issues/122472 From some inspection, it looks like `PerfMap::Initialize` does not depend on the methods right above it. ``` #ifdef FEATURE_PERFTRACING DiagnosticServerAdapter::Initialize(); DiagnosticServerAdapter::PauseForDiagnosticsMonitor(); #endif // FEATURE_PERFTRACING #ifdef FEATURE_GDBJIT // Initialize gdbjit NotifyGdb::Initialize(); #endif // FEATURE_GDBJIT #ifdef FEATURE_EVENT_TRACE // Initialize event tracing early so we can trace CLR startup time events. InitializeEventTracing(); // Fire the EE startup ETW event ETWFireEvent(EEStartupStart_V1); #endif // FEATURE_EVENT_TRACE InitGSCookie(); #ifdef LOGGING InitializeLogging() #endif ``` `PerfMap::Initialize` depends on `SString::Startup()` which occurs earlier in startup and some PAL/OS services. Instead of adding more logic to handle a racing DiagnosticServer IPC [`EnablePerfMap` command](https://github.com/dotnet/diagnostics/blob/main/documentation/design-docs/ipc-protocol.md#enableperfmap) and `PerfMap::Initialize` (e.g. lazy-init/queued commands), bump the PerfMap initialization ahead of the DiagnosticServer initialization. The DiagnosticServer also doesn't depend on PerfMap being initialized afterwards, and PerfMap's `CrstStatic` and `PerfMap::Enable(type, sendExisting)` suggests that the IPC enable perfmap command should occur after `PerfMap::Initialize`. This way, DiagnosticServer is still early in startup, and its `PerfMap::Enable` will not crash the runtime. As for the IPC command to set environment variables, it is unlikely that it was used to `DOTNET_PerfMapEnabled` because that would either not have applied early enough or have crashed the runtime like in the issue. Instead, the `EnablePerfMap`/`DisablePerfMap` commands should be used to toggle PerfMap status. --- src/coreclr/vm/ceemain.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/coreclr/vm/ceemain.cpp b/src/coreclr/vm/ceemain.cpp index aec61f1fc841a2..f039940ec375db 100644 --- a/src/coreclr/vm/ceemain.cpp +++ b/src/coreclr/vm/ceemain.cpp @@ -706,6 +706,10 @@ void EEStartupHelper() } #endif +#ifdef FEATURE_PERFMAP + PerfMap::Initialize(); +#endif + #ifdef FEATURE_PERFTRACING DiagnosticServerAdapter::Initialize(); DiagnosticServerAdapter::PauseForDiagnosticsMonitor(); @@ -731,7 +735,6 @@ void EEStartupHelper() #endif #ifdef FEATURE_PERFMAP - PerfMap::Initialize(); InitThreadManagerPerfMapData(); #endif From 0bb8d77537a336a1b731c87a9bb41c6dd0dce052 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 16 Jan 2026 18:34:22 -0500 Subject: [PATCH 11/32] Fix missing static libraries in NativeAOT packs for Apple mobile platforms (#123274) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description NativeAOT runtime packs for iOS and other Apple mobile platforms were missing required static libraries (libSystem.Globalization.Native.a, libSystem.IO.Compression.Native.a, libSystem.Native.a, libbrotli*.a) starting in version 11.0.0-alpha.1.26065.101. ## Changes Modified `eng/liveBuilds.targets` in the `ResolveLibrariesRuntimeFilesFromLocalBuild` target: - Added `BuildNativeAOTRuntimePack != 'true'` condition to the `ExcludeNativeLibrariesRuntimeFiles` item for Apple mobile platforms - Updated comment to clarify that the exclusion does not apply to NativeAOT **Rationale**: Regular CoreCLR packs retrieve these libraries from the CoreCLR shared framework directory, so they're excluded from LibrariesRuntimeFiles to avoid duplicates. NativeAOT packs do not reach back into CoreCLR shared framework artifacts and require these libraries from LibrariesRuntimeFiles. ```xml
Original prompt > > ---- > > *This section details on the original issue you should resolve* > > Latest .NET 11 NativeAOT packs missing some static libraries > The `11.0.0-alpha.1.26065.101` version of the `microsoft.netcore.app.runtime.nativeaot.ios-arm64` pack (and probably other packs for other RIDs as well, I haven't checked) are missing the following files: > > * libSystem.Globalization.Native.a > * libSystem.IO.Compression.Native.a > * libSystem.Native.a > * libbrotlicommon.a > * libbrotlidec.a > * libbrotlienc.a > > They were present in `11.0.0-alpha.1.26062.101` > > Complete file list: https://gist.github.com/rolfbjarne/7f42f3cf3ed3a815e2e9fe2b28c580fb#file-gistfile1-txt-L76-L82 > > This started happening in this maestro PR: https://github.com/dotnet/macios/pull/24508 > > specifically this commit: https://github.com/dotnet/macios/pull/24508/changes/ee15359640330dd565e18b28791918ffbc925d85 > > I tried looking through the relevant dotnet/runtime commits, but I didn't see anything obvious causing this. > > In the ResolveLibrariesRefAssembliesFromLocalBuild target in eng/livebuilds.targets, add BuildNativeAOTRuntimePack != 'true' to the condition in the ExcludeNativeLibrariesRuntimeFiles. It should only apply to the item where TargetsAppleMobile == 'true'. > > Additionally, update the comment above the item to indicate that it does not apply to nativeaot because we do not reach back into coreclr sharedFramework artifacts. > > ## Comments on the Issue (you are @copilot in this section) > > > @MichalStrehovsky > > and probably other packs for other RIDs as well, I haven't checked > > I checked Microsoft.NETCore.App.Runtime.NativeAOT.linux-arm64.11.0.0-preview.1.26065.113 and that one has these. Microsoft.NETCore.App.Runtime.NativeAOT.ios-arm64.11.0.0-preview.1.26065.113 doesn't have these. > > So looks to be iDevice specific. I can't build these things locally as I don't have a mac. > @steveisok > In the local build, it looks like coreclr and nativeaot are supposed to look back to `artifacts/bin/coreclr/ios../sharedFramework` for the native lib artifacts. NativeAOT does not appear to be doing that, hence the reason the static libs aren't included. > @steveisok > `ResolveLibrariesRuntimeFilesFromLocalBuild` typically copies all the libraries and native lib bits. There's a step to exclude some native bits if the runtime is coreclr or nativeaot because they are supposed to be accounted for in `ResolveRuntimeFilesFromLocalBuild`. That's not happening for nativeaot due to `BuildNativeAOTRuntimePack=true`. > @steveisok > @kotlarmilos if you want, I can fix it. It's no problem, so let me know. > >
- Fixes dotnet/runtime#123253 --- 💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://gh.io/copilot-coding-agent-survey). --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: steveisok <471438+steveisok@users.noreply.github.com> --- eng/liveBuilds.targets | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/eng/liveBuilds.targets b/eng/liveBuilds.targets index 67d3518b0cc0a7..0eb29f9db7da28 100644 --- a/eng/liveBuilds.targets +++ b/eng/liveBuilds.targets @@ -205,8 +205,9 @@ Include="$(LibrariesNativeArtifactsPath)System.Globalization.Native.dll;$(LibrariesNativeArtifactsPath)System.Globalization.Native.so;$(LibrariesNativeArtifactsPath)System.Globalization.Native.dylib" /> - + Date: Sat, 17 Jan 2026 17:14:25 +0100 Subject: [PATCH 14/32] Adding C#, F#, VB to StringSyntaxAttribute (#123211) Adding 3 constants that represent C#, F#, and Visual Basic to the StringSyntaxAttribute Fix #122604 --- .../Diagnostics/CodeAnalysis/StringSyntaxAttribute.cs | 9 +++++++++ src/libraries/System.Runtime/ref/System.Runtime.cs | 3 +++ .../CodeAnalysis/StringSyntaxAttributeTests.cs | 3 +++ 3 files changed, 15 insertions(+) diff --git a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/StringSyntaxAttribute.cs b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/StringSyntaxAttribute.cs index 26fcf5ca096408..66c5f9b3fc4f24 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/StringSyntaxAttribute.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/StringSyntaxAttribute.cs @@ -38,6 +38,9 @@ public StringSyntaxAttribute(string syntax, params object?[] arguments) /// The syntax identifier for strings containing composite formats for string formatting. public const string CompositeFormat = nameof(CompositeFormat); + /// The syntax identifier for strings containing C# code. + public const string CSharp = "C#"; + /// The syntax identifier for strings containing date format specifiers. public const string DateOnlyFormat = nameof(DateOnlyFormat); @@ -47,6 +50,9 @@ public StringSyntaxAttribute(string syntax, params object?[] arguments) /// The syntax identifier for strings containing format specifiers. public const string EnumFormat = nameof(EnumFormat); + /// The syntax identifier for strings containing F# code. + public const string FSharp = "F#"; + /// The syntax identifier for strings containing format specifiers. public const string GuidFormat = nameof(GuidFormat); @@ -68,6 +74,9 @@ public StringSyntaxAttribute(string syntax, params object?[] arguments) /// The syntax identifier for strings containing URIs. public const string Uri = nameof(Uri); + /// The syntax identifier for strings containing Visual Basic code. + public const string VisualBasic = "Visual Basic"; + /// The syntax identifier for strings containing XML. public const string Xml = nameof(Xml); } diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index ce6380fc645bf2..16fcc5f730823d 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -9096,9 +9096,11 @@ public SetsRequiredMembersAttribute() { } public sealed partial class StringSyntaxAttribute : System.Attribute { public const string CompositeFormat = "CompositeFormat"; + public const string CSharp = "C#"; public const string DateOnlyFormat = "DateOnlyFormat"; public const string DateTimeFormat = "DateTimeFormat"; public const string EnumFormat = "EnumFormat"; + public const string FSharp = "F#"; public const string GuidFormat = "GuidFormat"; public const string Json = "Json"; public const string NumericFormat = "NumericFormat"; @@ -9106,6 +9108,7 @@ public sealed partial class StringSyntaxAttribute : System.Attribute public const string TimeOnlyFormat = "TimeOnlyFormat"; public const string TimeSpanFormat = "TimeSpanFormat"; public const string Uri = "Uri"; + public const string VisualBasic = "Visual Basic"; public const string Xml = "Xml"; public StringSyntaxAttribute(string syntax) { } public StringSyntaxAttribute(string syntax, params object?[] arguments) { } diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Diagnostics/CodeAnalysis/StringSyntaxAttributeTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Diagnostics/CodeAnalysis/StringSyntaxAttributeTests.cs index ba53ab2c0f3bbe..251156cf037986 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Diagnostics/CodeAnalysis/StringSyntaxAttributeTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Diagnostics/CodeAnalysis/StringSyntaxAttributeTests.cs @@ -8,9 +8,12 @@ namespace System.Diagnostics.CodeAnalysis.Tests public sealed class StringSyntaxAttributeTests { [Theory] + [InlineData(StringSyntaxAttribute.CSharp)] [InlineData(StringSyntaxAttribute.DateTimeFormat)] + [InlineData(StringSyntaxAttribute.FSharp)] [InlineData(StringSyntaxAttribute.Json)] [InlineData(StringSyntaxAttribute.Regex)] + [InlineData(StringSyntaxAttribute.VisualBasic)] public void Ctor_Roundtrips(string syntax) { var attribute = new StringSyntaxAttribute(syntax); From 08553fc6f3295c88a21917ab6f6516b1dbec6d8a Mon Sep 17 00:00:00 2001 From: SingleAccretion <62474226+SingleAccretion@users.noreply.github.com> Date: Sat, 17 Jan 2026 20:31:05 +0300 Subject: [PATCH 15/32] [RyuJit/WASM] Establish SP & FP and home register arguments (#123270) I've decided to do a bespoke function for WASM, mainly due to the awkwardness of porting the register mask code that would be of questionable value. Instead, we place an allocation constraint on the RA. With these changes, we are very close to getting a functional "add(x, y)". The only remaining part is the SP arg, being worked on in https://github.com/dotnet/runtime/pull/123262. This is not a fully sound implementation since we have a limit on the number of instructions in the prolog (and here we're generating a potentially unbounded number of instruction). I've put a point about this into #121865. It seems this single-IG restriction ought to be fixable... --- src/coreclr/jit/codegencommon.cpp | 16 ++-- src/coreclr/jit/codegenwasm.cpp | 129 +++++++++++++++++++++++++++++- 2 files changed, 137 insertions(+), 8 deletions(-) diff --git a/src/coreclr/jit/codegencommon.cpp b/src/coreclr/jit/codegencommon.cpp index 14732425a2f032..5b338ba948f0b2 100644 --- a/src/coreclr/jit/codegencommon.cpp +++ b/src/coreclr/jit/codegencommon.cpp @@ -2929,6 +2929,7 @@ class RegGraph } #endif }; +#endif // !TARGET_WASM // ----------------------------------------------------------------------------- // genParamStackType: Get the type that a part of a parameter passed in a @@ -2998,6 +2999,7 @@ var_types CodeGen::genParamStackType(LclVarDsc* dsc, const ABIPassingSegment& se } } +#ifndef TARGET_WASM // ----------------------------------------------------------------------------- // genSpillOrAddRegisterParam: Handle a register parameter either by homing it // to stack immediately, or by adding it to the register graph. @@ -5214,6 +5216,11 @@ void CodeGen::genFnProlog() compiler->unwindPushMaskInt(regSet.rsMaskPreSpillRegs(true)); } #endif // TARGET_ARM +#else // TARGET_WASM + regNumber initReg = REG_NA; + bool initRegZeroed = false; + bool isOSRx64Root = false; +#endif // TARGET_WASM unsigned extraFrameSize = 0; @@ -5378,6 +5385,7 @@ void CodeGen::genFnProlog() } #endif // TARGET_ARM +#ifndef TARGET_WASM // TODO-WASM: temporary; un-undefine as needed. // // Zero out the frame as needed // @@ -5395,23 +5403,17 @@ void CodeGen::genFnProlog() #endif // JIT32_GCENCODER // Set up the GS security cookie - genSetGSSecurityCookie(initReg, &initRegZeroed); #ifdef PROFILING_SUPPORTED - // Insert a function entry callback for profiling, if requested. // OSR methods aren't called, so don't have enter hooks. if (!compiler->opts.IsOSR()) { genProfilingEnterCallback(initReg, &initRegZeroed); } - #endif // PROFILING_SUPPORTED -#else // TARGET_WASM - regNumber initReg = REG_NA; - bool initRegZeroed = false; -#endif // TARGET_WASM +#endif // !TARGET_WASM // For OSR we may have a zero-length prolog. That's not supported // when the method must report a generics context,/ so add a nop if so. diff --git a/src/coreclr/jit/codegenwasm.cpp b/src/coreclr/jit/codegenwasm.cpp index 1083a96ad2a2f7..64059f66026592 100644 --- a/src/coreclr/jit/codegenwasm.cpp +++ b/src/coreclr/jit/codegenwasm.cpp @@ -12,9 +12,11 @@ #ifdef TARGET_64BIT static const instruction INS_I_const = INS_i64_const; static const instruction INS_I_add = INS_i64_add; +static const instruction INS_I_sub = INS_i64_sub; #else // !TARGET_64BIT static const instruction INS_I_const = INS_i32_const; static const instruction INS_I_add = INS_i32_add; +static const instruction INS_I_sub = INS_i32_sub; #endif // !TARGET_64BIT void CodeGen::genMarkLabelsForCodegen() @@ -43,6 +45,50 @@ void CodeGen::genBeginFnProlog() } } +//------------------------------------------------------------------------ +// genPushCalleeSavedRegisters: no-op since we don't need to save anything. +// +void CodeGen::genPushCalleeSavedRegisters() +{ +} + +//------------------------------------------------------------------------ +// genAllocLclFrame: initialize the SP and FP locals. +// +// Arguments: +// frameSize - Size of the frame to establish +// initReg - Unused +// pInitRegZeroed - Unused +// maskArgRegsLiveIn - Unused +// +void CodeGen::genAllocLclFrame(unsigned frameSize, regNumber initReg, bool* pInitRegZeroed, regMaskTP maskArgRegsLiveIn) +{ + assert(compiler->compGeneratingProlog); + regNumber spReg = GetStackPointerReg(); + if (spReg == REG_NA) + { + assert(!isFramePointerUsed()); + return; + } + + unsigned initialSPLclIndex = 0; // TODO-WASM: remove this hardcoding once we have the SP arg local. + unsigned spLclIndex = WasmRegToIndex(spReg); + assert(initialSPLclIndex == spLclIndex); + if (frameSize != 0) + { + GetEmitter()->emitIns_I(INS_local_get, EA_PTRSIZE, initialSPLclIndex); + GetEmitter()->emitIns_I(INS_I_const, EA_PTRSIZE, frameSize); + GetEmitter()->emitIns(INS_I_sub); + GetEmitter()->emitIns_I(INS_local_set, EA_PTRSIZE, spLclIndex); + } + regNumber fpReg = GetFramePointerReg(); + if ((fpReg != REG_NA) && (fpReg != spReg)) + { + GetEmitter()->emitIns_I(INS_local_get, EA_PTRSIZE, spLclIndex); + GetEmitter()->emitIns_I(INS_local_set, EA_PTRSIZE, WasmRegToIndex(fpReg)); + } +} + //------------------------------------------------------------------------ // genEnregisterOSRArgsAndLocals: enregister OSR args and locals. // @@ -51,9 +97,90 @@ void CodeGen::genEnregisterOSRArgsAndLocals() unreached(); // OSR not supported on WASM. } +//------------------------------------------------------------------------ +// genHomeRegisterParams: place register arguments into their RA-assigned locations. +// +// For the WASM RA, we have a much simplified (compared to LSRA) contract of: +// - If an argument is live on entry in a set of registers, then the RA will +// assign those registers to that argument on entry. +// This means we never need to do any copying or cycle resolution here. +// +// The main motivation for this (along with the obvious CQ implications) is +// obviating the need to adapt the general "RegGraph"-based algorithm to +// !HAS_FIXED_REGISTER_SET constraints (no reg masks). +// +// Arguments: +// initReg - Unused +// initRegStillZeroed - Unused +// void CodeGen::genHomeRegisterParams(regNumber initReg, bool* initRegStillZeroed) { - // NYI_WASM("Un-undefine 'genHomeRegisterParams' in codegencommon.cpp and proceed from there"); + JITDUMP("*************** In genHomeRegisterParams()\n"); + + auto spillParam = [this](unsigned lclNum, unsigned offset, unsigned paramLclNum, const ABIPassingSegment& segment) { + assert(segment.IsPassedInRegister()); + + LclVarDsc* varDsc = compiler->lvaGetDesc(lclNum); + if (varDsc->lvTracked && !VarSetOps::IsMember(compiler, compiler->fgFirstBB->bbLiveIn, varDsc->lvVarIndex)) + { + return; + } + + if (varDsc->lvOnFrame && (!varDsc->lvIsInReg() || varDsc->lvLiveInOutOfHndlr)) + { + LclVarDsc* paramVarDsc = compiler->lvaGetDesc(paramLclNum); + var_types storeType = genParamStackType(paramVarDsc, segment); + if (!varDsc->TypeIs(TYP_STRUCT) && (genTypeSize(genActualType(varDsc)) < genTypeSize(storeType))) + { + // Can happen for struct fields due to padding. + storeType = genActualType(varDsc); + } + + GetEmitter()->emitIns_I(INS_local_get, EA_PTRSIZE, WasmRegToIndex(GetFramePointerReg())); + GetEmitter()->emitIns_I(INS_local_get, emitActualTypeSize(storeType), + WasmRegToIndex(segment.GetRegister())); + GetEmitter()->emitIns_S(ins_Store(storeType), emitActualTypeSize(storeType), lclNum, offset); + } + + if (varDsc->lvIsInReg()) + { + assert(varDsc->GetRegNum() == segment.GetRegister()); + } + }; + + for (unsigned lclNum = 0; lclNum < compiler->info.compArgsCount; lclNum++) + { + LclVarDsc* lclDsc = compiler->lvaGetDesc(lclNum); + const ABIPassingInformation& abiInfo = compiler->lvaGetParameterABIInfo(lclNum); + + for (const ABIPassingSegment& segment : abiInfo.Segments()) + { + if (!segment.IsPassedInRegister()) + { + continue; + } + + const ParameterRegisterLocalMapping* mapping = + compiler->FindParameterRegisterLocalMappingByRegister(segment.GetRegister()); + + bool spillToBaseLocal = true; + if (mapping != nullptr) + { + spillParam(mapping->LclNum, mapping->Offset, lclNum, segment); + + // If home is shared with base local, then skip spilling to the base local. + if (lclDsc->lvPromoted) + { + spillToBaseLocal = false; + } + } + + if (spillToBaseLocal) + { + spillParam(lclNum, segment.Offset, lclNum, segment); + } + } + } } void CodeGen::genFnEpilog(BasicBlock* block) From 169cea3327d5642e4dbaa17465056002f21574fc Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 17 Jan 2026 15:08:46 -0800 Subject: [PATCH 16/32] Remove nonExpansive parameter from Dictionary::PopulateEntry (#122758) The `nonExpansive` parameter in `Dictionary::PopulateEntry` was always passed as `FALSE` from its only call site in `jithelpers.cpp`. This change removes the dead parameter and simplifies the implementation by eliminating unreachable code paths. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com> --- src/coreclr/vm/genericdict.cpp | 48 +++++----------------------------- src/coreclr/vm/genericdict.h | 1 - src/coreclr/vm/jithelpers.cpp | 2 +- 3 files changed, 7 insertions(+), 44 deletions(-) diff --git a/src/coreclr/vm/genericdict.cpp b/src/coreclr/vm/genericdict.cpp index 4e512fa7cb38f6..e9d15c0fa6bb63 100644 --- a/src/coreclr/vm/genericdict.cpp +++ b/src/coreclr/vm/genericdict.cpp @@ -669,7 +669,6 @@ Dictionary::PopulateEntry( MethodDesc * pMD, MethodTable * pMT, LPVOID signature, - BOOL nonExpansive, DictionaryEntry ** ppSlot, DWORD dictionaryIndexAndSlot, /* = -1 */ Module * pModule /* = NULL */) @@ -811,16 +810,11 @@ Dictionary::PopulateEntry( declaringType = ptr.GetTypeHandleThrowing( pLookupModule, &typeContext, - (nonExpansive ? ClassLoader::DontLoadTypes : ClassLoader::LoadTypes), + ClassLoader::LoadTypes, CLASS_LOADED, FALSE, NULL, pZapSigContext); - if (declaringType.IsNull()) - { - _ASSERTE(nonExpansive); - return NULL; - } IfFailThrow(ptr.SkipExactlyOne()); FALLTHROUGH; @@ -831,16 +825,11 @@ Dictionary::PopulateEntry( TypeHandle th = ptr.GetTypeHandleThrowing( pLookupModule, &typeContext, - (nonExpansive ? ClassLoader::DontLoadTypes : ClassLoader::LoadTypes), + ClassLoader::LoadTypes, CLASS_LOADED, FALSE, NULL, pZapSigContext); - if (th.IsNull()) - { - _ASSERTE(nonExpansive); - return NULL; - } IfFailThrow(ptr.SkipExactlyOne()); if (!declaringType.IsNull()) @@ -864,16 +853,11 @@ Dictionary::PopulateEntry( constraintType = ptr.GetTypeHandleThrowing( pLookupModule, &typeContext, - (nonExpansive ? ClassLoader::DontLoadTypes : ClassLoader::LoadTypes), + ClassLoader::LoadTypes, CLASS_LOADED, FALSE, NULL, pZapSigContext); - if (constraintType.IsNull()) - { - _ASSERTE(nonExpansive); - return NULL; - } IfFailThrow(ptr.SkipExactlyOne()); FALLTHROUGH; @@ -967,7 +951,6 @@ Dictionary::PopulateEntry( if (ownerType.IsNull()) ownerType = pMethod->GetMethodTable(); - _ASSERT(!ownerType.IsNull() && !nonExpansive); pOwnerMT = ownerType.GetMethodTable(); if (kind == DispatchStubAddrSlot && pMethod->IsVtableMethod()) @@ -981,22 +964,13 @@ Dictionary::PopulateEntry( ownerType = ptr.GetTypeHandleThrowing( pLookupModule, &typeContext, - (nonExpansive ? ClassLoader::DontLoadTypes : ClassLoader::LoadTypes), + ClassLoader::LoadTypes, CLASS_LOADED, FALSE, NULL, pZapSigContext); - if (ownerType.IsNull()) - { - _ASSERTE(nonExpansive); - return NULL; - } IfFailThrow(ptr.SkipExactlyOne()); - // wsperf: Create a path that doesn't load types or create new handles if nonExpansive is set - if (nonExpansive) - return NULL; - pOwnerMT = ownerType.GetMethodTable(); _ASSERTE(pOwnerMT != NULL); @@ -1026,16 +1000,11 @@ Dictionary::PopulateEntry( TypeHandle thMethodDefType = ptr.GetTypeHandleThrowing( pLookupModule, &typeContext, - (nonExpansive ? ClassLoader::DontLoadTypes : ClassLoader::LoadTypes), + ClassLoader::LoadTypes, CLASS_LOADED, FALSE, NULL, pZapSigContext); - if (thMethodDefType.IsNull()) - { - _ASSERTE(nonExpansive); - return NULL; - } IfFailThrow(ptr.SkipExactlyOne()); MethodTable * pMethodDefMT = thMethodDefType.GetMethodTable(); _ASSERTE(pMethodDefMT != NULL); @@ -1257,16 +1226,11 @@ Dictionary::PopulateEntry( ownerType = ptr.GetTypeHandleThrowing( pLookupModule, &typeContext, - (nonExpansive ? ClassLoader::DontLoadTypes : ClassLoader::LoadTypes), + ClassLoader::LoadTypes, CLASS_LOADED, FALSE, NULL, pZapSigContext); - if (ownerType.IsNull()) - { - _ASSERTE(nonExpansive); - return NULL; - } IfFailThrow(ptr.SkipExactlyOne()); // Computed by MethodTable::GetIndexForFieldDesc(). diff --git a/src/coreclr/vm/genericdict.h b/src/coreclr/vm/genericdict.h index b0cf4ee6c4b666..be2a0af5569029 100644 --- a/src/coreclr/vm/genericdict.h +++ b/src/coreclr/vm/genericdict.h @@ -276,7 +276,6 @@ class Dictionary static DictionaryEntry PopulateEntry(MethodDesc * pMD, MethodTable * pMT, LPVOID signature, - BOOL nonExpansive, DictionaryEntry ** ppSlot, DWORD dictionaryIndexAndSlot = -1, Module * pModule = NULL); diff --git a/src/coreclr/vm/jithelpers.cpp b/src/coreclr/vm/jithelpers.cpp index 0520a652ff4ac1..4a63050a9493b5 100644 --- a/src/coreclr/vm/jithelpers.cpp +++ b/src/coreclr/vm/jithelpers.cpp @@ -557,7 +557,7 @@ DictionaryEntry GenericHandleWorkerCore(MethodDesc * pMD, MethodTable * pMT, LPV } DictionaryEntry * pSlot; - result = Dictionary::PopulateEntry(pMD, pDeclaringMT, signature, FALSE, &pSlot, dictionaryIndexAndSlot, pModule); + result = Dictionary::PopulateEntry(pMD, pDeclaringMT, signature, &pSlot, dictionaryIndexAndSlot, pModule); if (pMT != NULL && pDeclaringMT != pMT) { From e56355d00f6a152d6fbfe2db88e718e4af5ca24c Mon Sep 17 00:00:00 2001 From: prozolic <42107886+prozolic@users.noreply.github.com> Date: Mon, 19 Jan 2026 01:08:54 +0900 Subject: [PATCH 17/32] Add early return in TryGetLast for empty results. (#123306) This PR adds a fast path in `IEnumerableSkipTakeIterator.TryGetLast`. When source is an Iterator and `GetCount(onlyIfCheap: true)` returns a valid count (not -1), check if count <= _minIndexInclusive. In this case, immediately return with found=false. --------- Co-authored-by: Stephen Toub --- .../System.Linq/src/System/Linq/SkipTake.SpeedOpt.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Linq/src/System/Linq/SkipTake.SpeedOpt.cs b/src/libraries/System.Linq/src/System/Linq/SkipTake.SpeedOpt.cs index 944e0fa4cda70e..0a98b5c88473ca 100644 --- a/src/libraries/System.Linq/src/System/Linq/SkipTake.SpeedOpt.cs +++ b/src/libraries/System.Linq/src/System/Linq/SkipTake.SpeedOpt.cs @@ -441,8 +441,14 @@ public override Iterator Take(int count) { if (_source is Iterator iterator && iterator.GetCount(onlyIfCheap: true) is int count && - count > _minIndexInclusive) + count >= 0) { + if (count <= _minIndexInclusive) + { + found = false; + return default; + } + // If there's no upper bound, or if there are fewer items in the list // than the upper bound allows, just return the last element of the list. // Otherwise, get the element at the upper bound. From 5ea3028c78647d308b6dc8c7e14cd3b7a095e596 Mon Sep 17 00:00:00 2001 From: Rachel Date: Sun, 18 Jan 2026 23:00:40 -0800 Subject: [PATCH 18/32] [NativeAOT] Source to native mapping fix for out-of-order code (#123333) Sometimes code is placed at a higher native offset than the code at IL offsets adjacent to it. Our debugger algorithm deals with this by, during traversal of mappings, [setting the IL offset back](https://github.com/dotnet/runtime/blob/6afecd4adc64210e5b46767d02a6495af1a08c46/src/coreclr/vm/debugdebugger.cpp#L1321) if one is encountered that is lower than the current IL offset in the traversal. Native AOT does not have this mechanism, which leads to such code being misattributed to higher-than-actual source lines. This change reproduces this mechanism in NativeAOT by inserting a native sequence point whenever the IL offset drops below the previous offset. Repro issue (line attribution of DoWork): ```csharp using System.Runtime.CompilerServices; static class Program { #line 53 [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] public async static Task Main() { await DoWork(0); } [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] public async static Task BackgroundWorker1() { // await Task.Delay(10000); // await Task.Yield(); throw new InvalidOperationException("BackgroundWorker1 exception."); } [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] public async static Task BackgroundWorker2() { await Task.Delay(10); Console.WriteLine("BackgroundWorker2 completed."); } [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] public async static Task BackgroundWorker3() { await Task.Delay(10); Console.WriteLine("BackgroundWorker3 completed."); } [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)] static async Task DoWork(int i) { Task worker1 = BackgroundWorker1(); Task worker2 = BackgroundWorker2(); Task worker3 = BackgroundWorker3(); await Task.WhenAll(worker1, worker2, worker3); Console.WriteLine("All background workers completed."); } } ``` Config: Windows x64 Debug, .NET 11.0.100-alpha.1.25618.104, runtime-async on
NativeOffset 0 106 107 107 116 124 124 133 141 141 150 158 169 183 192 205 222 231 244 261 270 283 307 312 357 379 380 387 392 393 420 431 867 878 1067 1096
ILOffset 0 0 1 1 6 7 7 12 13 13 18 19 27 30 35 37 40 45 47 50 55 57 60 65 70 75 76 81 86 87 87 70 87 70 0 87
ILOffset 0 1 7 13 19 76
LineNumber 80 81 82 83 84 85
Before:
NativeOffset 0 106 107 124 141 158 380 1067
LineNumber 80 80 81 82 83 84 85 80
Notice in particular that code after native offset 431, the suspension code for DoWork, is mistakenly attributed to line 85 (Console.WriteLine). After:
NativeOffset 0 106 107 124 141 158 380 431 878 1067
LineNumber 80 80 81 82 83 84 85 84 84 80
Here we see that 380-431 is attributed to Console.WriteLine, while the suspension code that follows is correctly attributed to the previous line of await. [aot.txt](https://github.com/user-attachments/files/24697543/aot.txt) --- .../DependencyAnalysis/MethodCodeNode.cs | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/DependencyAnalysis/MethodCodeNode.cs b/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/DependencyAnalysis/MethodCodeNode.cs index 389181e6c8511c..f2853124566b34 100644 --- a/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/DependencyAnalysis/MethodCodeNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/DependencyAnalysis/MethodCodeNode.cs @@ -260,9 +260,10 @@ public IEnumerable GetNativeSequencePoints() if (_debugLocInfos == null) yield break; - var sequencePoints = new (string Document, int LineNumber)[_debugLocInfos.Length * 4 /* chosen empirically */]; + var sequencePoints = new (string Document, int LineNumber, bool IsBackedSequencePoint)[_debugLocInfos.Length * 4 /* chosen empirically */]; try { + int maxOffset = 0; foreach (var sequencePoint in _debugInfo.GetSequencePoints()) { int offset = sequencePoint.Offset; @@ -271,7 +272,17 @@ public IEnumerable GetNativeSequencePoints() int newLength = Math.Max(2 * sequencePoints.Length, sequencePoint.Offset + 1); Array.Resize(ref sequencePoints, newLength); } - sequencePoints[offset] = (sequencePoint.Document, sequencePoint.LineNumber); + sequencePoints[offset] = (sequencePoint.Document, sequencePoint.LineNumber, true); + maxOffset = Math.Max(maxOffset, offset); + } + + // Propagate last known document/line number forward to enable correct mapping when IL offsets decrease at higher native offsets + for (int i = 1; i <= maxOffset; i++) + { + if (sequencePoints[i].Document == null && sequencePoints[i - 1].Document != null) + { + sequencePoints[i] = (sequencePoints[i - 1].Document, sequencePoints[i - 1].LineNumber, false); + } } } catch (BadImageFormatException) @@ -283,6 +294,7 @@ public IEnumerable GetNativeSequencePoints() } int previousNativeOffset = -1; + int previousIlOffset = -1; foreach (var nativeMapping in _debugLocInfos) { if (nativeMapping.NativeOffset == previousNativeOffset) @@ -291,13 +303,18 @@ public IEnumerable GetNativeSequencePoints() if (nativeMapping.ILOffset < sequencePoints.Length) { var sequencePoint = sequencePoints[nativeMapping.ILOffset]; - if (sequencePoint.Document != null) + // Emit sequence point if we have it from _debugInfo or if ILOffset decreases. + // This handles the case of IL offsets decreasing at higher native offsets. + // See WalkILOffsetsCallback in src/coreclr/vm/debugdebugger.cpp for more details. + if ((sequencePoint.IsBackedSequencePoint || nativeMapping.ILOffset < previousIlOffset) && + sequencePoint.Document != null) { yield return new NativeSequencePoint( nativeMapping.NativeOffset, sequencePoint.Document, sequencePoint.LineNumber); previousNativeOffset = nativeMapping.NativeOffset; + previousIlOffset = nativeMapping.ILOffset; } } } From b1ab9b3a65cacfbb30bff4d06f159ce3914be65f Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 08:35:04 +0100 Subject: [PATCH 19/32] Simplify branching in ILImporter.Scanner.cs with Debug.Assert (#123235) --- .../tools/aot/ILCompiler.Compiler/IL/ILImporter.Scanner.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/IL/ILImporter.Scanner.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/IL/ILImporter.Scanner.cs index 5dc003a79f081c..49820dc03374d9 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/IL/ILImporter.Scanner.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/IL/ILImporter.Scanner.cs @@ -796,8 +796,9 @@ private void ImportCall(ILOpcode opcode, int token) { instParam = GetGenericLookupHelper(ReadyToRunHelperId.TypeHandle, runtimeDeterminedMethod.OwningType); } - else if (targetMethod.AcquiresInstMethodTableFromThis()) + else { + Debug.Assert(targetMethod.AcquiresInstMethodTableFromThis()); _dependencies.Add(_factory.ShadowNonConcreteMethod(concreteMethod), reason); } From a2e63e7feae2098cafab7cff28d2b6779874233b Mon Sep 17 00:00:00 2001 From: Gordon Ross Date: Mon, 19 Jan 2026 06:22:59 -0500 Subject: [PATCH 20/32] SunOS process and thread support (#105403) Read binary psinfo for System.Diagnostic.Process on SunOS (Solaris or illumos). No failures in System.Diagnostic.Process.Tests (but lots of skip) --------- Co-authored-by: Austin Wise Co-authored-by: Adeel Mujahid <3840695+am11@users.noreply.github.com> Co-authored-by: Jan Kotas --- .../Interop.ProcFs.GetProcessInfoById.cs | 74 +++++ .../Interop.ProcFs.GetThreadInfoById.cs | 53 ++++ ...rop.ProcFsStat.TryReadProcessStatusInfo.cs | 37 --- .../TestUtilities/System/PlatformDetection.cs | 1 + .../src/System.Diagnostics.Process.csproj | 13 +- .../src/System/Diagnostics/Process.SunOS.cs | 136 ++++++++++ .../Diagnostics/ProcessManager.SunOS.cs | 254 ++++++++++++++++++ .../System/Diagnostics/ProcessThread.SunOS.cs | 131 +++++++++ .../tests/ProcessTests.cs | 4 +- .../System.Private.CoreLib.Shared.projitems | 2 +- .../src/System/Environment.SunOS.cs | 3 +- src/native/libs/System.Native/entrypoints.c | 3 +- src/native/libs/System.Native/pal_io.c | 86 +++++- src/native/libs/System.Native/pal_io.h | 42 ++- src/native/libs/System.Native/pal_process.c | 3 + 15 files changed, 782 insertions(+), 60 deletions(-) create mode 100644 src/libraries/Common/src/Interop/SunOS/procfs/Interop.ProcFs.GetProcessInfoById.cs create mode 100644 src/libraries/Common/src/Interop/SunOS/procfs/Interop.ProcFs.GetThreadInfoById.cs delete mode 100644 src/libraries/Common/src/Interop/SunOS/procfs/Interop.ProcFsStat.TryReadProcessStatusInfo.cs create mode 100644 src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.SunOS.cs create mode 100644 src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.SunOS.cs create mode 100644 src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessThread.SunOS.cs diff --git a/src/libraries/Common/src/Interop/SunOS/procfs/Interop.ProcFs.GetProcessInfoById.cs b/src/libraries/Common/src/Interop/SunOS/procfs/Interop.ProcFs.GetProcessInfoById.cs new file mode 100644 index 00000000000000..0fb1f430198a08 --- /dev/null +++ b/src/libraries/Common/src/Interop/SunOS/procfs/Interop.ProcFs.GetProcessInfoById.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class @procfs + { + // Constants from sys/procfs.h + private const int PRARGSZ = 80; + + // Output type for GetProcessInfoById() + // Keep in sync with pal_io.h ProcessInfo + [StructLayout(LayoutKind.Sequential)] + internal struct ProcessInfo + { + internal ulong VirtualSize; + internal ulong ResidentSetSize; + internal long StartTime; + internal long StartTimeNsec; + internal long CpuTotalTime; + internal long CpuTotalTimeNsec; + internal int Pid; + internal int ParentPid; + internal int SessionId; + internal int Priority; + internal int NiceVal; + } + + [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_ReadProcessInfo", SetLastError = true)] + private static unsafe partial int ReadProcessInfo(int pid, ProcessInfo* processInfo, byte* argBuf, int argBufSize); + + /// + /// Attempts to get status info for the specified process ID. + /// + /// PID of the process to read status info for. + /// The pointer to ProcessInfo instance. + /// + /// true if the process status was read; otherwise, false. + /// + internal static unsafe bool GetProcessInfoById(int pid, out ProcessInfo processInfo) + { + fixed (ProcessInfo* pProcessInfo = &processInfo) + { + if (ReadProcessInfo(pid, pProcessInfo, null, 0) < 0) + { + return false; + } + } + return true; + } + + // Variant that also gets the arg string. + internal static unsafe bool GetProcessInfoById(int pid, out ProcessInfo processInfo, out string argString) + { + byte* argBuf = stackalloc byte[PRARGSZ]; + fixed (ProcessInfo* pProcessInfo = &processInfo) + { + if (ReadProcessInfo(pid, pProcessInfo, argBuf, PRARGSZ) < 0) + { + argString = ""; + return false; + } + } + argString = Marshal.PtrToStringUTF8((IntPtr)argBuf)!; + return true; + } + } +} diff --git a/src/libraries/Common/src/Interop/SunOS/procfs/Interop.ProcFs.GetThreadInfoById.cs b/src/libraries/Common/src/Interop/SunOS/procfs/Interop.ProcFs.GetThreadInfoById.cs new file mode 100644 index 00000000000000..a993c89f7b9a35 --- /dev/null +++ b/src/libraries/Common/src/Interop/SunOS/procfs/Interop.ProcFs.GetThreadInfoById.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class @procfs + { + // Output type for GetThreadInfoById() + // Keep in sync with pal_io.h ThreadInfo + [StructLayout(LayoutKind.Sequential)] + internal struct ThreadInfo + { + internal long StartTime; + internal long StartTimeNsec; + internal long CpuTotalTime; // user+sys + internal long CpuTotalTimeNsec; + internal int Tid; + internal int Priority; + internal int NiceVal; + internal char StatusCode; + } + + [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_ReadThreadInfo", SetLastError = true)] + private static unsafe partial int ReadThreadInfo(int pid, int tid, ThreadInfo* threadInfo); + + /// + /// Attempts to get status info for the specified thread ID. + /// + /// PID of the process to read status info for. + /// TID of the thread to read status info for. + /// The pointer to ThreadInfo instance. + /// + /// true if the process status was read; otherwise, false. + /// + internal static unsafe bool GetThreadInfoById(int pid, int tid, out ThreadInfo threadInfo) + { + fixed (ThreadInfo* pThreadInfo = &threadInfo) + { + if (ReadThreadInfo(pid, tid, pThreadInfo) < 0) + { + return false; + } + } + return true; + } + } +} diff --git a/src/libraries/Common/src/Interop/SunOS/procfs/Interop.ProcFsStat.TryReadProcessStatusInfo.cs b/src/libraries/Common/src/Interop/SunOS/procfs/Interop.ProcFsStat.TryReadProcessStatusInfo.cs deleted file mode 100644 index 5d835f241cda1b..00000000000000 --- a/src/libraries/Common/src/Interop/SunOS/procfs/Interop.ProcFsStat.TryReadProcessStatusInfo.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Runtime.InteropServices; - -internal static partial class Interop -{ - internal static partial class @procfs - { - /// - /// Attempts to get status info for the specified process ID. - /// - /// PID of the process to read status info for. - /// The pointer to processStatus instance. - /// - /// true if the process status was read; otherwise, false. - /// - [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_ReadProcessStatusInfo", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - private static unsafe partial bool TryReadProcessStatusInfo(int pid, ProcessStatusInfo* processStatus); - - internal struct ProcessStatusInfo - { - internal nuint ResidentSetSize; - // add more fields when needed. - } - - internal static unsafe bool TryReadProcessStatusInfo(int pid, out ProcessStatusInfo statusInfo) - { - statusInfo = default; - fixed (ProcessStatusInfo* pStatusInfo = &statusInfo) - { - return TryReadProcessStatusInfo(pid, pStatusInfo); - } - } - } -} diff --git a/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs b/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs index 4b4f2b3a32ca35..3a32912136e992 100644 --- a/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs +++ b/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs @@ -50,6 +50,7 @@ public static partial class PlatformDetection public static bool IsNotMacCatalyst => !IsMacCatalyst; public static bool Isillumos => RuntimeInformation.IsOSPlatform(OSPlatform.Create("ILLUMOS")); public static bool IsSolaris => RuntimeInformation.IsOSPlatform(OSPlatform.Create("SOLARIS")); + public static bool IsSunOS => Isillumos || IsSolaris; public static bool IsBrowser => RuntimeInformation.IsOSPlatform(OSPlatform.Create("BROWSER")); public static bool IsWasi => RuntimeInformation.IsOSPlatform(OSPlatform.Create("WASI")); public static bool IsWasm => IsBrowser || IsWasi; diff --git a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj index 1207ea97a10aef..6738fbbe805725 100644 --- a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj +++ b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj @@ -1,7 +1,7 @@ - $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-freebsd;$(NetCoreAppCurrent)-linux;$(NetCoreAppCurrent)-osx;$(NetCoreAppCurrent)-maccatalyst;$(NetCoreAppCurrent)-ios;$(NetCoreAppCurrent)-tvos;$(NetCoreAppCurrent) + $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-freebsd;$(NetCoreAppCurrent)-linux;$(NetCoreAppCurrent)-osx;$(NetCoreAppCurrent)-maccatalyst;$(NetCoreAppCurrent)-ios;$(NetCoreAppCurrent)-tvos;$(NetCoreAppCurrent)-illumos;$(NetCoreAppCurrent)-solaris;$(NetCoreAppCurrent) $(DefineConstants);FEATURE_REGISTRY true false @@ -369,6 +369,17 @@ Link="Common\Interop\FreeBSD\Interop.Process.GetProcInfo.cs" /> + + + + + + + + + diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.SunOS.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.SunOS.cs new file mode 100644 index 00000000000000..f40076a3882aca --- /dev/null +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.SunOS.cs @@ -0,0 +1,136 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.IO; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Text; +using System.Threading; + +namespace System.Diagnostics +{ + public partial class Process : IDisposable + { + /// Gets the time the associated process was started. + internal DateTime StartTimeCore + { + get + { + Interop.procfs.ProcessInfo processInfo = GetProcInfo(); + + DateTime startTime = DateTime.UnixEpoch + + TimeSpan.FromSeconds(processInfo.StartTime) + + TimeSpan.FromMicroseconds(processInfo.StartTimeNsec / 1000); + + // The return value is expected to be in the local time zone. + return startTime.ToLocalTime(); + } + } + + /// Gets the parent process ID + private int ParentProcessId => GetProcInfo().ParentPid; + + /// Gets execution path + private static string? GetPathToOpenFile() + { + return FindProgramInPath("xdg-open"); + } + + /// + /// Gets the amount of time the associated process has spent utilizing the CPU. + /// It is the sum of the and + /// . + /// + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + [SupportedOSPlatform("maccatalyst")] + public TimeSpan TotalProcessorTime + { + get + { + // a.k.a. "user" + "system" time + Interop.procfs.ProcessInfo processInfo = GetProcInfo(); + TimeSpan ts = TimeSpan.FromSeconds(processInfo.CpuTotalTime) + + TimeSpan.FromMicroseconds(processInfo.CpuTotalTimeNsec / 1000); + return ts; + } + } + + /// + /// Gets the amount of time the associated process has spent running code + /// inside the application portion of the process (not the operating system core). + /// + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + [SupportedOSPlatform("maccatalyst")] + public TimeSpan UserProcessorTime + { + get + { + // a.k.a. "user" time + // Could get this from /proc/$pid/status + // Just say it's all user time for now + return TotalProcessorTime; + } + } + + /// + /// Gets the amount of time the process has spent running code inside the operating + /// system core. + /// + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + [SupportedOSPlatform("maccatalyst")] + public TimeSpan PrivilegedProcessorTime + { + get + { + // a.k.a. "system" time + // Could get this from /proc/$pid/status + // Just say it's all user time for now + EnsureState(State.HaveNonExitedId); + return TimeSpan.Zero; + } + } + + // ---------------------------------- + // ---- Unix PAL layer ends here ---- + // ---------------------------------- + + /// Gets the name that was used to start the process, or null if it could not be retrieved. + internal static string? GetUntruncatedProcessName(ref Interop.procfs.ProcessInfo processInfo, ref string argString) + { + // This assumes the process name is the first part of the Args string + // ending at the first space. That seems to work well enough for now. + // If someday this need to support a process name containing spaces, + // this could call a new Interop function that reads /proc/$pid/auxv + // (sys/auxv.h) and gets the AT_SUN_EXECNAME string from that file. + if (processInfo.Pid != 0 && !string.IsNullOrEmpty(argString)) + { + string[] argv = argString.Split(' ', 2); + if (!string.IsNullOrEmpty(argv[0])) + { + return Path.GetFileName(argv[0]); + } + } + return null; + } + + /// Reads the information for this process from the procfs file system. + private Interop.procfs.ProcessInfo GetProcInfo() + { + EnsureState(State.HaveNonExitedId); + Interop.procfs.ProcessInfo processInfo; + if (!Interop.procfs.GetProcessInfoById(_processId, out processInfo)) + { + throw new Win32Exception(SR.ProcessInformationUnavailable); + } + return processInfo; + } + } +} diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.SunOS.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.SunOS.cs new file mode 100644 index 00000000000000..9b8cc20ac57335 --- /dev/null +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.SunOS.cs @@ -0,0 +1,254 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Runtime.InteropServices; + +namespace System.Diagnostics +{ + internal static partial class ProcessManager + { + /// Gets the IDs of all processes on the current machine. + public static int[] GetProcessIds() + { + IEnumerable pids = EnumerateProcessIds(); + return new List(pids).ToArray(); + } + + /// Gets process infos for each process on the specified machine. + /// Optional process name to use as an inclusion filter. + /// The target machine. + /// An array of process infos, one per found process. + public static ProcessInfo[] GetProcessInfos(string? processNameFilter, string machineName) + { + ThrowIfRemoteMachine(machineName); + + // Iterate through all process IDs to load information about each process + IEnumerable pids = EnumerateProcessIds(); + ArrayBuilder processes = default; + foreach (int pid in pids) + { + ProcessInfo? pi = CreateProcessInfo(pid, processNameFilter); + if (pi != null) + { + processes.Add(pi); + } + } + + return processes.ToArray(); + } + + /// Gets an array of module infos for the specified process. + /// The ID of the process whose modules should be enumerated. + /// The array of modules. + internal static ProcessModuleCollection GetModules(int processId) + { + + // Negative PIDs aren't valid + ArgumentOutOfRangeException.ThrowIfNegative(processId); + + // GetModules(x)[0].FileName is often used to find the path to the executable, + // so at least get that. That appears to be sufficient, at least for now. + // If needed, the full list of loaded modules could be obtained using another + // Interop function to read /proc/$pid/auxv similar to how the "pargs" and "pldd" + // commands do their work. + + string argString; + Interop.procfs.ProcessInfo processInfo; + if (Interop.procfs.GetProcessInfoById(processId, out processInfo, out argString)) + { + string? fullName = Process.GetUntruncatedProcessName(ref processInfo, ref argString); + if (!string.IsNullOrEmpty(fullName)) + { + return new ProcessModuleCollection(1) + { + new ProcessModule(fullName, Path.GetFileName(fullName)) + }; + } + } + return new ProcessModuleCollection(0); + } + + /// + /// Creates a ProcessInfo from the specified process ID. + /// + internal static ProcessInfo? CreateProcessInfo(int pid, string? processNameFilter = null) + { + // Negative PIDs aren't valid + ArgumentOutOfRangeException.ThrowIfNegative(pid); + + Interop.procfs.ProcessInfo processInfo; + string argString; + if (!Interop.procfs.GetProcessInfoById(pid, out processInfo, out argString)) + { + return null; + } + + string? processName = Process.GetUntruncatedProcessName(ref processInfo, ref argString); + if (!string.IsNullOrEmpty(processNameFilter) && + !string.Equals(processName, processNameFilter, StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + return CreateProcessInfo(ref processInfo, ref argString); + } + + // ---------------------------------- + // ---- Unix PAL layer ends here ---- + // ---------------------------------- + + /// Enumerates the IDs of all processes on the current machine. + internal static IEnumerable EnumerateProcessIds() + { + // Parse /proc for any directory that's named with a number. Each such + // directory represents a process. + const string dir = "/proc"; + foreach (string procDir in Directory.EnumerateDirectories(dir)) + { + string dirName = Path.GetFileName(procDir); + int pid; + if (int.TryParse(dirName, NumberStyles.Integer, CultureInfo.InvariantCulture, out pid)) + { + Debug.Assert(pid >= 0); + yield return pid; + } + } + } + + /// Enumerates the IDs of all threads in the specified process. + internal static IEnumerable EnumerateThreadIds(int pid) + { + // Parse /proc/$pid/lwp for any directory that's named with a number. + // Each such directory represents a thread. + string dir = $"/proc/{(uint)pid}/lwp"; + foreach (string lwpDir in Directory.EnumerateDirectories(dir)) + { + string dirName = Path.GetFileName(lwpDir); + int tid; + if (int.TryParse(dirName, NumberStyles.Integer, CultureInfo.InvariantCulture, out tid)) + { + Debug.Assert(tid >= 0); + yield return tid; + } + } + } + + /// + /// Creates a ProcessInfo from the data read from a /proc/pid/psinfo file and the associated lwp directory. + /// + internal static ProcessInfo CreateProcessInfo(ref Interop.procfs.ProcessInfo processInfo, ref string argString) + { + int pid = processInfo.Pid; + + string? name = Process.GetUntruncatedProcessName(ref processInfo, ref argString); + if (string.IsNullOrEmpty(name)) { + // We were able to read the psinfo for this process, but the /proc reader + // found no arg string. The process exists, so rather than fail for this, + // just continue with a made-up arg string. + // Debug.Fail($"Failed to get args for proc {0:D}"); + name = string.Format("", pid); + } + + var pi = new ProcessInfo() + { + ProcessId = pid, + ProcessName = name, + BasePriority = processInfo.Priority, + SessionId = processInfo.SessionId, + VirtualBytes = (long)processInfo.VirtualSize, + WorkingSet = (long)processInfo.ResidentSetSize, + // StartTime: See Process.StartTimeCore() + }; + + // Then read through /proc/pid/lwp/ to find each thread in the process... + // Can we use a "get" method to avoid loading this for every process until it's asked for? + try + { + + // Iterate through all thread IDs to load information about each thread + IEnumerable tids = EnumerateThreadIds(pid); + + foreach (int tid in tids) + { + Interop.procfs.ThreadInfo threadInfo; + ThreadInfo? ti; + + if (!Interop.procfs.GetThreadInfoById(pid, tid, out threadInfo)) + { + continue; + } + + ti = CreateThreadInfo(ref processInfo, ref threadInfo); + if (ti != null) + { + pi._threadInfoList.Add(ti); + } + } + } + catch (IOException) + { + // Between the time that we get an ID and the time that we try to read the associated + // directories and files in procfs, the process could be gone. + } + + // Finally return what we've built up + return pi; + } + + /// + /// Creates a ThreadInfo from the data read from a /proc/pid/lwp/lwpsinfo file. + /// + internal static ThreadInfo CreateThreadInfo(ref Interop.procfs.ProcessInfo processInfo, + ref Interop.procfs.ThreadInfo threadInfo) + { + + var ti = new ThreadInfo() + { + _processId = processInfo.Pid, + _threadId = (ulong)threadInfo.Tid, + _basePriority = threadInfo.Priority, + _currentPriority = threadInfo.Priority, + _startAddress = null, + _threadState = ProcFsStateToThreadState(threadInfo.StatusCode), + _threadWaitReason = ThreadWaitReason.Unknown + }; + + return ti; + } + + /// Gets a ThreadState to represent the value returned from the status field of /proc/pid/stat. + /// The status field value. + /// + private static ThreadState ProcFsStateToThreadState(char c) + { + // Information on these in fs/proc/array.c + // `man proc` does not document them all + switch (c) + { + case 'O': // On-CPU + case 'R': // Runnable + return ThreadState.Running; + + case 'S': // Sleeping in a wait + case 'T': // Stopped on a signal + return ThreadState.Wait; + + case 'Z': // Zombie + return ThreadState.Terminated; + + case 'W': // Waiting for CPU + return ThreadState.Transition; + + case '\0': // new, not started yet + return ThreadState.Initialized; + + default: + Debug.Fail($"Unexpected status character: {(int)c}"); + return ThreadState.Unknown; + } + } + } +} diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessThread.SunOS.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessThread.SunOS.cs new file mode 100644 index 00000000000000..4f585dce18aee3 --- /dev/null +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessThread.SunOS.cs @@ -0,0 +1,131 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.Versioning; + +namespace System.Diagnostics +{ + public partial class ProcessThread + { + /// Gets the time this thread was started. + internal DateTime GetStartTime() + { + Interop.procfs.ThreadInfo threadInfo = GetThreadInfo(); + + DateTime startTime = DateTime.UnixEpoch + + TimeSpan.FromSeconds(threadInfo.StartTime) + + TimeSpan.FromMicroseconds(threadInfo.StartTimeNsec / 1000); + + // The return value is expected to be in the local time zone. + return startTime.ToLocalTime(); + } + + /// + /// Returns or sets the priority level of the associated thread. The priority level is + /// not an absolute level, but instead contributes to the actual thread priority by + /// considering the priority class of the process. + /// + private ThreadPriorityLevel PriorityLevelCore + { + get + { + Interop.procfs.ThreadInfo threadInfo = GetThreadInfo(); + return GetThreadPriorityFromSysPri(threadInfo.Priority); + } + set + { + // Raising priority is a privileged operation. + // Might be able to adjust our "nice" value. Maybe later... + throw new PlatformNotSupportedException(); + } + } + + /// + /// Gets the amount of time the associated thread has spent utilizing the CPU. + /// It is the sum of the System.Diagnostics.ProcessThread.UserProcessorTime and + /// System.Diagnostics.ProcessThread.PrivilegedProcessorTime. + /// + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + [SupportedOSPlatform("maccatalyst")] + public TimeSpan TotalProcessorTime + { + get + { + // a.k.a. "user" + "system" time + Interop.procfs.ThreadInfo threadInfo = GetThreadInfo(); + TimeSpan ts = TimeSpan.FromSeconds(threadInfo.CpuTotalTime) + + TimeSpan.FromMicroseconds(threadInfo.CpuTotalTimeNsec / 1000); + return ts; + } + } + + /// + /// Gets the amount of time the associated thread has spent running code + /// inside the application (not the operating system core). + /// + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + [SupportedOSPlatform("maccatalyst")] + public TimeSpan UserProcessorTime + { + get + { + // a.k.a. "user" time + // Could get this from /proc/$pid/lwp/$lwpid/lwpstatus + // Just say it's all user time for now + return TotalProcessorTime; + } + } + + /// + /// Gets the amount of time the thread has spent running code inside the operating + /// system core. + /// + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + [SupportedOSPlatform("maccatalyst")] + public TimeSpan PrivilegedProcessorTime + { + get + { + // a.k.a. "system" time + // Could get this from /proc/$pid/lwp/$lwpid/lwpstatus + // Just say it's all user time for now + return TimeSpan.Zero; + } + + } + + // ---------------------------------- + // ---- exported stuff ends here ---- + // ---------------------------------- + + // System priorities go from 1 to 100, where 60 and above are for "system" things + // These mappingsare relatively arbitrary. Normal user processes run at priority 59. + // and the other values above and below are simply distributed somewhat evenly. + private static System.Diagnostics.ThreadPriorityLevel GetThreadPriorityFromSysPri(int pri) + { + Debug.Assert((pri >= 0) && (pri <= 100)); + return + (pri >= 90) ? ThreadPriorityLevel.TimeCritical : + (pri >= 80) ? ThreadPriorityLevel.Highest : + (pri >= 60) ? ThreadPriorityLevel.AboveNormal : + (pri == 59) ? ThreadPriorityLevel.Normal : + (pri >= 40) ? ThreadPriorityLevel.BelowNormal : + (pri >= 20) ? ThreadPriorityLevel.Lowest : + ThreadPriorityLevel.Idle; + } + + /// Reads the information for this thread from the procfs file system. + private Interop.procfs.ThreadInfo GetThreadInfo() + { + Interop.procfs.ThreadInfo threadInfo; + if (!Interop.procfs.GetThreadInfoById(_processId, tid: Id, out threadInfo)) + { + throw new InvalidOperationException(SR.Format(SR.ThreadExited, Id)); + } + return threadInfo; + } + } +} diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.cs index 086ad89f0d53ad..901cfcfa78a024 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.cs @@ -652,7 +652,7 @@ public void TestMaxWorkingSet() Assert.InRange((long)p.MinWorkingSet, 0, long.MaxValue); } - if (OperatingSystem.IsMacOS() || OperatingSystem.IsFreeBSD()) { + if (OperatingSystem.IsMacOS() || OperatingSystem.IsFreeBSD() || PlatformDetection.IsSunOS) { return; // doesn't support getting/setting working set for other processes } @@ -700,7 +700,7 @@ public void TestMinWorkingSet() Assert.InRange((long)p.MinWorkingSet, 0, long.MaxValue); } - if (OperatingSystem.IsMacOS() || OperatingSystem.IsFreeBSD()) { + if (OperatingSystem.IsMacOS() || OperatingSystem.IsFreeBSD() || PlatformDetection.IsSunOS) { return; // doesn't support getting/setting working set for other processes } diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index d30c305a7e6fae..e2766fbc58a8d4 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -2661,7 +2661,7 @@ - + diff --git a/src/libraries/System.Private.CoreLib/src/System/Environment.SunOS.cs b/src/libraries/System.Private.CoreLib/src/System/Environment.SunOS.cs index fb7ff75cb2e6c6..50a2150ffa613d 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Environment.SunOS.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Environment.SunOS.cs @@ -7,7 +7,6 @@ namespace System { public static partial class Environment { - public static long WorkingSet => - (long)(Interop.procfs.TryReadProcessStatusInfo(ProcessId, out Interop.procfs.ProcessStatusInfo status) ? status.ResidentSetSize : 0); + public static long WorkingSet => (long)(Interop.procfs.GetProcessInfoById(ProcessId, out Interop.procfs.ProcessInfo iProcInfo) ? iProcInfo.ResidentSetSize : 0); } } diff --git a/src/native/libs/System.Native/entrypoints.c b/src/native/libs/System.Native/entrypoints.c index d5121676bbf642..7d2acc37e8af07 100644 --- a/src/native/libs/System.Native/entrypoints.c +++ b/src/native/libs/System.Native/entrypoints.c @@ -121,7 +121,8 @@ static const Entry s_sysNative[] = DllImportEntry(SystemNative_LChflagsCanSetHiddenFlag) DllImportEntry(SystemNative_FChflags) DllImportEntry(SystemNative_CanGetHiddenFlag) - DllImportEntry(SystemNative_ReadProcessStatusInfo) + DllImportEntry(SystemNative_ReadThreadInfo) + DllImportEntry(SystemNative_ReadProcessInfo) DllImportEntry(SystemNative_Log) DllImportEntry(SystemNative_LogError) DllImportEntry(SystemNative_AlignedAlloc) diff --git a/src/native/libs/System.Native/pal_io.c b/src/native/libs/System.Native/pal_io.c index f7af86614ded42..4e1587be8e4f0b 100644 --- a/src/native/libs/System.Native/pal_io.c +++ b/src/native/libs/System.Native/pal_io.c @@ -1849,31 +1849,97 @@ int32_t SystemNative_CanGetHiddenFlag(void) #endif } -int32_t SystemNative_ReadProcessStatusInfo(pid_t pid, ProcessStatus* processStatus) +int32_t SystemNative_ReadThreadInfo(int32_t pid, int32_t tid, ThreadInfo* threadInfo) { #ifdef __sun - char statusFilename[64]; - snprintf(statusFilename, sizeof(statusFilename), "/proc/%d/psinfo", pid); + char infoFilename[64]; + snprintf(infoFilename, sizeof(infoFilename), "/proc/%d/lwp/%d/lwpsinfo", pid, tid); intptr_t fd; - while ((fd = open(statusFilename, O_RDONLY)) < 0 && errno == EINTR); + while ((fd = open(infoFilename, O_RDONLY)) < 0 && errno == EINTR); if (fd < 0) { return 0; } - psinfo_t status; - int result = Common_Read(fd, &status, sizeof(psinfo_t)); + lwpsinfo_t pr; + int result = Common_Read(fd, &pr, sizeof(pr)); close(fd); - if (result >= 0) + if (result < sizeof (pr)) { - processStatus->ResidentSetSize = status.pr_rssize * 1024; // pr_rssize is in Kbytes - return 1; + errno = EIO; + return -1; + } + + threadInfo->Tid = pr.pr_lwpid; + threadInfo->Priority = pr.pr_pri; + threadInfo->NiceVal = pr.pr_nice; + // Status code, a char: ... + threadInfo->StatusCode = (uchar_t)pr.pr_sname; + // Thread start time and CPU time + threadInfo->StartTime = pr.pr_start.tv_sec; + threadInfo->StartTimeNsec = pr.pr_start.tv_nsec; + threadInfo->CpuTotalTime = pr.pr_time.tv_sec; + threadInfo->CpuTotalTimeNsec = pr.pr_time.tv_nsec; + + return 0; +#else + (void)pid, (void)tid, (void)threadInfo; + errno = ENOTSUP; + return -1; +#endif // __sun +} + +// The struct passing is limited, so the args string is handled separately here. +int32_t SystemNative_ReadProcessInfo(int32_t pid, ProcessInfo* processInfo, uint8_t *argBuf, int32_t argBufSize) +{ +#ifdef __sun + if (argBufSize != 0 && argBufSize < PRARGSZ) + { + errno = EINVAL; + return -1; + } + + char infoFilename[64]; + snprintf(infoFilename, sizeof(infoFilename), "/proc/%d/psinfo", pid); + + intptr_t fd; + while ((fd = open(infoFilename, O_RDONLY)) < 0 && errno == EINTR); + if (fd < 0) + { + return 0; + } + + psinfo_t pr; + int result = Common_Read(fd, &pr, sizeof(pr)); + close(fd); + if (result < sizeof (pr)) + { + errno = EIO; + return -1; + } + + processInfo->Pid = pr.pr_pid; + processInfo->ParentPid = pr.pr_ppid; + processInfo->SessionId = pr.pr_sid; + processInfo->Priority = pr.pr_lwp.pr_pri; + processInfo->NiceVal = pr.pr_lwp.pr_nice; + // pr_size and pr_rsize are in Kbytes. + processInfo->VirtualSize = (uint64_t)pr.pr_size * 1024; + processInfo->ResidentSetSize = (uint64_t)pr.pr_rssize * 1024; + processInfo->StartTime = pr.pr_start.tv_sec; + processInfo->StartTimeNsec = pr.pr_start.tv_nsec; + processInfo->CpuTotalTime = pr.pr_time.tv_sec; + processInfo->CpuTotalTimeNsec = pr.pr_time.tv_nsec; + + if (argBuf != NULL && argBufSize != 0) + { + SafeStringCopy((char*)argBuf, PRARGSZ, pr.pr_psargs); } return 0; #else - (void)pid, (void)processStatus; + (void)pid, (void)processInfo, (void)argBuf, (void)argBufSize; errno = ENOTSUP; return -1; #endif // __sun diff --git a/src/native/libs/System.Native/pal_io.h b/src/native/libs/System.Native/pal_io.h index 330fa2997b9f20..2517e5d6297d8b 100644 --- a/src/native/libs/System.Native/pal_io.h +++ b/src/native/libs/System.Native/pal_io.h @@ -38,9 +38,32 @@ typedef struct typedef struct { - size_t ResidentSetSize; - // add more fields when needed. -} ProcessStatus; + // note: sorted by size to avoid alignment padding + uint64_t VirtualSize; + uint64_t ResidentSetSize; + int64_t StartTime; // time proc. started + int64_t StartTimeNsec; + int64_t CpuTotalTime; // Cumulative CPU time (user+sys) + int64_t CpuTotalTimeNsec; + int32_t Pid; + int32_t ParentPid; + int32_t SessionId; + int32_t Priority; + int32_t NiceVal; +} ProcessInfo; + +typedef struct +{ + // note: sorted by size to avoid alignment padding + int64_t StartTime; // time thread started + int64_t StartTimeNsec; + int64_t CpuTotalTime; // cumulative CPU time (user+sys) + int64_t CpuTotalTimeNsec; + int32_t Tid; + int32_t Priority; + int32_t NiceVal; + uint16_t StatusCode; // See ProcFsStateToThreadState() +} ThreadInfo; // NOTE: the layout of this type is intended to exactly match the layout of a `struct iovec`. There are // assertions in pal_networking.c that validate this. @@ -805,11 +828,18 @@ PALEXPORT int32_t SystemNative_LChflagsCanSetHiddenFlag(void); PALEXPORT int32_t SystemNative_CanGetHiddenFlag(void); /** - * Reads the psinfo_t struct and converts into ProcessStatus. + * Reads the lwpsinfo_t struct and converts into ThreadInfo. * - * Returns 1 if the process status was read; otherwise, 0. + * Returns 0 on success; otherwise, returns -1 and sets errno. + */ +PALEXPORT int32_t SystemNative_ReadThreadInfo(int32_t pid, int32_t tid, ThreadInfo* threadInfo); + +/** + * Reads the psinfo_t struct and converts into ProcessInfo. + * + * Returns 0 on success; otherwise, returns -1 and sets errno. */ -PALEXPORT int32_t SystemNative_ReadProcessStatusInfo(pid_t pid, ProcessStatus* processStatus); +PALEXPORT int32_t SystemNative_ReadProcessInfo(int32_t pid, ProcessInfo* processInfo, uint8_t *argBuf, int32_t argBufSize); /** * Reads the number of bytes specified into the provided buffer from the specified, opened file descriptor at specified offset. diff --git a/src/native/libs/System.Native/pal_process.c b/src/native/libs/System.Native/pal_process.c index 129fd612e33168..845c4ed3293622 100644 --- a/src/native/libs/System.Native/pal_process.c +++ b/src/native/libs/System.Native/pal_process.c @@ -553,6 +553,9 @@ static int32_t ConvertRLimitResourcesPalToPlatform(RLimitResources value) #ifdef RLIMIT_RSS case PAL_RLIMIT_RSS: return RLIMIT_RSS; +#elif defined(RLIMIT_VMEM) + case PAL_RLIMIT_RSS: + return RLIMIT_VMEM; #endif #ifdef RLIMIT_MEMLOCK case PAL_RLIMIT_MEMLOCK: From 1d202d4d17dd875e61f3d842ad6df438bad18e9c Mon Sep 17 00:00:00 2001 From: Jaroslav Ruzicka Date: Mon, 19 Jan 2026 14:07:33 +0100 Subject: [PATCH 21/32] fix analyzer warnings --- .../ServiceProviderEngineScopeTests.cs | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderEngineScopeTests.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderEngineScopeTests.cs index d8844ff3c137eb..26bc1fbe4bf9a2 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderEngineScopeTests.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderEngineScopeTests.cs @@ -2,12 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection.Specification.Fakes; using Xunit; -using Xunit.Abstractions; -using Xunit.Sdk; namespace Microsoft.Extensions.DependencyInjection.ServiceLookup { @@ -51,7 +48,6 @@ public void Dispose_ServiceThrows_DisposesAllAndThrows() services.AddKeyedTransient("throws", (_, _) => new TestDisposable(true)); services.AddKeyedTransient("doesnotthrow", (_, _) => new TestDisposable(false)); - var scope = services.BuildServiceProvider().GetRequiredService().CreateScope().ServiceProvider; var disposables = new TestDisposable[] @@ -66,12 +62,11 @@ public void Dispose_ServiceThrows_DisposesAllAndThrows() } [Fact] - public void Dispose_TwoServicesThrows_DisposesAllAndThrowsAgggregatedException() + public void Dispose_TwoServicesThrows_DisposesAllAndThrowsAggregateException() { var services = new ServiceCollection(); services.AddKeyedTransient("throws", (_, _) => new TestDisposable(true)); services.AddKeyedTransient("doesnotthrow", (_, _) => new TestDisposable(false)); - var scope = services.BuildServiceProvider().GetRequiredService().CreateScope().ServiceProvider; @@ -96,7 +91,6 @@ public async Task DisposeAsync_ServiceThrows_DisposesAllAndThrows() services.AddKeyedTransient("throws", (_, _) => new TestDisposable(true)); services.AddKeyedTransient("doesnotthrow", (_, _) => new TestDisposable(false)); - var scope = services.BuildServiceProvider().GetRequiredService().CreateScope().ServiceProvider; var disposables = new TestDisposable[] @@ -105,7 +99,7 @@ public async Task DisposeAsync_ServiceThrows_DisposesAllAndThrows() scope.GetRequiredKeyedService("doesnotthrow") }; - var exception = await Assert.ThrowsAsync(async() => await ((IAsyncDisposable)scope).DisposeAsync()); + var exception = await Assert.ThrowsAsync(async () => await ((IAsyncDisposable)scope).DisposeAsync()); Assert.Equal(TestDisposable.ErrorMessage, exception.Message); Assert.All(disposables, disposable => Assert.True(disposable.IsDisposed)); } @@ -117,7 +111,6 @@ public async Task DisposeAsync_TwoServicesThrows_DisposesAllAndThrowsAggregateEx services.AddKeyedTransient("throws", (_, _) => new TestDisposable(true)); services.AddKeyedTransient("doesnotthrow", (_, _) => new TestDisposable(false)); - var scope = services.BuildServiceProvider().GetRequiredService().CreateScope().ServiceProvider; var disposables = new TestDisposable[] @@ -139,8 +132,8 @@ private class TestDisposable : IDisposable, IAsyncDisposable public const string ErrorMessage = "Dispose failed."; private readonly bool _throwsOnDispose; - public int DisposedCounter { get; private set; } - public bool IsDisposed => DisposedCounter > 0; + + public bool IsDisposed { get; private set; } public TestDisposable(bool throwsOnDispose) { @@ -149,13 +142,12 @@ public TestDisposable(bool throwsOnDispose) public void Dispose() { + IsDisposed = true; + if (_throwsOnDispose) { - DisposedCounter++; throw new InvalidOperationException(ErrorMessage); } - - DisposedCounter++; } public ValueTask DisposeAsync() From c64cd8f381ed1cb6f894d7d13424f6eb84d7226b Mon Sep 17 00:00:00 2001 From: Jaroslav Ruzicka Date: Mon, 19 Jan 2026 14:07:33 +0100 Subject: [PATCH 22/32] fix analyzer warnings --- .../ServiceProviderEngineScopeTests.cs | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderEngineScopeTests.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderEngineScopeTests.cs index d8844ff3c137eb..26bc1fbe4bf9a2 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderEngineScopeTests.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderEngineScopeTests.cs @@ -2,12 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection.Specification.Fakes; using Xunit; -using Xunit.Abstractions; -using Xunit.Sdk; namespace Microsoft.Extensions.DependencyInjection.ServiceLookup { @@ -51,7 +48,6 @@ public void Dispose_ServiceThrows_DisposesAllAndThrows() services.AddKeyedTransient("throws", (_, _) => new TestDisposable(true)); services.AddKeyedTransient("doesnotthrow", (_, _) => new TestDisposable(false)); - var scope = services.BuildServiceProvider().GetRequiredService().CreateScope().ServiceProvider; var disposables = new TestDisposable[] @@ -66,12 +62,11 @@ public void Dispose_ServiceThrows_DisposesAllAndThrows() } [Fact] - public void Dispose_TwoServicesThrows_DisposesAllAndThrowsAgggregatedException() + public void Dispose_TwoServicesThrows_DisposesAllAndThrowsAggregateException() { var services = new ServiceCollection(); services.AddKeyedTransient("throws", (_, _) => new TestDisposable(true)); services.AddKeyedTransient("doesnotthrow", (_, _) => new TestDisposable(false)); - var scope = services.BuildServiceProvider().GetRequiredService().CreateScope().ServiceProvider; @@ -96,7 +91,6 @@ public async Task DisposeAsync_ServiceThrows_DisposesAllAndThrows() services.AddKeyedTransient("throws", (_, _) => new TestDisposable(true)); services.AddKeyedTransient("doesnotthrow", (_, _) => new TestDisposable(false)); - var scope = services.BuildServiceProvider().GetRequiredService().CreateScope().ServiceProvider; var disposables = new TestDisposable[] @@ -105,7 +99,7 @@ public async Task DisposeAsync_ServiceThrows_DisposesAllAndThrows() scope.GetRequiredKeyedService("doesnotthrow") }; - var exception = await Assert.ThrowsAsync(async() => await ((IAsyncDisposable)scope).DisposeAsync()); + var exception = await Assert.ThrowsAsync(async () => await ((IAsyncDisposable)scope).DisposeAsync()); Assert.Equal(TestDisposable.ErrorMessage, exception.Message); Assert.All(disposables, disposable => Assert.True(disposable.IsDisposed)); } @@ -117,7 +111,6 @@ public async Task DisposeAsync_TwoServicesThrows_DisposesAllAndThrowsAggregateEx services.AddKeyedTransient("throws", (_, _) => new TestDisposable(true)); services.AddKeyedTransient("doesnotthrow", (_, _) => new TestDisposable(false)); - var scope = services.BuildServiceProvider().GetRequiredService().CreateScope().ServiceProvider; var disposables = new TestDisposable[] @@ -139,8 +132,8 @@ private class TestDisposable : IDisposable, IAsyncDisposable public const string ErrorMessage = "Dispose failed."; private readonly bool _throwsOnDispose; - public int DisposedCounter { get; private set; } - public bool IsDisposed => DisposedCounter > 0; + + public bool IsDisposed { get; private set; } public TestDisposable(bool throwsOnDispose) { @@ -149,13 +142,12 @@ public TestDisposable(bool throwsOnDispose) public void Dispose() { + IsDisposed = true; + if (_throwsOnDispose) { - DisposedCounter++; throw new InvalidOperationException(ErrorMessage); } - - DisposedCounter++; } public ValueTask DisposeAsync() From 43087795c6fc426f089d8068fcd395ca30037f74 Mon Sep 17 00:00:00 2001 From: Jaroslav Ruzicka Date: Fri, 23 Jan 2026 11:20:18 +0100 Subject: [PATCH 23/32] implement PR comments --- .../src/ServiceLookup/ServiceProviderEngineScope.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs index 96a6b8154f76a5..b828e53a563ed9 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs @@ -120,7 +120,7 @@ public object GetRequiredKeyedService(Type serviceType, object? serviceKey) public void Dispose() { List? toDispose = BeginDispose(); - var exceptions = default(List); + List? exceptions = null; var index = (toDispose?.Count ?? 0) - 1; while (index >= 0) @@ -163,8 +163,8 @@ public void Dispose() public ValueTask DisposeAsync() { List? toDispose = BeginDispose(); - var exceptions = default(List); - var index = toDispose is null ? -1 : toDispose.Count - 1; + List? exceptions = null; + var index = (toDispose?.Count ?? 0) - 1; while (index >= 0) { From c92eac0daf30448bdd734167ba1bf9a96c0e6115 Mon Sep 17 00:00:00 2001 From: Jaroslav Ruzicka Date: Fri, 23 Jan 2026 14:52:35 +0100 Subject: [PATCH 24/32] implement PR comments --- .../ServiceLookup/ServiceProviderEngineScope.cs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs index b828e53a563ed9..d7a7704f3f3285 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs @@ -152,12 +152,7 @@ public void Dispose() return; } - if (exceptions.Count == 1) - { - throw exceptions[0]; - } - - throw new AggregateException(exceptions); + throw exceptions.Count == 1 ? exceptions[0] : new AggregateException(exceptions); } public ValueTask DisposeAsync() @@ -204,12 +199,8 @@ public ValueTask DisposeAsync() return default; } - if (exceptions.Count == 1) - { - return new ValueTask(Task.FromException(exceptions[0])); - } - - return new ValueTask(Task.FromException(new AggregateException(exceptions))); + var exception = exceptions.Count == 1 ? exceptions[0] : new AggregateException(exceptions); + return new ValueTask(Task.FromException(exception)); static async ValueTask Await(int i, ValueTask vt, List toDispose) { From 549f7c729b5b8509009922a264e65f04ecb4a918 Mon Sep 17 00:00:00 2001 From: Jaroslav Ruzicka Date: Wed, 28 Jan 2026 23:26:53 +0100 Subject: [PATCH 25/32] run try-catch inside loops --- .../ServiceProviderEngineScope.cs | 68 +++++++++---------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs index d7a7704f3f3285..ad2441655e3c1c 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs @@ -120,30 +120,29 @@ public object GetRequiredKeyedService(Type serviceType, object? serviceKey) public void Dispose() { List? toDispose = BeginDispose(); - List? exceptions = null; - var index = (toDispose?.Count ?? 0) - 1; + if (toDispose is null) + { + return; + } - while (index >= 0) + List? exceptions = null; + for (var i = toDispose.Count - 1; i >= 0; i--) { try { - for (; index >= 0; index--) + if (toDispose[i] is IDisposable disposable) { - if (toDispose![index] is IDisposable disposable) - { - disposable.Dispose(); - } - else - { - throw new InvalidOperationException(SR.Format(SR.AsyncDisposableServiceDispose, TypeNameHelper.GetTypeDisplayName(toDispose[index]))); - } + disposable.Dispose(); + } + else + { + throw new InvalidOperationException(SR.Format(SR.AsyncDisposableServiceDispose, TypeNameHelper.GetTypeDisplayName(toDispose[i]))); } } - catch (Exception ex) + catch (Exception exception) { exceptions ??= new List(); - exceptions.Add(ex); - index--; + exceptions.Add(exception); } } @@ -158,39 +157,38 @@ public void Dispose() public ValueTask DisposeAsync() { List? toDispose = BeginDispose(); - List? exceptions = null; - var index = (toDispose?.Count ?? 0) - 1; + if (toDispose is null) + { + return default; + } - while (index >= 0) + List? exceptions = null; + for (var i = toDispose.Count - 1; i >= 0; i--) { try { - for (; index >= 0; index--) + object disposable = toDispose[i]; + if (disposable is IAsyncDisposable asyncDisposable) { - object disposable = toDispose![index]; - if (disposable is IAsyncDisposable asyncDisposable) + ValueTask vt = asyncDisposable.DisposeAsync(); + if (!vt.IsCompletedSuccessfully) { - ValueTask vt = asyncDisposable.DisposeAsync(); - if (!vt.IsCompletedSuccessfully) - { - return Await(index, vt, toDispose); - } - - // If its a IValueTaskSource backed ValueTask, - // inform it its result has been read so it can reset - vt.GetAwaiter().GetResult(); - } - else - { - ((IDisposable)disposable).Dispose(); + return Await(i, vt, toDispose); } + + // If its a IValueTaskSource backed ValueTask, + // inform it its result has been read so it can reset + vt.GetAwaiter().GetResult(); + } + else + { + ((IDisposable)disposable).Dispose(); } } catch (Exception ex) { exceptions ??= new List(); exceptions.Add(ex); - index--; } } From f03e2ba9c0d2f777130ca4f0a36bbeb7258280b9 Mon Sep 17 00:00:00 2001 From: Jaroslav Ruzicka Date: Fri, 30 Jan 2026 16:23:05 +0100 Subject: [PATCH 26/32] capture exception dispatch info --- .../ServiceProviderEngineScope.cs | 83 +++++++++---------- 1 file changed, 37 insertions(+), 46 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs index ad2441655e3c1c..fe70dd82def24a 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Net.NetworkInformation; +using System.Runtime.ExceptionServices; using System.Threading.Tasks; using Microsoft.Extensions.Internal; @@ -125,7 +127,7 @@ public void Dispose() return; } - List? exceptions = null; + object? exceptionsCache = null; for (var i = toDispose.Count - 1; i >= 0; i--) { try @@ -141,28 +143,22 @@ public void Dispose() } catch (Exception exception) { - exceptions ??= new List(); - exceptions.Add(exception); + AddExceptionToCache(ref exceptionsCache, exception); } } - if (exceptions is null) - { - return; - } - - throw exceptions.Count == 1 ? exceptions[0] : new AggregateException(exceptions); + PropagateExceptionsFromCache(exceptionsCache); } - public ValueTask DisposeAsync() + public async ValueTask DisposeAsync() { List? toDispose = BeginDispose(); if (toDispose is null) { - return default; + return; } - List? exceptions = null; + object? exceptionsCache = null; for (var i = toDispose.Count - 1; i >= 0; i--) { try @@ -170,56 +166,51 @@ public ValueTask DisposeAsync() object disposable = toDispose[i]; if (disposable is IAsyncDisposable asyncDisposable) { - ValueTask vt = asyncDisposable.DisposeAsync(); - if (!vt.IsCompletedSuccessfully) - { - return Await(i, vt, toDispose); - } - - // If its a IValueTaskSource backed ValueTask, - // inform it its result has been read so it can reset - vt.GetAwaiter().GetResult(); + await asyncDisposable.DisposeAsync().ConfigureAwait(false); } else { ((IDisposable)disposable).Dispose(); } } - catch (Exception ex) + catch (Exception exception) { - exceptions ??= new List(); - exceptions.Add(ex); + AddExceptionToCache(ref exceptionsCache, exception); } } - if (exceptions is null) + PropagateExceptionsFromCache(exceptionsCache); + } + + private static void AddExceptionToCache(ref object? exceptionsCache, Exception exception) + { + if (exceptionsCache is null) { - return default; + exceptionsCache = exception; } + else if (exceptionsCache is ExceptionDispatchInfo exceptionInfo) + { + exceptionsCache = new List { exceptionInfo.SourceException, exception }; + } + else + { + ((List)exceptionsCache).Add(exception); + } + } - var exception = exceptions.Count == 1 ? exceptions[0] : new AggregateException(exceptions); - return new ValueTask(Task.FromException(exception)); - - static async ValueTask Await(int i, ValueTask vt, List toDispose) + private static void PropagateExceptionsFromCache(object? exceptionsCache) + { + if (exceptionsCache is null) { - await vt.ConfigureAwait(false); - // vt is acting on the disposable at index i, - // decrement it and move to the next iteration - i--; + return; + } - for (; i >= 0; i--) - { - object disposable = toDispose[i]; - if (disposable is IAsyncDisposable asyncDisposable) - { - await asyncDisposable.DisposeAsync().ConfigureAwait(false); - } - else - { - ((IDisposable)disposable).Dispose(); - } - } + if (exceptionsCache is ExceptionDispatchInfo exceptionInfo) + { + exceptionInfo.Throw(); } + + throw new AggregateException((List)exceptionsCache); } private List? BeginDispose() From 2b4e1112aac4ebb4e8b895987e7655ef3124e827 Mon Sep 17 00:00:00 2001 From: Jaroslav Ruzicka Date: Fri, 30 Jan 2026 16:40:47 +0100 Subject: [PATCH 27/32] simplify stack traces --- .../ServiceProviderEngineScope.cs | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs index fe70dd82def24a..4deed83d852f58 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs @@ -147,7 +147,15 @@ public void Dispose() } } - PropagateExceptionsFromCache(exceptionsCache); + if (exceptionsCache is not null) + { + if (exceptionsCache is ExceptionDispatchInfo exceptionInfo) + { + exceptionInfo.Throw(); + } + + throw new AggregateException((List)exceptionsCache); + } } public async ValueTask DisposeAsync() @@ -179,14 +187,22 @@ public async ValueTask DisposeAsync() } } - PropagateExceptionsFromCache(exceptionsCache); + if (exceptionsCache is not null) + { + if (exceptionsCache is ExceptionDispatchInfo exceptionInfo) + { + exceptionInfo.Throw(); + } + + throw new AggregateException((List)exceptionsCache); + } } private static void AddExceptionToCache(ref object? exceptionsCache, Exception exception) { if (exceptionsCache is null) { - exceptionsCache = exception; + exceptionsCache = ExceptionDispatchInfo.Capture(exception); } else if (exceptionsCache is ExceptionDispatchInfo exceptionInfo) { @@ -198,21 +214,6 @@ private static void AddExceptionToCache(ref object? exceptionsCache, Exception e } } - private static void PropagateExceptionsFromCache(object? exceptionsCache) - { - if (exceptionsCache is null) - { - return; - } - - if (exceptionsCache is ExceptionDispatchInfo exceptionInfo) - { - exceptionInfo.Throw(); - } - - throw new AggregateException((List)exceptionsCache); - } - private List? BeginDispose() { lock (Sync) From 8682b4a4017c36f2d98f3d9734dc4ae852c82a38 Mon Sep 17 00:00:00 2001 From: Jaroslav Ruzicka Date: Fri, 30 Jan 2026 17:13:34 +0100 Subject: [PATCH 28/32] add tests for true async disposal --- .../ServiceProviderEngineScopeTests.cs | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderEngineScopeTests.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderEngineScopeTests.cs index 26bc1fbe4bf9a2..b5552c23e34a20 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderEngineScopeTests.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderEngineScopeTests.cs @@ -84,12 +84,14 @@ public void Dispose_TwoServicesThrows_DisposesAllAndThrowsAggregateException() Assert.All(disposables, disposable => Assert.True(disposable.IsDisposed)); } - [Fact] - public async Task DisposeAsync_ServiceThrows_DisposesAllAndThrows() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task DisposeAsync_ServiceThrows_DisposesAllAndThrows(bool synchronous) { var services = new ServiceCollection(); - services.AddKeyedTransient("throws", (_, _) => new TestDisposable(true)); - services.AddKeyedTransient("doesnotthrow", (_, _) => new TestDisposable(false)); + services.AddKeyedTransient("throws", (_, _) => new TestDisposable(true, synchronous)); + services.AddKeyedTransient("doesnotthrow", (_, _) => new TestDisposable(false, synchronous)); var scope = services.BuildServiceProvider().GetRequiredService().CreateScope().ServiceProvider; @@ -104,12 +106,14 @@ public async Task DisposeAsync_ServiceThrows_DisposesAllAndThrows() Assert.All(disposables, disposable => Assert.True(disposable.IsDisposed)); } - [Fact] - public async Task DisposeAsync_TwoServicesThrows_DisposesAllAndThrowsAggregateException() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task DisposeAsync_TwoServicesThrows_DisposesAllAndThrowsAggregateException(bool synchronous) { var services = new ServiceCollection(); - services.AddKeyedTransient("throws", (_, _) => new TestDisposable(true)); - services.AddKeyedTransient("doesnotthrow", (_, _) => new TestDisposable(false)); + services.AddKeyedTransient("throws", (_, _) => new TestDisposable(true, synchronous)); + services.AddKeyedTransient("doesnotthrow", (_, _) => new TestDisposable(false, synchronous)); var scope = services.BuildServiceProvider().GetRequiredService().CreateScope().ServiceProvider; @@ -132,12 +136,14 @@ private class TestDisposable : IDisposable, IAsyncDisposable public const string ErrorMessage = "Dispose failed."; private readonly bool _throwsOnDispose; + private readonly bool _synchronous; public bool IsDisposed { get; private set; } - public TestDisposable(bool throwsOnDispose) + public TestDisposable(bool throwsOnDispose = false, bool synchronous = false) { _throwsOnDispose = throwsOnDispose; + _synchronous = synchronous; } public void Dispose() @@ -152,8 +158,19 @@ public void Dispose() public ValueTask DisposeAsync() { - Dispose(); - return default; + if (_synchronous) + { + Dispose(); + return default; + } + + return new ValueTask(DisposeAsyncInternal()); + + async Task DisposeAsyncInternal() + { + await Task.Delay(100); + Dispose(); + } } } } From c29af2409f1ba370f1066550a5194af8c9a9ef62 Mon Sep 17 00:00:00 2001 From: Jaroslav Ruzicka Date: Fri, 30 Jan 2026 17:24:32 +0100 Subject: [PATCH 29/32] remove unused using --- .../src/ServiceLookup/ServiceProviderEngineScope.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs index 4deed83d852f58..e861c20ef64d3b 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Net.NetworkInformation; using System.Runtime.ExceptionServices; using System.Threading.Tasks; using Microsoft.Extensions.Internal; From 606290836e8468b3ff29a3475a2b4e1921b8d1d5 Mon Sep 17 00:00:00 2001 From: Jaroslav Ruzicka Date: Sat, 31 Jan 2026 11:54:56 +0100 Subject: [PATCH 30/32] checkpoint --- .../ServiceProviderEngineScope.cs | 79 +++++++++++++++---- 1 file changed, 63 insertions(+), 16 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs index e861c20ef64d3b..f60c0e2a2783c4 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs @@ -146,23 +146,15 @@ public void Dispose() } } - if (exceptionsCache is not null) - { - if (exceptionsCache is ExceptionDispatchInfo exceptionInfo) - { - exceptionInfo.Throw(); - } - - throw new AggregateException((List)exceptionsCache); - } + CheckExceptionCache(exceptionsCache); } - public async ValueTask DisposeAsync() + public ValueTask DisposeAsync() { List? toDispose = BeginDispose(); if (toDispose is null) { - return; + return default; } object? exceptionsCache = null; @@ -173,7 +165,15 @@ public async ValueTask DisposeAsync() object disposable = toDispose[i]; if (disposable is IAsyncDisposable asyncDisposable) { - await asyncDisposable.DisposeAsync().ConfigureAwait(false); + ValueTask vt = asyncDisposable.DisposeAsync(); + if (!vt.IsCompletedSuccessfully) + { + return Await(i, vt, toDispose, exceptionsCache); + } + + // If its a IValueTaskSource backed ValueTask, + // inform it its result has been read so it can reset + vt.GetAwaiter().GetResult(); } else { @@ -186,15 +186,62 @@ public async ValueTask DisposeAsync() } } - if (exceptionsCache is not null) + CheckExceptionCache(exceptionsCache); + + return default; + + static async ValueTask Await(int i, ValueTask vt, List toDispose, object? exceptionsCache) { - if (exceptionsCache is ExceptionDispatchInfo exceptionInfo) + try + { + await vt.ConfigureAwait(false); + } + catch (Exception exception) + { + AddExceptionToCache(ref exceptionsCache, exception); + } + + // vt is acting on the disposable at index i, + // decrement it and move to the next iteration + i--; + + for (; i >= 0; i--) { - exceptionInfo.Throw(); + try + { + object disposable = toDispose[i]; + if (disposable is IAsyncDisposable asyncDisposable) + { + await asyncDisposable.DisposeAsync().ConfigureAwait(false); + } + else + { + ((IDisposable)disposable).Dispose(); + } + } + catch (Exception exception) + { + AddExceptionToCache(ref exceptionsCache, exception); + } } - throw new AggregateException((List)exceptionsCache); + CheckExceptionCache(exceptionsCache); + } + } + + private static void CheckExceptionCache(object? exceptionsCache) + { + if (exceptionsCache is null) + { + return; } + + if (exceptionsCache is ExceptionDispatchInfo exceptionInfo) + { + exceptionInfo.Throw(); + } + + throw new AggregateException((List)exceptionsCache); } private static void AddExceptionToCache(ref object? exceptionsCache, Exception exception) From a8926be0f003eda4ff7fe90f70a05526522e2af9 Mon Sep 17 00:00:00 2001 From: Jaroslav Ruzicka Date: Sat, 31 Jan 2026 16:49:27 +0100 Subject: [PATCH 31/32] formatting --- .../ServiceProviderEngineScope.cs | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs index f60c0e2a2783c4..edeb6330091ece 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs @@ -229,21 +229,6 @@ static async ValueTask Await(int i, ValueTask vt, List toDispose, object } } - private static void CheckExceptionCache(object? exceptionsCache) - { - if (exceptionsCache is null) - { - return; - } - - if (exceptionsCache is ExceptionDispatchInfo exceptionInfo) - { - exceptionInfo.Throw(); - } - - throw new AggregateException((List)exceptionsCache); - } - private static void AddExceptionToCache(ref object? exceptionsCache, Exception exception) { if (exceptionsCache is null) @@ -260,6 +245,21 @@ private static void AddExceptionToCache(ref object? exceptionsCache, Exception e } } + private static void CheckExceptionCache(object? exceptionsCache) + { + if (exceptionsCache is null) + { + return; + } + + if (exceptionsCache is ExceptionDispatchInfo exceptionInfo) + { + exceptionInfo.Throw(); + } + + throw new AggregateException((List)exceptionsCache); + } + private List? BeginDispose() { lock (Sync) From c95e5e58005ec720eaa76ea6ae02e9ff772017f0 Mon Sep 17 00:00:00 2001 From: Jaroslav Ruzicka <14963300+rosebyte@users.noreply.github.com> Date: Sun, 1 Feb 2026 08:31:54 +0100 Subject: [PATCH 32/32] Update src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderEngineScopeTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../tests/DI.Tests/ServiceProviderEngineScopeTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderEngineScopeTests.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderEngineScopeTests.cs index b5552c23e34a20..ccbedc43f1e16d 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderEngineScopeTests.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderEngineScopeTests.cs @@ -168,7 +168,7 @@ public ValueTask DisposeAsync() async Task DisposeAsyncInternal() { - await Task.Delay(100); + await Task.Yield(); Dispose(); } }