Skip to content

Commit 15719ee

Browse files
Reject negative char index in the length-and-char string switch
MatchSwitchOnCharBlock's case 2 and default paths guarded against a negative character index, but case 1 (a bare switch on get_Chars) did not. Crafted IL whose get_Chars/get_Item index is negative - a value no compiler emits, but valid IL - therefore reached the pattern reconstruction unchecked. For a length-1 group this silently miscompiled the switch (it rebuilds the string switch from the char labels without using the index), turning IL that reads s[-1] into `switch (s)`; for longer strings it threw IndexOutOfRangeException and aborted the method. Move the check into MatchGetChars so all three call sites reject a negative index by construction, and drop the two now-redundant guards. Same class of unvalidated-integer robustness issue as #3878, in a different switch-on-string pattern. Assisted-by: Claude:claude-opus-4-8:Claude Code
1 parent e3fbca9 commit 15719ee

4 files changed

Lines changed: 138 additions & 6 deletions

File tree

ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,12 @@ public async Task Issue2260SwitchString()
267267
await Run();
268268
}
269269

270+
[Test]
271+
public async Task SwitchOnStringNegativeCharIndex()
272+
{
273+
await Run();
274+
}
275+
270276
[Test]
271277
public async Task ConstantBlobs()
272278
{
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
namespace ICSharpCode.Decompiler.Tests.TestCases.ILPretty
2+
{
3+
internal class SwitchOnStringNegativeCharIndex
4+
{
5+
public static int M(string s)
6+
{
7+
if (s != null)
8+
{
9+
int length = s.Length;
10+
if (length == 1)
11+
{
12+
switch (s[-1])
13+
{
14+
case 'a':
15+
return 1;
16+
case 'b':
17+
return 2;
18+
case 'c':
19+
return 3;
20+
case 'd':
21+
return 4;
22+
case 'e':
23+
return 5;
24+
case 'f':
25+
return 6;
26+
case 'g':
27+
return 7;
28+
case 'h':
29+
return 8;
30+
}
31+
}
32+
}
33+
return 0;
34+
}
35+
}
36+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Regression fixture: real Roslyn length-1 "switch on string" codegen (switch on s[0]) with the
2+
// get_Chars index edited to -1 - a value no compiler emits. Without a negative-index guard the
3+
// transform silently miscompiles this to `switch (s)` even though the IL reads s[-1]; the fixed
4+
// transform declines and preserves the raw `switch (s[-1])`.
5+
6+
.assembly extern mscorlib
7+
{
8+
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 )
9+
.ver 4:0:0:0
10+
}
11+
.assembly SwitchOnStringNegativeCharIndex
12+
{
13+
.hash algorithm 0x00008004
14+
.ver 1:0:0:0
15+
}
16+
.module SwitchOnStringNegativeCharIndex.dll
17+
18+
.class private auto ansi beforefieldinit ICSharpCode.Decompiler.Tests.TestCases.ILPretty.SwitchOnStringNegativeCharIndex
19+
extends [mscorlib]System.Object
20+
{
21+
.method public hidebysig static int32 M(string s) cil managed
22+
{
23+
24+
.maxstack 2
25+
.locals init (int32 V_0,
26+
char V_1)
27+
IL_0000: ldarg.0
28+
IL_0001: brfalse.s IL_0051
29+
30+
IL_0003: ldarg.0
31+
IL_0004: call instance int32 [mscorlib]System.String::get_Length()
32+
IL_0009: stloc.0
33+
IL_000a: ldloc.0
34+
IL_000b: ldc.i4.1
35+
IL_000c: bne.un.s IL_0051
36+
37+
IL_000e: ldarg.0
38+
IL_000f: ldc.i4.m1
39+
IL_0010: call instance char [mscorlib]System.String::get_Chars(int32)
40+
IL_0015: stloc.1
41+
IL_0016: ldloc.1
42+
IL_0017: ldc.i4.s 97
43+
IL_0019: sub
44+
IL_001a: switch (
45+
IL_0041,
46+
IL_0043,
47+
IL_0045,
48+
IL_0047,
49+
IL_0049,
50+
IL_004b,
51+
IL_004d,
52+
IL_004f)
53+
IL_003f: br.s IL_0051
54+
55+
IL_0041: ldc.i4.1
56+
IL_0042: ret
57+
58+
IL_0043: ldc.i4.2
59+
IL_0044: ret
60+
61+
IL_0045: ldc.i4.3
62+
IL_0046: ret
63+
64+
IL_0047: ldc.i4.4
65+
IL_0048: ret
66+
67+
IL_0049: ldc.i4.5
68+
IL_004a: ret
69+
70+
IL_004b: ldc.i4.6
71+
IL_004c: ret
72+
73+
IL_004d: ldc.i4.7
74+
IL_004e: ret
75+
76+
IL_004f: ldc.i4.8
77+
IL_0050: ret
78+
79+
IL_0051: ldc.i4.0
80+
IL_0052: ret
81+
} // end of method M
82+
83+
.method public hidebysig specialname rtspecialname
84+
instance void .ctor() cil managed
85+
{
86+
87+
.maxstack 8
88+
IL_0000: ldarg.0
89+
IL_0001: call instance void [mscorlib]System.Object::.ctor()
90+
IL_0006: ret
91+
} // end of method .ctor
92+
93+
} // end of class SwitchOnStringNegativeCharIndex
94+

ICSharpCode.Decompiler/IL/Transforms/SwitchOnStringTransform.cs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1354,15 +1354,15 @@ bool MatchGetChars(ILInstruction instruction, ILVariable switchValueVar, out int
13541354
|| call.Method.FullNameIs("System.Span", "get_Item"))
13551355
&& call.Arguments.Count == 2
13561356
&& call.Arguments[0].MatchLdLoca(switchValueVar)
1357-
&& call.Arguments[1].MatchLdcI4(out index);
1357+
&& call.Arguments[1].MatchLdcI4(out index) && index >= 0;
13581358
}
13591359
else
13601360
{
13611361
return instruction is Call call
13621362
&& call.Method.FullNameIs("System.String", "get_Chars")
13631363
&& call.Arguments.Count == 2
13641364
&& call.Arguments[0].MatchLdLoc(switchValueVar)
1365-
&& call.Arguments[1].MatchLdcI4(out index);
1365+
&& call.Arguments[1].MatchLdcI4(out index) && index >= 0;
13661366
}
13671367
}
13681368

@@ -1390,8 +1390,6 @@ bool MatchSwitchOnCharBlock(Block block, int length, ILVariable switchValueVar,
13901390
return false;
13911391
if (!MatchGetChars(getCharsCall, switchValueVar, out index))
13921392
return false;
1393-
if (index < 0)
1394-
return false;
13951393
@switch = block.Instructions[1] as SwitchInstruction;
13961394
if (@switch == null)
13971395
return false;
@@ -1409,8 +1407,6 @@ bool MatchSwitchOnCharBlock(Block block, int length, ILVariable switchValueVar,
14091407
return false;
14101408
if (!MatchGetChars(getCharsCall, switchValueVar, out index))
14111409
return false;
1412-
if (index < 0)
1413-
return false;
14141410
if (analysis.SwitchVariable != charTempVar)
14151411
return false;
14161412
sections = analysis.Sections;

0 commit comments

Comments
 (0)