This commit is contained in:
2026-08-21 11:17:55 +03:00
parent f14dc633ae
commit 61ec566f63
11 changed files with 403 additions and 28 deletions
+1
View File
@@ -649,6 +649,7 @@ public static class Codec
public static byte[] Compose(object valueOrSource, Warehouse warehouse, EpConnection connection)
{
var tdu = ComposeInternal(valueOrSource, warehouse, connection);
ParserGuard.EnsureRemotePacketSize(connection, (ulong)tdu.Composed.LongLength);
return tdu.Composed;
}
+32 -2
View File
@@ -363,6 +363,10 @@ public static class DataSerializer
public static Tdu StringComposer(object value, Warehouse warehouse, EpConnection connection)
{
var b = Encoding.UTF8.GetBytes((string)value);
ParserGuard.EnsureRemoteAllocation(
connection,
ParserGuard.MultiplySaturated((ulong)b.LongLength, 2),
"string");
return new Tdu(TduIdentifier.String, b, (uint)b.Length, null, null);
}
@@ -370,6 +374,10 @@ public static class DataSerializer
public static Tdu ResourceLinkComposer(object value, Warehouse warehouse, EpConnection connection)
{
var b = Encoding.UTF8.GetBytes((ResourceLink)value);
ParserGuard.EnsureRemoteAllocation(
connection,
ParserGuard.MultiplySaturated((ulong)b.LongLength, 2),
"resource link");
return new Tdu(TduIdentifier.ResourceLink, b, (uint)b.Length, null, null);
}
@@ -446,12 +454,14 @@ public static class DataSerializer
public static Tdu RawDataComposerFromArray(object value, Warehouse warehouse, EpConnection connection)
{
var b = (byte[])value;
ParserGuard.EnsureRemoteAllocation(connection, (ulong)b.LongLength, "raw data");
return new Tdu(TduIdentifier.RawData, b, (uint)b.Length, null, null);
}
public static Tdu RawDataComposerFromList(dynamic value, Warehouse warehouse, EpConnection connection)
{
var b = value as List<byte>;
ParserGuard.EnsureRemoteAllocation(connection, (ulong)b.Count, "raw data");
return new Tdu(TduIdentifier.RawData, b.ToArray(), (uint)b.Count, null, null);
}
@@ -516,6 +526,9 @@ public static class DataSerializer
if (value == null)
return null;
if (value is ICollection collection)
ParserGuard.EnsureRemoteCollectionCount(connection, collection.Count, GetTypedArrayElementSize(tru));
if (tru.Identifier == TruIdentifier.Int32)
{
composed = GroupInt32Codec.Encode((IList<int>)value);
@@ -748,12 +761,18 @@ public static class DataSerializer
// Pre-size the buffer from the element count (when known) to avoid repeated
// List<byte> reallocations as items are appended. 4 bytes/element is a rough hint.
var rt = new List<byte>(value is ICollection collection ? collection.Count * 4 : 16);
var knownCount = value is ICollection collection ? collection.Count : -1;
if (knownCount >= 0)
ParserGuard.EnsureRemoteCollectionCount(connection, knownCount, IntPtr.Size);
var rt = new List<byte>(knownCount >= 0 ? knownCount * 4 : 16);
Tdu? previous = null;
var count = 0;
foreach (var i in value)
{
if (knownCount < 0)
ParserGuard.EnsureRemoteCollectionCount(connection, ++count, IntPtr.Size);
var tdu = Codec.ComposeInternal(i, warehouse, connection);
if (previous != null && tdu.MatchType(previous.Value))
{
@@ -862,13 +881,24 @@ public static class DataSerializer
var rt = new List<byte>();
var map = (IMap)value;
var serialized = map.Serialize();
ParserGuard.EnsureRemoteCollectionCount(connection, serialized.Length, IntPtr.Size);
foreach (var el in map.Serialize())
foreach (var el in serialized)
rt.AddRange(Codec.Compose(el, warehouse, connection));
return new Tdu(TduIdentifier.Map, rt.ToArray(), (uint)rt.Count, null, null);
}
static int GetTypedArrayElementSize(Tru tru)
=> tru.Identifier switch
{
TruIdentifier.Int16 or TruIdentifier.UInt16 => 2,
TruIdentifier.Int32 or TruIdentifier.UInt32 => 4,
TruIdentifier.Int64 or TruIdentifier.UInt64 => 8,
_ => IntPtr.Size
};
/// <summary>
/// Composes an indexed CLR structure using the compatible Map&lt;byte, object&gt; wire shape.
/// </summary>
+70
View File
@@ -15,6 +15,16 @@ public sealed class ParserLimitException : Exception
}
}
/// <summary>
/// Raised before transmission when a composed value exceeds a budget advertised by the peer.
/// </summary>
public sealed class RemoteParserLimitException : Exception
{
public RemoteParserLimitException(string message) : base(message)
{
}
}
internal static class ParserGuard
{
internal static Warehouse? GetWarehouse(EpConnection? connection)
@@ -70,4 +80,64 @@ internal static class ParserGuard
internal static ulong MultiplySaturated(ulong value, ulong multiplier)
=> value > ulong.MaxValue / multiplier ? ulong.MaxValue : value * multiplier;
internal static void EnsureRemotePacketSize(EpConnection? connection, ulong size)
{
var limit = connection?.Session?.RemoteMaximumPacketSize ?? 0;
if (limit > 0 && size > limit)
throw new RemoteParserLimitException(
$"Composed packet payload of {size} bytes exceeds the peer's advertised {limit}-byte limit.");
}
internal static void EnsureRemoteAllocation(
EpConnection? connection,
ulong size,
string kind)
{
var limit = connection?.Session?.RemoteMaximumAllocationSize ?? 0;
if (limit > 0 && size > limit)
throw new RemoteParserLimitException(
$"Composed {kind} would allocate {size} bytes at the peer, exceeding its advertised {limit}-byte limit.");
}
internal static void EnsureRemoteCollectionCount(
EpConnection? connection,
int count,
int estimatedBytesPerItem = 0)
{
var limit = connection?.Session?.RemoteMaximumCollectionItems ?? 0;
if (limit > 0 && count > limit)
throw new RemoteParserLimitException(
$"Composed collection has {count} items, exceeding the peer's advertised {limit}-item limit.");
if (estimatedBytesPerItem > 0)
EnsureRemoteAllocation(
connection,
MultiplySaturated((ulong)count, (ulong)estimatedBytesPerItem),
"collection");
}
internal static void EnsureRemoteTypeMetadataDepth(EpConnection? connection, Tru? tru)
{
var limit = connection?.Session?.RemoteMaximumTypeMetadataDepth ?? 0;
if (limit <= 0 || tru == null)
return;
var depth = GetTypeMetadataDepth(tru);
if (depth > limit)
throw new RemoteParserLimitException(
$"Composed TRU type metadata depth of {depth} exceeds the peer's advertised limit of {limit}.");
}
static int GetTypeMetadataDepth(Tru tru)
{
if (tru is not TruComposite composite || composite.SubTypes == null || composite.SubTypes.Length == 0)
return 1;
var maximumChildDepth = 0;
foreach (var child in composite.SubTypes)
maximumChildDepth = Math.Max(maximumChildDepth, GetTypeMetadataDepth(child));
return 1 + maximumChildDepth;
}
}
+1
View File
@@ -176,6 +176,7 @@ public struct Tdu
if (metadata == null)
throw new Exception("Metadata must be provided for types.");
ParserGuard.EnsureRemoteTypeMetadataDepth(connection, metadata);
var metadataData = metadata.Compose(connection);
@@ -23,6 +23,11 @@ namespace Esiur.Net.Packets
AuthenticationProtocol,
AuthenticationData,
ErrorMessage,
CipherNonce
CipherNonce,
MaximumPacketSize,
MaximumAllocationSize,
MaximumCollectionItems,
MaximumTypeMetadataDepth,
MaximumEncryptedRecordSize
}
}
+30
View File
@@ -369,17 +369,25 @@ public partial class EpConnection : NetworkConnection, IStore
var provider = _session.EncryptionProvider
?? throw new InvalidOperationException("Session encryption is active without a provider.");
var maximumRecordSize = ParsingWarehouse.Configuration.Encryption.MaximumRecordSize;
var remoteMaximumRecordSize = _session?.RemoteMaximumEncryptedRecordSize ?? 0;
if (maximumRecordSize > 0
&& (ulong)plaintext.LongLength + provider.MaximumRecordOverhead > maximumRecordSize)
throw new ParserLimitException(
$"Encrypted record would exceed the {maximumRecordSize}-byte limit.");
if (remoteMaximumRecordSize > 0
&& (ulong)plaintext.LongLength + provider.MaximumRecordOverhead > remoteMaximumRecordSize)
throw new RemoteParserLimitException(
$"Encrypted record would exceed the peer's advertised {remoteMaximumRecordSize}-byte limit.");
var protectedPayload = cipher.Encrypt(plaintext);
if (maximumRecordSize > 0 && protectedPayload.Length > maximumRecordSize)
throw new InvalidOperationException(
$"Encryption provider `{provider.DefaultName}` exceeded its declared record overhead.");
if (remoteMaximumRecordSize > 0 && protectedPayload.Length > remoteMaximumRecordSize)
throw new RemoteParserLimitException(
$"Encrypted record of {protectedPayload.Length} bytes exceeds the peer's advertised {remoteMaximumRecordSize}-byte limit.");
if (protectedPayload.Length > int.MaxValue - EncryptedRecordHeaderSize)
throw new ParserLimitException("Encrypted record exceeds the runtime allocation limit.");
@@ -454,6 +462,7 @@ public partial class EpConnection : NetworkConnection, IStore
}
}
PopulateLocalLimitHeaders();
var headers = _session.LocalHeaders.Copy();
// Anonymous sessions still exchange typed records. They therefore
@@ -491,6 +500,16 @@ public partial class EpConnection : NetworkConnection, IStore
| ((byte)_session.EncryptionMode & 0x3)), headers);
}
void PopulateLocalLimitHeaders()
{
var configuration = ParsingWarehouse.Configuration;
_session.LocalHeaders.MaximumPacketSize = configuration.Parser.MaximumPacketSize;
_session.LocalHeaders.MaximumAllocationSize = configuration.Parser.MaximumAllocationSize;
_session.LocalHeaders.MaximumCollectionItems = configuration.Parser.MaximumCollectionItems;
_session.LocalHeaders.MaximumTypeMetadataDepth = configuration.Parser.MaximumTypeMetadataDepth;
_session.LocalHeaders.MaximumEncryptedRecordSize = configuration.Encryption.MaximumRecordSize;
}
void PrepareEncryptionOffer()
{
if (_session.AuthenticationMode == AuthenticationMode.None)
@@ -1653,6 +1672,7 @@ public partial class EpConnection : NetworkConnection, IStore
? $"anonymous:{RemoteEndPoint?.Address}"
: remoteHeaders.Domain;
_session.AuthenticationMode = _authPacket.AuthMode;
PopulateLocalLimitHeaders();
var localHeaders = _session.LocalHeaders.Copy();
if (!NegotiateEncryptionAsResponder(localHeaders))
@@ -1787,6 +1807,16 @@ public partial class EpConnection : NetworkConnection, IStore
if (_session.AuthenticationMode == AuthenticationMode.None
&& _authPacket.Method == EpAuthPacketMethod.SessionEstablished)
{
var remoteHeaders = new SessionHeaders();
if (_authPacket.Tdu != null)
{
remoteHeaders = Codec.ParseIndexedType<SessionHeaders>(
_authPacket.Tdu.Value,
ParsingWarehouse);
remoteHeaders.AuthenticationData = null;
}
_session.RemoteHeaders = remoteHeaders;
_session.Authenticated = true;
_session.LocalIdentity = null;
_session.RemoteIdentity = null;
+118 -25
View File
@@ -329,7 +329,18 @@ partial class EpConnection
//callbackCounter++; // avoid thread racing
_requests.Add(c, reply);
SendRequestPacket(action, c, args);
try
{
SendRequestPacket(action, c, args);
}
catch (RemoteParserLimitException ex)
{
_requests.Take(c);
reply.TriggerError(new AsyncException(
ErrorType.Management,
(ushort)ExceptionCode.ParserLimitExceeded,
ex.Message));
}
return reply;
}
@@ -372,7 +383,18 @@ partial class EpConnection
() => SendRequest(EpPacketRequest.ResumeExecution, callbackId));
_requests.Add(callbackId, reply);
SendRequestPacket(action, callbackId, args);
try
{
SendRequestPacket(action, callbackId, args);
}
catch (RemoteParserLimitException ex)
{
_requests.Take(callbackId);
reply.TriggerError(new AsyncException(
ErrorType.Management,
(ushort)ExceptionCode.ParserLimitExceeded,
ex.Message));
}
return reply;
}
@@ -387,7 +409,18 @@ partial class EpConnection
() => SendRequest(EpPacketRequest.ResumeExecution, callbackId));
_requests.Add(callbackId, reply);
SendRequestPacket(action, callbackId, args);
try
{
SendRequestPacket(action, callbackId, args);
}
catch (RemoteParserLimitException ex)
{
_requests.Take(callbackId);
reply.TriggerError(new AsyncException(
ErrorType.Management,
(ushort)ExceptionCode.ParserLimitExceeded,
ex.Message));
}
return reply;
}
@@ -502,28 +535,42 @@ partial class EpConnection
if (Instance == null)
return;
if (args.Length == 0)
try
{
var bl = new BinaryList();
bl.AddUInt8((byte)(0x80 | (byte)action))
.AddUInt32(callbackId);
Send(bl.ToArray());
if (args.Length == 0)
{
var bl = new BinaryList();
bl.AddUInt8((byte)(0x80 | (byte)action))
.AddUInt32(callbackId);
Send(bl.ToArray());
}
else if (args.Length == 1)
{
var bl = new BinaryList();
bl.AddUInt8((byte)(0xA0 | (byte)action))
.AddUInt32(callbackId)
.AddUInt8Array(Codec.Compose(args[0], this.Instance?.Warehouse ?? _serverWarehouse, this));
Send(bl.ToArray());
}
else
{
var bl = new BinaryList();
bl.AddUInt8((byte)(0xA0 | (byte)action))
.AddUInt32(callbackId)
.AddUInt8Array(Codec.Compose(args, this.Instance?.Warehouse ?? _serverWarehouse, this));
Send(bl.ToArray());
}
}
if (args.Length == 1)
catch (RemoteParserLimitException ex) when (
action != EpPacketReply.PermissionError
&& action != EpPacketReply.ExecutionError
&& action != EpPacketReply.Warning)
{
var bl = new BinaryList();
bl.AddUInt8((byte)(0xA0 | (byte)action))
.AddUInt32(callbackId)
.AddUInt8Array(Codec.Compose(args[0], this.Instance?.Warehouse ?? _serverWarehouse, this));
Send(bl.ToArray());
}
else
{
var bl = new BinaryList();
bl.AddUInt8((byte)(0xA0 | (byte)action))
.AddUInt32(callbackId)
.AddUInt8Array(Codec.Compose(args, this.Instance?.Warehouse ?? _serverWarehouse, this));
Send(bl.ToArray());
SendError(
ErrorType.Exception,
callbackId,
(ushort)ExceptionCode.ParserLimitExceeded,
ex.Message);
}
}
@@ -677,7 +724,19 @@ partial class EpConnection
return;
}
var pr = Codec.Parse(tdu.Value, this, null);
object pr;
try
{
pr = Codec.Parse(tdu.Value, this, null);
}
catch (ParserLimitException ex)
{
req.TriggerError(new AsyncException(
ErrorType.Management,
(ushort)ExceptionCode.ParserLimitExceeded,
ex.Message));
return;
}
if (pr is AsyncReply asyncReply)
{
@@ -724,7 +783,20 @@ partial class EpConnection
return;
}
var value = Codec.Parse(tdu, this, null);
object value;
try
{
value = Codec.Parse(tdu, this, null);
}
catch (ParserLimitException ex)
{
_requests.Take(callbackId);
req.TriggerError(new AsyncException(
ErrorType.Management,
(ushort)ExceptionCode.ParserLimitExceeded,
ex.Message));
return;
}
if (value is AsyncReply reply)
{
@@ -819,7 +891,20 @@ partial class EpConnection
if (req == null)
return;
var value = Codec.Parse(tdu, this, null);
object value;
try
{
value = Codec.Parse(tdu, this, null);
}
catch (ParserLimitException ex)
{
_requests.Take(callbackId);
req.TriggerError(new AsyncException(
ErrorType.Management,
(ushort)ExceptionCode.ParserLimitExceeded,
ex.Message));
return;
}
if (value is AsyncReply asyncReply)
{
@@ -3993,6 +4078,14 @@ partial class EpConnection
private void Instance_PropertyModified(PropertyModificationInfo info)
{
// Attachment permission does not imply property-read permission. A
// method-only resource can be attached so its exported functions are
// callable while all of its properties remain private. Re-evaluate the
// property operation before broadcasting each modification, matching
// the permission checks already applied to event notifications.
if (!IsOperationAllowed(info.Resource, info.PropertyDef, ActionType.GetProperty))
return;
SendNotification(EpPacketNotification.PropertyModified,
info.Resource.Instance.Id,
info.Cursor.Generation.ToByteArray(),
@@ -100,6 +100,21 @@ public sealed class SessionHeaders : IndexedStructure
[Index((int)EpAuthPacketHeader.CipherNonce)]
public byte[]? CipherNonce { get; set; }
[Index((int)EpAuthPacketHeader.MaximumPacketSize)]
public uint? MaximumPacketSize { get; set; }
[Index((int)EpAuthPacketHeader.MaximumAllocationSize)]
public uint? MaximumAllocationSize { get; set; }
[Index((int)EpAuthPacketHeader.MaximumCollectionItems)]
public int? MaximumCollectionItems { get; set; }
[Index((int)EpAuthPacketHeader.MaximumTypeMetadataDepth)]
public int? MaximumTypeMetadataDepth { get; set; }
[Index((int)EpAuthPacketHeader.MaximumEncryptedRecordSize)]
public uint? MaximumEncryptedRecordSize { get; set; }
internal SessionHeaders Copy() => (SessionHeaders)MemberwiseClone();
}
@@ -121,6 +136,12 @@ public class Session
public SessionHeaders LocalHeaders { get; set; } = new SessionHeaders();
public SessionHeaders RemoteHeaders { get; set; } = new SessionHeaders();
public uint RemoteMaximumPacketSize => RemoteHeaders?.MaximumPacketSize ?? 0;
public uint RemoteMaximumAllocationSize => RemoteHeaders?.MaximumAllocationSize ?? 0;
public int RemoteMaximumCollectionItems => RemoteHeaders?.MaximumCollectionItems ?? 0;
public int RemoteMaximumTypeMetadataDepth => RemoteHeaders?.MaximumTypeMetadataDepth ?? 0;
public uint RemoteMaximumEncryptedRecordSize => RemoteHeaders?.MaximumEncryptedRecordSize ?? 0;
//public AuthenticationMethod AuthenticationMethod { get; set; }
//public AuthenticationMethod RemoteMethod { get; set; }