TL;DR
- QNX 6.6에서 프로세스 스레드를 확인하던 중 대상 프로세스가 종료되면
ps가 무한 루프에 빠져 CPU 코어를 포화시키는 오래된 경쟁 조건 버그임. - 업데이트 실패처럼 보인 문제는 실제로 업데이트 프로그램이 아니라, 특정 구형 하드웨어에서 실행되는 펌웨어 코드의
system("ps -e | grep Foo")호출에서 비롯됨. - 원인은
ps의 스레드 순회 루프가devctl()실패 시 종료되지 않는 데 있으며, QNX Momentics IDE의 원격 디버깅과 공개된 구버전 소스 코드로 재현·확인함. - 펌웨어에서 비대화형 코드의
ps사용을 제거해 문제를 해결했으며, 해당 업데이트 문제는 이후 다시 발생하지 않음. - 오래되고 검증된 코드도 버그를 포함할 수 있고, 타이밍 변화로 잠복 버그가 드러날 수 있으므로 루프 종료 조건을 보장하고
system()및 셸 유틸리티 사용을 피해야 함.
업데이트 실패로 보인 문제
- 기기 펌웨어 업데이트의 신뢰성을 담당하는 상황에서, 업데이트 전에 없던 문제가 업데이트 직후 나타나면 업데이트 프로그램이 원인처럼 보이지만 실제 원인은 다른 코드일 수 있음.
- 접수된 문제는 버전 X에서 버전 Y로 업데이트할 때 센서 로그 파일을 가져오는 단계에서 실패한다는 내용이었으며, 사용자가 기기를 수동으로 재부팅한 뒤에는 정상 작동함.
- 기기에 접속해 로그를 내려받은 결과 업데이트 자체는 성공적으로 설치된 상태였음. 새 펌웨어로 부팅한 뒤 일부 프로세스의 시작이 예상보다 늦었지만, 로그에는 이를 설명할 오류가 없었음.
- 여러 기기에 펌웨어 Y를 설치해도 재현되지 않았으나, 동료가 문제를 알아차리고 분석을 위해 시스템을 그대로 둔 덕분에 조사 가능했음.
CPU를 포화시킨 `ps`
- 문제가 발생한 기기는 지연이 심하고 간단한 명령도 겨우 처리하는 상태였음.
top결과는 프로세스 60개, 스레드 646개, 가용 메모리 543MB였으며 CPU 사용률은 사용자 영역 16%, 커널 영역 35%였음. - 두 CPU의 유휴율은 각각 약 55%와 42%였지만, 시스템은 하이퍼스레딩을 사용하면서 물리 코어가 하나뿐이므로 실제 코어 부하가 거의 100%인 상황이었음.
top목록에서 QNX 커널과ps가 높은 CPU 시간을 차지했으며,ps가 코어를 완전히 포화시키고 커널 부하도ps의 작업에서 비롯된 것으로 판단함.ps -ef로 부모 프로세스 ID(PPID)를 추적한 결과, 해당ps는 셸sh가 실행했고 셸은 사내 전용 프로세스가 실행한 것으로 드러남.- 로그로 해당 프로세스가 멈춘 지점을 찾아 다음 코드를 확인함. 이는 프로그램
Foo가 존재하는지 확인하는 레거시 코드였으며, 특정 구형 하드웨어에서만 실행됨. system("ps -e | grep Foo")호출은 오래 작동해 온 코드였으며, 최신 하드웨어에서는 경로가 실행되지 않아 대부분의 기기에서 문제가 재현되지 않음.- 구형 하드웨어에서도 재현 가능성이 매우 낮았음. 무한 루프에 빠진
ps프로세스를 종료하자 시스템이 풀리고 정상 작동을 회복함.
`ps`의 무한 루프 원인
- 시스템은 QNX 6.6을 사용하며
ps는 소스 코드가 공개되지 않은 바이너리였음. QNX Momentics IDE의 ‘QConn을 통한 원격 프로세스 연결’ 기능으로 실행 중인 프로세스를 어셈블리 수준에서 분석함. ps가devctl()을 반복 호출하는 루프에 갇혀 있었으며, 호출 결과는ESRCH(해당 프로세스 없음)였음. 이 오류는MsgSendv_r에서 반환된-3에 해당하며, 문서에는 호출 스레드가SEND또는REPLY상태로 대기하는 동안 서버가 종료된 경우라고 설명돼 있음.- 공개돼 있던 구버전 QNX 소스 코드를 살펴보면,
ps는/proc의 각 프로세스에 대해 주소 공간(as) 파일 디스크립터를 열어 표시할 정보를 읽음. 각 프로세스의 스레드를 순회하는 루프에는 종료 조건이 충분하지 않았음. - 스레드 정보를 가져오는
devctl()호출은DCMD_PROC_TIDSTATUS명령을 사용함. 대상 프로세스가 스레드 순회 루프에 들어가기 직전에 종료되면 호출이 실패하고, 성공 시에만 실행되는 조건문에 진입하지 않으므로 루프가 끝나지 않음. ps를 디버거에서 멈춘 뒤 조사 대상 프로세스를 종료하고ps실행을 재개하는 방식으로 가설을 확인했으며, 같은 무한 루프가 재현됨.- 구버전
ps소스는 QNX 6.6에서 수정 없이 컴파일됐고, 기존 비공개 바이너리와 똑같이 동작했음. 이 버전으로 밤새 재부팅을 반복하는 자동 테스트를 실행해 문제를 재현하고, 소스 코드가 있는 상태에서 디버거로 원인을 확인함. - 이 분석에 따라 비공개 바이너리에도 약 15년 된 버그가 여전히 남아 있는 것으로 판단함.
QNX 생태계와 버그의 배경
- 10여 년 전 QNX 소스 코드가 공개돼 있던 시기에는 커널 실험, 유틸리티 개발, 포럼의 상호 지원이 활발한 오픈소스 커뮤니티가 있었음.
- QNX는 완전한 데스크톱 그래픽 사용자 인터페이스(GUI)를 제공했고 Firefox를 실행했으며, 통합 개발 환경(IDE)과 컴파일러를 갖춘 자체 호스팅 개발 환경도 제공했음.
- QNX가 인수된 뒤 소스 코드 접근이 철회되고 커뮤니티가 크게 위축됐음. 질문은 공개된 공간보다 QNX에 직접 보내는 비공개 지원 요청으로 이동했고, QNX 관련 지식 습득은 점점 어려워졌으며 최신 QNX용 오픈소스 소프트웨어는 사실상 존재하지 않고 드라이버 상황도 심각함.
- QNX 커널은 뛰어난 설계와 흥미로운 구조를 갖춘 커널이지만 기업 소유의 제약 아래 놓여 있음.
해결 방법과 재발 여부
- 펌웨어에서 문제가 갑자기 드러난 이유는 확정할 수 없으며, 부팅 시 스케줄링 순서나 타이밍 변화가 원인일 가능성만 제시됨. 경쟁 조건 버그이므로 언제든 나타날 수 있으며, 해당 코드는 생산 환경에 오랫동안 배포돼 있었음.
- 자체
ps유틸리티를 배포하는 방안도 고려했으나, 최신 비공개 바이너리에서 수정됐을 수 있는 다른 버그를 구버전 공개 소스로 되돌릴 경우 수정 사항을 잃을 우려가 있었음. - 대신 코드베이스를 살펴 비대화형 코드에서
ps를 호출하는 곳을 제거했으며, 해당 사용 사례는 많지 않았음.ps자체의 버그는 남아 있지만 대화형 터미널에서는 재현이 거의 불가능하고 펌웨어도 더 이상 이를 사용하지 않음. - 이후 해당 업데이트 문제는 다시 발생하지 않았음. QNX 6.6보다 최신 생태계에도 버그가 남아 있을 가능성이 높다고 보지만, 소스 코드가 공개되지 않아 직접 수정 사항을 제출할 수는 없음.
교훈
- 충분히 검증되고 오래된 코드와 평판이 좋은 배포자도 버그를 피할 수 없음.
- 오래된 버그도 타이밍이나 메모리 배치의 미묘한 변화로 갑자기 드러날 수 있음.
- 파일 시스템이 관여하는 경우 경쟁 조건으로 인한 버그 위험이 상당함.
- 소스가 공개되지 않은 운영체제와 생태계는 디버깅이 어렵고, 오래된 오픈소스 릴리스도 분석에 도움이 됨.
- 비대화형 코드에서 대화형 셸 유틸리티를 사용하지 말고 가능한 한
system()을 피해야 함. 성능이 좋지 않을 뿐 아니라 이 사례 같은 버그를 유발할 수 있음. - 반복문은 반드시 종료되도록 해야 함. 단조롭게 증가하거나 감소하는 경계형 반복문은 종료를 보장하며, 반복문을 지나치게 복잡하게 만들지 않는 것이 중요함.
댓글 (0)
로그인하면 이 기사에 내 생각을 남길 수 있어요