프론티어_CodeQL실습
PyTorch issue #182277 기반 CodeQL 실습: PyLong_AsSsize_t 호출 후 PyErr_Occurred 확인 누락 패턴 탐지
그전에 프로젝트를 해보려다가 setup을 claude한테 시켰는데, 해당 claude가 제보할 issue가 있다며 나한테 준 보고서를 토대로 한번 CodeQL 실습을 진행해보려고 한다.
https://github.com/pytorch/pytorch/issues/182277
실제로 issue사항으로 올라갔다 ㅎㅎ.
해당 issue에서 잡아야할 포인트
torch.cuda.memory.caching_allocator_alloc(2**63)
↓
PyLong_AsSsize_t(size_o) 호출
↓
2**63은 PY_SSIZE_T_MAX를 넘음
↓
PyLong_AsSsize_t가 -1을 반환하고 OverflowError를 set
↓
그런데 PyErr_Occurred() 확인 없이 다음 로직 진행
↓
SystemError 발생
원래 2**63 - 1은 CUDA OOM이 나지만, 2**63은 SystemError가 발생하는 것을 확인하였다.
그래서 원인 후보로 PyLong_AsSsize_t()를 OverflowError로 set하면 즉시 PyErr_Occurred()로 확인하지 않은 흐름이 언급되어 있다.
그래서 CodeQL를 이용해서 PyTorch Python C API 예외 처리 누락 패턴을 분석해보겠다.

일단 src와 queries에 대한 디렉토리를 먼저 설정한다.

그 후에 PyTorch에 보고한 issue와 비슷하게 setup을 한 bad.cpp를 만든다
auto size = PyLong_AsSize_t(size_o);
이 부분에서 overflow가 발생하면 -1을 반환하면서 Python excetion을 set할 수 있다. 하지만 바로 아래에 있는 PyErr_Occurred()를 확인하지 않고 size < 0만 검사한다.
그래서 CodeQL로 찾고 싶은 패턴은
PyLong_AsSize_t를 호출 한뒤, PyErr_Occurred 확인이 없음 -> 반환값 사용
에 관한 것을 찾고 싶은 것 이다.
추후 비교 분석을 위해서 정상 패턴 예제도 작성한다.

제대로 size == -1과 PyErr_Occuerred를 예외처리로 넣은 모습이다.
실제로 issue를 등록하고 pr을 봤을 때 이런 형식으로 처리를 하였다.
이제 CodeQL DB를 생성해본다.

하기 전에 codeQL 설치하기

실제 codeql을 설치하고 입력하면 정상적으로 뜬다!

CodeQl을 설치했으니 일단 컴파일을 진행한다.
-> 컴파일을 하는 이유 CodeQl이 C/C++ 코드를 제대로 분석하려면 빌드 과정을 알아야한다.
JavaScript나 Python은 그냥 파일을 읽으면 어느 정도 분석이 가능하지만, C/C++은 다르다.
예를 들어 #include 경로, 매크로 정의, 컴파일 옵션 등 여러 가지가 컴파일 과정에서 결정되기 때문에 실행 파일로 만드는 것이 아니라, 오브젝트 파일까지만 딱 만들어서 CodeQL이 분석할 수 있는 C++ 코드인지를 확인 및 검증하는 단계인 것 이다.

실제로 bad.o와 good.o가 생성된 모습이다.

codeql database create codeql-db \
--language=cpp \
--command='g++ $(python3-config --includes) -c src/bad.cpp -o bad.o && g++ $(python3-config --includes) -c src/good.cpp -o good.o'
실제 코드는 이렇게 구성을 한다.
코드 해석 ->
codeql database create codeql-db
CodeQL 데이터베이스를 생성한다는 뜻이다.
codeql-db는 생성될 DB 폴더 이름이고, 이 명령어만 실행하면 codeql-db 라는 폴더가 생길 것 이다.
--language=cpp
분석 대상 언어를 C/C++로 지정하는 옵션이다.
CodeQL은 언어별로 데이터베이스 생성 방식이 다르기 때문에 C++ 코드를 분석하려면 cpp를 지정해야 한다.
--command='g++ $(python3-config --includes) -c src/bad.cpp -o bad.o && g++ $(python3-config --includes) -c src/good.cpp -o good.o'
CodeQL이 데이터베이스를 만들 때 실행할 빌드 명령어를 지정하는 옵션이다.
C/C++은 JavaScript나 Python처럼 그냥 파일만 읽는 방식이 아니라, 실제 컴파일 과정에서 나오는 정보를 기반으로 분석해야 하기 때문에 지정을 해주는 것 이다.

했더니 오류가 나서 잠시 issue 처리

여러 명령이 필요하면 --command를 여러 번 지정하고, 커스텀 빌드 스크립트를 짜서 넣는 것이 좋다고 되어 있다.
해결 방법
1. --command 여러 개 지정하기

2. build.sh 활용하기

2개 모두 뭐로 해도 상관 없다!
이제 처음 만든 쿼리 dir에 실제 쿼리를 만들어본다,.
/** * @name Find PyLong_AsSsize_t calls * @description Finds calls to PyLong_AsSsize_t in C/C++ Python extension code. * @kind problem * @problem.severity warning * @id cpp/python-api/find-pylong-as-ssize-t */import cpp
from FunctionCall call where call.getTarget().getName() = "PyLong_AsSsize_t" select call, "PyLong_AsSsize_t 호출이 발견되었습니다. 반환 직후 PyErr_Occurred 확인 여부를 검토해야 합니다."
로 작성을 하였는데 자세한 코드 설명은 아래와 같다.
/**
* @name Find PyLong_AsSsize_t calls
* @description Finds calls to PyLong_AsSsize_t in C/C++ Python extension code.
* @kind problem
* @problem.severity warning
* @id cpp/python-api/find-pylong-as-ssize-t
*/
이것은 C/C++ 코드 안에서 특정 패턴을 찾아내는 분석 규칙이다.
| @name | 쿼리 이름 |
| @description | 쿼리가 무엇을 찾는지 설명 |
| @kind problem | 문제점을 찾는 쿼리라는 의미 |
| @problem.severity warning | 심각도는 warning |
| @id | 쿼리 고유 ID |
매핑을 해보자면 해당 정보들을 찾는 것 이다.
import cpp
from FunctionCall call where call.getTarget().getName() = "PyLong_AsSsize_t" select call, "PyLong_AsSsize_t 호출이 발견되었습니다. 반환 직후 PyErr_Occurred 확인 여부를 검토해야 합니다."
import cpp는 CodeQL 라이브러리를 불러오는 부분이고,
from FunctionCall call은 C/C++ 코드 안에 있는 함수 호출들을 대상으로 보겠다는 것 이다.
where call.~ 이 구분은 pyLong_AsSize_t인 호출 대상을 찾는 다는 것 이다.
만약에 해당 call이 찾아지면 " " <- 여기 안에 있는 문자열을 출력할 수 있도록 setup 해놓는다.
codeql query run queries/find.ql --database=codeql-db
위에 있는 ql파일을 find.ql로 만들고 나서 해당 명령어를 실행한다.

실제로 call이 뜬 것을 확인할 수 있다. 결과 로그들을 조금 더 상세하게 분석해보자.
왜 call이 2번이나 확인되었을까?
이유는 bad.cpp와 good.cpp안에 auto size = PyLong_AsSsize_t(size_o);가 있기 때문에.
지금 실행한 것은 PyLong_asSize_t 호출이 있는지를 보는 것 이기 때문에 이런 결과가 나오는 것이 맞다.
그러면 내가 올린 issue를 분석하기 위한 .ql을 다시 작성해본다.
/** * @name Missing PyErr_Occurred check after PyLong_AsSsize_t * @description Detects PyLong_AsSsize_t calls that are not followed by a nearby PyErr_Occurred check. * @kind problem * @problem.severity warning * @id cpp/python-api/missing-pyerr-occurred-after-pylong-as-ssize-t */import cpp
predicate nearbyPyErrOccurredCheck(FunctionCall conv) { exists(FunctionCall err | err.getEnclosingFunction() = conv.getEnclosingFunction() and err.getTarget().getName() = "PyErr_Occurred" and err.getLocation().getStartLine() > conv.getLocation().getStartLine() and err.getLocation().getStartLine() <= conv.getLocation().getStartLine() + 5 ) }
from FunctionCall conv where conv.getTarget().getName() = "PyLong_AsSsize_t" and not nearbyPyErrOccurredCheck(conv) select conv, "PyLong_AsSsize_t 호출 이후 가까운 위치에 PyErr_Occurred 확인이 없습니다. overflow 시 pending Python exception이 남을 수 있습니다."
그전이랑 달라진 부분만 설명해보자면, 단순히 PyLong_AsSsize_t 호출을 찾는 것에서 끝나는 게 아니라, 호출 이후 5줄 이내에 PyErr_Occurred() 확인이 있는지까지 검사하도록 바뀌었다.
predicate nearbyPyErrOccurredCheck(FunctionCall conv)
이 부분이 새로 추가된 검사 로직이다.
같은 함수 내부에서 PyLong_AsSsize_t 호출보다 뒤에 있고, 5줄 이내에 PyErr_Occurred()가 호출되면 정상 확인 코드가 있다고 판단한다.
이유는? -> PyLong_AsSsize_t는 실패 시 -1을 반환할 수 있는데, -1은 정상 값일 수도 있어서 반환값만으로는 오류 여부를 구분하기 어렵기 때문이다.
그래서 이번 쿼리는 아래 조건을 추가했다.
and not nearbyPyErrOccurredCheck(conv)
그래서 PyLong_AsSsize_t를 호출했는데 근처에 PyErr_Occurred() 확인이 없는 코드만 경고로 출력한다.
정리하면 이전 쿼리는 진짜 호출을 하는지 확인한 것 이고 이번 쿼리는 실제로 오류 확인이 빠진 위험한 패턴을 찾는 쿼리로 발전한 것이다.

실제로 그전과 다르게 하나만 나오는 것을 확인할 수 있다.
근데 어디 파일인지가 안나와서
conv.getFile().getRelativePath()
라는 코드를 추가하여서 다시 돌려보면

실제로 bad.cpp에서 확인되는 것을 볼 수 있다.
정리->
PyTorch 이슈 #182277의 핵심 패턴인 PyLong_AsSsize_t 호출 이후 PyErr_Occurred() 확인 누락을 CodeQL 실습 주제로 잡고, bad.cpp와 good.cpp 예제를 만들어 CodeQL DB를 구축 후 실습.
이후 PyLong_AsSsize_t 호출을 찾는 1차 쿼리를 실행했고, 두 파일 모두 해당 함수를 호출하므로 bad.cpp, good.cpp가 둘 다 탐지되는 것이 정상임을 확인했다.
다음 단계로 PyErr_Occurred() 체크가 없는 경우만 필터링하는 2차 쿼리를 작성해서 bad.cpp만 탐지되도록 만든 후 실제 성공하였다.
codeQL 명령어 정리
# CodeQL 데이터베이스 생성 codeql database create--language=<언어> --command='<빌드_명령어>' #예시: C/C++ 프로젝트를 build.sh로 빌드하면서 DB 생성 codeql database create codeql-db --language=cpp --command='./build.sh'
CodeQL 쿼리 실행
codeql query run <쿼리파일.ql> --database=<DB_이름>
예시: find.ql 실행
codeql query run queries/find.ql --database=codeql-db