From a71663a2a2c04c0b6052850e6594b98456bb4bf6 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 20 Aug 2026 09:30:56 -0400 Subject: [PATCH] fix(IPC-39): make the CommandTooLarge pre-check allow for the sequence stamp The pre-check measures the command envelope while Sequence is still 0, and an unset protobuf field costs nothing to encode -- so the measured size is a lower bound on the frame WriteLoopAsync emits after GWC-28 stamps the sequence. Being a lower bound is the right kind of conservative for the knownSize question the comment answers, and the wrong kind for the pre-check itself: it admits an envelope in the last eleven bytes of the range that the writer then rejects with a session-fatal MessageTooLarge. Compare against MaxMessageBytes - MaxSequenceEncodingBytes (1 tag byte + a 10-byte varint, the widest 64-bit field encoding) and say in the comment that the pre-check is a lower bound relying on that allowance. Not reachable under the shipped validator (caller-controlled commands arrive through gRPC capped at least 64 KiB below the pipe max, gateway-authored commands are tiny), so this closes the argument rather than a live defect. The existing CommandTooLarge test still passes unchanged. --- .../Workers/WorkerClient.cs | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerClient.cs b/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerClient.cs index bff47b1..ce83155 100644 --- a/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerClient.cs +++ b/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerClient.cs @@ -237,15 +237,22 @@ public sealed class WorkerClient : IWorkerClient // CalculateSize. WriteLoopAsync stamps envelope.Sequence immediately before the write // (GWC-28), and a non-zero varint field grows the encoding — so the number computed here // is a lower bound on the frame the writer actually emits, never the frame length. Passing - // it as a knownSize would under-length the prefix and desync the worker's framing. The - // pre-check stays a pre-check: it is conservative in the right direction. + // it as a knownSize would under-length the prefix and desync the worker's framing. + // + // IPC-39: being a lower bound is conservative for the knownSize question but the wrong + // direction for this pre-check, which must not admit an envelope the writer will then + // reject with a session-fatal MessageTooLarge. Compare against the frame maximum less the + // MaxSequenceEncodingBytes the stamp can add, so every envelope this check accepts still + // fits once the sequence is written. int envelopeSize = commandEnvelope.CalculateSize(); - if (envelopeSize > _connection.FrameOptions.MaxMessageBytes) + int preCheckLimit = _connection.FrameOptions.MaxMessageBytes - MaxSequenceEncodingBytes; + if (envelopeSize > preCheckLimit) { throw new WorkerClientException( WorkerClientErrorCode.CommandTooLarge, $"Worker command {method} serializes to {envelopeSize} bytes, exceeding the negotiated " - + $"worker-frame maximum of {_connection.FrameOptions.MaxMessageBytes} bytes."); + + $"worker-frame maximum of {_connection.FrameOptions.MaxMessageBytes} bytes less the " + + $"{MaxSequenceEncodingBytes}-byte sequence-stamp allowance."); } await EnqueueAsync(commandEnvelope, cancellationToken).ConfigureAwait(false); @@ -455,6 +462,18 @@ public sealed class WorkerClient : IWorkerClient _stopCts.Dispose(); } + /// + /// Worst-case number of bytes 's sequence stamp adds to an + /// envelope that was sized before it: one tag byte plus a ten-byte varint, the maximum + /// encoding of a 64-bit protobuf field. The CommandTooLarge pre-check subtracts this + /// from the negotiated frame maximum (IPC-39) because it measures the envelope while + /// Sequence is still 0 — an unset field costs nothing to encode, so the measured size + /// is a lower bound on the frame the writer emits, and without the allowance an envelope in + /// the last eleven bytes of the range would pass the pre-check and then fault the session on + /// the write. + /// + private const int MaxSequenceEncodingBytes = 11; + /// Manages writing envelopes to the worker pipe. private async Task WriteLoopAsync() {