C snprintf 반환값은 저장 길이가 아니다: 잘림 검사와 UTF-8 경계 처리
snprintf의 필요 길이와 실제 저장 길이를 구분한다. 실행 가능한 C 예제로 널 종료 공간, 잘림 거부, UTF-8 바이트 경계와 전송 길이 사용을 검증한다.
C의 snprintf 반환값은 버퍼에 실제로 저장된 길이가 아니라, 공간이 충분했다면 필요했을 길이다. 고정 버퍼로 로그·프로토콜 문자열을 만들 때는 음수 오류를 먼저 검사하고, 반환값이 용량 이상이면 잘림으로 처리해야 한다. 아래 예제는 이 차이와 UTF-8 잘림을 재현하고, 완성된 문자열에만 전송 가능한 길이를 부여하는 방식을 보여 준다.
긴 청록색 타일 줄과 짧은 투명 트레이로 필요한 출력 길이와 버퍼 용량의 차이를 표현한 개념 일러스트. 실제 메모리 배치도는 아니다.
작성·문서 확인: 2026-10-31. 실행 환경: Linux amd64, GCC 13.3.0, glibc 2.39. 표준 snprintf를 대상으로 하며 Windows CRT에서의 실행 결과는 아니다.
크기 제한이 있어도 길이 사용은 별도 문제다
char buf[5]에 "ABCDEFG"를 쓰면 예제 환경의 snprintf는 "ABCD"와 널 종료 바이트를 저장하고 7을 반환한다. 따라서 그 값을 그대로 send(fd, buf, n, ...) 같은 전송 API의 길이로 넘기면 버퍼보다 많은 바이트를 읽게 할 수 있다. 잘못된 전송 호출은 이 글에서 실행하지 않는다.
GNU C Library 문서는 널 종료를 제외한 필요 길이와 용량 비교를 설명한다. char 버퍼의 길이는 여기서 바이트 단위이며, 화면에 보이는 글자 수가 아니다.
반환값은 세 갈래로 처리한다
| 검사 순서 | 의미 | 예제의 정책 |
n < 0 | 포맷 처리 오류 | 실패, 길이 0 |
n >= 0이고 (size_t)n >= cap | 널 종료까지 담을 공간 부족 | 잘림 거부, 길이 0 |
| 그 외 | 전체 결과와 널 종료가 들어감 | 길이 n을 반환 |
음수인지 확인하기 전에 size_t로 변환하면 오류를 큰 양수로 바꿔 버린다. n == cap도 성공이 아니다. 마지막 널 종료를 담을 한 바이트가 더 필요하다.
이 예제는 잘림을 허용하지 않는 정책이다. 실패하면 용량이 있는 버퍼의 첫 바이트를 널로 바꾸고, 호출자에게 줄 길이는 0으로 둔다. 호출자는 길이만 보지 말고 반환 상태가 FORMAT_OK일 때만 결과를 사용해야 한다.
실행 코드: 성공한 결과에만 길이를 부여한다
아래를 snprintf_demo.c로 저장한다. 입력은 유효한 널 종료 C 문자열이며 출력 버퍼와 겹치지 않아야 한다. 출력 버퍼는 cap바이트 이상이어야 하며, NULL은 cap == 0일 때만 허용한다. 이는 소유권이나 입력 유효성을 검증하는 범용 문자열 라이브러리가 아니다.
sizeof로 용량을 구하는 코드는 지역 배열에 적용한 것이다. 함수 인자로 받은 char *에 sizeof를 적용하면 버퍼 용량이 나오지 않으므로 용량을 별도 인자로 전달한다. 예제의 len은 널 종료를 제외한 길이다. 실제 소켓 전송에서 부분 쓰기와 오류 처리는 추가로 필요하다.
검증 명령과 실제 결과
의도적으로 잘리는 두 원시 호출에 대해 GCC가 -Wformat-truncation 경고를 출력했다. 이는 재현하려는 상황과 일치한다. 실행은 종료 코드 0으로 끝났고 AddressSanitizer·UndefinedBehaviorSanitizer 진단은 없었다. LeakSanitizer는 실행 환경의 프로세스 접근 제약 때문에 비활성화했다. 이 예제는 동적 할당을 사용하지 않는다.
| 경계 사례 8개 | 확인 내용 |
| 원시 호출: 용량 5, 입력 7바이트 | 필요 길이 7, 실제 길이 4 |
| 래퍼: 용량 8, 입력 7바이트 | 성공, 길이 7 |
| 래퍼: 용량 7, 입력 7바이트 | 널 종료 공간 부족으로 거부 |
| 래퍼: 용량 1, 입력 A | 잘림 거부 |
| 래퍼: 용량 1, 빈 문자열 | 성공한 빈 문자열 |
| 래퍼: 용량 0, 입력 A | 원래 메모리 불변, 거부 |
| 래퍼: NULL·용량 0, 빈 문자열 | 저장 가능한 결과가 없으므로 거부 |
| UTF-8 3바이트, 용량 3 | 원시 호출은 2바이트만 남고, 래퍼는 거부 |
마지막 사례는 U+AC00 ‘가’의 UTF-8 바이트 EA B0 80을 사용한다. 원시 결과 EA B0에는 세 번째 바이트가 없어 완전한 UTF-8 문자가 아니다. RFC 3629 §3의 바이트 구조와 대조하면 확인할 수 있다. 널 종료된 C 문자열이라는 사실만으로 인코딩이 유효해지는 것은 아니다.
적용 전에 결정할 정책
- TTS 입력·JSON·인증 데이터처럼 일부만 남기면 의미가 바뀌는 값은 잘림을 거부하거나 더 큰 버퍼로 다시 생성한다.
min(n, cap - 1)로 길이만 줄이면 범위 문제 일부를 피하더라도 내용의 완전성은 해결되지 않는다.cap == 0에서의 뺄셈도 주의해야 한다.
- 화면용 미리보기라서 잘림을 허용한다면 UTF-8 경계와 사용자에게 보이는 문자 경계를 고려한 별도 정책을 둔다. 이 래퍼는 UTF-8 검증기가 아니며, 잘리지 않은 잘못된 인코딩도 걸러 주지 않는다.
- 음수 오류 분기는 포함했지만 이 고정
"%s"예제에서 실제 오류를 강제로 발생시키지는 않았다. 잘못된 포인터·겹치는 버퍼·인자 타입 불일치는 안전한 실패 반환을 기대할 수 있는 테스트 입력이 아니다.
- Microsoft 문서는 UCRT의 표준
snprintf와 레거시_snprintf의 잘림 반환값·널 종료 차이를 구분한다. Windows 코드를 이식할 때 이름을 임의로 서로 치환하지 않는다.
English version: C snprintf Return Values: Detect Truncation Before Using the Length