N 36 21 22
E 127 23 46
Archive vol. 01
Depth0000mSURFACE
Research Note / Technical Document

프론티어_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로 만들고 나서 해당 명령어를 실행한다.

 codeql-db 데이터베이스를 대상으로 queries/find.ql에 작성한 CodeQL 쿼리를 실행하는 명령어이다. 즉, 앞에서 만든 C++ 분석 DB 안에서 PyLong_AsSsize_t 호출 같은 패턴을 찾아 결과로 출력하는 단계인 것 이다.
 

실제로 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