Throughout 2024, the PVS-Studio team has been actively sharing articles about checking open-source C# projects. Continuing the tradition, we've compiled the Top 10 of the most intriguing bugs detected this year. Enjoy reading!
There are several criteria the project code should meet to earn a place in our top list:
As we deliver such tops annually, we've gathered a wealth of curious bugs uncovered over the past years. You can explore the previous compilations of C# bugs here:
P.S. Don't take the rankings too seriously—they're based on the author's subjective opinion. If you think this or that bug deserves another place, feel free to leave a comments :)
Now, let's dive into the fascinating abyss of C# errors for 2024!
Our top opens with the analyzer warning highlighted in the article about Unity 6:
public readonly struct SearchField : IEquatable<SearchField>
{
....
public override bool Equals(object other)
{
return other is SearchIndexEntry l && Equals(l);
}
public bool Equals(SearchField other)
{
return string.Equals(name, other.name, StringComparison.Ordinal);
}
}
The PVS-Studio warning:
V3197 The compared value inside the 'Object.Equals' override is converted to the 'SearchIndexEntry' type instead of 'SearchField' that contains the override. SearchItem.cs 634.
As the analyzer message states, in the first Equals
method, the other
parameter is incorrectly casted to the SearchIndexEntry
type instead of SearchField
. This results in the same method overload being invoked when Equals(l)
is called later. Yet, if other
actually belongs to the SearchIndexEntry
type, the code will loop, and the StackOverflowException
will be thrown.
This issue may occur due to careless copy-paste.
The next place is held by a classic error from the article about checking Garnet:
public IEnumerable<long> GetPendingRequests()
{
foreach (var kvp in ctx.prevCtx?.ioPendingRequests)
yield return kvp.Value.serialNum;
foreach (var kvp in ctx.ioPendingRequests)
yield return kvp.Value.serialNum;
}
The PVS-Studio warning:
V3153 Enumerating the result of null-conditional access operator can lead to NullReferenceException. Consider inspecting: ctx.prevCtx?.ioPendingRequests. ClientSession.cs 748
A developer used the ?.
operator here, likely assuming that ctx.prevCtx
could be null
. However, foreach
doesn't work with null
, so the exception will still hit the developer—but when the GetEnumerator
method is called.
As we've mentioned in other articles, many developers may not know this case. For those interested, we've dedicated an article to this topic: Using the ?. operator in foreach: protection against NullReferenceException that doesn't work.
On the one hand, unlike in C++, such errors in C# aren't critical and are quickly detected if the reference becomes null
. In some cases, the null reference may be rare or even impossible, so these errors can persist for a long time and accumulate. Yet, it may pop up in the future. Crashing a program due to a NullReferenceException
can lead to a frustrating and time-consuming search for the issue, especially if the error only occurs on the client side, making it difficult or even impossible to reproduce.
The warning from the nopCommerce check takes the 8th place.
public virtual async Task<....> GetOrderAverageReportLineAsync(....)
{
....
if (!string.IsNullOrEmpty(orderNotes))
{
query = from o in query
join n in _orderNoteRepository.Table on o.Id equals n.OrderId
where n.Note.Contains(orderNotes)
select o;
query.Distinct(); // <=
}
....
}
The PVS-Studio warning:
V3010 The return value of function 'Distinct' is required to be utilized. OrderReportService.cs 342
To delete duplicated items, we can use Distinct
, the LINQ method. That's what developers wanted to do here, but something went wrong. The Distinct
method doesn't modify the collection for which it's called. So, if we don't use the return value of a method, the call is meaningless. This is exactly what happens in this code snippet.
Most likely, the result of the Distinct
execution should be assigned to the query
variable.
We're moving on. This error lurks in the Unity 6.
void UpdateInfo()
{
....
var infoLine3_format = "<color=\"white\">CurrentElement:" +
" Visible:{0}" +
" Enable:{1}" +
" EnableInHierarchy:{2}" +
" YogaNodeDirty:{3}";
m_InfoLine3.text = string.Format(infoLine3_format,
m_LastDrawElement.visible,
m_LastDrawElement.enable,
m_LastDrawElement.enabledInHierarchy,
m_LastDrawElement.isDirty);
var infoLine4_format = "<color=\"white\">" +
"Count of ZeroSize Element:{0} {1}%" +
" Count of Out of Root Element:{0} {1}%";
m_InfoLine4.text = string.Format(infoLine4_format,
countOfZeroSizeElement,
100.0f * countOfZeroSizeElement / count,
outOfRootVE,
100.0f * outOfRootVE / count);
....
}
The PVS-Studio warning:
V3025 Incorrect format. A different number of format items is expected while calling 'Format' function. Arguments not used: 3rd, 4th. UILayoutDebugger.cs 179.
Let's take a look at the second string.Format(....)
. Its format string (the first argument) has four placeholders, and four values are passed for insertion (from the second to the fifth argument). The issue is that the placeholders contain only 0 and 1. As a result, only the first and second values are inserted.
The fixed format string looks as follows:
var infoLine4_format = "<color=\"white\">" +
"Count of ZeroSize Element:{0} {1}%" +
" Count of Out of Root Element:{2} {3}%";
Rounding out the first half of the top is a bug from the Diablo 3 server emulator:
public static void GrantCriteria(....)
{
....
else
{
....
uint alcount = alreadycrits.CriteriaId32AndFlags8;
var newrecord = D3.Achievements
....
.SetQuantity32(alcount++) // <=
.Build();
int critCount = UnserializeBytes(achievement.Criteria).Count;
if (critCount >= 5)
{
GrantCriteria(client, 74987246353740);
}
client.Account.GameAccount.AchievementCriteria
.Remove(alreadycrits);
client.Account.GameAccount.AchievementCriteria
.Add(newrecord);
}
....
}
The PVS-Studio warning:
V3159 Modified value of the 'alcount' operand is not used after the postfix increment operation. AchievementManager.cs 360
We expect that alcount
should be incremented by 1, and then the resulting value should be passed to the SetQuantity32
method. In reality, the opposite happens: first, the alcount
value is passed to the method, and only then the variable value is incremented by 1.
We can easily fix the error by replacing alcount++
with the alcount + 1
or ++alcount
expression.
An error detected in .NET 8 kicks off the top five! It's worth noting that we've analyzed this project at the very end of 2023, so its errors didn't make it into the previous top list. But we simply couldn't resist adding errors from such a renowned project to the compilation. Here's one of them:
Instruction[]? GetArgumentsOnStack (MethodDefinition method)
{
int length = method.GetMetadataParametersCount ();
Debug.Assert (length != 0);
if (stack_instr?.Count < length)
return null;
var result = new Instruction[length];
while (length != 0)
result[--length] = stack_instr!.Pop (); // <=
return result;
}
The PVS-Studio warning:
V3125 The 'stack_instr!' object was used after it was verified against null. Check lines: 1918, 1913. UnreachableBlocksOptimizer.cs 1918
A developer used the ?.
operator, suggesting that the stack_instr
field could be null
. Everything seemed fine, there was a check, but... no such luck. It was still possible to dereference a null reference in the specified line. Most likely, the developer thought that the stack_instr?.Count < length
expression with stack_instr
equal to null
would return true
, and the method would exit. But no—the result would be false
.
Moreover, the developer suppressed the compiler message about the possible dereference of the null reference with !
. They might have thought that the compiler static analysis just failed, and it didn't identify the check.
What do you think about a nullable context? If you're interested in our opinion or not yet familiar with it, I suggest you read our articles:
In the fourth place, there's also an error from .NET 8:
private static string GetTypeNameDebug(TypeDesc type)
{
string result;
TypeDesc typeDefinition = type.GetTypeDefinition();
if (type != typeDefinition)
{
result = GetTypeNameDebug(typeDefinition) + "<";
for (int i = 0; i < type.Instantiation.Length; i++)
result += (i == 0 ? "" : ",") + GetTypeNameDebug(type.Instantiation[0]);
return result + ">";
}
else
{
....
}
....
}
The PVS-Studio warning:
V3102 Suspicious access to element of 'type.Instantiation' object by a constant index inside a loop. TypeLoaderEnvironment.GVMResolution.cs 32
Here, the following element may be created from information about a type: ConsoleApp1.Program.MyClass<string, int, double>
. However, the type.Instantiation
object is accessed in the loop by a constant index of 0. It's possible that it works as it should, but it looks odd because GetTypeNameDebug(type.Instantiation[i])
is expected.
The bronze place goes to the error from Unity 6. It ranks so high because of its non-obvious nature. You can see more details here:
[RequiredByNativeCode]
internal static void InvalidateAll()
{
lock (s_Instances)
{
foreach (var kvp in s_Instances)
{
WeakReference wr = kvp.Value;
if (wr.IsAlive)
(wr.Target as TextGenerator).Invalidate();
}
}
}
The PVS-Studio warning:
V3145 Unsafe dereference of a WeakReference target. The object could have been garbage collected between checking 'IsAlive' and accessing the 'Target' property. TextGenerator.cs 140.
Despite the small amount of code, this is probably the most complex error in the article. Here are the reasons why:
WeakReference
. The garbage collector can always remove the object referenced by a weak reference, despite the presence of that reference.WeakReference.IsAlive
property enables us to check whether the object referenced by the weak reference still exists. However, this code ignores the possibility that the object can be cleaned after passing the check but before the reference is dereferenced. As a result, the NullReferenceException
may be thrown anyway.lock
operator could protect the object from the garbage collector, but after reproducing the case, we discovered this is not true.So, how do we protect ourselves in this case? To ensure that the NullReferenceException
doesn't occur, we need to create a strong reference to the object. Then, the garbage collector will no longer delete it as long as the reference is still in use. In other words, create a simple local variable referring to the object and work with it. The safe method can look like this:
[RequiredByNativeCode]
internal static void InvalidateAll()
{
lock (s_Instances)
{
foreach (var kvp in s_Instances)
{
WeakReference wr = kvp.Value;
var target = wr.Target;
If (target != null)
(target as TextGenerator).Invalidate();
}
}
}
The second place goes to an error from the article on analyzing ScreenToGif. It's notable for also being a vulnerability. What this issue is and what it can lead to, we'll explore next:
public static string ReplaceRegexInName(string name)
{
const string dateTimeFileNameRegEx = @"[?]([ymdhsfzgkt]+[-_ ]*)+[?]"; // <=
if (!Regex.IsMatch(name, dateTimeFileNameRegEx, RegexOptions.IgnoreCase))
return name;
var match = Regex.Match(name, dateTimeFileNameRegEx, RegexOptions.IgnoreCase);
var date = DateTime.Now.ToString(Regex.Replace(match.Value, "[?]", ""));
return name.Replace(match.ToString(), date);
}
The PVS-Studio warning:
V5626 Possible ReDoS vulnerability. Potentially tainted data from the 'name' variable is processed by regular expression that contains unsafe pattern: '(...sfzgkt]+...)+'
The code contains a ReDoS vulnerability that can slow down or completely crash an application. The issue stems from using a vulnerable regular expression, in which an arbitrary string can be passed. If the input line is composed in a certain way, the application will hang.
The code contains the following vulnerable regular expression: "[?]([ymdhsfzgkt]+[-_ ]*)+[?]"
. Let's simplify it for clarity: "[?]([ymdhsfzgkt]+)+[?]"
. The problematic pattern is "(x+)+"
here. It significantly slows down the regular expression when a certain string is passed. This vulnerability is relevant only for algorithms based on a nondeterministic finite automaton.
We've managed to reproduce it in the application without much difficulty:
As shown in the screenshot, if we enter a certain line in the output file name, the application will hang.
In our case, we used the following line: "?ymdhsfzgktymdhsfzgktymdhsfzgktymdhsfzgktymdhsfzgkt"
.
Indeed, it's unlikely that anyone would use a name like that, so the vulnerability is harmless here. However, if one lets this happen in the server-related code, it can slow down its work or even cause a crash.
You can read more about this vulnerability in the article: Catastrophic backtracking: how can a regular expression cause a ReDoS vulnerability?
PVS-Studio is a SAST solution that uses static analysis to detect various potential vulnerabilities. This helps improve the security of tested applications. The analyzer can find many security flaws, including SQL and LDAP injections, XSS, XXE, and others.
Finally, we've reached the top spot! The error is straightforward, but that doesn't make it any less intriguing. It seems that any developer may easily make this mistake. By the way, this one is also from .NET 8.
You can try to find it yourself.
public void WriteTo(TextWriter writer, int methodRva, bool dumpRva)
{
....
switch (Flags & CorExceptionFlag.COR_ILEXCEPTION_CLAUSE_KIND_MASK)
{
case CorExceptionFlag.COR_ILEXCEPTION_CLAUSE_NONE:
writer.Write($" CATCH: {0}", ClassName ?? "null");
break;
case CorExceptionFlag.COR_ILEXCEPTION_CLAUSE_FILTER:
writer.Write($" FILTER (RVA {0:X4})",
ClassTokenOrFilterOffset + methodRva);
break;
....
}
....
}
The PVS-Studio warning:
V3025 Incorrect format. A different number of format items is expected while calling 'Write' function. Arguments not used: ClassName ?? "null". EHInfo.cs 135
Here's another format string error, but in the TextWriter
class. A developer uses the string interpolation $
character. It simply substitutes 0 into the string, and the format string will be equal to " CATCH: 0"
. As a result, the text they want to substitute for the {0}
placeholder isn't used. The same error occurs in the next case
method.
While 2024 may not have been packed with articles about analyzing C# projects, that didn't stop us from creating this top list. From a relatively small number of articles, we've tried to select the most interesting and diverse errors. You can rate how well we did in the comments :)
You can also check your own project at any time—I've left here a link to the page where you can download PVS-Studio. Enjoy the holidays, and see you in the new year!
0