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#
- 2026-04-30 06:22:56Z — pointcloud 108456
runByMessage시작 (eu-central-1, team: innovo) - 2026-04-30 06:22:58Z — PotreeConverter exec 시작
- 2026-04-30 06:23:00Z — 첫 번째 SIGSEGV (exit code 139, ~2초 후), SQS message
bd5110d8삭제 - 2026-04-30 06:55:10Z — pointcloud 199896
runByMessage시작 (ap-southeast-2, team: unispace) - 2026-04-30 06:55:13Z — PotreeConverter exec 시작
- 2026-04-30 06:56:02Z — 두 번째 SIGSEGV (pointcloud 199896, ~49초 후), SQS message
719e3de7삭제 - 2026-04-30 06:56:04Z — pointcloud 199894
runByMessage시작 (동일 호스트 ip-10-1-36-79) - 2026-04-30 06:56:07Z — PotreeConverter exec 시작
- 2026-04-30 06:56:41Z — 세 번째 SIGSEGV (pointcloud 199894, ~34초 후), SQS message
80ab536f삭제
Error Log#
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_state를Error로 전환하므로 자동 재시도 불가. 해당 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#
- Entry point — SQS 메시지 수신 및 처리 시작 (
potree-service.ts:180):
runByMessage가 SQS 메시지에서 pointcloud ID를 추출하고, 인증 → 메타데이터 로드 → 파일 다운로드 → PotreeConverter 실행 순으로 파이프라인을 실행한다.
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
- PotreeConverter 바이너리 실행 (
potree-service.ts:258-318):
입력 파일 경로와 옵션을 JSON으로 작성하여 child_process.exec로 바이너리를 실행한다. 바이너리 경로는 /tmp/PotreeConverter/build/PotreeConverter(constants.ts:9).
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);
}
});
- Failure point — exit code 139 감지 후 인자 없는
reject()호출 (potree-service.ts:309-317):
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가 전달된다.
- 에러 핸들링 — undefined error로 SQS 메시지 즉시 삭제 (
potree-service.ts:456-517):
private getApiErrorToDeleteMessage = (error: any): any => {
if (error == undefined) {
logger.warn('PotreeService::getApiErrorToDeleteMessage | undefined error');
return 'undefined error'; // 문자열 반환 → truthy → 메시지 즉시 삭제
}
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_state가 Error로 전환된다.
- Docker 이미지 구성 — PotreeConverter 바이너리는 S3에서 pre-built 의존성을 다운로드하여 Docker 이미지에 포함:
COPY --from=deps /tmp/lib/potree /tmp/lib/potree
RUN mkdir /tmp/PotreeConverter
RUN ln -s /tmp/lib/potree /tmp/PotreeConverter/build
"production": {
"stable": "0.0.173"
}
Log Evidence#
사용한 Datadog 쿼리:
service:cupixworks-any-potree-agent status:error @environment:production "PotreeService::runPotreeConvertor"
time: 2026-04-30T05:23:00Z to 2026-04-30T07:00:00Z
service:cupixworks-any-potree-agent (status:warn OR status:info) @environment:production
time: 2026-04-30T05:23:00Z to 2026-04-30T07:00:00Z
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):
{"timestamp": "2026-04-30T06:22:56.743Z", "status": "info", "message": "PotreeService::runByMessage | id: 108456"}
{"timestamp": "2026-04-30T06:22:56.809Z", "status": "info", "message": "CupixAuth::setSession | session_id: 5e55e307418aaf453d02bd50510f782a02e817e7"}
{"timestamp": "2026-04-30T06:22:56.850Z", "status": "info", "message": "PotreeService::runByMessage | state: queued, resource_state: uploaded, potree_state: created"}
{"timestamp": "2026-04-30T06:22:58.519Z", "status": "info", "message": "PotreeService::runPotreeConvertor | exec - /tmp/PotreeConverter/build/PotreeConverter ./potree_converter_input.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)"
}
{"timestamp": "2026-04-30T06:23:00.416Z", "status": "warn", "message": "PotreeService::runPotreeConvertor | Segmentation fault (core dumped)"}
{"timestamp": "2026-04-30T06:23:00.416Z", "status": "error", "message": "PotreeService::runPotreeConvertor | end - code: 139"}
{"timestamp": "2026-04-30T06:23:00.417Z", "status": "warn", "message": "PotreeService::getApiErrorToDeleteMessage | undefined error"}
{
"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):
{"timestamp": "2026-04-30T06:55:10.513Z", "status": "info", "message": "PotreeService::runByMessage | id: 199896"}
{"timestamp": "2026-04-30T06:55:10.610Z", "status": "info", "message": "PotreeService::runByMessage | state: queued, resource_state: uploaded, potree_state: created"}
{"timestamp": "2026-04-30T06:55:13.870Z", "status": "info", "message": "PotreeService::runPotreeConvertor | exec - /tmp/PotreeConverter/build/PotreeConverter ./potree_converter_input.json"}
{"timestamp": "2026-04-30T06:56:02.674Z", "status": "error", "message": "PotreeService::runPotreeConvertor | Error: Command failed... Segmentation fault (core dumped) {code: 139}"}
{"timestamp": "2026-04-30T06:56:02.675Z", "status": "error", "message": "PotreeService::runPotreeConvertor | end - code: 139"}
{"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):
{"timestamp": "2026-04-30T06:56:04.112Z", "status": "info", "message": "PotreeService::runByMessage | id: 199894"}
{"timestamp": "2026-04-30T06:56:07.484Z", "status": "info", "message": "PotreeService::runPotreeConvertor | exec - /tmp/PotreeConverter/build/PotreeConverter ./potree_converter_input.json"}
{"timestamp": "2026-04-30T06:56:41.926Z", "status": "error", "message": "PotreeService::runPotreeConvertor | Error: Command failed... Segmentation fault (core dumped) {code: 139}"}
{"timestamp": "2026-04-30T06:56:41.929Z", "status": "error", "message": "PotreeService::runPotreeConvertor | end - code: 139"}
{"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를 반환하도록 변경. 이렇게 하면checkReceiveCountToDeleteMessage의MaxReceiveCount(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.exec→child_process.spawn전환 검토:exec은 stdout/stderr를 버퍼에 저장하므로 대용량 출력에 취약.spawn으로 스트림 기반 처리 가능.
Monitoring#
- SIGSEGV 발생 알림:
service:cupixworks-any-potree-agent status:error "code: 139"
- PotreeConverter 비정상 종료 추적:
service:cupixworks-any-potree-agent "end - code:" NOT "end - code: 0"
- 에러 핸들링으로 인한 SQS 메시지 삭제 모니터링:
service:cupixworks-any-potree-agent "handlingMessageErrors" "undefined error"
Risk Assessment#
- Risk level: low
- 예상 복잡도: standard
- 14일간 3건만 발생한 간헐적 패턴. 그러나
reject()시 에러 컨텍스트 유실과 SQS 메시지 즉시 삭제로 인해 재시도 기회가 차단되는 구조적 결함이 존재하여, 동일 패턴 재발 시 영향이 누적될 수 있다. Node.js 래퍼의 에러 핸들링 개선은 코드 변경 난이도가 낮으나, C++ 바이너리 자체의 segfault 근본 원인 파악에는 core dump 분석이 필요하다.