Source: https://bugs.chromium.org/p/project-zero/issues/detail?id=780

Several functions in the GPU command buffer service interact with the GPU
mailbox manager (gpu/command_buffer/service/mailbox_manager_impl.cc), passing a reference to shared memory as the mailbox argument.

MailboxManagerImpl does not expect this mailbox argument to be malleable in this way, and it is in several places copied and passed to various stl functions, resulting in unexpected behaviour from double-reads when an attacker modifies the mailbox name mid function.

The attached POC uses the GPU command 'ProduceTextureDirectCHROMIUMImmediate' to trigger a use-after-free but other commands that interact with the mailbox manager should also be vulnerable in a similar way.

error::Error GLES2DecoderImpl::HandleProduceTextureDirectCHROMIUMImmediate(
    uint32_t immediate_data_size,
    const void* cmd_data) {
  const gles2::cmds::ProduceTextureDirectCHROMIUMImmediate& c =
      *static_cast<const gles2::cmds::ProduceTextureDirectCHROMIUMImmediate*>(
          cmd_data);
  (void)c;
  GLuint texture = c.texture;
  GLenum target = static_cast<GLenum>(c.target);
  uint32_t data_size;
  if (!ComputeDataSize(1, sizeof(GLbyte), 64, &data_size)) {
    return error::kOutOfBounds;
  }
  if (data_size > immediate_data_size) {
    return error::kOutOfBounds;
  }

  // ** mailbox is a pointer into our shared memory buffer **
  const GLbyte* mailbox =
      GetImmediateDataAs<const GLbyte*>(c, data_size, immediate_data_size);
  if (!validators_->texture_bind_target.IsValid(target)) {
    LOCAL_SET_GL_ERROR_INVALID_ENUM("glProduceTextureDirectCHROMIUM", target,
                                    "target");
    return error::kNoError;
  }
  if (mailbox == NULL) {
    return error::kOutOfBounds;
  }
  DoProduceTextureDirectCHROMIUM(texture, target, mailbox);
  return error::kNoError;
}

void GLES2DecoderImpl::DoProduceTextureDirectCHROMIUM(GLuint client_id,
    GLenum target, const GLbyte* data) {
  TRACE_EVENT2("gpu", "GLES2DecoderImpl::DoProduceTextureDirectCHROMIUM",
      "context", logger_.GetLogPrefix(),
      "mailbox[0]", static_cast<unsigned char>(data[0]));

  ProduceTextureRef("glProduceTextureDirectCHROMIUM", GetTexture(client_id),
      target, data);
}

void GLES2DecoderImpl::ProduceTextureRef(const char* func_name,
                                         TextureRef* texture_ref,
                                         GLenum target,
                                         const GLbyte* data) {
  // ** mailbox is still a pointer to shared memory **
  const Mailbox& mailbox = *reinterpret_cast<const Mailbox*>(data);
  DLOG_IF(ERROR, !mailbox.Verify()) << func_name << " was passed a "
                                       "mailbox that was not generated by "
                                       "GenMailboxCHROMIUM.";

  if (!texture_ref) {
    LOCAL_SET_GL_ERROR(
        GL_INVALID_OPERATION, func_name, "unknown texture for target");
    return;
  }

  Texture* produced = texture_manager()->Produce(texture_ref);
  if (!produced) {
    LOCAL_SET_GL_ERROR(
        GL_INVALID_OPERATION, func_name, "invalid texture");
    return;
  }

  if (produced->target() != target) {
    LOCAL_SET_GL_ERROR(
        GL_INVALID_OPERATION, func_name, "invalid target");
    return;
  }

  group_->mailbox_manager()->ProduceTexture(mailbox, produced);
}

void MailboxManagerImpl::ProduceTexture(const Mailbox& mailbox,
                                        Texture* texture) {
  MailboxToTextureMap::iterator it = mailbox_to_textures_.find(mailbox);
  if (it != mailbox_to_textures_.end()) {
    if (it->second->first == texture)
      return;
    TextureToMailboxMap::iterator texture_it = it->second;
    mailbox_to_textures_.erase(it);
    textures_to_mailboxes_.erase(texture_it);
  }
  InsertTexture(mailbox, texture);
}

void MailboxManagerImpl::InsertTexture(const Mailbox& mailbox,
                                       Texture* texture) {
  texture->SetMailboxManager(this);
  TextureToMailboxMap::iterator texture_it =
      textures_to_mailboxes_.insert(std::make_pair(texture, mailbox));
  // ** modify mailbox at this point **
  mailbox_to_textures_.insert(std::make_pair(mailbox, texture_it));
  DCHECK_EQ(mailbox_to_textures_.size(), textures_to_mailboxes_.size());
}

We can leverage this for memory corruption in the GPU process by a slightly roundabout route; since each entry in the mailbox_to_textures_ has an iterator to an entry in textures_to_mailboxes_, and it is an expected invariant that both of these entries have the same mailbox name. By starting to create a mailbox named aaaa...aaaa and waiting until the first insert is complete before changing the mailbox name to AAAA...aaaa, then adding a second entry with the same texture reference but with the actual name aaaa...aaaa we get the following situation (example trace through the code from ProduceTexture with InsertTexture call inlined):

// first entry - mailbox.name = 'aaaa...aaaa', texture = t1
MailboxToTextureMap::iterator it = mailbox_to_textures_.find(mailbox);
// if body not executed
if (it != mailbox_to_textures_.end()) {}
texture->SetMailboxManager(this);
TextureToMailboxMap::iterator texture_it =
  textures_to_mailboxes_.insert(std::make_pair(texture, mailbox));
// modify      - mailbox.name = 'AAAA...aaaa'
mailbox_to_textures_.insert(std::make_pair(mailbox, texture_it));
DCHECK_EQ(mailbox_to_textures_.size(), textures_to_mailboxes_.size());

// mailbox_to_textures_                            textures_to_mailboxes_
// AAAA...aaaa, it ------------------------------> aaaa...aaaa, t1

// second entry - mailbox.name = 'aaaa...aaaa', texture = t1
MailboxToTextureMap::iterator it = mailbox_to_textures_.find(mailbox);
// if body not executed (since 'AAAA...aaaa' != 'aaaa...aaaa')
if (it != mailbox_to_textures_.end()) {}
texture->SetMailboxManager(this);
TextureToMailboxMap::iterator texture_it =
  textures_to_mailboxes_.insert(std::make_pair(texture, mailbox));
// texture_it is the same as before; since there already exists the pair
// 'aaaa...aaaa', t1* in textures_to_mailboxes, the insert returns an
// iterator to the existing element.
mailbox_to_textures_.insert(std::make_pair(mailbox, texture_it));
DCHECK_EQ(mailbox_to_textures_.size(), textures_to_mailboxes_.size());

// mailbox_to_textures_                            textures_to_mailboxes_
// AAAA...aaaa, it ------------------------------> aaaa...aaaa, t1
// aaaa...aaaa, it -----------/
// so both entries in mailbox_to_textures_ have the same iterator.

// third entry - mailbox.name = 'aaaa...aaaa', texture = t2
MailboxToTextureMap::iterator it = mailbox_to_textures_.find(mailbox);
// this time we find an entry
if (it != mailbox_to_textures_.end()) {
  // t1 != t2
  if (it->second->first == texture)
    return;
  // so we remove the previous entry for aaaa...aaaa
  TextureToMailboxMap::iterator texture_it = it->second;
  mailbox_to_textures_.erase(it);
  textures_to_mailboxes_.erase(texture_it);
}

// mailbox_to_textures_                            textures_to_mailboxes_
// AAAA...aaaa, it ------------------------------> ??
// aaaa...aaaa, it ------------------------------> aaaa...aaaa, t2

Leaving mailbox AAAA...aaaa with a dangling reference to a no-longer valid iterator into textures_to_mailboxes_; (std::map and std::multimap iterators are invalidated by a call to erase() the element that they reference). The attached poc then calls some further methods on the mailbox to trigger a use of the dangling iterator that is detectable by ASAN.

Updated POC to work on the latest asan linux build, command buffer ids were out of date.


Proof of Concept:
https://gitlab.com/exploit-database/exploitdb-bin-sploits/-/raw/main/bin-sploits/39961.zip