2025년 오픈 소스에서 발견한 가장 흥미로운 C# 버그 10가지 | akiradoko666


2025년 오픈소스 C# 버그 Top 10

#c-sharp #pvs-studio #static-analysis #open-source #dotnet

PVS-Studio 팀이 2025년 한 해 동안 오픈소스 C# 프로젝트에서 발견한 가장 흥미로운 버그 10가지를 선정하여 공개했습니다. 이 목록에는 .NET 9, MSBuild, ScottPlot, Lean 트레이딩 엔진 등 주요 프로젝트에서 발견된 결함들이 포함되어 있으며, 복사-붙여넣기 오류부터 LINQ 지연 실행과 캡처 변수의 미묘한 상호작용까지 다양한 유형의 버그가 다뤄집니다. 특히 1위로 선정된 버그는 LINQ의 지연 실행 메서드와 캡처 변수를 함께 사용할 때 발생하는 문제로, 개발자들이 놓치기 쉬운 미묘한 오류를 보여줍니다.


선정 기준

선정된 코드는 다음 기준을 충족해야 합니다:

  • 오픈소스 프로젝트에서 발견된 것
  • PVS-Studio에 의해 탐지된 것
  • 오류를 포함할 가능성이 높은 코드
  • 검토할 가치가 있는 흥미로운 코드
  • 각 오류가 고유한 것

10위: ToInt64 대신 ToUInt64 사용 오류

.NET 9 검사 관련 기사에서 언급된 오류입니다.

case TypeCode.Int64: 
  variant = ComVariant.Create(value.ToInt64(ci)); break;
case TypeCode.UInt64: 
  variant = ComVariant.Create(value.ToInt64(ci)); break; // 오류
  • 경고 코드: V3139 - 두 개 이상의 case 분기가 동일한 작업을 수행함
  • 문제점: TypeCode.UInt64 case에서 ToInt64() 대신 ToUInt64() 메서드를 사용해야 함
  • 원인: 복사-붙여넣기 오류로 추정됨

9위: 잘못된 포맷 문자열

Neo 및 NBitcoin 프로젝트 검사에서 발견된 오류입니다.

public override string ToString()
{
  var sb = new StringBuilder();
  sb.AppendFormat("{1:X04} {2,-10}{3}{4}", 
                  Position, 
                  OpCode, 
                  DecodeOperand());
  return sb.ToString();
}
  • 경고 코드: V3025 [CWE-685] - 잘못된 포맷. AppendFormat 호출 시 예상되는 포맷 항목 수가 다름
  • 문제점 1: 삽입할 인자 수가 포맷 문자열의 플레이스홀더 수보다 적어 예외 발생
  • 문제점 2: 플레이스홀더 인덱싱이 0이 아닌 1부터 시작하여 인덱스 4에 해당하는 5번째 인자가 없음

8위: 자기 자신과 비교하는 Equals

Lean 트레이딩 엔진 검사에서 발견된 오류입니다.

public bool Equals(OptionStrategyDefinitionMatch other)
{
  var positions = other.Legs.ToDictionary(leg => leg.Position, 
                                          leg => leg.Multiplier);
  foreach (var leg in other.Legs)  // 오류: this.Legs여야 함
  {
    int multiplier;
    if (!positions.TryGetValue(leg.Position, out multiplier))
      return false;
    if (leg.Multiplier != multiplier)
      return false;
  }
  return true;
}
  • 경고 코드: V3192 - Legs 속성이 GetHashCode에서 사용되지만 Equals에서 누락됨
  • 문제점: other.Legs를 반복하면서 동일한 other.Legs에서 파생된 딕셔너리와 비교함
  • 수정 방법: other.LegsLegs로 교체해야 함

7위: 중복된 Equals 비교

ScottPlot 검사에서 발견된 오류입니다.

public bool Equals(CoordinateRangeMutable? other)
{
  if (other is null)
    return false;
  return Equals(Min, other.Min) && Equals(Min, other.Min);  // 오류
}

public override int GetHashCode()
{
  return Min.GetHashCode() ^ Max.GetHashCode();
}
  • 경고 코드 1: V3192 - Max 속성이 GetHashCode에서 사용되지만 Equals에서 누락됨
  • 경고 코드 2: V3001 - && 연산자 좌우에 동일한 하위 표현식 Equals(Min, other.Min) 존재
  • 수정 방법: && 피연산자 중 하나를 Equals(Max, other.Max)로 변경해야 함

6위: 비트 연산 트릭

ScottPlot 검사에서 발견된 비트 연산 관련 오류입니다.

public static Interactivity.Key GetKey(this Keys keys)
{
  Keys keyCode = keys & ~Keys.Modifiers;
  Interactivity.Key key = keyCode switch
  {
    Keys.Alt => Interactivity.StandardKeys.Alt,        // 도달 불가
    Keys.Shift => Interactivity.StandardKeys.Shift,   // 도달 불가
    Keys.Control => Interactivity.StandardKeys.Control, // 도달 불가
    // ...
  };
}

열거형 값의 이진 표현:

  • Modifiers = 0xFFFF0000

  • Shift = 0x00010000

  • Control = 0x00020000

  • Alt = 0x00040000

  • 경고 코드: V3202 - 도달 불가능한 코드 탐지됨. case 값이 매치 표현식 범위를 벗어남

  • 문제점: ModifiersShift, Control, Alt가 이미 포함되어 있어 keys & ~Keys.Modifiers 연산 후에는 해당 값들이 나올 수 없음

5위: 박싱 문제

.NET 9 검사에서 발견된 구조체 박싱 관련 오류입니다.

struct StackValue
{
  public override bool Equals(object obj)
  {
    if (Object.ReferenceEquals(this, obj))  // 항상 false
      return true;
    // ...
  }
}
  • 경고 코드: V3161 - 값 형식 변수를 ReferenceEquals로 비교하는 것은 this가 박싱되므로 올바르지 않음
  • 문제점: 구조체에서 ReferenceEquals 호출 시 this가 박싱되어 힙에 새 참조가 생성되므로 항상 false 반환
  • 결과: 최적화를 의도했으나 오히려 매 호출마다 불필요한 박싱 연산이 발생함

4위: 익명 함수로 이벤트 구독 해제

MSBuild 검사에서 발견된 오류입니다.

private static void SubscribeImmutablePathsInitialized()
{
  NotifyOnScopingReadiness?.Invoke();
  
  FileClassifier.Shared.OnImmutablePathsInitialized -= () =>
    NotifyOnScopingReadiness?.Invoke();  // 효과 없음
}
  • 경고 코드: V3084 - 익명 함수를 사용하여 이벤트에서 구독 해제함. 각 익명 함수 선언마다 별도의 델리게이트 인스턴스가 생성되므로 핸들러가 해제되지 않음
  • 문제점: 구독 시와 해제 시 각각 새로운 델리게이트 인스턴스가 생성되어 구독 해제가 실제로 동작하지 않음

3위: 연산자 우선순위 혼란

Neo 및 NBitcoin 프로젝트 검사에서 발견된 오류입니다.

public override int Size =>   base.Size
                            + ChangeViewMessages?.Values.GetVarSize() ?? 0
                            + 1 + PrepareRequestMessage?.Size ?? 0
                            + PreparationHash?.Size ?? 0
                            + PreparationMessages?.Values.GetVarSize() ?? 0
                            + CommitMessages?.Values.GetVarSize() ?? 0;
  • 경고 코드: V3123 [CWE-783] - ?? 연산자가 예상과 다르게 동작할 수 있음. 좌측의 다른 연산자보다 우선순위가 낮음
  • 문제점: ?? 연산자가 + 연산자보다 우선순위가 낮음
  • 예시: base.Size + ChangeViewMessages?.Values.GetVarSize() ?? 0에서 ChangeViewMessagesnull이면 base.Size + nullnull이 되어 결과가 항상 0임
  • 수정 방법: (ChangeViewMessages?.Values.GetVarSize() ?? 0)처럼 괄호로 감싸야 함

2위: 패턴 매칭 함정

Files 파일 관리자 검사에서 발견된 오류입니다.

var modeSeparatorWidth = 
  itemCount is not 0 or 1  // 문제 발생
    ? _modesHostGrid.Children[1] is FrameworkElement frameworkElement
      ? frameworkElement.ActualWidth
      : 0
    : 0;
  • 경고 코드: V3207 [CWE-670] - not 0 or 1 논리 패턴이 예상대로 동작하지 않을 수 있음. not 패턴이 or 패턴의 첫 번째 표현식에만 적용됨
  • 의도한 의미: x가 0도 1도 아닌 경우
  • 실제 의미: C#에서는 x is (not 0) or 1로 해석되어 두 번째 부분이 무의미해짐
  • 유사 오류 예시: list is not null or list.Count == 0과 같은 패턴에서 NullReferenceException 발생 가능

1위: LINQ 지연 실행과 캡처 변수

Lean 트레이딩 엔진 검사에서 발견된 가장 미묘한 오류입니다.

public void FutureMarginModel_MarginEntriesValid(string market)
{
  var lineNumber = 0;
  var csv = File.ReadLines(marginFile.FullName)
                .Where(x => !x.StartsWithInvariant("#") 
                         && !string.IsNullOrWhiteSpace(x))
                .Skip(1)
                .Select(x =>
  {
    lineNumber++;  // 지연 실행됨
    // ...
  });

  lineNumber = 0;  // foreach 전에 초기화
  foreach (var line in csv)
  {
    lineNumber++;  // 여기서도 증가
    // ...
  }
}
  • 경고 코드: V3219 - lineNumber 변수가 지연 실행되는 LINQ 메서드에서 캡처된 후 변경됨. 메서드 실행 시 원래 값이 사용되지 않음
  • 문제점:
    • Select는 지연 실행 메서드이므로 델리게이트 코드가 Select 호출 시가 아닌 컬렉션 반복 시 실행됨
    • csv 컬렉션 반복 중 lineNumber가 델리게이트 내부와 foreach 내부에서 모두 증가하여 각 반복마다 2씩 증가함
    • 개발자는 델리게이트가 루프 진입 전에 실행될 것으로 예상했으나 실제로는 그렇지 않음

결론

PVS-Studio 팀이 선정한 2025년 가장 흥미로운 C# 버그 10가지를 살펴보았습니다. 프로젝트에 유사한 문제가 있는지 확인하려면 정적 분석기를 사용하는 것이 권장됩니다.

참고 자료