[Site Isolation] Add multi-process BFCache restoration with Site Isolation
Component: WebKit | fceeb85
BFCache는 back/forward navigation을 즉시 처리할 수 있도록 페이지 전체 스냅샷을 메모리에 보존합니다. Site Isolation 환경에서는 cross-origin iframe이 별도의 WebContent process에서 실행되기 때문에, 캐시된 페이지 하나가 N개의 process에 걸쳐 존재할 수 있습니다. SuspendedPageProxy는 UI process에서 suspended 상태를 보관하는 객체이고, BrowsingContextGroup은 어떤 WebPageProxy가 어떤 process group에 속하는지 추적하면서 RemotePageProxy 참조를 subframe process로 라우팅합니다.
Source/WebKit/UIProcess/SuspendedPageProxy.cpp
Source/WebKit/UIProcess/BrowsingContextGroup.cpp
기존에 단일 IPC였던 SetIsSuspended는 별도의 SuspendWithFrameItem과 RestoreWithFrameItem 메시지로 분리되었습니다. 이제 unsuspend()는 RestoreWithFrameItem을 모든 subframe process에 fan-out 방식으로 전송하고, MainRunLoopSuccessCallbackAggregator를 통해 결과를 수집합니다. 어느 한 process라도 복원에 실패하면 reload(ExpiredOnly)를 통한 전체 재로드가 트리거됩니다. Navigation 중 process 선택 방식도 함께 바뀌었습니다. Suspended page의 browsing context group을 새 navigation에서 그대로 재사용하여 기존 RemotePageProxy handle이 유효하게 유지되는데, 이것이 바로 addPage()가 이미 존재하는 page를 허용하도록 변경된 이유입니다.
Before (single-process BFCache):
SuspendedPageProxy
├─► SetIsSuspended(true) ──────► WebPage [main frame process]
└─► SetIsSuspended(false) ──────► WebPage [main frame process]
After (multi-process BFCache, Site Isolation):
SuspendedPageProxy
├─ startSuspension():
│ └─► SuspendWithFrameItem(id) ─────► WebPage [main frame process]
│ └─◄ didSuspend: bool ◄────────┘
│
└─ unsuspend(mainFrameItemID):
├─► SetIsSuspended(false) ──────────► WebPage [main frame process]
│
├── create MainRunLoopSuccessCallbackAggregator
│
└─► forEachRemotePage:
└─► RestoreWithFrameItem(id) ─► WebPage [subframe process 1]
└─► RestoreWithFrameItem(id) ─► WebPage [subframe process 2]
└─◄ success/fail ◄─────────┘
└─ [if any fail → page.reload(ExpiredOnly)]
Significance
이제 BFCache 복원은 N개의 WebContent process에 걸쳐 suspend/restore를 동시에 조율하며, 그 결과 Site-Isolated된 캐시 페이지가 back/forward navigation을 거쳐도 살아남을 수 있게 됩니다. 새로 추가된 IPC 경로, aggregated failure handling, process 라우팅 변경 사항 모두 cross-origin isolation 보장이 걸려 있는 multi-process 경계에 위치합니다.
Audit directions
Narrow: partial-restoration window를 살펴볼 필요가 있습니다. SetIsSuspended(false)는 aggregator가 생성되기 전, 그리고 어떤 RestoreWithFrameItem도 전송되기 전에 main frame process에 먼저 도달합니다. 이후 subframe 하나가 실패하면, aggregator는 이미 live 상태가 된 main frame에 대해 reload(ExpiredOnly)를 발동시킵니다. 즉 main frame은 이미 복원되었는데 subframe은 알 수 없는 상태에 놓이는 구간이 존재합니다. 이 gap에서 실행되는 navigation이나 script는 mixed-state 페이지를 대상으로 동작하게 됩니다. 이 패턴의 forward-facing 버전으로는, WebKit/UIProcess/ 내 다른 multi-process fan-out 복원 로직들도 함께 점검할 필요가 있습니다. 특히 어느 한 참여자가 커밋된 시점이, 작업 성공 여부를 판단하는 aggregator가 생성되기 이전인 경우를 찾아야 합니다. send/sendWithAsyncReply 호출이 이를 gate해야 할 aggregator의 create보다 텍스트상 앞에 위치하는 지점이 단서입니다.
Wider: multi-process 라우팅 경로에서 invariant가 ASSERT로 격하된 부분을 점검해야 합니다. addPage()는 이제 release build에서 page가 이미 존재하는 경우 조용히 반환되며, "suspended BCG는 정확히 하나의 page만 보유한다"는 주장은 오직 ASSERT(!hasMultiplePages())만으로 지켜지고 있습니다. Re-entrant navigation이나 suspension과 새 page attach 사이의 race가 이 조건을 위반하면, 관찰 가능한 에러 없이 process 라우팅이 손상될 수 있습니다. 이는 이식성 있는 패턴이므로, BrowsingContextGroup, WebProcessProxy, WebPageProxy 전반에서 security-relevant한 라우팅 결정이 debug-only assertion 하나에만 의존하는 조기 반환(early-return) 지점들을 훑어볼 필요가 있습니다. 리뷰 시 단서는 if (...) { ASSERT(...); return; } 형태이면서, invariant를 실제로 지키는 것이 조건문이 아니라 assertion인 경우입니다.
Widest: BFCache entry의 lifetime과 process 재사용 관계를 살펴봐야 합니다. removeEntriesForPageAndProcess가 main frame으로만 범위가 좁혀지면서, cross-site iframe process swap이 더 이상 destination process에서 main-frame BFCache entry를 축출하지 않게 되었습니다. 그 결과 나중에 다른 origin용으로 재활용되는 process에 stale entry가 쌓일 가능성이 있습니다. 복원 과정에서 browsing-context-group이 재사용되고, suspension 이전에 확립된 RemotePageProxy 참조가 restore 동안 그대로 바인딩되어 유지된다는 점과 결합하면, 질문은 다음과 같이 일반화됩니다. (page, process) 조합을 key로 삼는 모든 cache에 대해, origin 간 process 재활용이 eviction과 짝을 이루고 있는지 점검해야 합니다. 우선적으로 살펴볼 지점은 BFCache hit로 시작했다가 mid-flight에 network load로 전환되는 navigation으로, subframe process가 잘못된 context group으로 라우팅될 가능성이 가장 높은 형태입니다.