New issue
Advanced search Search tips
Starred by 1 user
Status: Fixed
Owner:
Closed: Jun 2016
Cc:



Sign in to add a comment
Chrome - GPU process BufferManager double-reads
Project Member Reported by markbrand@google.com, Mar 30 2016 Back to list
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.

 
Project Member Comment 1 by scvitti@google.com, Mar 31 2016
Labels: -Reported-30-Mar-2016 Reported-2016-Mar-30
Project Member Comment 2 by markbrand@google.com, Jun 15 2016
Labels: -Restrict-View-Commit Fixed-2016-May-25
Status: Fixed
Derestricting as Chrome team confirmed fix was released in stable 51.0.2704.* releases. (http://googlechromereleases.blogspot.ch/2016/05/stable-channel-update_25.html)
Sign in to add a comment