Browse Source

Fix #1142: recover enum members for out-of-range constant operands

An addition or subtraction on an enum whose constant operand's numeric
value does not fit the underlying type (an int constant standing for a
high uint member, e.g. -501 for 0xfffffe0b) failed to resolve as enum
arithmetic and fell back to integer arithmetic with casts, producing
'(uint)((int)value - -501)' and, for the same source expression in an
argument position, '(uint)value - 4294966795u'. Retry the failed
resolution once with constant operands reinterpreted in the enum type;
the reinterpretation is lossless whenever the constant's stack type
matches the enum's underlying stack type, because the IL constant is
the member's bit pattern. Valid non-enum resolutions like 'data - 1'
(enum minus underlying, yielding the enum) are unaffected because the
retry only runs when the plain resolution fails.

Assisted-by: Claude:claude-fable-5:Claude Code
pull/3897/head
Siegfried Pammer 2 months ago committed by Siegfried Pammer
parent
commit
63d03f1b49
  1. 3
      ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
  2. 6
      ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs
  3. 22
      ICSharpCode.Decompiler.Tests/TestCases/ILPretty/EnumArithmeticOutOfRange.cs
  4. 50
      ICSharpCode.Decompiler.Tests/TestCases/ILPretty/EnumArithmeticOutOfRange.il
  5. 52
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/TypeAnalysisTests.cs
  6. 28
      ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs

3
ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj

@ -126,6 +126,7 @@ @@ -126,6 +126,7 @@
<None Include="TestCases\ILPretty\Issue2104.il" />
<None Include="TestCases\ILPretty\Issue3421.il" />
<None Include="TestCases\ILPretty\WeirdEnums.il" />
<None Include="TestCases\ILPretty\EnumArithmeticOutOfRange.il" />
<None Include="TestCases\ILPretty\ConstantBlobs.il" />
<None Include="TestCases\ILPretty\CS1xSwitch_Debug.il" />
<None Include="TestCases\ILPretty\CS1xSwitch_Release.il" />
@ -199,6 +200,8 @@ @@ -199,6 +200,8 @@
<Compile Remove="TestCases\ILPretty\InstanceOperatorCall.cs" />
<None Include="TestCases\ILPretty\InstanceOperatorCall.cs" />
<None Include="TestCases\ILPretty\InstanceOperatorCall.il" />
<Compile Remove="TestCases\ILPretty\EnumArithmeticOutOfRange.cs" />
<None Include="TestCases\ILPretty\EnumArithmeticOutOfRange.cs" />
<Compile Remove="TestCases\ILPretty\EmptyBodies.cs" />
<None Include="TestCases\ILPretty\EmptyBodies.cs" />
<Compile Remove="TestCases\ILPretty\TruncatedAccessorBody.cs" />

6
ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs

@ -359,6 +359,12 @@ namespace ICSharpCode.Decompiler.Tests @@ -359,6 +359,12 @@ namespace ICSharpCode.Decompiler.Tests
await Run();
}
[Test]
public async Task EnumArithmeticOutOfRange()
{
await Run();
}
[Test]
public async Task GuessAccessors()
{

22
ICSharpCode.Decompiler.Tests/TestCases/ILPretty/EnumArithmeticOutOfRange.cs

@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
// The constants are out of range for the enums' underlying types, so they must not be
// recovered as the members whose bit patterns they happen to truncate to (Val1 in both
// cases): the enum arithmetic would compute a different result than the IL does.
public enum ByteEnum : byte
{
Val1 = 112
}
public static class EnumArithmeticOutOfRange
{
public static int SubtractFromUShortEnum(UShortEnum value)
{
return (int)value - -501;
}
public static int SubtractFromByteEnum(ByteEnum value)
{
return (int)value - 70000;
}
}
public enum UShortEnum : ushort
{
Val1 = 65035
}

50
ICSharpCode.Decompiler.Tests/TestCases/ILPretty/EnumArithmeticOutOfRange.il

@ -0,0 +1,50 @@ @@ -0,0 +1,50 @@
#define CORE_ASSEMBLY "System.Runtime"
.assembly extern CORE_ASSEMBLY
{
.publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A ) // .?_....:
.ver 4:0:0:0
}
// The constant operands below are out of range for the enums' underlying types, but their
// low bits do match a member: -501 truncates to UShortEnum.Val1 (0xfe0b) and 70000 to
// ByteEnum.Val1 (0x70). Reinterpreting them as those members would change the result of the
// subtraction, so the decompiler has to keep the integer arithmetic.
.class public auto ansi sealed UShortEnum
extends [CORE_ASSEMBLY]System.Enum
{
.field public specialname rtspecialname uint16 value__
.field public static literal valuetype UShortEnum Val1 = uint16(0xfe0b)
}
.class public auto ansi sealed ByteEnum
extends [CORE_ASSEMBLY]System.Enum
{
.field public specialname rtspecialname uint8 value__
.field public static literal valuetype ByteEnum Val1 = uint8(0x70)
}
.class public auto ansi abstract sealed beforefieldinit EnumArithmeticOutOfRange
extends [CORE_ASSEMBLY]System.Object
{
.method public hidebysig static int32 SubtractFromUShortEnum (valuetype UShortEnum 'value') cil managed
{
.maxstack 8
ldarg.0
ldc.i4 -501
sub
ret
}
.method public hidebysig static int32 SubtractFromByteEnum (valuetype ByteEnum 'value') cil managed
{
.maxstack 8
ldarg.0
ldc.i4 70000
sub
ret
}
}

52
ICSharpCode.Decompiler.Tests/TestCases/Pretty/TypeAnalysisTests.cs

@ -245,6 +245,39 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -245,6 +245,39 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
return byteArray[(ushort)i];
}
public void EnumSubtractionWithHighBitValues(UIntEnum value)
{
if (value - UIntEnum.Val1 <= 4)
{
Console.WriteLine(value - UIntEnum.Val1);
}
}
public void EnumSubtractionWithHighBitValues16(UShortEnum value)
{
// A 16-bit enum member always fits its underlying type as a positive constant,
// so the compiler emits it as such and enum-minus-underlying resolves right away.
if ((int)(value - 65035) <= 4)
{
Console.WriteLine((int)(value - 65035));
}
}
public void EnumSubtractionWithHighBitValues64(ULongEnum value)
{
if (value - ULongEnum.Val1 <= 4)
{
Console.WriteLine(value - ULongEnum.Val1);
}
}
public void EnumSubtractionWithZeroExtendedValue(ULongEnum value)
{
// Val3 is emitted as a zero-extended uint constant (ldc.i4.m1 + conv.u8), so it
// must not be reinterpreted as the sign-extended long -1.
Console.WriteLine((ulong)(value - uint.MaxValue));
}
public StringComparison EnumDiffNumber(StringComparison data)
{
return data - 1;
@ -345,4 +378,23 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -345,4 +378,23 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
return (int)a == 0;
}
}
public enum UIntEnum : uint
{
Val1 = 4294966795u,
Val2 = 4294966799u
}
public enum ULongEnum : ulong
{
Val1 = 18446744073709551115uL,
Val2 = 18446744073709551119uL,
Val3 = 4294967295uL
}
public enum UShortEnum : ushort
{
Val1 = 65035,
Val2 = 65039
}
}

28
ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs

@ -1711,6 +1711,26 @@ namespace ICSharpCode.Decompiler.CSharp @@ -1711,6 +1711,26 @@ namespace ICSharpCode.Decompiler.CSharp
}
var rr = resolverWithOverflowCheck.ResolveBinaryOperator(op, left.ResolveResult, right.ResolveResult);
if ((rr.IsError || NullableType.GetUnderlyingType(rr.Type).GetStackType() != inst.UnderlyingResultType
|| !IsCompatibleWithSign(rr.Type, inst.Sign))
&& op is BinaryOperatorType.Add or BinaryOperatorType.Subtract
&& (left.Type.Kind == TypeKind.Enum || right.Type.Kind == TypeKind.Enum))
{
// enum +/- constant did not resolve (e.g. an int constant whose numeric value
// does not fit the unsigned underlying type, issue #1142); the IL constant is
// the enum's bit pattern, so retry with it reinterpreted in the enum type
// before falling back to integer arithmetic with casts.
var adjustedLeft = AdjustConstantExpressionToType(left, right.Type);
var adjustedRight = AdjustConstantExpressionToType(right, left.Type);
var adjustedRR = resolverWithOverflowCheck.ResolveBinaryOperator(op, adjustedLeft.ResolveResult, adjustedRight.ResolveResult);
if (!(adjustedRR.IsError || NullableType.GetUnderlyingType(adjustedRR.Type).GetStackType() != inst.UnderlyingResultType
|| !IsCompatibleWithSign(adjustedRR.Type, inst.Sign)))
{
left = adjustedLeft;
right = adjustedRight;
rr = adjustedRR;
}
}
if (rr.IsError || NullableType.GetUnderlyingType(rr.Type).GetStackType() != inst.UnderlyingResultType
|| !IsCompatibleWithSign(rr.Type, inst.Sign))
{
@ -4006,7 +4026,13 @@ namespace ICSharpCode.Decompiler.CSharp @@ -4006,7 +4026,13 @@ namespace ICSharpCode.Decompiler.CSharp
}
else if (typeHint.Kind == TypeKind.Enum || typeHint.IsKnownType(KnownTypeCode.Char) || typeHint.IsCSharpSmallIntegerType())
{
var castRR = resolver.WithCheckForOverflow(true).ResolveCast(typeHint, rr);
// An IL constant is a bit pattern: converting it to an integer type at least as
// wide only reinterprets it, which is lossless even where the numeric value
// changes sign (e.g. int -501 for a uint-based enum member 0xfffffe0b, or for a
// ulong-based one where the IL sign-extends it with conv.i8). Only narrowing can
// lose information, so limit the overflow check to that case.
bool mayBeLossy = !rr.Type.GetStackType().IsIntegerType() || rr.Type.GetSize() > typeHint.GetSize();
var castRR = resolver.WithCheckForOverflow(mayBeLossy).ResolveCast(typeHint, rr);
if (castRR.IsCompileTimeConstant && !castRR.IsError)
{
rr = castRR;

Loading…
Cancel
Save