Skip to content

Commit 8bd8975

Browse files
authored
Merge pull request #568 from calumgrant/cs/index-out-of-bounds
C#: Fix false-positives in cs/index-out-of-bounds
2 parents 11ca7b7 + 6a1ab51 commit 8bd8975

File tree

3 files changed

+122
-11
lines changed

3 files changed

+122
-11
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Improvements to C# analysis
2+
3+
## General improvements
4+
5+
## New queries
6+
7+
| **Query** | **Tags** | **Purpose** |
8+
|-----------------------------|-----------|--------------------------------------------------------------------|
9+
10+
## Changes to existing queries
11+
12+
| *@name of query (Query ID)*| *Impact on results* | *How/why the query has changed* |
13+
| Off-by-one comparison against container length (cs/index-out-of-bounds) | Fewer false positives | Results have been removed when there are additional guards on the index. |
14+
15+
## Changes to code extraction
16+
17+
## Changes to QL libraries
18+
19+
## Changes to the autobuilder

csharp/ql/src/Likely Bugs/Collections/ContainerLengthCmpOffByOne.ql

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,53 @@
1414

1515
import csharp
1616
import semmle.code.csharp.controlflow.Guards
17+
import semmle.code.csharp.commons.ComparisonTest
1718

18-
from RelationalOperation cmp, Variable array, Variable index, ElementAccess ea, VariableAccess indexAccess
19-
// Look for `index <= array.Length` or `array.Length >= index`
20-
where (cmp instanceof GEExpr or cmp instanceof LEExpr)
21-
and cmp.getGreaterOperand() = any(PropertyAccess pa | pa.getQualifier() = array.getAnAccess() and pa.getTarget().hasName("Length"))
22-
and cmp.getLesserOperand() = index.getAnAccess()
19+
/** A comparison of an index variable with the length of an array. */
20+
class IndexGuard extends ComparisonTest {
21+
VariableAccess indexAccess;
22+
Variable array;
23+
24+
IndexGuard() {
25+
this.getFirstArgument() = indexAccess and
26+
this.getSecondArgument() = any(PropertyAccess lengthAccess |
27+
lengthAccess.getQualifier() = array.getAnAccess() and
28+
lengthAccess.getTarget().hasName("Length")
29+
)
30+
}
31+
32+
/** Holds if this comparison applies to array `arr` and index `ind`. */
33+
predicate controls(Variable arr, Variable ind) {
34+
arr = array and
35+
ind.getAnAccess() = indexAccess
36+
}
37+
38+
/** Holds if this comparison guards `expr`. */
39+
predicate guards(GuardedExpr expr, boolean condition) {
40+
expr.isGuardedBy(this.getExpr(), indexAccess, condition)
41+
}
42+
43+
/** Holds if this comparison is an incorrect `<=` or equivalent. */
44+
predicate isIncorrect() {
45+
this.getComparisonKind().isLessThanEquals()
46+
}
47+
}
48+
49+
from IndexGuard incorrectGuard, Variable array, Variable index, ElementAccess ea, GuardedExpr indexAccess
50+
where
51+
// Look for `index <= array.Length` or `array.Length >= index`
52+
incorrectGuard.controls(array, index) and
53+
incorrectGuard.isIncorrect() and
2354
// Look for `array[index]`
24-
and ea.getQualifier() = array.getAnAccess()
25-
and ea.getIndex(0) = indexAccess
26-
and indexAccess = index.getAnAccess()
55+
ea.getQualifier() = array.getAnAccess() and
56+
ea.getIndex(0) = indexAccess and
57+
indexAccess = index.getAnAccess() and
2758
// Where the index access is guarded by the comparison
28-
and indexAccess.(GuardedExpr).isGuardedBy(cmp, index.getAnAccess(), true)
29-
select cmp, "Off-by-one index comparison against length leads to possible out of bounds $@.", ea, ea.toString()
59+
incorrectGuard.guards(indexAccess, true) and
60+
// And there are no other guards
61+
not exists(IndexGuard validGuard |
62+
not validGuard.isIncorrect() and
63+
validGuard.controls(array, index) and
64+
validGuard.guards(indexAccess, _)
65+
)
66+
select incorrectGuard, "Off-by-one index comparison against length leads to possible out of bounds $@.", ea, ea.toString()

csharp/ql/test/query-tests/Likely Bugs/Collections/ContainerLengthCmpOffByOne/ContainerLengthCmpOffByOne.cs

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
class Test
44
{
5-
static void Main(string[] args)
5+
void Test1(string[] args)
66
{
77
// BAD: Loop upper bound is off-by-one
88
for (int i = 0; i <= args.Length; i++)
@@ -34,11 +34,66 @@ static void Main(string[] args)
3434
{
3535
Console.WriteLine(args[j]);
3636
}
37+
}
38+
39+
void Test2(string[] args)
40+
{
41+
int j = 0;
3742

3843
// GOOD: Correct terminating value
3944
if (args.Length > j)
4045
{
4146
Console.WriteLine(args[j]);
4247
}
4348
}
49+
50+
void Test3(string[] args)
51+
{
52+
// GOOD: Guarded by ternary operator.
53+
for (int i = 0; i <= args.Length; i++)
54+
{
55+
string s = i < args.Length ? args[i] : "";
56+
}
57+
}
58+
59+
void Test4(string[] args)
60+
{
61+
int j = 0;
62+
63+
// GOOD: Guarded by ternary operator.
64+
if( j <= args.Length )
65+
{
66+
string s = j < args.Length ? args[j] : "";
67+
}
68+
}
69+
70+
void Test5(string[] args)
71+
{
72+
// GOOD: A valid test of Length.
73+
for (int i = 0; i != args.Length; i++)
74+
{
75+
Console.WriteLine(args[i]);
76+
}
77+
}
78+
79+
void Test6(string[] args)
80+
{
81+
int j = 0;
82+
83+
// GOOD: There is another correct test.
84+
if( j <= args.Length )
85+
{
86+
if (j == args.Length) return;
87+
Console.WriteLine(args[j]);
88+
}
89+
}
90+
91+
void Test7(string[] args)
92+
{
93+
// GOOD: Guarded by ||.
94+
for (int i = 0; i <= args.Length; i++)
95+
{
96+
bool b = i == args.Length || args[i] == "x";
97+
}
98+
}
4499
}

0 commit comments

Comments
 (0)