2P by GN⁺ | ★ favorite | 댓글 1개
  • Bun v1.1.5는 크래시나 패닉이 나도 개인정보 없는 약 150바이트 URL만으로 Zig/C++ 스택 정보를 전달하는 bun.report 크래시 리포터를 추가함
  • 기존 OS 크래시 리포터와 코어 덤프는 디버그 심볼·성능·개인정보·실행 파일 크기 부담이 커서 CLI 도구인 Bun에 적용하기 어려움
  • 새 방식은 ASLR로 의미가 흐려진 주소를 모듈 기준 상대 주소로 바꾸고, 서버가 커밋 SHA와 플랫폼에 맞는 디버그 심볼로 함수명을 복원함
  • URL에는 플랫폼, 서브커맨드, 커밋 SHA, 기능 플래그, 스택 주소, 크래시 타입과 메시지가 들어가며, 스택 주소는 base64 VLQ로 짧게 인코딩됨
  • JavaScript/TypeScript 소스 코드나 환경 변수는 보내지 않고, Bun 팀이 진단에 필요한 Zig/C++ 스택 정보와 일부 메타데이터만 전달함

Bun이 자체 크래시 리포터를 만든 이유

  • Bun에는 작성 시점 기준 2,600개 이상의 열린 GitHub 이슈가 있으며, 일부 이슈는 재현과 디버깅이 특히 어려움
  • Sentry 같은 크래시 리포팅 서비스는 앱과 SaaS 제품에는 적합하지만, Bun 같은 CLI 도구에서 코어 덤프를 업로드하면 개인정보, 성능, 실행 파일 크기 문제가 커짐
  • Bun v1.1.5는 Zig와 C++ 크래시 리포트를 위한 작은 새 포맷을 도입함
    • 크래시 리포트는 약 150바이트 URL에 들어감
    • 개인정보는 포함하지 않음

OS 크래시 리포터만으로 부족한 점

  • macOS 같은 일부 운영체제에는 내장 크래시 리포터가 있지만, 제대로 활용하려면 보통 애플리케이션과 함께 디버그 심볼을 배포해야 함
  • 디버그 심볼은 Bun 배포 크기를 크게 늘림
    • Linux 디버그 심볼은 약 30MB
    • macOS 디버그 심볼은 약 9MB
    • Windows .pdb 파일은 250MB 이상
  • Bun 실행 파일 예시는 llvm-strip 전후로 60M에서 51M로 줄어듦
  • 디버그 심볼 없이 크래시가 발생하면 스택 트레이스에는 ???와 주소만 남아 활용도가 낮음
  • ASLR(Address space layout randomization) 때문에 함수 주소에는 무작위 오프셋이 섞이며, 그대로는 함수명을 복원할 수 없음

bun.report의 동작 방식

  • Bun v1.1.5에서 크래시나 패닉이 발생하면 Bun은 버전, 플랫폼, 실행 인자, 메모리 사용량, 크래시 메시지와 함께 bun.report 링크를 출력함
  • 사용자가 링크를 열면 미리 채워진 GitHub 이슈 폼으로 리디렉션됨
  • URL 안에는 재매핑된 스택 트레이스가 인코딩되어 있음
  • 서버는 URL에 담긴 정보를 바탕으로 스택 주소를 복원하고, Bun 팀이 읽을 수 있는 크래시 리포트로 변환함

주소를 읽을 수 있는 스택 트레이스로 바꾸는 절차

  • 함수 주소는 애플리케이션 코드가 메모리에 로드된 위치를 가리키는 포인터이며, 보안상 무작위 오프셋이 포함됨
  • 기본 아이디어는 원시 주소에서 바이너리의 기준 주소(base address) 를 빼 상대 주소를 얻는 것임
  • 실제 구현은 플랫폼별 API 차이 때문에 더 복잡함
    • Windows는 GetModuleHandleExWGET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS 플래그를 사용하고, 모듈 포인터를 기준 주소로 삼음
    • Linux는 dl_iterate_phdr로 로드된 모듈을 순회하고, 주소가 포함된 모듈의 dl_phdr_info.dlpi_addr를 기준 주소로 사용함
    • macOS는 _dyld_image_count, _dyld_get_image_header로 모듈을 순회하고 _dyld_get_image_vmaddr_slide로 ASLR 슬라이드를 얻음
      • macOS 결과 주소에는 이미지 오프셋이 남아 있으며, Bun의 경우 0x100000000
      • URL을 짧게 만들기 위해 이 오프셋을 제거하지만, llvm-symbolizer로 재매핑하기 전에는 다시 더해야 함
  • Linux와 macOS에서는 첫 번째 모듈이 메인 애플리케이션 바이너리를 가리킴
  • Windows에서는 모듈 이름과 peb.ProcessParameters.ImagePathName을 비교해 메인 바이너리인지 판단할 수 있음
  • Bun은 로컬에서 디버그 심볼을 내려받고 파싱하지 않고, 서버에 디맹글링을 맡김
    • 서버는 디버그 심볼을 캐시할 수 있음
    • 스택 트레이스를 몇 초 안에 디맹글링할 수 있음
    • 동시에 새 GitHub 이슈를 여는 링크 역할을 함

bun.report URL 구조

  • bun.report URL에는 다음 정보가 인코딩됨
    • Platform: 플랫폼을 나타내는 한 글자. 예를 들어 w는 x86_64 Windows, M은 aarch64 macOS
    • Subcommand: bun test, bun install, bun run 같은 서브커맨드를 나타내는 한 글자
    • Commit SHA: 현재 Bun 버전의 커밋 SHA이며, 나중에 디버그 심볼을 가져오는 데 사용됨
    • Feature Flags: 크래시 전 사용된 API와 기능을 나타내는 표시
    • Stack Trace Addresses: 앞 단계에서 계산한 주소들
    • Crash Type: 크래시 타입을 나타내는 한 글자
    • Crash Message: 크래시 타입에 따라 형식이 달라지는 메시지
  • URL에 들어간 버전 번호는 실제 처리보다 사람이 읽기 위한 표시임
  • 이 정보만으로도 일부 크래시 특징을 손으로 파악할 수 있음
    • w 식별자를 보면 Windows 크래시임을 빠르게 알 수 있음
    • 문자열 끝부분의 A2를 보면 세그멘테이션 폴트를 식별할 수 있음

짧은 URL을 위한 VLQ 인코딩

  • 스택 트레이스 주소는 URL을 짧게 유지하기 위해 base64 Variable Length Quantity(VLQ) 숫자로 인코딩됨
  • VLQ는 작은 숫자를 더 적은 문자로 표현하면서 큰 숫자도 인코딩할 수 있음
  • JavaScript 소스 맵에서 줄 번호를 저장할 때도 같은 기법이 사용됨
  • 서버는 VLQ 값을 다시 상대 주소로 디코딩하고, 커밋 해시와 플랫폼을 사용해 디버그 심볼을 다운로드한 뒤 llvm-symbolizer로 함수명을 디맹글링함
  • 예시 크래시에서는 Windows의 모듈 리졸버 코드 일부인 dirInfoCachedMaybeLog에서 어설션이 실패한 사실이 드러남

기능 플래그 인코딩

  • URL은 64비트 정수도 인코딩하며, 각 비트는 Bun의 특정 기능 사용 여부에 대응함
  • 이 플래그는 어떤 API와 시스템이 크래시에 영향을 줬을 가능성이 있는지 힌트를 줌
    • .env 파일이 자동 로드되면 dotenv 기능이 설정됨
    • fetch()를 사용하면 fetch 기능이 설정됨
  • Bun은 전역 변수 컨테이너로 기능 사용 여부를 추적하고, 각 API 내부에서 해당 숫자를 증가시켜 사용을 표시함
  • Zig의 컴파일 타임 메타프로그래밍을 사용해 기능 목록을 순회하고, 기능마다 1비트씩 쓰는 packed struct를 동적으로 생성함
  • inline for를 사용하면 컴파일 타임에 기능 목록을 순회하되, 실제 비트 설정은 런타임에 수행할 수 있음
  • 새 기능을 기존 Features struct에 추가하면 크래시 리포터에서도 반복 작성 없이 처리됨
  • 같은 방식은 C나 Rust 매크로로도 가능하지만, Bun 측 구현에서는 Zig comptime이 더 단순하고 읽기 쉬운 방식으로 사용됨

코어 덤프와의 차이

  • 코어 덤프는 훨씬 많은 정보를 담지만, 크기가 크고 디버그 심볼이 있어야 유용하며 민감하거나 기밀인 정보를 많이 포함할 수 있음
  • Bun의 새 리포트 방식은 JavaScript/TypeScript 소스 코드, 환경 변수, 기타 민감 정보를 보내는 상황을 피함
  • 기본적으로 모든 것을 보내는 대신, 문제 진단에 필요할 가능성이 높은 Zig/C++ 스택 트레이스와 몇 가지 세부 정보만 보냄
  • 추가 정보가 필요하면 사용자에게 별도로 요청할 수 있음
  • 이전처럼 매핑되지 않은 주소만 남는 상황보다 Bun 팀이 크래시를 진단하기 쉬워짐

데모

  • 크래시 리포터를 테스트할 수 있는 작은 웹앱이 bun.report에 제공됨
  • 어떤 크래시 리포트 URL이든 끝에 /view를 붙이면 해당 웹앱 화면으로 이동함

댓글과 토론

Hacker News 의견들
  • 일반 스택 추적 대신 이 방식을 쓰는 이유가 수 MB짜리 디버그 심볼을 배포하지 않기 위해서라면, 더 나은 선택지인 디버그 테이블에 함수 이름만 넣는 방식을 무시한 것처럼 보임
    스택 추적을 보려고 웹 서비스를 써야 하는 것보다 훨씬 나은 방법이고, 이건 이론만이 아니라 LLVM에 이미 구현돼 있음: https://clang.llvm.org/docs/UsersManual.html#cmdoption-gline...

    • 일반 스택 추적보다 이 방식을 쓰는 핵심 이유는 디버그 심볼 크기 때문이 아니라, GitHub 이슈에 크래시 리포트를 올릴 만큼 참을성 있는 사람이 거의 없기 때문임
      URL 하나로 필요한 내용을 거의 자동 입력해 주면 충분히 쉬워지고, 그래야 개발자들이 실제로 크래시 리포트를 올림. 크기도 중요해서 사용자에게 단점이 생기지 않게 하려 했지만, 핵심은 전체 과정을 아주 쉽게 만드는 것임
    • “더 나은 선택지를 무시했다”, “당연히 더 낫다”는 표현은 조금 단정적으로 들림. 아마 그런 가능성은 알고 있었을 것임
      이 사용 시나리오에서는 웹 서비스로 스택 추적을 봐야 한다는 점이 큰 단점이 아님. 프런트엔드 JavaScript 번들을 난독화/축소하고 소스 맵을 Sentry에 올린 뒤, 사용자 브라우저에서 온 스택 추적을 Sentry로 복원해 보는 것과 거의 같음. 사용자는 어차피 그 스택 추적을 볼 일이 없고, 나도 Sentry를 써서 보는 게 불편하지 않음. 그게 아니었으면 아예 보지도 못했을 테니까
    • 논의 맥락도 모르고, 어떤 절충안이 있었는지도 모르는 상태에서 “당연히” “그냥” 다른 걸 하면 된다고 비판하면 좀 오만하게 들림
      대안을 제안하는 방식은 여러 가지가 있음
    • macOS/iOS에서는 Mach-O 바이너리에 LC_FUNCTION_STARTS 섹션만 넣어 배포하는 방법도 있음
      이 플랫폼들에서 전체 디버그 심볼 없이도 시스템 라이브러리의 함수 이름을 심볼화가 찾아내는 방식임
    • 그래도 꽤 커질 수 있음. 개인적으로는 가치가 있다고 봐서 보통 무조건 포함하지만, 대부분의 소프트웨어는 그렇게 하지 않음
  • 훌륭하고 아주 창의적임. 많은 프로젝트가 이 방식을 따라 해도 좋겠음. 핵심은 실행 파일/공유 객체 기준의 상대 프로그램 카운터로 스택 추적을 남기는 것임
    Bun은 정적 링크된 것으로 알고 있는데, 동적 링크 시스템이라면 정규화된 프로그램 카운터마다 작은 숫자 형태의 공유 객체 ID를 앞에 붙여야 할 것임

    • 완전히 새로운 방식은 아님. 게임처럼 심볼을 배포할 수 없는 환경에서 플레이어 PC에서 크래시가 날 때 흔히 쓰임
      예를 들어 Unreal Engine 크래시 리포터는 수년 전부터 이런 단순한 형식을 보낼 수 있었고, 각 스택 프레임에서 꽤 정확한 함수/줄 번호를 복원할 수 있음. 다만 보통은 스택 변수까지 있으면 무슨 일이 있었는지 추가 힌트를 얻을 수 있어서 미니덤프를 더 선호함
  • Microsoft는 이런 걸 정말 잘함. SQL Server에서는 개인정보를 제거한 미니덤프를 썼는데, 아주 작고 굉장히 유용했음
    당시에도, 15년 전인데도 프로덕션 SQL Server의 전체 덤프는 옮기기 어려울 정도로 거대한 파일이었음

    • 궁금한데, Microsoft 내부 서비스용이었는지 고객 배포 환경용이었는지 모르겠음. 후자라면 무엇이 개인식별정보인지 어떻게 알았을까?
  • Zig 관련 첫 트윗을 본 뒤로 몇 년간 Bun을 지켜보다가 최근에 쓰기 시작했는데, 별다른 번거로움 없이 그냥 잘 동작함

  • Bun은 꽤 매력적임. 작은 예제 프로젝트 몇 개에서 써 봤는데 속도도 좋고, 패키지 관리와 JavaScript 런타임을 합쳐 놓은 점이 마음에 듦
    다만 진지한 프로젝트 대부분에서 Dependabot을 쓰고 있음. Dependabot의 Bun 지원은 작업 중이거나 적어도 몇몇 저장소 이슈에서 논의 중인 것으로 알고 있어서, 지원이 배포될 때까지는 사용을 미루는 중임

    • 우리도 전환 전에 Dependabot 미지원 때문에 망설였지만, Renovate가 Bun과 동작하고 당분간 충분한 대체재가 된다는 걸 알게 됨
      후회는 전혀 없음. 빨라진 부분들이 누적해서 주는 절감 효과와 개발자 경험의 큰 향상은 기대만큼 가치가 있음
  • 이런 데 얼마나 많은 신경을 썼는지 알아차릴 사람은 많지 않을 것임. Bun 팀이 자기 craft를 얼마나 신경 쓰는지 잘 보여 줘서 좋음

  • Bun은 놀랍지만, 최근 Fastify로 HTTP/2 서버를 만들려고 했더니 안 됐음
    node:http2 createServer is not yet implemented in Bun 오류가 나왔고, 메시지가 가리키는 이슈는 사실 HTTP/2 클라이언트 지원에 관한 것임. 클라이언트 지원은 v1.0.13에서 이미 출시됨: https://bun.sh/blog/bun-v1.0.13#http2-client-support
    NotImplementedError 메시지는 서버 쪽 이슈를 가리키도록 바뀌어야 함: https://github.com/oven-sh/bun/issues/8823
    HTTP/2 서버 지원은 상위권 기능 요청 중 하나임: https://github.com/oven-sh/bun/issues?q=is%3Aissue+is%3Aopen...
    이 기능이 나오면 훨씬 많은 사람이 Bun으로 옮길 수 있을 듯함

    • 그게 지금 Bun의 상태임. 구현을 기다리면, 끝난 뒤에는 다른 API 구현이 더 필요하다는 걸 알게 되고, 또 기다리면 나오긴 하지만 여러 경계 조건에서 크래시가 나고, 다시 기다리는 식으로 반복됨
      Bun은 아직 생애주기상 너무 이른 단계임. 그래도 프로젝트에는 기대가 큼
  • 실제로 Bun 쓰는 사람이 있는지 궁금함. 기대만큼 좋은가?

    • 아직 프로덕션에서는 안 써 봤지만, 일회성 스크립트와 사이드 프로젝트에는 아주 좋았음
      TypeScript Node 환경에서 ts-node, ts-jest, ESM 지원, 최상위 await 등을 세팅하는 건 필요 이상으로 번거로움. 최근 Node 릴리스들이 일부 불편을 줄이긴 했지만, bun init만큼 간단하진 않음. bun shell API도 즐겁게 쓰고 있음: https://bun.sh/blog/the-bun-shell
    • REPL이 필요하거나 네이티브 모듈을 쓸 계획이 없다면 괜찮음. REPL이 있다고는 하지만 업데이트될 때마다 항상 6초 넘게 지연돼서 매우 불편함
      오류 메시지도 Node보다 상당히 나쁨. 한동안 써 봤지만 요즘은 Node에 —loader tsx를 붙이면 내가 원하는 걸 모두 해 주고 단점도 없음. 단순한 서버, 예를 들어 WebSocket을 쓰고 네이티브 모듈이 필요 없다고 확신한다면 Bun을 고려할 만함. 실제로 그런 서비스 몇 개를 운영 중임
    • 1.0이 나오자마자 쓰기 시작했고 다시 돌아가지 않았음. 지금은 모든 프로젝트에 적용하는 중임
    • 약 1만5천 줄짜리 프로그래밍 언어 프로젝트에서 개발 및 테스트 실행기로 쓰고 있는데, 지금까지 Bun 특유의 문제는 없었음
      즉시 시작되는 속도는 아직도 놀라움
    • 최근 쓰고 있는데 아주 좋음. TypeScript 컴파일을 신경 쓰지 않아도 되는 식의 삶의 질 개선이 정말 편하고 속도도 빠름
      아직 빠진 것들이 있긴 하지만, 나에게는 이미 Node보다 나음
  • 이 글은 Zig 사례 연구로도 훌륭하게 느껴짐. 흥미로움

  • Bun은 REPL을 쓰기 전에 패키지 37개를 내려받아야 함. 인터넷이 없으면 REPL도 못 씀
    bun repl 실행 시 bun-repl 패키지 매니페스트 다운로드에 실패한다는 오류가 남. 큰 문제는 아니지만, 단일 실행 파일을 PATH에 넣기만 하면 설치 없이 바로 동작하길 기대했고 꽤 기대하고 있었음

    • 아직 REPL 구현을 우선순위에 두지 않았음. 현재 REPL은 커뮤니티가 구현한 bun-repl npm 패키지
      내부적으로 bun replbunx bun-repl과 같은 일을 함