장애 분석 · 2026-09-17 ~ 2026-09-26
gcloud IAP tunnel은 왜 자기 설정 DB에 TLS 메시지를 써 넣었나
운영 중인 Spring Boot 앱이 간헐적으로 PostgreSQL에 연결하지 못했다. 원인은 DB도 네트워크도 아니었다. DB 앞단의 gcloud compute start-iap-tunnel이 이미 닫은 파일 디스크립터(fd) 번호로 계속 읽고 쓰다가, 그 번호를 새로 받은 자신의 SQLite 설정 파일에 TLS alert 24 bytes를 써 넣고 있었다. 이 페이지는 증상에서 syscall 한 줄까지 범위를 좁혀 간 과정과 그 증거를 담았다.
한눈에 보기
증상
Spring이 ConnectionFactoryImpl.enableSSL()에서 Read timed out. 이후 새 DB 연결이 전부 실패. 그런데 gcloud process와 :15432 listener는 살아 있었다.
근본 원인
gcloud IAP tunnel의 fd use-after-close race. 연결 종료 thread가 SSL socket을 닫는 동안 수신 thread는 OpenSSL 안에서 같은 원시 fd 번호로 계속 읽고 쓴다.
트리거
클라이언트(Hikari pool)가 연결을 닫고 곧바로 새 연결을 연다. 새 연결이 default_configs.db를 열 때 방금 반납된 fd 번호를 받는다.
영향
default_configs.db offset 29에 17 03 03 00 13 + 19 bytes가 들어가 손상. 이후 모든 새 tunnel 연결과 같은 설정을 쓰는 모든 gcloud 명령이 database disk image is malformed로 실패.
배제된 것
PostgreSQL 서버, GCP VM, IAP 서비스, 네트워크 경로, system OpenSSL 혼용, proxy/LD 환경변수. gcloud 583~586 릴리스 노트에 관련 수정 없음.
아직 추정인 것 1건
같은 race가 DB 파일이 아니라 다른 연결의 socket에 걸리는 변형. 초기의 간헐적 Read timed out을 설명할 수 있지만 syscall로 잡지는 못했다.
1421083이 fd 7로 read(29)를 해서 SQLite 헤더를 읽은 뒤, 같은 fd 7로 write(24)를 했다. 그 24 bytes(17 03 03 00 13 aa 23 cb …)가 손상된 DB의 offset 29 내용과 byte 단위로 일치한다. fd 7은 140ms 전에 다른 thread가 닫은 WebSocket socket 번호였다. → 확증 타임라인gcloud compute start-iap-tunnel (SDK 582.0.0, lightweight WebSocket path) intermittently broke all new tunnel connections while the process and its listener stayed up. We traced it to an fd use-after-close race: the per-connection handler thread closes the WebSocket's SSL socket without synchronizing with the receive thread, which is still inside OpenSSL's SSL_read holding the raw integer fd from SSL_set_fd. When a new connection opens default_configs.db and receives the same fd number, the stale reader reads 29 bytes of the SQLite header, fails to decrypt them, and writes a 24-byte TLS 1.3 alert record (17 03 03 00 13…) into the database at offset 29. bpftrace captured the full close(7) → openat(db)=7 → read(7,29) → write(7,24) chain in production, and the written bytes match the corrupted file exactly.조사 여정: 범위를 좁혀 간 16단계
각 단계는 가설 → 한 일 → 관측 → 결론 순서로 적었다. 오른쪽 막대는 그 시점에 "gcloud 내부 fd race"라는 최종 결론에 대해 가졌던 확신 정도다.
가설 트리
노드를 누르면 판정 근거가 나온다. 표시: ✕ 기각 △ 약화·보류 ◎ 지지 ✔ 확정 ? 추정
확증: 2026-09-26 19:31:20의 syscall 타임라인
production gcloud process에 붙여 둔 bpftrace가 기록한 이벤트 중, 관련된 thread 3개와 fd 7만 추렸다. 기준점 t0는 연결 [93]의 처리 thread가 WebSocket socket(fd 7)을 close()한 순간이다. 위쪽 띠는 그 시점에 fd 7 번호가 무엇을 가리켰는지를 보여 준다.
무엇이 일어났나
- 1421083(연결 [93]의 수신 thread)은 95초 전부터 fd 7에서 다음 TLS record를 기다리며
read()안에 잠들어 있었다. - 1421118(연결 [93]의 처리 thread)이 local EOF를 받고 WebSocket Close를 보낸 뒤 fd 7을 닫았다.
- 4.9ms 뒤 1431669(새 연결 [94])가 설정 파일들을 열었고, 커널은 가장 낮은 빈 번호인 7을 줬다.
- 140ms 뒤 서버 frame이 도착해 1421083이 깨어났다. 잠들어 있던
read()는 옛 socket에서 record header를 받았고, OpenSSL은 본문 29 bytes를 더 읽으려고read(7, 29)를 다시 불렀다. 이때 7은 이미 DB 파일이었다. - SQLite 헤더를 복호화할 수 없으니 OpenSSL은 fatal alert를 보냈다.
write(7, 24)가 파일 위치 29, 즉 방금 29 bytes를 읽은 바로 그 자리에 떨어졌다.
왜 이것이 확증인가
- DB에 쓴 주체와 syscall, 파일 위치, bytes가 모두 기록에 있다. 이제 "구조가 닮았다"는 추론이 아니다.
- trace의 write payload와 손상 파일의 offset 29 bytes가 일치한다. Hex 뷰어에서 나란히 비교할 수 있다.
- offset 29라는 이상한 위치가 직전 read 29 bytes로 정확히 설명된다. 4번의 손상이 모두 offset 29였다.
- 쓴 thread의 이름은
Thread-275 (_ReceiveFromWebSocket)이고, user stack은__write ← 정적 OpenSSL ← _ssl ← Python이다.
Hex 뷰어: 손상된 default_configs.db
관측한 손상 파일 4개의 앞 96 bytes다. 노란 칸이 덮어써진 24 bytes(offset 29~52)이고, 빨간 테두리는 TLS record header 5 bytes다. 칸에 마우스를 올리거나 탭하면 그 위치의 SQLite 헤더 필드가 나온다.
덮어쓴 24 bytes의 구조 (TLS 1.3 record)
trace에 기록된 write payload (19:31:20)
손상 파일 offset 29~44 (같은 날짜 파일)
0x17)로 나간다. 본문은 alert 2 bytes, 실제 content type 1 byte, AEAD 인증 태그 16 bytes를 합쳐 19 bytes(0x13)다. 연결마다 키가 다르므로 header 5 bytes는 매번 같고 뒤 19 bytes는 매번 다르다. 4개 파일이 정확히 그랬다.00 00 00 03(3 pages)이어야 할 값이 00 17 03 03, 즉 1,508,099 pages가 됐다. 이어서 freelist 정보, schema cookie, schema format(offset 32~52)까지 덮였다.코드 경로: 동기화 없는 두 thread
gcloud는 tunnel로 들어오는 TCP 연결 하나마다 thread 두 개를 쓴다. 둘은 같은 SSL socket을 공유하지만, 닫는 쪽은 읽는 쪽이 끝나기를 기다리지 않는다.
conn.recv()가 빈 값 → local EOFfinally:conn.close()(trace: shutdown(6), close(6))IapTunnelWebSocket.Close()→helper.SendClose()(write(7))helper.Close()→IapLightWeightWebsocket.close()if self.connected and self.sock:→self.sock.close()
fd 7 반납. 수신 thread를 join하지 않고 lock도 없다
run_forever()루프:while self.connected:self.recv()→recv_frame()→_recv_bytes()self.sock.recv_into()(PythonSSLSocket)_sslC 모듈 → OpenSSLSSL_read()
GIL을 놓고 실행. fd는SSL_set_fd때 받은 정수 7- OpenSSL 내부에서
read(7)/ 실패 시 alertwrite(7)
Python의 fd=-1 보호를 거치지 않는다
소스 발췌 보기Apache 2.0 · SDK 582.0.0
# Google Cloud SDK 582.0.0 (Apache License 2.0) — 발췌. "..."는 생략 표시.
## lib/googlecloudsdk/command_lib/compute/iap_tunnel.py — RunReceiveLocalData (닫는 쪽 thread)
def RunReceiveLocalData(self, conn, socket_address, user_agent, conn_id=0):
websocket_conn = None
try:
websocket_conn = self._InitiateConnection(conn, ...)
while not self._shutdown:
data = conn.recv(utils.SUBPROTOCOL_MAX_DATA_FRAME_SIZE)
if not data:
websocket_conn.LocalEOF()
log.debug('[%d] Received local EOF, closing connection', conn_id)
if not websocket_conn.WaitForAllSent():
...
break
websocket_conn.Send(data)
except (Exception, exceptions.Error) as e:
log.exception('[%d] Error during local connection to [%s]: %s', ...)
finally:
...
try:
conn.close() # client 쪽 socket (trace: shutdown(6), close(6))
except EnvironmentError:
pass
try:
if websocket_conn:
websocket_conn.Close() # → WebSocket SSL socket close (trace: write(7) x2, close(7))
except (EnvironmentError, exceptions.Error):
pass
## lib/googlecloudsdk/api_lib/compute/iap_tunnel_websocket.py — IapTunnelWebSocket.Close
def Close(self):
"""Close down local connection and WebSocket connection."""
self._stopping = True
self._unsent_data.put(StoppingError)
...
if self._websocket_helper:
if not self._close_message_sent:
self._websocket_helper.SendClose()
self._close_message_sent = True
self._websocket_helper.Close() # receive thread 를 join 하지 않음
## lib/googlecloudsdk/api_lib/compute/iap_tunnel_websocket_helper.py
def Close(self, msg=''):
"""Close the WebSocket."""
if not self._is_closed:
self._is_closed = True
try:
self._websocket.close()
except: # pylint: disable=bare-except
pass
...
def _ReceiveFromWebSocket(self): # 별도 daemon thread (receive thread)
"""Receive data from WebSocket connection."""
try:
...
self._websocket.run_forever(origin=TUNNEL_CLOUDPROXY_ORIGIN, sslopt=self._sslopt)
except: # pylint: disable=bare-except
...
try:
self.Close()
...
## lib/googlecloudsdk/api_lib/compute/iap_tunnel_lightweight_websocket.py
def close(self, close_code=CLOSE_STATUS_NORMAL, close_message=six.b("")):
"""Closes the connection."""
if self.connected and self.sock: # lock 없는 check-then-act
try:
self.send_close(close_code, close_message)
self.sock.close() # ← fd 번호 반납. 다른 thread 는 아직 SSL_read 중일 수 있음
self.sock = None
self.connected = False
except (websocket_exceptions.WebSocketConnectionClosedException, socket.error) as e:
...
def run_forever(self, sslopt, **options):
"""Main method that will stay running while the connection is open."""
try:
...
self._connect(sslopt, **options)
while self.connected:
if self.sock.fileno() == -1:
raise websocket_exceptions.WebSocketConnectionClosedException(...)
self._wait_for_socket_to_ready(timeout=WEBSOCKET_RETRY_TIMEOUT_SECS)
frame = self.recv() # → recv_frame → _recv_bytes
if frame.opcode == websocket_frame_utils.ABNF.OPCODE_CLOSE:
close_args = self._get_close_args(frame.data)
self.close()
self.on_close(*close_args)
else:
self.on_data(frame.data, frame.opcode)
...
def _recv_bytes(self, buffersize):
...
while total_bytes_read < buffersize:
bytes_received = self.sock.recv_into(view[total_bytes_read:],
buffersize - total_bytes_read)
# SSLSocket.recv_into → _ssl → OpenSSL SSL_read → read(<SSL_set_fd 로 받은 원시 fd>)
...
## lib/googlecloudsdk/command_lib/compute/iap_tunnel.py — _CleanDeadClientConnections (accept loop)
def _CleanDeadClientConnections(self):
"""Erase reference to dead connections so they can be garbage collected."""
...
for client_thread, conn in self._connections:
if not client_thread.is_alive():
dead_connections += 1
try:
conn.close()
...
if dead_connections:
log.debug('Cleaned [%d] dead connection(s).' % dead_connections)
self._connections = conn_still_alive
gc.collect(2)
log.debug('connections alive: [%d]' % len(self._connections))
# 옛 eBPF 감시 (trace-default-config.bt) 의 필터 — 결함
if ($inode->i_ino == 20396383 &&
$inode->i_sb->s_dev == 0xfc00) { # ← 사용자 공간 표기(stat %D) 값
# 같은 파일시스템에서 실측
stat -c %D <file> → fc00 (new_encode_dev: major 252, minor 0)
bpftrace: f_inode->i_sb->s_dev → 0xfc00000 (커널 MKDEV: 252 << 20)
→ 필터가 한 번도 참이 된 적 없음 → "TARGET WRITE 없음" 은 증거가 아니었음왜 Python의 보호가 통하지 않았나
Python socket 객체는 close()하면 자기 fd를 -1로 바꾼다. 그래서 Python 코드가 닫힌 socket을 다시 써도 fd=-1로 syscall이 나가 EBADF로 안전하게 실패한다. 문제는 TLS 읽기와 쓰기를 실제로 하는 OpenSSL이 이 값을 보지 않는다는 점이다.
| 구분 | syscall | 대상 fd | 실험실 1,500연결 | 의미 |
|---|---|---|---|---|
| Python 수준 재사용 | sendto | -1 | 68 | 안전 닫힌 socket 객체가 -1로 막힘 |
| Python 수준 재사용 | recvfrom | -1 | 19 | 안전 위와 같음 |
| OpenSSL 원시 fd | read | 실제 번호 (7, 8, 11, 12, 17, 23) | 6 | 위험 번호가 재사용됐으면 남의 파일이나 socket을 읽음 |
원시 fd read()가 close 뒤 얼마 만에 일어났나
실험실 6건은 close 후 0.07~110.68ms 사이였다. production near-miss는 89.46ms, 확증 사건은 139.97ms였다. 이 사이에 다른 thread가 파일을 열면 번호가 겹친다.
실험실 재현: 전제 조건은 흔했지만 손상은 재현되지 않았다
운영 tunnel은 건드리지 않고, 설정 디렉터리를 복사해 별도 :15433 tunnel을 띄웠다. 그 process에만 필터를 건 bpftrace로 fd 생성과 close, 모든 I/O, EBADF를 기록했다.
| 부하 | 연결 수 | 관측 | 손상 |
|---|---|---|---|
| 무작위 (full / abrupt / RST 혼합), 동시 6 | 300 | socket close 1,780건 중 다른 thread가 닫은 경우 597건. 그렇게 닫힌 번호를 200ms 안에 default_configs.db가 다시 받은 경우 197건 (최단 0.03ms, 중앙값 4.2ms) | 0 |
| 무작위, 동시 10 | 1,500 | EBADF 93건. 그중 원시 fd read() 6건 | 0 |
| 순차 (종료 후 0~300ms 뒤 다음 연결), 동시 3, 12라운드 | 6,000 | 원시 fd use-after-close 0건 | 0 |
| 표적 쌍 (A 종료 직후 −2~+8ms에 B 연결) | 1,000쌍 | B 응답 1,000/1,000 'S' | 0 |
| pgjdbc 흉내 (TLS handshake → Terminate → 즉시 close → B) | ~100쌍 | production 관측으로 전환하면서 중단 | 0 |
'S'만 주고받은 조용한 연결이라, 닫힌 뒤 도착하는 서버 frame이 WebSocket Close 하나뿐이었다. 수신 thread는 그것을 받고 곧바로 종료한다. production 연결은 실제 PostgreSQL TLS 세션이라 종료 때 frame이 여러 개 온다. 그래서 첫 frame을 처리한 뒤 두 번째 read()를 하게 되고, 그 위험 구간이 약 90~140ms로 길다. 그래서 운영 환경에 관측 장치를 붙이는 쪽으로 전환했다.production 사건 비교
gcloud 로그(초 단위 timestamp는 로그를 읽은 시각)와 DB 파일 mtime을 나란히 놓았다. 세 번 모두 순서가 같다: 이전 연결 종료 → 새 연결 → DB가 덮어써짐 → 이전 연결의 수신 thread가 아직 동작 → 새 연결 실패.
| 사건 | 이전 연결 CLOSE | 새 연결 시작 | DB mtime | 이전 연결 WS Close 수신 | 새 연결 malformed |
|---|---|---|---|---|---|
| 09-24 05:44 ([518]→[519]) | 01.614 | 01.623 | 01.7206 | 01.726 | 01.838 |
| 09-25 10:32 ([9]→[10]) | 43.015 | 43.139 | 43.1347 | 43.204 | 43.242 |
| 09-26 19:31 ([93]→[94]) · trace 확보 | 19.984 | 20.023 | 20.1018 | 20.108 | 20.204 |
09-17 08:05에도 같은 형태(offset 29, 17 03 03 00 13)의 손상 파일이 있었다. 당시에는 timestamp 기록이 없어 순서를 비교할 수 없다.
손상 이후: 실패 로그 폭증 (09-24, 시간당 gcloud 로그 줄 수)
near-miss: production에서 본 "번호가 재사용되지 않은" 경우
write 2회 후 close(7). 89.46ms 뒤 약 43.8초 동안 read(7)에서 대기하던 수신 thread 784460이 다시 read(7)를 했고 EBADF가 났다. 이번에는 그 사이 7번을 연 파일이 없었다. 같은 순간 DB가 열렸다면 손상이었다.수정 제안 (gcloud 쪽)
1. 닫는 순서를 바꾼다
sock.shutdown(SHUT_RDWR)로 수신 thread의 read를 깨우고, 그 thread를 join()한 뒤에만 close()한다. 아니면 socket 소유권을 수신 thread에 주고 닫기도 그쪽에서 하게 한다.
2. close와 I/O를 같은 lock으로
IapLightWeightWebsocket.close()는 lock 없는 check-then-act다. send에서 쓰는 lock을 close와 수신 경로에도 적용하고, close가 시작되면 run_forever가 더 읽지 않게 한다.
의사 코드
def close(self):
with self._lock:
if not self.connected:
return
self.connected = False # run_forever 루프가 더 읽지 않게
try:
self.send_close()
finally:
self.sock.shutdown(socket.SHUT_RDWR) # 블로킹 read 를 깨움 (fd 는 아직 살아 있음)
if threading.current_thread() is not self._receiver:
self._receiver.join(timeout=5) # 수신 thread 가 OpenSSL 밖으로 나온 뒤
self.sock.close() # 그때 비로소 번호 반납운영 교훈
listener가 살아 있어도 end-to-end가 정상인 것은 아니다
TCP connect 성공은 tunnel이 건강하다는 뜻이 아니다. PostgreSQL SSLRequest(00 00 00 08 04 d2 16 2f)를 보내 'S'/'N'를 받는 probe가 필요하다. A/B 시험에서 :15432는 connect 0.1ms에 성공하고 SSLRequest에서 reset됐다.
독립 경로 대조군이 가장 빨리 범위를 줄인다
같은 목적지로 가는 다른 호스트의 tunnel이 같은 시각에 'S'를 받았다. 이 한 번의 비교로 원격 전체(PG, VM, IAP, 네트워크)가 빠졌다.
감시 도구도 검증해야 한다
eBPF 필터가 s_dev == 0xfc00(사용자 공간 표기)였다. 커널 값은 0xfc00000(MKDEV)라 필터가 한 번도 참이 되지 않았다. "기록 없음"을 "사건 없음"으로 믿었다가 철회했다. 필터는 알려진 이벤트로 먼저 시험해야 한다.
1차 원인과 2차 증폭을 분리한다
앱 전체가 멈춘 직접 원인은 Slack 알림 코드였다. timeout 없는 동기 호출, .body(sendSlack(...)) 이중 호출, 컨테이너에 없던 webhook URL(기본값이 자기 자신)이 겹쳐 self-deadlock이 났다. 고치지 않았다면 DB 원인을 찾아도 장애 규모는 그대로였을 것이다.
숫자의 모양이 가설을 준다
24 bytes, 0x13 = 19 = 2 + 1 + 16, 그리고 항상 offset 29. 이 크기와 위치가 "암호화된 TLS alert가 파일에 쓰였다", 이어서 "fd 번호가 섞였다"는 가설을 곧바로 가리켰다.
재현이 안 되면 관측을 옮긴다
실험실 약 10,000연결로도 손상은 재현되지 않았다. 대신 운영 process에 PID를 따라가는 tracer와 손상 즉시 증거를 보존하는 supervisor를 붙였고, 다음 자연 발생에서 전체 syscall 사슬을 얻었다.
증거 부록
원본은 운영 서버의 /var/oled/diary-forensics/ 아래에 있다. 공개를 위해 IP, 프로젝트, 계정, invocation-id 등을 가렸다(<CLIENT_IP>, <ID> 등).
E1 A/B 진단 결과 (2026-09-25 08:34)root-cause-20260925T083457Z/
### ROOT-CAUSE-CLASSIFICATION.txt
Captured: 2026-09-25T08:35:21.325Z
OLD_RESULT=3
OLD_SECOND_RESULT=3
FRESH_RESULT=99
CLASSIFICATION=CONTROL_TUNNEL_NOT_AVAILABLE
The fresh :15433 control could not be created.
Inspect fresh tunnel logs before drawing a conclusion.
### probe-old.txt
[OLD-15432] target=127.0.0.1:15432
[OLD-15432] start=1790325310314511163
[OLD-15432] TCP_CONNECT=OK
[OLD-15432] TCP_TIME=0.000137s
[OLD-15432] PG_SSL_RESPONSE=TIMEOUT_OR_ERROR
[OLD-15432] PG_SSL_TIME=0.201741s
[OLD-15432] ERROR=ConnectionResetError(104, 'Connection reset by peer')
### probe-old-second.txt
[OLD-15432-SECOND] target=127.0.0.1:15432
[OLD-15432-SECOND] start=1790325316783490690
[OLD-15432-SECOND] TCP_CONNECT=OK
[OLD-15432-SECOND] TCP_TIME=0.000136s
[OLD-15432-SECOND] PG_SSL_RESPONSE=TIMEOUT_OR_ERROR
[OLD-15432-SECOND] PG_SSL_TIME=4.027823s
[OLD-15432-SECOND] ERROR=ConnectionResetError(104, 'Connection reset by peer')
### probe-fresh.txt
FRESH_RESULT=NOT_AVAILABLE
### fresh-15433.stderr.log (tail)
sqlite3.DatabaseError: database disk image is malformed
ERROR: gcloud crashed (DatabaseError): database disk image is malformed
If you would like to report this issue, please run the following command:
gcloud feedback
To check gcloud for common problems, please run the following command:
gcloud info --run-diagnosticsE2 09-24 05:44 손상 전후 gcloud 로그와 시간당 줄 수iap-timestamp/stderr-timestamped.log
### hourly line counts
359 2026-09-24T01
262 2026-09-24T02
261 2026-09-24T03
376 2026-09-24T04
3322 2026-09-24T05
10799 2026-09-24T06
10799 2026-09-24T07
10800 2026-09-24T08
### 05:44 sequence
2026-09-24T05:41:54.222Z DEBUG: [518] ENQUEUED data_len [44] bytes_to_send[:20] [b"\x17\x03\x03\x00'\x14!\xf3\xae\x02(KK\xfe\x1a\x86?\x02Z~"]
2026-09-24T05:41:54.225Z DEBUG: [518] SEND data_len [50] send_data[:20] b"\x00\x04\x00\x00\x00,\x17\x03\x03\x00'\x14!\xf3\xae\x02(KK\xfe"
2026-09-24T05:41:54.345Z DEBUG: [518] RECV opcode [2] data_len [10] binary_data[:20] [b'\x00\x07\x00\x00\x00\x00\x00\x00\x06:']
2026-09-24T05:41:54.347Z DEBUG: [518] RECV opcode [2] data_len [39] binary_data[:20] [b'\x00\x04\x00\x00\x00!\x17\x03\x03\x00\x1c*h\x1c\x8e\x9a\xdbiS;']
2026-09-24T05:41:56.223Z DEBUG: [518] SEND data_len [10] send_data[:20] b'\x00\x07\x00\x00\x00\x00\x00\x00\t\xee'
2026-09-24T05:43:52.020Z DEBUG: [518] ENQUEUED data_len [44] bytes_to_send[:20] [b"\x17\x03\x03\x00'\x0f\x12\x8e\xe9\x96'%(\x8d\xc9\xe5II\xef\x90"]
2026-09-24T05:43:52.022Z DEBUG: [518] SEND data_len [50] send_data[:20] b"\x00\x04\x00\x00\x00,\x17\x03\x03\x00'\x0f\x12\x8e\xe9\x96'%(\x8d"
2026-09-24T05:43:52.142Z DEBUG: [518] RECV opcode [2] data_len [10] binary_data[:20] [b'\x00\x07\x00\x00\x00\x00\x00\x00\x06f']
2026-09-24T05:43:52.144Z DEBUG: [518] RECV opcode [2] data_len [39] binary_data[:20] [b'\x00\x04\x00\x00\x00!\x17\x03\x03\x00\x1c\x8c\x9af\xd9Sc\xd3v#']
2026-09-24T05:43:54.021Z DEBUG: [518] SEND data_len [10] send_data[:20] b'\x00\x07\x00\x00\x00\x00\x00\x00\n\x0f'
2026-09-24T05:44:01.601Z DEBUG: [518] ENQUEUED data_len [43] bytes_to_send[:20] [b'\x17\x03\x03\x00&D5>\\V\x1fc\xf9\x10\x8aKQ(\x83q']
2026-09-24T05:44:01.604Z DEBUG: [518] ENQUEUED data_len [80] bytes_to_send[:20] [b'\x17\x03\x03\x00#\xd0pG\x87\xc7\x99{\x10\x8a\x03\x98\xed\xdf\xc2\xbb']
2026-09-24T05:44:01.606Z DEBUG: [518] Received local EOF, closing connection
2026-09-24T05:44:01.609Z DEBUG: [518] SEND data_len [49] send_data[:20] b'\x00\x04\x00\x00\x00+\x17\x03\x03\x00&D5>\\V\x1fc\xf9\x10'
2026-09-24T05:44:01.611Z DEBUG: [518] SEND data_len [86] send_data[:20] b'\x00\x04\x00\x00\x00P\x17\x03\x03\x00#\xd0pG\x87\xc7\x99{\x10\x8a'
2026-09-24T05:44:01.614Z DEBUG: [518] CLOSE
2026-09-24T05:44:01.617Z INFO: [518] Client closed connection from [('<CLIENT_IP>', 47348)].
2026-09-24T05:44:01.620Z DEBUG: On GCE from memory cache: False
2026-09-24T05:44:01.623Z DEBUG: [519] user-agent [gcloud/582.0.0 command/gcloud.compute.start-iap-tunnel invocation-id/<ID> environment/None environment-version/None client-os/LINUX client-os-ver/6.12.0 client-pltf-arch/x86_64 interactive/False from-script/False python/3.14.7 term/ (Linux 6.12.0-204.92.4.4.3.el9uek.x86_64)]
2026-09-24T05:44:01.626Z DEBUG: Cleaned [1] dead connection(s).
2026-09-24T05:44:01.629Z DEBUG: credentials type for _GetAccessTokenCallback is [<googlecloudsdk.core.credentials.google_auth_credentials.Credentials object at 0x7ff68dd14ad0>].
2026-09-24T05:44:01.721Z DEBUG: connections alive: [1]
2026-09-24T05:44:01.724Z DEBUG: On GCE from memory cache: False
2026-09-24T05:44:01.726Z INFO: [518] Received WebSocket Close message [1: 1].
2026-09-24T05:44:01.838Z ERROR: [519] Error during local connection to [('<CLIENT_IP>', 33692)]: database disk image is malformed
2026-09-24T05:44:01.840Z Traceback (most recent call last):
2026-09-24T05:44:01.843Z File "/home/opc/google-cloud-sdk/lib/googlecloudsdk/command_lib/compute/iap_tunnel.py", line 805, in RunReceiveLocalData
2026-09-24T05:44:01.850Z websocket_conn = self._InitiateConnection(
### 05:44 malformed first
38429:2026-09-24T05:44:01.838Z ERROR: [519] Error during local connection to [('<CLIENT_IP>', 33692)]: database disk image is malformedE3 09-25 10:32 손상 전후 gcloud 로그prod-corruption-20260925T103243Z/
2026-09-25T10:28:55.548Z DEBUG: [9] ENQUEUED data_len [44] bytes_to_send[:20] [b"\x17\x03\x03\x00'hJ\x8f\x86\x87#\xb5\xd7\x97L\xa7_\xefv\x0e"]
2026-09-25T10:28:55.550Z DEBUG: [9] SEND data_len [50] send_data[:20] b"\x00\x04\x00\x00\x00,\x17\x03\x03\x00'hJ\x8f\x86\x87#\xb5\xd7\x97"
2026-09-25T10:28:55.675Z DEBUG: [9] RECV opcode [2] data_len [10] binary_data[:20] [b'\x00\x07\x00\x00\x00\x00\x00\x00\x06f']
2026-09-25T10:28:55.677Z DEBUG: [9] RECV opcode [2] data_len [39] binary_data[:20] [b'\x00\x04\x00\x00\x00!\x17\x03\x03\x00\x1cx\x96\x11\x0f\xa9\xf2 t\xaa']
2026-09-25T10:28:57.549Z DEBUG: [9] SEND data_len [10] send_data[:20] b'\x00\x07\x00\x00\x00\x00\x00\x00\n\x0f'
2026-09-25T10:30:46.382Z DEBUG: [9] ENQUEUED data_len [44] bytes_to_send[:20] [b'\x17\x03\x03\x00\'\x1ar"\xca\xaf\x1d9\x16I\xef\xb5"\xa0\xc5\x04']
2026-09-25T10:30:46.384Z DEBUG: [9] SEND data_len [50] send_data[:20] b'\x00\x04\x00\x00\x00,\x17\x03\x03\x00\'\x1ar"\xca\xaf\x1d9\x16I'
2026-09-25T10:30:46.509Z DEBUG: [9] RECV opcode [2] data_len [10] binary_data[:20] [b'\x00\x07\x00\x00\x00\x00\x00\x00\x06\x92']
2026-09-25T10:30:46.512Z DEBUG: [9] RECV opcode [2] data_len [39] binary_data[:20] [b'\x00\x04\x00\x00\x00!\x17\x03\x03\x00\x1c\xad\xc9Xvh\\\x8e\x05\t']
2026-09-25T10:30:48.383Z DEBUG: [9] SEND data_len [10] send_data[:20] b'\x00\x07\x00\x00\x00\x00\x00\x00\n0'
2026-09-25T10:32:37.216Z DEBUG: [9] ENQUEUED data_len [44] bytes_to_send[:20] [b"\x17\x03\x03\x00'\xa0Yt\x82\x92\rG\xaf\x86\x1aQI\x99N\xcc"]
2026-09-25T10:32:37.219Z DEBUG: [9] SEND data_len [50] send_data[:20] b"\x00\x04\x00\x00\x00,\x17\x03\x03\x00'\xa0Yt\x82\x92\rG\xaf\x86"
2026-09-25T10:32:37.343Z DEBUG: [9] RECV opcode [2] data_len [10] binary_data[:20] [b'\x00\x07\x00\x00\x00\x00\x00\x00\x06\xbe']
2026-09-25T10:32:37.345Z DEBUG: [9] RECV opcode [2] data_len [39] binary_data[:20] [b'\x00\x04\x00\x00\x00!\x17\x03\x03\x00\x1c\xf6\xf3\xfb\x02gu\xe3\xa0x']
2026-09-25T10:32:39.217Z DEBUG: [9] SEND data_len [10] send_data[:20] b'\x00\x07\x00\x00\x00\x00\x00\x00\nQ'
2026-09-25T10:32:43.008Z DEBUG: [9] ENQUEUED data_len [123] bytes_to_send[:20] [b'\x17\x03\x03\x00&\xf2\xec\xc6&\xde\xd2~\x9c\x94K\xe1\xdc\xa9\x8e\xa5']
2026-09-25T10:32:43.012Z DEBUG: [9] SEND data_len [129] send_data[:20] b'\x00\x04\x00\x00\x00{\x17\x03\x03\x00&\xf2\xec\xc6&\xde\xd2~\x9c\x94'
2026-09-25T10:32:43.015Z DEBUG: [9] CLOSE
2026-09-25T10:32:43.017Z DEBUG: [9] Received local EOF, closing connection
2026-09-25T10:32:43.020Z INFO: [9] Client closed connection from [('<CLIENT_IP>', 50996)].
2026-09-25T10:32:43.022Z DEBUG: Cleaned [1] dead connection(s).
2026-09-25T10:32:43.131Z DEBUG: connections alive: [1]
2026-09-25T10:32:43.135Z DEBUG: On GCE from memory cache: False
2026-09-25T10:32:43.139Z DEBUG: [10] user-agent [gcloud/582.0.0 command/gcloud.compute.start-iap-tunnel invocation-id/<ID> environment/None environment-version/None client-os/LINUX client-os-ver/6.12.0 client-pltf-arch/x86_64 interactive/False from-script/False python/3.14.7 term/ (Linux 6.12.0-204.92.4.4.3.el9uek.x86_64)]
2026-09-25T10:32:43.142Z DEBUG: credentials type for _GetAccessTokenCallback is [<googlecloudsdk.core.credentials.google_auth_credentials.Credentials object at 0x7f9534c60cd0>].
2026-09-25T10:32:43.204Z INFO: [9] Received WebSocket Close message [1: 1].
2026-09-25T10:32:43.242Z ERROR: [10] Error during local connection to [('<CLIENT_IP>', 56058)]: database disk image is malformed
2026-09-25T10:32:43.244Z Traceback (most recent call last):
2026-09-25T10:32:43.247Z File "/home/opc/google-cloud-sdk/lib/googlecloudsdk/command_lib/compute/iap_tunnel.py", line 805, in RunReceiveLocalData
2026-09-25T10:32:43.249Z websocket_conn = self._InitiateConnection(
2026-09-25T10:32:43.252Z conn,
2026-09-25T10:32:43.302Z ...<4 lines>...E4 확증 사건 bpftrace 발췌 (19:31:20)prod-corruption-20260926T193120Z/fdrace.log
# bpftrace excerpt, gcloud start-iap-tunnel pid 784375 (nsecs = CLOCK_MONOTONIC)
# tid 1421083 = _ReceiveFromWebSocket thread of conn [93]; tid 1421118 = RunReceiveLocalData thread of conn [93]; tid 1431669 = handler thread of conn [94]
1782994483758112 IO tid=1421083 fd=7 tracepoint:syscalls:sys_enter_read
1782994484310035 IO tid=1421083 fd=6 tracepoint:syscalls:sys_enter_sendto
1782996355584267 IO tid=1421118 fd=7 tracepoint:syscalls:sys_enter_write
1782997486670764 IO tid=1421083 fd=7 tracepoint:syscalls:sys_enter_read
1783112618900675 IO tid=1421118 fd=7 tracepoint:syscalls:sys_enter_write
1783112744694864 IO tid=1421083 fd=7 tracepoint:syscalls:sys_enter_read
1783112746043553 IO tid=1421083 fd=7 tracepoint:syscalls:sys_enter_read
1783112746076144 IO tid=1421083 fd=7 tracepoint:syscalls:sys_enter_read
1783112746408372 IO tid=1421083 fd=6 tracepoint:syscalls:sys_enter_sendto
1783114619712111 IO tid=1421118 fd=7 tracepoint:syscalls:sys_enter_write
1783115748224655 IO tid=1421083 fd=7 tracepoint:syscalls:sys_enter_read
1783210794447320 IO tid=1421118 fd=7 tracepoint:syscalls:sys_enter_write
1783210794813040 IO tid=1421118 fd=7 tracepoint:syscalls:sys_enter_write
1783210794881670 SHUTDN tid=1421118 fd=6 how=2
1783210794934250 CLOSE tid=1421118 fd=6
1783210795261518 IO tid=1421118 fd=7 tracepoint:syscalls:sys_enter_write
1783210795334326 IO tid=1421118 fd=7 tracepoint:syscalls:sys_enter_write
1783210795370684 CLOSE tid=1421118 fd=7
1783210800071097 OPEN tid=1431669 fd=7 /home/opc/.config/gcloud-diary-iap/active_config
1783210800137653 IO tid=1431669 fd=7 tracepoint:syscalls:sys_enter_read
1783210800146870 FREAD tid=1431669 fd=7 ino=20396382 pos=0 len=8
1783210800153333 IO tid=1431669 fd=7 tracepoint:syscalls:sys_enter_read
1783210800154636 FREAD tid=1431669 fd=7 ino=20396382 pos=7 len=1
1783210800174753 CLOSE tid=1431669 fd=7
1783210800281204 OPEN tid=1431669 fd=7 /home/opc/.config/gcloud-diary-iap/default_configs.db
1783210800312674 IO tid=1431669 fd=7 tracepoint:syscalls:sys_enter_pread64
1783210800314698 FREAD tid=1431669 fd=7 ino=23194204 pos=0 len=100
1783210800492314 IO tid=1431669 fd=7 tracepoint:syscalls:sys_enter_pread64
1783210800617059 IO tid=1431669 fd=7 tracepoint:syscalls:sys_enter_pread64
1783210800632419 FREAD tid=1431669 fd=7 ino=23194204 pos=24 len=16
1783210800672694 CLOSE tid=1431669 fd=7
1783210800736124 OPEN tid=1431669 fd=7 /home/opc/.config/gcloud-diary-iap/default_configs.db
1783210800757786 IO tid=1431669 fd=7 tracepoint:syscalls:sys_enter_pread64
1783210800759549 FREAD tid=1431669 fd=7 ino=23194204 pos=0 len=100
1783210800881228 IO tid=1431669 fd=7 tracepoint:syscalls:sys_enter_pread64
1783210800962192 IO tid=1431669 fd=7 tracepoint:syscalls:sys_enter_pread64
1783210800963985 FREAD tid=1431669 fd=7 ino=23194204 pos=24 len=16
1783210800975998 IO tid=1431669 fd=7 tracepoint:syscalls:sys_enter_pread64
1783210800983442 IO tid=1431669 fd=7 tracepoint:syscalls:sys_enter_pread64
1783210801044016 CLOSE tid=1431669 fd=7
1783210875242833 OPEN tid=1431669 fd=7 /home/opc/.config/gcloud-diary-iap/active_config
1783210875296264 IO tid=1431669 fd=7 tracepoint:syscalls:sys_enter_read
1783210875298508 FREAD tid=1431669 fd=7 ino=20396382 pos=0 len=8
1783210875305011 IO tid=1431669 fd=7 tracepoint:syscalls:sys_enter_read
1783210875306433 FREAD tid=1431669 fd=7 ino=20396382 pos=7 len=1
1783210875324368 CLOSE tid=1431669 fd=7
1783210875422643 OPEN tid=1431669 fd=7 /home/opc/.config/gcloud-diary-iap/default_configs.db
1783210875438032 IO tid=1431669 fd=7 tracepoint:syscalls:sys_enter_pread64
1783210875439976 FREAD tid=1431669 fd=7 ino=23194204 pos=0 len=100
1783210875611861 IO tid=1431669 fd=7 tracepoint:syscalls:sys_enter_pread64
1783210875719143 IO tid=1431669 fd=7 tracepoint:syscalls:sys_enter_pread64
1783210875721067 FREAD tid=1431669 fd=7 ino=23194204 pos=24 len=16
1783210875757235 CLOSE tid=1431669 fd=7
1783210875818321 OPEN tid=1431669 fd=7 /home/opc/.config/gcloud-diary-iap/default_configs.db
1783210875829722 IO tid=1431669 fd=7 tracepoint:syscalls:sys_enter_pread64
1783210875831436 FREAD tid=1431669 fd=7 ino=23194204 pos=0 len=100
1783210875949419 IO tid=1431669 fd=7 tracepoint:syscalls:sys_enter_pread64
1783210935320369 IO tid=1431669 fd=7 tracepoint:syscalls:sys_enter_pread64
1783210935337711 FREAD tid=1431669 fd=7 ino=23194204 pos=24 len=16
1783210935339094 IO tid=1421083 fd=7 tracepoint:syscalls:sys_enter_read
1783210935341608 FREAD tid=1421083 fd=7 ino=23194204 pos=0 len=29
1783210935360444 IO tid=1431669 fd=7 tracepoint:syscalls:sys_enter_pread64
1783210935369572 IO tid=1431669 fd=7 tracepoint:syscalls:sys_enter_pread64
1783210935394108 IO tid=1421083 fd=7 tracepoint:syscalls:sys_enter_write
1783210935397805 FWRITE tid=1421083 fd=7 ino=23194204 pos=29 len=24 head=\x17\x03\x03\x00\x13\xaa#\xcb\xc9\x02r\xa9\x82\xf1=\xde
!!! TLS-LOOKING WRITE TO REGULAR FILE !!! tid=1421083 comm=Thread-275 (_Re
!!! TLS-LOOKING WRITE TO REGULAR FILE !!! tid=1421083 comm=Thread-275 (_Re
ustack:
__write+79
0x5627de228a30
0x5627de113346
0x5627de113227
0x5627de5e0a63
0x5627de5943be
0x5627de5eeb03
0x5627de5da308
0x5627de5da5b4
0x5627de592fcb
0x5627de59f3d9
0x5627ddfdda94
0x5627ddd38995
PyObject_Vectorcall+85
0x5627dde67cdf
_PyEval_EvalFrameDefault+254
0x5627dde57653
0x5627ddd2cf9f
0x5627dde5ca42
_PyEval_EvalFrameDefault+254
0x5627dde57653
0x5627ddd2cf9f
0x5627dde878f8
0x5627dde67499
_PyEval_EvalFrameDefault+254
0x5627dde57653
0x5627ddd2cf9f
0x5627de06e801
0x5627ddeead19
start_thread+825E5 확증 사건 gcloud 로그와 손상 DB 헤더prod-corruption-20260926T193120Z/
2026-09-26T19:27:43.653Z DEBUG: [93] RECV opcode [2] data_len [39] binary_data[:20] [b'\x00\x04\x00\x00\x00!\x17\x03\x03\x00\x1c\xfe\xf5_\xae-K\xec\x95\xdc']
2026-09-26T19:27:45.524Z DEBUG: [93] SEND data_len [10] send_data[:20] b'\x00\x07\x00\x00\x00\x00\x00\x00\x14\xda'
2026-09-26T19:29:41.788Z DEBUG: [93] ENQUEUED data_len [44] bytes_to_send[:20] [b"\x17\x03\x03\x00'\xcaQ\r\x80,\xe9D\xc3\x15\x14\xe5k$\xe3\xce"]
2026-09-26T19:29:41.791Z DEBUG: [93] SEND data_len [50] send_data[:20] b"\x00\x04\x00\x00\x00,\x17\x03\x03\x00'\xcaQ\r\x80,\xe9D\xc3\x15"
2026-09-26T19:29:41.914Z DEBUG: [93] RECV opcode [2] data_len [10] binary_data[:20] [b'\x00\x07\x00\x00\x00\x00\x00\x00\x08\x1d']
2026-09-26T19:29:41.916Z DEBUG: [93] RECV opcode [2] data_len [39] binary_data[:20] [b'\x00\x04\x00\x00\x00!\x17\x03\x03\x00\x1c\xa8\xc4\x8c!\x95\xbf\xb4\xe8\xdc']
2026-09-26T19:29:43.788Z DEBUG: [93] SEND data_len [10] send_data[:20] b'\x00\x07\x00\x00\x00\x00\x00\x00\x14\xfb'
2026-09-26T19:31:19.964Z DEBUG: [93] ENQUEUED data_len [43] bytes_to_send[:20] [b'\x17\x03\x03\x00&\xa5\xd8\xe6\x02We\xf1\xcd\xa0\xed\xb6}\xd3\xe8\x95']
2026-09-26T19:31:19.967Z DEBUG: [93] ENQUEUED data_len [80] bytes_to_send[:20] [b'\x17\x03\x03\x00#vQz\x9ec\x995\xa3n\xa6\x17\x0ci.F']
2026-09-26T19:31:19.974Z DEBUG: [93] Received local EOF, closing connection
2026-09-26T19:31:19.978Z DEBUG: [93] SEND data_len [49] send_data[:20] b'\x00\x04\x00\x00\x00+\x17\x03\x03\x00&\xa5\xd8\xe6\x02We\xf1\xcd\xa0'
2026-09-26T19:31:19.981Z DEBUG: [93] SEND data_len [86] send_data[:20] b'\x00\x04\x00\x00\x00P\x17\x03\x03\x00#vQz\x9ec\x995\xa3n'
2026-09-26T19:31:19.984Z DEBUG: [93] CLOSE
2026-09-26T19:31:20.016Z INFO: [93] Client closed connection from [('<CLIENT_IP>', 35734)].
2026-09-26T19:31:20.019Z DEBUG: On GCE from memory cache: False
2026-09-26T19:31:20.023Z DEBUG: [94] user-agent [gcloud/582.0.0 command/gcloud.compute.start-iap-tunnel invocation-id/<ID> environment/None environment-version/None client-os/LINUX client-os-v
2026-09-26T19:31:20.027Z DEBUG: Cleaned [1] dead connection(s).
2026-09-26T19:31:20.030Z DEBUG: credentials type for _GetAccessTokenCallback is [<googlecloudsdk.core.credentials.google_auth_credentials.Credentials object at 0x7f3970558cd0>].
2026-09-26T19:31:20.043Z DEBUG: connections alive: [1]
2026-09-26T19:31:20.105Z DEBUG: On GCE from memory cache: False
2026-09-26T19:31:20.108Z INFO: [93] Received WebSocket Close message [1: 1].
2026-09-26T19:31:20.204Z ERROR: [94] Error during local connection to [('<CLIENT_IP>', 56440)]: database disk image is malformed
2026-09-26T19:31:20.206Z Traceback (most recent call last):
2026-09-26T19:31:20.209Z File "/home/opc/google-cloud-sdk/lib/googlecloudsdk/command_lib/compute/iap_tunnel.py", line 805, in RunReceiveLocalData
2026-09-26T19:31:20.211Z websocket_conn = self._InitiateConnection(
2026-09-26T19:31:20.213Z conn,
2026-09-26T19:31:20.216Z ...<4 lines>...
2026-09-26T19:31:20.220Z conn_id=conn_id,
2026-09-26T19:31:20.227Z )
2026-09-26T19:31:20.302Z File "/home/opc/google-cloud-sdk/lib/googlecloudsdk/command_lib/compute/iap_tunnel.py", line 771, in _InitiateConnection
2026-09-26T19:31:20.305Z new_websocket.InitiateConnection()
== hexdump of corrupted default_configs.db (first 64 bytes)
00000000: 5351 4c69 7465 2066 6f72 6d61 7420 3300 SQLite format 3.
00000010: 1000 0101 0040 2020 0000 0003 0017 0303 .....@ ........
00000020: 0013 aa23 cbc9 0272 a982 f13d dea7 0fbe ...#...r...=....
00000030: 1559 2993 ea00 0000 0000 0001 0000 0000 .Y).............E6 production near-miss (09-25 13:44)prod-trace/fdrace-pid*.log
### near-miss (fdrace-pid784375-20260925T134124Z.log line 292)
1675931699860192 IO tid=784460 fd=7 tracepoint:syscalls:sys_enter_read
1675931701473721 IO tid=784460 fd=7 tracepoint:syscalls:sys_enter_read
1675931701490703 IO tid=784460 fd=7 tracepoint:syscalls:sys_enter_read
1675934704522372 IO tid=784460 fd=7 tracepoint:syscalls:sys_enter_read
1675978445696236 IO tid=784443 fd=7 tracepoint:syscalls:sys_enter_write
1675978445830710 IO tid=784443 fd=7 tracepoint:syscalls:sys_enter_write
1675978445870755 CLOSE tid=784443 fd=7
1675978535335496 IO tid=784460 fd=7 tracepoint:syscalls:sys_enter_read
1675978535348952 EBADF tid=784460 fd=7 tracepoint:syscalls:sys_exit_read
!!! RAW-FD USE-AFTER-CLOSE !!! tid=784460 fd=7
ustack:
__GI_read+76E7 실험실 재현 통계repro-20260925T090534Z/
### analyze fdrace2 (1500 conc10)
events: {'ACCEPT': 1500, 'OPEN': 7492, 'IO': 189646, 'FREAD': 9000, 'CLOSE': 16393, 'SOCKET': 7407, 'SHUTDN': 3000, 'EBADF': 93}
HIT (non-sqlite read/write on DB fd): 0
STALE (old socket user touching new file generation): 0
EBADF (use of already-closed fd): 93
68 tracepoint:syscalls:sys_exit_sendto
19 tracepoint:syscalls:sys_exit_recvfrom
6 tracepoint:syscalls:sys_exit_read
### hunt.log
09:26:41 round=1 corrupt_dbs=0 hit=0 raw_uaf=15 tls_file_writes=0 SUMMARY [(('abrupt', 'SENT'), 135), (('full', 'S'), 244), (('rst', 'S'), 121)]
09:29:38 round=2 corrupt_dbs=0 hit=0 raw_uaf=28 tls_file_writes=0 SUMMARY [(('abrupt', 'SENT'), 119), (('full', 'S'), 274), (('rst', 'S'), 107)]
09:32:32 round=3 corrupt_dbs=0 hit=0 raw_uaf=45 tls_file_writes=0 SUMMARY [(('abrupt', 'SENT'), 132), (('full', 'S'), 244), (('rst', 'S'), 124)]
09:35:22 round=4 corrupt_dbs=0 hit=0 raw_uaf=61 tls_file_writes=0 SUMMARY [(('abrupt', 'SENT'), 146), (('full', 'S'), 250), (('rst', 'S'), 104)]
09:38:16 round=5 corrupt_dbs=0 hit=0 raw_uaf=70 tls_file_writes=0 SUMMARY [(('abrupt', 'SENT'), 134), (('full', 'S'), 259), (('rst', 'S'), 107)]
09:41:17 round=6 corrupt_dbs=0 hit=0 raw_uaf=89 tls_file_writes=0 SUMMARY [(('abrupt', 'SENT'), 135), (('full', 'S'), 252), (('rst', 'S'), 113)]
09:44:16 round=7 corrupt_dbs=0 hit=0 raw_uaf=101 tls_file_writes=0 SUMMARY [(('abrupt', 'SENT'), 124), (('full', 'S'), 226), (('rst', 'S'), 150)]
09:47:13 round=8 corrupt_dbs=0 hit=0 raw_uaf=112 tls_file_writes=0 SUMMARY [(('abrupt', 'SENT'), 136), (('full', 'S'), 255), (('rst', 'S'), 109)]
09:50:07 round=9 corrupt_dbs=0 hit=0 raw_uaf=125 tls_file_writes=0 SUMMARY [(('abrupt', 'SENT'), 122), (('full', 'S'), 255), (('rst', 'S'), 123)]
09:53:06 round=10 corrupt_dbs=0 hit=0 raw_uaf=146 tls_file_writes=0 SUMMARY [(('abrupt', 'SENT'), 125), (('full', 'S'), 235), (('rst', 'S'), 140)]
09:56:07 round=11 corrupt_dbs=0 hit=0 raw_uaf=161 tls_file_writes=0 SUMMARY [(('abrupt', 'SENT'), 122), (('full', 'S'), 255), (('rst', 'S'), 123)]
09:59:11 round=12 corrupt_dbs=0 hit=0 raw_uaf=171 tls_file_writes=0 SUMMARY [(('abrupt', 'SENT'), 122), (('full', 'S'), 271), (('rst', 'S'), 107)]
no reproduction after 12 rounds
### pair summary
SUMMARY {'S': 1000}
10:21:57 #950 ok {'S': 950}
10:23:04 #1000 ok {'S': 1000}
### pair2
13:34:38 #50 ok {'S': 50}
13:36:32 #100 ok {'S': 100}
### load-summary
SUMMARY [(('abrupt', 'SENT'), 69), (('full', 'S'), 139), (('rst', 'S'), 92)]
SUMMARY [(('abrupt', 'SENT'), 357), (('full', 'S'), 759), (('full', 'TIMEOUT'), 1), (('rst', 'S'), 383)]
SUMMARY [(('abrupt', 'SENT'), 135), (('full', 'S'), 244), (('rst', 'S'), 121)]
SUMMARY [(('abrupt', 'SENT'), 119), (('full', 'S'), 274), (('rst', 'S'), 107)]
SUMMARY [(('abrupt', 'SENT'), 132), (('full', 'S'), 244), (('rst', 'S'), 124)]
SUMMARY [(('abrupt', 'SENT'), 146), (('full', 'S'), 250), (('rst', 'S'), 104)]
SUMMARY [(('abrupt', 'SENT'), 134), (('full', 'S'), 259), (('rst', 'S'), 107)]
SUMMARY [(('abrupt', 'SENT'), 135), (('full', 'S'), 252), (('rst', 'S'), 113)]
SUMMARY [(('abrupt', 'SENT'), 124), (('full', 'S'), 226), (('rst', 'S'), 150)]
SUMMARY [(('abrupt', 'SENT'), 136), (('full', 'S'), 255), (('rst', 'S'), 109)]
SUMMARY [(('abrupt', 'SENT'), 122), (('full', 'S'), 255), (('rst', 'S'), 123)]
SUMMARY [(('abrupt', 'SENT'), 125), (('full', 'S'), 235), (('rst', 'S'), 140)]
SUMMARY [(('abrupt', 'SENT'), 122), (('full', 'S'), 255), (('rst', 'S'), 123)]
SUMMARY [(('abrupt', 'SENT'), 122), (('full', 'S'), 271), (('rst', 'S'), 107)]
### lab300: same-thread 1183 cross-thread 597 db-reuse<200ms 197 min_ms 0.03 median_ms 4.2
### raw-fd EBADF detail (fdrace2)
read fd=17 reader_tid=502660 closer_tid=502798 gap_ms=0.18
read fd=23 reader_tid=505835 closer_tid=505975 gap_ms=0.26
read fd=17 reader_tid=510398 closer_tid=510317 gap_ms=0.07
read fd=12 reader_tid=512090 closer_tid=512252 gap_ms=0.35
read fd=11 reader_tid=520126 closer_tid=520250 gap_ms=3.79
read fd=8 reader_tid=523681 closer_tid=523635 gap_ms=110.68E8 손상 DB 4개의 hexdump각 사건 폴더
### /var/oled/diary-forensics/root-cause-20260925T083457Z/default_configs.db.corrupt.20260924T054401Z
size=12288 mtime=2026-09-24 05:44:01.720654520 +0000
00000000: 5351 4c69 7465 2066 6f72 6d61 7420 3300 SQLite format 3.
00000010: 1000 0101 0040 2020 0000 0003 0017 0303 .....@ ........
00000020: 0013 c98c 9768 fcee 268e 8137 ba38 7ac3 .....h..&..7.8z.
00000030: 7ed5 64c0 5a00 0000 0000 0001 0000 0000 ~.d.Z...........
00000040: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000050: 0000 0000 0000 0000 0000 0000 0000 0003 ................
### /var/oled/diary-forensics/prod-corruption-20260925T103243Z/default_configs.db.corrupt
size=12288 mtime=2026-09-25 10:32:43.134759753 +0000
00000000: 5351 4c69 7465 2066 6f72 6d61 7420 3300 SQLite format 3.
00000010: 1000 0101 0040 2020 0000 0003 0017 0303 .....@ ........
00000020: 0013 1205 4a46 10a2 aa88 9d1a 074e 5d57 ....JF.......N]W
00000030: 7b17 6a5f b700 0000 0000 0001 0000 0000 {.j_............
00000040: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000050: 0000 0000 0000 0000 0000 0000 0000 0003 ................
### /var/oled/diary-forensics/prod-corruption-20260926T193120Z/default_configs.db.corrupt
size=12288 mtime=2026-09-26 19:31:20.101826250 +0000
00000000: 5351 4c69 7465 2066 6f72 6d61 7420 3300 SQLite format 3.
00000010: 1000 0101 0040 2020 0000 0003 0017 0303 .....@ ........
00000020: 0013 aa23 cbc9 0272 a982 f13d dea7 0fbe ...#...r...=....
00000030: 1559 2993 ea00 0000 0000 0001 0000 0000 .Y).............
00000040: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000050: 0000 0000 0000 0000 0000 0000 0000 0003 ................
### /var/oled/diary-forensics/iap-ebpf/default_configs.db.corrupt.20260917T131711Z
size=12288 mtime=2026-09-17 08:05:24.605798898 +0000
00000000: 5351 4c69 7465 2066 6f72 6d61 7420 3300 SQLite format 3.
00000010: 1000 0101 0040 2020 0000 0003 0017 0303 .....@ ........
00000020: 0013 c10b f5c1 a02d 6581 8e43 e6b7 068f .......-e..C....
00000030: 920f 5f78 ee00 0000 0000 0001 0000 0000 .._x............
00000040: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000050: 0000 0000 0000 0000 0000 0000 0000 0003 ................E9 gcloud stderr의 TLS 오류 누적 (09-25 복구 전까지)pre-recovery-20260925T091158Z/
### /var/oled/diary-forensics/pre-recovery-20260925T091158Z/iap-stderr.log
159429 malformed
284 SSL: WRONG_VERSION_NUMBER
139 SSL: RECORD_LAYER_FAILURE
88 SSL: UNEXPECTED_MESSAGE
6 SSL: EXCESSIVE_MESSAGE_SIZE
--- sample UNEXPECTED_MESSAGE context
17906- File "/home/opc/google-cloud-sdk/platform/bundledpythonunix/lib/python3.14/ssl.py", line 1088, in _create
17907- self.do_handshake()
17908- ~~~~~~~~~~~~~~~~~^^
17909- File "/home/opc/google-cloud-sdk/platform/bundledpythonunix/lib/python3.14/ssl.py", line 1384, in do_handshake
17910- self._sslobj.do_handshake()
17911- ~~~~~~~~~~~~~~~~~~~~~~~~~^^
17912:ssl.SSLError: [SSL: UNEXPECTED_MESSAGE] unexpected message (_ssl.c:1082)E10 사용한 bpftrace 프로그램prod-fdrace.bt
// PROD: diary-db-iap-tunnel gcloud process fd lifecycle / file I/O / raw-fd use-after-close tracer (tgid = $1)
// 목적: "close된 socket fd 번호가 sqlite 파일로 재사용되고, 그 fd에 TLS record가 써지는가"를
// syscall 단위로 기록한다.
config = { max_strlen = 128 }
BEGIN { printf("=== fdrace tracer started, target pid=%d ===\n", $1); }
// ---- fd 생성 ----
tracepoint:syscalls:sys_enter_openat /pid == $1/ { @fn[tid] = str(args.filename); }
tracepoint:syscalls:sys_exit_openat /pid == $1 && has_key(@fn, tid)/ {
printf("%lu OPEN tid=%d fd=%d %s\n", nsecs, tid, args.ret, @fn[tid]);
delete(@fn, tid);
}
tracepoint:syscalls:sys_exit_socket /pid == $1/ { printf("%lu SOCKET tid=%d fd=%d\n", nsecs, tid, args.ret); }
tracepoint:syscalls:sys_exit_accept4 /pid == $1/ { printf("%lu ACCEPT tid=%d fd=%d\n", nsecs, tid, args.ret); }
tracepoint:syscalls:sys_exit_accept /pid == $1/ { printf("%lu ACCEPT tid=%d fd=%d\n", nsecs, tid, args.ret); }
tracepoint:syscalls:sys_exit_dup /pid == $1/ { printf("%lu DUP tid=%d fd=%d\n", nsecs, tid, args.ret); }
// ---- fd 해제 ----
tracepoint:syscalls:sys_enter_close /pid == $1/ { printf("%lu CLOSE tid=%d fd=%d\n", nsecs, tid, args.fd); }
tracepoint:syscalls:sys_enter_shutdown /pid == $1/ { printf("%lu SHUTDN tid=%d fd=%d how=%d\n", nsecs, tid, args.fd, args.how); }
// ---- write/read syscall 진입 시 fd 기록 (vfs_* 에는 fd가 없으므로 tid로 연결) ----
tracepoint:syscalls:sys_enter_write /pid == $1/ { @wfd[tid] = args.fd; }
tracepoint:syscalls:sys_enter_pwrite64 /pid == $1/ { @wfd[tid] = args.fd; }
tracepoint:syscalls:sys_exit_write /pid == $1/ { delete(@wfd, tid); }
tracepoint:syscalls:sys_exit_pwrite64 /pid == $1/ { delete(@wfd, tid); }
tracepoint:syscalls:sys_enter_read /pid == $1/ { @rfd[tid] = args.fd; }
tracepoint:syscalls:sys_enter_pread64 /pid == $1/ { @rfd[tid] = args.fd; }
tracepoint:syscalls:sys_exit_read /pid == $1/ { delete(@rfd, tid); }
tracepoint:syscalls:sys_exit_pread64 /pid == $1/ { delete(@rfd, tid); }
// ---- 일반 파일(S_IFREG)에 대한 write: sqlite 정상 write + 이상 write 모두 ----
kprobe:vfs_write /pid == $1/ {
$f = (struct file *)arg0;
if (($f->f_inode->i_mode & 0xF000) == 0x8000) {
$h0 = *(uint8 *)uptr(arg1);
printf("%lu FWRITE tid=%d fd=%d ino=%lu pos=%ld len=%lu head=%r\n",
nsecs, tid, @wfd[tid], $f->f_inode->i_ino, *(int64 *)arg3, arg2,
buf(uptr(arg1), arg2 < 16 ? arg2 : 16));
if ($h0 == 0x17 || $h0 == 0x15 || $h0 == 0x16) {
printf("!!! TLS-LOOKING WRITE TO REGULAR FILE !!! tid=%d comm=%s\nustack:%s\nkstack:%s\n",
tid, comm, ustack(30), kstack(15));
}
}
}
// ---- 일반 파일 read (OpenSSL이 TLS header 5 bytes를 파일에서 읽는지 확인) ----
kprobe:vfs_read /pid == $1/ {
$f = (struct file *)arg0;
if (($f->f_inode->i_mode & 0xF000) == 0x8000 && arg2 < 4096) {
printf("%lu FREAD tid=%d fd=%d ino=%lu pos=%ld len=%lu\n",
nsecs, tid, @rfd[tid], $f->f_inode->i_ino, *(int64 *)arg3, arg2);
if (arg2 == 5) {
printf("!!! 5-BYTE READ FROM REGULAR FILE (TLS record header size) !!! tid=%d\nustack:%s\n",
tid, ustack(30));
}
}
}
// fdrace.bt 보강: 모든 fd I/O의 (tid, fd)와 EBADF(닫힌 fd 사용)를 기록한다.
// 분석: DB 파일 generation의 fd를 opener가 아닌 thread가 I/O 하면 = race 적중.
tracepoint:syscalls:sys_enter_read,
tracepoint:syscalls:sys_enter_write,
tracepoint:syscalls:sys_enter_recvfrom,
tracepoint:syscalls:sys_enter_sendto,
tracepoint:syscalls:sys_enter_recvmsg,
tracepoint:syscalls:sys_enter_sendmsg,
tracepoint:syscalls:sys_enter_pread64,
tracepoint:syscalls:sys_enter_pwrite64
/pid == $1/ { printf("%lu IO tid=%d fd=%d %s\n", nsecs, tid, args.fd, probe); @iofd[tid] = args.fd; }
tracepoint:syscalls:sys_exit_read,
tracepoint:syscalls:sys_exit_write,
tracepoint:syscalls:sys_exit_recvfrom,
tracepoint:syscalls:sys_exit_sendto,
tracepoint:syscalls:sys_exit_recvmsg,
tracepoint:syscalls:sys_exit_sendmsg,
tracepoint:syscalls:sys_exit_pread64,
tracepoint:syscalls:sys_exit_pwrite64
/pid == $1/ {
if (args.ret == -9) {
printf("%lu EBADF tid=%d fd=%d %s\n", nsecs, tid, @iofd[tid], probe);
if ((int64)@iofd[tid] >= 0) { printf("!!! RAW-FD USE-AFTER-CLOSE !!! tid=%d fd=%d\nustack:%s\n", tid, @iofd[tid], ustack(40)); }
}
delete(@iofd, tid);
}
END { clear(@fn); clear(@wfd); clear(@rfd); clear(@iofd); }용어집
- fd (file descriptor)
- process가 연 파일이나 socket을 가리키는 작은 정수. Linux는 새로 열 때 비어 있는 가장 작은 번호를 준다.
- use-after-close
- 이미
close()한 fd 번호를 계속 쓰는 버그. 번호가 재사용되면 전혀 다른 파일이나 socket에 읽고 쓰게 된다. - race (경합)
- 두 thread의 실행 순서에 따라 결과가 달라지는 상태. 여기서는 "닫기"와 "읽기"의 순서.
- TLS record
- TLS가 데이터를 나르는 단위. header 5 bytes(type 1, version 2, length 2) 뒤에 본문이 온다. TLS 1.3에서는 alert도
0x17로 암호화되어 나간다. - AEAD tag
- 암호문에 붙는 16 bytes 인증 태그. 이것이 맞지 않으면 복호화 실패로 연결을 끊고 alert를 보낸다.
- SSL_set_fd
- OpenSSL에 socket의 정수 fd를 넘겨주는 함수. OpenSSL은 이후 이 번호로 직접
read()/write()를 한다. - EBADF
- "잘못된 fd" 오류. 닫힌 번호를 쓰면 나지만, 번호가 이미 재사용됐다면 나지 않고 남의 대상에 조용히 쓴다.
- bpftrace
- 커널 eBPF로 syscall과 커널 함수를 추적하는 도구. 여기서는 gcloud PID 하나만 골라 fd 수명과 I/O를 기록했다.
- SSLRequest
- PostgreSQL 클라이언트가 TLS를 쓰겠다고 알리는 8 bytes 요청. 서버는 1 byte(
'S'또는'N')로 답한다. - IAP TCP forwarding
- Google Cloud의 Identity-Aware Proxy가 외부 IP 없는 VM으로 TCP를 중계하는 기능. 클라이언트 쪽은
gcloud compute start-iap-tunnel이 WebSocket over TLS로 연결한다.