From dd9cdf58d4143e6bb7c59c4904371053d1e02145 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isma=C3=ABl=20Mej=C3=ADa?= Date: Sun, 23 Aug 2026 10:28:59 +0200 Subject: [PATCH] AVRO-3569: [csharp] Fix infinite recursion in AvroDecimal IConvertible conversions Every explicit IConvertible.ToXxx(IFormatProvider) implementation on AvroDecimal called Convert.ToXxx(this, provider). Because 'this' is an IConvertible, Convert.ToXxx(IConvertible, provider) calls back into the same IConvertible.ToXxx(provider) method, recursing until a StackOverflowException (which is uncatchable and crashes the process). Reproducer: Convert.ToByte(new AvroDecimal(0)). Route the 12 numeric/boolean conversions through IConvertible.ToType(type, provider) (the one method that performs the real conversion via Convert.ChangeType on the underlying decimal), and make ToString(provider) use the public ToString() override. Adds a test exercising all conversions. --- lang/csharp/src/apache/main/AvroDecimal.cs | 26 +++++++++---------- .../csharp/src/apache/test/AvroDecimalTest.cs | 24 +++++++++++++++++ 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/lang/csharp/src/apache/main/AvroDecimal.cs b/lang/csharp/src/apache/main/AvroDecimal.cs index ed748ae35f0..563056af021 100644 --- a/lang/csharp/src/apache/main/AvroDecimal.cs +++ b/lang/csharp/src/apache/main/AvroDecimal.cs @@ -917,7 +917,7 @@ TypeCode IConvertible.GetTypeCode() /// bool IConvertible.ToBoolean(IFormatProvider provider) { - return Convert.ToBoolean(this, provider); + return (bool)((IConvertible)this).ToType(typeof(bool), provider); } /// @@ -929,7 +929,7 @@ bool IConvertible.ToBoolean(IFormatProvider provider) /// byte IConvertible.ToByte(IFormatProvider provider) { - return Convert.ToByte(this, provider); + return (byte)((IConvertible)this).ToType(typeof(byte), provider); } /// @@ -967,7 +967,7 @@ DateTime IConvertible.ToDateTime(IFormatProvider provider) /// decimal IConvertible.ToDecimal(IFormatProvider provider) { - return Convert.ToDecimal(this, provider); + return (decimal)((IConvertible)this).ToType(typeof(decimal), provider); } /// @@ -979,7 +979,7 @@ decimal IConvertible.ToDecimal(IFormatProvider provider) /// double IConvertible.ToDouble(IFormatProvider provider) { - return Convert.ToDouble(this, provider); + return (double)((IConvertible)this).ToType(typeof(double), provider); } /// @@ -991,7 +991,7 @@ double IConvertible.ToDouble(IFormatProvider provider) /// short IConvertible.ToInt16(IFormatProvider provider) { - return Convert.ToInt16(this, provider); + return (short)((IConvertible)this).ToType(typeof(short), provider); } /// @@ -1003,7 +1003,7 @@ short IConvertible.ToInt16(IFormatProvider provider) /// int IConvertible.ToInt32(IFormatProvider provider) { - return Convert.ToInt32(this, provider); + return (int)((IConvertible)this).ToType(typeof(int), provider); } /// @@ -1015,7 +1015,7 @@ int IConvertible.ToInt32(IFormatProvider provider) /// long IConvertible.ToInt64(IFormatProvider provider) { - return Convert.ToInt64(this, provider); + return (long)((IConvertible)this).ToType(typeof(long), provider); } /// @@ -1027,7 +1027,7 @@ long IConvertible.ToInt64(IFormatProvider provider) /// sbyte IConvertible.ToSByte(IFormatProvider provider) { - return Convert.ToSByte(this, provider); + return (sbyte)((IConvertible)this).ToType(typeof(sbyte), provider); } /// @@ -1039,7 +1039,7 @@ sbyte IConvertible.ToSByte(IFormatProvider provider) /// float IConvertible.ToSingle(IFormatProvider provider) { - return Convert.ToSingle(this, provider); + return (float)((IConvertible)this).ToType(typeof(float), provider); } /// @@ -1051,7 +1051,7 @@ float IConvertible.ToSingle(IFormatProvider provider) /// string IConvertible.ToString(IFormatProvider provider) { - return Convert.ToString(this, provider); + return ToString(); } /// @@ -1063,7 +1063,7 @@ string IConvertible.ToString(IFormatProvider provider) /// ushort IConvertible.ToUInt16(IFormatProvider provider) { - return Convert.ToUInt16(this, provider); + return (ushort)((IConvertible)this).ToType(typeof(ushort), provider); } /// @@ -1075,7 +1075,7 @@ ushort IConvertible.ToUInt16(IFormatProvider provider) /// uint IConvertible.ToUInt32(IFormatProvider provider) { - return Convert.ToUInt32(this, provider); + return (uint)((IConvertible)this).ToType(typeof(uint), provider); } /// @@ -1087,7 +1087,7 @@ uint IConvertible.ToUInt32(IFormatProvider provider) /// ulong IConvertible.ToUInt64(IFormatProvider provider) { - return Convert.ToUInt64(this, provider); + return (ulong)((IConvertible)this).ToType(typeof(ulong), provider); } /// diff --git a/lang/csharp/src/apache/test/AvroDecimalTest.cs b/lang/csharp/src/apache/test/AvroDecimalTest.cs index c6d0d3a807e..75b39add41f 100644 --- a/lang/csharp/src/apache/test/AvroDecimalTest.cs +++ b/lang/csharp/src/apache/test/AvroDecimalTest.cs @@ -16,6 +16,7 @@ * limitations under the License. */ +using System; using System.Globalization; using NUnit.Framework; @@ -91,5 +92,28 @@ public int TestAvroDecimalCompareTo(string left, string right) return leftAvroDecimal.CompareTo(rightAvroDecimal); } + + // AVRO-3569: the IConvertible conversions previously called + // Convert.ToXxx(this, provider), which recurses back into the same + // IConvertible method and overflows the stack. Verify they now return + // the converted value instead of crashing. + [Test] + public void TestAvroDecimalIConvertibleDoesNotRecurse() + { + var d = new AvroDecimal(42); + Assert.AreEqual((byte)42, Convert.ToByte(d)); + Assert.AreEqual((sbyte)42, Convert.ToSByte(d)); + Assert.AreEqual((short)42, Convert.ToInt16(d)); + Assert.AreEqual(42, Convert.ToInt32(d)); + Assert.AreEqual(42L, Convert.ToInt64(d)); + Assert.AreEqual((ushort)42, Convert.ToUInt16(d)); + Assert.AreEqual(42u, Convert.ToUInt32(d)); + Assert.AreEqual(42ul, Convert.ToUInt64(d)); + Assert.AreEqual(42d, Convert.ToDouble(d)); + Assert.AreEqual(42f, Convert.ToSingle(d)); + Assert.AreEqual(42m, Convert.ToDecimal(d)); + Assert.AreEqual(true, Convert.ToBoolean(d)); + Assert.AreEqual("42", Convert.ToString(d, CultureInfo.InvariantCulture)); + } } }