PostgreSQL과 AddressSanitizer를 활용한 스택 버퍼 오버플로우 탐지 실습
작성자: Mark Wong 날짜: 2026년 2월 8일
이 글은 메모리 오염(Memory Corruption) 버그를 찾기 위해 설계된 프로그래밍 도구인 AddressSanitizer(ASan)가 어떻게 실제 버그를 발견했는지에 대한 이야기입니다. 또한, AddressSanitizer를 처음 접하는 분들이 PostgreSQL 회귀 테스트(Regression Tests)와 함께 실행하는 데 어려움을 겪지 않도록 로컬 환경에서 사용하는 방법도 함께 소개합니다.
버그 발견 사례
최근 저는 몇몇 시스템 카탈로그 함수를 최신 코딩 스타일에 맞춰 옵션 파라미터를 가질 수 있도록 리팩토링하는 #PatchPositivity 패치 시리즈를 제출했습니다.
로컬에서 회귀 테스트를 수행했을 때는 문제가 없었고, Cirrus CI 테스트도 대부분 통과했습니다. 하지만 AddressSanitizer를 사용하는 테스트 단계에서만 실패가 발생했습니다.
출력 결과를 검토하는 방법은 여러 가지가 있지만, 요점만 말씀드리면 다음과 같습니다. 빨간색으로 표시된 Run test_world 섹션은 회귀 테스트 실행 중 실패가 발생했음을 나타냈고, Run Cores 섹션을 확장해 보니 코어 덤프(Core file)가 생성된 것을 확인할 수 있었습니다. 코어의 백트레이스(Backtrace) 최상단에 있는 AddressSanitizer 설명을 보니 8-byte-read-stack-buffer-overflow가 발생했다고 명시되어 있었습니다.
간결한 설명을 위해 AddressSanitizer 리포트 바로 다음에 나오는, 코어 덤프를 유발한 첫 번째 라인으로 넘어가 보겠습니다.
Plaintext
[23:58:34.391] #9 0x000056200585ccf2 in pg_get_expr (fcinfo=0x7ffecac436a0) at ruleutils.c:2565 [23:58:34.391] expr = <optimized out> [23:58:34.391] relid = <optimized out> [23:58:34.391] pretty = <optimized out> [23:58:34.391] result = <optimized out> [23:58:34.391] prettyFlags = <optimized out>
이 결과는 그리 놀랍지 않았습니다. 왜냐하면 pg_get_expr() 함수는 제가 리팩토링 중이었던 함수 중 하나였고, 반환된 표현식을 사람이 읽기 쉬운 형태로 만들 것인지 결정하는 세 번째 옵션 인자인 pretty를 추가했기 때문입니다.
2561 pg_get_expr(PG_FUNCTION_ARGS)
2562 {
2563 text *expr = PG_GETARG_TEXT_PP(0);
2564 Oid relid = PG_GETARG_OID(1);
2565 bool pretty = PG_GETARG_BOOL(2);
그렇다면 2565번 라인(세 번째 인자를 가져오는 부분)에서 무엇이 문제를 일으켰을까요? 백트레이스의 다음 라인이 그 답을 알려줍니다.
Plaintext
[23:58:34.391] #10 0x000056200594baff in DirectFunctionCall2Coll (func=0x56200585cc76 <pg_get_expr>, collation=collation@entry=0, arg1=139953427822388, arg2=<optimized out>) at fmgr.c:825 [23:58:34.391] fcinfodata = <optimized out> [23:58:34.391] fcinfo = 0x7ffecac436a0 [23:58:34.391] result = <optimized out> [23:58:34.391] __func__ = "DirectFunctionCall2Coll"
문제의 원인은 DirectFunctionCall2Coll()이 세 개의 인자를 기대하는 pg_get_expr()에 인자를 두 개만 전달하고 있었기 때문입니다. 해결 방법은 간단합니다. 예상대로 세 개의 인자를 전달할 수 있도록 DirectFunctionCall3Coll()을 사용하면 됩니다.
이것으로 런타임 인스트루멘테이션(Runtime Instrumentation) 도구 없이는 발견하기 어려웠던 버퍼 오버플로우 상황을 AddressSanitizer가 어떻게 찾아냈는지에 대한 짧은 이야기를 마칩니다.
AddressSanitizer 사용 방법 (How-to)
마지막으로, AddressSanitizer를 로컬 환경에서 사용하는 빠른 가이드를 소개합니다. 이 정보는 Cirrus CI 웹 출력 결과보다는 PostgreSQL 소스 코드 내의 .cirrus.tasks.yml 파일에서 더 쉽게 찾아낼 수 있습니다.
1. PostgreSQL 설정
AddressSanitizer를 활성화하고 컴파일러 최적화를 비활성화하여 PostgreSQL을 구성합니다.
Bash
./configure --enable-cassert --enable-injection-points --enable-debug --enable-tap-tests --with-segsize-blocks=6 CLANG=clang CFLAGS="-Og -ggdb -fno-sanitize-recover=all -fsanitize=address"
2. 환경 변수 설정
환경 변수를 사용하여 AddressSanitizer의 동작 규칙을 정의합니다.
Bash
export UBSAN_OPTIONS="print_stacktrace=1:disable_coredump=0:abort_on_error=1:verbosity=2" export ASAN_OPTIONS="print_stacktrace=1:disable_coredump=0:abort_on_error=1:detect_leaks=0:detct_stack_use_after_return=0"
3. 회귀 테스트 실행
설정이 완료되면 PostgreSQL 회귀 테스트를 실행합니다.
Bash
make check
이 짧은 사례가 다른 개발자분들에게도 도움이 되기를 바랍니다.
메일: salesinquiry@enterprisedb.com

