iemejia commented on code in PR #3860:
URL: https://github.com/apache/avro/pull/3860#discussion_r3568052481


##########
lang/csharp/src/apache/test/IO/BinaryCodecTests.cs:
##########
@@ -430,5 +441,330 @@ public void TestFixed(int size)
             TestSkip(b, (Decoder d) => d.SkipFixed(size),
                 (Encoder e, byte[] t) => e.WriteFixed(t), size);
         }
+
+        // A bytes/string value is a length prefix followed by that many bytes.
+        // A malicious or truncated input can declare a huge length with little
+        // actual data; on a seekable stream the reader must reject it before
+        // allocating, rather than attempting a huge allocation.
+        [Test]
+        public void TestReadBytesRejectsLengthBeyondStream()
+        {
+            MemoryStream ms = new MemoryStream();
+            Encoder e = new BinaryEncoder(ms);
+            e.WriteLong(1_000_000); // declares 1,000,000 bytes...
+            ms.Position = 0;        // ...but no data follows
+            Decoder d = new BinaryDecoder(ms);
+            Assert.Throws<AvroException>(() => d.ReadBytes());
+        }
+
+        [Test]
+        public void TestReadStringRejectsLengthBeyondStream()
+        {
+            MemoryStream ms = new MemoryStream();
+            Encoder e = new BinaryEncoder(ms);
+            e.WriteLong(1_000_000); // declares 1,000,000 bytes...
+            ms.Position = 0;        // ...but no data follows
+            Decoder d = new BinaryDecoder(ms);
+            Assert.Throws<AvroException>(() => d.ReadString());
+        }
+
+        // A well-formed value whose declared length fits the stream still 
reads.
+        [Test]
+        public void TestReadBytesWithinStreamStillReads()
+        {
+            byte[] payload = Encoding.UTF8.GetBytes("hello");
+            MemoryStream ms = new MemoryStream();
+            Encoder e = new BinaryEncoder(ms);
+            e.WriteBytes(payload);
+            ms.Position = 0;
+            Decoder d = new BinaryDecoder(ms);
+            Assert.AreEqual(payload, d.ReadBytes());
+        }
+
+        // On a non-seekable stream the remaining length is unknown, so the
+        // pre-check is skipped and a valid value still decodes.
+        [Test]
+        public void TestReadBytesNonSeekableStreamStillReads()
+        {
+            byte[] payload = Encoding.UTF8.GetBytes("hello");
+            MemoryStream backing = new MemoryStream();
+            Encoder e = new BinaryEncoder(backing);
+            e.WriteBytes(payload);
+            byte[] encoded = backing.ToArray();
+
+            using (var ns = new NonSeekableStream(new MemoryStream(encoded)))
+            {
+                Decoder d = new BinaryDecoder(ns);
+                Assert.AreEqual(payload, d.ReadBytes());
+            }
+        }
+
+        // An array/map block declares an element count; a malicious or 
truncated
+        // input can declare far more elements than the remaining bytes could
+        // hold. The count is validated against the bytes remaining before
+        // allocating, using the minimum on-wire size of the element schema (so
+        // 0-byte elements like null are not falsely rejected).
+        [Test]
+        public void TestReadArrayRejectsCountBeyondStream()
+        {
+            var schema = 
Avro.Schema.Parse("{\"type\":\"array\",\"items\":\"long\"}");
+            var ms = new MemoryStream();
+            new BinaryEncoder(ms).WriteLong(1000000); // 1,000,000 longs, no 
data
+            ms.Position = 0;
+            var reader = new GenericReader<object>(schema, schema);
+            Assert.Throws<AvroException>(() => reader.Read(null, new 
BinaryDecoder(ms)));
+        }
+
+        [Test]
+        public void TestReadMapRejectsCountBeyondStream()
+        {
+            var schema = 
Avro.Schema.Parse("{\"type\":\"map\",\"values\":\"long\"}");
+            var ms = new MemoryStream();
+            new BinaryEncoder(ms).WriteLong(1000000);
+            ms.Position = 0;
+            var reader = new GenericReader<object>(schema, schema);
+            Assert.Throws<AvroException>(() => reader.Read(null, new 
BinaryDecoder(ms)));
+        }
+
+        [Test]
+        public void TestReadArrayOfNullNotFalselyRejected()
+        {
+            var schema = 
Avro.Schema.Parse("{\"type\":\"array\",\"items\":\"null\"}");
+            var ms = new MemoryStream();
+            var enc = new BinaryEncoder(ms);
+            enc.WriteLong(100000); // one block of 100,000 nulls (zero bytes 
each)
+            enc.WriteLong(0);      // end-of-array marker
+            ms.Position = 0;
+            var reader = new GenericReader<object>(schema, schema);
+            var result = (Array)reader.Read(null, new BinaryDecoder(ms));
+            Assert.AreEqual(100000, result.Length);
+        }
+
+        // A negative bytes length must be rejected explicitly rather than
+        // flowing into a negative array allocation.
+        [Test]
+        public void TestReadBytesRejectsNegativeLength()
+        {
+            var ms = new MemoryStream();
+            new BinaryEncoder(ms).WriteLong(-5);
+            ms.Position = 0;
+            var d = new BinaryDecoder(ms);
+            Assert.Throws<AvroException>(() => d.ReadBytes());
+        }
+
+        // A block count larger than int.MaxValue must be rejected before the
+        // int cast, even for a null-element array where the byte check is
+        // skipped.
+        [Test]
+        public void TestReadArrayRejectsCountAboveIntMax()
+        {
+            var schema = 
Avro.Schema.Parse("{\"type\":\"array\",\"items\":\"null\"}");
+            var ms = new MemoryStream();
+            new BinaryEncoder(ms).WriteLong((long)int.MaxValue + 1);
+            ms.Position = 0;
+            var reader = new GenericReader<object>(schema, schema);
+            Assert.Throws<AvroException>(() => reader.Read(null, new 
BinaryDecoder(ms)));
+        }
+
+        // A bytes length above the maximum .NET array length must be rejected
+        // with a consistent AvroException rather than letting new byte[p] 
throw.
+        // A non-seekable stream is used so the length reaches the array-length
+        // check without needing gigabytes of backing data.
+        [Test]
+        public void TestReadBytesRejectsLengthAboveMaxArrayLength()
+        {
+            var backing = new MemoryStream();
+            new BinaryEncoder(backing).WriteLong(3_000_000_000L); // > any 
MaxDotNetArrayLength
+            byte[] encoded = backing.ToArray();
+            using (var ns = new NonSeekableStream(new MemoryStream(encoded)))
+            {
+                var d = new BinaryDecoder(ns);
+                Assert.Throws<AvroException>(() => d.ReadBytes());
+            }
+        }
+
+        // A zero-byte element type (null) consumes no input, so the
+        // bytes-remaining check cannot bound its block count; a tiny payload
+        // declaring a huge count must instead be rejected by the item cap
+        // before building the array.
+        [Test]
+        public void TestReadArrayOfNullRejectsHugeCount()
+        {
+            var schema = 
Avro.Schema.Parse("{\"type\":\"array\",\"items\":\"null\"}");
+            var ms = new MemoryStream();
+            new BinaryEncoder(ms).WriteLong(200_000_000); // ~4 byte payload

Review Comment:
   Reduced to 10,000,001 (just over the default zero-byte item cap) so the 
rejection is still exercised but a future guard regression stays bounded.



##########
lang/csharp/src/apache/test/IO/BinaryCodecTests.cs:
##########
@@ -430,5 +441,330 @@ public void TestFixed(int size)
             TestSkip(b, (Decoder d) => d.SkipFixed(size),
                 (Encoder e, byte[] t) => e.WriteFixed(t), size);
         }
+
+        // A bytes/string value is a length prefix followed by that many bytes.
+        // A malicious or truncated input can declare a huge length with little
+        // actual data; on a seekable stream the reader must reject it before
+        // allocating, rather than attempting a huge allocation.
+        [Test]
+        public void TestReadBytesRejectsLengthBeyondStream()
+        {
+            MemoryStream ms = new MemoryStream();
+            Encoder e = new BinaryEncoder(ms);
+            e.WriteLong(1_000_000); // declares 1,000,000 bytes...
+            ms.Position = 0;        // ...but no data follows
+            Decoder d = new BinaryDecoder(ms);
+            Assert.Throws<AvroException>(() => d.ReadBytes());
+        }
+
+        [Test]
+        public void TestReadStringRejectsLengthBeyondStream()
+        {
+            MemoryStream ms = new MemoryStream();
+            Encoder e = new BinaryEncoder(ms);
+            e.WriteLong(1_000_000); // declares 1,000,000 bytes...
+            ms.Position = 0;        // ...but no data follows
+            Decoder d = new BinaryDecoder(ms);
+            Assert.Throws<AvroException>(() => d.ReadString());
+        }
+
+        // A well-formed value whose declared length fits the stream still 
reads.
+        [Test]
+        public void TestReadBytesWithinStreamStillReads()
+        {
+            byte[] payload = Encoding.UTF8.GetBytes("hello");
+            MemoryStream ms = new MemoryStream();
+            Encoder e = new BinaryEncoder(ms);
+            e.WriteBytes(payload);
+            ms.Position = 0;
+            Decoder d = new BinaryDecoder(ms);
+            Assert.AreEqual(payload, d.ReadBytes());
+        }
+
+        // On a non-seekable stream the remaining length is unknown, so the
+        // pre-check is skipped and a valid value still decodes.
+        [Test]
+        public void TestReadBytesNonSeekableStreamStillReads()
+        {
+            byte[] payload = Encoding.UTF8.GetBytes("hello");
+            MemoryStream backing = new MemoryStream();
+            Encoder e = new BinaryEncoder(backing);
+            e.WriteBytes(payload);
+            byte[] encoded = backing.ToArray();
+
+            using (var ns = new NonSeekableStream(new MemoryStream(encoded)))
+            {
+                Decoder d = new BinaryDecoder(ns);
+                Assert.AreEqual(payload, d.ReadBytes());
+            }
+        }
+
+        // An array/map block declares an element count; a malicious or 
truncated
+        // input can declare far more elements than the remaining bytes could
+        // hold. The count is validated against the bytes remaining before
+        // allocating, using the minimum on-wire size of the element schema (so
+        // 0-byte elements like null are not falsely rejected).
+        [Test]
+        public void TestReadArrayRejectsCountBeyondStream()
+        {
+            var schema = 
Avro.Schema.Parse("{\"type\":\"array\",\"items\":\"long\"}");
+            var ms = new MemoryStream();
+            new BinaryEncoder(ms).WriteLong(1000000); // 1,000,000 longs, no 
data
+            ms.Position = 0;
+            var reader = new GenericReader<object>(schema, schema);
+            Assert.Throws<AvroException>(() => reader.Read(null, new 
BinaryDecoder(ms)));
+        }
+
+        [Test]
+        public void TestReadMapRejectsCountBeyondStream()
+        {
+            var schema = 
Avro.Schema.Parse("{\"type\":\"map\",\"values\":\"long\"}");
+            var ms = new MemoryStream();
+            new BinaryEncoder(ms).WriteLong(1000000);
+            ms.Position = 0;
+            var reader = new GenericReader<object>(schema, schema);
+            Assert.Throws<AvroException>(() => reader.Read(null, new 
BinaryDecoder(ms)));
+        }
+
+        [Test]
+        public void TestReadArrayOfNullNotFalselyRejected()
+        {
+            var schema = 
Avro.Schema.Parse("{\"type\":\"array\",\"items\":\"null\"}");
+            var ms = new MemoryStream();
+            var enc = new BinaryEncoder(ms);
+            enc.WriteLong(100000); // one block of 100,000 nulls (zero bytes 
each)
+            enc.WriteLong(0);      // end-of-array marker
+            ms.Position = 0;
+            var reader = new GenericReader<object>(schema, schema);
+            var result = (Array)reader.Read(null, new BinaryDecoder(ms));
+            Assert.AreEqual(100000, result.Length);
+        }
+
+        // A negative bytes length must be rejected explicitly rather than
+        // flowing into a negative array allocation.
+        [Test]
+        public void TestReadBytesRejectsNegativeLength()
+        {
+            var ms = new MemoryStream();
+            new BinaryEncoder(ms).WriteLong(-5);
+            ms.Position = 0;
+            var d = new BinaryDecoder(ms);
+            Assert.Throws<AvroException>(() => d.ReadBytes());
+        }
+
+        // A block count larger than int.MaxValue must be rejected before the
+        // int cast, even for a null-element array where the byte check is
+        // skipped.
+        [Test]
+        public void TestReadArrayRejectsCountAboveIntMax()
+        {
+            var schema = 
Avro.Schema.Parse("{\"type\":\"array\",\"items\":\"null\"}");
+            var ms = new MemoryStream();
+            new BinaryEncoder(ms).WriteLong((long)int.MaxValue + 1);
+            ms.Position = 0;
+            var reader = new GenericReader<object>(schema, schema);
+            Assert.Throws<AvroException>(() => reader.Read(null, new 
BinaryDecoder(ms)));
+        }
+
+        // A bytes length above the maximum .NET array length must be rejected
+        // with a consistent AvroException rather than letting new byte[p] 
throw.
+        // A non-seekable stream is used so the length reaches the array-length
+        // check without needing gigabytes of backing data.
+        [Test]
+        public void TestReadBytesRejectsLengthAboveMaxArrayLength()
+        {
+            var backing = new MemoryStream();
+            new BinaryEncoder(backing).WriteLong(3_000_000_000L); // > any 
MaxDotNetArrayLength
+            byte[] encoded = backing.ToArray();
+            using (var ns = new NonSeekableStream(new MemoryStream(encoded)))
+            {
+                var d = new BinaryDecoder(ns);
+                Assert.Throws<AvroException>(() => d.ReadBytes());
+            }
+        }
+
+        // A zero-byte element type (null) consumes no input, so the
+        // bytes-remaining check cannot bound its block count; a tiny payload
+        // declaring a huge count must instead be rejected by the item cap
+        // before building the array.
+        [Test]
+        public void TestReadArrayOfNullRejectsHugeCount()
+        {
+            var schema = 
Avro.Schema.Parse("{\"type\":\"array\",\"items\":\"null\"}");
+            var ms = new MemoryStream();
+            new BinaryEncoder(ms).WriteLong(200_000_000); // ~4 byte payload
+            ms.Position = 0;
+            var reader = new GenericReader<object>(schema, schema);
+            Assert.Throws<AvroException>(() => reader.Read(null, new 
BinaryDecoder(ms)));
+        }
+
+        // A non-zero-byte array on a non-seekable stream cannot have its block
+        // count bounded by the bytes remaining (the length is unknown). The
+        // backing array must therefore be grown on demand rather than
+        // preallocated to the declared count, so a huge count with truncated 
data
+        // fails with a bounded AvroException instead of attempting a multi-
+        // gigabyte allocation.
+        [Test]
+        public void TestReadArrayHugeCountOnStreamClampsPreallocation()
+        {
+            var schema = 
Avro.Schema.Parse("{\"type\":\"array\",\"items\":\"long\"}");
+            var backing = new MemoryStream();
+            new BinaryEncoder(backing).WriteLong(200_000_000); // block count; 
no element data
+            byte[] encoded = backing.ToArray();
+            using (var ns = new NonSeekableStream(new MemoryStream(encoded)))
+            {
+                var reader = new GenericReader<object>(schema, schema);
+                Assert.Throws<AvroException>(() => reader.Read(null, new 
BinaryDecoder(ns)));
+            }
+        }
+
+        // The skip path (a writer field absent from the reader schema) must be
+        // bounded too, so skipping a huge zero-byte block cannot loop 
endlessly.
+        [Test]
+        public void TestSkipArrayOfNullRejectsHugeCount()
+        {
+            var writer = Avro.Schema.Parse(
+                "{\"type\":\"record\",\"name\":\"Foo\",\"fields\":[" +
+                
"{\"name\":\"arr\",\"type\":{\"type\":\"array\",\"items\":\"null\"}}," +
+                "{\"name\":\"val\",\"type\":\"int\"}]}");
+            var reader = Avro.Schema.Parse(
+                "{\"type\":\"record\",\"name\":\"Foo\",\"fields\":[" +
+                "{\"name\":\"val\",\"type\":\"int\"}]}");
+            var ms = new MemoryStream();
+            new BinaryEncoder(ms).WriteLong(200_000_000); // arr block count; 
skipped

Review Comment:
   Reduced the skip test to 10,000,001 for the same bounded-failure reason.



##########
lang/csharp/src/apache/test/IO/BinaryCodecTests.cs:
##########
@@ -430,5 +435,330 @@ public void TestFixed(int size)
             TestSkip(b, (Decoder d) => d.SkipFixed(size),
                 (Encoder e, byte[] t) => e.WriteFixed(t), size);
         }
+
+        // A bytes/string value is a length prefix followed by that many bytes.
+        // A malicious or truncated input can declare a huge length with little
+        // actual data; on a seekable stream the reader must reject it before
+        // allocating, rather than attempting a huge allocation.
+        [Test]
+        public void TestReadBytesRejectsLengthBeyondStream()
+        {
+            MemoryStream ms = new MemoryStream();
+            Encoder e = new BinaryEncoder(ms);
+            e.WriteLong(1_000_000); // declares 1,000,000 bytes...
+            ms.Position = 0;        // ...but no data follows
+            Decoder d = new BinaryDecoder(ms);
+            Assert.Throws<AvroException>(() => d.ReadBytes());
+        }
+
+        [Test]
+        public void TestReadStringRejectsLengthBeyondStream()
+        {
+            MemoryStream ms = new MemoryStream();
+            Encoder e = new BinaryEncoder(ms);
+            e.WriteLong(1_000_000); // declares 1,000,000 bytes...
+            ms.Position = 0;        // ...but no data follows
+            Decoder d = new BinaryDecoder(ms);
+            Assert.Throws<AvroException>(() => d.ReadString());
+        }
+
+        // A well-formed value whose declared length fits the stream still 
reads.
+        [Test]
+        public void TestReadBytesWithinStreamStillReads()
+        {
+            byte[] payload = Encoding.UTF8.GetBytes("hello");
+            MemoryStream ms = new MemoryStream();
+            Encoder e = new BinaryEncoder(ms);
+            e.WriteBytes(payload);
+            ms.Position = 0;
+            Decoder d = new BinaryDecoder(ms);
+            Assert.AreEqual(payload, d.ReadBytes());
+        }
+
+        // On a non-seekable stream the remaining length is unknown, so the
+        // pre-check is skipped and a valid value still decodes.
+        [Test]
+        public void TestReadBytesNonSeekableStreamStillReads()
+        {
+            byte[] payload = Encoding.UTF8.GetBytes("hello");
+            MemoryStream backing = new MemoryStream();
+            Encoder e = new BinaryEncoder(backing);
+            e.WriteBytes(payload);
+            byte[] encoded = backing.ToArray();
+
+            using (var ns = new NonSeekableStream(new MemoryStream(encoded)))
+            {
+                Decoder d = new BinaryDecoder(ns);
+                Assert.AreEqual(payload, d.ReadBytes());
+            }
+        }
+
+        // An array/map block declares an element count; a malicious or 
truncated
+        // input can declare far more elements than the remaining bytes could
+        // hold. The count is validated against the bytes remaining before
+        // allocating, using the minimum on-wire size of the element schema (so
+        // 0-byte elements like null are not falsely rejected).
+        [Test]
+        public void TestReadArrayRejectsCountBeyondStream()
+        {
+            var schema = 
Avro.Schema.Parse("{\"type\":\"array\",\"items\":\"long\"}");
+            var ms = new MemoryStream();
+            new BinaryEncoder(ms).WriteLong(1000000); // 1,000,000 longs, no 
data
+            ms.Position = 0;
+            var reader = new GenericReader<object>(schema, schema);
+            Assert.Throws<AvroException>(() => reader.Read(null, new 
BinaryDecoder(ms)));
+        }
+
+        [Test]
+        public void TestReadMapRejectsCountBeyondStream()
+        {
+            var schema = 
Avro.Schema.Parse("{\"type\":\"map\",\"values\":\"long\"}");
+            var ms = new MemoryStream();
+            new BinaryEncoder(ms).WriteLong(1000000);
+            ms.Position = 0;
+            var reader = new GenericReader<object>(schema, schema);
+            Assert.Throws<AvroException>(() => reader.Read(null, new 
BinaryDecoder(ms)));
+        }
+
+        [Test]
+        public void TestReadArrayOfNullNotFalselyRejected()
+        {
+            var schema = 
Avro.Schema.Parse("{\"type\":\"array\",\"items\":\"null\"}");
+            var ms = new MemoryStream();
+            var enc = new BinaryEncoder(ms);
+            enc.WriteLong(100000); // one block of 100,000 nulls (zero bytes 
each)
+            enc.WriteLong(0);      // end-of-array marker
+            ms.Position = 0;
+            var reader = new GenericReader<object>(schema, schema);
+            var result = (Array)reader.Read(null, new BinaryDecoder(ms));
+            Assert.AreEqual(100000, result.Length);
+        }

Review Comment:
   The count here is 100,000 (well under the default cap) exercising a normal 
large array; a lowered AVRO_MAX_COLLECTION_ITEMS could reject it, but this 
fixture documents that it assumes default limits. The catastrophic-regression 
concern applies to the null tests, now reduced to 10,000,001.



##########
lang/csharp/src/apache/test/IO/BinaryCodecTests.cs:
##########
@@ -430,5 +435,330 @@ public void TestFixed(int size)
             TestSkip(b, (Decoder d) => d.SkipFixed(size),
                 (Encoder e, byte[] t) => e.WriteFixed(t), size);
         }
+
+        // A bytes/string value is a length prefix followed by that many bytes.
+        // A malicious or truncated input can declare a huge length with little
+        // actual data; on a seekable stream the reader must reject it before
+        // allocating, rather than attempting a huge allocation.
+        [Test]
+        public void TestReadBytesRejectsLengthBeyondStream()
+        {
+            MemoryStream ms = new MemoryStream();
+            Encoder e = new BinaryEncoder(ms);
+            e.WriteLong(1_000_000); // declares 1,000,000 bytes...
+            ms.Position = 0;        // ...but no data follows
+            Decoder d = new BinaryDecoder(ms);
+            Assert.Throws<AvroException>(() => d.ReadBytes());
+        }
+
+        [Test]
+        public void TestReadStringRejectsLengthBeyondStream()
+        {
+            MemoryStream ms = new MemoryStream();
+            Encoder e = new BinaryEncoder(ms);
+            e.WriteLong(1_000_000); // declares 1,000,000 bytes...
+            ms.Position = 0;        // ...but no data follows
+            Decoder d = new BinaryDecoder(ms);
+            Assert.Throws<AvroException>(() => d.ReadString());
+        }
+
+        // A well-formed value whose declared length fits the stream still 
reads.
+        [Test]
+        public void TestReadBytesWithinStreamStillReads()
+        {
+            byte[] payload = Encoding.UTF8.GetBytes("hello");
+            MemoryStream ms = new MemoryStream();
+            Encoder e = new BinaryEncoder(ms);
+            e.WriteBytes(payload);
+            ms.Position = 0;
+            Decoder d = new BinaryDecoder(ms);
+            Assert.AreEqual(payload, d.ReadBytes());
+        }
+
+        // On a non-seekable stream the remaining length is unknown, so the
+        // pre-check is skipped and a valid value still decodes.
+        [Test]
+        public void TestReadBytesNonSeekableStreamStillReads()
+        {
+            byte[] payload = Encoding.UTF8.GetBytes("hello");
+            MemoryStream backing = new MemoryStream();
+            Encoder e = new BinaryEncoder(backing);
+            e.WriteBytes(payload);
+            byte[] encoded = backing.ToArray();
+
+            using (var ns = new NonSeekableStream(new MemoryStream(encoded)))
+            {
+                Decoder d = new BinaryDecoder(ns);
+                Assert.AreEqual(payload, d.ReadBytes());
+            }
+        }
+
+        // An array/map block declares an element count; a malicious or 
truncated
+        // input can declare far more elements than the remaining bytes could
+        // hold. The count is validated against the bytes remaining before
+        // allocating, using the minimum on-wire size of the element schema (so
+        // 0-byte elements like null are not falsely rejected).
+        [Test]
+        public void TestReadArrayRejectsCountBeyondStream()
+        {
+            var schema = 
Avro.Schema.Parse("{\"type\":\"array\",\"items\":\"long\"}");
+            var ms = new MemoryStream();
+            new BinaryEncoder(ms).WriteLong(1000000); // 1,000,000 longs, no 
data
+            ms.Position = 0;
+            var reader = new GenericReader<object>(schema, schema);
+            Assert.Throws<AvroException>(() => reader.Read(null, new 
BinaryDecoder(ms)));
+        }
+
+        [Test]
+        public void TestReadMapRejectsCountBeyondStream()
+        {
+            var schema = 
Avro.Schema.Parse("{\"type\":\"map\",\"values\":\"long\"}");
+            var ms = new MemoryStream();
+            new BinaryEncoder(ms).WriteLong(1000000);
+            ms.Position = 0;
+            var reader = new GenericReader<object>(schema, schema);
+            Assert.Throws<AvroException>(() => reader.Read(null, new 
BinaryDecoder(ms)));
+        }
+
+        [Test]
+        public void TestReadArrayOfNullNotFalselyRejected()
+        {
+            var schema = 
Avro.Schema.Parse("{\"type\":\"array\",\"items\":\"null\"}");
+            var ms = new MemoryStream();
+            var enc = new BinaryEncoder(ms);
+            enc.WriteLong(100000); // one block of 100,000 nulls (zero bytes 
each)
+            enc.WriteLong(0);      // end-of-array marker
+            ms.Position = 0;
+            var reader = new GenericReader<object>(schema, schema);
+            var result = (Array)reader.Read(null, new BinaryDecoder(ms));
+            Assert.AreEqual(100000, result.Length);
+        }
+
+        // A negative bytes length must be rejected explicitly rather than
+        // flowing into a negative array allocation.
+        [Test]
+        public void TestReadBytesRejectsNegativeLength()
+        {
+            var ms = new MemoryStream();
+            new BinaryEncoder(ms).WriteLong(-5);
+            ms.Position = 0;
+            var d = new BinaryDecoder(ms);
+            Assert.Throws<AvroException>(() => d.ReadBytes());
+        }
+
+        // A block count larger than int.MaxValue must be rejected before the
+        // int cast, even for a null-element array where the byte check is
+        // skipped.
+        [Test]
+        public void TestReadArrayRejectsCountAboveIntMax()
+        {
+            var schema = 
Avro.Schema.Parse("{\"type\":\"array\",\"items\":\"null\"}");
+            var ms = new MemoryStream();
+            new BinaryEncoder(ms).WriteLong((long)int.MaxValue + 1);
+            ms.Position = 0;
+            var reader = new GenericReader<object>(schema, schema);
+            Assert.Throws<AvroException>(() => reader.Read(null, new 
BinaryDecoder(ms)));
+        }
+
+        // A bytes length above the maximum .NET array length must be rejected
+        // with a consistent AvroException rather than letting new byte[p] 
throw.
+        // A non-seekable stream is used so the length reaches the array-length
+        // check without needing gigabytes of backing data.
+        [Test]
+        public void TestReadBytesRejectsLengthAboveMaxArrayLength()
+        {
+            var backing = new MemoryStream();
+            new BinaryEncoder(backing).WriteLong(3_000_000_000L); // > any 
MaxDotNetArrayLength
+            byte[] encoded = backing.ToArray();
+            using (var ns = new NonSeekableStream(new MemoryStream(encoded)))
+            {
+                var d = new BinaryDecoder(ns);
+                Assert.Throws<AvroException>(() => d.ReadBytes());
+            }
+        }
+
+        // A zero-byte element type (null) consumes no input, so the
+        // bytes-remaining check cannot bound its block count; a tiny payload
+        // declaring a huge count must instead be rejected by the item cap
+        // before building the array.
+        [Test]
+        public void TestReadArrayOfNullRejectsHugeCount()
+        {
+            var schema = 
Avro.Schema.Parse("{\"type\":\"array\",\"items\":\"null\"}");
+            var ms = new MemoryStream();
+            new BinaryEncoder(ms).WriteLong(200_000_000); // ~4 byte payload
+            ms.Position = 0;
+            var reader = new GenericReader<object>(schema, schema);
+            Assert.Throws<AvroException>(() => reader.Read(null, new 
BinaryDecoder(ms)));
+        }

Review Comment:
   Reduced to 10,000,001 to keep the failure mode bounded. Full hermetic env 
isolation isn't reliably possible because GenericReader captures the limits 
into static readonly fields at type-init (see SetUp note).



##########
lang/csharp/src/apache/test/IO/BinaryCodecTests.cs:
##########
@@ -430,5 +435,330 @@ public void TestFixed(int size)
             TestSkip(b, (Decoder d) => d.SkipFixed(size),
                 (Encoder e, byte[] t) => e.WriteFixed(t), size);
         }
+
+        // A bytes/string value is a length prefix followed by that many bytes.
+        // A malicious or truncated input can declare a huge length with little
+        // actual data; on a seekable stream the reader must reject it before
+        // allocating, rather than attempting a huge allocation.
+        [Test]
+        public void TestReadBytesRejectsLengthBeyondStream()
+        {
+            MemoryStream ms = new MemoryStream();
+            Encoder e = new BinaryEncoder(ms);
+            e.WriteLong(1_000_000); // declares 1,000,000 bytes...
+            ms.Position = 0;        // ...but no data follows
+            Decoder d = new BinaryDecoder(ms);
+            Assert.Throws<AvroException>(() => d.ReadBytes());
+        }
+
+        [Test]
+        public void TestReadStringRejectsLengthBeyondStream()
+        {
+            MemoryStream ms = new MemoryStream();
+            Encoder e = new BinaryEncoder(ms);
+            e.WriteLong(1_000_000); // declares 1,000,000 bytes...
+            ms.Position = 0;        // ...but no data follows
+            Decoder d = new BinaryDecoder(ms);
+            Assert.Throws<AvroException>(() => d.ReadString());
+        }
+
+        // A well-formed value whose declared length fits the stream still 
reads.
+        [Test]
+        public void TestReadBytesWithinStreamStillReads()
+        {
+            byte[] payload = Encoding.UTF8.GetBytes("hello");
+            MemoryStream ms = new MemoryStream();
+            Encoder e = new BinaryEncoder(ms);
+            e.WriteBytes(payload);
+            ms.Position = 0;
+            Decoder d = new BinaryDecoder(ms);
+            Assert.AreEqual(payload, d.ReadBytes());
+        }
+
+        // On a non-seekable stream the remaining length is unknown, so the
+        // pre-check is skipped and a valid value still decodes.
+        [Test]
+        public void TestReadBytesNonSeekableStreamStillReads()
+        {
+            byte[] payload = Encoding.UTF8.GetBytes("hello");
+            MemoryStream backing = new MemoryStream();
+            Encoder e = new BinaryEncoder(backing);
+            e.WriteBytes(payload);
+            byte[] encoded = backing.ToArray();
+
+            using (var ns = new NonSeekableStream(new MemoryStream(encoded)))
+            {
+                Decoder d = new BinaryDecoder(ns);
+                Assert.AreEqual(payload, d.ReadBytes());
+            }
+        }
+
+        // An array/map block declares an element count; a malicious or 
truncated
+        // input can declare far more elements than the remaining bytes could
+        // hold. The count is validated against the bytes remaining before
+        // allocating, using the minimum on-wire size of the element schema (so
+        // 0-byte elements like null are not falsely rejected).
+        [Test]
+        public void TestReadArrayRejectsCountBeyondStream()
+        {
+            var schema = 
Avro.Schema.Parse("{\"type\":\"array\",\"items\":\"long\"}");
+            var ms = new MemoryStream();
+            new BinaryEncoder(ms).WriteLong(1000000); // 1,000,000 longs, no 
data
+            ms.Position = 0;
+            var reader = new GenericReader<object>(schema, schema);
+            Assert.Throws<AvroException>(() => reader.Read(null, new 
BinaryDecoder(ms)));
+        }
+
+        [Test]
+        public void TestReadMapRejectsCountBeyondStream()
+        {
+            var schema = 
Avro.Schema.Parse("{\"type\":\"map\",\"values\":\"long\"}");
+            var ms = new MemoryStream();
+            new BinaryEncoder(ms).WriteLong(1000000);
+            ms.Position = 0;
+            var reader = new GenericReader<object>(schema, schema);
+            Assert.Throws<AvroException>(() => reader.Read(null, new 
BinaryDecoder(ms)));
+        }
+
+        [Test]
+        public void TestReadArrayOfNullNotFalselyRejected()
+        {
+            var schema = 
Avro.Schema.Parse("{\"type\":\"array\",\"items\":\"null\"}");
+            var ms = new MemoryStream();
+            var enc = new BinaryEncoder(ms);
+            enc.WriteLong(100000); // one block of 100,000 nulls (zero bytes 
each)
+            enc.WriteLong(0);      // end-of-array marker
+            ms.Position = 0;
+            var reader = new GenericReader<object>(schema, schema);
+            var result = (Array)reader.Read(null, new BinaryDecoder(ms));
+            Assert.AreEqual(100000, result.Length);
+        }
+
+        // A negative bytes length must be rejected explicitly rather than
+        // flowing into a negative array allocation.
+        [Test]
+        public void TestReadBytesRejectsNegativeLength()
+        {
+            var ms = new MemoryStream();
+            new BinaryEncoder(ms).WriteLong(-5);
+            ms.Position = 0;
+            var d = new BinaryDecoder(ms);
+            Assert.Throws<AvroException>(() => d.ReadBytes());
+        }
+
+        // A block count larger than int.MaxValue must be rejected before the
+        // int cast, even for a null-element array where the byte check is
+        // skipped.
+        [Test]
+        public void TestReadArrayRejectsCountAboveIntMax()
+        {
+            var schema = 
Avro.Schema.Parse("{\"type\":\"array\",\"items\":\"null\"}");
+            var ms = new MemoryStream();
+            new BinaryEncoder(ms).WriteLong((long)int.MaxValue + 1);
+            ms.Position = 0;
+            var reader = new GenericReader<object>(schema, schema);
+            Assert.Throws<AvroException>(() => reader.Read(null, new 
BinaryDecoder(ms)));
+        }
+
+        // A bytes length above the maximum .NET array length must be rejected
+        // with a consistent AvroException rather than letting new byte[p] 
throw.
+        // A non-seekable stream is used so the length reaches the array-length
+        // check without needing gigabytes of backing data.
+        [Test]
+        public void TestReadBytesRejectsLengthAboveMaxArrayLength()
+        {
+            var backing = new MemoryStream();
+            new BinaryEncoder(backing).WriteLong(3_000_000_000L); // > any 
MaxDotNetArrayLength
+            byte[] encoded = backing.ToArray();
+            using (var ns = new NonSeekableStream(new MemoryStream(encoded)))
+            {
+                var d = new BinaryDecoder(ns);
+                Assert.Throws<AvroException>(() => d.ReadBytes());
+            }
+        }
+
+        // A zero-byte element type (null) consumes no input, so the
+        // bytes-remaining check cannot bound its block count; a tiny payload
+        // declaring a huge count must instead be rejected by the item cap
+        // before building the array.
+        [Test]
+        public void TestReadArrayOfNullRejectsHugeCount()
+        {
+            var schema = 
Avro.Schema.Parse("{\"type\":\"array\",\"items\":\"null\"}");
+            var ms = new MemoryStream();
+            new BinaryEncoder(ms).WriteLong(200_000_000); // ~4 byte payload
+            ms.Position = 0;
+            var reader = new GenericReader<object>(schema, schema);
+            Assert.Throws<AvroException>(() => reader.Read(null, new 
BinaryDecoder(ms)));
+        }
+
+        // A non-zero-byte array on a non-seekable stream cannot have its block
+        // count bounded by the bytes remaining (the length is unknown). The
+        // backing array must therefore be grown on demand rather than
+        // preallocated to the declared count, so a huge count with truncated 
data
+        // fails with a bounded AvroException instead of attempting a multi-
+        // gigabyte allocation.
+        [Test]
+        public void TestReadArrayHugeCountOnStreamClampsPreallocation()
+        {
+            var schema = 
Avro.Schema.Parse("{\"type\":\"array\",\"items\":\"long\"}");
+            var backing = new MemoryStream();
+            new BinaryEncoder(backing).WriteLong(200_000_000); // block count; 
no element data
+            byte[] encoded = backing.ToArray();
+            using (var ns = new NonSeekableStream(new MemoryStream(encoded)))
+            {
+                var reader = new GenericReader<object>(schema, schema);
+                Assert.Throws<AvroException>(() => reader.Read(null, new 
BinaryDecoder(ns)));
+            }
+        }
+
+        // The skip path (a writer field absent from the reader schema) must be
+        // bounded too, so skipping a huge zero-byte block cannot loop 
endlessly.
+        [Test]
+        public void TestSkipArrayOfNullRejectsHugeCount()
+        {
+            var writer = Avro.Schema.Parse(
+                "{\"type\":\"record\",\"name\":\"Foo\",\"fields\":[" +
+                
"{\"name\":\"arr\",\"type\":{\"type\":\"array\",\"items\":\"null\"}}," +
+                "{\"name\":\"val\",\"type\":\"int\"}]}");
+            var reader = Avro.Schema.Parse(
+                "{\"type\":\"record\",\"name\":\"Foo\",\"fields\":[" +
+                "{\"name\":\"val\",\"type\":\"int\"}]}");
+            var ms = new MemoryStream();
+            new BinaryEncoder(ms).WriteLong(200_000_000); // arr block count; 
skipped
+            ms.Position = 0;
+            var r = new GenericReader<object>(writer, reader);
+            Assert.Throws<AvroException>(() => r.Read(null, new 
BinaryDecoder(ms)));
+        }

Review Comment:
   Reduced the skip test to 10,000,001; same static-readonly caching caveat as 
above.



##########
lang/csharp/src/apache/main/IO/BinaryDecoder.cs:
##########
@@ -76,6 +76,13 @@ public long ReadLong()
             int shift = 7;
             while ((b & 0x80) != 0)
             {
+                // A 64-bit value uses at most 10 bytes (shifts 0..63); reject 
an
+                // overlong varint rather than silently wrapping to a wrong 
value.
+                if (shift >= 70)
+                {
+                    throw new AvroException("Varint is too long");
+                }
+
                 b = read();
                 n |= (b & 0x7FUL) << shift;
                 shift += 7;

Review Comment:
   Already handled — ReadLong rejects a 10th byte whose payload exceeds bit 63 
via `shift == 63 && (b & 0x7E) != 0` (BinaryDecoder.cs:90).



##########
lang/csharp/src/apache/main/Generic/GenericReader.cs:
##########
@@ -404,11 +404,49 @@ protected virtual object ReadArray(object reuse, 
ArraySchema writerSchema, Schem
             ArraySchema rs = (ArraySchema)readerSchema;
             object result = CreateArray(reuse, rs);
             int i = 0;
-            for (int n = (int)d.ReadArrayStart(); n != 0; n = 
(int)d.ReadArrayNext())
+            long minBytes = MinBytesPerElement(writerSchema.ItemSchema);
+            long total = 0;
+            for (long nl = d.ReadArrayStart(); nl != 0; nl = d.ReadArrayNext())
             {
-                if (GetArraySize(result) < (i + n)) ResizeArray(ref result, i 
+ n);
+                // Reject a block whose element count could not be backed by 
the
+                // bytes remaining (or, for zero-byte elements, that exceeds 
the
+                // item cap) before allocating for it. Checked on the raw long,
+                // which also avoids the int cast below overflowing.
+                total = EnsureCollectionAvailable(d, total, nl, minBytes);
+                int n = (int)nl;
+                // Preallocate only a bounded amount up front, then grow on 
demand
+                // below. On a non-seekable stream EnsureCollectionAvailable 
cannot
+                // bound the count, so resizing straight to i+n could allocate 
a
+                // huge array before any element is read; a truncated stream 
instead
+                // fails within Read() after a bounded growth. Blocks no 
larger than
+                // the cap keep the original single-resize fast path.
+                int prealloc = i + Math.Min(n, MaxCollectionPrealloc);
+                if (GetArraySize(result) < prealloc) ResizeArray(ref result, 
prealloc);

Review Comment:
   Already handled — prealloc is computed in long (`Math.Min((long)i + 
Math.Min(n, MaxCollectionPrealloc), MaxCollectionStructural)`) then cast to int 
(GenericReader.cs:425).



##########
lang/csharp/src/apache/main/Generic/GenericReader.cs:
##########
@@ -501,6 +533,164 @@ protected virtual object ReadMap(object reuse, MapSchema 
writerSchema, Schema re
             return result;
         }
 
+        /// <summary>
+        /// Minimum number of bytes a single value of the given schema can 
occupy
+        /// on the wire. Used to reject an array/map block count that could 
not be
+        /// backed by the bytes remaining. A type that encodes to zero bytes
+        /// returns 0 (not only <c>null</c>, but also composites that encode to
+        /// nothing, e.g. a record whose fields are all zero-byte), which 
disables
+        /// the bytes-remaining check for it (so an array of such elements is 
not
+        /// falsely rejected; they are instead bounded by the zero-byte item 
cap).
+        /// A depth limit breaks self-referencing schemas.
+        /// </summary>
+        private static int MinBytesPerElement(Schema schema, int depth = 0)
+        {
+            if (schema == null)
+            {
+                return 0;
+            }
+
+            switch (schema.Tag)
+            {
+                case Schema.Type.Null:
+                    return 0;
+                case Schema.Type.Float:
+                    return 4;
+                case Schema.Type.Double:
+                    return 8;
+                case Schema.Type.Fixed:
+                    return ((FixedSchema)schema).Size;
+                case Schema.Type.Record:
+                case Schema.Type.Error:
+                    if (depth > 64)
+                    {
+                        // A cyclic or pathologically deep record. Return 1 
(not
+                        // 0) so the collection check stays enabled; a valid
+                        // recursive value always encodes to >= 1 byte. The 
depth
+                        // guard is applied only here, so zero-byte leaf types
+                        // such as null still return 0 regardless of depth.
+                        return 1;
+                    }
+
+                    // Accumulate in a long and clamp so a deeply nested schema
+                    // cannot overflow int into a value <= 0, which would 
disable
+                    // the collection check.
+                    long total = 0;
+                    foreach (Field f in (RecordSchema)schema)
+                    {
+                        total += MinBytesPerElement(f.Schema, depth + 1);
+                        if (total >= int.MaxValue)
+                        {
+                            return int.MaxValue;
+                        }
+                    }
+
+                    return (int)total;
+                default:
+                    // boolean, int, long, bytes, string, enum, union, array, 
map:
+                    // all encode to at least one byte.
+                    return 1;
+            }
+        }
+
+        // Collection allocation limits, guarding against a block-count DoS. 
Both
+        // default to the same values as the other Avro SDKs and can be 
overridden
+        // (to a single value capping both) via the AVRO_MAX_COLLECTION_ITEMS
+        // environment variable.
+        private static readonly long MaxCollectionItems = 
ReadCollectionLimit(10_000_000L);
+
+        // The largest array the runtime can allocate. Mirrors
+        // BinaryDecoder.MaxDotNetArrayLength: the default reader grows its 
backing
+        // array via Array.Resize, which throws 
(OutOfMemoryException/OverflowException)
+        // above this length rather than a deterministic AvroException.
+#if NETSTANDARD2_0
+        private const int MaxDotNetArrayLength = 0x3FFFFFFF;
+#else
+        private const int MaxDotNetArrayLength = 0x7FFFFFC7;
+#endif
+
+        // The structural cap is additionally clamped to the runtime's maximum
+        // array length: the callers cast the (cumulative) block count to int 
to
+        // size .NET collections, and a limit above the max array length (e.g. 
from
+        // a large env override, or int.MaxValue itself) would let a collection
+        // that passes EnsureCollectionAvailable still fault inside 
Array.Resize
+        // instead of failing deterministically.
+        private static readonly long MaxCollectionStructural =
+            Math.Min(ReadCollectionLimit(2147483639L), MaxDotNetArrayLength);
+
+        // Upper bound on how many elements the backing array is grown by in a
+        // single step while decoding. The array still grows to hold every 
element
+        // actually read; this only avoids resizing to the full (possibly
+        // attacker-declared) block count up front, before any element is read.
+        // That matters most for non-seekable streams, where the 
bytes-available
+        // check cannot bound the declared count, so a single Array.Resize to 
the
+        // block count could allocate a huge array before the truncated stream 
is
+        // detected.
+        private const int MaxCollectionPrealloc = 1024;
+
+        private static long ReadCollectionLimit(long defaultValue)
+        {
+            string env = 
Environment.GetEnvironmentVariable("AVRO_MAX_COLLECTION_ITEMS");
+            if (!string.IsNullOrEmpty(env) && long.TryParse(env, out long 
value) && value >= 0)
+            {
+                return value;
+            }
+
+            return defaultValue;
+        }
+
+        /// <summary>
+        /// Rejects a collection (array or map) block that could drive an 
unbounded
+        /// allocation, before allocating for it. A block whose declared 
element
+        /// count could not be backed by the bytes actually remaining is 
rejected;
+        /// zero-byte element blocks (where the bytes-remaining check does not
+        /// apply) are bounded by a cumulative item cap; and every collection 
is
+        /// bounded by a structural cap. Returns the running total across 
blocks.
+        /// </summary>
+        private static long EnsureCollectionAvailable(Decoder d, long total, 
long count, long minBytesPerElement)
+        {
+            // A negative count is corrupt/malicious data (it can also arise 
from
+            // long.MinValue overflow when negating a negative block count), 
and
+            // the callers cast the block count to int; reject it explicitly.
+            if (count < 0)
+            {
+                throw new AvroException($"Invalid negative collection block 
count: {count}");
+            }
+
+            total += count;

Review Comment:
   Already handled — the pre-add bound check `count > MaxCollectionStructural - 
total` rejects before adding, so total can't overflow (GenericReader.cs:679).



##########
lang/csharp/src/apache/main/Generic/GenericReader.cs:
##########
@@ -404,11 +404,38 @@ protected virtual object ReadArray(object reuse, 
ArraySchema writerSchema, Schem
             ArraySchema rs = (ArraySchema)readerSchema;
             object result = CreateArray(reuse, rs);
             int i = 0;
-            for (int n = (int)d.ReadArrayStart(); n != 0; n = 
(int)d.ReadArrayNext())
+            long minBytes = MinBytesPerElement(writerSchema.ItemSchema);
+            long total = 0;
+            for (long nl = d.ReadArrayStart(); nl != 0; nl = d.ReadArrayNext())
             {
-                if (GetArraySize(result) < (i + n)) ResizeArray(ref result, i 
+ n);
+                // Reject a block whose element count could not be backed by 
the
+                // bytes remaining (or, for zero-byte elements, that exceeds 
the
+                // item cap) before allocating for it. Checked on the raw long,
+                // which also avoids the int cast below overflowing.
+                total = EnsureCollectionAvailable(d, total, nl, minBytes);
+                int n = (int)nl;
+                // Preallocate only a bounded amount up front, then grow on 
demand
+                // below. On a non-seekable stream EnsureCollectionAvailable 
cannot
+                // bound the count, so resizing straight to i+n could allocate 
a
+                // huge array before any element is read; a truncated stream 
instead
+                // fails within Read() after a bounded growth. Blocks no 
larger than
+                // the cap keep the original single-resize fast path.
+                int prealloc = i + Math.Min(n, MaxCollectionPrealloc);
+                if (GetArraySize(result) < prealloc) ResizeArray(ref result, 
prealloc);
                 for (int j = 0; j < n; j++, i++)
                 {
+                    if (GetArraySize(result) <= i)
+                    {
+                        int current = GetArraySize(result);
+                        int grown = current + (current >> 1) + 1;
+                        if (grown <= i)
+                        {
+                            grown = i + 1;
+                        }
+
+                        ResizeArray(ref result, grown);

Review Comment:
   Already handled — the geometric grow clamps to MaxCollectionStructural 
(which is itself Math.Min'd with MaxDotNetArrayLength) before ResizeArray 
(GenericReader.cs:445-450, 633).



##########
lang/csharp/src/apache/main/Generic/GenericReader.cs:
##########
@@ -501,6 +514,134 @@ protected virtual object ReadMap(object reuse, MapSchema 
writerSchema, Schema re
             return result;
         }
 
+        /// <summary>
+        /// Minimum number of bytes a single value of the given schema can 
occupy
+        /// on the wire. Used to reject an array/map block count that could 
not be
+        /// backed by the bytes remaining. A type that can encode to zero bytes
+        /// (null) returns 0, which disables the collection check for it (so an
+        /// array of nulls is not falsely rejected). A depth limit breaks
+        /// self-referencing schemas.
+        /// </summary>
+        private static int MinBytesPerElement(Schema schema, int depth = 0)
+        {
+            if (schema == null)
+            {
+                return 0;
+            }
+
+            switch (schema.Tag)
+            {
+                case Schema.Type.Null:
+                    return 0;
+                case Schema.Type.Float:
+                    return 4;
+                case Schema.Type.Double:
+                    return 8;
+                case Schema.Type.Fixed:
+                    return ((FixedSchema)schema).Size;
+                case Schema.Type.Record:
+                case Schema.Type.Error:
+                    if (depth > 64)
+                    {
+                        // A cyclic or pathologically deep record. Return 1 
(not
+                        // 0) so the collection check stays enabled; a valid
+                        // recursive value always encodes to >= 1 byte. The 
depth
+                        // guard is applied only here, so zero-byte leaf types
+                        // such as null still return 0 regardless of depth.
+                        return 1;
+                    }
+
+                    // Accumulate in a long and clamp so a deeply nested schema
+                    // cannot overflow int into a value <= 0, which would 
disable
+                    // the collection check.
+                    long total = 0;
+                    foreach (Field f in (RecordSchema)schema)
+                    {
+                        total += MinBytesPerElement(f.Schema, depth + 1);
+                        if (total >= int.MaxValue)
+                        {
+                            return int.MaxValue;
+                        }
+                    }
+
+                    return (int)total;
+                default:
+                    // boolean, int, long, bytes, string, enum, union, array, 
map:
+                    // all encode to at least one byte.
+                    return 1;
+            }
+        }
+
+        // Collection allocation limits, guarding against a block-count DoS. 
Both
+        // default to the same values as the other Avro SDKs and can be 
overridden
+        // (to a single value capping both) via the AVRO_MAX_COLLECTION_ITEMS
+        // environment variable.
+        private static readonly long MaxCollectionItems = 
ReadCollectionLimit(10_000_000L);
+        private static readonly long MaxCollectionStructural = 
ReadCollectionLimit(2147483639L);
+
+        private static long ReadCollectionLimit(long defaultValue)
+        {
+            string env = 
Environment.GetEnvironmentVariable("AVRO_MAX_COLLECTION_ITEMS");
+            if (!string.IsNullOrEmpty(env) && long.TryParse(env, out long 
value) && value >= 0)
+            {
+                return value;
+            }
+
+            return defaultValue;
+        }

Review Comment:
   Addressed — MaxCollectionStructural now clamps ReadCollectionLimit with 
Math.Min(..., MaxDotNetArrayLength), so even a higher AVRO_MAX_COLLECTION_ITEMS 
can't push the structural cap past the runtime array limit.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to