Repository navigation
Fix mono handling of SwiftError - #131634
Conversation
|
/azp run runtime-extra-platforms |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
|
Azure Pipelines: Successfully started running 3 pipeline(s). 13 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
|
Tagging subscribers to this area: @steveisok, @vitek-karas |
There was a problem hiding this comment.
Pull request overview
This PR expands the existing Swift interop test asset to add additional ABI/calling-convention stress cases around SwiftError placement, and wires them up from C# via Swift-callconv P/Invokes with new xUnit tests to exercise those scenarios on Apple platforms.
Changes:
- Add a new Swift exported throwing function with argument shaping intended to place
SwiftErrorin the first stack slot scenario. - Add a new Swift exported throwing function that takes multiple 2-word struct-like arguments (Swift
UnsafeBufferPointer) plus a 1-word scalar argument to stress mixed register/stack argument passing. - Add corresponding C# interop declarations and new xUnit tests validating both “no throw” behavior and ABI correctness via a shared hash computation.
Show a summary per file
| File | Description |
|---|---|
| src/tests/Interop/Swift/SwiftErrorHandling/SwiftErrorHandling.swift | Adds new exported Swift entry points and hashing helpers to exercise ABI argument placement while still returning a deterministic value. |
| src/tests/Interop/Swift/SwiftErrorHandling/SwiftErrorHandling.cs | Adds marshaling types, new Swift-callconv P/Invokes, and new xUnit tests to validate the ABI scenarios end-to-end. |
Copilot's findings
- Files reviewed: 2/2 changed files
- Comments generated: 2
|
/azp run runtime-extra-platforms |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
There was a problem hiding this comment.
Copilot's findings
Suppressed comments (2)
src/tests/Interop/Swift/SwiftErrorHandling/SwiftErrorHandling.cs:173
- This new test only covers the non-throwing path. To better stress/validate
SwiftErrorplacement and propagation, it should also exercise the throwing path (matching the existing pattern of having both thrown and not-thrown coverage for the other entrypoints). Converting this to a[Theory]overwillThrowkeeps the file compact while covering both behaviors.
[Fact]
public static unsafe void TestSwiftErrorInFirstStackSlotNotThrown()
{
SwiftError error = new SwiftError();
int i = 0;
int result = (int)conditionallyThrowErrorInFirstStackSlot(0, i + 1, i + 2, i + 3, i + 4, i + 5, i + 6, i + 7, ref error);
Assert.True(error.Value == null, "No Swift error was expected to be thrown.");
Assert.True(result == 42, "The result from Swift does not match the expected value.");
}
src/tests/Interop/Swift/SwiftErrorHandling/SwiftErrorHandling.cs:178
BufferPointerinstances are created withcountvalues up to 66, but the backingstackallocbuffer is only 6 bytes. Even though the current Swift implementation only hashes the pointer value and count,UnsafeBufferPointersemantically represents a range ofcountelements starting atbaseAddress, so passing an out-of-bounds range risks UB/traps if the Swift side changes (or if the optimizer/runtime assumes the range is valid). Allocate enough bytes so all (baseAddress,count) pairs describe valid ranges.
byte* values = stackalloc byte[6];
- Files reviewed: 2/2 changed files
- Comments generated: 0 new
There was a problem hiding this comment.
Copilot's findings
Suppressed comments (3)
src/tests/Interop/Swift/SwiftErrorHandling/SwiftErrorHandling.cs:177
- This new scenario is only validated for the non-throwing path. Since the purpose of the test is to stress SwiftError handling/calling convention edge cases, it should also cover the throwing path (non-null SwiftError + message retrieval) to ensure the error propagation logic is exercised.
[Fact]
public static unsafe void TestSwiftErrorInFirstStackSlotNotThrown()
{
SwiftError error = new SwiftError();
int i = 0;
int result = (int)conditionallyThrowErrorInFirstStackSlot(0, i + 1, i + 2, i + 3, i + 4, i + 5, i + 6, i + 7, ref error);
Assert.True(error.Value == null, "No Swift error was expected to be thrown.");
Assert.True(result == 42, "The result from Swift does not match the expected value.");
}
src/tests/Interop/Swift/SwiftErrorHandling/SwiftErrorHandling.cs:210
- The test constructs Swift-style buffer pointers whose
Countvalues exceed the actualstackalloced buffer size (e.g.values + 5with count 66). Even though the current Swift implementation doesn’t dereference the buffers, this violates the contract ofUnsafeBufferPointerand can become undefined behavior if the Swift side changes (or if the optimizer introduces unexpected accesses). Allocate enough stack space to cover the largest(base + count)range used by the test data.
byte* values = stackalloc byte[6];
src/mono/mono/mini/mini-amd64.c:1134
- The PR description frames this as adding stress tests to reproduce Mono Swift calling-convention issues, but this diff also changes Mono’s argument classification/stack allocation behavior for
SwiftErroron amd64 (and similarly on arm64). If the intent is tests-only, these runtime changes should be moved to a follow-up PR; otherwise, please update the title/description to reflect that this PR includes a functional fix as well.
if (sig->pinvoke) {
ainfo->reg = GINT32_TO_UINT8 (AMD64_R12);
ainfo->swift_error_in_reg = TRUE;
} else {
add_general (&gr, &stack_size, ainfo);
ainfo->swift_error_in_reg = ainfo->storage == ArgInIReg;
}
ainfo->storage = ArgSwiftError;
- Files reviewed: 6/6 changed files
- Comments generated: 0 new
|
Azure Pipelines: Successfully started running 4 pipeline(s). 12 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
|
@BrzVlad @matouskozak can one you please review? |
matouskozak
left a comment
There was a problem hiding this comment.
Overall looking good to me, just one question around the new ArgInfo field.
I've run it locally on arm64 macOS and without this change I'm getting NullReferenceException and passes with this change.
fyi: @dalexsoto since you're now working on this domain
There appear to be two problems with the Swift calling convention implementation in Mono for how it handles
SwiftError.On x64, structs can spill to the stack while a later scalar argument still has an available register. Consequently,
SwiftErrorcan be register passed while carrying a nonzero accumulated stack offset. Mono mistakes that offset for a stack location and reads an invalid pointer.On ARM64, the first argument placed on the stack naturally has offset zero. Mono mistakes that stack location for a register passed argument and uses the wrong value.
offset == 0is ambiguous: it can mean either "passed in a register" or "passed in the first stack slot." Right now it doesn't look like it can tell the difference.The fix records whether
SwiftErrorwas originally passed in a register before replacing its argument classification withArgSwiftError, rather than inferring its location from the stack offset. On x64, stack-passedSwiftErrorarguments also disable frame-pointer omission so their incoming stack slots remain addressable.The tests that have been added would fail without the fixes, and I confirmed this locally on an x64 macOS 26 machine.
This was originally found in #131630, where changes at the p/invoke boundary caused the
SwiftErrorto be identified as register passed but carried a nonzero accumulated stack offset because preceding two word buffer structs had spilled.This is a follow up to #103043, where it appears the fix was incomplete.