ES /docs

PotreeConverter SIGSEGV — memory safety violation on malformed pointcloud

RCA: PotreeService::runPotreeConvertor | end - code: 139

Overview#

What Happened#

2026-04-30 06:23~06:56 UTC 사이에 cupixworks-any-potree-agent 서비스에서 네이티브 C++ 바이너리 PotreeConverter(/tmp/PotreeConverter/build/PotreeConverter)가 SIGSEGV(Segmentation fault, exit code 139)로 크래시하여 pointcloud potree 변환이 3건 실패했다. eu-central-1과 ap-southeast-2 두 리전의 서로 다른 호스트에서 발생했으며, 동일 시간대 다른 pointcloud는 정상 처리 완료되었다.

Quick Facts#

Field Value
exception.class ChildProcess (exit code 139 — SIGSEGV)
exception.message Command failed: /tmp/PotreeConverter/build/PotreeConverter ./potree_converter_input.json — Segmentation fault (core dumped)
top_frame potree-service.ts:309-312
runtime Node.js 20 (child_process.exec → native C++ binary PotreeConverter v0.0.173)
env production, eu-central-1 / ap-southeast-2

Affected Teams#

Team / Domain Error Count Impact
innovo / eu-central-1 (pointcloud 108456) 1 pointcloud potree 변환 실패, potree_state: error 전환
unispace / ap-southeast-2 (pointcloud 199894, 199896) 2 pointcloud potree 변환 실패, potree_state: error 전환

Timeline#

  1. 2026-04-30 06:22:56Z — pointcloud 108456 runByMessage 시작 (eu-central-1, team: innovo)
  2. 2026-04-30 06:22:58Z — PotreeConverter exec 시작
  3. 2026-04-30 06:23:00Z — 첫 번째 SIGSEGV (exit code 139, ~2초 후), SQS message bd5110d8 삭제
  4. 2026-04-30 06:55:10Z — pointcloud 199896 runByMessage 시작 (ap-southeast-2, team: unispace)
  5. 2026-04-30 06:55:13Z — PotreeConverter exec 시작
  6. 2026-04-30 06:56:02Z — 두 번째 SIGSEGV (pointcloud 199896, ~49초 후), SQS message 719e3de7 삭제
  7. 2026-04-30 06:56:04Z — pointcloud 199894 runByMessage 시작 (동일 호스트 ip-10-1-36-79)
  8. 2026-04-30 06:56:07Z — PotreeConverter exec 시작
  9. 2026-04-30 06:56:41Z — 세 번째 SIGSEGV (pointcloud 199894, ~34초 후), SQS message 80ab536f 삭제

Error Log#

Datadog Logs

text
PotreeService::runPotreeConvertor | end - code: 139

Impact#

  • Service: cupixworks-any-potree-agent
  • 발생 횟수: 3
  • 최초 발생: 2026-04-30T06:23:00.416Z
  • 최근 발생: 2026-04-30T06:56:41.929Z
  • 영향 범위: pointcloud 3건(108456, 199896, 199894)의 potree 변환이 영구 실패. handlingMessageErrors에서 SQS 메시지를 삭제하고 potree_stateError로 전환하므로 자동 재시도 불가. 해당 pointcloud의 3D 뷰어 Potree 렌더링이 불가능하다. 동일 시간대 다른 pointcloud(eu-central-1: 108450108464 시리즈, ap-southeast-2: 199846199892 시리즈)는 정상 변환 완료.

Root Cause Summary#

PotreeConverter C++ 바이너리(/tmp/PotreeConverter/build/PotreeConverter, production v0.0.173)가 특정 pointcloud 입력 파일을 처리하는 과정에서 segmentation fault(SIGSEGV, exit code 128+11=139)를 발생시켰다. 3건 모두 killed: false, signal: null로 OS OOM killer가 아닌 프로세스 자체의 메모리 접근 위반이다. eu-central-1과 ap-southeast-2 두 리전의 서로 다른 호스트에서 동일 패턴으로 발생했으므로 특정 인프라 이슈가 아니라, 해당 입력 파일의 데이터가 PotreeConverter 바이너리의 메모리 안전하지 않은 코드 경로를 트리거한 것으로 판단된다. 크래시까지 소요 시간이 입력에 따라 2초~49초로 상이하여 입력 의존적 문제를 확인한다.

부차적으로, Node.js 래퍼의 에러 핸들링에 두 가지 결함이 있다: (1) reject() 호출 시 에러 객체를 전달하지 않아(potree-service.ts:312) 에러 컨텍스트가 유실되고, (2) getApiErrorToDeleteMessage'undefined error' 문자열(truthy)을 반환하여 ApproximateReceiveCount: 1(첫 시도)임에도 SQS 메시지가 즉시 삭제되어 재시도 기회가 차단된다.

Technical Analysis#

Code Path#

  1. Entry point — SQS 메시지 수신 및 처리 시작 (potree-service.ts:180):

runByMessage가 SQS 메시지에서 pointcloud ID를 추출하고, 인증 → 메타데이터 로드 → 파일 다운로드 → PotreeConverter 실행 순으로 파이프라인을 실행한다.

packages/cupix-tesla-potree-agent/src/potree-service.ts:180-206typescript
private runByMessage = async (message: AWS.SQS.Message) => {
    this._messageInProcess = message;
    const messageBody = message.Body;
    if (messageBody && CPUtils.isJsonString(messageBody)) {
        const msgObject = JSON.parse(messageBody);
        const targetId = Environment.DEBUG_MODE && Environment.CPX_MODEL_ID ? Environment.CPX_MODEL_ID : msgObject.id;
        setLogMeta({ pointcloud: { id: targetId } });
        // ...
        await this.authenticateByMessage(msgObject);
        const cpPointcloud = await this.createCPPointcloudByPointcloudId(targetId);
        if (cpPointcloud) {
            // ...
            await this.downloadFile(cpPointcloud.downloadUrl, cpPointcloud.originalFilePath);
            const entityParameters = await this.loadEntityParameter(cpPointcloud.id);
            const options = await this.createOptions(entityParameters);
            await this.runPotreeConvertor(cpPointcloud, options);  // Failure point
  1. PotreeConverter 바이너리 실행 (potree-service.ts:258-318):

입력 파일 경로와 옵션을 JSON으로 작성하여 child_process.exec로 바이너리를 실행한다. 바이너리 경로는 /tmp/PotreeConverter/build/PotreeConverter(constants.ts:9).

packages/cupix-tesla-potree-agent/src/potree-service.ts:290-307typescript
const potreeConverterInputJson = { 'potree-converter': {
    'input_filepath': cpPointcloud.originalFilePath,
    'options': options.options,
    'output_dirpath': cpPointcloud.potreeDirPath
}};
fs.writeFileSync('./potree_converter_input.json', JSON.stringify(potreeConverterInputJson));
const potreeConvertorCommand = Constants.DefaultPotreeConvertorPath + ' ./potree_converter_input.json';
logger.info('PotreeService::runPotreeConvertor | exec - %s', potreeConvertorCommand);
const execPotreeConvertor = child_process.exec(potreeConvertorCommand, (error, stdout, stderr) => {
    if (error) {
        logger.error('PotreeService::runPotreeConvertor | %s', error);
    }
    if (stderr) {
        logger.warn('PotreeService::runPotreeConvertor | %s', stderr);
    }
});
  1. Failure point — exit code 139 감지 후 인자 없는 reject() 호출 (potree-service.ts:309-317):
packages/cupix-tesla-potree-agent/src/potree-service.ts:309-317typescript
execPotreeConvertor.on('close', (code) => {
    if (code !== 0) {
        logger.error('PotreeService::runPotreeConvertor | end - code: %d', code);
        reject();  // 에러 객체 없이 reject — handlingMessageErrors에서 error=undefined
    } else {
        logger.debug('PotreeService::runPotreeConvertor | end');
        resolve();
    }
});

reject()가 인자 없이 호출되어 handlingMessageErrors(potree-service.ts:502)에 undefined가 전달된다.

  1. 에러 핸들링 — undefined error로 SQS 메시지 즉시 삭제 (potree-service.ts:456-517):
packages/cupix-tesla-potree-agent/src/potree-service.ts:456-460typescript
private getApiErrorToDeleteMessage = (error: any): any => {
    if (error == undefined) {
        logger.warn('PotreeService::getApiErrorToDeleteMessage | undefined error');
        return 'undefined error';  // 문자열 반환 → truthy → 메시지 즉시 삭제
    }
packages/cupix-tesla-potree-agent/src/potree-service.ts:512-517typescript
const apiErrorObject = this.getApiErrorToDeleteMessage(error);
if (apiErrorObject != undefined || this.checkReceiveCountToDeleteMessage()) {
    try {
        errorAndMessage.error = apiErrorObject;
        await this.deleteByMessage(this.messageInProcess);
        if (this._modelInProcess != undefined)
            await this.updatePotreeState(this._modelInProcess.id, TESLA.PointcloudPotreeState.Error);

getApiErrorToDeleteMessage'undefined error' 문자열(truthy)을 반환하므로 apiErrorObject != undefined 조건이 참이 되어, ApproximateReceiveCount: 1(첫 시도)임에도 메시지가 즉시 삭제되고 potree_stateError로 전환된다.

  1. Docker 이미지 구성 — PotreeConverter 바이너리는 S3에서 pre-built 의존성을 다운로드하여 Docker 이미지에 포함:
packages/cupix-tesla-potree-agent/Dockerfile:31-33dockerfile
COPY --from=deps /tmp/lib/potree /tmp/lib/potree
RUN mkdir /tmp/PotreeConverter
RUN ln -s /tmp/lib/potree /tmp/PotreeConverter/build
dependencies/potree.json:14-16json
"production": {
    "stable": "0.0.173"
}

Log Evidence#

사용한 Datadog 쿼리:

text
service:cupixworks-any-potree-agent status:error @environment:production "PotreeService::runPotreeConvertor"
time: 2026-04-30T05:23:00Z to 2026-04-30T07:00:00Z
text
service:cupixworks-any-potree-agent (status:warn OR status:info) @environment:production
time: 2026-04-30T05:23:00Z to 2026-04-30T07:00:00Z
text
service:cupixworks-any-potree-agent status:error "code: 139"
time: 2026-04-24T00:00:00Z to 2026-05-01T00:00:00Z
=> 6건 (3개 인시던트 × error 2건씩), 모두 2026-04-30에 집중
=> 이전 14일간 0건

Incident 1 — pointcloud 108456 (eu-central-1, host: ip-10-1-47-203, team: innovo):

json
{"timestamp": "2026-04-30T06:22:56.743Z", "status": "info", "message": "PotreeService::runByMessage | id: 108456"}
json
{"timestamp": "2026-04-30T06:22:56.809Z", "status": "info", "message": "CupixAuth::setSession | session_id: 5e55e307418aaf453d02bd50510f782a02e817e7"}
json
{"timestamp": "2026-04-30T06:22:56.850Z", "status": "info", "message": "PotreeService::runByMessage | state: queued, resource_state: uploaded, potree_state: created"}
json
{"timestamp": "2026-04-30T06:22:58.519Z", "status": "info", "message": "PotreeService::runPotreeConvertor | exec - /tmp/PotreeConverter/build/PotreeConverter ./potree_converter_input.json"}
json
{
  "timestamp": "2026-04-30T06:23:00.415Z",
  "status": "error",
  "message": "PotreeService::runPotreeConvertor | Error: Command failed: /tmp/PotreeConverter/build/PotreeConverter ./potree_converter_input.json\nSegmentation fault (core dumped)\n\n    at genericNodeError (node:internal/errors:984:15)\n    at wrappedFn (node:internal/errors:538:14)\n    at ChildProcess.exithandler (node:child_process:422:12)\n    at ChildProcess.emit (node:events:536:35)\n    at maybeClose (node:internal/child_process:1104:16)\n    at ChildProcess._handle.onexit (node:internal/child_process:304:5)"
}
json
{"timestamp": "2026-04-30T06:23:00.416Z", "status": "warn", "message": "PotreeService::runPotreeConvertor | Segmentation fault (core dumped)"}
json
{"timestamp": "2026-04-30T06:23:00.416Z", "status": "error", "message": "PotreeService::runPotreeConvertor | end - code: 139"}
json
{"timestamp": "2026-04-30T06:23:00.417Z", "status": "warn", "message": "PotreeService::getApiErrorToDeleteMessage | undefined error"}
json
{
  "timestamp": "2026-04-30T06:23:01.054Z",
  "status": "error",
  "message": "PotreeService::handlingMessageErrors | Error and message object - {\"error\":\"undefined error\",\"sqsMessage\":{\"MessageId\":\"bd5110d8-88d7-4483-b280-7bdb92bde6e3\",\"Attributes\":{\"ApproximateReceiveCount\":\"1\"}}}"
}

exec부터 크래시까지 ~2초 — 입력 파싱 초기 단계에서 크래시.

Incident 2 — pointcloud 199896 (ap-southeast-2, host: ip-10-1-36-79, team: unispace):

json
{"timestamp": "2026-04-30T06:55:10.513Z", "status": "info", "message": "PotreeService::runByMessage | id: 199896"}
json
{"timestamp": "2026-04-30T06:55:10.610Z", "status": "info", "message": "PotreeService::runByMessage | state: queued, resource_state: uploaded, potree_state: created"}
json
{"timestamp": "2026-04-30T06:55:13.870Z", "status": "info", "message": "PotreeService::runPotreeConvertor | exec - /tmp/PotreeConverter/build/PotreeConverter ./potree_converter_input.json"}
json
{"timestamp": "2026-04-30T06:56:02.674Z", "status": "error", "message": "PotreeService::runPotreeConvertor | Error: Command failed... Segmentation fault (core dumped) {code: 139}"}
json
{"timestamp": "2026-04-30T06:56:02.675Z", "status": "error", "message": "PotreeService::runPotreeConvertor | end - code: 139"}
json
{"timestamp": "2026-04-30T06:56:03.509Z", "status": "error", "message": "PotreeService::handlingMessageErrors | Error and message object - {\"error\":\"undefined error\",\"sqsMessage\":{\"MessageId\":\"719e3de7-f6c2-41d3-938c-8fb283532f90\",\"Attributes\":{\"ApproximateReceiveCount\":\"1\"}}}"}

exec부터 크래시까지 ~49초 — 데이터 처리 도중 크래시.

Incident 3 — pointcloud 199894 (ap-southeast-2, 동일 호스트, team: unispace):

json
{"timestamp": "2026-04-30T06:56:04.112Z", "status": "info", "message": "PotreeService::runByMessage | id: 199894"}
json
{"timestamp": "2026-04-30T06:56:07.484Z", "status": "info", "message": "PotreeService::runPotreeConvertor | exec - /tmp/PotreeConverter/build/PotreeConverter ./potree_converter_input.json"}
json
{"timestamp": "2026-04-30T06:56:41.926Z", "status": "error", "message": "PotreeService::runPotreeConvertor | Error: Command failed... Segmentation fault (core dumped) {code: 139}"}
json
{"timestamp": "2026-04-30T06:56:41.929Z", "status": "error", "message": "PotreeService::runPotreeConvertor | end - code: 139"}
json
{"timestamp": "2026-04-30T06:56:42.746Z", "status": "error", "message": "PotreeService::handlingMessageErrors | Error and message object - {\"error\":\"undefined error\",\"sqsMessage\":{\"MessageId\":\"80ab536f-b538-44fb-960e-5cf1e9102c27\",\"Attributes\":{\"ApproximateReceiveCount\":\"1\"}}}"}

exec부터 크래시까지 ~34초 — 데이터 처리 도중 크래시.

핵심 관찰:

  • 3건 모두 killed: false, signal: null — OS OOM killer가 아닌 프로세스 자체 SIGSEGV
  • 3건 모두 ApproximateReceiveCount: 1 — 첫 번째 수신 시도에서 즉시 삭제됨 (재시도 없음)
  • 크래시 소요 시간 차이: 2초(108456), 49초(199896), 34초(199894) — 입력 파일 크기/특성에 따른 차이
  • ap-southeast-2의 2건은 동일 호스트(ip-10-1-36-79), 동일 사용자(marc.harmsen@unispace.com), 연속 pointcloud ID(199894, 199896) — 동일 캡처 세션에서 업로드된 데이터일 가능성
  • 동일 시간대 다수의 다른 pointcloud가 정상 완료: eu-central-1(108450, 108452, 108454, 108458 등), ap-southeast-2(199846~199892)
  • 이전 14일간 동일 에러 0건 — 2026-04-30에 처음 발생

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 특정 입력 파일의 데이터가 PotreeConverter C++ 바이너리의 메모리 안전하지 않은 코드 경로를 트리거하여 segfault 유발 두 리전의 서로 다른 호스트에서 발생 (인프라 공통 이슈 배제). 동일 시간대 다른 pointcloud 정상 처리. 크래시 소요 시간이 입력에 따라 2초~49초로 상이. 14일간 3건만 발생 (특정 입력에서만 트리거). killed: false, signal: null로 프로세스 자체 SIGSEGV 확인. 실제 입력 파일 내용/core dump 미수집으로 정확한 크래시 지점 확인 불가 Confirmed
H2 ECS 컨테이너 메모리 부족(OOM)으로 프로세스 kill exit code 139가 메모리 관련으로 오인될 수 있음 OOM kill은 SIGKILL(exit code 137=128+9), 139는 SIGSEGV(128+11). stderr에 "Segmentation fault (core dumped)" 명시. killed: false로 외부 kill 아님. Datadog OOM 관련 로그 0건. 에러 직후 동일 에이전트에서 다음 pointcloud(199894) 즉시 처리 시작 Rejected
H3 PotreeConverter 바이너리 빌드/아키텍처 호환성 문제 또는 공유 라이브러리 불일치 Docker 이미지에 apt-get install g++ libboost-all-dev libtbb-dev 등 런타임 의존성 설치, pre-built 바이너리와 불일치 가능 동일 Docker 이미지/바이너리에서 다른 파일 정상 변환. 여러 리전(us-west-2, eu-central-1, ap-southeast-2) 모두 동일 이미지 사용. 정상 처리 건수가 실패 건수보다 압도적으로 많음 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • reject() 호출에 에러 정보 전달 (potree-service.ts:312): reject()reject(new Error(\PotreeConverter exited with code ${code}`))로 변경하여 handlingMessageErrors에서 의미 있는 에러 정보를 전달. 현재 undefined`가 전달되어 에러 컨텍스트가 완전히 유실됨.
  • getApiErrorToDeleteMessage의 undefined 처리 변경 (potree-service.ts:458-459): error == undefined 시 문자열 'undefined error'를 반환하는 대신 undefined를 반환하도록 변경. 이렇게 하면 checkReceiveCountToDeleteMessageMaxReceiveCount(10) 기반 재시도가 작동한다.
  • 실패한 pointcloud 재처리: pointcloud 108456, 199894, 199896의 potree_state를 확인하고, 필요시 created로 리셋하여 재처리 트리거.

단기 개선 (1주 이내)#

  • close 이벤트에서 signal 파라미터도 캡처하여 로그에 포함 (potree-service.ts:309): on('close', (code, signal) => {...}).
  • segfault 발생 시 실패한 pointcloud ID, 원본 파일 크기, 입력 JSON 내용을 명시적으로 로그에 포함하여 디버깅 용이성 확보.
  • PotreeConverter 실행 전 입력 파일 유효성 사전 검증 추가: 파일 존재 여부, 파일 크기 > 0 확인.

장기 개선 (재발 방지)#

  • PotreeConverter C++ 바이너리에 대해 ASAN(AddressSanitizer) 빌드를 QA 환경에 배포하여 segfault의 정확한 위치 파악.
  • 변환 실패 시 자동 재큐잉(exponential backoff 포함) 메커니즘 도입 — 일시적 문제와 영구적 문제를 exit code 기반으로 구분하여 처리.
  • core dump를 자동 수집하여 S3에 업로드하는 메커니즘 추가. 향후 C++ 바이너리 크래시 디버깅에 필수.
  • child_process.execchild_process.spawn 전환 검토: exec은 stdout/stderr를 버퍼에 저장하므로 대용량 출력에 취약. spawn으로 스트림 기반 처리 가능.

Monitoring#

  • SIGSEGV 발생 알림:
text
service:cupixworks-any-potree-agent status:error "code: 139"
  • PotreeConverter 비정상 종료 추적:
text
service:cupixworks-any-potree-agent "end - code:" NOT "end - code: 0"
  • 에러 핸들링으로 인한 SQS 메시지 삭제 모니터링:
text
service:cupixworks-any-potree-agent "handlingMessageErrors" "undefined error"

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: standard
  • 14일간 3건만 발생한 간헐적 패턴. 그러나 reject() 시 에러 컨텍스트 유실과 SQS 메시지 즉시 삭제로 인해 재시도 기회가 차단되는 구조적 결함이 존재하여, 동일 패턴 재발 시 영향이 누적될 수 있다. Node.js 래퍼의 에러 핸들링 개선은 코드 변경 난이도가 낮으나, C++ 바이너리 자체의 segfault 근본 원인 파악에는 core dump 분석이 필요하다.