Astral이 같은 tar 파일을 도구마다 다르게 해석하는 문제 를 줄이기 위해 Rust 라이브러리 tar-codec 를 개발함. uv 자체 벤치마크에서 기존 구현보다 약 2배 빠르며, uv 0.13부터 기본으로 사용함
핵심 동기는 보안 임. 조작된 아카이브가 패키지 인덱스의 검사에서는 정상 파일로 보이지만, 설치 도구에서는 다른 파일이나 악성 내용을 추출하게 만들 수 있음
tar는 여러 형식과 확장이 누적된 계열이므로, 헤더의 크기와 이름을 어떤 순서로 해석하느냐에 따라 결과가 달라짐. 첫 헤더에서 형식 계열을 고정하고 모호한 혼용을 거부 하도록 설계함
블록 해석, 파일 항목 조립, 디스크 추출을 계층으로 나누고, 추출 범위를 지정 디렉터리에 제한 해 링크와 경로 순회 관련 위험을 줄임
관대한 입력 수용보다 잘못된 입력을 명확히 거부하는 구조 를 택했지만 모든 해석 차이의 제거를 보장하지는 않음. 안전한 파일 추출에는 추가 비용이 들며, 특히 macOS에서 성능에 영향을 줌
Python 패키지 설치에 tar가 필요한 이유
uv 는 PyPI 같은 패키지 인덱스에서 설치 가능한 배포본을 받아 사용자를 대신해 설치함
Python 배포본은 크게 빌드 배포본(bdist/wheel) 과 소스 배포본(sdist) 으로 나뉨
wheel은 구조화된 메타데이터와 정해진 디렉터리 배치를 담은 ZIP 아카이브로, 설치기는 내용을 적절한 위치에 배치함
bdist의 ‘built’는 빌드 백엔드로 생성했다는 뜻이며, 네이티브 코드가 있다는 뜻은 아님. 순수 Python 패키지도 더 빠르고 안정적인 설치를 위해 wheel로 배포함
sdist는 기본적으로 릴리스 시점의 소스 트리를 묶은 tar 아카이브임
sdist는 설치 과정에서 임의 코드를 실행 하므로 강력하지만, 빌드 도구나 의존성이 빠지면 실패할 수 있고 공격자가 제어하면 사용자가 import
하지 않아도 코드를 실행할 수 있음
과거에는 루트에 pyproject.toml
이나 setup.py
가 있는 소스 아카이브에 가까웠음
최근 표준은 표준 메타데이터를 담은 PKG-INFO
를 요구하지만, 빌드 시스템을 실행해야 메타데이터를 얻을 수 있는 배포본도 여전히 많음. 관련 표준은 PEP 643 임
wheel은 일반적으로 sdist에 빌드 백엔드 를 실행해 생성하며, uv_build , maturin , flit 등이 해당함
따라서 Python 패키지 설치기는 최소한 ZIP과 tar 를 모두 처리해야 함. ZIP 관련 문제는 앞선 uv 보안 권고 에서도 다룬 바 있음
기존 구현을 교체한 이유
uv는 0.13 이전에 tar 파싱과 uv build
의 아카이브 생성에 astral-tokio-tar 를 사용함
약 1년 전부터 들어온 보안 제보 를 계기로 압축 및 아카이브 처리 계층을 조사하면서 추가 문제를 발견함
핵심 문제는 파서 간 해석 차이(parser differential) 가 완전한 파서 동기화 상실로 이어질 수 있다는 점임
같은 tar 아카이브를 uv, pip , 기타 도구가 서로 다른 내용으로 해석할 수 있음
특정 파서에서만 파일이나 기존 파일의 내용이 추가되는 형태뿐 아니라, 파서별 추출 내용을 완전히 분리하는 형태도 가능함
패키지 인덱스에는 안전해 보이지만 설치기에서는 위험한 내용이 추출되는 경우가 특히 우려됨. 구체적인 사례로 Edera의 TARmageddon 이 있음
다만 Python 패키징에서는 sdist가 원래 빌드 시 임의 코드를 실행 하므로 tar 해석 차이를 대체로 경미한 문제로 취급함
sdist를 제어하는 공격자는 이미 파일을 쓰거나 코드를 실행할 직접적인 경로를 가지고 있음
Astral은 기존 구현에서 해석 차이를 제거하는 작업 을 시작했지만, tokio-tar 계열의 설계가 지나치게 관대하다고 판단함
제약을 사후 추가하면 기존 외부 API 사용자에게도 영향을 주고 오류 가능성이 커짐
성능 목표 역시 사실상 재작성에 해당하는 리팩터링 없이는 달성하기 어려워, 형식 자체를 체계적으로 검토하며 새 구현을 만들기로 함
tar의 계보와 512바이트 블록 구조
tar는 하나의 형식이라기보다 여러 형식의 계열 이며, 실무상 UNIX V7, UStar/pax, GNU tar가 중요함
UNIX V7 은 원형으로, 기본 출력 형식으로 쓰는 구현은 드물지만 Python의 tarfile
을 비롯한 대부분의 구현이 읽을 수 있음
UStar/pax 는 읽기와 쓰기 모두 널리 지원하며, tar 계열 중 명세와 문서가 비교적 명확함. pax는 사실상 UStar의 상위 집합이고 둘 다 POSIX에서 나옴
GNU tar 는 V7과 초기 UStar 초안 에서 파생됨. 매뉴얼 은 있지만 CLI의 소스 코드가 사실상의 표준임
GNU 형식에는 oldgnu
와 gnu
가 있으며, GNU tar는 1997년 출시된 1.12부터 새로운 GNU 형식을 출력함
tar는 Tape ARchive의 약자로, 테이프 저장 방식에서 유래한 512바이트 고정 블록 형식 임
파일 전체를 위한 헤더가 따로 없고, 멤버 헤더 블록과 실제 내용 블록이 이어짐
멤버에는 파일과 디렉터리뿐 아니라 소켓이나 장치 파일 같은 항목도 들어갈 수 있음
일반적인 TLV 인코딩 중심 바이너리 형식과 구조가 다르며, cpio 역시 블록 기반 형식의 사례임
N
바이트 파일은 ceil(N / 512)
개 내용 블록 을 사용하며, 마지막 블록의 나머지는 패딩으로 채움
패딩은 관례상 NUL이지만 다른 데이터를 넣는 것을 막지 않음
이런 빈 공간의 유연성이 공격자가 예상 밖 데이터를 배치하고 파서별 해석 차이를 만들 여지를 줌
압축 전 공간 효율은 낮음. 513바이트 파일도 최소 1,536바이트 를 차지하며, 1바이트 파일 1,000개는 종료용 0 블록까지 최소 1,025KiB가 필요함
tar 자체는 압축 형식이 아니라 여러 파일을 하나로 묶는 형식임
형식의 역사와 변형은 Martin Dørum 의 tar 형식 해설 에서도 살펴볼 수 있음
V7의 한계와 UStar의 확장
V7 헤더의 고정 크기 필드는 세 가지 큰 제약 을 만듦
멤버 이름과 링크 대상 이름은 각각 최대 100바이트임
파일 크기는 8진수 문자열로 저장하며, 상호운용 가능한 최대치는 0o77777777777
, 즉 8GiB보다 1바이트 작음
일반 파일, 하드 링크, 심볼릭 링크만 정의하며 디렉터리 같은 별도 멤버 유형은 없음
V7 헤더는 경로, 권한 모드, UID/GID, 크기, 수정 시각, 체크섬, 링크 유형과 대상 이름을 담고 255바이트를 사용하지 않음
고정 길이 이름 필드는 NUL 종료가 필수는 아니지만 크기 필드는 NUL 종료로 취급함
디렉터리는 경로상 필요한 상위 디렉터리를 만들거나, /
로 끝나는 일반 파일 이름을 통해 생성함
UStar 는 기존 헤더 배치를 유지하면서 필드와 패딩을 재활용함
linkflag
를 typeflag
로 확장해 디렉터리를 뜻하는 '5'
등을 추가함
magic="ustar\0"
, version="00"
, 사용자/그룹 이름, 장치 번호, 155바이트 prefix
필드를 추가함
prefix
와 name
을 결합해 경로 한도를 255바이트로 늘리지만, 파일 크기와 100바이트 링크 이름 제한은 유지함
pax 확장 헤더와 우선순위
pax 는 단일 블록에 모든 메타데이터를 넣는 방식을 벗어나, UStar의 두 가지 새 typeflag
로 확장 데이터를 저장함
'x'
는 다음 파일에 적용하는 로컬 확장 헤더임
'g'
는 이후 모든 멤버에 적용하는 전역 확장 헤더임
해당 헤더의 크기는 파일 크기가 아니라 확장 데이터 자체의 크기임
확장 데이터는 <length> <keyword>=<value>\n
형태의 레코드로 구성됨
<length>
는 길이 숫자 자체와 마지막 줄바꿈까지 포함한 전체 바이트 길이임
일반적인 TLV/LTV의 고정 길이 필드나 LEB128 과 달리 자기 길이를 포함하는 방식임
13 size=8000\n
은 뒤따르는 멤버의 크기를 자체 헤더와 무관하게 8,000바이트로 설정함
기본적으로 로컬 확장 > 전역 확장 > 멤버 자체 UStar 헤더 순으로 값이 적용되며, pax에서는 파일 크기와 경로 및 링크 이름 길이 제한이 사라짐
예시의 hasdrubal.txt
는 로컬 확장이 원래 이름 hannibal.txt
와 크기 0
을 덮어써 10,737,418,240바이트 파일이 됨
로컬 확장에 수정 시각이 없으므로 전역 mtime=100
이 적용됨
로컬 확장이 없는 hamilcar.txt
는 자체 헤더의 크기 1,234바이트를 유지하고 전역 수정 시각 100을 사용함
확장으로 덮어쓸 필드도 원래 헤더에 남기는 이유는 하위 호환성 임
GNU와 pax의 확장 헤더 자체에도 유효한 경로를 넣으며, pax에서는 %d/PaxHeaders.%p/%f
같은 관례를 사용함
확장 헤더가 일반 파일로 추출되더라도 후속 도구가 복구하도록 의도했을 가능성이 있지만, 8GiB 초과 사례에서는 제대로 작동할지 의문이 남음
GNU tar의 숫자 인코딩과 이름 확장
GNU 헤더는 UStar의 prefix
대신 접근/변경 시각, 오프셋, 희소 파일 정보 등을 담으며, magic="ustar "
, version=" \0"
로 구분함
숫자 필드 첫 바이트의 상위 비트가 설정되면 나머지를 8진수 문자열 대신 base-256 바이너리 로 해석함
바이트당 정보량이 3비트에서 8비트로 늘어나며, size[12]
의 실질적 크기 한도는 2**80
, 즉 1 yobibyte 수준이 됨
첫 바이트와 NUL 종료 바이트를 제외한 계산이며, NUL 종료를 요구하지 않는 구현은 원리상 2**88
까지 허용할 수 있음
범용 키/값 확장 대신 특화된 L
과 K
확장 으로 긴 이름을 처리함
L
은 다음 내용 블록들을 파일 이름으로, K
는 링크 대상 이름으로 해석함
각각 pax의 path
, linkpath
레코드와 같은 역할을 함
예시에서 L
은 hannibal.txt
를 hasdrubal.txt
로 바꾸며, 10,737,418,240바이트 크기는 헤더에 직접 인코딩됨
K
는 publius.txt
의 링크 대상을 scipio.txt
에서 africanus.txt
로 바꿈
GNU 확장은 여러 개를 쌓을 수 있으며 , L → K → 멤버
와 K → L → 멤버
순서 모두 사용할 수 있음
tar-codec의 엄격한 상태 머신
tar-codec 는 첫 블록의 magic과 version으로 UStar/pax 또는 GNU를 분류하고, 이후 파싱을 해당 계열에 고정함
헤더들이 같은 계열에 속하지 않으면 아카이브를 거부함
실제 tar 생성기는 계열을 혼합하지 않으므로 이 제한의 실질적인 호환성 위험은 크지 않음
V7 파싱은 지원하지 않음. 초기 조사에서 Python 패키징 생태계에 V7 파일이 없었으며, 필요하다면 UStar와 차이가 작아 추가하기 어렵지는 않음
계열을 혼용하면 확장의 적용 대상이 모호 해짐
GNU L
/K
뒤에 pax x
가 오면, 구현마다 두 확장을 합치거나 하나만 적용하거나 GNU 확장을 다음 파일이 아닌 pax 확장 자체에 적용할 수 있음
이런 차이는 각각 악용 가능한 해석 차이가 될 수 있음
GNU 문서는 같은 종류를 포함해 L
/K
를 임의 순서로 여러 번 쌓는 것을 허용하지만, tar-codec는 멤버마다 각각 하나씩만 허용 함
pax의 x
와 g
는 스트림 어디에나 올 수 있으며, 확장 헤더는 통째로 덮어쓰기보다 레코드별 상태를 조합 함
Undeclared : 유효한 레코드가 없어 멤버 자체 UStar 헤더 값을 사용함
Active : 유효한 레코드와 값이 존재함
Tombstoned : 8 path=\n
처럼 레코드는 있지만 값이 비어 있어 명시적으로 삭제된 상태임
pax 명세 에서 빈 값은 이전 확장 값뿐 아니라 같은 이름의 헤더 필드도 삭제하므로, Tombstoned는 Undeclared와 다름
삭제된 path
는 경로 없는 파일이라는 상태를 만들 수 있음
다른 구현은 빈 경로로 추출하다 실패하거나 이전 이름 또는 확장 상태를 선택하는 등 동작이 제각각임
tar-codec는 빈 경로나 모든 멤버의 size
등 핵심 상태를 선언하려는 전역 확장 헤더처럼, 논리적으로 성립해도 무의미한 상태를 거부하려고 함
파싱 중 어느 지점에서든 I/O 또는 검증 실패 가 발생하면 실패 상태로 전환됨
블록 파싱에서 안전한 추출까지
tar-framing 은 블록 스트림을 두 단계로 정제함
물리 API는 상태 머신에 따라 블록을 하나씩 처리하고 해당 상태와 함께 반환함
논리 API는 이를 멤버 단위로 조립하며, 프레이밍에 영향을 주는 확장을 병합한 헤더, GNU L
/K
같은 별도 확장 상태, 읽을 수 있는 실제 내용을 제공함
tar-codec 크레이트 는 논리 API를 바탕으로 메모리나 디스크에 추출할 수 있는 인터페이스를 제공함
디스크 추출에는 파싱과 별개의 파일시스템 해석 차이 가 있음
기존 심볼릭 링크와 하드 링크, 파일시스템 상태에 따라 결과가 달라질 수 있음
macOS/Windows의 대소문자 처리, Windows의 심볼릭 링크 권한, 비 UTF-8 이름이나 NUL/제어 문자가 포함된 이름도 고려해야 함
archive-trait 는 형식별로 제각각이던 읽기, 생성, 추출 처리를 공통화하기 위해 만들어짐
Archive
와 ArchiveBuilder
트레이트로 공통 API를 제공함
파일시스템 추출은 cap-std 위에 구현해 추출 범위를 하나의 디렉터리에 고정하고, 여러 링크 및 경로 순회 위험을 피하도록 함
명세 준수와 실제 호환성 사이의 절충
tar 계열마다 고유한 상태 머신과 우선순위 규칙 이 있으며, 문서나 기준 구현이 이를 항상 명확히 규정하지는 않음
특히 pax의 size
처럼 프레이밍 자체를 바꾸는 상태는 먼저 처리해야 하므로, 512바이트 헤더를 각각 독립적으로 해석하는 추상화만으로는 부족함
실제 구현과 호환하기 위해 엄격한 pax 준수만을 고집할 수 없는 지점 도 있었음
pax는 레코드 값에 UTF-8을 요구하지만 GNU tar와 star
는 역사적으로 일부 벤더 레코드에 바이너리를 기록함. 관련 내용은 GNU tar 논의 에 있음
UStar/pax는 uname
, gname
의 NUL 종료를 요구해 각각 최대 31바이트여야 하지만, CPython tarfile
은 32바이트까지 채우고 33바이트부터 pax 레코드로 넘김. 관련 이슈는 CPython#159322 임
mode
, uid
, gid
, mtime
을 모두 NUL로 남기는 구현도 많음. 이는 프레이밍 위험이 없어 임의 기본값 대신 ‘누락’으로 추적함
벤더 레코드 는 VENDOR.keyword
형식이며, pax가 의미를 정의하지 않아 구현별 처리가 크게 다름
SCHILY.xattr.*
, 희소 멤버용 GNU.sparse.*
가 대표적임
tar-codec는 기본적으로 거부하며, 건너뛰거나 무시하도록 선택할 수 있음
발견한 주요 파서 해석 차이
pax/GNU 상태 혼동 으로 한 계열의 확장을 다른 계열의 확장에 적용하면, 다음 멤버가 아닌 잘못된 대상의 상태를 바꾸어 완전한 동기화 상실과 다른 추출 결과를 만들 수 있음
전역 확장과 삭제 상태 처리 오류 도 동기화를 깨뜨림
예를 들어 g
헤더의 size=1234
를 받아들이면서 무시하는 구현은, 해당 값이 UStar 크기를 덮어써야 한다는 pax 규칙과 어긋남
pax 레코드를 줄바꿈으로만 분리 하면 안 됨
<value>
자체에 줄바꿈이 포함될 수 있으므로 <length>
를 따라야 함
길이 접두사를 무시하면 역시 완전한 파서 동기화 상실로 이어질 수 있음
정수 변환의 관대함 도 문제가 됨
언어 내장 문자열→정수 API는 숫자 문자 체계나 앞뒤 공백을 다르게 받아들일 수 있음
같은 size
가 다른 정수로 파싱되면 동기화가 깨짐. 관련 예시는 Python 숫자 변환 사례 에 있음
개발 과정에서 발견한 여러 차이 중 일부는 보안 취약점 공개 절차가 진행 중 임
성능 개선과 보안 보장의 한계
tar-codec는 기존 astral-tokio-tar보다 약 2배 빠르며 , 인코딩과 디코딩 최적화 자체는 가장 어려운 부분이 아니었음
체크섬 생성과 pax 레코드 길이 접두사 계산에 쉽게 개선할 수 있는 부분이 있었음
할당 재사용, 배치 처리, 버퍼링도 효과가 컸으며, API 내부 일관성과 비동기 취소 시 정확성을 해치지 않는 범위에서 적용함
반면 빠르면서 안전한 파일 추출 은 더 어려움
cap-std의 디렉터리 권한 추적은 파일시스템 작업에 측정 가능한 비용을 추가함
openat2(2)
또는 유사한 RESOLVE_BENEATH
기능이 없으면 경로의 각 디렉터리를 열어야 하므로, 추가 시스템 호출 수가 경로 깊이에 비례함
특히 macOS의 추출 성능에 불리하게 작용함
설계는 입력을 관대하게 수용하기보다 보수적으로 수용 하고, 프레이밍에서 스트리밍과 추출로 갈수록 제약을 강화해 구조적으로 정확성을 확보하는 방식임
Astral은 잘못된 입력을 올바르게 거부하는 구조 덕분에 다른 tar 구현보다 해석 차이에 강하다고 판단하지만, 완전한 제거를 보장하지는 않음
tar의 분열된 형식과 불충분한 명세 때문에 새로운 차이가 발견될 가능성은 남아 있음
그런 경우에도 현재의 기본 구조가 대응에 적합하다고 보고 있음
uv 적용과 후속 계획
uv 0.12 부터 --preview-features tar-codec
로 사용할 수 있었으며, 0.13부터 기본 백엔드 가 됨
UV_LEGACY_TAR_BACKEND=1
로 비활성화할 수 있음
uv 자체 용도에는 사실상 필요한 기능을 갖췄지만, 더 넓은 오픈소스 생태계를 위해 희소 파일과 벤더별 pax 레코드 제어 같은 기능을 수요에 따라 검토할 계획임
다른 tar 구현의 정확성 개선에도 협력하며, Harmen Stoppels의 tar-minefield 를 더 엄격하고 해석 차이에 강한 동작을 시험하는 유망한 프로젝트로 보고 있음
같은 이유로 uv의 ZIP 파싱 교체 도 적극 검토 중이며, 후속 작업의 경험과 uv에 적용되는 개선을 공유할 계획임
댓글과 토론