[4] Remote snapshot recorder hard-codes DisplayList rendering mode
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::PDFDocumentcheck가 건너뛰어졌습니다. 이 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
Source/WebKit/UIProcess/WebPageProxy.cpp
Patch Details
이번 변경으로 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 경로를 타는 패턴.
Background
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는 실패 값으로 호출됩니다.
Analysis
수정 전에는 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로만 확인됩니다.
Audit directions
-
속성이 하드코딩된 대리 context. Proxy나 recorder graphics context가 하위 paint 코드의 분기 기준이 되는 속성을 하드코딩하는 경우입니다. 지켜져야 할 invariant는, 대리 context가 자신의 명령이 최종적으로 도달할 sink와 동일한 출력 종류 속성을 보고해야 한다는 것입니다. 좁은 범위:
Source/WebKit/WebProcess/GPU/graphics/의 다른RemoteGraphicsContextProxysubclass와 생성 지점을 살펴볼 필요가 있습니다. 생성자 인자로RenderingMode::,ColorSpace::,DrawGlyphsMode::리터럴을 넘기는 곳을 찾고, 각각을 기록이 replay되는 위치와 비교해야 합니다. 더 넓은 범위: 직접 생성하지 않은 context에 대해renderingMode()나renderingPurpose()를 호출하는 WebCore 코드 전반(BackgroundPainter, SVG resource painter, filter mode 선택)입니다. Snapshot, print, display-list 경로의 모든 context 타입이 정확한 값을 보고하는지 확인해야 합니다. 가장 넓은 범위: 기록용 또는 deferred backend가 최종 target을 대신하는 모든 rendering 추상화입니다. Skia의SkPicturerecorder와 PDF/raster canvas의 관계, Chromium의 paint-record playback이 여기에 해당합니다. 단서는 consumer가 나중에 code path 선택에 사용하는 속성을 생성자가 상수로 넘기는 구조입니다. 이때 실제 목적지는 상위 계층에서만 알 수 있습니다. -
Peer renderer 간 UI process relay. Site isolation 환경에서 relay가 sender가 제공한 필드를 한 WebContent process에서 다른 process로 전달하는 경우입니다. 수신 측은 UI process를 신뢰합니다. 그래서 수신 측이 아니라 relay가 모든 필드를 수신 측이 안전하게 처리할 수 있는 범위로 제한해야 한다는 것이 invariant입니다. 좁은 범위:
WebPageProxy.messages.in에서[EnabledBy=SiteIsolationEnabled]message를 검색해 볼 필요가 있습니다. 그중WebPageProxyhandler가 곧바로sendToProcessContainingFrame또는sendWithAsyncReplyToProcessContainingFrame를 호출하는 경우를 찾습니다. 그리고 전달되는 각 enum, rect, identifier가 relay 전에MESSAGE_CHECK를 거치는지 확인해야 합니다. 더 넓은 범위: frame-tree 동기화, remote frame geometry, accessibility relay처럼 broker를 거치는 peer renderer 간 모든 message입니다. 단서는 payload 필드 검증 없이 forwarding 호출 하나만 있는 handler 본문입니다. 이 유형은 UI process를 relay로 쓰는 WebKit 아키텍처와MESSAGE_CHECK관용 패턴에 묶여 있습니다. Chromium은 peer renderer 간 트래픽을 browser 측 frame host를 거쳐 Mojo 타입 기반 검증으로 처리합니다. 따라서 같은 형태가 그대로 적용되지는 않습니다. -
debug
ASSERT로만 허용 범위가 제한되는 enum 파라미터. IPC로 전달되는 enum 파라미터 중에는 수신 측이 의미상 허용 범위를ASSERT로만 강제하는 경우가 있습니다.RemoteSnapshotRecorderProxy의ASSERT(renderingMode == DisplayList || renderingMode == PDFDocument)가 그 예입니다. 가장 좁은 범위에서는Source/WebKit/WebProcess와Source/WebKit/GPUProcess를 먼저 살펴볼 수 있습니다..messages.inhandler에서 도달 가능한 메서드를 대상으로, enum 인자에 걸린ASSERT(를 검색하면 됩니다. 이후 각 지점마다 trust boundary에 release 빌드에서도 동작하는MESSAGE_CHECK가 있는지 확인해야 합니다. 범위를 넓히면 같은 공백이 다른 곳에서도 나타납니다. IPC decoder는 enum 값의 범위만 검증하는데 수신 측은 그중 일부만 지원하는 경우입니다. image buffer purpose, pixel format, display list item 종류 등이 여기에 해당합니다. 가장 넓게 보면 이 패턴은 특정 코드베이스에 국한되지 않습니다. schema 검증이 형식적 유효성(well-formedness)에서 멈추는 RPC framework라면 어디서든 나타날 수 있습니다. 즉 "type 측면에서는 유효하지만 의미상으로는 유효하지 않은 입력"이라는 일반적인 유형에 속합니다. 선언된 모든 멤버를 그대로 받아들이는 protobuf/Mojo enum이나 gRPC 서비스가 대표적인 예입니다. 판별 기준은 단순합니다. 외부에서 전달된 enum에 대해 수신 측이 거는 제약이 debug 전용 assertion 하나뿐이라면 점검 대상으로 볼 수 있습니다.