Skip to content

AVRO-3569: [csharp] Fix infinite recursion in AvroDecimal IConvertible conversions - #3951

Open
iemejia wants to merge 1 commit into
apache:mainfrom
iemejia:AVRO-3569-csharp-avrodecimal-recursion
Open

iemejia wants to merge 1 commit into
apache:mainfrom
iemejia:AVRO-3569-csharp-avrodecimal-recursion

Conversation

@iemejia

@iemejia iemejia commented Aug 23, 2026

Copy link
Copy Markdown
Member

What

Every explicit IConvertible.ToXxx(IFormatProvider) implementation on AvroDecimal called Convert.ToXxx(this, provider). Since this is an IConvertible, Convert.ToXxx(IConvertible, provider) calls straight back into the same IConvertible.ToXxx(provider) method → unbounded recursion → StackOverflowException, which is uncatchable in .NET and crashes the process.

Reproducer (from the issue):

Convert.ToByte(new AvroDecimal(0)); // StackOverflowException

This affected ToBoolean, ToByte, ToDecimal, ToDouble, ToInt16/32/64, ToSByte, ToSingle, ToUInt16/32/64 and ToString.

Fix

IConvertible.ToType(Type, provider) is the only implementation that does real work (converts the unscaled/scaled value to a decimal and calls Convert.ChangeType). Route the 12 numeric/boolean conversions through it (preserving the IFormatProvider), and make ToString(provider) use the public ToString() override. No Convert.To*(this, provider) self-calls remain.

Tests

Adds TestAvroDecimalIConvertibleDoesNotRecurse exercising all conversions (each would previously overflow the stack). Full C# suite passes (1528 tests, 0 failures).

JIRA: https://issues.apache.org/jira/browse/AVRO-3569

…e 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.
@github-actions github-actions Bot added the C# label Aug 23, 2026

@zcsizmadia zcsizmadia left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @iemejia, nice catch. The fix is correct. Routing through IConvertible.ToType matches what the public static ToXxx(AvroDecimal) helpers and the explicit operators already do via ToType<T>(), so the explicit interface methods now agree with the rest of the type. I ran the new test against main's AvroDecimal.cs and it takes the test host down with a stack overflow, so it does guard the regression.

One thing I'd like to see addressed, here or in a follow-up: IConvertible.ToString(IFormatProvider) now ignores provider and formats with CultureInfo.CurrentCulture. With a scale > 0 value this is visible:

CultureInfo.CurrentCulture = new CultureInfo("de-DE");
Convert.ToString(new AvroDecimal(1.5m), CultureInfo.InvariantCulture); // "1,5", expected "1.5"

The new test doesn't catch it because 42 has no decimal separator. IFormattable.ToString(string, IFormatProvider) has the same issue already, so it may be cleaner to fix both together. For example, add a ToString(IFormatProvider) that uses NumberFormatInfo.GetInstance(provider) for both the D{n} formatting and the separator, and have ToString() pass CultureInfo.CurrentCulture.

Minor: every assertion in the test uses a scale-0 value. Adding one fractional case (e.g. new AvroDecimal(1.5m) → ToDouble == 1.5, ToString(InvariantCulture) == "1.5") would cover the division/remainder path in ToType and the separator handling.

Happy to approve as-is if you'd rather track the provider handling separately.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants