Skip to content

Commit 4e1fefc

Browse files
PSanetraRyanSkraba
authored andcommitted
AVRO-3225: AVRO-3226: Fix possible StackOverflowException and OutOfMemoryException on invalid input
* AVRO-3225: Fix possible StackOverflowException on invalid input * AVRO-3226: Fix possible OutOfMemoryException on invalid input * AVRO-3226: Backport changes for netstandard2.0 (cherry picked from commit a1fce29) Signed-off-by: Ryan Skraba <ryan@skraba.com>
1 parent 423f40f commit 4e1fefc

3 files changed

Lines changed: 163 additions & 21 deletions

File tree

lang/csharp/src/apache/main/IO/BinaryDecoder.netstandard2.0.cs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
* limitations under the License.
1717
*/
1818
using System;
19+
using System.IO;
20+
using System.Text;
1921

2022
namespace Avro.IO
2123
{
@@ -24,6 +26,11 @@ namespace Avro.IO
2426
/// </content>
2527
public partial class BinaryDecoder
2628
{
29+
/// <summary>
30+
/// It is hard to find documentation about the real maximum array length in .NET Framework 4.6.1, but this seems to work :-/
31+
/// </summary>
32+
private const int MaxDotNetArrayLength = 0x3FFFFFFF;
33+
2734
/// <summary>
2835
/// A float is written as 4 bytes.
2936
/// The float is converted into a 32-bit integer using a method equivalent to
@@ -72,10 +79,28 @@ public double ReadDouble()
7279
public string ReadString()
7380
{
7481
int length = ReadInt();
75-
byte[] buffer = new byte[length];
76-
//TODO: Fix this because it's lame;
77-
ReadFixed(buffer);
78-
return System.Text.Encoding.UTF8.GetString(buffer);
82+
83+
if (length < 0)
84+
{
85+
throw new AvroException("Can not deserialize a string with negative length!");
86+
}
87+
88+
if (length > MaxDotNetArrayLength)
89+
{
90+
throw new AvroException("String length is not supported!");
91+
}
92+
93+
using (var binaryReader = new BinaryReader(stream, Encoding.UTF8, true))
94+
{
95+
var bytes = binaryReader.ReadBytes(length);
96+
97+
if (bytes.Length != length)
98+
{
99+
throw new AvroException("Could not read as many bytes from stream as expected!");
100+
}
101+
102+
return Encoding.UTF8.GetString(bytes);
103+
}
79104
}
80105

81106
private void Read(byte[] buffer, int start, int len)

lang/csharp/src/apache/main/IO/BinaryDecoder.notnetstandard2.0.cs

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
using System;
1919
using System.Buffers;
2020
using System.Buffers.Binary;
21+
using System.IO;
2122
using System.Text;
2223

2324
namespace Avro.IO
@@ -28,6 +29,8 @@ namespace Avro.IO
2829
public partial class BinaryDecoder
2930
{
3031
private const int StackallocThreshold = 256;
32+
private const int MaxFastReadLength = 4096;
33+
private const int MaxDotNetArrayLength = 0x7FFFFFC7;
3134

3235
/// <summary>
3336
/// A float is written as 4 bytes.
@@ -63,23 +66,54 @@ public double ReadDouble()
6366
/// <returns>String read from the stream.</returns>
6467
public string ReadString()
6568
{
66-
byte[] bufferArray = null;
67-
6869
int length = ReadInt();
69-
Span<byte> buffer = length <= StackallocThreshold ?
70-
stackalloc byte[length] :
71-
(bufferArray = ArrayPool<byte>.Shared.Rent(length)).AsSpan(0, length);
72-
73-
Read(buffer);
7470

75-
string result = Encoding.UTF8.GetString(buffer);
71+
if (length < 0)
72+
{
73+
throw new AvroException("Can not deserialize a string with negative length!");
74+
}
7675

77-
if (bufferArray != null)
76+
if (length <= MaxFastReadLength)
7877
{
79-
ArrayPool<byte>.Shared.Return(bufferArray);
78+
byte[] bufferArray = null;
79+
80+
try
81+
{
82+
Span<byte> buffer = length <= StackallocThreshold ?
83+
stackalloc byte[length] :
84+
(bufferArray = ArrayPool<byte>.Shared.Rent(length)).AsSpan(0, length);
85+
86+
Read(buffer);
87+
88+
return Encoding.UTF8.GetString(buffer);
89+
}
90+
finally
91+
{
92+
if (bufferArray != null)
93+
{
94+
ArrayPool<byte>.Shared.Return(bufferArray);
95+
}
96+
}
8097
}
98+
else
99+
{
100+
if (length > MaxDotNetArrayLength)
101+
{
102+
throw new AvroException("String length is not supported!");
103+
}
81104

82-
return result;
105+
using (var binaryReader = new BinaryReader(stream, Encoding.UTF8, true))
106+
{
107+
var bytes = binaryReader.ReadBytes(length);
108+
109+
if (bytes.Length != length)
110+
{
111+
throw new AvroException("Could not read as many bytes from stream as expected!");
112+
}
113+
114+
return Encoding.UTF8.GetString(bytes);
115+
}
116+
}
83117
}
84118

85119
private void Read(byte[] buffer, int start, int len)

lang/csharp/src/apache/test/IO/BinaryCodecTests.cs

Lines changed: 89 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
using NUnit.Framework;
2121
using System.IO;
2222
using System.Linq;
23+
using System.Text;
2324
using Avro.IO;
2425

2526
namespace Avro.Test
@@ -214,23 +215,105 @@ public void TestString(string n, int overhead)
214215
TestSkip(n, (Decoder d) => d.SkipString(), (Encoder e, string t) => e.WriteString(t), overhead + n.Length);
215216
}
216217

217-
#if NETCOREAPP3_1
218+
#if NETCOREAPP3_1_OR_GREATER
218219
[Test]
219-
public void TestLargeString()
220+
public void TestStringReadIntoArrayPool()
220221
{
222+
const int maxFastReadLength = 4096;
223+
221224
// Create a 16KB buffer in the Array Pool
222225
var largeBufferToSeedPool = ArrayPool<byte>.Shared.Rent(2 << 14);
223226
ArrayPool<byte>.Shared.Return(largeBufferToSeedPool);
224227

225-
// Create a slightly less than 16KB buffer, which will use the 16KB buffer in the pool
226-
var n = string.Concat(Enumerable.Repeat("1234567890", 1600));
227-
var overhead = 3;
228+
var n = string.Concat(Enumerable.Repeat("A", maxFastReadLength));
229+
var overhead = 2;
228230

229231
TestRead(n, (Decoder d) => d.ReadString(), (Encoder e, string t) => e.WriteString(t), overhead + n.Length);
230-
TestSkip(n, (Decoder d) => d.SkipString(), (Encoder e, string t) => e.WriteString(t), overhead + n.Length);
231232
}
233+
234+
[Test]
235+
public void TestStringReadByBinaryReader()
236+
{
237+
const int overhead = 2;
238+
const int maxFastReadLength = 4096;
239+
const int expectedStringLength = maxFastReadLength + 1;
240+
var n = string.Concat(Enumerable.Repeat("A", expectedStringLength));
241+
242+
TestRead(n, (Decoder d) => d.ReadString(), (Encoder e, string t) => e.WriteString(t), expectedStringLength + overhead);
243+
}
244+
#endif
245+
246+
[Test]
247+
public void TestInvalidInputWithNegativeStringLength()
248+
{
249+
using (MemoryStream iostr = new MemoryStream())
250+
{
251+
Encoder e = new BinaryEncoder(iostr);
252+
253+
e.WriteLong(-1);
254+
255+
iostr.Flush();
256+
iostr.Position = 0;
257+
Decoder d = new BinaryDecoder(iostr);
258+
259+
var exception = Assert.Throws<AvroException>(() => d.ReadString());
260+
261+
Assert.NotNull(exception);
262+
Assert.AreEqual("Can not deserialize a string with negative length!", exception.Message);
263+
iostr.Close();
264+
}
265+
}
266+
267+
[Test]
268+
public void TestInvalidInputWithMaxIntAsStringLength()
269+
{
270+
using (MemoryStream iostr = new MemoryStream())
271+
{
272+
Encoder e = new BinaryEncoder(iostr);
273+
274+
e.WriteLong(int.MaxValue);
275+
e.WriteBytes(Encoding.UTF8.GetBytes("SomeSmallString"));
276+
277+
iostr.Flush();
278+
iostr.Position = 0;
279+
Decoder d = new BinaryDecoder(iostr);
280+
281+
var exception = Assert.Throws<AvroException>(() => d.ReadString());
282+
283+
Assert.NotNull(exception);
284+
Assert.AreEqual("String length is not supported!", exception.Message);
285+
iostr.Close();
286+
}
287+
}
288+
289+
[Test]
290+
public void TestInvalidInputWithMaxArrayLengthAsStringLength()
291+
{
292+
using (MemoryStream iostr = new MemoryStream())
293+
{
294+
Encoder e = new BinaryEncoder(iostr);
295+
296+
#if NETCOREAPP3_1_OR_GREATER
297+
const int maximumArrayLength = 0x7FFFFFC7;
298+
#else
299+
const int maximumArrayLength = 0x7FFFFFFF / 2;
232300
#endif
233301

302+
e.WriteLong(maximumArrayLength);
303+
e.WriteBytes(Encoding.UTF8.GetBytes("SomeSmallString"));
304+
305+
iostr.Flush();
306+
iostr.Position = 0;
307+
Decoder d = new BinaryDecoder(iostr);
308+
309+
var exception = Assert.Throws<AvroException>(() => d.ReadString());
310+
311+
Assert.NotNull(exception);
312+
Assert.AreEqual("Could not read as many bytes from stream as expected!", exception.Message);
313+
iostr.Close();
314+
}
315+
}
316+
234317
[TestCase(0, 1)]
235318
[TestCase(1, 1)]
236319
[TestCase(64, 2)]

0 commit comments

Comments
 (0)