跳到内容

站点隔离

在不包含任何 iframe 和弹出窗口的文档加载最简单情况下,我们有一个 WebPageProxy 和一个 WebProcess,该进程中有一个 Page,并且该 Page 有一个 Frame,该 Frame 是一个 LocalFrame,如下图所示:

Frame Diagram 1

如果该文档包含从同一站点加载的 2 个 iframe,那么这些 Frame 会按照帧树结构排列,如下图所示:

Frame Diagram 2

如果关闭站点隔离,当 example.com 加载来自不同站点(如 a.com)的 iframe 时,我们会在同一进程中加载它。

Frame Diagram 3

然而,如果启用站点隔离,我们会将 a.com 的内容放入与 example.com 的内容不同的进程中。

Frame Diagram 4

远程帧

远程帧 (RemoteFrame) 用于指示帧树 (FrameTree) 中一个拥有来自不同进程的 Document 的 Frame 的位置。这是必需的,因为 JavaScript 即使无法访问第三方 Frame 中的 DOM,也能遍历帧树。帧树顶部的 LocalFrame 主帧所在的进程通过 IPC 与 WebPageProxy 通信,但所有其他进程都在帧树顶部有一个 RemoteFrame,它们直接与 RemotePageProxy 通信,后者主要只是将消息转发给 WebPageProxy。

如果主帧调用 window.open(‘https://b.com'),那么我们需要有 3 个进程。

Frame Diagram 5

每个进程都需要拥有帧树 (FrameTree) 的完整表示,因为 JavaScript 能够通过使用诸如 window.parent.opener.frames[1] 这样的属性来遍历帧树,如果它找到一个具有正确源的 Frame,就可以访问其 DOM。

浏览上下文组

浏览上下文组 (BrowsingContextGroup) 代表所有可能通过 opener 关系连接的 WebPageProxy 对象,以及它们当前用于其内容的全部进程。当一个 Frame 导航到新域时,整个帧树 (FrameTree) 森林需要在新进程中重新创建。此外,当通过调用 window.open 创建新的 WebPageProxy 时,所有现有进程都需要被通知已创建一个带有主帧的新 Page。

文档同步数据

Frame 的存在并非唯一需要向所有进程广播更新的事项。某些属性,例如 Frame 名称(可作为 window.open 的第二个参数来重用一个帧,而非创建新的 WebPageProxy),也需要在所有进程中尽可能保持最新。发送同步 IPC 进行更新会使同步性最佳,但浏览器会迅速变得无响应。相反,我们向每个进程广播更新,同时将 UI 进程作为事实的来源,该来源的状态要么与每个 Web 内容进程中的状态相同,要么 Web 内容进程即将收到带有更新的消息。这就是文档同步数据 (DocumentSyncData) 的作用。它是一组属性,我们努力在所有进程中尽可能保持其最新。

主帧和根帧

每个 Frame 都有一个主帧,它是该进程中帧树 (FrameTree) 顶部的 Frame。并非所有帧都将 LocalFrame 作为其主帧。有时,为了渲染层树事务等目的,我们需要位于其本地帧树顶部的 LocalFrame 集合。这些 LocalFrame 要么没有父级(在拥有主帧的进程中),要么有一个 RemoteFrame 作为父级。Page::rootFrames 是我们快速访问这些“根”帧的方式。请看这个深入的帧树 (FrameTree) 示例。主帧位于 example.com,它包含 2 个 iframe,一个在 example.com,一个在 a.com,并且每个 iframe 都有一个来自相对站点的孙子 iframe。

Frame Diagram 6

站点

站点隔离将来自不同站点 (Site) 的内容放入不同的进程中。站点 (Site) 是域名的协议和 eTLD+1 (可注册域 RegistrableDomain)。例如,http://example.com 和 https://example.com 将位于不同的进程中。https://www.example.com、https://blog.example.com 和 https://example.com 都将位于同一进程中。http://example.co.uk 和 http://blog.example.co.uk 也将位于同一进程中。

帧进程

如果 Web 内容在不同进程中有许多 iframe,并且主帧使用 JavaScript(例如 iframe.parentNode.removeChild(iframe))移除所有这些 iframe,我们希望停止使用所有现在未使用的进程。帧进程 (FrameProcess) 的存在是为了跟踪在站点隔离下哪些帧正在使用哪些进程。一旦 FrameProcess 被销毁,它所代表的 WebProcessProxy 将被终止或放入 WebProcessCache 中。FrameProcess 的销毁意味着 BrowsingContextGroup 不再跟踪 Web 内容进程,但由于 WebProcessProxy 可能会被 WebProcessCache 重用,因此 FrameProcess 和 WebProcessProxy 需要有独立的生命周期。

临时导航

在导航过程中,一个帧可以在短时间内同时在多个进程中拥有本地存在。当一个 iframe 从 a.com 导航到 b.com 时,它会创建一个 ProvisionalFrameProxy,该代理拥有一个针对 b.com 的帧进程 (FrameProcess)。在 b.com 进程内部,相应的 WebFrame 将创建一个 LocalFrame 并由 m_provisionalFrame 拥有,而不是将其放入帧树 (FrameTree) 中,直到从网络接收到响应。这是必需的,因为如果在 b.com 进程中调用 postMessage 向正在导航的帧发送消息,我们将希望使用 RemoteFrame 来传递该消息,而不是尚未在树中的 LocalFrame。此外,如果临时加载失败,例如在收到响应之前 TCP 连接丢失,我们可以丢弃临时帧并保持帧树不变。当成功接收到响应时,b.com 进程中的帧树 (FrameTree) 中的 RemoteFrame 将被 LocalFrame 替换,并且会向 a.com 进程发送一条消息,告知它将其 LocalFrame 替换为 RemoteFrame,因为该帧现在显示的是 b.com 的内容。当主帧导航时,我们使用 ProvisionalPageProxy 而不是 ProvisionalFrameProxy。

导航前: Frame Diagram 7 请求发送到响应接收之间: Frame Diagram 8 b.com 进程收到响应通知后: Frame Diagram 9 IPC 通知其他进程 b.com 进程已收到响应后: Frame Diagram 10

新的通信渠道

要从 WebCore::Page 获取其封装的 WebKit::WebPage,可以通过 ChromeClient。要从 WebCore::LocalFrame 获取其封装的 WebKit::WebFrame,可以通过 FrameLoaderClient。要从 WebCore::RemoteFrame 获取其封装的 WebKit::WebFrame,我们添加了 RemoteFrameClient。要向所有 Web 内容进程发送消息,请使用 WebPageProxy::forEachWebContentProcess。要向特定的 Web 内容进程发送消息,请使用 WebPageProxy::sendToProcessContainingFrame。

测试

我们目前约有 9 万个布局测试,它们具有相对良好的代码覆盖率,可以验证 WebKit 的每个功能是否按预期工作。我们向 WebKitTestRunner 中引入了一种特殊模式,通过向 run-webkit-tests 添加 --site-isolation 标志即可访问,用于重用这些测试以获取尽可能多的站点隔离相关覆盖率。使用此标志时,我们会先在一个关闭站点隔离的跨站点 iframe 中运行测试,收集测试发出的任何输出,然后在一个启用站点隔离的跨站点 iframe 中运行相同的测试,并比较输出。如果输出不同,通常表明我们尚未为站点隔离实现某些功能。某些测试在跨站点 iframe 中运行时无法完成,这些测试在此模式下没有帮助,但大多数都能完成并且有用。我们在 LayoutTests/platform/mac-site-isolation/TestExpectations 有一个测试预期文件,还有一个运行这些测试的机器人,您可以通过访问 results.webkit.org,然后点击顶部的“Suites”,再点击右下角的“Flavor”并开启 site-isolation 来查看。我们还有 API 测试,您可以使用“run-webkit-tests SiteIsolation -v —no-build”运行,以及一些专门用于测试 LayoutTests/http/tests/site-isolation 中站点隔离有趣情况的布局测试。

查找待完成事项

站点隔离最初被规划为一个简单的三步项目:1. 将来自每个站点 (Site) 的帧放入各自的进程中 2. 修复第 1 步导致的功能性问题 3. 修复第 2 步导致的性能回退。截至 2025 年 1 月,我们目前正处于第 2 步,并期待进入第 3 步。为此,我们需要修复 rdar://99665363 的所有子任务,这些任务是从代码中心视角组织的。QA 也一直从用户中心视角帮助发现需要修复的问题,这些问题是 rdar://138794978 的子任务。无法访问 radar 的人可以在 WebKit Slack 上联系。

修复 Bug 的策略

剩下的大多数功能性 Bug 可以描述为“我们过去可以跟踪指向另一个帧的指针,但现在不行了。”少数几种策略仍然非常有效。首先是,当我们需要执行某些操作时,或许我们可以重构代码,通过 IPC 向帧发送消息,而不是直接调用函数并操作帧。其次是,或许我们可以主动向所有进程发送状态,这样当它们需要执行某些操作时,它们已经拥有所需的信息。这只应在非敏感信息上进行,因为它会为推测执行攻击创建侧信道,从而读取其他站点不应访问的信息。第三是,或许我们可以在特权进程(例如 UI 进程或 GPU 进程)中执行某些操作,这些进程本身不包含 Web 内容,但可以与多个站点通信并了解其状态。第四是,如果一个帧位于另一个进程中,或许不采取任何操作是正确的。最后一个选项很少见,但有时如果访问受源检查限制,那么跳过另一个进程中的帧是正确的。