背景
最近项目有需求需要在前端使用ffmpeg
,于是我把ffmpeg
编译好的wasm
版本直接引入到项目中进行使用,本以为会比较顺利,结果一运行,直接爆红:
ShardArrayBuffer介绍
从上面的爆红信息中可以直白地看出浏览器提示我们ShardArrayBuffer
没有定义,那么ShardArrayBuffer
是什么?
SharedArrayBuffer
对象用来表示一个通用的、固定长度的原始二进制数据缓冲区,类似于ArrayBuffer
对象,它们都可以用来在共享内存(shared memory)上创建视图。与ArrayBuffer
不同的是,SharedArrayBuffer
不能被转移。
上面给出的是MDN上的介绍,读起来比较晦涩,要了解ShardArrayBuffer
,首先要理解ArrayBuffer
是什么。
ArrayBuffer
ArrayBuffer
是一个无法被直接操作的字节数组,只能通过类型化数组对象或 DataView
对象来操作。
那么为什么要使用ArrayBuffer
?
我们都知道,JavaScript
是一门单线程语言,这意味着我们无法把复杂的计算放在页面上执行,为了弥补这一缺点,子线程Web Worker
应运而生,Web Worker
独立于我们负责页面逻辑的主线程,不会影响页面的性能,但这也导致了一个问题,在Web Worke
r中,我们无法获取当前页面的信息,于是便有了postmessage
。
postmessage
可以接受任何对象,对其进行序列化,然后在各个线程之间进行通信,在反序列化后放入内存,解决了线程之间的通信问题,又一个问题出现了,那就是通信的性能问题。
我们注意到,每次通信都需要对数据进行序列化和反序列化,这是非常低效的一个过程,例如数组、字符串这些数据类型,需要先复制他们,将它们转化为字节数组,然后才能进行传输,传输之后还要进行反序列化,转化为原来的数据结构。
这里就引出了另外一个点,ArrayBuffer
是一个可转移对象 ,这意味着我们可以将ArrayBuffer从一个线程之间传输到另外一个线程,而无需复制和序列化,使用ArrayBuffer
能很大程度上提高线程通信的效率,但这样也有一个问题,就是,传输过后,发送ArrayBuffer
的线程将无法再次访问它,这对于想要拥有高性能并行性的项目来说是不能接受的。
于是,SharedArrayBuffer
诞生了。
ShardArrayBuffer
从名字上就能看出,这是ArrayBuffer
的升级版,事实也的确如此,ShardArrayBuffer
最大的优点就是它是一个真正的共享内存,允许多个进程(WebWorker
)对其进行读写,不再具有通信开销和延迟,这让JS真正可以模拟Java、C等语言所拥有的多线程高性能。
好,现在回到文章开头,为什么会出现ShardArrayBuffer is not defined
这个错误呢?
百度一下,得到的解决方案是为响应头设置:
makefile
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
我加上这两个请求头之后,报错果然消失了,ffmpeg
也成功运行了!但原理还没搞明白,不禁心里痒痒,决定对其一探究竟。
幽灵漏洞(Spectre)
现代CPU为了优化运行效率,采取了分支预测
的优化方式,当在运行到if这样的判断时,CPU通常会进行分支预测,而预测的结果受多方影响,比如在本次判断之前的十次判断结果都为正确,那么CPU会先预测这次的结果也为正确,然后去执行之后的代码逻辑,并将结果进行缓存,当判断结果吻合预测结果时,直接从CPU缓存中取出数据存入,如果判断结果不吻合,则进行回滚,但是缓存中的数据不会删除!
但是分支预测和缓存机制导致了一个很严重的CPU漏洞,也就是大名鼎鼎的幽灵漏洞。理解幽灵漏洞需要对计算机和CPU的底层原理有一定的理解,这里将通俗地介绍一下什么是幽灵漏洞。
先看一段代码:
js
if(x < A.length) {
y = instrument[A[x]]
}
在这段代码中,我们试图去访问instrument
数组中索引为A[x]
的值,我们先令x小于A的长度,重复运行这段代码,经过重复运行后,CPU对这个判断的分支预测趋于'正确',此时我们令x大于A的长度,基于分支预测的策略,CPU会先将instrument[A[x]]
的值读取并缓存,然后当判断结束后再进行回滚,此时,问题出现了,instrument[A[x]]
的值依然保留在缓存之中!而A[X]
这个值是非法的,正常来说,我们不应该能拿到这个值,因为它已经超过了A数组的长度,也就是非法越界,但是通过幽灵漏洞,我们可以拿到这个值。
(这里推荐观看b站视频【【计算机】15分钟读懂英特尔熔断幽灵漏洞-Emory】www.bilibili.com/video/BV1eW...)
接下来就是幽灵漏洞的关键,在这个漏洞中,还涉及到了另外一个经典的CPU漏洞,即旁信道攻击,这里不做赘述,你只需要知道,我们读取内存之中的值和读取缓存之中的值所需要的时间是不一样的,也就是说,如果我们能够获得精确的时间,只需要遍历instrument
数组,然后判断读取每个值所花的时间,其中instrument[A[x]]
由于已经在缓存之中,读取它所花费的时间是远低于其他值的,这样我们就可以拿到A[x]
的值了。
为什么我要突然提到幽灵漏洞呢?聪明的你一定注意到了,幽灵漏洞中有一个很关键的地方,那就是高精度的时间。
在幽灵漏洞提出后,浏览器厂商已经降低了类似performance.now()
这样的计时精度来阻止恶意代码获取高精度CPU时间,但通过SharedArrayBuffer
依然可以制作一个高精度的计时器,基本原理就是在一个woker线程不停的递增SharedArrayBuffer
的值,另外一个线程在任意计算代码的前后分别读取这个SharedArrayBuffer
的值,取差值来作为时间的增量。浏览器没有办法去判断这种行为,所以只能先禁用SharedArrayBuffer
。
解决方案
在当前环境下前端需要高性能计算的场景越来越多,长久地禁用显然是不现实的,于是为了解决这一问题,Web标准提出了一种解决方案,那就是引入两个新的跨域策略:COOP/COEP,也就是上面提到的在html响应头上设置:
js
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
其中,COEP禁止加载没有任何显示设置的CORP/CORS的跨域资源,COOP则是限制当前文档打开的跨域请求,设置为'same-origin'
,则表示对打开的窗口执行隔离,同时禁止两个窗口相互通信。
这种解决方案也就是将页面放入了一个封闭的隔离沙箱,所有的跨域请求都需要由拥有资源的服务器进行明确审查,如果未审查,则数据永远不会进入攻击者的浏览上下文,也就可以规避掉攻击者的幽灵漏洞攻击,此时,浏览器可以认为发出请求的站点不太危险,于是允许使用SharedArrayBuffer等高风险API。
本地调试
我们在本地运行Vite
项目时,在proxy
中设置响应头即可,不过这个设置只有使用localhost有效,如果使用network地址实际上是无效的,因为Vite的代理只是在本地进行了处理,对于外部的访问是无法处理的。
生产环境
在生产环境中。则需要在nginx
进行配置,同样也是在响应头中进行设置。
跨域问题
经过我们上面的设置后,又带来了一个新的问题,所有的跨域资源都无法使用,包括存储在OSS中的图片与视频,这是不能接受的,对此,Web标准给出了两种解决方案:CORP和CORS。
CORP
对于我们需要访问的远程跨域资源,为其设置跨域资源策略响应头:Cross-Origin-Resource-Policy: cross-origin
,也就是允许跨域访问该资源。
CORS
这个大家应该很熟悉了,针对<audio>、<img>、<link>、<script>、<video
>标签,我们也可以采用设置crossorigin
属性,如果资源支持CORS,也可以访问(资源的响应头必须设置Access-Control-Allow-Origin
,本地开发时该值只能为*
)
最后的问题
经过漫长的调试,就当我以为终于可以同时使用SharedArrayBuffer
又解决了跨域问题时,突然意识到背景图片无法设置crossorigin
!,那我们使用CORP
来解决?存储背景图片的OSS目前还无法配置CORP
策略!
最后有一个曲线救国的办法,为OSS配置CDN,在CDN上配置请求头来解决跨域问题。
如果你没有CDN,那只能妥协一下,把所有的背景图片换成<img>
标签吧!
参考资料
- ffmpeg.wasm处理视频 - 掘金 (juejin.cn)
- 15分钟读懂英特尔熔断幽灵漏洞-Emory - 知乎 (zhihu.com)
- SharedArrayBuffer - JavaScript | MDN (mozilla.org)
- ArrayBuffer - JavaScript | MDN (mozilla.org)
- 前端必须要懂的ArrayBuffers和SharedArrayBuffers - 知乎 (zhihu.com)
- 由一个报错引发的浏览器跨域隔离探索-CSDN博客
(如有侵权,请联系作者删除)