scenemapperutils native addon crashes on specific input — SIGABRT
RCA: Process killed by signal SIGABRT (code: null)
Overview#
What Happened#
2026-06-25 15:22:20 KST에 cupixworks-capture-preprocessor-agent (ap-southeast-2, production)의 forked child process가 SIGABRT 신호로 종료되어 capture 78526의 preprocessor 작업이 실패했다. parent 프로세스의 ChildProcessManager가 close 이벤트를 받아 Error("Process killed by signal SIGABRT (code: null)") 로 pending Promise 들을 reject 했고, 최종적으로 BaseService::handlingMessageErrors가 "undefined response" 로 분류하여 SQS 메시지를 삭제 처리했다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | Error |
| exception.message | Process killed by signal SIGABRT (code: null) |
| top_frame | /tmp/agent/dist/app.cjs:1518:33 (bundled ChildProcessManager.setupEventHandlers close handler) |
| signal / code | SIGABRT / null (no exit code → killed by signal) |
| runtime | Node.js child process via child_process.fork() |
| env | production, region ap-southeast-2, tenant cupix |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| scs-assetfuture (capture preprocessing) | 1건 (이 클러스터) | capture 78526 preprocessing 실패, SQS 메시지 1건 삭제, capture 모델 error state 로 전환 |
동일 SIGABRT 패턴이 지난 7일 동안 9회 반복 관측되어 만성적 결함의 가능성이 있다 (아래 Log Evidence 참조).
Timeline#
- 2026-06-25 15:22:15 KST —
PreprocessorService::runPreprocessor | photo count: 0, video count: 1, app AR file path: /tmp/workspace/78526/app/processing_options.json— capture 78526에 대해 native preprocessor 호출 시작 - 2026-06-25 15:22:20 KST —
ChildProcessManager::setupEventHandlers | Child process exited(signal: SIGABRT, code: null) - 2026-06-25 15:22:20 KST —
ChildProcessManager::setupEventHandlers | Child process closed→ pending Promise reject - 2026-06-25 15:22:20 KST —
Process killed by signal SIGABRT (code: null)(status error) 및CupixAuth::handleError | Undefined response: ...stack... - 2026-06-25 15:22:21 KST —
BaseService::handlingMessageErrors | Error and message object - {"error":"undefined response","sqsMessage":{"MessageId":"456c5187-..."}} - 2026-06-25 15:22:21 KST —
BaseService::cleanUpAnythingRelatedModel | path: /tmp/workspace/78526— workspace 정리
Error Log#
Process killed by signal SIGABRT (code: null)
Stack (from CupixAuth::handleError | Undefined response):
Error: Process killed by signal SIGABRT (code: null)
at ChildProcess.<anonymous> (/tmp/agent/dist/app.cjs:1518:33)
at ChildProcess.emit (node:events:524:28)
at ChildProcess.emit (node:domain:489:12)
at maybeClose (node:internal/child_process:1104:16)
at ChildProcess._handle.onexit (node:internal/child_process:304:5)
at Process.callbackTrampoline (node:internal/async_hooks:130:17)
Impact#
- Service:
cupixworks-capture-preprocessor-agent - Team: scs-assetfuture
- 발생 횟수: 1 (클러스터 집계), 단 동일 패턴 7일간 9회 발생
- 최초 발생: 2026-06-25 15:22:20 KST
- 최근 발생: 2026-06-25 15:22:20 KST
Root Cause Summary#
ChildProcessManager가 child_process.fork()로 띄운 자식 Node.js 프로세스가 외부에서 보내지 않은 SIGABRT (signal 6) 으로 종료되었다. 자식 프로세스는 PreprocessorProcess로, require(this.libPath)를 통해 native scenemapperutils C++ 모듈(SCENEMAPPERUTILS_API_PATH)을 로드하고 Preprocessor.process() 같은 native 메서드를 호출한다. JavaScript 코드 경로에서는 자식에게 SIGABRT를 보내지 않으며 (stop() 은 SIGTERM/SIGKILL 사용, sendMessage 타임아웃 시에도 SIGKILL) — SIGABRT는 native binding 내부에서 abort() 호출, 처리되지 않은 C++ 예외, 또는 assertion 실패가 발생했다는 신호이다. parent의 close 이벤트 핸들러가 Error("Process killed by signal SIGABRT (code: null)")로 변환해 reject한 결과가 우리가 보는 에러 로그이며, 이는 root cause 의 후속 효과(surface)일 뿐 root cause 자체는 native 모듈의 abort 이다.
Technical Analysis#
Code Path#
Entry point: capture preprocessing SQS message → BaseService::runByMessage → PreprocessorService pipeline.
await this.loadVideos(cpCapture);
// ...
await this.downloadBimFloorplanFile(cpCapture);
// ...
await this.preprocessorManager.initialize();
// ...
this.setVideoImageMatchData(cpCapture);
// ...
await this.checkSupportedVideo(cpCapture);
// ...
const results = await this.runPreprocessor(cpCapture);
PreprocessorManager는 native 호출을 ChildProcessManager를 통해 별도 forked 프로세스에 위임한다:
constructor() {
this.childProcessManager = new ChildProcessManager(__dirname, 'process/preprocessor.process');
}
자식 프로세스에서 native 모듈이 로드되고 호출된다:
// eslint-disable-next-line @typescript-eslint/no-require-imports
const PreprocessorApi = require(this.libPath);
const Preprocessor = new PreprocessorApi.scenemapperutils();
// ...
videoInputs.forEach(it => {
Preprocessor.append_timelapse_video_filepath(it.inputFile, it.name, it.outputDir);
});
// ...
await CPUtils.sleep(100);
Preprocessor.process(); // ← native 호출. SIGABRT 발생 시점 추정
await CPUtils.sleep(100);
Failure point: parent의 close 이벤트 핸들러가 SIGABRT 신호를 Error 메시지로 변환한다.
this.process.on('close', (code: number | null, signal: string | null) => {
logger.error('ChildProcessManager::setupEventHandlers | Child process closed', {
code,
signal
});
const errorMsg = signal
? `Process killed by signal ${signal} (code: ${code})`
: `Process exited with code ${code}`;
this.rejectAllPending(new Error(errorMsg));
this.process = undefined;
this.emit('close', code, signal);
});
이 Error는 native binding 의 결함을 직접 가리키지 않고 신호만 표면화한다. parent JavaScript 코드에는 SIGABRT 를 보내는 경로가 없다 — stop() 은 SIGTERM/SIGKILL, sendMessage timeout 도 SIGKILL 만 사용:
if (timeoutMs && timeoutMs > 0) {
pending.timer = setTimeout(() => {
this.pendingMessages.delete(message.id);
const error = new Error(`Execution timeout after ${timeoutMs}ms for message type: ${message.type}`);
logger.error(`ChildProcessManager::sendMessage | ${error.message}`);
if (this.process && !this.process.killed) {
logger.warn('ChildProcessManager::sendMessage | Killing child process due to timeout');
this.process.kill('SIGKILL');
}
reject(error);
}, timeoutMs);
}
따라서 SIGABRT 는 외부에서 들어왔거나 (예: OOM-killer 는 보통 SIGKILL 사용, 따라서 가능성 낮음) native 모듈 내부의 abort() / unhandled C++ exception / assertion 실패에서 발생한 것이다.
reject 된 에러는 BaseService.handlingMessageErrors → getApiErrorToDeleteMessage 로 흘러가, HTTP 응답 형태가 아니므로 'undefined response' 문자열로 분류된 뒤 SQS 메시지가 삭제된다:
private getApiErrorToDeleteMessage = (error: any): any => {
if (error == undefined) {
logger.warn('BaseService::getApiErrorToDeleteMessage | undefined error');
return 'undefined error';
}
if (error.errno != undefined && error.code != undefined && error.syscall != undefined) {
logger.warn('BaseService::getApiErrorToDeleteMessage | nodejs common system error', error);
return;
}
const response = CPUtils.isJsonString(error) ? JSON.parse(error) : error.response;
if (response == undefined) {
logger.warn('BaseService::getApiErrorToDeleteMessage | undefined response', error);
return 'undefined response';
}
기대 동작 vs 실제 동작:
- 기대: native preprocessor가 정상 처리 결과를 IPC 메시지로 반환하면
Preprocessor.process()가 성공하고 결과 panos/videos 가 parent 에 전달된다. - 실제: native preprocessor 가
SIGABRT로 즉시 종료되어 IPC 가 끊기고, parent 는 신호 이름만 보고 generic Error 로 변환한다. 실제 abort 의 원인 (스택, dmesg, core dump) 은 로그에 남지 않는다.
Log Evidence#
Datadog query 1 — SIGABRT 발생 시점 7일 추이:
service:cupixworks-capture-preprocessor-agent "SIGABRT"
지난 7일간 동일 패턴 9건 (각 발생마다 3로그: warn handleError + warn raw + error raw):
2026-06-25 15:22:20 Process killed by signal SIGABRT (code: null)
2026-06-23 12:59:15 Process killed by signal SIGABRT (code: null)
2026-06-22 20:24:57 Process killed by signal SIGABRT (code: null)
2026-06-22 08:09:18 Process killed by signal SIGABRT (code: null)
2026-06-19 11:24:02 Process killed by signal SIGABRT (code: null)
2026-06-19 11:17:23 Process killed by signal SIGABRT (code: null)
2026-06-18 23:58:38 Process killed by signal SIGABRT (code: null)
2026-06-18 22:21:12 Process killed by signal SIGABRT (code: null)
2026-06-18 18:06:25 Process killed by signal SIGABRT (code: null)
Datadog query 2 — 2026-06-25 발생 시점 ±10분 context:
service:cupixworks-capture-preprocessor-agent
핵심 시퀀스 (capture 78526 / job 직전, ap-southeast-2 production):
2026-06-25 15:22:15 info PreprocessorService::runPreprocessor | photo count: 0, video count: 1, app AR file path: /tmp/workspace/78526/app/processing_options.json
2026-06-25 15:22:20 error ChildProcessManager::setupEventHandlers | Child process exited
2026-06-25 15:22:20 error ChildProcessManager::setupEventHandlers | Child process closed
2026-06-25 15:22:20 error Process killed by signal SIGABRT (code: null)
2026-06-25 15:22:20 warn Process killed by signal SIGABRT (code: null)
2026-06-25 15:22:20 warn CupixAuth::handleError | Undefined response: {"stack":"Error: Process killed by signal SIGABRT (code: null)\n at ChildProcess.<anonymous> (/tmp/agent/dist/app.cjs:1518:33)...","message":"Process killed by signal SIGABRT (code: null)"}
2026-06-25 15:22:21 info BaseService::cleanUpAnythingRelatedModel | path: /tmp/workspace/78526
2026-06-25 15:22:21 error BaseService::handlingMessageErrors | Error and message object - {"error":"undefined response","sqsMessage":{"MessageId":"456c5187-d11f-47f3-8023-115f000edef4","Attributes":{"ApproximateReceiveCount":"1"}}}
ApproximateReceiveCount: 1 은 첫 시도였다는 뜻이며, getApiErrorToDeleteMessage 가 'undefined response' 를 반환했기 때문에 retry 없이 즉시 SQS 메시지가 삭제되었다 (base-service.ts:300-304).
native 모듈의 stdout/stderr 가 ChildProcessManager에서 debug 레벨로만 로깅되기 때문에 (child-process.manager.ts:138-147) Datadog 에는 abort 직전의 native 메시지가 남아 있지 않다 — Cupix Watch (Kibana) 또는 인스턴스 상의 stderr/core dump 가 있어야 정확한 native 원인 파악 가능. 현 시점에서 native abort 의 정확한 원인은 uncertain -- needs verification.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | Native scenemapperutils 모듈에서 abort() / unhandled C++ exception / assertion 실패 발생 |
child_process.fork()로 띄운 Node 프로세스가 외부 SIGABRT 없이 SIGABRT 로 종료 (child-process.manager.ts:107). SIGABRT 는 signal 6, 일반적으로 native abort() 또는 C++ runtime이 발생. parent JS 코드에는 SIGABRT 송신 경로 없음 (stop()/timeout 모두 SIGTERM/SIGKILL — child-process.manager.ts:213-226, 296-329). 자식 프로세스는 require(this.libPath) 로 native 바인딩 로드 (preprocessor.process.ts:263) |
정확한 native 스택/메시지가 Datadog에 없음 (debug 레벨로만 로깅되므로) | Confirmed (mechanism), Inconclusive (specific cause) |
| H2 | Parent JavaScript 코드의 timeout 처리가 child 에 SIGABRT 송신 | — | parent 코드에서 SIGABRT 송신 경로 없음. kill('SIGKILL') (child-process.manager.ts:221, 305), kill('SIGTERM') (child-process.manager.ts:322, 327) 만 사용 |
Rejected |
| H3 | Linux OOM-killer 가 child 프로세스를 SIGABRT 로 죽임 | preprocessor 는 비디오/이미지 등 대용량 메모리 사용 | OOM-killer 는 보통 SIGKILL 을 사용. 또한 SIGABRT 의 경우 process 가 graceful 한 abort 경로를 거치므로 OOM 시그너처와 다름 | Rejected |
| H4 | Datadog 가 표시하는 에러가 1회성 transient 이슈 | 클러스터 집계 1건 | 같은 fingerprint 패턴이 지난 7일간 9회 반복 (6/18, 6/19 x2, 6/22 x2, 6/23, 6/25) — 만성적 결함 | Rejected |
| H5 | 손상된 video 파일 등 특정 입력이 native 모듈을 abort 시킴 | 입력별 (capture 별) 산발적 발생, 매번 다른 capture id (78526 등) | 같은 capture가 반복적으로 abort 시키는지는 미확인. 7일 9회는 cluster-wide 가 아니라 특정 입력 의존 가능성 시사 | Inconclusive — needs verification (capture id 와 video 파일 특성 cross-check 필요) |
Fix Recommendation#
즉시 조치 (Critical)#
- Native abort 원인 파악을 위한 진단 데이터 확보.
packages/base/src/manager/child-process.manager.ts:107-120의close핸들러에서 신호가SIGABRT일 때 child stderr 버퍼를 함께 로깅하도록 강화하는 방향 검토. 현재 stderr 는error레벨로 출력되긴 하지만 (child-process.manager.ts:138-141) abort 직전 출력이 flush 되지 않으면 누락되므로, ChildProcessManager 에서 stderr 누적 버퍼를 보존했다가 SIGABRT 시 함께 dump 하는 패턴이 필요. 구현은 후속 작업에서 결정. - Cupix Watch (Kibana) 의 debug/silly 로그 확인. Datadog 에는 abort 직전의 native stdout/stderr 가 없으므로 (디버그 레벨로만 기록), Watch 측 인덱스에서 capture 78526 시간대 native 출력을 조사해 정확한 abort 원인을 식별한다. 발견 즉시 scenemapperutils 팀에 이슈 전달.
- (uncertain) capture 78526 의 입력 video 파일 검증. 손상되었거나 비표준 메타데이터를 가진 비디오일 경우 native 모듈이 assertion 으로 abort 할 수 있다. EC2 / S3 의 원본 video 와
/tmp/workspace/78526산출물 (가능하다면) 을 보존해 재현 시도. 본 RCA 시점에는 워크스페이스가 이미 cleanup 됨 (BaseService::cleanUpAnythingRelatedModel15:22:21) — 다음 발생 시 자동 보존 hook 이 필요.
단기 개선 (1주 이내)#
- SIGABRT 케이스에서 SQS 메시지를 즉시 삭제하지 말 것.
packages/base/src/base-service.ts:240-253의getApiErrorToDeleteMessage는error.response가 없으면'undefined response'를 반환해 메시지를 즉시 delete 시키는데 (line 300-304), native abort 는 transient 일 수 있으므로 (특정 worker 인스턴스 / 메모리 상태에 의존)ApproximateReceiveCount가MaxReceiveCount미만이면 retry 하도록 분기 추가. 단, 같은 입력이 반복 abort 시키면 무한 retry 가 되므로 receive count 기반 cutoff 는 유지. - Native crash 발생 시 capture 단위 분류 메트릭 추가. capture_id, region, video count 등을 태그로 붙여 SIGABRT 발생 분포를 파악 → 특정 입력 패턴 (예: 특정 카메라/펌웨어/길이) 에서 빈발하는지 확인.
장기 개선 (재발 방지)#
- Native 모듈 자체의 오류 처리 강화. scenemapperutils 측에서
abort()또는 unhandled C++ exception 으로 빠지기 전에 invalid input / OOM / assertion 을 IPC 메시지로 보고하도록 변경. 이를 위해 native 측에 try/catch 를 추가하고 N-API 콜백 경계에서 예외를 변환해 JS Error 로 throw 하도록 한다. - Child process recycling / sandbox 강화. 한 워커가 N 개 job 처리 후 child process 를 재시작하면 메모리 누수성 abort 를 차단할 수 있다.
- Core dump 수집 파이프라인. EC2 인스턴스에서 SIGABRT 시 core dump 를 S3 로 자동 업로드하면 native 측 디버깅이 용이.
Monitoring#
- Datadog timeseries — SIGABRT 발생률 추이:
sum:logs.hits{service:cupixworks-capture-preprocessor-agent,status:error,@message:"Process killed by signal SIGABRT*"}.as_count()
- Datadog timeseries — preprocessor 전체 에러율:
sum:logs.hits{service:cupixworks-capture-preprocessor-agent,status:error}.as_count()
- Datadog timeseries — ChildProcessManager close 이벤트:
sum:logs.hits{service:cupixworks-capture-preprocessor-agent,@message:"ChildProcessManager::setupEventHandlers | Child process closed"}.as_count()
- 알림: 위 SIGABRT 카운트 쿼리 기반 monitor 를 1시간 윈도우 threshold 3 이상으로 설정 (현재 7일 9건 ≈ 평균 1.3/day, 시간당 3 이상이면 비정상 burst 의심).
Risk Assessment#
- Risk level: medium
- 발생률이 낮고 (7일 9건) capture 단위로 격리되어 다른 정상 capture 의 처리에는 영향이 없다.
- 그러나
getApiErrorToDeleteMessage가 즉시 SQS 메시지를 삭제하므로 영향받은 capture 는 retry 없이 곧바로 error state 가 되어 사용자 입장에서는 preprocessing 영구 실패로 보인다. - native abort 의 정확한 원인이 식별되지 않아 동일 입력 / 동일 모듈 버전에서 재발 가능성이 지속된다.
- 예상 복잡도: standard
- 진단 데이터 확보 (stderr 버퍼링, Watch 로그 분석) 와 SQS retry 정책 분기 추가는 표준 작업. 다만 root cause 확정 후 native scenemapperutils 측 수정은 별도 팀 작업이 될 수 있어 cross-team coordination 필요.