WebKit 网络拦截导致 SharedArrayBuffer 失效问题

604 阅读3分钟

在 iOS 15.2 WebKit 支持了 COOP/COEP 响应头,满足 Cross-Origin-Opener-Policy: same-origin / Cross-Origin-Embedder-Policy: require-corp 时则 SharedArrayBuffer 可用,参考官方文档

WASM 线程基于 Web Worker 实现,多线程数据共享若使用postMessage方案性能会非常差,为此它强依赖 SharedArrayBuffer 来实现高效数据共享。

然而在对 WebKit 进行网络拦截后,SharedArrayBuffer 竟然不可用了,需要挖一下原因。

提前放一下结论简图:

Pasted Graphic.png

正常流程分析

以 JS 层为入口

JS 报错信息是:can’t find variable: SharedArrayBuffer,说明构造函数都没有挂载上,标准函数window.crossOriginIsolated检查发现当前也不是跨域隔离环境。

而此时 COOP/COEP 响应头都满足了跨域隔离的要求,说明出问题的点在底层,而非 JS 代码维度能解决的。

看一下window.crossOriginIsolated的底层实现:

ExceptionOr<bool> DOMWindow::crossOriginIsolated() const {
    …
    return localThis->crossOriginIsolated();
}
bool LocalDOMWindow::crossOriginIsolated() const {
    …
    return ScriptExecutionContext::crossOriginMode() == CrossOriginMode::Isolated;
}

再看一下在JSGlobalObject::init中挂载 SharedArrayBuffer 构造函数的代码:

if (Options::useSharedArrayBuffer()) 
    putDirectWithoutTransition(vm, vm.propertyNames->SharedArrayBuffer, m_sharedArrayBufferStructure.constructor(this), static_cast<unsigned>(PropertyAttribute::DontEnum));

可见,核心落在了两个函数:

ScriptExecutionContext::crossOriginMode() 
Options::useSharedArrayBuffer()

Web 进程逻辑

在 Web 进程 IPC 入口处发现:

        if (xpc_dictionary_get_bool(initializerMessage, "enable-shared-array-buffer")) {
            JSC::Options::initialize();
            JSC::Options::AllowUnfinalizedAccessScope scope;
            JSC::Options::useSharedArrayBuffer() = true;
            optionsChanged = true;
        }

在 Web 进程initializeWebProcess函数发现:

ScriptExecutionContext::setCrossOriginMode(parameters.crossOriginMode);

并且这些值在 Web 进程期间是没有改变的,所以需要找到初始化 Web 进程的逻辑。

UI 进程逻辑

ProcessLauncher::finishLaunchingProcess函数发现:

       if (m_client->shouldEnableSharedArrayBuffer())
           xpc_dictionary_set_bool(bootstrapMessage.get(), "enable-shared-array-buffer", true);

经查m_client就是熟悉的 WebProcessProxy 类,其实现为:

bool shouldEnableSharedArrayBuffer() const final { 
	// m_crossOriginMode 默认是 Shared
	return m_crossOriginMode == WebCore::CrossOriginMode::Isolated; 
}

排查后发现只有一个链路可以让 WebProcess 支持CcrossOrignMode::Isolated

void WebPageProxy::triggerBrowsingContextGroupSwitchForNavigation(…) {
…
    if (browsingContextGroupSwitchDecision == BrowsingContextGroupSwitchDecision::NewIsolatedGroup)
        processForNavigation = m_legacyMainFrameProcess->protectedProcessPool()->createNewWebProcess(protectedWebsiteDataStore().ptr(), lockdownMode, WebProcessProxy::IsPrewarmed::No, CrossOriginMode::Isolated);
    else
        processForNavigation = m_legacyMainFrameProcess->protectedProcessPool()->processForRegistrableDomain(protectedWebsiteDataStore(), responseDomain, lockdownMode, protectedConfiguration());
…
}

满足browsingContextGroupSwitchDecision == BrowsingContextGroupSwitchDecision::NewIsolatedGroup条件,会新建支持CrossOriginMode::Isolated的 WebProcess 去打开页面。

Network 进程逻辑

最后找到核心逻辑:

static BrowsingContextGroupSwitchDecision toBrowsingContextGroupSwitchDecision(…) {
    …
    if (currentCoopEnforcementResult->crossOriginOpenerPolicy.value == CrossOriginOpenerPolicyValue::SameOriginPlusCOEP)
        return BrowsingContextGroupSwitchDecision::NewIsolatedGroup;
    return BrowsingContextGroupSwitchDecision::NewSharedGroup;
}

看到熟悉的 COOP/COEP 响应头判定:

CrossOriginOpenerPolicy obtainCrossOriginOpenerPolicy(const ResourceResponse& response)
…
    auto ensureCOEP = [&coep, &response]() -> CrossOriginEmbedderPolicy& {
        if (!coep)
		// 这是从响应header里面找数据
            coep = obtainCrossOriginEmbedderPolicy(response, nullptr);
        return *coep;
    };
…
        if (policyString->string() == "same-origin"_s) {
            auto& coep = ensureCOEP();
            if (coep.value == CrossOriginEmbedderPolicyValue::RequireCORP || (headerName == HTTPHeaderName::CrossOriginOpenerPolicyReportOnly && coep.reportOnlyValue == CrossOriginEmbedderPolicyValue::RequireCORP))
                value = CrossOriginOpenerPolicyValue::SameOriginPlusCOEP;
            else
                value = CrossOriginOpenerPolicyValue::SameOrigin;
        } else if (policyString->string() == "same-origin-allow-popups"_s)
            value = CrossOriginOpenerPolicyValue::SameOriginAllowPopups;

当满足条件时,会触发前面的 UI 进程逻辑,从而让 Web 进程支持跨域隔离环境。

前面的触发只会来自于 Network 进程的 NetworkResourceLoader 网络请求回包链路。

结论

而当 WebKit 网络拦截使用的 WKURLSchemeHandler 方案,网页的网络请求会直接 IPC 到 UI 进程实现,跨过 Network 进程的链路,所以无法触达前面的 COOP/COEP 判定链路。

也不能怪社区工程师没有去兼容 WKURLSchemeHandler 链路,因为原则上说这并不是拿给开发者去实现 HTTP 请求的。

得益于 WKURLSchemeHandler 设计是基于 WKWebViewConfiguration 实例的,各个 WKWebView 实例可做到互不干扰,所以想使用 Sharedarraybuffer 最简单方案就是使用纯净的 WKWebView。