IndexedDB: Reject requests after transaction commit
IndexedDB 트랜잭션의 커밋 요청 이후 들어오는 IPC를 거부하고, 브라우저가
트랜잭션을 중단한 경우와 구분하는 변경을 제안했습니다. CQ dry run은
통과했지만, 입증된 효과에 비해 복잡성과 유지보수 비용이 크다는 리뷰를
수용해 2026년 9월 2일 CL을 중단(abandoned)했습니다.
문제 설명
Chromium Issue 40791538은
이미 커밋을 시작한 트랜잭션에 renderer가 추가 IPC를 보내면 잘못된 메시지로
처리하는 방안을 제안했습니다. 반면 브라우저가 먼저 트랜잭션을 abort한
경우에는 renderer가 이를 알기 전에 보낸 요청이 도착할 수 있으므로,
해당 요청을 무시해야 한다는 조건도 있었습니다.
기존 Transaction::IsAcceptingRequests()와Connection::GetTransactionAndVerifyState()에는 이 구분을 구현하자는
TODO가 남아 있었습니다. 요청을 받지 않는 트랜잭션이라는 사실만으로는
renderer의 프로토콜 위반인지, 브라우저의 abort와 경합한 정상 요청인지
판단할 수 없었습니다.
조사 중에는 비동기 저장 공간 검사와 관련된 시점 차이도 확인했습니다.
기존 브라우저 측 Transaction::Commit()은 쓰기 크기 추정치가 양수이면CheckCanUseDiskSpace()로 quota를 확인하고, 성공 콜백에서SetCommitFlag()를 호출했습니다. 따라서 quota 검사가 끝나기 전에는
커밋 요청을 받았어도 IsAcceptingRequests()가 계속 참일 수 있었습니다.
다만 이 차이가 일반 웹 페이지에서 커밋 이후 요청을 추가할 수 있다는
뜻은 아닙니다. IndexedDB 명세는
기존 요청의 완료를 허용하면서 새 요청은 받지 않도록 규정하고,
기존 Blink 구현은
IPC를 보내기 전에 이미 state_ = kCommitting으로 전환합니다.
기존 WPT도
커밋 직후나 후속 요청 콜백에서 새 요청을 시도하면TransactionInactiveError가 발생하는 동작을 검증하고 있었습니다.
남은 쟁점은 오동작하거나 손상된 renderer가 직접 보내는 IPC의 처리였고,
이번 CL에서는 그로 인한 사용자 오류나 보안 취약점을 입증하지 못했습니다.
해결 내용
아래는 병합되지 않은 최종 Patch Set 6에 구현했던 내용입니다.
1. 커밋 요청 수신과 실제 커밋 진행을 구분
Transaction에 renderer_commit_requested_를 추가하고, 유효한Commit() 요청을 받으면 quota 검사 전에 설정했습니다.IsAcceptingRequests()는 이 값을 함께 검사해 비동기 검사 중에도
새로운 일반 요청을 받지 않도록 했습니다.
bool IsAcceptingRequests() const {
return !renderer_commit_requested_ && !is_commit_pending_ &&
state_ != COMMITTING && state_ != FINISHED;
}이 변경으로 SetCommitFlag()가 IsAcceptingRequests()를 그대로 사용하면
정상적인 quota 검사 완료 후에도 커밋을 진행하지 못하게 됩니다. 따라서
이 메서드는 abort 여부, 기존 커밋 대기 여부, COMMITTING 및 FINISHED
상태를 직접 확인하도록 조정했습니다. 이미 큐에 들어간 작업은 계속 실행해
커밋 요청이 기존 작업의 완료를 막지 않도록 했습니다.
2. 커밋 이후 IPC와 abort 이후 IPC를 구분
ReportBadMessageIfRequestAfterCommit()을 추가해renderer_commit_requested_ && !aborted_인 경우에만 bad message를
보고하도록 했습니다.
| 상황 | 제안한 처리 |
|---|---|
| 커밋 요청 이후 새 일반 요청 도착 | 요청 거부 및 bad message 보고 |
| 브라우저의 abort 이후 요청 도착 | bad message를 보고하지 않고 무시 |
| 커밋 요청 후 브라우저가 abort한 뒤 요청 도착 | abort와 경합한 요청으로 처리 |
커밋 2단계 실패 후 aborted_가 설정되지 않은 상태에서 요청 도착 |
커밋 요청 수신 이력을 기준으로 bad message 보고 |
이 검사를 Transaction::Put(), Transaction::Commit(),Connection::GetTransactionAndVerifyState()에 연결했습니다.
renderer가 보내는 Connection::Abort()도 공통 상태 검증을 거치도록
바꿨습니다. 보고 사유는 BadMessageReason::kTransactionRequestAfterCommit으로
추가하고 UMA enum 메타데이터와 값을 맞췄습니다.
3. 진행 중인 인덱스 생성 작업의 완료 허용
트랜잭션에는 일반 요청뿐 아니라 인덱스 생성을 위한 preemptive 작업도
있습니다. 커밋 요청 이후 모든 IPC를 막으면 이미 시작한 인덱스 생성에
필요한 후속 메시지까지 차단할 수 있었습니다.
이를 위해 IsAcceptingPreemptiveRequests()를 추가했습니다. 일반 요청을
받을 수 있거나, 트랜잭션이 STARTED이고 대기 중인 preemptive event가
있으면 후속 처리를 허용했습니다. 이 검사는 다음 경로에 적용했습니다.
SetIndexKeys()와SetIndexKeysDone()- preemptive cursor의
Continue()
이 예외 처리는 기존 작업을 완료시키는 데 필요했지만, 커밋 상태 검사 하나가
인덱스 생성과 cursor 처리까지 영향을 준다는 점도 드러냈습니다.
최종 패치는 7개 파일에서 396줄을 추가하고 29줄을 삭제했으며,
이 중 transaction_unittest.cc에 추가한 코드가 334줄이었습니다.
4. 리뷰 결과와 CL 중단
Evan Stade의 리뷰는
추가된 상태와 예외 처리 때문에 코드가 복잡해지고 정확성을 검증하기
어려워졌다고 지적했습니다. 새 단위 테스트 다수도 사용자에게 중요한
동작보다 구현 변경 자체를 감지하는 테스트에 가깝다는 평가였습니다.
또한 실제로 악용할 수 있는 보안 결함을 막는 가치와, 이미 잘못 동작하는
renderer를 더 적극적으로 종료하는 가치를 구분해야 한다고 설명했습니다.
악성 renderer는 감지에 사용되는 메시지를 보내지 않을 수도 있으므로,
bad message 보고를 추가하는 것만으로 보안상 이득이 입증되지는 않습니다.
후속 답변에서
기존 Blink와 WPT가 웹에 노출되는 동작을 이미 다루고 있고, 남은 브라우저
측 차이의 실제 영향은 확인하지 못했다는 점을 인정했습니다.
quota 검사 중 mutation이 들어오는 더 좁은 문제는 영향부터 별도로
조사할 필요가 있다고 정리하고 CL을 중단했습니다. 이 가능성을 이번 CL이
해결한 버그나 확인된 취약점으로 결론내리지는 않았습니다.
테스트 방법
1. LevelDB와 SQLite 단위 테스트
content/browser/indexed_db/instance/transaction_unittest.cc의TransactionTest에 9개 테스트를 추가했습니다. 기존 parameterized suite를
사용하므로 LevelDB와 SQLite에서 각각 같은 시나리오를 검증하도록 했습니다.
| 검증 대상 | 확인한 동작 |
|---|---|
| quota 검사 대기 중 커밋 요청 | 새 일반 요청은 받지 않고 기존 queued task는 실행 |
| 커밋 이후 transaction IPC | Put() 요청에 bad message 보고 |
| 커밋 이후 connection IPC | Count() 및 renderer의 Abort()에 bad message 보고 |
| 브라우저 abort 이후 IPC | transaction의 Put()과 connection의 Count()를 bad message 없이 처리 |
| 커밋 2단계 실패 | FINISHED이지만 aborted_가 거짓인 경우에도 커밋 요청 이력을 유지 |
| 인덱스 생성 후속 처리 | quota 검사 대기 중 preemptive cursor continuation과 SetIndexKeysDone() 허용 |
Mojo 경로의 테스트에는 mojo::test::BadMessageObserver를 사용해
bad message 보고 여부를 확인했습니다. 인덱스 생성 테스트는 transaction의
내부 상태와 preemptive event 수를 제어해야 했고, 이런 구현 의존성이
리뷰에서 지적한 유지보수 비용과 연결됐습니다.
2. Gerrit CQ 실행 결과
Gerrit의 CQ 기록에
남은 실행 결과는 다음과 같습니다. 날짜는 한국 시간 기준입니다.
- 2026년 9월 1일, Patch Set 5:
mac-rel의content_unittests에서CommitAllowsPreemptiveCursorContinuationWhileQuotaCheckPending가
LevelDB와 SQLite 모두 실패했습니다. - 2026년 9월 1일, Patch Set 6 첫 실행:
ios-simulator의ios_chrome_unittestsflakiness 검사 단계에서 테스트 결과가 생성되지 않아
CQ가 실패했습니다. - 2026년 9월 1일, Patch Set 6 재실행:
CQ dry run을 통과했습니다. - 2026년 9월 2일: 코드 리뷰 후 CL을 중단했습니다. Chromium main에는
병합되지 않았습니다.
위 내용은 CL에 남은 테스트 코드와 CQ 실행 기록을 정리한 것입니다.
이 기여 기록을 작성하면서 Chromium 테스트를 새로 실행한 것은 아닙니다.
배운 점
- 오래된 TODO나 이슈의 제안이 현재도 필요한지 먼저 확인해야 합니다.
작업 시작 전에 담당자에게 이슈의 유효성과 기대 효과를 확인하면
구현 방향을 더 일찍 조율할 수 있습니다. - 웹 API의 상태 전이를 담당하는 Blink와 IPC를 처리하는 브라우저의
책임을 구분해야 합니다. 내부 상태 차이를 발견했다면 실제로 어떤
호출자가 도달할 수 있고 어떤 문제가 생기는지까지 확인해야 합니다. - 비동기 검사에서는 요청 수신 시점과 작업 실행 시점이 다를 수 있습니다.
새 요청을 막는 변경이 기존 작업의 완료까지 막지 않는지 살펴야 합니다. - 상태 플래그를 추가하는 비용에는 모든 호출 경로의 검증과 예외 처리도
포함됩니다. 이번에는 quota 검사, abort, 커밋 실패, 인덱스 생성이
연결되면서 최초 의도보다 변경 범위가 커졌습니다. - 테스트 수와 CQ 통과만으로 변경의 필요성을 증명할 수는 없습니다.
내부 구현에 강하게 결합한 테스트를 늘리기 전에 보호할 동작과
실제 실패 사례를 먼저 명확히 해야 합니다. - 리뷰를 통해 변경의 효과가 비용을 정당화하지 못한다고 판단했다면
CL을 중단하는 것도 타당한 결론입니다. 남은 의문은 입증된 사실과
구분해 기록하고, 더 좁은 조사 과제로 남길 수 있습니다.