GNU Gzip 1.15, 오래된 버그를 한꺼번에 바로잡다

GNU Gzip 1.15, 오래된 버그를 한꺼번에 바로잡다

압축 파일을 만들고 푸는 gzip은 Linux를 사용하는 유저에게는 너무 익숙한 프로그램입니다. 로그를 묶어 보관할 때도, 소스 코드를 tar.gz로 배포할 때도 아주 편리하죠.

그런데 이렇게 오래 쓰인 기본 도구일수록 작은 예외가 넓은 환경에 남을 수 있습니다. GNU Gzip 1.15는 바로 그런 오래된 문제들을 여러 갈래에서 정리한 버전입니다.

이번 릴리스에는 100개가 넘는 커밋이 들어갔습니다. 새 기능을 크게 추가하기보다 지난 1년 반 동안 모인 버그를 고치는 데 무게를 두었습니다.

발표된 수정 목록을 보면 첫 출시 때부터 남아 있던 문제도 적지 않습니다. 오래된 코드가 무조건 나쁘다는 뜻은 아니지만 오랜 시간 동안 드러나지 않던 경계 조건을 이제야 정확히 다룬 셈입니다.

엉뚱한 파일을 지우게 되는 경쟁 조건을 수정했습니다

엉뚱한 파일을 지우는 경쟁 조건을 수정
엉뚱한 파일을 지우는 경쟁 조건을 수정

gzip은 압축 결과를 만든 뒤 원본이나 대상 파일을 다루는 과정에서 파일 시스템과 계속 상호작용합니다. 이때 다른 프로세스가 gzip 대상 경로의 상위 디렉터리 이름을 동시에 바꾸면, 예전에는 전혀 다른 파일을 잘못 지울 가능성이 있었습니다. 여러 프로그램이 같은 디렉터리에서 작업하는 서버라면 단순한 이론 문제가 아닙니다.

이 문제는 경쟁 조건에 해당합니다. 프로그램이 “이 경로가 아까 확인한 바로 그 파일”이라고 믿는 순간과 실제로 파일을 삭제하는 순간, 그 사이에 다른 작업이 끼어드는 상황입니다.

Gzip 1.15에서는 상위 경로가 바뀌는 경우를 포함해 이 틈을 다뤄 실수로 엉뚱한 파일을 제거하지 않도록 했습니다.

파일 이름을 진단 메시지에 표시하는 방식도 바뀌었습니다. 특수 문자가 들어간 이름을 출력할 때 이제 이름을 더 분명하게 인용합니다. 장애가 났을 때 로그만 보고도 실제 파일 이름과 메시지의 구분을 파악하기 쉬워진 변화입니다.

압축 해제 과정의 메모리 문제도 손봤습니다

이번 버전은 비정상적으로 만들어진 입력을 처리할 때 초기화되지 않은 메모리를 읽을 수 있던 문제를 고쳤습니다.

초기화되지 않은 메모리는 이전 작업의 흔적이 남아 있거나 예측할 수 없는 값일 수 있습니다.

입력 파일이 정상적일 때는 드러나지 않다가, 손상되거나 일부러 변형된 파일을 넣었을 때 문제가 나타나는 유형입니다.

LZH 파일을 다루는 경로에서는 더 구체적인 결함도 수정됐습니다. 먼저 .Z 파일을 푼 뒤 LZH 파일을 해제하는 순서에서 버퍼 오버플로가 발생할 수 있었고 내부 비트 버퍼가 제대로 비워지지 않으면 결과가 손상될 수도 있었습니다.

LZH 파일을 연달아 처리할 때 앞선 파일의 디코딩 테이블이 다음 결과에 영향을 주는 문제도 함께 정리됐습니다.

버퍼 오버플로는 프로그램이 미리 확보한 메모리의 경계를 넘어 데이터를 기록하는 현상입니다. 데이터가 틀리게 나오는 데서 끝나지 않고 프로그램 충돌이나 보안 문제를 일으킬 수 있기 때문에, 자주 쓰이지 않는 압축 형식이라도 해제 코드에서는 가볍게 넘기기 어렵습니다.

ZIP 스트림과 플랫폼 지원의 변화

gzip -d는 이제 잘 만들어진 스트리밍 ZIP 파일에서 나타날 수 있는 PKZIP 서명과 로컬 헤더와 데이터 디스크립터를 막무가내로 거부하지 않습니다.

스트리밍 방식은 파일 전체 크기나 정보를 앞에서 모두 알지 못한 채 데이터를 순서대로 보내는 방식입니다. 네트워크나 파이프를 거치는 처리에서는 이런 형식이 특히 중요합니다.

동기화 기능도 보완됐습니다. gzip --synchronous가 GNU/Linux에서 O_PATH를 사용하는 환경의 읽을 수 없는 상위 디렉터리를 제대로 동기화하지 못하던 문제가 수정됐습니다.

FreeBSD에서 O_SEARCH는 제공하지만 O_PATH는 제공하지 않는 경우에도 상위 디렉터리를 동기화하도록 고쳤습니다.

저장 작업이 끝났다는 신호를 더 실제 상태에 가깝게 만들기 위한 수정입니다.

원문 뉴스 : Phoronix

🐧 Linuxroom의 생각 :

gzip은 GNU 재단의 압축 프로그램이죠.

뉴스 기사에 대해서 쉽게 말하자면, 이번 업데이트는 새로운 기능을 추가한게 아닌, 30년 넘게 쓰이면서 숨어있던 치명적인 버그와 보안 리스크를 해결한 업데이트 입니다.

원래부터 무슨 문제가 있었냐면, 여러 프로그램이 동시에 작동하는 서버 환경에서 gzip이 압축을 마치고 원본 파일을 지우려는 순간, 다른 프로그램이 폴더 이름을 바꿔버리면 전혀 상관없는 다른 파일을 지워버리는 버그가 있었습니다. 즉 경쟁 조건이라는 버그인데요.

이번 업데이트에서는 중간에 폴더 경로가 바뀌어도 착오 없이 삭제해야 할 파일만 안전하게 지우도록 하게 바뀌었다는 점이죠.

두 번째로는, 해커가 의도적으로 훼손하거나 이상하게 만든 압축파일(LZH 압축 파일이나 Z등의 오래된 압축 형식)을 압축 해제할 때, 메모리가 꼬이면서 프로그램이 튕기거나 쓰레기 데이터가 섞이는 문제 (버퍼 오버플로우 등)의 문제가 있었습니다.

이 문제 또한 이상한 파일을 넣어도 프로그램이 튕기거나 보안 리스크가 발생하지 않도록 안전하게 에러를 내는 방식으로 수정되었습니다.

마지막으로서는 레거시 시스템 지원을 중단했는데요. 이제는 정말 아무도 쓰지 않는 구닥다리 운영체제 (FreeBSD 4.11이나, 윈도우 8.1등의 레거시 시스템 등)에 대한 지원을 깔끔하게 없앴습니다. 이 덕분에 누더기 마냥 굴러다니던 레거시 코드를 삭제하고 개선하면서 프로그램을 훨씬 가볍고 안전하게 유지할 수 있게 되었습니다.


※ 각주

[1] gzip: 파일을 DEFLATE 알고리즘으로 압축하거나 .gz 형식 파일의 압축을 해제하는 GNU 표준 명령줄 도구입니다.

[2] 경쟁 조건: 두 개 이상의 프로세스나 쓰레드가 공유 자원에 동시에 접근할 때, 실행 순서나 타이밍에 따라 결과가 달라지는 오류입니다.

[3] 버퍼 오버플로: 할당된 메모리 공간(버퍼)의 경계를 넘어 데이터를 기록하면서 인접한 메모리를 덮어쓰거나 프로그램을 튕기게 만드는 오류입니다.

[4] LZH: Lempel-Ziv와 Huffman 코딩 방식을 조합한 오래된 압축 형식으로, 주로 과거 DOS/Windows 시절 아카이브(.lzh, .lha)에 쓰였습니다.

[5] 스트리밍: 전체 데이터를 다 받아서 저장한 뒤 처리하는 대신, 네트워크나 파이프를 통해 들어오는 순서대로 실시간 처리하는 방식입니다.

[6] 초기화되지 않은 메모리: 값이나 공간을 새로 할당만 하고 초기값을 채우지 않아, 이전에 사용하던 무작위 잔여 데이터가 들어있는 메모리 상태입니다.

[7] O_PATH: Linux에서 파일을 직접 열어 읽고 쓰지 않고, 오직 파일 경로나 디렉터리 위치(핸들) 자체만 안전하게 참조할 때 사용하는 운영체제 플래그입니다.

[8] O_SEARCH: FreeBSD 등 일부 UNIX 계열 운영체제에서 특정 디렉터리의 읽기 권한이 없어도 내부 검색/접근 권한만 확인할 때 사용하는 경로 플래그입니다.

[9] PKZIP 서명: ZIP 파일 형식의 표준 구조를 구분하기 위해 파일이나 로컬 헤더 시작 부분에 들어가는 고유한 식별 코드(보통 PK\x03\x04)입니다.

[10] 데이터 디스크립터: 스트리밍 압축처럼 파일 전체 크기나 CRC 검사값을 미리 알 수 없을 때, 압축 데이터가 모두 끝난 뒤 맨 뒤에 붙이는 정보 블록입니다.

[11] 동기화: 메모리(버퍼)에 임시로 저장된 파일 변경 사항을 물리적인 저장 장치(SSD/HDD)에 즉시 확실하게 기록하도록 강제하는 작업입니다.

[12] 디코딩 테이블: 압축된 데이터를 원래대로 복원(해제)할 때, 압축 기호와 원래 데이터를 1:1로 매핑해두는 내부 변환 표입니다.

[13] UCRT: Microsoft가 Windows 10부터 도입한 표준 C 라이브러리 runtime 환경으로, 최신 C 표준 응용 프로그램 실행을 지원합니다.

[14] MinGW: Windows 환경에서 Visual Studio 없이도 GNU 도구모음(GCC 등)을 이용해 윈도우용 실행 파일을 빌드할 수 있게 해주는 컴파일러 포팅 프로젝트입니다.

[15] 비트 버퍼: 바이트 단위가 아닌 1비트(0 또는 1) 단위로 데이터를 쪼개어 처리할 때 사용되는 임시 메모리 공간입니다.

[16] 경계 조건: 프로그램이 정상적이고 일반적인 상황이 아니라, 극단적인 입력값이나 예외적인 환경과 만나는 경계 상태를 의미합니다.