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