From c1c06a6fa242bba9aff0b01c6ced31bd3afb7e97 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Thu, 14 May 2026 14:39:43 -0600 Subject: [PATCH 01/17] Fix WriteRawX methods to advance by written length Before this change, the length of the source span would dictate how large the destination span became and how far we advanced the writer, without any regard to how many bytes would actually be copied into that buffer. --- src/MessagePack/Internal/UnsafeMemory.Low.cs | 56 +++-- src/MessagePack/Internal/UnsafeMemory.cs | 208 +++++++++---------- src/MessagePack/Internal/UnsafeMemory.tt | 8 +- tests/MessagePack.Tests/UnsafeMemoryTest.cs | 35 +++- 4 files changed, 173 insertions(+), 134 deletions(-) diff --git a/src/MessagePack/Internal/UnsafeMemory.Low.cs b/src/MessagePack/Internal/UnsafeMemory.Low.cs index 00683ea89..08224a7a5 100644 --- a/src/MessagePack/Internal/UnsafeMemory.Low.cs +++ b/src/MessagePack/Internal/UnsafeMemory.Low.cs @@ -15,12 +15,20 @@ public static class UnsafeMemory public static readonly bool Is32Bit = IntPtr.Size == 4; } + /// + /// Highly tuned method for writing raw bytes to a . + /// + /// + /// The methods on this class are not safe, in that they use pointer arithmetic + /// and assume that the caller has provided a with a length + /// of at least the number of bytes being written. The caller must ensure that this is the case. + /// public static partial class UnsafeMemory32 { [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw1(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(1); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -28,13 +36,13 @@ public static unsafe void WriteRaw1(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(2); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -42,13 +50,13 @@ public static unsafe void WriteRaw2(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(3); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -57,16 +65,24 @@ public static unsafe void WriteRaw3(ref MessagePackWriter writer, ReadOnlySpan + /// Highly tuned method for writing raw bytes to a . + /// + /// + /// The methods on this class are not safe, in that they use pointer arithmetic + /// and assume that the caller has provided a with a length + /// of at least the number of bytes being written. The caller must ensure that this is the case. + /// public static partial class UnsafeMemory64 { [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw1(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(1); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -74,13 +90,13 @@ public static unsafe void WriteRaw1(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(2); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -88,13 +104,13 @@ public static unsafe void WriteRaw2(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(3); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -103,13 +119,13 @@ public static unsafe void WriteRaw3(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(4); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -117,13 +133,13 @@ public static unsafe void WriteRaw4(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(5); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -132,13 +148,13 @@ public static unsafe void WriteRaw5(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(6); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -147,13 +163,13 @@ public static unsafe void WriteRaw6(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(7); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -162,7 +178,7 @@ public static unsafe void WriteRaw7(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(4); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -26,13 +26,13 @@ public static unsafe void WriteRaw4(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(5); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -41,13 +41,13 @@ public static unsafe void WriteRaw5(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(6); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -56,13 +56,13 @@ public static unsafe void WriteRaw6(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(7); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -71,13 +71,13 @@ public static unsafe void WriteRaw7(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(8); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -86,13 +86,13 @@ public static unsafe void WriteRaw8(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(9); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -102,13 +102,13 @@ public static unsafe void WriteRaw9(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(10); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -118,13 +118,13 @@ public static unsafe void WriteRaw10(ref MessagePackWriter writer, ReadOnlySpan< *(int*)(pDst + 6) = *(int*)(pSrc + 6); } - writer.Advance(src.Length); + writer.Advance(10); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw11(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(11); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -134,13 +134,13 @@ public static unsafe void WriteRaw11(ref MessagePackWriter writer, ReadOnlySpan< *(int*)(pDst + 7) = *(int*)(pSrc + 7); } - writer.Advance(src.Length); + writer.Advance(11); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw12(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(12); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -150,13 +150,13 @@ public static unsafe void WriteRaw12(ref MessagePackWriter writer, ReadOnlySpan< *(int*)(pDst + 8) = *(int*)(pSrc + 8); } - writer.Advance(src.Length); + writer.Advance(12); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw13(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(13); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -167,13 +167,13 @@ public static unsafe void WriteRaw13(ref MessagePackWriter writer, ReadOnlySpan< *(int*)(pDst + 9) = *(int*)(pSrc + 9); } - writer.Advance(src.Length); + writer.Advance(13); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw14(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(14); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -184,13 +184,13 @@ public static unsafe void WriteRaw14(ref MessagePackWriter writer, ReadOnlySpan< *(int*)(pDst + 10) = *(int*)(pSrc + 10); } - writer.Advance(src.Length); + writer.Advance(14); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw15(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(15); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -201,13 +201,13 @@ public static unsafe void WriteRaw15(ref MessagePackWriter writer, ReadOnlySpan< *(int*)(pDst + 11) = *(int*)(pSrc + 11); } - writer.Advance(src.Length); + writer.Advance(15); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw16(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(16); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -218,13 +218,13 @@ public static unsafe void WriteRaw16(ref MessagePackWriter writer, ReadOnlySpan< *(int*)(pDst + 12) = *(int*)(pSrc + 12); } - writer.Advance(src.Length); + writer.Advance(16); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw17(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(17); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -236,13 +236,13 @@ public static unsafe void WriteRaw17(ref MessagePackWriter writer, ReadOnlySpan< *(int*)(pDst + 13) = *(int*)(pSrc + 13); } - writer.Advance(src.Length); + writer.Advance(17); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw18(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(18); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -254,13 +254,13 @@ public static unsafe void WriteRaw18(ref MessagePackWriter writer, ReadOnlySpan< *(int*)(pDst + 14) = *(int*)(pSrc + 14); } - writer.Advance(src.Length); + writer.Advance(18); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw19(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(19); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -272,13 +272,13 @@ public static unsafe void WriteRaw19(ref MessagePackWriter writer, ReadOnlySpan< *(int*)(pDst + 15) = *(int*)(pSrc + 15); } - writer.Advance(src.Length); + writer.Advance(19); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw20(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(20); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -290,13 +290,13 @@ public static unsafe void WriteRaw20(ref MessagePackWriter writer, ReadOnlySpan< *(int*)(pDst + 16) = *(int*)(pSrc + 16); } - writer.Advance(src.Length); + writer.Advance(20); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw21(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(21); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -309,13 +309,13 @@ public static unsafe void WriteRaw21(ref MessagePackWriter writer, ReadOnlySpan< *(int*)(pDst + 17) = *(int*)(pSrc + 17); } - writer.Advance(src.Length); + writer.Advance(21); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw22(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(22); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -328,13 +328,13 @@ public static unsafe void WriteRaw22(ref MessagePackWriter writer, ReadOnlySpan< *(int*)(pDst + 18) = *(int*)(pSrc + 18); } - writer.Advance(src.Length); + writer.Advance(22); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw23(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(23); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -347,13 +347,13 @@ public static unsafe void WriteRaw23(ref MessagePackWriter writer, ReadOnlySpan< *(int*)(pDst + 19) = *(int*)(pSrc + 19); } - writer.Advance(src.Length); + writer.Advance(23); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw24(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(24); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -366,13 +366,13 @@ public static unsafe void WriteRaw24(ref MessagePackWriter writer, ReadOnlySpan< *(int*)(pDst + 20) = *(int*)(pSrc + 20); } - writer.Advance(src.Length); + writer.Advance(24); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw25(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(25); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -386,13 +386,13 @@ public static unsafe void WriteRaw25(ref MessagePackWriter writer, ReadOnlySpan< *(int*)(pDst + 21) = *(int*)(pSrc + 21); } - writer.Advance(src.Length); + writer.Advance(25); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw26(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(26); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -406,13 +406,13 @@ public static unsafe void WriteRaw26(ref MessagePackWriter writer, ReadOnlySpan< *(int*)(pDst + 22) = *(int*)(pSrc + 22); } - writer.Advance(src.Length); + writer.Advance(26); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw27(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(27); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -426,13 +426,13 @@ public static unsafe void WriteRaw27(ref MessagePackWriter writer, ReadOnlySpan< *(int*)(pDst + 23) = *(int*)(pSrc + 23); } - writer.Advance(src.Length); + writer.Advance(27); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw28(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(28); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -446,13 +446,13 @@ public static unsafe void WriteRaw28(ref MessagePackWriter writer, ReadOnlySpan< *(int*)(pDst + 24) = *(int*)(pSrc + 24); } - writer.Advance(src.Length); + writer.Advance(28); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw29(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(29); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -467,13 +467,13 @@ public static unsafe void WriteRaw29(ref MessagePackWriter writer, ReadOnlySpan< *(int*)(pDst + 25) = *(int*)(pSrc + 25); } - writer.Advance(src.Length); + writer.Advance(29); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw30(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(30); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -488,13 +488,13 @@ public static unsafe void WriteRaw30(ref MessagePackWriter writer, ReadOnlySpan< *(int*)(pDst + 26) = *(int*)(pSrc + 26); } - writer.Advance(src.Length); + writer.Advance(30); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw31(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(31); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -509,7 +509,7 @@ public static unsafe void WriteRaw31(ref MessagePackWriter writer, ReadOnlySpan< *(int*)(pDst + 27) = *(int*)(pSrc + 27); } - writer.Advance(src.Length); + writer.Advance(31); } } @@ -518,7 +518,7 @@ public static partial class UnsafeMemory64 [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw8(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(8); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -526,13 +526,13 @@ public static unsafe void WriteRaw8(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(9); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -541,13 +541,13 @@ public static unsafe void WriteRaw9(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(10); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -556,13 +556,13 @@ public static unsafe void WriteRaw10(ref MessagePackWriter writer, ReadOnlySpan< *(long*)(pDst + 2) = *(long*)(pSrc + 2); } - writer.Advance(src.Length); + writer.Advance(10); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw11(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(11); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -571,13 +571,13 @@ public static unsafe void WriteRaw11(ref MessagePackWriter writer, ReadOnlySpan< *(long*)(pDst + 3) = *(long*)(pSrc + 3); } - writer.Advance(src.Length); + writer.Advance(11); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw12(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(12); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -586,13 +586,13 @@ public static unsafe void WriteRaw12(ref MessagePackWriter writer, ReadOnlySpan< *(long*)(pDst + 4) = *(long*)(pSrc + 4); } - writer.Advance(src.Length); + writer.Advance(12); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw13(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(13); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -601,13 +601,13 @@ public static unsafe void WriteRaw13(ref MessagePackWriter writer, ReadOnlySpan< *(long*)(pDst + 5) = *(long*)(pSrc + 5); } - writer.Advance(src.Length); + writer.Advance(13); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw14(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(14); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -616,13 +616,13 @@ public static unsafe void WriteRaw14(ref MessagePackWriter writer, ReadOnlySpan< *(long*)(pDst + 6) = *(long*)(pSrc + 6); } - writer.Advance(src.Length); + writer.Advance(14); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw15(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(15); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -631,13 +631,13 @@ public static unsafe void WriteRaw15(ref MessagePackWriter writer, ReadOnlySpan< *(long*)(pDst + 7) = *(long*)(pSrc + 7); } - writer.Advance(src.Length); + writer.Advance(15); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw16(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(16); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -646,13 +646,13 @@ public static unsafe void WriteRaw16(ref MessagePackWriter writer, ReadOnlySpan< *(long*)(pDst + 8) = *(long*)(pSrc + 8); } - writer.Advance(src.Length); + writer.Advance(16); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw17(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(17); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -662,13 +662,13 @@ public static unsafe void WriteRaw17(ref MessagePackWriter writer, ReadOnlySpan< *(long*)(pDst + 9) = *(long*)(pSrc + 9); } - writer.Advance(src.Length); + writer.Advance(17); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw18(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(18); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -678,13 +678,13 @@ public static unsafe void WriteRaw18(ref MessagePackWriter writer, ReadOnlySpan< *(long*)(pDst + 10) = *(long*)(pSrc + 10); } - writer.Advance(src.Length); + writer.Advance(18); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw19(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(19); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -694,13 +694,13 @@ public static unsafe void WriteRaw19(ref MessagePackWriter writer, ReadOnlySpan< *(long*)(pDst + 11) = *(long*)(pSrc + 11); } - writer.Advance(src.Length); + writer.Advance(19); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw20(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(20); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -710,13 +710,13 @@ public static unsafe void WriteRaw20(ref MessagePackWriter writer, ReadOnlySpan< *(long*)(pDst + 12) = *(long*)(pSrc + 12); } - writer.Advance(src.Length); + writer.Advance(20); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw21(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(21); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -726,13 +726,13 @@ public static unsafe void WriteRaw21(ref MessagePackWriter writer, ReadOnlySpan< *(long*)(pDst + 13) = *(long*)(pSrc + 13); } - writer.Advance(src.Length); + writer.Advance(21); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw22(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(22); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -742,13 +742,13 @@ public static unsafe void WriteRaw22(ref MessagePackWriter writer, ReadOnlySpan< *(long*)(pDst + 14) = *(long*)(pSrc + 14); } - writer.Advance(src.Length); + writer.Advance(22); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw23(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(23); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -758,13 +758,13 @@ public static unsafe void WriteRaw23(ref MessagePackWriter writer, ReadOnlySpan< *(long*)(pDst + 15) = *(long*)(pSrc + 15); } - writer.Advance(src.Length); + writer.Advance(23); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw24(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(24); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -774,13 +774,13 @@ public static unsafe void WriteRaw24(ref MessagePackWriter writer, ReadOnlySpan< *(long*)(pDst + 16) = *(long*)(pSrc + 16); } - writer.Advance(src.Length); + writer.Advance(24); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw25(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(25); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -791,13 +791,13 @@ public static unsafe void WriteRaw25(ref MessagePackWriter writer, ReadOnlySpan< *(long*)(pDst + 17) = *(long*)(pSrc + 17); } - writer.Advance(src.Length); + writer.Advance(25); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw26(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(26); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -808,13 +808,13 @@ public static unsafe void WriteRaw26(ref MessagePackWriter writer, ReadOnlySpan< *(long*)(pDst + 18) = *(long*)(pSrc + 18); } - writer.Advance(src.Length); + writer.Advance(26); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw27(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(27); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -825,13 +825,13 @@ public static unsafe void WriteRaw27(ref MessagePackWriter writer, ReadOnlySpan< *(long*)(pDst + 19) = *(long*)(pSrc + 19); } - writer.Advance(src.Length); + writer.Advance(27); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw28(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(28); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -842,13 +842,13 @@ public static unsafe void WriteRaw28(ref MessagePackWriter writer, ReadOnlySpan< *(long*)(pDst + 20) = *(long*)(pSrc + 20); } - writer.Advance(src.Length); + writer.Advance(28); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw29(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(29); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -859,13 +859,13 @@ public static unsafe void WriteRaw29(ref MessagePackWriter writer, ReadOnlySpan< *(long*)(pDst + 21) = *(long*)(pSrc + 21); } - writer.Advance(src.Length); + writer.Advance(29); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw30(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(30); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -876,13 +876,13 @@ public static unsafe void WriteRaw30(ref MessagePackWriter writer, ReadOnlySpan< *(long*)(pDst + 22) = *(long*)(pSrc + 22); } - writer.Advance(src.Length); + writer.Advance(30); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw31(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(31); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -893,7 +893,7 @@ public static unsafe void WriteRaw31(ref MessagePackWriter writer, ReadOnlySpan< *(long*)(pDst + 23) = *(long*)(pSrc + 23); } - writer.Advance(src.Length); + writer.Advance(31); } } } diff --git a/src/MessagePack/Internal/UnsafeMemory.tt b/src/MessagePack/Internal/UnsafeMemory.tt index 3410e207b..3610b7c9e 100644 --- a/src/MessagePack/Internal/UnsafeMemory.tt +++ b/src/MessagePack/Internal/UnsafeMemory.tt @@ -28,7 +28,7 @@ namespace MessagePack.Internal [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw<#= i #>(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(<#= i #>); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -41,7 +41,7 @@ namespace MessagePack.Internal <# } #> } - writer.Advance(src.Length); + writer.Advance(<#= i #>); } <# } #> } @@ -52,7 +52,7 @@ namespace MessagePack.Internal [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void WriteRaw<#= i #>(ref MessagePackWriter writer, ReadOnlySpan src) { - Span dst = writer.GetSpan(src.Length); + Span dst = writer.GetSpan(<#= i #>); fixed (byte* pSrc = &src[0]) fixed (byte* pDst = &dst[0]) @@ -65,7 +65,7 @@ namespace MessagePack.Internal <# } #> } - writer.Advance(src.Length); + writer.Advance(<#= i #>); } <# } #> } diff --git a/tests/MessagePack.Tests/UnsafeMemoryTest.cs b/tests/MessagePack.Tests/UnsafeMemoryTest.cs index c9b5f023e..8c3af1431 100644 --- a/tests/MessagePack.Tests/UnsafeMemoryTest.cs +++ b/tests/MessagePack.Tests/UnsafeMemoryTest.cs @@ -3,15 +3,9 @@ using System; using System.Buffers; -using System.Collections.Generic; using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; -using MessagePack.Formatters; using MessagePack.Internal; using Nerdbank.Streams; -using Xunit; namespace MessagePack.Tests { @@ -70,6 +64,35 @@ public void WriteRaw() } } + [Fact] + public void WriteRaw_LargerSpan() + { + ReadOnlySpan src = new byte[MessagePackRange.MaxFixStringLength + 15]; + Sequence dst = new(); + + // x86 + for (int i = 1; i <= MessagePackRange.MaxFixStringLength; i++) + { + dst.Reset(); + MessagePackWriter dstWriter = new(dst); + (typeof(UnsafeMemory32).GetMethod("WriteRaw" + i).CreateDelegate(typeof(WriteDelegate)) as WriteDelegate).Invoke(ref dstWriter, src); + dstWriter.Flush(); + dst.Length.Is(i); + src[0..i].SequenceEqual(CodeGenHelpers.GetSpanFromSequence(dst.AsReadOnlySequence)).IsTrue(); + } + + // x64 + for (int i = 1; i <= MessagePackRange.MaxFixStringLength; i++) + { + dst.Reset(); + var dstWriter = new MessagePackWriter(dst); + (typeof(UnsafeMemory64).GetMethod("WriteRaw" + i).CreateDelegate(typeof(WriteDelegate)) as WriteDelegate).Invoke(ref dstWriter, src); + dstWriter.Flush(); + dst.Length.Is(i); + src[0..i].SequenceEqual(CodeGenHelpers.GetSpanFromSequence(dst.AsReadOnlySequence)).IsTrue(); + } + } + #endif } } From 719e690abae8d4dba6b0c43088c8ded29cdc1ba2 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 29 Apr 2026 13:59:17 -0600 Subject: [PATCH 02/17] Bound LZ4 input reads for CWE-125 The LZ4 block decoder accepted only the destination length and advanced through the compressed input without knowing where the source buffer ended. Malformed compressed payloads could therefore drive unchecked native reads before the existing post-decode length check ran. Pass the compressed input length into the 32-bit and 64-bit decoders and reject malformed blocks before token, literal, offset, and match-length reads would move past the source buffer. Add a regression test that verifies malformed LZ4 data fails as a normal serialization exception. --- src/MessagePack/LZ4/LZ4Codec.Unsafe.cs | 4 +- .../LZ4/LZ4Codec.Unsafe32.Dirty.cs | 49 ++++++++++++++++--- .../LZ4/LZ4Codec.Unsafe64.Dirty.cs | 49 ++++++++++++++++--- tests/MessagePack.Tests/LZ4Test.cs | 41 ++++++++++++++++ 4 files changed, 125 insertions(+), 18 deletions(-) diff --git a/src/MessagePack/LZ4/LZ4Codec.Unsafe.cs b/src/MessagePack/LZ4/LZ4Codec.Unsafe.cs index ec9a28bc9..1efabf184 100644 --- a/src/MessagePack/LZ4/LZ4Codec.Unsafe.cs +++ b/src/MessagePack/LZ4/LZ4Codec.Unsafe.cs @@ -99,11 +99,11 @@ public static unsafe int Decode(ReadOnlySpan input, Span output) int length; if (IntPtr.Size == 4) { - length = LZ4_uncompress_32(inputPtr, outputPtr, output.Length); + length = LZ4_uncompress_32(inputPtr, input.Length, outputPtr, output.Length); } else { - length = LZ4_uncompress_64(inputPtr, outputPtr, output.Length); + length = LZ4_uncompress_64(inputPtr, input.Length, outputPtr, output.Length); } if (length != input.Length) diff --git a/src/MessagePack/LZ4/LZ4Codec.Unsafe32.Dirty.cs b/src/MessagePack/LZ4/LZ4Codec.Unsafe32.Dirty.cs index 22d39b25d..23a40ddca 100644 --- a/src/MessagePack/LZ4/LZ4Codec.Unsafe32.Dirty.cs +++ b/src/MessagePack/LZ4/LZ4Codec.Unsafe32.Dirty.cs @@ -600,6 +600,7 @@ private static unsafe int LZ4_compress64kCtx_32( private static unsafe int LZ4_uncompress_32( byte* src, + int src_len, byte* dst, int dst_len) { @@ -609,6 +610,7 @@ private static unsafe int LZ4_uncompress_32( { // r93 var src_p = src; + var src_end = src + src_len; byte* xxx_ref; var dst_p = dst; @@ -627,16 +629,26 @@ private static unsafe int LZ4_uncompress_32( int length; // get runlength + if (src_p >= src_end) + { + goto _output_error; + } + xxx_token = *src_p++; if ((length = (int)(xxx_token >> ML_BITS)) == RUN_MASK) { int len; - for (; (len = *src_p++) == 255; length += 255) + do { - /* do nothing */ - } + if (src_p >= src_end) + { + goto _output_error; + } - length += len; + len = *src_p++; + length += len; + } + while (len == 255); } // copy literals @@ -649,11 +661,21 @@ private static unsafe int LZ4_uncompress_32( goto _output_error; // Error : not enough place for another match (min 4) + 5 literals } + if (length > src_end - src_p) + { + goto _output_error; + } + BlockCopy32(src_p, dst_p, length); src_p += length; break; // EOF } + if (length > src_end - src_p) + { + goto _output_error; + } + do { *(uint*)dst_p = *(uint*)src_p; @@ -668,6 +690,11 @@ private static unsafe int LZ4_uncompress_32( dst_p = dst_cpy; // get offset + if (src_end - src_p < 2) + { + goto _output_error; + } + xxx_ref = dst_cpy - (*(ushort*)src_p); src_p += 2; if (xxx_ref < dst) @@ -678,12 +705,18 @@ private static unsafe int LZ4_uncompress_32( // get matchlength if ((length = (int)(xxx_token & ML_MASK)) == ML_MASK) { - for (; *src_p == 255; length += 255) + int len; + do { - src_p++; - } + if (src_p >= src_end) + { + goto _output_error; + } - length += *src_p++; + len = *src_p++; + length += len; + } + while (len == 255); } // copy repeated sequence diff --git a/src/MessagePack/LZ4/LZ4Codec.Unsafe64.Dirty.cs b/src/MessagePack/LZ4/LZ4Codec.Unsafe64.Dirty.cs index ba10e68d2..f807ef17e 100644 --- a/src/MessagePack/LZ4/LZ4Codec.Unsafe64.Dirty.cs +++ b/src/MessagePack/LZ4/LZ4Codec.Unsafe64.Dirty.cs @@ -612,6 +612,7 @@ private static unsafe int LZ4_compress64kCtx_64( private static unsafe int LZ4_uncompress_64( byte* src, + int src_len, byte* dst, int dst_len) { @@ -622,6 +623,7 @@ private static unsafe int LZ4_uncompress_64( { // r93 var src_p = src; + var src_end = src + src_len; byte* dst_ref; var dst_p = dst; @@ -640,16 +642,26 @@ private static unsafe int LZ4_uncompress_64( int length; // get runlength + if (src_p >= src_end) + { + goto _output_error; + } + token = *src_p++; if ((length = token >> ML_BITS) == RUN_MASK) { int len; - for (; (len = *src_p++) == 255; length += 255) + do { - /* do nothing */ - } + if (src_p >= src_end) + { + goto _output_error; + } - length += len; + len = *src_p++; + length += len; + } + while (len == 255); } // copy literals @@ -662,11 +674,21 @@ private static unsafe int LZ4_uncompress_64( goto _output_error; // Error : not enough place for another match (min 4) + 5 literals } + if (length > src_end - src_p) + { + goto _output_error; + } + BlockCopy64(src_p, dst_p, length); src_p += length; break; // EOF } + if (length > src_end - src_p) + { + goto _output_error; + } + do { *(ulong*)dst_p = *(ulong*)src_p; @@ -678,6 +700,11 @@ private static unsafe int LZ4_uncompress_64( dst_p = dst_cpy; // get offset + if (src_end - src_p < 2) + { + goto _output_error; + } + dst_ref = dst_cpy - (*(ushort*)src_p); src_p += 2; if (dst_ref < dst) @@ -688,12 +715,18 @@ private static unsafe int LZ4_uncompress_64( // get matchlength if ((length = token & ML_MASK) == ML_MASK) { - for (; *src_p == 255; length += 255) + int len; + do { - src_p++; - } + if (src_p >= src_end) + { + goto _output_error; + } - length += *src_p++; + len = *src_p++; + length += len; + } + while (len == 255); } // copy repeated sequence diff --git a/tests/MessagePack.Tests/LZ4Test.cs b/tests/MessagePack.Tests/LZ4Test.cs index f314b572a..062576fcd 100644 --- a/tests/MessagePack.Tests/LZ4Test.cs +++ b/tests/MessagePack.Tests/LZ4Test.cs @@ -32,6 +32,47 @@ public void Lz4Compress() Execute(10000); } + [Fact] + [Trait("CWE", "125")] + public void Lz4BlockRejectsTruncatedLiteralRun() + { + const int extensionByteCount = 1024; + int uncompressedLength = 15 + (255 * extensionByteCount) + 16; + + byte[] sizeHeader = + { + 0xCE, + (byte)(uncompressedLength >> 24), + (byte)(uncompressedLength >> 16), + (byte)(uncompressedLength >> 8), + (byte)uncompressedLength, + }; + + byte[] lz4 = new byte[1 + extensionByteCount]; + lz4[0] = 0xF0; + for (int i = 1; i < lz4.Length; i++) + { + lz4[i] = 0xFF; + } + + int bodyLength = sizeHeader.Length + lz4.Length; + byte[] payload = new byte[6 + bodyLength]; + int offset = 0; + payload[offset++] = 0xC9; + payload[offset++] = (byte)(bodyLength >> 24); + payload[offset++] = (byte)(bodyLength >> 16); + payload[offset++] = (byte)(bodyLength >> 8); + payload[offset++] = (byte)bodyLength; + payload[offset++] = 99; + System.Array.Copy(sizeHeader, 0, payload, offset, sizeHeader.Length); + offset += sizeHeader.Length; + System.Array.Copy(lz4, 0, payload, offset, lz4.Length); + + var options = MessagePackSerializerOptions.Standard.WithCompression(MessagePackCompression.Lz4Block); + + Assert.Throws(() => MessagePackSerializer.Deserialize(payload, options)); + } + private void Execute(int count) { // Large From f093bdc1207f6576088d39801fa43e92cec1e5c1 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 29 Apr 2026 14:06:32 -0600 Subject: [PATCH 03/17] Reject nested typeless blocklist bypass for CWE-502 Typeless deserialization previously validated only the outer wire-supplied type before resolving a formatter. Container types could therefore hide a disallowed element or generic argument from the mitigation and from custom option overrides. Validate element and constructed generic argument types before formatter resolution, and add regression coverage for nested typeless disallowed types. --- .../MessagePackSerializerOptions.cs | 47 +++++++++++++++++-- .../MessagePackSerializerTypelessTests.cs | 24 ++++++++-- 2 files changed, 64 insertions(+), 7 deletions(-) diff --git a/src/MessagePack/MessagePackSerializerOptions.cs b/src/MessagePack/MessagePackSerializerOptions.cs index dc4598239..18c197f78 100644 --- a/src/MessagePack/MessagePackSerializerOptions.cs +++ b/src/MessagePack/MessagePackSerializerOptions.cs @@ -175,16 +175,32 @@ protected MessagePackSerializerOptions(MessagePackSerializerOptions copyFrom) /// The type to be instantiated. /// Thrown if the is not allowed to be deserialized. /// + /// /// This method provides a means for an important security mitigation when using the Typeless formatter to prevent untrusted messagepack from /// deserializing objects that may be harmful if instantiated, disposed or finalized. - /// The default implementation throws for only a few known dangerous types. + /// The default implementation throws for only a few known dangerous types, or types that nest those dangerous types as generic type arguments or array element types. /// Applications that deserialize from untrusted sources should override this method and throw if the type is not among the expected set. + /// + /// + /// This method is for backward compatibility reasons. + /// For better security, the preferred method to override is . + /// /// public virtual void ThrowIfDeserializingTypeIsDisallowed(Type type) { - if (type.FullName is string fullName && DisallowedTypes.Contains(fullName)) + this.ThrowIfDeserializingTypeIsDisallowedCore(type); + + if (type.HasElementType && type.GetElementType() is Type elementType) { - throw new MessagePackSerializationException($"Deserialization attempted to create the type {fullName} which is not allowed."); + this.ThrowIfDeserializingTypeIsDisallowed(elementType); + } + + if (type.IsConstructedGenericType) + { + foreach (Type genericTypeArgument in type.GenericTypeArguments) + { + this.ThrowIfDeserializingTypeIsDisallowed(genericTypeArgument); + } } } @@ -361,6 +377,31 @@ public MessagePackSerializerOptions WithPool(SequencePool pool) return result; } + /// + /// Checks whether a specific given type may be deserialized, disregarding generic type arguments or array element types. + /// + /// The type to be instantiated. + /// Thrown if the is not allowed to be deserialized. + /// + /// + /// This method provides a means for an important security mitigation when using the Typeless formatter to prevent untrusted messagepack from + /// deserializing objects that may be harmful if instantiated, disposed or finalized. + /// The default implementation throws for only a few known dangerous types. + /// Applications that deserialize from untrusted sources should override this method and throw if the type is not among the expected set. + /// + /// + /// This method is called from the default implementation of + /// for the top-level type and again for each generic type argument or array element type. + /// + /// + protected virtual void ThrowIfDeserializingTypeIsDisallowedCore(Type type) + { + if (type.FullName is string fullName && DisallowedTypes.Contains(fullName)) + { + throw new MessagePackSerializationException($"Deserialization attempted to create the type {fullName} which is not allowed."); + } + } + /// /// Creates a clone of this instance with the same properties set. /// diff --git a/tests/MessagePack.Tests/MessagePackSerializerTypelessTests.cs b/tests/MessagePack.Tests/MessagePackSerializerTypelessTests.cs index c92c8c994..da7246a43 100644 --- a/tests/MessagePack.Tests/MessagePackSerializerTypelessTests.cs +++ b/tests/MessagePack.Tests/MessagePackSerializerTypelessTests.cs @@ -4,12 +4,10 @@ #if !UNITY_2018_3_OR_NEWER using System; +using System.Collections.Generic; using System.Runtime.Serialization; -using MessagePack; using MessagePack.Formatters; using MessagePack.Resolvers; -using Xunit; -using Xunit.Abstractions; public class MessagePackSerializerTypelessTests { @@ -47,6 +45,24 @@ public void SerializationOfDisallowedType() Assert.IsType(ex.InnerException); } + [Theory] + [MemberData(nameof(DisallowedNestedTypeData))] + [Trait("CWE", "502")] + public void SerializationOfDisallowedNestedType(object value) + { + var myOptions = new MyTypelessOptions(); + byte[] msgpack = MessagePackSerializer.Typeless.Serialize(value, myOptions); + this.logger.WriteLine(MessagePackSerializer.ConvertToJson(msgpack, myOptions)); + var ex = Assert.Throws(() => MessagePackSerializer.Typeless.Deserialize(msgpack, myOptions)); + Assert.IsType(ex.InnerException); + } + + public static IEnumerable DisallowedNestedTypeData() + { + yield return new object[] { new MyObject[] { new() { SomeValue = 5 } } }; + yield return new object[] { new List { new() { SomeValue = 5 } } }; + } + [Fact(Skip = "Known bug https://github.com/neuecc/MessagePack-CSharp/issues/651")] public void DecimalShouldBeDeserializedAsDecimal() { @@ -137,7 +153,7 @@ internal MyTypelessOptions(MyTypelessOptions copyFrom) { } - public override void ThrowIfDeserializingTypeIsDisallowed(Type type) + protected override void ThrowIfDeserializingTypeIsDisallowedCore(Type type) { if (type == typeof(MyObject)) { From b9cb6050908f75ad813a662fbe7cc06f84fabd23 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 13 May 2026 16:30:16 -0600 Subject: [PATCH 04/17] Use iteration for skipping msgpack structures for CWE-674 The `MessagePackReader` has no concept of a depth limit, and it implemented its `Skip()` method recursively which can easily blow the stack for deeply nested msgpack structures. Rather than introduce a new API with an adjustable depth limit, set to a 'secure' but otherwise arbitrary default, we can make `Skip()` iterate instead of recurse in order to avoid ever crashing. --- src/MessagePack/MessagePackReader.cs | 185 +++++++++++------- .../MessagePackReaderTests.cs | 43 ++++ 2 files changed, 157 insertions(+), 71 deletions(-) diff --git a/src/MessagePack/MessagePackReader.cs b/src/MessagePack/MessagePackReader.cs index b7eebae74..3a858cc8c 100644 --- a/src/MessagePack/MessagePackReader.cs +++ b/src/MessagePack/MessagePackReader.cs @@ -153,64 +153,123 @@ public byte NextCode /// internal bool TrySkip() { - if (this.reader.Remaining == 0) + long remainingStructures = 1; + while (remainingStructures > 0) { - return false; - } + if (this.reader.Remaining == 0) + { + return false; + } - byte code = this.NextCode; - switch (code) - { - case byte x when MessagePackCode.IsPositiveFixInt(x) || MessagePackCode.IsNegativeFixInt(x): - case MessagePackCode.Nil: - case MessagePackCode.True: - case MessagePackCode.False: - return this.reader.TryAdvance(1); - case MessagePackCode.Int8: - case MessagePackCode.UInt8: - return this.reader.TryAdvance(2); - case MessagePackCode.Int16: - case MessagePackCode.UInt16: - return this.reader.TryAdvance(3); - case MessagePackCode.Int32: - case MessagePackCode.UInt32: - case MessagePackCode.Float32: - return this.reader.TryAdvance(5); - case MessagePackCode.Int64: - case MessagePackCode.UInt64: - case MessagePackCode.Float64: - return this.reader.TryAdvance(9); - case byte x when MessagePackCode.IsFixMap(x): - case MessagePackCode.Map16: - case MessagePackCode.Map32: - return this.TrySkipNextMap(); - case byte x when MessagePackCode.IsFixArray(x): - case MessagePackCode.Array16: - case MessagePackCode.Array32: - return this.TrySkipNextArray(); - case byte x when MessagePackCode.IsFixStr(x): - case MessagePackCode.Str8: - case MessagePackCode.Str16: - case MessagePackCode.Str32: - return this.TryGetStringLengthInBytes(out uint length) && this.reader.TryAdvance(length); - case MessagePackCode.Bin8: - case MessagePackCode.Bin16: - case MessagePackCode.Bin32: - return this.TryGetBytesLength(out length) && this.reader.TryAdvance(length); - case MessagePackCode.FixExt1: - case MessagePackCode.FixExt2: - case MessagePackCode.FixExt4: - case MessagePackCode.FixExt8: - case MessagePackCode.FixExt16: - case MessagePackCode.Ext8: - case MessagePackCode.Ext16: - case MessagePackCode.Ext32: - return this.TryReadExtensionFormatHeader(out ExtensionHeader header) && this.reader.TryAdvance(header.Length); - default: - // We don't actually expect to ever hit this point, since every code is supported. - Debug.Fail("Missing handler for code: " + code); - throw ThrowInvalidCode(code); + remainingStructures--; + byte code = this.NextCode; + switch (code) + { + case byte x when MessagePackCode.IsPositiveFixInt(x) || MessagePackCode.IsNegativeFixInt(x): + case MessagePackCode.Nil: + case MessagePackCode.True: + case MessagePackCode.False: + if (!this.reader.TryAdvance(1)) + { + return false; + } + + break; + case MessagePackCode.Int8: + case MessagePackCode.UInt8: + if (!this.reader.TryAdvance(2)) + { + return false; + } + + break; + case MessagePackCode.Int16: + case MessagePackCode.UInt16: + if (!this.reader.TryAdvance(3)) + { + return false; + } + + break; + case MessagePackCode.Int32: + case MessagePackCode.UInt32: + case MessagePackCode.Float32: + if (!this.reader.TryAdvance(5)) + { + return false; + } + + break; + case MessagePackCode.Int64: + case MessagePackCode.UInt64: + case MessagePackCode.Float64: + if (!this.reader.TryAdvance(9)) + { + return false; + } + + break; + case byte x when MessagePackCode.IsFixMap(x): + case MessagePackCode.Map16: + case MessagePackCode.Map32: + if (!this.TryReadMapHeader(out int count)) + { + return false; + } + + remainingStructures = checked(remainingStructures + ((long)count * 2)); + break; + case byte x when MessagePackCode.IsFixArray(x): + case MessagePackCode.Array16: + case MessagePackCode.Array32: + if (!this.TryReadArrayHeader(out count)) + { + return false; + } + + remainingStructures = checked(remainingStructures + count); + break; + case byte x when MessagePackCode.IsFixStr(x): + case MessagePackCode.Str8: + case MessagePackCode.Str16: + case MessagePackCode.Str32: + if (!this.TryGetStringLengthInBytes(out uint length) || !this.reader.TryAdvance(length)) + { + return false; + } + + break; + case MessagePackCode.Bin8: + case MessagePackCode.Bin16: + case MessagePackCode.Bin32: + if (!this.TryGetBytesLength(out length) || !this.reader.TryAdvance(length)) + { + return false; + } + + break; + case MessagePackCode.FixExt1: + case MessagePackCode.FixExt2: + case MessagePackCode.FixExt4: + case MessagePackCode.FixExt8: + case MessagePackCode.FixExt16: + case MessagePackCode.Ext8: + case MessagePackCode.Ext16: + case MessagePackCode.Ext32: + if (!this.TryReadExtensionFormatHeader(out ExtensionHeader header) || !this.reader.TryAdvance(header.Length)) + { + return false; + } + + break; + default: + // We don't actually expect to ever hit this point, since every code is supported. + Debug.Fail("Missing handler for code: " + code); + throw ThrowInvalidCode(code); + } } + + return true; } /// @@ -1099,21 +1158,5 @@ private string ReadStringSlow(uint byteLength) return value; } - private bool TrySkipNextArray() => this.TryReadArrayHeader(out int count) && this.TrySkip(count); - - private bool TrySkipNextMap() => this.TryReadMapHeader(out int count) && this.TrySkip(count * 2); - - private bool TrySkip(int count) - { - for (int i = 0; i < count; i++) - { - if (!this.TrySkip()) - { - return false; - } - } - - return true; - } } } diff --git a/tests/MessagePack.Tests/MessagePackReaderTests.cs b/tests/MessagePack.Tests/MessagePackReaderTests.cs index 7ac748023..126e518fc 100644 --- a/tests/MessagePack.Tests/MessagePackReaderTests.cs +++ b/tests/MessagePack.Tests/MessagePackReaderTests.cs @@ -330,6 +330,49 @@ public void ReadRaw() Assert.True(reader.End); } + [Fact] + [Trait("CWE", "674")] + public void Skip_DeeplyNestedArrays_DoesNotOverflowStack() + { + const int depth = 100_000; + byte[] msgpack = new byte[depth + 1]; + + for (int i = 0; i < depth; i++) + { + msgpack[i] = MessagePackCode.MinFixArray + 1; + } + + msgpack[^1] = MessagePackCode.Nil; + + MessagePackReader reader = new(msgpack); + + reader.Skip(); + + Assert.True(reader.End); + } + + [Fact] + [Trait("CWE", "674")] + public void Skip_DeeplyNestedMaps_DoesNotOverflowStack() + { + const int depth = 100_000; + byte[] msgpack = new byte[(depth * 2) + 1]; + + for (int i = 0; i < depth; i++) + { + msgpack[i * 2] = MessagePackCode.MinFixMap + 1; + msgpack[(i * 2) + 1] = MessagePackCode.Nil; + } + + msgpack[^1] = MessagePackCode.Nil; + + MessagePackReader reader = new(msgpack); + + reader.Skip(); + + Assert.True(reader.End); + } + [Fact] public void Depth() { From 26d4e743ca2a88a5626e62cced3762820e392325 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 29 Apr 2026 14:02:55 -0600 Subject: [PATCH 05/17] Reject invalid DateTime ext lengths for CWE-789 DateTime extension decoding previously treated any declared timestamp length as a partial token before validating whether that length was legal. Malformed input could therefore force the reader slow path to prepare a stack buffer sized from the untrusted extension header. Validate timestamp extension lengths before insufficient-buffer handling so only the supported timestamp encodings can proceed to buffering. Add a regression test that verifies oversized malformed DateTime extension headers fail as a normal serialization exception. --- src/MessagePack/MessagePackPrimitives.Readers.cs | 6 ++++++ tests/MessagePack.Tests/MessagePackReaderTests.cs | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/src/MessagePack/MessagePackPrimitives.Readers.cs b/src/MessagePack/MessagePackPrimitives.Readers.cs index e7da34e48..1de3a33d5 100644 --- a/src/MessagePack/MessagePackPrimitives.Readers.cs +++ b/src/MessagePack/MessagePackPrimitives.Readers.cs @@ -342,6 +342,12 @@ public static DecodeResult TryReadDateTime(ReadOnlySpan source, ExtensionH return DecodeResult.TokenMismatch; } + if (header.Length is not (4 or 8 or 12)) + { + value = default; + return DecodeResult.TokenMismatch; + } + if (source.Length < tokenSize) { value = default; diff --git a/tests/MessagePack.Tests/MessagePackReaderTests.cs b/tests/MessagePack.Tests/MessagePackReaderTests.cs index 7ac748023..30f96ffc2 100644 --- a/tests/MessagePack.Tests/MessagePackReaderTests.cs +++ b/tests/MessagePack.Tests/MessagePackReaderTests.cs @@ -400,6 +400,15 @@ void AssertIncomplete(WriterEncoder encoder, ReadOperation decoder, bool v AssertIncomplete((ref MessagePackWriter writer) => writer.Write(0xff), (ref MessagePackReader reader) => reader.ReadUInt64()); } + [Fact] + [Trait("CWE", "789")] + public void ReadDateTime_RejectsInvalidExtensionLengthBeforeBuffering() + { + byte[] payload = [MessagePackCode.Ext32, 0x00, 0x10, 0x00, 0x00, 0xff]; + + Assert.Throws(() => new MessagePackReader(new ReadOnlySequence(payload)).ReadDateTime()); + } + [Fact] public void CreatePeekReader() { From 9b5783a7e40b1281ffce32d8631b9cf4b76b2404 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 29 Apr 2026 14:15:31 -0600 Subject: [PATCH 06/17] Fix CWE-789 multidimensional array allocation validation Multidimensional array deserialization trusted dimension values and allocated arrays before confirming that the flattened element count matched the serialized element array header. This could let malformed data request disproportionate allocations before validation. Validate non-negative dimensions and checked flattened lengths for 2D, 3D, and 4D array formatters before allocation, and add regression coverage for mismatched element counts under untrusted data options. --- .../MultiDimensionalArrayFormatter.cs | 38 +++++++++++- .../MultiDimensionalArrayTest.cs | 58 ++++++++++++++++++- 2 files changed, 92 insertions(+), 4 deletions(-) diff --git a/src/MessagePack/Formatters/MultiDimensionalArrayFormatter.cs b/src/MessagePack/Formatters/MultiDimensionalArrayFormatter.cs index 30b2f462b..4455b31e6 100644 --- a/src/MessagePack/Formatters/MultiDimensionalArrayFormatter.cs +++ b/src/MessagePack/Formatters/MultiDimensionalArrayFormatter.cs @@ -2,9 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using System.Buffers; -using System.Collections.Generic; -using System.Text; +using System.Diagnostics.CodeAnalysis; using MessagePack.Internal; #pragma warning disable SA1402 // File may only contain a single type @@ -64,6 +62,7 @@ public void Serialize(ref MessagePackWriter writer, T[,]? value, MessagePackSeri var iLength = reader.ReadInt32(); var jLength = reader.ReadInt32(); var maxLen = reader.ReadArrayHeader(); + MultiDimensionalArrayFormatterHelper.ThrowIfLengthsDontMatch("T[,]", maxLen, iLength, jLength); var array = new T[iLength, jLength]; @@ -151,6 +150,7 @@ public void Serialize(ref MessagePackWriter writer, T[,,]? value, MessagePackSer var jLength = reader.ReadInt32(); var kLength = reader.ReadInt32(); var maxLen = reader.ReadArrayHeader(); + MultiDimensionalArrayFormatterHelper.ThrowIfLengthsDontMatch("T[,,]", maxLen, iLength, jLength, kLength); var array = new T[iLength, jLength, kLength]; @@ -248,6 +248,8 @@ public void Serialize(ref MessagePackWriter writer, T[,,,]? value, MessagePackSe var kLength = reader.ReadInt32(); var lLength = reader.ReadInt32(); var maxLen = reader.ReadArrayHeader(); + MultiDimensionalArrayFormatterHelper.ThrowIfLengthsDontMatch("T[,,,]", maxLen, iLength, jLength, kLength, lLength); + var array = new T[iLength, jLength, kLength, lLength]; var i = 0; @@ -295,4 +297,34 @@ public void Serialize(ref MessagePackWriter writer, T[,,,]? value, MessagePackSe } } } + + internal static class MultiDimensionalArrayFormatterHelper + { + internal static void ThrowIfLengthsDontMatch(string format, int actualLength, int firstLength, int secondLength, int thirdLength = 1, int fourthLength = 1) + { + if (firstLength < 0 || secondLength < 0 || thirdLength < 0 || fourthLength < 0) + { + ThrowInvalidFormat(format); + } + + int expectedLength; + try + { + expectedLength = checked(firstLength * secondLength * thirdLength * fourthLength); + } + catch (OverflowException) + { + ThrowInvalidFormat(format); + return; + } + + if (expectedLength != actualLength) + { + ThrowInvalidFormat(format); + } + } + + [DoesNotReturn] + private static void ThrowInvalidFormat(string format) => throw new MessagePackSerializationException($"Invalid {format} format"); + } } diff --git a/tests/MessagePack.Tests/MultiDimensionalArrayTest.cs b/tests/MessagePack.Tests/MultiDimensionalArrayTest.cs index f4a0a2619..591e5472f 100644 --- a/tests/MessagePack.Tests/MultiDimensionalArrayTest.cs +++ b/tests/MessagePack.Tests/MultiDimensionalArrayTest.cs @@ -10,7 +10,7 @@ namespace MessagePack.Tests { - public class MultiDimensionalArrayTest + public class MultiDimensionalArrayTest(ITestOutputHelper logger) { private T Convert(T value) { @@ -67,5 +67,61 @@ public void MultiDimensional(int dataI, int dataJ, int dataK, int dataL) } } } + + [Fact] + [Trait("CWE", "789")] + public void RejectsTwoDimensionalArrayWithMismatchedElementCount() + { + byte[] payload = + { + 0x93, + 0xCE, 0x00, 0x00, 0x07, 0xD0, + 0xCE, 0x00, 0x00, 0x07, 0xD0, + 0x90, + }; + + AssertRejects(payload); + } + + [Fact] + [Trait("CWE", "789")] + public void RejectsThreeDimensionalArrayWithMismatchedElementCount() + { + byte[] payload = + { + 0x94, + 0xCC, 0x80, + 0xCC, 0x80, + 0xCC, 0x80, + 0x90, + }; + + AssertRejects(payload); + } + + [Fact] + [Trait("CWE", "789")] + public void RejectsFourDimensionalArrayWithMismatchedElementCount() + { + byte[] payload = + { + 0x95, + 0x20, + 0x20, + 0x20, + 0x20, + 0x90, + }; + + AssertRejects(payload); + } + + private void AssertRejects(byte[] payload) + { + var options = MessagePackSerializerOptions.Standard.WithSecurity(MessagePackSecurity.UntrustedData); + + var ex = Assert.Throws(() => MessagePackSerializer.Deserialize(payload, options)); + logger.WriteLine(ex.ToString()); + } } } From b414e6dffc7bd115003e7dd669417ee198cc4da8 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 29 Apr 2026 14:22:23 -0600 Subject: [PATCH 07/17] Guard dynamic union depth for CWE-674 DynamicUnionResolver emitted deserializers did not count the union formatter frame against MessagePackSecurity's object graph depth budget. That left recursive union values and skipped unknown union payloads less constrained than source-generated unions and dynamic object formatters. Emit DepthStep after nil handling and decrement reader.Depth before returning, matching the existing dynamic object formatter pattern. Add a regression test proving unknown union payloads respect the depth limit without including exploit payload details. --- .../Resolvers/DynamicUnionResolver.cs | 21 +++++++++- .../MessagePackSerializerTest.cs | 38 +++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/MessagePack/Resolvers/DynamicUnionResolver.cs b/src/MessagePack/Resolvers/DynamicUnionResolver.cs index 9281c6667..01fe027e7 100644 --- a/src/MessagePack/Resolvers/DynamicUnionResolver.cs +++ b/src/MessagePack/Resolvers/DynamicUnionResolver.cs @@ -321,6 +321,14 @@ private static void BuildDeserialize(Type type, UnionAttribute[] infos, MethodBu il.MarkLabel(falseLabel); + var reader = new ArgumentField(il, 1); + + // options.Security.DepthStep(ref reader); + il.EmitLdarg(2); + il.EmitCall(getSecurityFromOptions); + reader.EmitLdarg(); + il.EmitCall(securityDepthStep); + // IFormatterResolver resolver = options.Resolver; LocalBuilder localResolver = il.DeclareLocal(typeof(IFormatterResolver)); il.EmitLdarg(2); @@ -329,7 +337,6 @@ private static void BuildDeserialize(Type type, UnionAttribute[] infos, MethodBu // read-array header and validate, reader.ReadArrayHeader() != 2) throw; Label rightLabel = il.DefineLabel(); - var reader = new ArgumentField(il, 1); reader.EmitLdarg(); il.EmitCall(MessagePackReaderTypeInfo.ReadArrayHeader); il.EmitLdc_I4(2); @@ -396,6 +403,14 @@ private static void BuildDeserialize(Type type, UnionAttribute[] infos, MethodBu il.MarkLabel(loopEnd); + // reader.Depth--; + reader.EmitLdarg(); + il.Emit(OpCodes.Dup); + il.EmitCall(readerDepthGet); + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Sub_Ovf); + il.EmitCall(readerDepthSet); + il.Emit(OpCodes.Ldloc, result); il.Emit(OpCodes.Ret); } @@ -420,6 +435,10 @@ private static bool IsZeroStartSequential(UnionAttribute[] infos) private static readonly Type refKvp = typeof(KeyValuePair).MakeByRefType(); private static readonly MethodInfo getFormatterWithVerify = typeof(FormatterResolverExtensions).GetRuntimeMethods().First(x => x.Name == "GetFormatterWithVerify"); private static readonly MethodInfo getResolverFromOptions = typeof(MessagePackSerializerOptions).GetRuntimeProperty(nameof(MessagePackSerializerOptions.Resolver))!.GetMethod!; + private static readonly MethodInfo getSecurityFromOptions = typeof(MessagePackSerializerOptions).GetRuntimeProperty(nameof(MessagePackSerializerOptions.Security))!.GetMethod!; + private static readonly MethodInfo securityDepthStep = typeof(MessagePackSecurity).GetRuntimeMethod(nameof(MessagePackSecurity.DepthStep), new[] { typeof(MessagePackReader).MakeByRefType() })!; + private static readonly MethodInfo readerDepthGet = typeof(MessagePackReader).GetRuntimeProperty(nameof(MessagePackReader.Depth))!.GetMethod!; + private static readonly MethodInfo readerDepthSet = typeof(MessagePackReader).GetRuntimeProperty(nameof(MessagePackReader.Depth))!.SetMethod!; private static readonly Func getSerialize = t => typeof(IMessagePackFormatter<>).MakeGenericType(t).GetRuntimeMethod("Serialize", new[] { typeof(MessagePackWriter).MakeByRefType(), t, typeof(MessagePackSerializerOptions) })!; private static readonly Func getDeserialize = t => typeof(IMessagePackFormatter<>).MakeGenericType(t).GetRuntimeMethod("Deserialize", new[] { typeof(MessagePackReader).MakeByRefType(), typeof(MessagePackSerializerOptions) })!; diff --git a/tests/MessagePack.Tests/MessagePackSerializerTest.cs b/tests/MessagePack.Tests/MessagePackSerializerTest.cs index 211a35143..31f1a95e8 100644 --- a/tests/MessagePack.Tests/MessagePackSerializerTest.cs +++ b/tests/MessagePack.Tests/MessagePackSerializerTest.cs @@ -275,6 +275,18 @@ public void StackDepthCheck_DynamicObjectResolver() Assert.IsType(ex.InnerException); } + [Fact] + [Trait("CWE", "674")] + public void StackDepthCheck_DynamicUnionResolver() + { + byte[] msgpack = MessagePackSerializer.Serialize(new DepthCheckedUnionBranch()); + var options = MessagePackSerializerOptions.Standard + .WithSecurity(MessagePackSecurity.UntrustedData.WithMaximumObjectGraphDepth(1)); + + var ex = Assert.Throws(() => MessagePackSerializer.Deserialize(msgpack, options)); + Assert.IsType(ex.InnerException); + } + #endif private delegate void WriterHelper(ref MessagePackWriter writer); @@ -414,4 +426,30 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } } + + [MessagePackObject(keyAsPropertyName: true)] + public class SkipUnknownMemberTarget + { + public int Known { get; set; } + } + + [Union(0, typeof(DepthCheckedUnionLeaf))] + [Union(999, typeof(DepthCheckedUnionBranch))] + public interface IDepthCheckedUnionNode + { + } + + [MessagePackObject] + public class DepthCheckedUnionLeaf : IDepthCheckedUnionNode + { + [Key(0)] + public int Value { get; set; } + } + + [MessagePackObject] + public class DepthCheckedUnionBranch : IDepthCheckedUnionNode + { + [Key(0)] + public IDepthCheckedUnionNode Child { get; set; } + } } From f96fcf053fd2b22f4b13b0a68d8981fecc2fee1a Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 29 Apr 2026 14:18:40 -0600 Subject: [PATCH 08/17] Use secure lookup comparer for CWE-407 InterfaceLookupFormatter created its intermediate Dictionary with the default comparer, so ILookup deserialization did not honor MessagePackSecurity.UntrustedData hash-collision resistance. Pass the security-provided equality comparer into the intermediate dictionary and add regression coverage that verifies colliding long keys use the hardened comparer. --- src/MessagePack/Formatters/CollectionFormatter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MessagePack/Formatters/CollectionFormatter.cs b/src/MessagePack/Formatters/CollectionFormatter.cs index 2ed9bf563..cf1086b5b 100644 --- a/src/MessagePack/Formatters/CollectionFormatter.cs +++ b/src/MessagePack/Formatters/CollectionFormatter.cs @@ -928,7 +928,7 @@ protected override ILookup Complete(Dictionary> Create(int count, MessagePackSerializerOptions options) { - return new Dictionary>(count); + return new Dictionary>(count, options.Security.GetEqualityComparer()); } } From 0555f07cbf217773070a150c97cbbd2b3a37ac8d Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 29 Apr 2026 14:26:16 -0600 Subject: [PATCH 09/17] Validate Unity blit lengths for CWE-789 UnsafeBlitFormatter trusted the nested byte count from extension payloads when allocating arrays, even though the surrounding extension length had already bounded the body. Malformed inputs could request allocations that were not supported by the declared extension data. Parse the extension body through a bounded reader and reject negative, unaligned, or mismatched byte counts before allocating. Add a regression test for the malformed length case. --- .../MessagePack/Extension/UnsafeBlitFormatter.cs | 16 +++++++++++----- .../ExtensionTests/UnityShimTest.cs | 12 ++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/MessagePack.UnityClient/Assets/Scripts/MessagePack/Extension/UnsafeBlitFormatter.cs b/src/MessagePack.UnityClient/Assets/Scripts/MessagePack/Extension/UnsafeBlitFormatter.cs index 63f137a39..fdbe234fb 100644 --- a/src/MessagePack.UnityClient/Assets/Scripts/MessagePack/Extension/UnsafeBlitFormatter.cs +++ b/src/MessagePack.UnityClient/Assets/Scripts/MessagePack/Extension/UnsafeBlitFormatter.cs @@ -55,19 +55,25 @@ public unsafe void Serialize(ref MessagePackWriter writer, T[]? value, MessagePa return null; } - ExtensionHeader header = reader.ReadExtensionFormatHeader(); - if (header.TypeCode != this.TypeCode) + ExtensionResult extension = reader.ReadExtensionFormat(); + if (extension.TypeCode != this.TypeCode) { throw new InvalidOperationException("Invalid typeCode."); } - var byteLength = reader.ReadInt32(); - var isLittleEndian = reader.ReadBoolean(); + MessagePackReader extensionReader = reader.Clone(extension.Data); + var byteLength = extensionReader.ReadInt32(); + var isLittleEndian = extensionReader.ReadBoolean(); + long remainingBytes = extensionReader.Sequence.Length - extensionReader.Consumed; + if (byteLength < 0 || byteLength % sizeof(T) != 0 || byteLength != remainingBytes) + { + throw new MessagePackSerializationException("Invalid Unity blit extension length."); + } // Allocate a T[] that we will return. We'll then cast the T[] as byte[] so we can copy the byte sequence directly into it. var result = new T[byteLength / sizeof(T)]; Span resultAsBytes = MemoryMarshal.Cast(result); - reader.ReadRaw(byteLength).CopyTo(resultAsBytes); + extensionReader.ReadRaw(byteLength).CopyTo(resultAsBytes); // Reverse the byte order if necessary. if (isLittleEndian != BitConverter.IsLittleEndian && result.Length > 0) diff --git a/tests/MessagePack.Tests/ExtensionTests/UnityShimTest.cs b/tests/MessagePack.Tests/ExtensionTests/UnityShimTest.cs index 7e35b2448..899f839a7 100644 --- a/tests/MessagePack.Tests/ExtensionTests/UnityShimTest.cs +++ b/tests/MessagePack.Tests/ExtensionTests/UnityShimTest.cs @@ -94,6 +94,18 @@ public void EnsureSpecCompatibilityTest(BlitContainer data) EnsureSpecCompatibility(data.Array); } + [Fact] + [Trait("CWE", "789")] + public void BlitRejectsByteLengthThatExceedsExtensionBody() + { + MessagePackSerializerOptions options = MessagePackSerializerOptions.Standard.WithResolver(new WithUnityBlitResolver()); + byte[] payload = { 0xC7, 0x06, unchecked((byte)ReservedExtensionTypeCodes.UnityInt), 0xCE, 0x00, 0x00, 0x00, 0x08, 0xC3 }; + + var ex = Assert.Throws(() => MessagePackSerializer.Deserialize(payload, options)); + var inner = Assert.IsType(ex.InnerException); + Assert.Contains("Invalid Unity blit extension length", inner.Message); + } + public class WithUnityBlitResolver : IFormatterResolver { public IMessagePackFormatter GetFormatter() From b3af7cf7041e1bd33212c37d555cae4364407763 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 29 Apr 2026 14:29:15 -0600 Subject: [PATCH 10/17] Guard JSON conversion depth for CWE-674 ConvertFromJson now applies the configured MessagePackSecurity maximum object graph depth while translating nested JSON objects and arrays, preventing deeply nested input from recursing until stack exhaustion. Added a bounded regression test that verifies over-depth JSON is rejected for both compressed and uncompressed conversion paths. --- src/MessagePack/MessagePackSerializer.Json.cs | 21 +++++++++++++++++-- tests/MessagePack.Tests/ToJsonTest.cs | 13 ++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/MessagePack/MessagePackSerializer.Json.cs b/src/MessagePack/MessagePackSerializer.Json.cs index ebcddd555..5e5c2177c 100644 --- a/src/MessagePack/MessagePackSerializer.Json.cs +++ b/src/MessagePack/MessagePackSerializer.Json.cs @@ -187,6 +187,11 @@ public static void ConvertFromJson(TextReader reader, ref MessagePackWriter writ } private static uint FromJsonCore(TinyJsonReader jr, ref MessagePackWriter writer, MessagePackSerializerOptions options) + { + return FromJsonCore(jr, ref writer, options, 0); + } + + private static uint FromJsonCore(TinyJsonReader jr, ref MessagePackWriter writer, MessagePackSerializerOptions options, int depth) { uint count = 0; while (jr.Read()) @@ -196,11 +201,13 @@ private static uint FromJsonCore(TinyJsonReader jr, ref MessagePackWriter writer case TinyJsonToken.None: break; case TinyJsonToken.StartObject: + VerifyJsonObjectGraphDepth(options, depth); + // Set up a scratch area to serialize the collection since we don't know its length yet, which must be written first. using (var scratchRental = options.SequencePool.Rent()) { MessagePackWriter scratchWriter = writer.Clone(scratchRental.Value); - var mapCount = FromJsonCore(jr, ref scratchWriter, options); + var mapCount = FromJsonCore(jr, ref scratchWriter, options, depth + 1); scratchWriter.Flush(); mapCount = mapCount / 2; // remove propertyname string count. @@ -213,11 +220,13 @@ private static uint FromJsonCore(TinyJsonReader jr, ref MessagePackWriter writer case TinyJsonToken.EndObject: return count; // break case TinyJsonToken.StartArray: + VerifyJsonObjectGraphDepth(options, depth); + // Set up a scratch area to serialize the collection since we don't know its length yet, which must be written first. using (var scratchRental = options.SequencePool.Rent()) { MessagePackWriter scratchWriter = writer.Clone(scratchRental.Value); - var arrayCount = FromJsonCore(jr, ref scratchWriter, options); + var arrayCount = FromJsonCore(jr, ref scratchWriter, options, depth + 1); scratchWriter.Flush(); writer.WriteArrayHeader(arrayCount); @@ -273,6 +282,14 @@ private static uint FromJsonCore(TinyJsonReader jr, ref MessagePackWriter writer return count; } + private static void VerifyJsonObjectGraphDepth(MessagePackSerializerOptions options, int depth) + { + if (depth >= options.Security.MaximumObjectGraphDepth) + { + throw new InsufficientExecutionStackException($"This JSON sequence has an object graph that exceeds the maximum depth allowed of {options.Security.MaximumObjectGraphDepth}."); + } + } + private static void ToJsonCore(ref MessagePackReader reader, TextWriter writer, MessagePackSerializerOptions options) { MessagePackType type = reader.NextMessagePackType; diff --git a/tests/MessagePack.Tests/ToJsonTest.cs b/tests/MessagePack.Tests/ToJsonTest.cs index 8069ea266..c2e3be2bb 100644 --- a/tests/MessagePack.Tests/ToJsonTest.cs +++ b/tests/MessagePack.Tests/ToJsonTest.cs @@ -47,6 +47,19 @@ public void ComplexToJson() this.JsonConvert(json, LZ4Standard).Is(json); } + [Theory] + [InlineData(false)] + [InlineData(true)] + [Trait("CWE", "674")] + public void ConvertFromJsonRejectsExcessiveNesting(bool compression) + { + var options = MessagePackSerializerOptions.Standard + .WithCompression(compression ? MessagePackCompression.Lz4Block : MessagePackCompression.None) + .WithSecurity(MessagePackSecurity.UntrustedData.WithMaximumObjectGraphDepth(3)); + + Assert.Throws(() => MessagePackSerializer.ConvertFromJson("[[[[1]]]]", options)); + } + [Fact] public void FloatJson() { From 66ad0894bcbddc696e9dd77410e5ea4c6291f696 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 29 Apr 2026 14:32:00 -0600 Subject: [PATCH 11/17] Avoid JSON separator recursion for CWE-674 TinyJsonReader previously skipped JSON separators with a recursive ReadNextToken call, so a long separator sequence supplied to ConvertFromJson could consume one stack frame per separator and terminate the process. Change separator skipping to stay within the tokenizer loop instead, preserving the existing tokenization behavior without stack growth. Add regression coverage that converts a long separator-prefixed JSON value successfully. --- src/MessagePack/Internal/TinyJsonReader.cs | 108 +++++++++++---------- tests/MessagePack.Tests/ToJsonTest.cs | 10 ++ 2 files changed, 65 insertions(+), 53 deletions(-) diff --git a/src/MessagePack/Internal/TinyJsonReader.cs b/src/MessagePack/Internal/TinyJsonReader.cs index dd386177c..67dbf01e8 100644 --- a/src/MessagePack/Internal/TinyJsonReader.cs +++ b/src/MessagePack/Internal/TinyJsonReader.cs @@ -137,62 +137,64 @@ private static bool IsWordBreak(char c) private void ReadNextToken() { - this.SkipWhiteSpace(); - - var intChar = this.reader.Peek(); - if (intChar == -1) + while (true) { - this.TokenType = TinyJsonToken.None; - return; - } + this.SkipWhiteSpace(); - var c = (char)intChar; - switch (c) - { - case '{': - this.TokenType = TinyJsonToken.StartObject; - return; - case '}': - this.TokenType = TinyJsonToken.EndObject; - return; - case '[': - this.TokenType = TinyJsonToken.StartArray; - return; - case ']': - this.TokenType = TinyJsonToken.EndArray; - return; - case '"': - this.TokenType = TinyJsonToken.String; - return; - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - case '-': - this.TokenType = TinyJsonToken.Number; - return; - case 't': - this.TokenType = TinyJsonToken.True; - return; - case 'f': - this.TokenType = TinyJsonToken.False; - return; - case 'n': - this.TokenType = TinyJsonToken.Null; - return; - case ',': - case ':': - this.reader.Read(); - this.ReadNextToken(); + var intChar = this.reader.Peek(); + if (intChar == -1) + { + this.TokenType = TinyJsonToken.None; return; - default: - throw new TinyJsonException("Invalid String:" + c); + } + + var c = (char)intChar; + switch (c) + { + case '{': + this.TokenType = TinyJsonToken.StartObject; + return; + case '}': + this.TokenType = TinyJsonToken.EndObject; + return; + case '[': + this.TokenType = TinyJsonToken.StartArray; + return; + case ']': + this.TokenType = TinyJsonToken.EndArray; + return; + case '"': + this.TokenType = TinyJsonToken.String; + return; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '-': + this.TokenType = TinyJsonToken.Number; + return; + case 't': + this.TokenType = TinyJsonToken.True; + return; + case 'f': + this.TokenType = TinyJsonToken.False; + return; + case 'n': + this.TokenType = TinyJsonToken.Null; + return; + case ',': + case ':': + this.reader.Read(); + continue; + default: + throw new TinyJsonException("Invalid String:" + c); + } } } diff --git a/tests/MessagePack.Tests/ToJsonTest.cs b/tests/MessagePack.Tests/ToJsonTest.cs index 8069ea266..d65acb5ca 100644 --- a/tests/MessagePack.Tests/ToJsonTest.cs +++ b/tests/MessagePack.Tests/ToJsonTest.cs @@ -47,6 +47,16 @@ public void ComplexToJson() this.JsonConvert(json, LZ4Standard).Is(json); } + [Fact] + [Trait("CWE", "674")] + public void ConvertFromJsonSkipsLongSeparatorRunIteratively() + { + var json = new string(',', 200_000) + "null"; + var msgpack = MessagePackSerializer.ConvertFromJson(json); + + MessagePackSerializer.ConvertToJson(msgpack).Is("null"); + } + [Fact] public void FloatJson() { From 082ba7daba9b6a1acda945d0840abf9903d53cc2 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 29 Apr 2026 14:35:35 -0600 Subject: [PATCH 12/17] Guard typeless JSON depth for CWE-674 ConvertToJson recursively expands typeless extension values while composing JSON, but that path was not counted against MessagePackSecurity.MaximumObjectGraphDepth. Deeply nested typeless values could therefore bypass UntrustedData depth checks and exhaust the call stack. Wrap typeless extension JSON processing in DepthStep/decrement accounting and add regression coverage that expects the configured depth limit to be enforced. --- src/MessagePack/MessagePackSerializer.Json.cs | 68 +++++++++++-------- .../MessagePackSerializerTest.cs | 34 ++++++++++ 2 files changed, 72 insertions(+), 30 deletions(-) diff --git a/src/MessagePack/MessagePackSerializer.Json.cs b/src/MessagePack/MessagePackSerializer.Json.cs index ebcddd555..9edda47fd 100644 --- a/src/MessagePack/MessagePackSerializer.Json.cs +++ b/src/MessagePack/MessagePackSerializer.Json.cs @@ -395,46 +395,54 @@ private static void ToJsonCore(ref MessagePackReader reader, TextWriter writer, } else if (extHeader.TypeCode == ReservedExtensionTypeCodes.TypelessFormatter) { - // prepare type name token - var privateBuilder = new StringBuilder(); - var typeNameTokenBuilder = new StringBuilder(); - SequencePosition positionBeforeTypeNameRead = reader.Position; - ToJsonCore(ref reader, new StringWriter(typeNameTokenBuilder), options); - int typeNameReadSize = (int)reader.Sequence.Slice(positionBeforeTypeNameRead, reader.Position).Length; - if (extHeader.Length > typeNameReadSize) + options.Security.DepthStep(ref reader); + try { - // object map or array - MessagePackType typeInside = reader.NextMessagePackType; - if (typeInside != MessagePackType.Array && typeInside != MessagePackType.Map) + // prepare type name token + var privateBuilder = new StringBuilder(); + var typeNameTokenBuilder = new StringBuilder(); + SequencePosition positionBeforeTypeNameRead = reader.Position; + ToJsonCore(ref reader, new StringWriter(typeNameTokenBuilder), options); + int typeNameReadSize = (int)reader.Sequence.Slice(positionBeforeTypeNameRead, reader.Position).Length; + if (extHeader.Length > typeNameReadSize) { - privateBuilder.Append("{"); - } + // object map or array + MessagePackType typeInside = reader.NextMessagePackType; + if (typeInside != MessagePackType.Array && typeInside != MessagePackType.Map) + { + privateBuilder.Append("{"); + } - ToJsonCore(ref reader, new StringWriter(privateBuilder), options); + ToJsonCore(ref reader, new StringWriter(privateBuilder), options); - // insert type name token to start of object map or array - if (typeInside != MessagePackType.Array) - { - typeNameTokenBuilder.Insert(0, "\"$type\":"); - } + // insert type name token to start of object map or array + if (typeInside != MessagePackType.Array) + { + typeNameTokenBuilder.Insert(0, "\"$type\":"); + } - if (typeInside != MessagePackType.Array && typeInside != MessagePackType.Map) - { - privateBuilder.Append("}"); - } + if (typeInside != MessagePackType.Array && typeInside != MessagePackType.Map) + { + privateBuilder.Append("}"); + } - if (privateBuilder.Length > 2) - { - typeNameTokenBuilder.Append(","); - } + if (privateBuilder.Length > 2) + { + typeNameTokenBuilder.Append(","); + } - privateBuilder.Insert(1, typeNameTokenBuilder.ToString()); + privateBuilder.Insert(1, typeNameTokenBuilder.ToString()); - writer.Write(privateBuilder.ToString()); + writer.Write(privateBuilder.ToString()); + } + else + { + writer.Write("{\"$type\":" + typeNameTokenBuilder.ToString() + "}"); + } } - else + finally { - writer.Write("{\"$type\":" + typeNameTokenBuilder.ToString() + "}"); + reader.Depth--; } } else diff --git a/tests/MessagePack.Tests/MessagePackSerializerTest.cs b/tests/MessagePack.Tests/MessagePackSerializerTest.cs index 211a35143..81b2b6cba 100644 --- a/tests/MessagePack.Tests/MessagePackSerializerTest.cs +++ b/tests/MessagePack.Tests/MessagePackSerializerTest.cs @@ -209,6 +209,18 @@ public async Task SerializeAndDeserializeAsync_MultipleValues_SeekableStream(boo Assert.Equal(3, await MessagePackSerializer.DeserializeAsync(stream)); } + [Fact] + [Trait("CWE", "674")] + public void StackDepthCheck_ConvertToJsonTypelessExtension() + { + const int maxDepth = 3; + byte[] msgpack = BuildNestedTypelessExtension(maxDepth + 1); + var options = MessagePackSerializerOptions.Standard + .WithSecurity(MessagePackSecurity.UntrustedData.WithMaximumObjectGraphDepth(maxDepth)); + + AssertConvertToJsonRecursionCheckThrows(new ReadOnlySequence(msgpack), options); + } + [Theory] [InlineData(true)] [InlineData(false)] @@ -318,6 +330,28 @@ private static void AssertConvertToJsonRecursionCheckThrows(ReadOnlySequence(ex.InnerException); } + private static byte[] BuildNestedTypelessExtension(int levels) + { + byte[] msgpack = new byte[(levels * 6) + 2]; + int offset = msgpack.Length; + msgpack[--offset] = (byte)'x'; + msgpack[--offset] = 0xa1; + int innerLength = 2; + + for (int level = 0; level < levels; level++) + { + msgpack[--offset] = unchecked((byte)ReservedExtensionTypeCodes.TypelessFormatter); + msgpack[--offset] = (byte)innerLength; + msgpack[--offset] = (byte)(innerLength >> 8); + msgpack[--offset] = (byte)(innerLength >> 16); + msgpack[--offset] = (byte)(innerLength >> 24); + msgpack[--offset] = 0xc9; + innerLength += 6; + } + + return msgpack; + } + [DataContract] public class RecursiveObjectGraph { From 46c6a0fe2f2403a282d2e5de2771f242f963044b Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 29 Apr 2026 14:43:06 -0600 Subject: [PATCH 13/17] Fix CWE-190 map header length overflow Promote map entry count multiplication to long when validating the minimum encoded payload length and when skipping map contents. This keeps oversized malformed map headers on the insufficient-buffer path instead of relying on Int32 arithmetic behavior. Add regression coverage for oversized map headers in ReadMapHeader and Skip. --- src/MessagePack/MessagePackReader.cs | 2 +- .../MessagePackReaderTests.cs | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/MessagePack/MessagePackReader.cs b/src/MessagePack/MessagePackReader.cs index b7eebae74..6383ff667 100644 --- a/src/MessagePack/MessagePackReader.cs +++ b/src/MessagePack/MessagePackReader.cs @@ -371,7 +371,7 @@ public int ReadMapHeader() // Protect against corrupted or mischievous data that may lead to allocating way too much memory. // We allow for each primitive to be the minimal 1 byte in size, and we have a key=value map, so that's 2 bytes. // Formatters that know each element is larger can optionally add a stronger check. - ThrowInsufficientBufferUnless(this.reader.Remaining >= count * 2); + ThrowInsufficientBufferUnless(this.reader.Remaining >= (long)count * 2); return count; } diff --git a/tests/MessagePack.Tests/MessagePackReaderTests.cs b/tests/MessagePack.Tests/MessagePackReaderTests.cs index 7ac748023..0957d8f76 100644 --- a/tests/MessagePack.Tests/MessagePackReaderTests.cs +++ b/tests/MessagePack.Tests/MessagePackReaderTests.cs @@ -101,6 +101,32 @@ public void ReadMapHeader_MitigatesLargeAllocations() }); } + [Fact] + [Trait("CWE", "190")] + public void ReadMapHeader_MitigatesLargeAllocations_WhenMinimumPayloadLengthOverflowsInt32() + { + byte[] msgpack = { MessagePackCode.Map32, 0x40, 0, 0, 0 }; + + Assert.Throws(() => + { + var reader = new MessagePackReader(msgpack); + reader.ReadMapHeader(); + }); + } + + [Fact] + [Trait("CWE", "190")] + public void SkipMap_MitigatesLargeAllocations_WhenMinimumPayloadLengthOverflowsInt32() + { + byte[] msgpack = { MessagePackCode.Map32, 0x40, 0, 0, 0 }; + + Assert.Throws(() => + { + var reader = new MessagePackReader(msgpack); + reader.Skip(); + }); + } + [Fact] public void TryReadMapHeader() { From 25a3493e6c531203221e56b2e862ce628cd9acaf Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 29 Apr 2026 14:39:01 -0600 Subject: [PATCH 14/17] Limit untrusted ExpandoObject maps for CWE-407 ExpandoObject map materialization inserts each member through ExpandoObject, whose member table grows with linear scans and array copies. Under the untrusted-data resolver preset, very large maps could consume disproportionate CPU before application code sees the result. Reject oversized ExpandoObject maps when hash-collision hardening is active, covering both direct ExpandoObject deserialization and nested maps produced by ExpandoObjectResolver. Add focused regression coverage for both paths. --- .../Formatters/ExpandoObjectFormatter.cs | 11 +++++++ .../Resolvers/ExpandoObjectResolver.cs | 1 + tests/MessagePack.Tests/ExpandoObjectTests.cs | 30 +++++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/src/MessagePack/Formatters/ExpandoObjectFormatter.cs b/src/MessagePack/Formatters/ExpandoObjectFormatter.cs index 2d9328880..98932401f 100644 --- a/src/MessagePack/Formatters/ExpandoObjectFormatter.cs +++ b/src/MessagePack/Formatters/ExpandoObjectFormatter.cs @@ -8,6 +8,8 @@ namespace MessagePack.Formatters { public class ExpandoObjectFormatter : IMessagePackFormatter { + internal const int MaximumUntrustedDataMemberCount = 1024; + public static readonly IMessagePackFormatter Instance = new ExpandoObjectFormatter(); private ExpandoObjectFormatter() @@ -23,6 +25,7 @@ private ExpandoObjectFormatter() var result = new ExpandoObject(); int count = reader.ReadMapHeader(); + ThrowIfMapTooLargeForUntrustedData(count, options); if (count > 0) { IFormatterResolver resolver = options.Resolver; @@ -49,6 +52,14 @@ private ExpandoObjectFormatter() return result; } + internal static void ThrowIfMapTooLargeForUntrustedData(int count, MessagePackSerializerOptions options) + { + if (options.Security.HashCollisionResistant && count > MaximumUntrustedDataMemberCount) + { + throw new MessagePackSerializationException($"ExpandoObject map size exceeds the limit of {MaximumUntrustedDataMemberCount} entries allowed under untrusted data security mode."); + } + } + public void Serialize(ref MessagePackWriter writer, ExpandoObject? value, MessagePackSerializerOptions options) { if (value is null) diff --git a/src/MessagePack/Resolvers/ExpandoObjectResolver.cs b/src/MessagePack/Resolvers/ExpandoObjectResolver.cs index ffc7b149c..11107efac 100644 --- a/src/MessagePack/Resolvers/ExpandoObjectResolver.cs +++ b/src/MessagePack/Resolvers/ExpandoObjectResolver.cs @@ -40,6 +40,7 @@ private class PrimitiveObjectWithExpandoMaps : PrimitiveObjectFormatter { protected override object DeserializeMap(ref MessagePackReader reader, int length, MessagePackSerializerOptions options) { + ExpandoObjectFormatter.ThrowIfMapTooLargeForUntrustedData(length, options); IMessagePackFormatter keyFormatter = options.Resolver.GetFormatterWithVerify(); IMessagePackFormatter? objectFormatter = options.Resolver.GetFormatterWithVerify(); IDictionary dictionary = new ExpandoObject(); diff --git a/tests/MessagePack.Tests/ExpandoObjectTests.cs b/tests/MessagePack.Tests/ExpandoObjectTests.cs index 61ae7b0bf..40546e58e 100644 --- a/tests/MessagePack.Tests/ExpandoObjectTests.cs +++ b/tests/MessagePack.Tests/ExpandoObjectTests.cs @@ -3,6 +3,7 @@ #if !UNITY_2018_3_OR_NEWER +using System.Collections.Generic; using System.Dynamic; using System.Runtime.Serialization; using MessagePack.Resolvers; @@ -76,6 +77,24 @@ public void ExpandoObject_DeepGraphContainsCustomTypes() Assert.Equal(expando.Other.OtherProperty, expando2.Other.OtherProperty); } + [Fact] + [Trait("CWE", "407")] + public void ExpandoObject_UntrustedDataRejectsLargeMaps() + { + byte[] msgpack = CreateMapWithNilValues(1025); + + Assert.Throws(() => MessagePackSerializer.Deserialize(msgpack, ExpandoObjectResolver.Options)); + } + + [Fact] + [Trait("CWE", "407")] + public void ExpandoObjectNestedMap_UntrustedDataRejectsLargeMaps() + { + byte[] msgpack = CreateMapWithNilValues(1025); + + Assert.Throws(() => MessagePackSerializer.Deserialize(msgpack, ExpandoObjectResolver.Options)); + } + #if !UNITY_2018_3_OR_NEWER [Fact] @@ -106,6 +125,17 @@ public class CustomObject [DataMember] public string OtherProperty { get; set; } } + + private static byte[] CreateMapWithNilValues(int count) + { + var dictionary = new Dictionary(); + for (int index = 0; index < count; index++) + { + dictionary.Add("k" + index.ToString(System.Globalization.CultureInfo.InvariantCulture), null); + } + + return MessagePackSerializer.Serialize(dictionary, MessagePackSerializerOptions.Standard); + } } } From f077798ea3998ddc937056dd53afa2d0fc679d81 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 29 Apr 2026 14:50:01 -0600 Subject: [PATCH 15/17] Default MVC input formatter to UntrustedData for CWE-1188 MessagePackInputFormatter handles HTTP request bodies, but its default/null options path fell back to MessagePackSerializerOptions.Standard and therefore TrustedData. That left hash-based model binding without the untrusted-data collision-resistance defaults expected at this trust boundary. Default null input-formatter options to Standard.WithSecurity(UntrustedData), while preserving caller-supplied options. Add a regression test that verifies the parameterless formatter deserializes dictionary request bodies with the collision-resistant comparer. --- .../MessagePackInputFormatter.cs | 7 +++-- .../AspNetCoreMvcFormatterTest.cs | 29 +++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/MessagePack.AspNetCoreMvcFormatter/MessagePackInputFormatter.cs b/src/MessagePack.AspNetCoreMvcFormatter/MessagePackInputFormatter.cs index c2038f5a0..f84b4bf7a 100644 --- a/src/MessagePack.AspNetCoreMvcFormatter/MessagePackInputFormatter.cs +++ b/src/MessagePack.AspNetCoreMvcFormatter/MessagePackInputFormatter.cs @@ -9,16 +9,17 @@ namespace MessagePack.AspNetCoreMvcFormatter public class MessagePackInputFormatter : InputFormatter { private const string ContentType = "application/x-msgpack"; - private readonly MessagePackSerializerOptions? options; + private static readonly MessagePackSerializerOptions DefaultOptions = MessagePackSerializerOptions.Standard.WithSecurity(MessagePackSecurity.UntrustedData); + private readonly MessagePackSerializerOptions options; public MessagePackInputFormatter() - : this(null) + : this(DefaultOptions) { } public MessagePackInputFormatter(MessagePackSerializerOptions? options) { - this.options = options; + this.options = options ?? DefaultOptions; SupportedMediaTypes.Add(ContentType); } diff --git a/tests/MessagePack.AspNetCoreMvcFormatter.Tests/AspNetCoreMvcFormatterTest.cs b/tests/MessagePack.AspNetCoreMvcFormatter.Tests/AspNetCoreMvcFormatterTest.cs index 96b54b315..ed2219b1e 100644 --- a/tests/MessagePack.AspNetCoreMvcFormatter.Tests/AspNetCoreMvcFormatterTest.cs +++ b/tests/MessagePack.AspNetCoreMvcFormatter.Tests/AspNetCoreMvcFormatterTest.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; +using System.Collections.Generic; using System.IO; using System.Text; using System.Threading; @@ -195,6 +196,34 @@ public void MessagePackInputFormatterSupportsXMsgPack() inputFormatter.SupportedMediaTypes.Is(MsgPackContentType); } + [Fact] + public async Task MessagePackInputFormatterDefaultsToUntrustedData() + { + const long value1 = 0x100000001; + const long value2 = 0x200000002; + + var messagePackBinary = MessagePackSerializer.Serialize(new Dictionary + { + [value1] = 1, + [value2] = 2, + }); + + var httpContext = new DefaultHttpContext(); + httpContext.Features.Set(new TestResponseFeature()); + httpContext.Request.Body = new NonSeekableReadStream(messagePackBinary); + httpContext.Request.ContentType = MsgPackContentType; + + InputFormatterContext inputFormatterContext = this.CreateInputFormatterContext(typeof(Dictionary), httpContext); + var inputFormatter = new MessagePackInputFormatter(); + + InputFormatterResult result = await inputFormatter.ReadAsync(inputFormatterContext); + + Assert.False(result.HasError); + var dictionary = Assert.IsType>(result.Model); + Assert.Equal(EqualityComparer.Default.GetHashCode(value1), EqualityComparer.Default.GetHashCode(value2)); + Assert.NotEqual(dictionary.Comparer.GetHashCode(value1), dictionary.Comparer.GetHashCode(value2)); + } + /// /// JsonOutputFormatterTests.cs#L453. /// From 2b5a500ac56c2ae59183337e93cbe7dd1a5a6164 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 29 Apr 2026 14:46:53 -0600 Subject: [PATCH 16/17] Guard LZ4 decompression length for CWE-409 Validate declared LZ4 decompressed block sizes against the compressed block size before requesting an output buffer. This rejects unreasonable Lz4Block and Lz4BlockArray declarations before allocation while preserving normal compressed payload handling. --- src/MessagePack/MessagePackSecurity.cs | 33 +++++++ src/MessagePack/MessagePackSerializer.Json.cs | 2 +- src/MessagePack/MessagePackSerializer.cs | 16 +++- tests/MessagePack.Tests/LZ4Test.cs | 91 +++++++++++++++++++ .../MessagePackSecurityTests.cs | 11 +++ 5 files changed, 150 insertions(+), 3 deletions(-) diff --git a/src/MessagePack/MessagePackSecurity.cs b/src/MessagePack/MessagePackSecurity.cs index 7b3fe62b0..515d8a024 100644 --- a/src/MessagePack/MessagePackSecurity.cs +++ b/src/MessagePack/MessagePackSecurity.cs @@ -19,6 +19,8 @@ namespace MessagePack /// public class MessagePackSecurity { + private const int DefaultUntrustedDataMaximumDecompressedSize = 64 * 1024 * 1024; + /// /// Gets an instance preconfigured with settings that omit hash collision resistance protections. /// Useful for deserializing fully-trusted and valid msgpack sequences. @@ -27,6 +29,7 @@ public class MessagePackSecurity { HashCollisionResistant = false, MaximumObjectGraphDepth = 500, + MaximumDecompressedSize = int.MaxValue, }; /// @@ -36,6 +39,7 @@ public class MessagePackSecurity { HashCollisionResistant = true, MaximumObjectGraphDepth = 500, + MaximumDecompressedSize = DefaultUntrustedDataMaximumDecompressedSize, }; private static readonly SipHash Hash = new(); @@ -62,6 +66,7 @@ protected MessagePackSecurity(MessagePackSecurity copyFrom) this.HashCollisionResistant = copyFrom.HashCollisionResistant; this.MaximumObjectGraphDepth = copyFrom.MaximumObjectGraphDepth; + this.MaximumDecompressedSize = copyFrom.MaximumDecompressedSize; } /// @@ -86,6 +91,12 @@ protected MessagePackSecurity(MessagePackSecurity copyFrom) /// public int MaximumObjectGraphDepth { get; private set; } = 500; + /// + /// Gets the maximum decompressed size in bytes allowed when deserializing compressed payloads. + /// + /// The default value is for and 64MB for . + public int MaximumDecompressedSize { get; private set; } = int.MaxValue; + /// /// Gets a copy of these options with the property set to a new value. /// @@ -103,6 +114,28 @@ public MessagePackSecurity WithMaximumObjectGraphDepth(int maximumObjectGraphDep return clone; } + /// + /// Gets a copy of these options with the property set to a new value. + /// + /// The new value for the property. Must not be negative. + /// The new instance; or the original if the value is unchanged. + public MessagePackSecurity WithMaximumDecompressedSize(int maximumDecompressedSize) + { + if (this.MaximumDecompressedSize == maximumDecompressedSize) + { + return this; + } + + if (maximumDecompressedSize < 0) + { + throw new ArgumentOutOfRangeException(nameof(maximumDecompressedSize)); + } + + var clone = this.Clone(); + clone.MaximumDecompressedSize = maximumDecompressedSize; + return clone; + } + /// /// Gets a copy of these options with the property set to a new value. /// diff --git a/src/MessagePack/MessagePackSerializer.Json.cs b/src/MessagePack/MessagePackSerializer.Json.cs index ebcddd555..a012aab44 100644 --- a/src/MessagePack/MessagePackSerializer.Json.cs +++ b/src/MessagePack/MessagePackSerializer.Json.cs @@ -92,7 +92,7 @@ public static void ConvertToJson(ref MessagePackReader reader, TextWriter jsonWr { using (var scratchRental = options.SequencePool.Rent()) { - if (TryDecompress(ref reader, scratchRental.Value)) + if (TryDecompress(ref reader, scratchRental.Value, options)) { var scratchReader = new MessagePackReader(scratchRental.Value) { diff --git a/src/MessagePack/MessagePackSerializer.cs b/src/MessagePack/MessagePackSerializer.cs index 3cbb44cec..98764cde6 100644 --- a/src/MessagePack/MessagePackSerializer.cs +++ b/src/MessagePack/MessagePackSerializer.cs @@ -233,7 +233,7 @@ public static T Deserialize(ref MessagePackReader reader, MessagePackSerializ using (var msgPackUncompressedRental = options.SequencePool.Rent()) { var msgPackUncompressed = msgPackUncompressedRental.Value; - if (TryDecompress(ref reader, msgPackUncompressed)) + if (TryDecompress(ref reader, msgPackUncompressed, options)) { MessagePackReader uncompressedReader = reader.Clone(msgPackUncompressed.AsReadOnlySequence); return options.Resolver.GetFormatterWithVerify().Deserialize(ref uncompressedReader, options); @@ -481,7 +481,7 @@ private static int LZ4Operation(in ReadOnlySequence input, Span outp } } - private static bool TryDecompress(ref MessagePackReader reader, IBufferWriter writer) + private static bool TryDecompress(ref MessagePackReader reader, IBufferWriter writer, MessagePackSerializerOptions options) { if (!reader.End) { @@ -503,6 +503,7 @@ private static bool TryDecompress(ref MessagePackReader reader, IBufferWriter compressedData = extReader.Sequence.Slice(extReader.Position); + ThrowIfInvalidLz4BlockLength(uncompressedLength, options.Security.MaximumDecompressedSize); Span uncompressedSpan = writer.GetSpan(uncompressedLength).Slice(0, uncompressedLength); int actualUncompressedLength = LZ4Operation(compressedData, uncompressedSpan, LZ4CodecDecode); Debug.Assert(actualUncompressedLength == uncompressedLength, "Unexpected length of uncompressed data."); @@ -529,6 +530,7 @@ private static bool TryDecompress(ref MessagePackReader reader, IBufferWriter.Shared.Rent(sequenceCount); try { + long remainingMaxDecompressedSize = options.Security.MaximumDecompressedSize; for (int i = 0; i < sequenceCount; i++) { uncompressedLengths[i] = reader.ReadInt32(); @@ -538,6 +540,8 @@ private static bool TryDecompress(ref MessagePackReader reader, IBufferWriter lz4Block = reader.ReadBytes() ?? throw MessagePackSerializationException.ThrowUnexpectedNilWhileDeserializing>(); + ThrowIfInvalidLz4BlockLength(uncompressedLength, remainingMaxDecompressedSize); + remainingMaxDecompressedSize -= uncompressedLength; Span uncompressedSpan = writer.GetSpan(uncompressedLength).Slice(0, uncompressedLength); var actualUncompressedLength = LZ4Operation(lz4Block, uncompressedSpan, LZ4CodecDecode); Debug.Assert(actualUncompressedLength == uncompressedLength, "Unexpected length of uncompressed data."); @@ -558,6 +562,14 @@ private static bool TryDecompress(ref MessagePackReader reader, IBufferWriter remainingMaxDecompressedSize) + { + throw new MessagePackSerializationException("LZ4 block declares a decompressed length that exceeds the configured maximum."); + } + } + private static void ToLZ4BinaryCore(in ReadOnlySequence msgpackUncompressedData, ref MessagePackWriter writer, MessagePackCompression compression, int minCompressionSize) { if (msgpackUncompressedData.Length < minCompressionSize) diff --git a/tests/MessagePack.Tests/LZ4Test.cs b/tests/MessagePack.Tests/LZ4Test.cs index f314b572a..eb80bb2a2 100644 --- a/tests/MessagePack.Tests/LZ4Test.cs +++ b/tests/MessagePack.Tests/LZ4Test.cs @@ -1,6 +1,7 @@ // Copyright (c) All contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Buffers; using System.Linq; using Xunit; using Xunit.Abstractions; @@ -22,6 +23,64 @@ public LZ4Test(ITestOutputHelper logger) #endif + [Theory] + [InlineData(MessagePackCompression.Lz4Block)] + [InlineData(MessagePackCompression.Lz4BlockArray)] + [Trait("CWE", "409")] + public void Lz4RejectsDeclaredOutputOverMaximumBeforeAllocating(MessagePackCompression compression) + { + byte[] payload = compression == MessagePackCompression.Lz4Block + ? [0xC7, 0x06, 0x63, 0xD2, 0x7F, 0xFF, 0xFF, 0xFF, 0x00] + : [0x92, 0xC7, 0x05, 0x62, 0xD2, 0x7F, 0xFF, 0xFF, 0xFF, 0xC4, 0x01, 0x00]; + var arrayPool = new ThrowingArrayPool(1024); + var options = MessagePackSerializerOptions.Standard + .WithCompression(compression) + .WithSecurity(MessagePackSecurity.UntrustedData) + .WithPool(new SequencePool(1, arrayPool)); + + MessagePackSerializationException ex = Assert.Throws(() => MessagePackSerializer.Deserialize(payload, options)); + + Assert.Contains("exceeds the configured maximum", FlattenMessages(ex)); + Assert.Null(arrayPool.LargestRequestedLength); + } + + [Fact] + [Trait("CWE", "409")] + public void Lz4BlockArrayRejectsTotalDeclaredOutputOverMaximum() + { + byte[] payload = + [ + 0x93, + 0xC7, 0x02, 0x62, 0x04, 0x04, + 0xC4, 0x05, 0x40, 0x20, 0x20, 0x20, 0x20, + 0xC4, 0x05, 0x40, 0x20, 0x20, 0x20, 0x20, + ]; + var options = MessagePackSerializerOptions.Standard + .WithCompression(MessagePackCompression.Lz4BlockArray) + .WithSecurity(MessagePackSecurity.UntrustedData.WithMaximumDecompressedSize(7)); + + MessagePackSerializationException ex = Assert.Throws(() => MessagePackSerializer.Deserialize(payload, options)); + + Assert.Contains("exceeds the configured maximum", FlattenMessages(ex)); + } + + [Theory] + [InlineData(MessagePackCompression.Lz4Block)] + [InlineData(MessagePackCompression.Lz4BlockArray)] + [Trait("CWE", "409")] + public void Lz4AllowsHighlyCompressiblePayloadWithinMaximum(MessagePackCompression compression) + { + string data = new string(' ', 100_000); + var options = MessagePackSerializerOptions.Standard + .WithCompression(compression) + .WithSecurity(MessagePackSecurity.UntrustedData.WithMaximumDecompressedSize(200_000)); + + byte[] payload = MessagePackSerializer.Serialize(data, options); + string actual = MessagePackSerializer.Deserialize(payload, options); + + Assert.Equal(data, actual); + } + [Fact] public void Lz4Compress() { @@ -82,5 +141,37 @@ private static void SequenceStructuralEqual(SharedData.SimpleStringKeyData[] act actual[i].Prop3.Is(expected[i].Prop3); } } + + private static string FlattenMessages(System.Exception ex) + { + return ex.InnerException is null ? ex.Message : ex.Message + " " + FlattenMessages(ex.InnerException); + } + + private class ThrowingArrayPool : ArrayPool + { + private readonly int maximumLength; + + internal ThrowingArrayPool(int maximumLength) + { + this.maximumLength = maximumLength; + } + + internal int? LargestRequestedLength { get; private set; } + + public override byte[] Rent(int minimumLength) + { + this.LargestRequestedLength = this.LargestRequestedLength.HasValue ? System.Math.Max(this.LargestRequestedLength.Value, minimumLength) : minimumLength; + if (minimumLength > this.maximumLength) + { + throw new System.InvalidOperationException("Unexpected decompression allocation request: " + minimumLength); + } + + return new byte[minimumLength]; + } + + public override void Return(byte[] array, bool clearArray = false) + { + } + } } } diff --git a/tests/MessagePack.Tests/MessagePackSecurityTests.cs b/tests/MessagePack.Tests/MessagePackSecurityTests.cs index 499425777..b517810b8 100644 --- a/tests/MessagePack.Tests/MessagePackSecurityTests.cs +++ b/tests/MessagePack.Tests/MessagePackSecurityTests.cs @@ -22,12 +22,14 @@ public MessagePackSecurityTests(ITestOutputHelper logger) public void Untrusted() { Assert.True(MessagePackSecurity.UntrustedData.HashCollisionResistant); + Assert.Equal(64 * 1024 * 1024, MessagePackSecurity.UntrustedData.MaximumDecompressedSize); } [Fact] public void Trusted() { Assert.False(MessagePackSecurity.TrustedData.HashCollisionResistant); + Assert.Equal(int.MaxValue, MessagePackSecurity.TrustedData.MaximumDecompressedSize); } [Fact] @@ -37,6 +39,15 @@ public void WithHashCollisionResistant() Assert.True(MessagePackSecurity.TrustedData.WithHashCollisionResistant(true).HashCollisionResistant); } + [Fact] + [Trait("CWE", "409")] + public void WithMaximumDecompressedSize() + { + Assert.Same(MessagePackSecurity.UntrustedData, MessagePackSecurity.UntrustedData.WithMaximumDecompressedSize(64 * 1024 * 1024)); + Assert.Throws(() => MessagePackSecurity.UntrustedData.WithMaximumDecompressedSize(-1)); + Assert.Equal(1024, MessagePackSecurity.UntrustedData.WithMaximumDecompressedSize(1024).MaximumDecompressedSize); + } + [Fact] public void EqualityComparer_CollisionResistance_Int64() { From fb0fe9f0095ebadf4b62c563852ce68ba2655ae4 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Wed, 29 Apr 2026 15:35:24 -0600 Subject: [PATCH 17/17] Honor TypeFormatter options hooks for CWE-470 --- .../StandardClassLibraryFormatter.cs | 11 ++- .../StandardClassLibraryFormatterTests.cs | 84 +++++++++++++++++++ 2 files changed, 92 insertions(+), 3 deletions(-) diff --git a/src/MessagePack/Formatters/StandardClassLibraryFormatter.cs b/src/MessagePack/Formatters/StandardClassLibraryFormatter.cs index 71a64908f..0558394d8 100644 --- a/src/MessagePack/Formatters/StandardClassLibraryFormatter.cs +++ b/src/MessagePack/Formatters/StandardClassLibraryFormatter.cs @@ -837,9 +837,14 @@ public void Serialize(ref MessagePackWriter writer, T? value, MessagePackSeriali public T? Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) { - return reader.ReadString() is string value - ? (T?)Type.GetType(value, throwOnError: true) - : null; + if (reader.ReadString() is not string value) + { + return null; + } + + Type type = options.LoadType(value) ?? throw new TypeLoadException(value); + options.ThrowIfDeserializingTypeIsDisallowed(type); + return (T?)type; } } diff --git a/tests/MessagePack.Tests/StandardClassLibraryFormatterTests.cs b/tests/MessagePack.Tests/StandardClassLibraryFormatterTests.cs index 2a86f2d49..991956eba 100644 --- a/tests/MessagePack.Tests/StandardClassLibraryFormatterTests.cs +++ b/tests/MessagePack.Tests/StandardClassLibraryFormatterTests.cs @@ -36,6 +36,27 @@ public void SystemType_Serializable_Null() Assert.Equal(type, type2); } + [Fact] + public void SystemType_DeserializeUsesLoadType() + { + byte[] msgpack = MessagePackSerializer.Serialize(new TypeHolder { Type = typeof(Uri) }, MessagePackSerializerOptions.Standard); + var options = new RejectingTypeLoadOptions(typeof(Uri)); + + var ex = Assert.Throws(() => MessagePackSerializer.Deserialize(msgpack, options)); + Assert.IsType(ex.InnerException); + Assert.Equal(1, options.LoadTypeCalls); + } + + [Fact] + public void SystemType_DeserializeRejectsDisallowedType() + { + byte[] msgpack = MessagePackSerializer.Serialize(new TypeHolder { Type = typeof(Uri) }, MessagePackSerializerOptions.Standard); + var options = new DisallowingTypeOptions(typeof(Uri)); + + var ex = Assert.Throws(() => MessagePackSerializer.Deserialize(msgpack, options)); + Assert.IsType(ex.InnerException); + } + [Fact] public void DeserializeByteArrayFromFixArray() { @@ -145,5 +166,68 @@ private T Roundtrip(T value, bool breakupBuffer = false) return MessagePackSerializer.Deserialize(msgpack, MessagePackSerializerOptions.Standard); } } + + [MessagePackObject] + public class TypeHolder + { + [Key(0)] + public Type Type { get; set; } + } + + private class RejectingTypeLoadOptions : MessagePackSerializerOptions + { + private readonly Type rejectedType; + + internal RejectingTypeLoadOptions(Type rejectedType) + : base(MessagePackSerializerOptions.Standard) + { + this.rejectedType = rejectedType; + } + + private RejectingTypeLoadOptions(RejectingTypeLoadOptions copyFrom) + : base(copyFrom) + { + this.rejectedType = copyFrom.rejectedType; + this.LoadTypeCalls = copyFrom.LoadTypeCalls; + } + + public int LoadTypeCalls { get; private set; } + + public override Type LoadType(string typeName) + { + Type type = base.LoadType(typeName); + this.LoadTypeCalls++; + return type == this.rejectedType ? null : type; + } + + protected override MessagePackSerializerOptions Clone() => new RejectingTypeLoadOptions(this); + } + + private class DisallowingTypeOptions : MessagePackSerializerOptions + { + private readonly Type rejectedType; + + internal DisallowingTypeOptions(Type rejectedType) + : base(MessagePackSerializerOptions.Standard) + { + this.rejectedType = rejectedType; + } + + private DisallowingTypeOptions(DisallowingTypeOptions copyFrom) + : base(copyFrom) + { + this.rejectedType = copyFrom.rejectedType; + } + + public override void ThrowIfDeserializingTypeIsDisallowed(Type type) + { + if (type == this.rejectedType) + { + throw new TypeAccessException(); + } + } + + protected override MessagePackSerializerOptions Clone() => new DisallowingTypeOptions(this); + } } }