1차 공개일 · 8월 18일1차 강의가 모두 공개됩니다
TECH 으로 돌아가기
TECH HACKER NEWS 오늘 7분 읽기 24 READS

10년 묵은 Zsh 히스토리 유실 버그, 범인은 종료 중 SIGINT

사라지는 명령어의 정체

리눅스와 macOS 사용자에게 셸 히스토리는 사실상 개인 지식 베이스다. Ctrl+R로 과거 명령을 되짚어 재사용하는 습관은 실무 효율의 핵심이다. 그런데 한 개발자가 수년간 겪은 현상은 이 신뢰를 흔들었다. 분명히 어제 실행한 명령이 ~/.zsh_history에서 검색되지 않았고, 파일을 열어 보면 최신 수년치 기록이 통째로 사라진 채 아주 오래된 항목만 남아 있었다. 눈에 띄는 손상은 없었다. 비출력 문자나 잘린 줄 없이 내용은 멀쩡했지만, 줄 수만 들쭉날쭉 줄어 있었다.

처음 몇 번은 매일 백업에서 복원하고 넘어갔지만 반복되자 추적이 시작됐다. 원인이 Zsh 자체인지, 다른 프로그램인지, 아니면 공용 파일에 여러 zsh 프로세스가 동시에 쓰면서 생기는 문제인지조차 불분명했다. 참고로 이 사용자는 각 셸을 독립 세션으로 두고 명령을 공용 파일에 흘려보내되, 히스토리를 세션 간에 즉시 공유하지 않고 필요할 때 exec zsh로 다시 읽어 오는 설정을 쓰고 있었다.

관측 도구로 좁혀 간 범위

먼저 파일 변경 감시가 동원됐다. inotify로 히스토리 파일만 보면 열리고 읽힌 뒤 삭제되는 것처럼 보이지만, 상위 디렉터리를 함께 감시하자 전모가 드러났다. Zsh는 기존 내용을 읽어 새 파일(.zsh_history.new)에 쓰고, 이를 원본 위에 rename해 교체한다. 즉 '삭제'로 보인 것은 원자적 교체 과정이었다. 다만 inotify나 fsnotifywait로는 어느 프로세스가 범인인지 알 수 없었고, 프로세스명과 PID까지 보여 주는 fatrace로 넘어가서야 책임 프로세스를 특정할 수 있었다.

그래도 각 프로세스가 얼마나 읽고 쓰는지는 보이지 않았다. 다음 무기는 bpftrace였다. open(2)을 비롯한 시스템 콜 시점에 히스토리 파일을 건드리는 프로세스와 스택 트레이스를 기록하도록 프로그램을 짜고, systemd 유닛으로 상시 백그라운드 실행했다. 어느 날 히스토리가 잘린 직후 로그를 확인하자 결정적 단서가 나왔다. 평소보다 적은 줄만 읽었고, EOF까지 읽었음을 뜻하는 read=0 기록이 없었다. Zsh가 파일 끝까지 다 읽지 않은 채 재작성한 것이다.

크래시를 유도해 코어 덤프로 확인

여기서 접근법이 흥미롭다. savehistfile의 제어 흐름은 따라가기 어려웠기에, 저자는 Zsh 소스(5.9.1)를 직접 고쳐 5만 줄 미만의 짧은 새 파일을 쓴 직후, 원본을 교체하기 직전에 일부러 크래시하도록 만들었다. systemd-coredump를 설치해 두면 코어 덤프가 자동 수집된다. 다만 덤프에는 히스토리 전체가 담기므로 외부 서비스에 올리는 것은 금물이다. 디버그 심볼을 켠 패치판을 며칠 돌리자 실제 크래시가 잡혔고, 백트레이스는 문제가 쓰기가 아니라 읽기 쪽에 있음을 가리켰다.

상대적으로 읽기 쉬운 readhistfile에는 조기 반환 지점이 하나 있었다. 시그널이 오면 errflag의 인터럽트 플래그를 확인하고 읽기 루프를 break로 중단한다. 반면 종료 시 히스토리를 압축(크기 제한 적용 등)하기 위해 호출되는 savehistfile은 쓰기 도중 인터럽트 여부를 확인하지 않았다. 그래서 읽기가 중간에 끊겨 불완전해진 히스토리를 savehistfile이 그대로 다시 써 버리며 원본을 잘라 냈다. 트리거는 종료 습관이었다. 저자는 mosh 위에 장시간 SSH를 띄우고 그 위에 여러 세션을 다중화해 쓰는데, 하루를 마칠 때 Ctrl+D로 세션을 종료하고 곧이어 Ctrl+C를 연타한다. Ctrl+D로 종료 중이던 Zsh의 readhistfile이 재작성에 시간이 걸리는 사이 Ctrl+C(SIGINT)를 맞으면 이 버그가 발동한다.

재현기를 만들어 2025년 3월 zsh-workers 메일링 리스트에 신고했고, Bart Schaefer가 4월에 수정안을 올렸다. 다만 릴리스가 오래 멈춰 있던 탓에 배포가 늦어졌고, 5.9.1에서는 이 수정이 누락됐다가 지적을 거쳐 5.9.2(2026년 7월 12일)에 비로소 포함됐다. Debian에서 zsh를 핀 고정한다면 zsh와 zsh-common 두 패키지를 함께 고정해야 한다는 조언도 남겼다.

실무자가 챙길 점

데이터 유실을 일으키는 버그가 널리 쓰이는 셸에서 약 10년간 방치됐다는 사실은 곱씹을 만하다. Apple이 2019년 macOS 기본 로그인 셸을 Zsh로 바꾼 것을 떠올리면 영향권은 결코 좁지 않다. 물론 SIGINT가 날아들기 쉬운 이 사용자의 종료 습관은 특수하지만, 일부 사용자는 자기도 모르게 히스토리 일부를 잃었을 가능성이 크다. 당장은 Zsh 5.9.2 이상으로 올리는 것이 근본 해법이다.

한편 히스토리 유실이 이 버그가 아닐 수도 있다. 대표적 함정이 HISTFILE의 의도치 않은 export다. Emacs의 TRAMP 모드는 기본적으로 HISTFILE을 export하는데, 대다수 셸 설정은 HISTFILE 값을 바꾸기만 할 뿐 unexport하지 않는다. 그 결과 다른 셸이 다른 크기 제한을 갖도록 설정된 환경에서 파일이 엉뚱하게 잘릴 수 있다. 실제로 저자는 bash 기본값 64000줄 설정이 걸린 업무용 컴퓨터에서 ~/.zsh_history가 그 길이로 잘린 적이 있었고, 이후 ~/.zshrc에서 HISTFILE을 명시적으로 unexport하기로 했다. 도구 하나로 끝나지 않고 inotify에서 fatrace, bpftrace, 코어 덤프로 이어진 이 추적기는, 재현하기 어려운 간헐적 결함을 관측 가능한 신호로 바꿔 가는 과정 자체가 실무 디버깅의 교본이라 할 만하다.

SOURCE · HACKER NEWS
원문 전체 보기 → https://michael.stapelberg.ch/posts/2026-08-09-zsh-history-t...
SHARE
NEXT · CHOOSE

변화를 읽었다면,
내가 만들 수익 구조를 고릅니다.

정보를 더 모으는 데서 멈추지 않고, 광고·외주·판매·중개·구독 중 내 상황에 맞는 출발점을 정해보세요.

21가지 수익 구조 살펴보기
처리 중...