리눅스 커널에는 Sparse
라는 이름의 정적 분석 도구가 존재한다. Sparse
는 Semantic parser(구문 파서)
의 준말로 Linus Torvalds
가 Linux Kernel
개발에 사용하려고 만든 여러 도구들 중 하나이다. (이 중에는 git
도 있다.)
여기에서 정적 분석 도구 (Static Analysis Tool)
란 프로그램 실행 없이 코드만 보고 뚝딱 문제를 찾아주는 훌륭한 도구들을 일컫는 말이다. 이 친구의 기능은 참으로 다양하고, 복잡하고, 다른 의미에서는 좀 빡이 치는데 필자는 여러 기능들 중 락(lock)
과 관련된 내용을 중심으로 이 툴을 소개하려 한다.
SPARSE
사용해서 빌드하기 일단 SPARSE
를 쓰려면 Sparse
를 설치해야 한다. 당연한 얘기이다.
sudo dnf install sparse
독자들이 사용하는 배포판의 패키지 매니저를 통해 설치할 수 있다. 보통 등록되어 있어서 설치가 가능할 것이다. 이 패키지가 없다면 직접 프로젝트를 clone
하여 설치하길 바란다.
우리는 Sparse
를 사용하기 위해 커널을 빌드해야 한다. 구문 분석만 하면 될 것이지 왜 빌드를 하느냐? 하면 필자도 잘 모르겠다. 다만 유추할 수 있는 것은 커널 빌드 옵션에 따라 코드가 바뀌기도 하고 파일이 추가되고 하니 어찌 보면 당연할 수도 있겠다. (궁금해서 실제로 V=1
옵션으로 확인해봤는데 전달되는 인자가 config
에 영향을 받는다)
# Sparse 를 사용한 빌드
make C=1
# 위에서 사용한 명령어
make O=../build/x86_64 -j$(nproc) W=1 C=2 2>&1 | tee ../build/x86_64/output
make
의 C
(Checker) 변수의 값을 1
로 바꿈으로써 Sparse
를 통한 구문 분석을 수행할 수 있다. 만약 이미 빌드가 완료되었는데 다시 확인하고 싶다면 C=2
옵션을 주어 확인하는 것도 가능하다. 필자의 경우 Sparse
결과를 계속 확인하기 위해서 redirection
한 뒤 파일로 저장했다.
여기에서 필자가 관심을 가지는 부분은 lock
이다. 182행부터 187행까지가 Sparse
프로그램이 뱉은 경고 문구이다. 이런 부분을 찾아보면 된다.
우선 Sparse
가 대체 어떻게 lock
과 관련한 문제를 추적하는지 설명하는게 좋을 것 같다. 일반적으로 lock
은 걸고 난 뒤에 반드시 unlock
을 해야 한다. 이게 기본이다. Sparse
는 이 단순한 규칙에 따라 lock
이 걸리고 잠기는 횟수를 추적한다. 기본 원칙은 다음과 같다:
함수에 진입했을 때 락을 걸었다면 나가기 전에는 풀어야 한다. 또한 락을 건 횟수 만큼 풀어야 한다.
이 원칙을 지키지 않았을 경우에 아래의 세 가지 중 하나의 경고를 출력한다:
wrong count at exit
unexpected unlock
different lock contexts for basic block
wrong count at exit
은 lock
을 풀지 않고 함수를 탈출했을 때 발생한다. 실제 코드를 보면서 확인해보자. v6.11
의 icmp.c
코드에서 Sparse
가 뱉은 경고 메세지이다:
warning: context imbalance in 'icmpv6_xmit_lock' - wrong count at exit
실제 코드는 어떨까?
아하! 락을 걸고 탈출을 안 했구나! if
에 걸리지 않았다면 이하의 코드는 spin_lock
이 걸린 상태이므로 탈출 전에 반드시 락을 해제하는 코드가 있어야 하는데 없어서 경고가 발생한 것이다. 하지만 함수의 이름을 잘 보면 느낌이 올 것이다. 이건 의도된 것이다. 또 다른 경고 메세지를 보면서 더 자세히 확인해보자.
unexpected unlock
은 lock
이 잠기지 않았는데 unlock
을 수행한 경우 발생한다. 위에서
warning: context imbalance in 'icmpv6_xmit_unlock' - unexpected unlock
어디서 많이 본 이름의 함수에서 비슷한 경고가 발생했다. 코드를 보자:
이번에는 spin_lock
을 푸는 코드만 있고 락을 거는 코드가 없다. 실제로는 icmpv6_xmit_lock()
과 icmpv6_xmit_unlock()
함수가 페어로 사용되기 때문에 이건 의도된 설계이다. 하지만 Sparse
의 관점에서 보자면 문제다. 이 친구는 멍청해서 이게 의도된 설계라는 사실을 인식하지 못하기 때문이다.
진짜 락이 잘못 걸리고 풀렸다면 올바르게 고쳐야 하지만 위와 같은 경우는 Sparse
가 멍청해서 발생한 문제이다. 이런 경우에는 어떻게 하면 될까? 이때에는 annotation
을 붙이면 된다:
__releases()
annotation
을 붙여서 컴파일러에게 다음의 사실을 알린다:
이 함수(
icmpv6_xmit_unlock
) 는 이게 맞는거야. 다른 함수가 이 함수를 호출하면unlock
했다고 생각해.
반대로 락을 획득하고 그냥 탈출하는 함수의 경우 __acquires()
를 붙이면 된다.
static void icmpv6_xmit_unlock(struct sock *sk)
__releases(&sk->sk_lock.slock)
{
sock_net_set(sk, &init_net);
spin_unlock(&sk->sk_lock.slock);
}
이럼 다음과 같이 문제가...
늘어난다! 와우!
확실히 icmpv6_xmit_unlock
과 관련한 경고는 사라졌지만 새로운 경고 2개가 추가되었다. 638 line
을 보자:
이게 638 line
의 코드인데 보는 바와 같이 함수 끝에서 unlock
을 했다. 그럼 위에서 lock
을 안 걸었나? 다른 말로 하자면 icmpv6_xmit_lock()
을 호출하지 않았나? 이렇게 생각할 수 있다.
제대로 걸었다. icmpv6_xmit_lock()
함수는 락을 거는데 실패하면 NULL
을 반환하므로 if
이하부터는 spin_lock
이 걸린 상태이다. trylock
이 문제일까?
막말로 icmpv6_xmit_lock()
함수의 trylock
을 lock
으로 바꾼 뒤에 __acquires()
추가해도 에러는 계속 발생한다.
또 __must_hold()
도 있는데 이건 함수 진입 전부터 이미 잠긴 락을 풀고 거는 경우, 그러니까 계속 락을 붙들고 있어야 하는(must_hold
) 함수에 쓸 수 있는 annotation
이다.
위 코드를 보면 잠기지도 않은 락을 풀어 버리는 것(6286 line
)처럼 보이지만 실제로는 이 함수 호출 전에 이미 락을 걸고 진입한 상태라고 이해하면 된다.
Sparse
의 한계마지막으로 하나 남은 경고 메세지가 출력되었다:
warning: context imbalance in 'icmp6_send' -
different lock contexts for basic block
이 경고 메세지는 분기에 따라 락이 제대로 풀리거나 잠기지 않는 상황, 혹은! sparse
본인이 락의 추적 관리가 안되는 상황 에서 발생한다. 위의 icmp6_send
함수는 goto
문으로 함수를 탈출하는 구성을 가지는데 이런 경우에 위와 같은 경고가 발생한다.
또한 sparse
는 데이터를 추적하는 기능도 없기 때문에 다음의 코드에서도 문제가 생긴다:
우리는 직관적으로 556 line
은 lock
을 잠그는데 실패한 경우에 실행된다는 사실을 쉽게 알 수 있지만 sparse
의 관점에서는 sk
의 값의 추적을 하지 않으므로 실행 여부를 알 수 없다. 이런 경우에는 코드 리팩터링을 통해 Sparse
가 알아 쳐먹을 수 있는 형태로 코드의 구성을 바꿔야 한다.
ㅇㅈ ㅋㅋ
https://patchwork.kernel.org/project/netdevbpf/patch/20240919142149.282175-1-yyyynoom@gmail.com/
패치 올린 뒤에 Simon
한테 개털리고 수정하다가 빡쳐서 분노와 진실로 후려갈긴 장문의 포스팅. 모두에게 도움이 되었으면 합니다.
https://linux-kernel.vger.kernel.narkive.com/AQEc5UkN/sparse-context-checking
https://sparse.docs.kernel.org/en/v0.6.4/
https://lwn.net/Articles/689907/
https://www.kernel.org/doc/html/v4.12/dev-tools/sparse.html
https://groups.google.com/g/linux.kernel/c/d0IoCx0Wwus?pli=1
https://julesbashizi.wordpress.com/2020/02/29/types-context-imbalance-warnings-and-how-to-annotate/