Go 퍼즈 테스트가 놓치는 슬라이스 경계: len·cap과 AI 검증의 역할
11월 12일 Google의 Go 버그 탐지 사례를 바탕으로 슬라이스 길이·용량의 차이, 퍼즈 입력 제한, 파서 계약과 AI 제약 모델의 검증 한계를 살펴본다.
슬라이스의 논리적 경계와 여유 공간을 표현한 생성형 개념 이미지. 실제 도구 화면이 아니다.
퍼즈 테스트가 통과했는데 실제 입력에서는 슬라이스 범위 오류가 발생한다면, 입력 바이트뿐 아니라 len과 cap도 확인해야 한다. 파서가 받은 입력 길이를 넘겨 읽지 않아야 한다는 계약이라면, 테스트에서 용량을 길이로 제한하는 것이 유용하다. 다만 이것은 파서의 경계 검사를 대신하지 않는다.
최근 공개된 사례에서 얻을 점
Google은 2026년 11월 12일 Gemini와 Z3 제약 해결기를 이용해 Go의 경계 오류를 찾는 사례를 공개했다. 컴파일러가 제거하지 못한 경계 검사 경로를 모델링하고, 얻은 후보 입력을 실제 Go 테스트로 재실행하는 방식이다.
주목할 부분은 AI 모델 이름보다 테스트 입력의 여유 용량이었다. 저자의 실험에서는 퍼저가 전달한 슬라이스의 여유 공간이 일부 잘못된 재슬라이싱을 panic 없이 허용했다. 용량을 길이에 맞춘 뒤 해당 오류가 드러났다. 이 관찰을 모든 Go 버전이나 모든 퍼즈 입력의 보장된 동작으로 일반화하면 안 된다. Google 공식 사례
인덱싱과 재슬라이싱은 경계가 다르다
b[i]의 유효 범위는 길이 안이다. 반면 슬라이스에 대한 b[low:high]는 용량까지 확장할 수 있다. 따라서 len(b)==2, cap(b)==8인 경우 b[:4]는 가능하지만 b[2]는 허용되지 않는다.
전체 슬라이스 표현식 b[:len(b):len(b)]는 같은 배열을 참조하면서 결과 용량을 제한한다. 복사가 아니므로 원본 배열과의 별칭 관계까지 끊어주지는 않는다. Go 언어 명세: 슬라이스 표현식
다음은 원리를 확인하기 위한 독립 예제다. 이 글의 작성 환경에서는 Go 도구체인을 사용할 수 없어 실행하지 않았으며, 아래 결과는 언어 명세에 따른 예상이다.
예상으로는 첫 호출이 두 바이트의 영값까지 포함한 4바이트를 출력하고, 두 번째 호출은 panic을 포착한다. 여기의 recover는 비교 시연용이다. 퍼즈 테스트에서 오류를 무조건 복구해 성공 처리하면 발견하려던 실패를 숨길 수 있다.
파서의 계약부터 고친다
길이 필드를 가진 패킷이라면, 저장 공간이 넉넉한지와 실제 수신한 데이터가 충분한지는 별개다. 예를 들어 첫 바이트를 본문 길이로 해석하는 파서는 다음처럼 길이를 검사할 수 있다.
이 예제는 길이 필드가 한 바이트라는 가정이다. 여러 바이트 길이, 음수 변환, 덧셈 오버플로가 있는 프로토콜은 별도의 검증이 필요하다. 용량 제한만 추가하고 입력 검사를 생략하는 수정은 피한다.
퍼즈 테스트에 넣는 방법
다음 코드를 같은 패키지의 payload_test.go에 둔다. 빈 입력, 정상 입력, 본문 부족 입력을 시드로 넣고 입력 용량을 제한한다. 코드와 명령은 실행 예시이며 실제 측정 결과가 아니다.
Go는 1.18부터 표준 도구체인에 퍼징을 제공한다. 퍼즈 타깃은 빠르고 결정적으로 만들고 전역 상태에 의존하지 않는 것이 좋다. 실패 입력이 발견되면 수정 후 회귀 테스트로 유지한다. 제한 시간 내 실패가 없다는 결과는 모든 입력의 안전성 증명이 아니다. Go 공식 퍼징 문서
AI와 제약 해결기는 어디까지 믿을까
LLM이 만든 제약식에서 해를 찾았다는 사실과 실제 프로그램에서 오류가 재현된다는 사실을 분리해야 한다. 반대로 해가 없다는 결과도 잘못 번역한 모델의 한계 안에서 나온 것일 수 있다. 실제 입력 재실행을 마지막 관문으로 두는 이유다.
실무에서는 순수 파서와 오프셋 계산부터 작은 테스트로 분리하고, 길이·용량·빈 입력·최대 길이의 경계를 먼저 점검하는 순서가 합리적이다. AI나 solver를 먼저 붙이기보다 테스트가 검사하려는 계약을 명확히 해야 한다. 이 글에서는 Gemini API나 Z3 실행, 성능 벤치마크를 수행하지 않았다.