Issue metadata
Sign in to add a comment
|
Security: GPU process BufferManager double-reads |
||||||||||||||||||||||
Issue description
This template is ONLY for reporting security bugs. If you are reporting a
Download Protection Bypass bug, please use the "Security - Download
Protection" template. For all other reports, please use a different
template.
Please see the following link for instructions on filing security bugs:
http://www.chromium.org/Home/chromium-security/reporting-security-bugs
VULNERABILITY DETAILS
The GPU buffer manager doesn't handle pointers to shared memory with adequate care, allowing an attacker to bypass chrome's validation and pass invalid buffer data to the hosting OpenGL implementation.
In some places in chrome, validation is performed on buffer data before allowing OpenGL calls which depend on that data; one example is the checking of vertex attribute data in VertexAttribManager::ValidateBindings called to sanity check vertex data before a DrawElements call.
The issue occurs in HandleBufferData (and HandleBufferSubData), where we see a pointer to shared memory passed as an argument to ValidateAndDoBufferData
error::Error GLES2DecoderImpl::HandleBufferData(uint32_t immediate_data_size,
const void* cmd_data) {
const gles2::cmds::BufferData& c =
*static_cast<const gles2::cmds::BufferData*>(cmd_data);
GLenum target = static_cast<GLenum>(c.target);
GLsizeiptr size = static_cast<GLsizeiptr>(c.size);
uint32_t data_shm_id = static_cast<uint32_t>(c.data_shm_id);
uint32_t data_shm_offset = static_cast<uint32_t>(c.data_shm_offset);
GLenum usage = static_cast<GLenum>(c.usage);
const void* data = NULL;
if (data_shm_id != 0 || data_shm_offset != 0) {
data = GetSharedMemoryAs<const void*>(data_shm_id, data_shm_offset, size);
if (!data) {
return error::kOutOfBounds;
}
}
buffer_manager()->ValidateAndDoBufferData(&state_, target, size, data, usage);
return error::kNoError;
}
void BufferManager::ValidateAndDoBufferData(
ContextState* context_state, GLenum target, GLsizeiptr size,
const GLvoid* data, GLenum usage) {
ErrorState* error_state = context_state->GetErrorState();
if (!feature_info_->validators()->buffer_target.IsValid(target)) {
ERRORSTATE_SET_GL_ERROR_INVALID_ENUM(
error_state, "glBufferData", target, "target");
return;
}
if (!feature_info_->validators()->buffer_usage.IsValid(usage)) {
ERRORSTATE_SET_GL_ERROR_INVALID_ENUM(
error_state, "glBufferData", usage, "usage");
return;
}
if (size < 0) {
ERRORSTATE_SET_GL_ERROR(
error_state, GL_INVALID_VALUE, "glBufferData", "size < 0");
return;
}
Buffer* buffer = GetBufferInfoForTarget(context_state, target);
if (!buffer) {
ERRORSTATE_SET_GL_ERROR(
error_state, GL_INVALID_VALUE, "glBufferData", "unknown buffer");
return;
}
if (!memory_type_tracker_->EnsureGPUMemoryAvailable(size)) {
ERRORSTATE_SET_GL_ERROR(
error_state, GL_OUT_OF_MEMORY, "glBufferData", "out of memory");
return;
}
DoBufferData(error_state, buffer, target, size, usage, data);
}
Which finally ends up in DoBufferData, which calls the relevant OpenGL call (glBufferData) to set the data in GPU memory, and then SetInfo, using the same pointer, which is still pointing to shared memory. If an attacker changes the data pointed to by this pointer in between these calls, they can arrange for one set of data to be sent to the GPU, and a different set of data to be stored in chrome's buffer shadow.
void BufferManager::DoBufferData(
ErrorState* error_state,
Buffer* buffer,
GLenum target,
GLsizeiptr size,
GLenum usage,
const GLvoid* data) {
// Clear the buffer to 0 if no initial data was passed in.
scoped_ptr<int8_t[]> zero;
if (!data) {
zero.reset(new int8_t[size]);
memset(zero.get(), 0, size);
data = zero.get();
}
ERRORSTATE_COPY_REAL_GL_ERRORS_TO_WRAPPER(error_state, "glBufferData");
if (IsUsageClientSideArray(usage)) {
GLsizei empty_size = UseNonZeroSizeForClientSideArrayBuffer() ? 1 : 0;
glBufferData(target, empty_size, NULL, usage);
} else {
glBufferData(target, size, data, usage);
}
GLenum error = ERRORSTATE_PEEK_GL_ERROR(error_state, "glBufferData");
if (error == GL_NO_ERROR) {
SetInfo(buffer, target, size, usage, data);
} else {
SetInfo(buffer, target, 0, usage, NULL);
}
}
void BufferManager::SetInfo(Buffer* buffer, GLenum target, GLsizeiptr size,
GLenum usage, const GLvoid* data) {
DCHECK(buffer);
memory_type_tracker_->TrackMemFree(buffer->size());
const bool is_client_side_array = IsUsageClientSideArray(usage);
const bool support_fixed_attribs =
gfx::GetGLImplementation() == gfx::kGLImplementationEGLGLES2;
// TODO(zmo): Don't shadow buffer data on ES3. crbug.com/491002.
const bool shadow = target == GL_ELEMENT_ARRAY_BUFFER ||
allow_buffers_on_multiple_targets_ ||
(allow_fixed_attribs_ && !support_fixed_attribs) ||
is_client_side_array;
buffer->SetInfo(size, usage, shadow, data, is_client_side_array);
memory_type_tracker_->TrackMemAlloc(buffer->size());
}
void Buffer::SetInfo(
GLsizeiptr size, GLenum usage, bool shadow, const GLvoid* data,
bool is_client_side_array) {
usage_ = usage;
is_client_side_array_ = is_client_side_array;
ClearCache();
if (size != size_ || shadow != shadowed_) {
shadowed_ = shadow;
size_ = size;
if (shadowed_) {
shadow_.reset(new int8_t[size]);
} else {
shadow_.reset();
}
}
if (shadowed_) {
if (data) {
memcpy(shadow_.get(), data, size); // <-- data can have changed by here
} else {
memset(shadow_.get(), 0, size);
}
}
mapped_range_.reset(nullptr);
}
If we look at how the validation of vertex buffers is performed inside DrawElements;
error::Error GLES2DecoderImpl::DoDrawElements(const char* function_name,
bool instanced,
GLenum mode,
GLsizei count,
GLenum type,
int32_t offset,
GLsizei primcount) {
error::Error error = WillAccessBoundFramebufferForDraw();
if (error != error::kNoError)
return error;
if (!state_.vertex_attrib_manager->element_array_buffer()) {
LOCAL_SET_GL_ERROR(
GL_INVALID_OPERATION, function_name, "No element array buffer bound");
return error::kNoError;
}
if (count < 0) {
LOCAL_SET_GL_ERROR(GL_INVALID_VALUE, function_name, "count < 0");
return error::kNoError;
}
if (offset < 0) {
LOCAL_SET_GL_ERROR(GL_INVALID_VALUE, function_name, "offset < 0");
return error::kNoError;
}
if (!validators_->draw_mode.IsValid(mode)) {
LOCAL_SET_GL_ERROR_INVALID_ENUM(function_name, mode, "mode");
return error::kNoError;
}
if (!validators_->index_type.IsValid(type)) {
LOCAL_SET_GL_ERROR_INVALID_ENUM(function_name, type, "type");
return error::kNoError;
}
if (primcount < 0) {
LOCAL_SET_GL_ERROR(GL_INVALID_VALUE, function_name, "primcount < 0");
return error::kNoError;
}
if (!CheckBoundDrawFramebufferValid(true, function_name)) {
return error::kNoError;
}
if (count == 0 || primcount == 0) {
return error::kNoError;
}
GLuint max_vertex_accessed;
Buffer* element_array_buffer =
state_.vertex_attrib_manager->element_array_buffer();
if (!element_array_buffer->GetMaxValueForRange(
offset, count, type, &max_vertex_accessed)) {
LOCAL_SET_GL_ERROR(
GL_INVALID_OPERATION, function_name, "range out of bounds for buffer");
return error::kNoError;
}
if (IsDrawValid(function_name, max_vertex_accessed, instanced, primcount)) {
if (!ClearUnclearedTextures()) {
LOCAL_SET_GL_ERROR(GL_INVALID_VALUE, function_name, "out of memory");
return error::kNoError;
}
bool simulated_attrib_0 = false;
if (!SimulateAttrib0(
function_name, max_vertex_accessed, &simulated_attrib_0)) {
return error::kNoError;
}
bool simulated_fixed_attribs = false;
if (SimulateFixedAttribs(
function_name, max_vertex_accessed, &simulated_fixed_attribs,
primcount)) {
bool textures_set = !PrepareTexturesForRender();
ApplyDirtyState();
// TODO(gman): Refactor to hide these details in BufferManager or
// VertexAttribManager.
const GLvoid* indices = reinterpret_cast<const GLvoid*>(offset);
bool used_client_side_array = false;
if (element_array_buffer->IsClientSideArray()) {
used_client_side_array = true;
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
indices = element_array_buffer->GetRange(offset, 0);
}
if (!instanced) {
glDrawElements(mode, count, type, indices);
} else {
glDrawElementsInstancedANGLE(mode, count, type, indices, primcount);
}
When using an index buffer for vertex indices, we use GetMaxValueForRange to compute the maximum index that will be referenced by the vertex shader into the array buffer; this will perform validation based on the data stored in the shadow buffer, instead of the data that the shader will actually use; meaning that completely invalid indices can be passed to OpenGL.
I haven't attempted to find an OpenGL implementation which does something dangerous with this input; but it seems likely to be exploitable under the case where use_client_side_arrays_for_stream_buffers is set, or in any case where the native OpenGL implementation does not bounds check indices from the index buffer. (I only have access to an x86_64 linux box this week, and nothing bad appears to happen there with bad indexes)
I haven't attached a PoC, since I haven't tested one to have obvious side-effects to reproduce; if you want to try reproducing this on different OpenGL implementations I can provide one.
This bug is subject to a 90 day disclosure deadline. If 90 days elapse
without a broadly available patch, then the bug report will automatically
become visible to the public.
VERSION
Chrome Version: latest trunk
Operating System: [Please indicate OS, version, and service pack level]
REPRODUCTION CASE
Please include a demonstration of the security bug, such as an attached
HTML or binary file that reproduces the bug when loaded in Chrome. PLEASE
make the file as small as possible and remove any content not required to
demonstrate the bug.
FOR CRASHES, PLEASE INCLUDE THE FOLLOWING ADDITIONAL INFORMATION
Type of crash: [tab, browser, etc.]
Crash State: [see link above: stack trace, registers, exception record]
Client ID (if relevant): [see link above]
,
Mar 31 2016
This is a similar problem to Issue 597625 . To avoid the race, any shadow copy needs to be made first, and the upload has to occur from that shadow copy when a shadow copy is in fact made. It ought to be possible to restructure the code to optimistically make the shadow copy, and clear things out in the unlikely event that the glBufferData call generates an error (which would generally only be a GL_OUT_OF_MEMORY error). It also ought to be possible to refactor the BufferManager APIs so it can decide when to use a pointer into the shadow copy vs. the original data when calling glBufferData. Victor, would it be possible for you to find someone else to help implement this?
,
Mar 31 2016
,
Mar 31 2016
,
Mar 31 2016
David, I'd be grateful if you could look into this.
,
Mar 31 2016
,
Mar 31 2016
,
Apr 1 2016
Sorry for the late reply; was OOO. This issue has been there for a while, definitely out there in stable releases.
,
Apr 1 2016
The following revision refers to this bug: https://chromium.googlesource.com/chromium/src.git/+/f936e84eb05867b3c3311da561e05332a97362a3 commit f936e84eb05867b3c3311da561e05332a97362a3 Author: dyen <dyen@chromium.org> Date: Fri Apr 01 20:57:21 2016 Make sure we call glBufferData on the same data we store internally. The BufferManager now shadows the glBufferData data before we pass it to GL so that a user cannot modify the data while we are processing it. R=kbr@chromium.org, vmiura@chromium.org BUG= 599081 CQ_INCLUDE_TRYBOTS=tryserver.chromium.win:win_optional_gpu_tests_rel;tryserver.chromium.mac:mac_optional_gpu_tests_rel Review URL: https://codereview.chromium.org/1845963003 Cr-Commit-Position: refs/heads/master@{#384688} [modify] https://crrev.com/f936e84eb05867b3c3311da561e05332a97362a3/gpu/command_buffer/service/buffer_manager.cc [modify] https://crrev.com/f936e84eb05867b3c3311da561e05332a97362a3/gpu/command_buffer/service/buffer_manager.h
,
Apr 1 2016
This problem certainly exists in Chrome Stable but I'm doubtful it's really Security_Severity-High and warrants merging back to earlier releases. The main side-effect would be either crashing Chrome's GPU sub-process or causing bogus vertices to be fetched for individual triangles somewhere in the compositor, WebGL, etc. It's difficult to see how this bug could be exploited to read information with any reliability.
,
Apr 7 2016
Unless someone has a good reason to merge the fix to stable, I'll mark this as fixed.
,
Apr 8 2016
Adding Merge-Triage label for tracking purposes. Once your fix had sufficient bake time (on canary, dev as appropriate), please nominate your fix for merge by adding the Merge-Requested label. When your merge is approved by the release manager, please start merging with higher milestone label first. Make sure to re-request merge for every milestone in the label list. You can get branch information on omahaproxy.appspot.com. Your fix is very close to the branch point. After the branch happens, please make sure to check if your fix is in. - Your friendly ClusterFuzz
,
Apr 14 2016
,
May 9 2016
This will ship with M51 based on commit position. Updating labels.
,
Jul 15 2016
This bug has been closed for more than 14 weeks. Removing security view restrictions. For more details visit https://www.chromium.org/issue-tracking/autotriage - Your friendly Sheriffbot
,
Oct 1 2016
This bug has been closed for more than 14 weeks. Removing security view restrictions. For more details visit https://www.chromium.org/issue-tracking/autotriage - Your friendly Sheriffbot
,
Oct 2 2016
This bug has been closed for more than 14 weeks. Removing security view restrictions. For more details visit https://www.chromium.org/issue-tracking/autotriage - Your friendly Sheriffbot
,
Oct 2 2016
|
|||||||||||||||||||||||
►
Sign in to add a comment |
|||||||||||||||||||||||
Comment 1 by penny...@chromium.org
, Mar 31 2016Components: Internals>GPU
Labels: Security_Severity-Medium OS-All Pri-1
Owner: kbr@chromium.org