본문으로 건너뛰기

[Grafana Loki] Bitmap.Slice에서 바이트 정렬 경계의 off-by-one 패닉 수정

PR 링크: grafana/loki#21354 상태: Merged | 변경: +23 / -1

들어가며

비트맵은 불리언 값의 배열을 메모리 효율적으로 저장하는 자료구조로, Loki의 쿼리 엔진에서 널러빌리티(nullable) 마킹 등에 사용된다. Bitmap.Slice(i, j) 메서드는 비트맵의 일부를 추출하는데, 슬라이스 끝 위치가 정확히 바이트 경계(8의 배수)에 떨어질 때 endWord 계산이 1바이트를 초과하여 out-of-bounds 패닉이 발생하는 버그가 있었다.

핵심 코드 분석

Before: 잘못된 endWord 계산

startWord = (bmap.off + i) / 8
endWord   = ((bmap.off + j) / 8) + 1

(off + j)가 8의 배수일 때, 예를 들어 off=0, j=65536이면:

  • endWord = (65536 / 8) + 1 = 8193
  • 실제 데이터는 8192바이트만 존재하므로 인덱스 초과

After: 올림 나눗셈 적용

startWord = (bmap.off + i) / 8
endWord   = ((bmap.off + j) + 7) / 8

같은 예시에서:

  • endWord = (65536 + 7) / 8 = 8192
  • 정확히 필요한 바이트 수만 참조

왜 이게 좋은가

  1. 올림 나눗셈 관용구: (n + d - 1) / d는 양의 정수에 대한 올림 나눗셈의 표준 패턴이다. nd의 배수일 때 불필요한 +1을 방지하고, 배수가 아닐 때는 올바르게 올림한다.
  2. 회귀 테스트 추가: 65536비트(정확히 8192바이트) 비트맵과 24비트 비트맵의 바이트 정렬 경계에서 패닉이 발생하지 않음을 검증하는 테스트를 추가했다.
  3. 한 줄 수정: 핵심 변경은 수식 한 줄이지만, 대규모 비트맵 슬라이싱에서 발생하는 런타임 패닉을 완전히 제거한다.

바이트/비트 경계 계산에서 off-by-one은 매우 흔한 버그 유형이다. 특히 / 8 + 1 패턴은 (n + 7) / 8로 대체해야 하는 대표적인 예시다.

참고 자료

댓글

관련 포스트

PR Analysis 의 다른글