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 1/2] 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)); + } } } From 395bf8e630946de52a64ed366d4a1bb2d2756a59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isma=C3=ABl=20Mej=C3=ADa?= Date: Sun, 23 Aug 2026 10:40:59 +0200 Subject: [PATCH 2/2] AVRO-3235: [java] Add regression test for enum resolution across namespaces Enums (and their enclosing records) that share the same simple name but live in different namespaces must still resolve, so data versioned by namespace stays backward/forward compatible. This behaviour is already correct in the resolver (enum matching uses the simple name), but there was no dedicated regression test for the cross-namespace case reported in AVRO-3235. Adds resolveEnumAcrossNamespaces: asserts the enum action is an EnumAdjust (not NAMES_DONT_MATCH) and does a full v1 -> v2 record round-trip where the reader adds a defaulted field, confirming the enum resolves and the default is inserted. --- .../java/org/apache/avro/TestResolver.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/lang/java/avro/src/test/java/org/apache/avro/TestResolver.java b/lang/java/avro/src/test/java/org/apache/avro/TestResolver.java index 1d3919319af..546a724a956 100644 --- a/lang/java/avro/src/test/java/org/apache/avro/TestResolver.java +++ b/lang/java/avro/src/test/java/org/apache/avro/TestResolver.java @@ -22,8 +22,13 @@ import org.apache.avro.data.TimeConversions; import org.apache.avro.generic.IndexedRecord; +import org.apache.avro.generic.GenericData; +import org.apache.avro.generic.GenericDatumReader; +import org.apache.avro.generic.GenericDatumWriter; +import org.apache.avro.generic.GenericRecord; import org.apache.avro.io.DatumReader; import org.apache.avro.io.DecoderFactory; +import org.apache.avro.io.EncoderFactory; import org.apache.avro.io.FastReaderBuilder; import org.apache.avro.io.JsonDecoder; import org.hamcrest.MatcherAssert; @@ -103,6 +108,44 @@ void resolveEnum() throws IOException { Assertions.assertEquals("e3", result.get(0).toString()); } + // AVRO-3235: enums (and their enclosing records) that share the same simple + // name but live in different namespaces must still resolve, so that data + // versioned by namespace stays backward/forward compatible. + @Test + void resolveEnumAcrossNamespaces() throws IOException { + final Schema writeEnum = Schema.createEnum("Status", "", "test.v1", Arrays.asList("ON", "OFF")); + final Schema readEnum = Schema.createEnum("Status", "", "test.v2", Arrays.asList("ON", "OFF")); + + // The enum action must be an EnumAdjust, not a NAMES_DONT_MATCH error. + Resolver.Action enumAction = Resolver.resolve(writeEnum, readEnum); + MatcherAssert.assertThat(enumAction, Matchers.instanceOf(Resolver.EnumAdjust.class)); + + // Full record round-trip: writer record in v1, reader record in v2 with an + // extra defaulted field; the enum resolves and the default is inserted. + Schema writeRecord = Schema.createRecord("Simple", "", "test.v1", false, + Arrays.asList(new Schema.Field("name", Schema.create(Schema.Type.STRING), ""), + new Schema.Field("status", writeEnum, "", "ON"))); + Schema readRecord = Schema.createRecord("Simple", "", "test.v2", false, + Arrays.asList(new Schema.Field("name", Schema.create(Schema.Type.STRING), ""), + new Schema.Field("description", Schema.create(Schema.Type.STRING), "", ""), + new Schema.Field("status", readEnum, "", "ON"))); + + GenericData.Record rec = new GenericData.Record(writeRecord); + rec.put("name", "A"); + rec.put("status", new GenericData.EnumSymbol(writeEnum, "ON")); + java.io.ByteArrayOutputStream bos = new java.io.ByteArrayOutputStream(); + org.apache.avro.io.Encoder enc = EncoderFactory.get().binaryEncoder(bos, null); + new GenericDatumWriter(writeRecord).write(rec, enc); + enc.flush(); + + GenericDatumReader datumReader = new GenericDatumReader<>(writeRecord, readRecord); + GenericRecord out = datumReader.read(null, + DecoderFactory.get().binaryDecoder(bos.toByteArray(), null)); + Assertions.assertEquals("A", out.get("name").toString()); + Assertions.assertEquals("", out.get("description").toString()); + Assertions.assertEquals("ON", out.get("status").toString()); + } + @Test void promoteIsValid() { Assertions.assertThrows(IllegalArgumentException.class,