← All reports

[4] Remote snapshot recorder hard-codes DisplayList rendering mode

LowWebKit remote snapshottingLogicError

87adf3a

Low. 결함 자체는 memory safety가 아니라 PDF 출력의 rendering 정확도 문제입니다. 그럼에도 security 목록에 포함된 이유는 sender가 선택하는 enum이 새로 추가되었기 때문입니다. 이 값은 WebContent → UI → WebContent relay를 거쳐 전달됩니다. 다만 release 모드 check를 통해 지원되는 두 값으로 제한됩니다.

Paint 코드는 출력이 최종적으로 어떤 surface에 도달하는지에 따라 전략을 바꾸는 경우가 많습니다. 그래서 대리 역할을 하는 drawing context는 자신의 목적지를 정확하게 보고해야 합니다. WebKit의 remote snapshotting에서는 WebContent process가 paint 명령을 RemoteSnapshotRecorderProxy에 기록합니다. 이 context는 drawing 연산을 GPU process로 스트리밍합니다. GPU process는 이를 bitmap 또는 PDF로 replay합니다. 이때 paint 코드는 context의 RenderingMode(display list 또는 PDF document)를 조회해 drawing 경로를 선택합니다. 즉 보고되는 mode가 실제 sink와 일치한다는 전제가 깔려 있습니다.

관전 포인트: site isolation 환경에서 compromised renderer는 다른 site의 frame 기록에 사용될 paint mode를 지원되는 두 값 중에서 고를 수 있습니다. 출력 결과의 모양은 달라지지만 memory safety에는 영향이 없습니다. 오래 남는 가치는 relay 단계의 검증 패턴에 있습니다.

Remote snapshotting을 활성화하면 PDF drawing API test 두 개가 실패합니다. RemoteSnapshottingEnabled 상태에서는 콘텐츠가 RemoteSnapshotRecorderProxy에 그려지고, 이후 GPU process가 이를 CG PDF context로 replay합니다. 문제는 recorder가 항상 RenderingMode::DisplayList를 보고했다는 점입니다. 그 결과 Page::preferredFilterRenderingModes, BackgroundPainter::paintFillLayer, LegacyRenderSVGResourceGradient의 RenderingMode::PDFDocument check가 건너뛰어졌습니다. 이 check들이 고쳤던 버그(filter: invert() 미적용, background-clip: text가 단색 사각형으로 그려지는 문제)가 다시 나타났습니다. Patch는 recorder 생성 시 rendering mode를 전달하도록 변경했습니다. Snapshot이 PDF로 들어가는 경우에는 PDFDocument, 그 외에는 DisplayList를 사용합니다. 또한 mode를 DrawFrameToSnapshot으로 전달해, site isolation 환경의 cross-site iframe도 같은 방식으로 기록되도록 했습니다.

Source/WebKit/WebProcess/GPU/graphics/RemoteSnapshotRecorderProxy.cpp

-RemoteSnapshotRecorderProxy::RemoteSnapshotRecorderProxy(const FloatRect& initialClip, RemoteRenderingBackendProxy& renderingBackend)
 
- : RemoteGraphicsContextProxy(GraphicsContextState::initialIndeterminate(), initialClip, { }, ColorSpace::SRGB(), DrawGlyphsMode::Normal, std::nullopt, RenderingMode::DisplayList, RemoteGraphicsContextIdentifier::generate(), renderingBackend)
+RemoteSnapshotRecorderProxy::RemoteSnapshotRecorderProxy(const FloatRect& initialClip, RenderingMode renderingMode, RemoteRenderingBackendProxy& renderingBackend)
+ : RemoteGraphicsContextProxy(GraphicsContextState::initialIndeterminate(), initialClip, { }, ColorSpace::SRGB(), DrawGlyphsMode::Normal, std::nullopt, renderingMode, RemoteGraphicsContextIdentifier::generate(), renderingBackend)
{
+ ASSERT(renderingMode == RenderingMode::DisplayList || renderingMode == RenderingMode::PDFDocument);
}
...

Source/WebKit/UIProcess/WebPageProxy.cpp

-void WebPageProxy::drawFrameToSnapshot(FrameIdentifier frameID, const IntRect& rect, RemoteSnapshotIdentifier snapshotIdentifier, CompletionHandler<void(bool)>&& completionHandler)
+void WebPageProxy::drawFrameToSnapshot(IPC::Connection& connection, FrameIdentifier frameID, const IntRect& rect, RemoteSnapshotIdentifier snapshotIdentifier, RenderingMode renderingMode, CompletionHandler<void(bool)>&& completionHandler)
{
 
- sendWithAsyncReplyToProcessContainingFrame(frameID, Messages::WebPage::DrawFrameToSnapshot(frameID, rect, snapshotIdentifier), WTF::move(completionHandler));
+ MESSAGE_CHECK_COMPLETION(WebProcessProxy::fromConnection(connection), renderingMode == RenderingMode::DisplayList || renderingMode == RenderingMode::PDFDocument, completionHandler(false));
+ sendWithAsyncReplyToProcessContainingFrame(frameID, Messages::WebPage::DrawFrameToSnapshot(frameID, rect, snapshotIdentifier, renderingMode), WTF::move(completionHandler));
}

이번 변경으로 recorder는 생성자에서 rendering mode를 전달받게 되었습니다. 각 생성 지점에서 mode를 선택하도록 했고, site isolation relay를 거칠 때는 UI process에서 범위 check를 수행합니다.

Recorder와 생성 지점. RemoteSnapshotRecorderProxy의 생성자는 이제 RenderingMode를 인자로 받습니다. 이 값은 하드코딩된 DisplayList 대신 RemoteGraphicsContextProxy base로 전달됩니다. 이와 함께 mode가 DisplayList 또는 PDFDocument인지 확인하는 debug ASSERT가 추가되었습니다. RemoteRenderingBackendProxy::createSnapshotRecorder에도 같은 인자가 추가되었습니다. WebPage::drawToSnapshot은 PDFDocument를, WebPage::takeRemoteSnapshot은 DisplayList를 전달합니다. WebPageCocoa.mm에서는 rect를 넘기는 지점이 DisplayList를, mediaBox를 넘기는 지점이 PDFDocument를 사용합니다. 한편 WebPageIOS.mm에서는 두 지점 모두 mediaBox를 넘기는데, 첫 번째는 DisplayList, 두 번째는 PDFDocument를 사용합니다.

Relay. Site isolation 환경에서 WebPage::paintRemoteFrameContents는 Messages::WebPageProxy::DrawFrameToSnapshot에 context.renderingMode()를 담아 전송합니다. UI process는 이 값을 Messages::WebPage::DrawFrameToSnapshot에 실어 cross-site frame을 호스팅하는 process로 relay합니다. 수신 측은 WebPage::drawFrameToSnapshot에서 이 값으로 자체 recorder를 생성합니다. WebPageProxy::drawFrameToSnapshot은 이제 IPC::Connection&를 인자로 받으며, 위에 나온 MESSAGE_CHECK_COMPLETION이 추가되었습니다. 두 .messages.in 파일에도 해당 필드가 추가되었습니다. 또한 DrawingToPDF.mm에서는 enableSiteIsolation이 enableFeature로 일반화되었습니다. filter-invert와 background-clip test의 ...WithRemoteSnapshotting 변형도 함께 추가되었습니다.

Wrapper context가 자신이 보고하는 출력 종류를 하드코딩해, 그 값으로 분기하는 하위 코드가 잘못된 fidelity 경로를 타는 패턴.

RenderingMode. GraphicsContext나 ImageBuffer가 최종적으로 어떤 종류의 surface에 그리는지를 나타내는 WebCore enum입니다. DisplayList는 연산을 기록해 두었다가 나중에 replay한다는 의미이고, PDFDocument는 PDF context로 향한다는 의미입니다. Paint 코드는 GraphicsContext::renderingMode()를 조회해 drawing 전략을 선택합니다. 예를 들어 CSS filter나 text로 clip된 background를 그릴 때, vector PDF 출력과 raster surface에서 서로 다른 방식을 사용합니다.

Remote snapshotting. RemoteSnapshottingEnabled가 켜져 있으면 WebContent process는 process 내부에서 직접 그리지 않습니다. 대신 RemoteSnapshotRecorderProxy(RemoteGraphicsContextProxy의 subclass)에 그립니다. 이후 GPU process가 이 stream을 최종 sink인 bitmap 또는 CG PDF context로 replay합니다. WebPage의 m_remoteSnapshotState는 활성 recorder와 MainRunLoopSuccessCallbackAggregator를 보관합니다. 이 aggregator는 관련된 모든 frame의 drawing이 끝나는 시점에 완료됩니다.

Site isolation relay. Site isolation 환경에서는 cross-site iframe이 별도의 WebContent process에 위치합니다. Root process가 snapshot을 그리는 도중 remote frame을 만나면(WebPage::paintRemoteFrameContents), UI process로 DrawFrameToSnapshot을 전송합니다. 그러면 WebPageProxy::drawFrameToSnapshot이 해당 frame을 포함한 process로 이를 전달합니다. 그 process는 같은 snapshot identifier로 기록을 수행합니다.

MESSAGE_CHECK_COMPLETION. UI process에서 사용하는 validator 관용 패턴입니다. 조건을 만족하지 못하면 전송한 process를 비정상 동작으로 간주해 종료시킵니다. 이때 completion handler는 실패 값으로 호출됩니다.

수정 전에는 RemoteSnapshotRecorderProxy가 기록의 목적지와 관계없이 항상 RenderingMode::DisplayList로 RemoteGraphicsContextProxy base를 생성했습니다. PDF 출력으로 향하는 콘텐츠도 이 recorder에 그려졌기 때문에, renderingMode()는 DisplayList를 보고했습니다. 그 결과 PDFDocument 여부로 분기하는 WebCore 코드는 모두 non-PDF 경로를 탔습니다. 즉 context가 자신의 출력 종류를 잘못 보고한 logic error에 해당합니다. Commit message는 이런 분기 지점으로 Page::preferredFilterRenderingModes, BackgroundPainter::paintFillLayer, LegacyRenderSVGResourceGradient를 언급합니다. 다만 이 함수들의 소스는 제공된 context에 포함되어 있지 않으므로, 해당 출처 표기는 그대로 인용합니다. 새로 추가된 ...WithRemoteSnapshotting test는 PDF 출력에서의 filter: invert()와 background-clip: text를 대상으로 합니다. Commit message에 따르면 증상은 filter가 적용되지 않는 문제와 text clip이 단색 사각형으로 그려지는 문제였습니다.

Site isolation 경로에는 한 단계 더 깊은 곳에 같은 결함이 있었습니다. WebPage::drawFrameToSnapshot 역시 cross-site frame의 recorder를 생성할 때 기본값으로 DisplayList를 사용했습니다. 그래서 fix는 두 번의 IPC hop을 거쳐 mode를 전달해야 합니다.

  WebContent A (root)          UIProcess                          WebContent B (cross-site frame)
  ───────────────────          ─────────                          ──────────────────────────────
  paintRemoteFrameContents
   DrawFrameToSnapshot(frame, rect, id, mode) ─►
                               MESSAGE_CHECK: mode in {DisplayList, PDFDocument}
                               forward(frame, rect, id, mode) ───► new RemoteSnapshotRecorderProxy(mode)
                                                                     (ASSERT only; relies on relay check)

Attack surface 측면의 변화는 기존 relay에 새 필드 WebCore::RenderingMode renderingMode가 추가되었다는 점입니다. 이 relay는 WebContent에서 UI(WebPageProxy::DrawFrameToSnapshot)를 거쳐 다른 WebContent process(WebPage::DrawFrameToSnapshot)로 이어집니다. 이제 송신 process가 다른 site의 process가 recorder를 생성할 때 사용할 mode를 고르게 됩니다. 그리고 그 mode는 renderingMode()를 기준으로 하는 모든 WebCore paint 결정으로 흘러갑니다.

UI process는 값이 DisplayList 또는 PDFDocument인지 강제합니다. 반면 수신 측의 ASSERT는 debug 전용이며, relay 단계의 check에 의존합니다. 문제는 허용된 두 mode 중 어느 쪽이 실제 sink와 일치하는지 확인하는 곳이 없다는 점입니다. 따라서 compromised sender는 cross-site frame에 대해 둘 중 어느 쪽이든 고를 수 있습니다. 만약 relay check가 없거나 우회된다면, 다른 RenderingMode 값(예: accelerated 또는 unaccelerated image-buffer mode)이 recorder에 도달할 가능성이 있습니다. 이 recorder는 애초에 그런 값을 고려해 설계되지 않았습니다. 그 경우 RemoteGraphicsContextProxy와 GPU 측 replay에서 어떤 동작이 발생할지는 제공된 context로는 판단할 수 없습니다.

Memory safety 문제로서의 exploitability는 입증되지 않았습니다. 수정 전 결함은 PDF rendering 정확도 문제였습니다. 수정 후 남는 primitive도 rendering 출력에만 영향을 주는, 두 mode 중 하나를 고르는 선택권에 그칩니다. 남아 있는 점검 질문은 하위 코드가 보고된 mode와 실제 sink의 일치를 가정하는지입니다. 대상은 GPU 측 replay와 renderingMode() 기반 paint 경로입니다. 핵심은 그 가정이 drawing 선택이 아니라 memory layout이나 buffer 크기 계산에 관여하는지 여부입니다. 만약 그렇다면 compromised sender가 bitmap으로 향하는 snapshot에 PDFDocument를 고르는 경우, 또는 그 반대의 경우가 단순한 외형 문제를 넘어설 가능성이 있습니다. 다만 제공된 context에는 그런 방향을 시사하는 근거가 없습니다. 버그는 sandbox 내부의 WebContent와 GPU process에 위치합니다. 또한 새 필드는 WebContent-to-UI 경계를 넘는 지점에서 범위 check를 거칩니다. 따라서 어느 쪽도 sandbox escape로 이어지지 않습니다.

원래 버그는 memory safety나 origin 경계를 약화시키지 않습니다. Security 관점에서 의미 있는 부분은 cross-site relay 위에 놓인, sender가 제어하는 enum입니다. 이 check가 유지하는 invariant는 다음과 같습니다. Compromised WebContent process가 다른 site의 recorder를 유도할 수 있는 범위는 그 recorder가 지원하는 mode로 한정됩니다.

Insight: 실제 context를 대신하는 proxy context는 paint 코드가 분기에 사용하는 속성(rendering mode, color space, accelerated 여부)을 정확하게 보고해야 합니다. 그렇지 않으면 feature flag로 painting이 out-of-process로 옮겨가는 순간 동작이 조용히 어긋납니다. 기존의 non-remote CSSFilterInvertAppliedOnPDF, BackgroundClipText test를 보면, in-process 경로는 이미 이 동작들을 처리하고 있었던 것으로 보입니다. 한편 site isolation은 snapshot 단위의 모든 parameter를 cross-process relay로 바꿔 놓습니다. 예전에는 로컬 상태에서 암묵적으로 정해지던 값이 이제 IPC로 전달되고, 그 범위를 제한해야 하는 책임은 UI process로 넘어갑니다. 눈여겨볼 패턴은 이렇습니다. Correctness fix의 일부로, 어떤 값이 로컬에서 유도되던 방식에서 다른 WebContent process로부터 수신되는 방식으로 바뀝니다. 이때 검증은 relay에 추가되지만, 수신 측에서는 ASSERT로만 확인됩니다.