DispatchMessageInspector on CallbackDispatchRuntime breaks all inbound duplex callbacks (IndexOutOfRangeException in ImmutableDispatchRuntime)
- Dominant language
- C#
- Stars
- 1.8k
- Forks
- 576
- Avg merge
- 6d 9h
- Merged PRs (30d)
- 2
Description
## AI note
While I'm filing this issue, AI produced everything below and the repo file. It was found while porting a WCF duplex client to .net 10 from net fx, using a message inspector caused the callbacks to fail. For us it just added some logging, so we can disable it. However, I also wanted to log the issue.
Single .cs repo is attached:
[WcfDuplexCallbackRepro.cs](https://github.com/user-attachments/files/30545436/WcfDuplexCallbackRepro.cs)
## Description
Adding an `IDispatchMessageInspector` to `ClientRuntime.CallbackDispatchRuntime.MessageInspectors` —
the extensibility point for observing inbound duplex callbacks — makes the first callback throw
`IndexOutOfRangeException` internally. Dispatch aborts, the channel closes, the callback contract
method is never invoked, and the outbound call that triggered it fails with:
```
System.ServiceModel.CommunicationException: The server did not provide a meaningful reply; this might
be caused by a contract mismatch, a premature session shutdown or an internal server error.
```
.NET Framework runs the identical client code correctly.
Not a regression: unchanged on `main` since the port, and reproduced identically on
`System.ServiceModel` 8.1.2 and 10.0.652802.
## Root cause
`ImmutableDispatchRuntime` mis-sizes the per-message correlation-state array. `ImmutableClientRuntime`
gets it right; the dispatch counterpart is missing both halves. Verified at [`1ef7b5b`][commit]; all
files under `src/System.ServiceModel.Primitives/src/System/ServiceModel/Dispatcher/`.
| | `ImmutableClientRuntime` (correct) | `ImmutableDispatchRuntime` (broken) |
|---|---|---|
| sizing | [L47][c47] `CorrelationCount = _messageInspectors.Length + behavior.MaxParameterInspectors;` | [L52][d52] `CorrelationCount = dispatch.MaxParameterInspectors;` |
| message-inspector offset | [L50-53][c50] `=> 0` | [L91][d91] `=> 0` |
| parameter-inspector offset | [L55-58][c55] `=> _messageInspectors.Length` | **absent entirely** |
[`MessageRpc`][mr22] allocates the array as
[`EmptyArray.Allocate(operation.Parent.CorrelationCount)`][mr74]. So with N message inspectors
and no parameter inspectors, `CorrelationCount == 0`, and [L108][d108] writes index 0 of a zero-length
array. The [catch at L115-126][d115] rethrows via `ThrowHelperCallback`, aborting dispatch.
Two distinct defects in that one line:
1. **Under-sizing** — hard `IndexOutOfRangeException` when message inspectors exist and parameter
inspectors do not. Breaks callbacks outright.
2. **Aliasing** — with both kinds present the array is sized 1 and both write index 0, so the message
inspector's correlation state is silently replaced before `BeforeSendReply` reads it. No exception.
[`DispatchOperationRuntime` L135][o135]/[L156][o156] index parameter-inspector slots with a bare
`rpc.Correlation[i]`, confirming the missing offset property is never applied.
## Behaviour
Duplex over `NetHttpBinding` + `WebSocketTransportUsage.Always`, callback dispatched from the server:
| callback extensibility | result |
|---|---|
| none | OK |
| `IDispatchMessageInspector` on `CallbackDispatchRuntime.MessageInspectors` | **FAILS** — callback never invoked |
| `IParameterInspector` on `CallbackDispatchRuntime.Operations[i].ParameterInspectors` | OK |
| `IErrorHandler` on `CallbackDispatchRuntime.ChannelDispatcher.ErrorHandlers` | OK |
| message + parameter inspector | delivered, but message-inspector correlation state clobbered (defect 2) |
Plain duplex works, so duplex itself is not at fault. Adding a dummy parameter inspector sizes the
array to 1 and avoids the exception, but both then write index 0 — not a safe workaround.
**This leaves no supported alternative.** `DispatchRuntime.Operations` and
`DispatchOperation.ParameterInspectors` are not in the .NET `System.ServiceModel.Primitives` reference
assembly (they bind on .NET Framework; CS1061 here on both 8.1.2 and 10.0.652802). So
`CallbackDispatchRuntime.MessageInspectors` is the only publicly bindable per-message hook for inbound
duplex callbacks — and it is the broken one.
## Repro
Attached: `WcfDuplexCallbackRepro.cs` — a single file, no project and no `nuget.config`. With the
.NET 10 SDK:
```
dotnet run WcfDuplexCallbackRepro.cs
```
.NET only, no .NET Framework required. CoreWCF hosts the duplex contract in-process,
`System.ServiceModel` is the client, the server invokes a one-way callback from a two-way operation.
Each configuration runs in turn; the process exits non-zero unless the defect reproduces. The
client-side trigger is just:
```csharp
public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{
clientRuntime.CallbackDispatchRuntime.MessageInspectors.Add(new MyInspector());
}
```
Output:
```
[PASS] none callback delivered
[PASS] param-inspector callback delivered
[PASS] error-handler callback delivered
[FAIL] msg-inspector callback NOT delivered (CommunicationException: The server did not provide a meaningful reply...)
[FAIL] msg-inspector+error-handler callback NOT delivered (CommunicationException: The server did not provide a meaningful reply...)
[WARN] msg+param-inspector callback delivered, but message-inspector correlation state was CLOBBERED
```
In the failing modes the inspector reports `AfterReceiveRequest=True BeforeSendReply=False` — it is
entered, and the write of its return value throws. Captured via `IErrorHandler` and
`FirstChanceException`:
```
System.IndexOutOfRangeException: Index was outside the bounds of the array.
at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.AfterReceiveRequestCore(MessageRpc& rpc)
```
`dotnet run -p:SystemServiceModelVersion=8.1.2 WcfDuplexCallbackRepro.cs` gives the same pattern and
the same frame.
## Suggested fix
Mirror `ImmutableClientRuntime`.
```diff
--- a/src/System.ServiceModel.Primitives/src/System/ServiceModel/Dispatcher/ImmutableDispatchRuntime.cs
+++ b/src/System.ServiceModel.Primitives/src/System/ServiceModel/Dispatcher/ImmutableDispatchRuntime.cs
@@ -49,7 +49,7 @@ internal ImmutableDispatchRuntime(DispatchRuntime dispatch)
_sendAsynchronously = dispatch.ChannelDispatcher.SendAsynchronously;
- CorrelationCount = dispatch.MaxParameterInspectors;
+ CorrelationCount = _messageInspectors.Length + dispatch.MaxParameterInspectors;
@@ -89,6 +89,8 @@ internal ImmutableDispatchRuntime(DispatchRuntime dispatch)
internal int MessageInspectorCorrelationOffset => 0;
+
+ internal int ParameterInspectorCorrelationOffset => _messageInspectors.Length;
--- a/src/System.ServiceModel.Primitives/src/System/ServiceModel/Dispatcher/DispatchOperationRuntime.cs
+++ b/src/System.ServiceModel.Primitives/src/System/ServiceModel/Dispatcher/DispatchOperationRuntime.cs
@@ -132,7 +132,7 @@
- rpc.Correlation[i] = inspector.BeforeCall(Name, rpc.InputParameters);
+ rpc.Correlation[Parent.ParameterInspectorCorrelationOffset + i] = inspector.BeforeCall(Name, rpc.InputParameters);
@@ -153,7 +153,7 @@
- inspector.AfterCall(Name, rpc.OutputParameters, rpc.ReturnParameter, rpc.Correlation[i]);
+ inspector.AfterCall(Name, rpc.OutputParameters, rpc.ReturnParameter, rpc.Correlation[Parent.ParameterInspectorCorrelationOffset + i]);
```
## Configuration
.NET 10.0.10 win-x64 / Windows 10.0.26100; `System.ServiceModel.*` 10.0.652802 and 8.1.2; CoreWCF
1.9.1 as the repro host. Not OS- or architecture-specific — the defect is in managed dispatch code.
[commit]: https://github.com/dotnet/wcf/commit/1ef7b5bc6537c22f473f8a24f36a77bf38a5dd9e
[c47]: https://github.com/dotnet/wcf/blob/1ef7b5bc6537c22f473f8a24f36a77bf38a5dd9e/src/System.ServiceModel.Primitives/src/System/ServiceModel/Dispatcher/ImmutableClientRuntime.cs#L47
[c50]: https://github.com/dotnet/wcf/blob/1ef7b5bc6537c22f473f8a24f36a77bf38a5dd9e/src/System.ServiceModel.Primitives/src/System/ServiceModel/Dispatcher/ImmutableClientRuntime.cs#L50-L53
[c55]: https://github.com/dotnet/wcf/blob/1ef7b5bc6537c22f473f8a24f36a77bf38a5dd9e/src/System.ServiceModel.Primitives/src/System/ServiceModel/Dispatcher/ImmutableClientRuntime.cs#L55-L58
[d52]: https://github.com/dotnet/wcf/blob/1ef7b5bc6537c22f473f8a24f36a77bf38a5dd9e/src/System.ServiceModel.Primitives/src/System/ServiceModel/Dispatcher/ImmutableDispatchRuntime.cs#L52
[d91]: https://github.com/dotnet/wcf/blob/1ef7b5bc6537c22f473f8a24f36a77bf38a5dd9e/src/System.ServiceModel.Primitives/src/System/ServiceModel/Dispatcher/ImmutableDispatchRuntime.cs#L91
[d108]: https://github.com/dotnet/wcf/blob/1ef7b5bc6537c22f473f8a24f36a77bf38a5dd9e/src/System.ServiceModel.Primitives/src/System/ServiceModel/Dispatcher/ImmutableDispatchRuntime.cs#L108
[d115]: https://github.com/dotnet/wcf/blob/1ef7b5bc6537c22f473f8a24f36a77bf38a5dd9e/src/System.ServiceModel.Primitives/src/System/ServiceModel/Dispatcher/ImmutableDispatchRuntime.cs#L115-L126
[o135]: https://github.com/dotnet/wcf/blob/1ef7b5bc6537c22f473f8a24f36a77bf38a5dd9e/src/System.ServiceModel.Primitives/src/System/ServiceModel/Dispatcher/DispatchOperationRuntime.cs#L135
[o156]: https://github.com/dotnet/wcf/blob/1ef7b5bc6537c22f473f8a24f36a77bf38a5dd9e/src/System.ServiceModel.Primitives/src/System/ServiceModel/Dispatcher/DispatchOperationRuntime.cs#L156
[mr22]: https://github.com/dotnet/wcf/blob/1ef7b5bc6537c22f473f8a24f36a77bf38a5dd9e/src/System.ServiceModel.Primitives/src/System/ServiceModel/Dispatcher/MessageRpc.cs#L22
[mr74]: https://github.com/dotnet/wcf/blob/1ef7b5bc6537c22f473f8a24f36a77bf38a5dd9e/src/System.ServiceModel.Primitives/src/System/ServiceModel/Dispatcher/MessageRpc.cs#L74
Contributor guide
Assessment
This issue has not been assessed yet.